Sfoglia il codice sorgente

feat(job): 实现门店打卡内容AI审核功能

- 新增门店打卡内容合规性检查逻辑
- 调用AI服务对图片、视频及文本内容进行审核
- 支持审核任务提交与结果轮询获取
- 完善异常处理与日志记录机制
- 优化REST请求认证流程,复用访问令牌
- 增加对审核不通过内容的自动下架处理
Lhaibo 2 settimane fa
parent
commit
4f99bdfe86

+ 12 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreClockIn.java

@@ -83,6 +83,18 @@ public class StoreClockIn extends Model<StoreClockIn> {
     @TableField(value = "updated_user_id", fill = FieldFill.INSERT_UPDATE)
     private Integer updatedUserId;
 
+    @ApiModelProperty(value = "是否审核(未审核:0,审核中:1,审核完成:2)")
+    @TableField("check_flag")
+    private Integer checkFlag;
+
+    @ApiModelProperty(value = "AI审核结果查询id")
+    @TableField("ai_task_id")
+    private String aiTaskId;
+
+    @ApiModelProperty(value = "审核失败原因")
+    @TableField("reason")
+    private String reason;
+
 
     @Override
     protected Serializable pkVal() {

+ 249 - 134
alien-job/src/main/java/shop/alien/job/store/AiTagJob.java

@@ -46,6 +46,8 @@ public class AiTagJob {
 
     private final LifeUserDynamicsMapper lifeUserDynamicsMapper;
 
+    private final StoreClockInMapper storeClockInMapper;
+
     // 第三方接口地址 获取所有标签主表信息
     @Value("${third-party-tag.base-url}")
     private String tagMainUrl;
@@ -589,9 +591,39 @@ public class AiTagJob {
                 "mp4", "mov", "mkv", "avi", "flv", "wmv", "mpeg", "mpg", "webm"
         ));
 
-        for (LifeUserDynamics lifeUserDynamic : lifeUserDynamics) {
-            String imagePath = lifeUserDynamic.getImagePath();
+        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);
+        ResponseEntity<String> postForEntity = null;
+        try {
+            log.info("请求Ai服务登录接口===================>");
+            postForEntity = restTemplate.postForEntity(loginUrl, requestEntity, String.class);
+        } catch (Exception e) {
+            log.error("请求AI服务登录接口失败", e);
+        }
+        HttpHeaders aiHeaders = new HttpHeaders();
+        if (postForEntity != null && postForEntity.getStatusCodeValue() == 200) {
+            log.info("请求Ai服务登录成功 postForEntity.getBody()\t" + postForEntity.getBody());
+            String responseBody = postForEntity.getBody();
+            JSONObject jsonObject = JSONObject.parseObject(responseBody);
+            if (jsonObject != null) {
+                JSONObject dataJson = jsonObject.getJSONObject("data");
+                String accessToken = dataJson.getString("access_token");
 
+                aiHeaders.setContentType(MediaType.APPLICATION_JSON);
+                aiHeaders.set("Authorization", "Bearer " + accessToken);
+            }
+        }
+
+        List<StoreClockIn> storeClockIns = storeClockInMapper.selectList(new LambdaQueryWrapper<StoreClockIn>()
+                .eq(StoreClockIn::getDeleteFlag, 0).eq(StoreClockIn::getCheckFlag, 0));
+        for (StoreClockIn storeClockIn : storeClockIns) {
+            String imagePath = storeClockIn.getImgUrl() != null ? storeClockIn.getImgUrl() : "";
             List<String> imageList = new ArrayList<>();
             List<String> videoList = new ArrayList<>();
             // 按分隔符拆分路径数组(处理连续分隔符如 ",," 产生的空字符串)
@@ -613,79 +645,117 @@ public class AiTagJob {
                 } else if (VIDEO_SUFFIXES.contains(suffix)) {
                     videoList.add(trimmedPath);
                 }
-                // 对每一条动态逐条申请 token 并提交,避免批量失败导致重试逻辑复杂化
+            }
+            // 对每一条动态逐条申请 token 并提交,避免批量失败导致重试逻辑复杂化
+            try {
+                Map<String, Object> jsonBody = new HashMap<>();
+                // text/img/video 三种维度一起传入 AI,便于一次完成合规审查
+                jsonBody.put("text", storeClockIn.getContent());
+                jsonBody.put("image_urls", imageList);
+                jsonBody.put("video_urls", videoList);
+
+                HttpEntity<Map<String, Object>> request = new HttpEntity<>(jsonBody, aiHeaders);
+                ResponseEntity<String> response = null;
                 try {
-                    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);
-                    ResponseEntity<String> postForEntity = null;
-                    try {
-                        log.info("请求Ai服务登录接口===================>");
-                        postForEntity = restTemplate.postForEntity(loginUrl, requestEntity, String.class);
-                    } catch (Exception e) {
-                        log.error("请求AI服务登录接口失败", e);
+                    response = restTemplate.postForEntity(contentComplianceUrl, request, String.class);
+                    if (response.getStatusCodeValue() != 200) {
+                        log.error("AI内容审核接口调用失败 http状态:" + response.getStatusCode());
+                    }
+                    JSONObject responseNode = JSONObject.parseObject(response.getBody());
+                    if (responseNode == null) {
+                        log.error("AI接口调用失败,响应内容为空");
+                    }
+                    Integer code = null;
+                    if (responseNode != null) {
+                        code = responseNode.getInteger("code");
+                        if (code == 200) {
+                            JSONObject dataNode = JSONObject.from(responseNode.get("data"));
+                            // 审核发起后仅标记 checkFlag=1 并保存任务号,等待回调或后续轮询更新
+                            StoreClockIn clockIn = new StoreClockIn();
+                            clockIn.setId(storeClockIn.getId());
+                            clockIn.setCheckFlag(1);
+                            clockIn.setAiTaskId(dataNode.get("task_id").toString());
+                            storeClockInMapper.updateById(clockIn);
+                            log.info("动态审核成功,AI返回内容: {}", response.getBody());
+                            XxlJobHelper.handleSuccess("动态内容审核任务执行成功");
+                        }
+                    } else {
+                        log.error("AI接口调用失败,错误码: " + code);
                     }
+                } catch (Exception e) {
+                    log.error("调用AI内容审核接口失败", e);
+                }
+            } catch (RuntimeException ex) {
+                XxlJobHelper.handleFail("动态内容审核任务执行失败:" + ex.getMessage());
+                return R.fail("动态内容审核任务执行失败:" + ex.getMessage());
+            }
+        }
 
-                    if (postForEntity != null && postForEntity.getStatusCodeValue() == 200) {
-                        log.info("请求Ai服务登录成功 postForEntity.getBody()\t" + postForEntity.getBody());
-                        String responseBody = postForEntity.getBody();
-                        JSONObject jsonObject = JSONObject.parseObject(responseBody);
-                        if (jsonObject != null) {
-                            JSONObject dataJson = jsonObject.getJSONObject("data");
-                            String accessToken = dataJson.getString("access_token");
-
-                            HttpHeaders aiHeaders = new HttpHeaders();
-                            aiHeaders.setContentType(MediaType.APPLICATION_JSON);
-                            aiHeaders.set("Authorization", "Bearer " + accessToken);
-
-                            Map<String, Object> jsonBody = new HashMap<>();
-                            // text/img/video 三种维度一起传入 AI,便于一次完成合规审查
-                            jsonBody.put("text", lifeUserDynamic.getContext());
-                            jsonBody.put("image_urls", imageList);
-                            jsonBody.put("video_urls", videoList);
-
-                            HttpEntity<Map<String, Object>> request = new HttpEntity<>(jsonBody, aiHeaders);
-                            ResponseEntity<String> response = null;
-                            try {
-                                response = restTemplate.postForEntity(contentComplianceUrl, request, String.class);
-                                if (response.getStatusCodeValue() != 200) {
-                                    log.error("AI内容审核接口调用失败 http状态:" + response.getStatusCode());
-                                }
-                                JSONObject responseNode = JSONObject.parseObject(response.getBody());
-                                if (responseNode == null) {
-                                    log.error("AI接口调用失败,响应内容为空");
-                                }
-                                Integer code = null;
-                                if (responseNode != null) {
-                                    code = responseNode.getInteger("code");
-                                    if(code==200) {
-                                        JSONObject dataNode = JSONObject.from(responseNode.get("data"));
-                                        // 审核发起后仅标记 checkFlag=1 并保存任务号,等待回调或后续轮询更新
-                                        LifeUserDynamics dynamics = new LifeUserDynamics();
-                                        dynamics.setId(lifeUserDynamic.getId());
-                                        dynamics.setCheckFlag(1);
-                                        dynamics.setAiTaskId(dataNode.get("task_id").toString());
-                                        lifeUserDynamicsMapper.updateById(dynamics);
-                                        log.info("动态审核成功,AI返回内容: {}", response.getBody());
-                                        XxlJobHelper.handleSuccess("动态内容审核任务执行成功");
-                                    }
-                                }else {
-                                    log.error("AI接口调用失败,错误码: " + code);
-                                }
-                            } catch (Exception e) {
-                                log.error("调用AI内容审核接口失败", e);
-                            }
+        for (LifeUserDynamics lifeUserDynamic : lifeUserDynamics) {
+            String imagePath = lifeUserDynamic.getImagePath();
+            List<String> imageList = new ArrayList<>();
+            List<String> videoList = new ArrayList<>();
+            // 按分隔符拆分路径数组(处理连续分隔符如 ",," 产生的空字符串)
+            String[] allPaths = imagePath.split(",");
+            for (String path : allPaths) {
+                // 去除路径前后空格(避免 " a.jpg " 这类情况)
+                String trimmedPath = path.trim();
+                if (trimmedPath.isEmpty()) {
+                    continue; // 跳过空路径
+                }
+                // 获取文件后缀(忽略大小写)
+                // 找到最后一个 "." 的位置
+                int lastDotIndex = trimmedPath.lastIndexOf('.');
+                // 截取后缀(从 "." 后一位到结尾)
+                String suffix = trimmedPath.substring(lastDotIndex + 1);
+                // 分类添加到对应列表
+                if (IMAGE_SUFFIXES.contains(suffix)) {
+                    imageList.add(trimmedPath);
+                } else if (VIDEO_SUFFIXES.contains(suffix)) {
+                    videoList.add(trimmedPath);
+                }
+            }
+            try {
+                Map<String, Object> jsonBody = new HashMap<>();
+                // text/img/video 三种维度一起传入 AI,便于一次完成合规审查
+                jsonBody.put("text", lifeUserDynamic.getContext());
+                jsonBody.put("image_urls", imageList);
+                jsonBody.put("video_urls", videoList);
+
+                HttpEntity<Map<String, Object>> request = new HttpEntity<>(jsonBody, aiHeaders);
+                ResponseEntity<String> response = null;
+                try {
+                    response = restTemplate.postForEntity(contentComplianceUrl, request, String.class);
+                    if (response.getStatusCodeValue() != 200) {
+                        log.error("AI内容审核接口调用失败 http状态:" + response.getStatusCode());
+                    }
+                    JSONObject responseNode = JSONObject.parseObject(response.getBody());
+                    if (responseNode == null) {
+                        log.error("AI接口调用失败,响应内容为空");
+                    }
+                    Integer code = null;
+                    if (responseNode != null) {
+                        code = responseNode.getInteger("code");
+                        if (code == 200) {
+                            JSONObject dataNode = JSONObject.from(responseNode.get("data"));
+                            // 审核发起后仅标记 checkFlag=1 并保存任务号,等待回调或后续轮询更新
+                            LifeUserDynamics dynamics = new LifeUserDynamics();
+                            dynamics.setId(lifeUserDynamic.getId());
+                            dynamics.setCheckFlag(1);
+                            dynamics.setAiTaskId(dataNode.get("task_id").toString());
+                            lifeUserDynamicsMapper.updateById(dynamics);
+                            log.info("动态审核成功,AI返回内容: {}", response.getBody());
+                            XxlJobHelper.handleSuccess("动态内容审核任务执行成功");
                         }
+                    } else {
+                        log.error("AI接口调用失败,错误码: " + code);
                     }
-                } catch (RuntimeException ex) {
-                    XxlJobHelper.handleFail("动态内容审核任务执行失败:" + ex.getMessage());
-                    return R.fail("动态内容审核任务执行失败:" + ex.getMessage());
+                } catch (Exception e) {
+                    log.error("调用AI内容审核接口失败", e);
                 }
+            } catch (RuntimeException ex) {
+                XxlJobHelper.handleFail("动态内容审核任务执行失败:" + ex.getMessage());
+                return R.fail("动态内容审核任务执行失败:" + ex.getMessage());
             }
         }
         return R.success("动态内容审核任务执行成功");
@@ -698,82 +768,127 @@ public class AiTagJob {
     public R<String> getCheckTask() {
         List<LifeUserDynamics> lifeUserDynamics = lifeUserDynamicsMapper.selectList(new LambdaQueryWrapper<LifeUserDynamics>()
                 .eq(LifeUserDynamics::getCheckFlag, 1).eq(LifeUserDynamics::getDeleteFlag, 0));
+        log.info("登录Ai服务获取token..." + loginUrl);
+        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
+        formData.add("username", userName);
+        formData.add("password", passWord);
 
-        for (LifeUserDynamics lifeUserDynamic : lifeUserDynamics) {
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+        HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(formData, headers);
+        ResponseEntity<String> postForEntity = null;
+        try {
+            log.info("请求Ai服务登录接口===================>");
+            postForEntity = restTemplate.postForEntity(loginUrl, requestEntity, String.class);
+        } catch (Exception e) {
+            log.error("请求AI服务登录接口失败", e);
+        }
+        RestTemplate restTemplateWithAuth = new RestTemplate();
+        if (postForEntity != null && postForEntity.getStatusCodeValue() == 200) {
+            log.info("请求Ai服务登录成功 postForEntity.getBody()\t" + postForEntity.getBody());
+            String responseBody = postForEntity.getBody();
+            JSONObject jsonObject = JSONObject.parseObject(responseBody);
+            if (jsonObject != null) {
+                JSONObject dataJson = jsonObject.getJSONObject("data");
+                String accessToken = dataJson.getString("access_token");
+                List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
+                interceptors.add((request, body, execution) -> {
+                    request.getHeaders().set("Authorization", "Bearer " + accessToken);
+                    return execution.execute(request, body);
+                });
+                restTemplateWithAuth.setInterceptors(interceptors);
+            }
+        }
+
+        List<StoreClockIn> storeClockIns = storeClockInMapper.selectList(new LambdaQueryWrapper<StoreClockIn>().eq(StoreClockIn::getCheckFlag, 1));
+        for (StoreClockIn storeClockIn : storeClockIns) {
             // 针对已提交且未删除的动态轮询查询结果
             try {
-                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);
-                ResponseEntity<String> postForEntity = null;
+                ResponseEntity<String> response = null;
                 try {
-                    log.info("请求Ai服务登录接口===================>");
-                    postForEntity = restTemplate.postForEntity(loginUrl, requestEntity, String.class);
+                    response = restTemplateWithAuth.getForEntity(getResultUrl + "?task_id=" + storeClockIn.getAiTaskId(), String.class);
+                    if (response.getStatusCodeValue() != 200) {
+                        log.error("AI内容审核结果获取接口调用失败 http状态:" + response.getStatusCode());
+                    }
+                    JSONObject responseNode = JSONObject.parseObject(response.getBody());
+                    if (responseNode == null) {
+                        log.error("AI接口调用失败,响应内容为空");
+                    }
+                    Integer code = null;
+                    if (responseNode != null) {
+                        code = responseNode.getInteger("code");
+                        if (code == 200) {
+                            JSONObject dataNode = JSONObject.from(responseNode.get("data"));
+                            StoreClockIn clockIn = new StoreClockIn();
+                            clockIn.setId(storeClockIn.getId());
+                            if ("completed".equals(dataNode.get("status"))) {
+                                if (!(boolean) dataNode.get("is_compliant")) {
+                                    // 只要 AI 判定不合规,立即禁用动态并记录原因
+                                    clockIn.setDeleteFlag(1);
+                                    clockIn.setReason(String.valueOf(dataNode.get("failure_reason")));
+                                }
+                                clockIn.setCheckFlag(2);
+                                storeClockInMapper.updateById(clockIn);
+                                if (!(boolean) dataNode.get("is_compliant")) {
+                                    storeClockInMapper.deleteById(clockIn);
+                                }
+                                log.info("动态审核结果获取成功,AI返回内容: {}", response.getBody());
+                                XxlJobHelper.handleSuccess("动态内容审核任务结果获取执行成功");
+                            } else {
+                                log.info("动态审核未完成,AI返回内容: {}", response.getBody());
+                            }
+                        } else {
+                            log.error("AI接口调用失败,错误码: " + code);
+                        }
+                    }
                 } catch (Exception e) {
-                    log.error("请求AI服务登录接口失败", e);
+                    log.error("调用AI内容审核结果获取接口失败", e);
                 }
+            } catch (RuntimeException ex) {
+                XxlJobHelper.handleFail("动态内容审核任务结果获取执行失败:" + ex.getMessage());
+                return R.fail("动态内容审核任务结果获取执行失败:" + ex.getMessage());
+            }
+        }
 
-                if (postForEntity != null && postForEntity.getStatusCodeValue() == 200) {
-                    log.info("请求Ai服务登录成功 postForEntity.getBody()\t" + postForEntity.getBody());
-                    String responseBody = postForEntity.getBody();
-                    JSONObject jsonObject = JSONObject.parseObject(responseBody);
-                    if (jsonObject != null) {
-                        JSONObject dataJson = jsonObject.getJSONObject("data");
-                        String accessToken = dataJson.getString("access_token");
-
-                        // 创建带拦截器的RestTemplate
-                        // 使用拦截器统一追加 Authorization,避免每次手动设置 header
-                        RestTemplate restTemplateWithAuth = new RestTemplate();
-                        List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
-                        interceptors.add((request, body, execution) -> {
-                            request.getHeaders().set("Authorization", "Bearer " + accessToken);
-                            return execution.execute(request, body);
-                        });
-                        restTemplateWithAuth.setInterceptors(interceptors);
-
-                        ResponseEntity<String> response = null;
-                        try {
-                            response = restTemplateWithAuth.getForEntity(getResultUrl + "?task_id="+lifeUserDynamic.getAiTaskId(), String.class);
-                            if (response.getStatusCodeValue() != 200) {
-                                log.error("AI内容审核结果获取接口调用失败 http状态:" + response.getStatusCode());
-                            }
-                            JSONObject responseNode = JSONObject.parseObject(response.getBody());
-                            if (responseNode == null) {
-                                log.error("AI接口调用失败,响应内容为空");
-                            }
-                            Integer code = null;
-                            if (responseNode != null) {
-                                code = responseNode.getInteger("code");
-                                if(code==200) {
-                                    JSONObject dataNode = JSONObject.from(responseNode.get("data"));
-                                    LifeUserDynamics dynamics = new LifeUserDynamics();
-                                    if ("completed".equals(dataNode.get("status"))) {
-                                        if (!(boolean) dataNode.get("is_compliant")) {
-                                            // 只要 AI 判定不合规,立即禁用动态并记录原因
-                                            dynamics.setId(lifeUserDynamic.getId());
-                                            dynamics.setEnableStatus(1);
-                                            dynamics.setReason(String.valueOf(dataNode.get("failure_reason")));
-                                        }
-                                        dynamics.setCheckFlag(2);
-                                        lifeUserDynamicsMapper.updateById(dynamics);
-                                        log.info("动态审核结果获取成功,AI返回内容: {}", response.getBody());
-                                        XxlJobHelper.handleSuccess("动态内容审核任务结果获取执行成功");
-                                    } else {
-                                        log.info("动态审核未完成,AI返回内容: {}", response.getBody());
-                                    }
-                                } else {
-                                    log.error("AI接口调用失败,错误码: " + code);
+        for (LifeUserDynamics lifeUserDynamic : lifeUserDynamics) {
+            // 针对已提交且未删除的动态轮询查询结果
+            try {
+                ResponseEntity<String> response = null;
+                try {
+                    response = restTemplateWithAuth.getForEntity(getResultUrl + "?task_id=" + lifeUserDynamic.getAiTaskId(), String.class);
+                    if (response.getStatusCodeValue() != 200) {
+                        log.error("AI内容审核结果获取接口调用失败 http状态:" + response.getStatusCode());
+                    }
+                    JSONObject responseNode = JSONObject.parseObject(response.getBody());
+                    if (responseNode == null) {
+                        log.error("AI接口调用失败,响应内容为空");
+                    }
+                    Integer code = null;
+                    if (responseNode != null) {
+                        code = responseNode.getInteger("code");
+                        if (code == 200) {
+                            JSONObject dataNode = JSONObject.from(responseNode.get("data"));
+                            LifeUserDynamics dynamics = new LifeUserDynamics();
+                            dynamics.setId(lifeUserDynamic.getId());
+                            if ("completed".equals(dataNode.get("status"))) {
+                                if (!(boolean) dataNode.get("is_compliant")) {
+                                    // 只要 AI 判定不合规,立即禁用动态并记录原因
+                                    dynamics.setEnableStatus(1);
+                                    dynamics.setReason(String.valueOf(dataNode.get("failure_reason")));
                                 }
+                                dynamics.setCheckFlag(2);
+                                lifeUserDynamicsMapper.updateById(dynamics);
+                                log.info("动态审核结果获取成功,AI返回内容: {}", response.getBody());
+                                XxlJobHelper.handleSuccess("动态内容审核任务结果获取执行成功");
+                            } else {
+                                log.info("动态审核未完成,AI返回内容: {}", response.getBody());
                             }
-                        } catch (Exception e) {
-                            log.error("调用AI内容审核结果获取接口失败", e);
+                        } else {
+                            log.error("AI接口调用失败,错误码: " + code);
                         }
                     }
+                } catch (Exception e) {
+                    log.error("调用AI内容审核结果获取接口失败", e);
                 }
             } catch (RuntimeException ex) {
                 XxlJobHelper.handleFail("动态内容审核任务结果获取执行失败:" + ex.getMessage());