Explorar o código

Merge branch 'sit' into uat-20260202

dujian hai 1 mes
pai
achega
44088f713f

+ 13 - 9
alien-dining/src/main/java/shop/alien/dining/service/impl/DiningUserServiceImpl.java

@@ -93,8 +93,8 @@ public class DiningUserServiceImpl implements DiningUserService {
             return null;
         }
 
-        // 5. 生成并存储 token
-        String token = generateAndStoreToken(openid, user);
+        // 5. 生成并存储 token(传入本次解析的手机号,确保有手机号时一定写入 miniprogram_user_{phone})
+        String token = generateAndStoreToken(openid, user, parsedPhone);
 
         // 6. 构建返回对象,优先使用解析后的手机号返回给前端
         return buildDiningUserVo(user, token, openid, parsedPhone);
@@ -283,19 +283,23 @@ public class DiningUserServiceImpl implements DiningUserService {
 
     /**
      * 生成并存储 token
+     * @param openid 微信 openid
+     * @param user 用户实体(可能尚未持久化手机号)
+     * @param currentLoginPhone 本次登录解析到的手机号(可选),有则优先用其写入 miniprogram_user_{phone},避免只存 openid 不存 phone 的情况
      */
-    private String generateAndStoreToken(String openid, LifeUser user) {
+    private String generateAndStoreToken(String openid, LifeUser user, String currentLoginPhone) {
         // 构建 token 信息
         Map<String, String> tokenMap = buildTokenMap(openid, user);
         String userName = StringUtils.isNotBlank(user.getUserName()) ? user.getUserName() : "用户";
         String token = generateToken(openid, userName, tokenMap);
 
-        // 存入 Redis(使用 openid 作为 key)
+        // 存入 Redis(使用 openid 作为 key,始终写入
         baseRedisService.setString(REDIS_KEY_TOKEN_PREFIX + openid, token, TOKEN_EXPIRE_SECONDS);
-        
-        // 兼容旧版本:如果用户有手机号,也存储
-        if (StringUtils.isNotBlank(user.getUserPhone())) {
-            baseRedisService.setString(REDIS_KEY_USER_PHONE_PREFIX + user.getUserPhone(), token, TOKEN_EXPIRE_SECONDS);
+
+        // 兼容旧版本:有手机号则同时按手机号存一份(优先用本次登录解析到的手机号,否则用用户实体中的手机号)
+        String phoneToStore = StringUtils.isNotBlank(currentLoginPhone) ? currentLoginPhone : user.getUserPhone();
+        if (StringUtils.isNotBlank(phoneToStore)) {
+            baseRedisService.setString(REDIS_KEY_USER_PHONE_PREFIX + phoneToStore, token, TOKEN_EXPIRE_SECONDS);
         }
 
         return token;
@@ -310,7 +314,7 @@ public class DiningUserServiceImpl implements DiningUserService {
         tokenMap.put("phone", StringUtils.isNotBlank(user.getUserPhone()) ? user.getUserPhone() : "");
         tokenMap.put("userName", StringUtils.isNotBlank(user.getUserName()) ? user.getUserName() : "用户");
         tokenMap.put("userId", user.getId().toString());
-        tokenMap.put("userType", "user");
+        tokenMap.put("userType", "miniprogram_user");
         return tokenMap;
     }
 

+ 2 - 0
alien-dining/src/main/resources/bootstrap-test.yml

@@ -9,6 +9,7 @@ spring:
         server-addr: 120.26.186.130:8848
         username: dev
         password: Alien123456
+        namespace: acd615de-5b62-4d92-996b-740183cd7f15
 
       #配置中心
       config:
@@ -19,6 +20,7 @@ spring:
         password: Alien123456
         group: DEFAULT_GROUP
         file-extension: yml
+        namespace: acd615de-5b62-4d92-996b-740183cd7f15
         shared-configs:
           - data-id: common.yml
             group: DEFAULT_GROUP

+ 4 - 0
alien-dining/src/main/resources/bootstrap-uat.yml

@@ -12,6 +12,9 @@ spring:
         server-addr: 192.168.2.251:8848
         username: nacos
         password: Alien123456
+        namespace: a3f3cdfc-8324-4a37-b01b-33ff1194e153
+        metadata:
+          env: uat
 
       #配置中心
       config:
@@ -22,6 +25,7 @@ spring:
         password: Alien123456
         group: DEFAULT_GROUP
         file-extension: yml
+        namespace: a3f3cdfc-8324-4a37-b01b-33ff1194e153
         shared-configs:
           - data-id: common.yml
             group: DEFAULT_GROUP

+ 17 - 22
alien-dining/src/main/resources/logback-spring.xml

@@ -40,38 +40,30 @@
     </appender>
 
     <!--2. 输出到文档-->
-    <!-- DEBUG 日志 -->
+    <!-- DEBUG 日志:按 1MB 大小滚动,满 1MB 自动切到下一个文件 -->
     <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
-        <!-- 当前的日志文件存放路径 -->
         <file>${logging.path}/DEBUG.log</file>
-        <!-- 日志滚动策略 -->
-        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 历史日志文件的存放路径和名称 -->
-            <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_DEBUG.log.gz</fileNamePattern>
-            <!-- 日志文件最大的保存历史 数量-->
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_DEBUG.%i.log.gz</fileNamePattern>
+            <maxFileSize>1MB</maxFileSize>
             <maxHistory>${log.maxHistory}</maxHistory>
         </rollingPolicy>
         <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
-            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
             <pattern>${FILE_LOG_PATTERN}</pattern>
         </encoder>
-        <!--日志文件最大的大小-->
-        <!--        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">-->
-        <!--            <MaxFileSize>10MB</MaxFileSize>-->
-        <!--        </triggeringPolicy>-->
-        <!-- 此日志文档只记录debug级别的 -->
         <filter class="ch.qos.logback.classic.filter.LevelFilter">
             <level>DEBUG</level>
-            <onMatch>ACCEPT</onMatch>  <!-- 用过滤器,只接受DEBUG级别的日志信息,其余全部过滤掉 -->
+            <onMatch>ACCEPT</onMatch>
             <onMismatch>DENY</onMismatch>
         </filter>
     </appender>
 
-    <!-- INFO 日志 -->
+    <!-- INFO 日志:按 1MB 大小滚动 -->
     <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
         <file>${logging.path}/INFO.log</file>
-        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_INFO.log.gz</fileNamePattern>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_INFO.%i.log.gz</fileNamePattern>
+            <maxFileSize>1MB</maxFileSize>
             <maxHistory>${log.maxHistory}</maxHistory>
         </rollingPolicy>
         <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
@@ -84,11 +76,12 @@
         </filter>
     </appender>
 
-    <!-- WARN 日志 -->
+    <!-- WARN 日志:按 1MB 大小滚动 -->
     <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
         <file>${logging.path}/WARN.log</file>
-        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_WARN.log.gz</fileNamePattern>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_WARN.%i.log.gz</fileNamePattern>
+            <maxFileSize>1MB</maxFileSize>
             <maxHistory>${log.maxHistory}</maxHistory>
         </rollingPolicy>
         <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
@@ -101,10 +94,12 @@
         </filter>
     </appender>
 
+    <!-- ERROR 日志:按 1MB 大小滚动 -->
     <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
         <file>${logging.path}/ERROR.log</file>
-        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_ERROR.log.gz</fileNamePattern>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_ERROR.%i.log.gz</fileNamePattern>
+            <maxFileSize>1MB</maxFileSize>
             <maxHistory>${log.maxHistory}</maxHistory>
         </rollingPolicy>
         <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">

+ 40 - 15
alien-gateway/src/main/java/shop/alien/gateway/config/JwtTokenFilter.java

@@ -113,16 +113,35 @@ public class JwtTokenFilter implements GlobalFilter, Ordered {
                 JSONObject tokenData = JSONObject.parseObject(claims.get("sub").toString());
                 String deviceType = tokenData.getString("userType");
                 String phone = tokenData.getString("phone");
+                String openid = tokenData.getString("openid");
                 String redisKey;
+                String redisVal = null;
                 //区分
                 if ("web".equals(deviceType)) {
                     //管理端单设备登录
                     //不限制
                     return allowChain(exchange, chain);
+                } else if ("user".equals(deviceType) || "miniprogram_user".equals(deviceType)) {
+                    // 用户/小程序:兼容 openid(点餐小程序存 miniprogram_user_token:{openid})与手机号(miniprogram_user_{phone} / user_{phone})
+                    List<String> candidateKeys = new ArrayList<>();
+                    if (StringUtils.isNotBlank(openid)) {
+                        candidateKeys.add("miniprogram_user_token:" + openid);
+                    }
+                    if (StringUtils.isNotBlank(phone)) {
+                        candidateKeys.add("miniprogram_user_" + phone);
+                        candidateKeys.add("user_" + phone);
+                    }
+                    for (String key : candidateKeys) {
+                        String val = baseRedisService.getString(key);
+                        if (StringUtils.isNotBlank(val) && token.equals(val)) {
+                            redisVal = val;
+                            break;
+                        }
+                    }
                 } else {
                     redisKey = deviceType + "_" + phone;
+                    redisVal = baseRedisService.getString(redisKey);
                 }
-                String redisVal = baseRedisService.getString(redisKey);
                 if (StringUtils.isEmpty(redisVal) || !token.equals(redisVal)) {
                     if ("store".equals(deviceType) || "storePlatform".equals(deviceType)) {
                         //判断程序是否为用户禁用
@@ -132,37 +151,43 @@ public class JwtTokenFilter implements GlobalFilter, Ordered {
                             //别问, 问就是约定俗成
                             map.put("code", 777);
                         }
-                        if (!redisVal.equals(token)) {
+                        if (!token.equals(redisVal)) {
                             map.put("msg", "账号在别处登录");
                             //别问, 问就是约定俗成
                             map.put("code", 666);
                         }
                     } else if ("user".equals(deviceType) || "miniprogram_user".equals(deviceType)) {
-                        //判断程序是否为用户禁用
-                        LifeUser lifeUser = lifeUserMapper.selectOne(new LambdaQueryWrapper<LifeUser>().eq(LifeUser::getUserPhone, phone));
-                        //注销标记, 0:未注销, 1:已注销
-                        if (null != lifeUser.getLogoutFlag() && lifeUser.getLogoutFlag() == 1) {
-                            map.put("msg", "你的账号已注销");
-                            //别问, 问就是约定俗成
-                            map.put("code", 777);
+                        //判断程序是否为用户禁用/注销(支持按 userId 查,兼容无手机号的小程序用户)
+                        LifeUser lifeUser = null;
+                        if (StringUtils.isNotBlank(phone)) {
+                            lifeUser = lifeUserMapper.selectOne(new LambdaQueryWrapper<LifeUser>().eq(LifeUser::getUserPhone, phone));
+                        }
+                        if (lifeUser == null && tokenData.containsKey("userId") && StringUtils.isNotBlank(tokenData.getString("userId"))) {
+                            try {
+                                lifeUser = lifeUserMapper.selectById(Integer.parseInt(tokenData.getString("userId")));
+                            } catch (NumberFormatException ignored) { }
+                        }
+                        if (lifeUser != null) {
+                            //注销标记, 0:未注销, 1:已注销
+                            if (null != lifeUser.getLogoutFlag() && lifeUser.getLogoutFlag() == 1) {
+                                map.put("msg", "你的账号已注销");
+                                map.put("code", 777);
+                            }
                         }
-                        if (!redisVal.equals(token)) {
+                        if (!token.equals(redisVal)) {
                             map.put("msg", "账号在别处登录");
-                            //别问, 问就是约定俗成
                             map.put("code", 666);
                         }
                     } else if ("lawyer".equals(deviceType)) {
                         //判断程序是否为用户禁用
                         LawyerUser lawyerUser = lawyerUserMapper.selectOne(new LambdaQueryWrapper<LawyerUser>().eq(LawyerUser::getPhone, phone));
                         //注销标记, 0:未注销, 1:已注销
-                        if (null != lawyerUser.getLogoutFlag() && lawyerUser.getLogoutFlag() == 1) {
+                        if (lawyerUser != null && null != lawyerUser.getLogoutFlag() && lawyerUser.getLogoutFlag() == 1) {
                             map.put("msg", "你的账号已注销");
-                            //别问, 问就是约定俗成
                             map.put("code", 777);
                         }
-                        if (!redisVal.equals(token)) {
+                        if (!token.equals(redisVal)) {
                             map.put("msg", "账号在别处登录");
-                            //别问, 问就是约定俗成
                             map.put("code", 666);
                         }
                     }

+ 194 - 0
alien-job/src/main/java/shop/alien/job/second/AiProductComplaintJob.java

@@ -0,0 +1,194 @@
+package shop.alien.job.second;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.xxl.job.core.handler.annotation.XxlJob;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestTemplate;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeNotice;
+import shop.alien.entity.store.LifeUser;
+import shop.alien.entity.store.LifeUserViolation;
+import shop.alien.mapper.LifeNoticeMapper;
+import shop.alien.mapper.LifeUserMapper;
+import shop.alien.mapper.LifeUserViolationMapper;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 商品举报AI审核结果查询定时任务
+ * <p>
+ * 定时轮询待处理的商品举报任务,调用AI接口 POST /ai/auto-review/api/v1/product_complaint_record/result 查询审核结果,
+ * 并更新本地状态、发送通知。
+ * </p>
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class AiProductComplaintJob {
+
+    private final RestTemplate restTemplate;
+    private final LifeUserViolationMapper lifeUserViolationMapper;
+    private final LifeUserMapper lifeUserMapper;
+    private final LifeNoticeMapper lifeNoticeMapper;
+
+    @Value("${third-party-user-name.base-url}")
+    private String userName;
+
+    @Value("${third-party-pass-word.base-url}")
+    private String passWord;
+
+    @Value("${third-party-login.base-url}")
+    private String loginUrl;
+
+    /**
+     * 商品举报审核结果查询接口地址,例如: http://xxx/ai/auto-review/api/v1/product_complaint_record/result
+     */
+    @Value("${third-party-aiProductComplaintResultUrl.base-url}")
+    private String aiProductComplaintResultUrl;
+
+    @XxlJob("getAiProductComplaintResult")
+    public R<String> getAiProductComplaintResult() {
+        String accessToken = fetchAiServiceToken();
+        if (!StringUtils.hasText(accessToken)) {
+            return R.fail("调用AI商品举报审核系统登录接口失败");
+        }
+        return getAiProductComplaintCheck(accessToken);
+    }
+
+    private String fetchAiServiceToken() {
+        log.info("登录Ai服务获取token...{}", loginUrl);
+        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
+        formData.add("username", userName);
+        formData.add("password", passWord);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+        HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(formData, headers);
+
+        try {
+            ResponseEntity<String> response = restTemplate.postForEntity(loginUrl, requestEntity, String.class);
+            if (response != null && response.getStatusCodeValue() == 200 && response.getBody() != null) {
+                JSONObject jsonObject = JSONObject.parseObject(response.getBody());
+                JSONObject dataJson = jsonObject.getJSONObject("data");
+                return dataJson != null ? dataJson.getString("access_token") : null;
+            }
+            log.error("请求AI商品举报审核系统登录接口失败 http状态:{}", response != null ? response.getStatusCode() : null);
+        } catch (Exception e) {
+            log.error("调用AI商品举报审核系统登录接口异常", e);
+        }
+        return null;
+    }
+
+    private R<String> getAiProductComplaintCheck(String accessToken) {
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        headers.set("Authorization", "Bearer " + accessToken);
+
+        // 查询所有状态为处理中的商品举报(report_context_type=4 表示二手商品举报)
+        List<LifeUserViolation> pendingTasks = lifeUserViolationMapper.selectList(
+                new QueryWrapper<LifeUserViolation>()
+                        .eq("report_context_type", "4")
+                        .eq("processing_status", "0")
+                        .isNotNull("ai_task_id")
+                        .ne("ai_task_id", "")
+        );
+
+        for (LifeUserViolation task : pendingTasks) {
+            Map<String, String> body = new HashMap<>();
+            body.put("task_id", task.getAiTaskId());
+
+            HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(body, headers);
+
+            try {
+                ResponseEntity<String> response = restTemplate.postForEntity(aiProductComplaintResultUrl, requestEntity, String.class);
+
+                if (response == null || response.getStatusCodeValue() != 200 || response.getBody() == null) {
+                    if (response != null) {
+                        log.error("调用AI商品举报审核结果接口失败, http状态: {}", response.getStatusCode());
+                    }
+                    continue;
+                }
+
+                String responseBody = response.getBody();
+                log.info("AI商品举报审核结果接口返回: {}", responseBody);
+
+                JSONObject json = JSONObject.parseObject(responseBody);
+                JSONObject dataJson = json.getJSONObject("data");
+
+                if (dataJson == null) {
+                    log.error("AI商品举报审核返回数据为空, task_id={}", task.getAiTaskId());
+                    continue;
+                }
+
+                String status = dataJson.getString("status");
+                if ("pending".equals(status)) {
+                    log.debug("任务尚未完成, task_id={}", task.getAiTaskId());
+                    continue;
+                }
+
+                if (!"completed".equals(status)) {
+                    log.warn("未知状态 status={}, task_id={}", status, task.getAiTaskId());
+                    continue;
+                }
+
+                LifeUserViolation update = new LifeUserViolation();
+                String isValid = dataJson.getString("is_valid");
+                String decisionReason = dataJson.getString("decision_reason");
+                if ("true".equals(isValid)) {
+                    update.setProcessingStatus("1");
+                    update.setReportResult(decisionReason);
+                } else {
+                    update.setProcessingStatus("2");
+                    update.setReportResult(decisionReason);
+                }
+                update.setProcessingTime(new Date());
+
+                QueryWrapper<LifeUserViolation> queryWrapper = new QueryWrapper<>();
+                queryWrapper.eq("ai_task_id", task.getAiTaskId());
+                LifeUserViolation existing = lifeUserViolationMapper.selectOne(queryWrapper);
+                if (existing == null) {
+                    log.error("AI商品举报记录不存在, ai_task_id={}", task.getAiTaskId());
+                    continue;
+                }
+                lifeUserViolationMapper.update(update, queryWrapper);
+
+                // 发送通知
+                LifeUser lifeUser = lifeUserMapper.selectById(task.getReportingUserId());
+                if (lifeUser != null) {
+                    LifeNotice lifeMessage = new LifeNotice();
+                    lifeMessage.setReceiverId("user_" + lifeUser.getUserPhone());
+                    String text = "您的举报商品结果为" + ("1".equals(update.getProcessingStatus()) ? "违规" : "未违规");
+                    com.alibaba.fastjson.JSONObject lifeMessageJson = new com.alibaba.fastjson.JSONObject();
+                    lifeMessageJson.put("title", "平台已受理");
+                    lifeMessageJson.put("message", text);
+                    lifeMessage.setContext(lifeMessageJson.toJSONString());
+                    lifeMessage.setTitle("举报通知");
+                    lifeMessage.setBusinessId(task.getId());
+                    lifeMessage.setSenderId("system");
+                    lifeMessage.setIsRead(0);
+                    lifeMessage.setNoticeType(1);
+                    lifeNoticeMapper.insert(lifeMessage);
+                }
+
+            } catch (Exception e) {
+                log.error("调用AI商品举报审核结果接口异常, task_id={}", task.getAiTaskId(), e);
+            }
+        }
+
+        return R.success("调用AI商品举报审核结果接口完成");
+    }
+}

+ 23 - 10
alien-second/src/main/java/shop/alien/second/service/impl/SecondGoodsReportingServiceImpl.java

@@ -197,16 +197,16 @@ public class SecondGoodsReportingServiceImpl implements SecondGoodsReportingServ
     @Override
     public int reporting(SecondUserViolationVo lifeuserViolation) throws Exception  {
         try {
-            if ("4".equals(lifeuserViolation.getReportContextType())) {
-                lifeuserViolation.setGoodsId(lifeuserViolation.getBusinessId());
-                LambdaQueryWrapper<SecondGoodsRecord> goodsWrapper = new LambdaQueryWrapper<>();
-                goodsWrapper.eq(SecondGoodsRecord::getGoodsId, lifeuserViolation.getBusinessId());
-                goodsWrapper.eq(SecondGoodsRecord::getGoodsStatus, "3");
-                goodsWrapper.orderByDesc(SecondGoodsRecord::getCreatedTime);
-                goodsWrapper.last(" limit 1 ");
-                SecondGoodsRecord goodsRecord = secondGoodsRecordMapper.selectOne(goodsWrapper);
-                if (null != goodsRecord) lifeuserViolation.setBusinessId(goodsRecord.getId());
-            }
+//            if ("4".equals(lifeuserViolation.getReportContextType())) {
+//                lifeuserViolation.setGoodsId(lifeuserViolation.getBusinessId());
+//                LambdaQueryWrapper<SecondGoodsRecord> goodsWrapper = new LambdaQueryWrapper<>();
+//                goodsWrapper.eq(SecondGoodsRecord::getGoodsId, lifeuserViolation.getBusinessId());
+//                goodsWrapper.eq(SecondGoodsRecord::getGoodsStatus, "3");
+//                goodsWrapper.orderByDesc(SecondGoodsRecord::getCreatedTime);
+//                goodsWrapper.last(" limit 1 ");
+//                SecondGoodsRecord goodsRecord = secondGoodsRecordMapper.selectOne(goodsWrapper);
+//                if (null != goodsRecord) lifeuserViolation.setBusinessId(goodsRecord.getId());
+//            }
             int result = lifeUserViolationMapper.insert(lifeuserViolation);
             if (result > 0) {
 
@@ -223,6 +223,19 @@ public class SecondGoodsReportingServiceImpl implements SecondGoodsReportingServ
                     lifeuserViolation.setAiTaskId(taskId);
                     lifeUserViolationMapper.updateById(lifeuserViolation);
                 }
+                if ("4".equals(lifeuserViolation.getReportContextType())) {
+                    // AI审核
+                    //登录获取token
+                    String token = aiUserViolationUtils.getAccessToken();
+                    //调用AI接口
+                    String taskId = aiUserViolationUtils.createGoodsTask(token, lifeuserViolation);
+                    if (org.springframework.util.StringUtils.isEmpty(taskId)) {
+                        log.warn("Failed to create AI task for second round review, lifeuserViolation id={}", lifeuserViolation.getId());
+                        return 0;
+                    }
+                    lifeuserViolation.setAiTaskId(taskId);
+                    lifeUserViolationMapper.updateById(lifeuserViolation);
+                }
 
                 if (lifeuserViolation.getReportContextType().equals("4") || lifeuserViolation.getReportContextType().equals("5")) {
                     String phoneId = Objects.requireNonNull(JwtUtil.getCurrentUserInfo()).getString("userType") + "_" + JwtUtil.getCurrentUserInfo().getString("phone");

+ 143 - 0
alien-second/src/main/java/shop/alien/second/util/AiUserViolationUtils.java

@@ -14,11 +14,14 @@ import org.springframework.util.MultiValueMap;
 import org.springframework.util.StringUtils;
 import org.springframework.web.client.RestTemplate;
 import shop.alien.entity.result.R;
+import shop.alien.entity.second.SecondGoods;
+import shop.alien.entity.second.vo.SecondUserViolationVo;
 import shop.alien.entity.store.LifeMessage;
 import shop.alien.entity.store.LifeUserViolation;
 import shop.alien.entity.store.vo.LifeUserVo;
 import shop.alien.mapper.LifeMessageMapper;
 import shop.alien.mapper.LifeUserMapper;
+import shop.alien.mapper.second.SecondGoodsMapper;
 
 import java.text.SimpleDateFormat;
 import java.util.Date;
@@ -34,6 +37,7 @@ public class AiUserViolationUtils {
     private final RestTemplate restTemplate;
     private final LifeUserMapper lifeUserMapper;
     private final LifeMessageMapper lifeMessageMapper;
+    private final SecondGoodsMapper secondGoodsMapper;
 
     @Value("${third-party-login.base-url}")
     private String loginUrl;
@@ -47,6 +51,8 @@ public class AiUserViolationUtils {
     @Value("${third-party-userComplaintRecordUrl.base-url}")
     private String userComplaintRecordUrl;
 
+    @Value("${third-party-goodsComplaintRecordUrl.base-url:${third-party-userComplaintRecordUrl.base-url}}")
+    private String goodsComplaintRecordUrl;
 
     /**
      * 登录 AI 服务,获取 token
@@ -210,6 +216,143 @@ public class AiUserViolationUtils {
     }
 
     /**
+     * 调用AI服务创建商品举报审核任务(参数格式:complaint_id、complaint_type、reporter/reported 信息、product 信息、complaint_text、evidence_images)
+     *
+     * @param accessToken 访问令牌
+     * @param lifeuserViolation 举报信息(商品举报时可为 SecondUserViolationVo,含 imgUrl)
+     * @return task_id 任务ID,失败返回 null
+     */
+    public String createGoodsTask(String accessToken, SecondUserViolationVo lifeuserViolation) {
+        log.info("创建Ai服务商品举报任务...{}", goodsComplaintRecordUrl);
+
+        HttpHeaders analyzeHeaders = new HttpHeaders();
+        analyzeHeaders.setContentType(MediaType.APPLICATION_JSON);
+        analyzeHeaders.set("Authorization", "Bearer " + accessToken);
+
+        Map<String, Object> analyzeRequest = new HashedMap<>();
+        // complaint_id
+        analyzeRequest.put("complaint_id", lifeuserViolation.getId().toString());
+
+        // complaint_type:举报商品类型 dictId 1-8(见图表)
+        Integer dictId = lifeuserViolation.getDictId() != null ? lifeuserViolation.getDictId() : 8;
+        if (1 == dictId) {
+            analyzeRequest.put("complaint_type", "禁限售商品");
+        } else if (2 == dictId) {
+            analyzeRequest.put("complaint_type", "政治敏感类");
+        } else if (3 == dictId) {
+            analyzeRequest.put("complaint_type", "有害信息");
+        } else if (4 == dictId) {
+            analyzeRequest.put("complaint_type", "假货/盗版");
+        } else if (5 == dictId) {
+            analyzeRequest.put("complaint_type", "盗用他人图片");
+        } else if (6 == dictId) {
+            analyzeRequest.put("complaint_type", "危害用户产权");
+        } else if (7 == dictId) {
+            analyzeRequest.put("complaint_type", "危害国家与社会安全");
+        } else {
+            analyzeRequest.put("complaint_type", "其他");
+        }
+
+        String reporterUserId = lifeuserViolation.getReportingUserId() != null ? lifeuserViolation.getReportingUserId() : "";
+        LifeUserVo reporterUser = null;
+        if (StringUtils.hasText(reporterUserId)) {
+            QueryWrapper<LifeUserVo> reporterQ = new QueryWrapper<>();
+            reporterQ.eq("id", reporterUserId);
+            reporterUser = lifeUserMapper.getUserById(reporterQ);
+        }
+        // 被举报人:通过 businessId 查 second_goods_record(id=businessId) 得到 userId,再查 life_user
+        String reportedUserId = lifeuserViolation.getReportedUserId() != null ? lifeuserViolation.getReportedUserId() : "";
+        LifeUserVo reportedUser = null;
+        Integer businessId = lifeuserViolation.getBusinessId();
+        SecondGoods secondGoods = secondGoodsMapper.selectById(businessId);
+        if (secondGoods != null && secondGoods.getUserId() != null) {
+            reportedUserId = String.valueOf(secondGoods.getUserId());
+            QueryWrapper<LifeUserVo> reportedQ = new QueryWrapper<>();
+            reportedQ.eq("id", secondGoods.getUserId());
+            reportedUser = lifeUserMapper.getUserById(reportedQ);
+        }
+
+        // 用user_去拼接,查询life_message表,获取双方两天记录
+        analyzeRequest.put("reporter_user_id", "user_" + reporterUser.getUserPhone());
+        analyzeRequest.put("reporter_user_type", "buyer");
+        analyzeRequest.put("reported_user_id", "user_" + reportedUser.getUserPhone());
+        analyzeRequest.put("reported_user_type", "seller");
+
+        // reporter_info
+        Map<String, Object> reporterInfo = new HashedMap<>();
+        QueryWrapper<LifeUserVo> reporterQ = new QueryWrapper<>();
+        reporterQ.eq("id", reporterUserId);
+        reporterInfo.put("name", reporterUser != null && StringUtils.hasText(reporterUser.getUserName()) ? reporterUser.getUserName() : "");
+        reporterInfo.put("phone", reporterUser != null && StringUtils.hasText(reporterUser.getUserPhone()) ? maskPhone(reporterUser.getUserPhone()) : "");
+        analyzeRequest.put("reporter_info", reporterInfo);
+
+        // reported_info(二手无店铺,用被举报人姓名/电话填充或填空)
+        Map<String, Object> reportedInfo = new HashedMap<>();
+        reportedInfo.put("shop_name", reportedUser != null && StringUtils.hasText(reportedUser.getUserName()) ? reportedUser.getUserName() : "");
+        reportedInfo.put("shop_id", reportedUserId);
+        analyzeRequest.put("reported_info", reportedInfo);
+
+        // product_id / product_name / product_info
+        analyzeRequest.put("product_id", secondGoods.getId().toString());
+        analyzeRequest.put("product_name", secondGoods.getTitle());
+        Map<String, Object> productInfo = new HashedMap<>();
+        productInfo.put("price", secondGoods.getPrice());
+        productInfo.put("category", secondGoods.getLabel());
+        productInfo.put("sku", secondGoods.getLabel());
+        analyzeRequest.put("product_info", productInfo);
+
+        analyzeRequest.put("complaint_text", StringUtils.hasText(lifeuserViolation.getOtherReasonContent()) ? lifeuserViolation.getOtherReasonContent() : "");
+
+        analyzeRequest.put("evidence_images", lifeuserViolation.getImgUrl());
+
+        HttpEntity<Map<String, Object>> analyzeEntity = new HttpEntity<>(analyzeRequest, analyzeHeaders);
+
+        ResponseEntity<String> analyzeResp = null;
+        try {
+            analyzeResp = restTemplate.postForEntity(goodsComplaintRecordUrl, analyzeEntity, String.class);
+        } catch (org.springframework.web.client.HttpServerErrorException.ServiceUnavailable e) {
+            log.error("调用提交商品举报审核任务接口返回503 Service Unavailable错误: {}", e.getResponseBodyAsString());
+            return null;
+        } catch (Exception e) {
+            log.error("调用提交商品举报审核任务接口异常", e);
+            return null;
+        }
+
+        if (analyzeResp != null && analyzeResp.getStatusCodeValue() == 200) {
+            String analyzeBody = analyzeResp.getBody();
+            log.info("提交商品举报审核任务成功, 返回: {}", analyzeBody);
+
+            JSONObject analyzeJson = JSONObject.parseObject(analyzeBody);
+            JSONObject dataJsonObj = analyzeJson.getJSONObject("data");
+
+            if (dataJsonObj == null) {
+                log.error("提交商品举报审核任务返回数据为空");
+                return null;
+            }
+
+            String taskId = dataJsonObj.getString("task_id");
+            if (taskId == null) {
+                log.error("提交商品举报审核任务返回task_id为空");
+                return null;
+            }
+            return taskId;
+        } else {
+            if (analyzeResp != null) {
+                log.error("调用提交商品举报审核任务接口失败, http状态: {}", analyzeResp.getStatusCode());
+                return null;
+            }
+        }
+        return null;
+    }
+
+    private static String maskPhone(String phone) {
+        if (phone == null || phone.length() < 8) {
+            return phone != null ? phone : "";
+        }
+        return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
+    }
+
+    /**
      * 获取双方用户的最近聊天记录
      * @param userReporterUserById 举报人ID
      * @param userReportedUserById 被举报人ID

+ 1 - 1
alien-store/src/main/java/shop/alien/store/service/impl/OperationalActivityServiceImpl.java

@@ -159,7 +159,7 @@ public class OperationalActivityServiceImpl implements OperationalActivityServic
             wrapper.in(StoreOperationalActivity::getStoreId, storeIds);
         }
         wrapper.eq(StoreOperationalActivity::getDeleteFlag, 0);
-        wrapper.orderByDesc(StoreOperationalActivity::getSignupStartTime);
+        wrapper.orderByDesc(StoreOperationalActivity::getCreatedTime);
 
         IPage<StoreOperationalActivity> entityPage = activityMapper.selectPage(new Page<>(current, size), wrapper);