Просмотр исходного кода

feat:人员配置调用ai视频审核

penghao 2 дней назад
Родитель
Сommit
1c2898138e

+ 105 - 12
alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffConfigServiceImpl.java

@@ -25,6 +25,7 @@ import shop.alien.store.service.StoreStaffConfigService;
 import shop.alien.store.service.StoreStaffFitnessBaseService;
 import shop.alien.store.service.StoreStaffFitnessCertificationService;
 import shop.alien.store.service.StoreStaffFitnessCourseService;
+import shop.alien.store.service.StoreStaffFitnessExperienceService;
 import shop.alien.store.util.CommonConstant;
 import shop.alien.store.util.ai.AiContentModerationUtil;
 import shop.alien.util.ali.AliOSSUtil;
@@ -72,6 +73,10 @@ public class StoreStaffConfigServiceImpl implements StoreStaffConfigService {
 
     private final AiContentModerationUtil aiContentModerationUtil;
 
+    private final shop.alien.store.util.ai.AiVideoModerationUtil aiVideoModerationUtil;
+
+    private final StoreStaffFitnessExperienceService storeStaffFitnessExperienceService;
+
     /**
      * 认证类型:认证
      */
@@ -186,6 +191,8 @@ public class StoreStaffConfigServiceImpl implements StoreStaffConfigService {
         }
 
         List<String> imageUrls = new ArrayList<>();
+        List<String> videoUrls = new ArrayList<>();
+
         if (StringUtils.isNotEmpty(storeStaffConfig.getStaffImage())) {
             imageUrls.add(storeStaffConfig.getStaffImage());
         }
@@ -193,26 +200,61 @@ public class StoreStaffConfigServiceImpl implements StoreStaffConfigService {
             String[] urls = storeStaffConfig.getBackgroundUrl().split(",");
             for (String url : urls) {
                 if (StringUtils.isNotEmpty(url.trim())) {
-                    imageUrls.add(url.trim());
+                    String trimmedUrl = url.trim();
+                    // 判断是视频还是图片
+                    if (isVideoUrl(trimmedUrl)) {
+                        videoUrls.add(trimmedUrl);
+                    } else {
+                        imageUrls.add(trimmedUrl);
+                    }
                 }
             }
         }
 
-        AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(
+        // 1. 审核文本和图片
+        AiContentModerationUtil.AuditResult textImageAuditResult = aiContentModerationUtil.auditContent(
                 textContent.toString().trim(), imageUrls
         );
 
-        // 根据 AI 审核结果更新状态(1:审核通过;2:审核拒绝)
+        // 2. 审核视频(如果有)
+        shop.alien.store.util.ai.AiVideoModerationUtil.VideoAuditResult videoAuditResult = null;
+        if (!videoUrls.isEmpty()) {
+            log.info("开始审核视频,视频数量:{}", videoUrls.size());
+            videoAuditResult = aiVideoModerationUtil.auditVideos(videoUrls);
+        }
+
+        // 3. 综合审核结果:文本图片审核和视频审核都必须通过
+        boolean allPassed = (textImageAuditResult != null && textImageAuditResult.isPassed()) &&
+                            (videoAuditResult == null || videoAuditResult.isPassed());
+
+        // 根据 AI 审核结果更新状态
+        // 审核通过:状态保持为"0"(审核中),等待人工审核
+        // 审核失败:状态设置为"2"(审核拒绝)
         StoreStaffConfig auditUpdate = new StoreStaffConfig();
         auditUpdate.setId(staffId);
-        if (auditResult != null && auditResult.isPassed()) {
-            auditUpdate.setStatus("1");
+        if (allPassed) {
+            // AI审核通过,状态保持为"审核中"(0),等待人工审核
+            auditUpdate.setStatus("0");
             auditUpdate.setRejectionReason(null);
+            log.info("人员AI审核通过,状态保持为审核中,等待人工审核:staffId={}", staffId);
         } else {
-            String reason = (auditResult != null && StringUtils.isNotEmpty(auditResult.getFailureReason()))
-                    ? auditResult.getFailureReason()
-                    : "审核未通过";
-            log.warn("人员内容审核失败:{}", reason);
+            // AI审核失败,状态设置为"审核拒绝"(2)
+            // 收集所有失败原因
+            List<String> failureReasons = new ArrayList<>();
+            if (textImageAuditResult != null && !textImageAuditResult.isPassed()) {
+                if (StringUtils.isNotEmpty(textImageAuditResult.getFailureReason())) {
+                    failureReasons.add("图文审核:" + textImageAuditResult.getFailureReason());
+                } else {
+                    failureReasons.add("图文审核未通过");
+                }
+            }
+            if (videoAuditResult != null && !videoAuditResult.isPassed()) {
+                // 业务要求:视频审核失败统一记录“视频内容违规”
+                failureReasons.add("视频内容违规");
+            }
+
+            String reason = failureReasons.isEmpty() ? "审核未通过" : String.join("; ", failureReasons);
+            log.warn("人员AI审核失败,状态设置为审核拒绝:staffId={}, reason={}", staffId, reason);
             auditUpdate.setStatus("2");
             auditUpdate.setRejectionReason(reason);
         }
@@ -624,6 +666,27 @@ public class StoreStaffConfigServiceImpl implements StoreStaffConfigService {
     }
 
     /**
+     * 判断URL是否为视频
+     *
+     * @param url URL地址
+     * @return 是否为视频
+     */
+    private boolean isVideoUrl(String url) {
+        if (url == null || url.isEmpty()) {
+            return false;
+        }
+        String lowerUrl = url.toLowerCase();
+        return lowerUrl.endsWith(".mp4") ||
+               lowerUrl.endsWith(".avi") ||
+               lowerUrl.endsWith(".flv") ||
+               lowerUrl.endsWith(".mkv") ||
+               lowerUrl.endsWith(".rmvb") ||
+               lowerUrl.endsWith(".wmv") ||
+               lowerUrl.endsWith(".3gp") ||
+               lowerUrl.endsWith(".mov");
+    }
+
+    /**
      * 将明细行结构组装成分组结构(用于前端绑定)
      * - 优先按 courseType 分组(DB里可能存 dictDetail 或 dictId)
      * - courseTypeName:能从字典表解析到名称则回填,否则回填为原 courseType
@@ -894,17 +957,47 @@ public class StoreStaffConfigServiceImpl implements StoreStaffConfigService {
             List<shop.alien.entity.store.StoreStaffFitnessCertification> honorList =
                     storeStaffFitnessCertificationService.getListByStaffIdAndType(id, HONOR_TYPE);
 
+
+            // 查询从业经历列表
+            List<shop.alien.entity.store.StoreStaffFitnessExperience> experienceList = null;
+            try {
+                experienceList = storeStaffFitnessExperienceService.getListByStaffId(id);
+            } catch (Exception e) {
+                log.warn("查询从业经历列表失败,id={},异常信息:{}", id, e.getMessage());
+                // 从业经历查询失败不影响整体返回,设置为null即可
+            }
+
+            // 根据店铺ID查询店铺名称
+            String storeName = null;
+            if (staffConfig.getStoreId() != null && staffConfig.getStoreId() > 0) {
+                try {
+                    StoreInfo storeInfo = storeInfoMapper.selectById(staffConfig.getStoreId());
+                    if (storeInfo != null) {
+                        storeName = storeInfo.getStoreName();
+                    } else {
+                        log.warn("查询店铺信息失败,店铺不存在:storeId={}", staffConfig.getStoreId());
+                    }
+                } catch (Exception e) {
+                    log.warn("查询店铺信息异常,storeId={},异常信息:{}", staffConfig.getStoreId(), e.getMessage());
+                    // 店铺信息查询失败不影响整体返回,设置为null即可
+                }
+            }
+
             // 构建返回对象
             StoreStaffFitnessDetailVo detailVo = new StoreStaffFitnessDetailVo();
             detailVo.setStaffInfo(staffConfig);
             detailVo.setBaseInfo(baseInfo);
             detailVo.setCertificationList(certificationList != null ? certificationList : new ArrayList<>());
             detailVo.setHonorList(honorList != null ? honorList : new ArrayList<>());
+            detailVo.setExperienceList(experienceList != null ? experienceList : new ArrayList<>());
+            detailVo.setStoreName(storeName);
 
-            log.info("查询健身教练详情成功,id={},认证数量:{},荣誉数量:{}",
-                    id,
+            log.info("查询健身教练详情成功,id={},认证数量:{},荣誉数量:{},从业经历数量:{},店铺名称:{}",
+                    id, 
                     certificationList != null ? certificationList.size() : 0,
-                    honorList != null ? honorList.size() : 0);
+                    honorList != null ? honorList.size() : 0,
+                    experienceList != null ? experienceList.size() : 0,
+                    storeName);
             return detailVo;
         } catch (Exception e) {
             log.error("查询健身教练详情异常,id={},异常信息:{}", id, e.getMessage(), e);

+ 351 - 0
alien-store/src/main/java/shop/alien/store/util/ai/AiVideoModerationUtil.java

@@ -0,0 +1,351 @@
+package shop.alien.store.util.ai;
+
+import com.alibaba.fastjson2.JSONObject;
+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.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 视频审核工具类
+ * 调用AI视频审核接口审核视频内容
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class AiVideoModerationUtil {
+
+    private final RestTemplate restTemplate;
+
+    /**
+     * 视频上传接口地址
+     */
+    @Value("${ai.service.video-submit-url}")
+    private String videoSubmitUrl;
+
+    /**
+     * 视频审查接口地址
+     */
+    @Value("${ai.service.video-status-url}")
+    private String videoStatusUrl;
+
+    /**
+     * 轮询间隔(毫秒)
+     */
+    @Value("${ai.service.video-poll-interval:2000}")
+    private long pollInterval;
+
+    /**
+     * 最大轮询次数
+     */
+    @Value("${ai.service.video-max-poll-count:30}")
+    private int maxPollCount;
+
+    /**
+     * 审核结果类
+     */
+    public static class VideoAuditResult {
+        private boolean passed;
+        private String failureReason;
+        private String taskId;
+        private JSONObject rawResult;
+
+        public VideoAuditResult(boolean passed, String failureReason) {
+            this.passed = passed;
+            this.failureReason = failureReason;
+        }
+
+        public VideoAuditResult(boolean passed, String failureReason, String taskId, JSONObject rawResult) {
+            this.passed = passed;
+            this.failureReason = failureReason;
+            this.taskId = taskId;
+            this.rawResult = rawResult;
+        }
+
+        public boolean isPassed() {
+            return passed;
+        }
+
+        public String getFailureReason() {
+            return failureReason;
+        }
+
+        public String getTaskId() {
+            return taskId;
+        }
+
+        public JSONObject getRawResult() {
+            return rawResult;
+        }
+    }
+
+    /**
+     * 审核视频内容
+     *
+     * @param videoUrls 视频URL列表
+     * @return 审核结果
+     */
+    public VideoAuditResult auditVideos(List<String> videoUrls) {
+        if (videoUrls == null || videoUrls.isEmpty()) {
+            log.info("视频URL列表为空,跳过审核");
+            return new VideoAuditResult(true, null);
+        }
+
+        log.info("开始审核视频:videoCount={}", videoUrls.size());
+
+        try {
+            List<VideoAuditResult> results = new ArrayList<>();
+            for (String videoUrl : videoUrls) {
+                if (StringUtils.hasText(videoUrl)) {
+                    VideoAuditResult result = auditSingleVideo(videoUrl);
+                    results.add(result);
+                }
+            }
+
+            // 如果任何一个视频审核失败,整体审核失败
+            for (VideoAuditResult result : results) {
+                if (!result.isPassed()) {
+                    log.warn("视频审核失败:{}", result.getFailureReason());
+                    return result;
+                }
+            }
+
+            log.info("所有视频审核通过");
+            return new VideoAuditResult(true, null);
+
+        } catch (Exception e) {
+            log.error("审核视频异常", e);
+            return new VideoAuditResult(false, "审核服务异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 审核单个视频
+     *
+     * @param videoUrl 视频URL
+     * @return 审核结果
+     */
+    private VideoAuditResult auditSingleVideo(String videoUrl) {
+        try {
+            // 1. 提交视频审核任务
+            String taskId = submitVideo(videoUrl);
+            if (taskId == null) {
+                return new VideoAuditResult(false, "视频提交失败");
+            }
+
+            log.info("视频提交成功,taskId={}, videoUrl={}", taskId, videoUrl);
+
+            // 2. 轮询审核状态
+            return pollVideoStatus(taskId, videoUrl);
+
+        } catch (Exception e) {
+            log.error("审核视频异常,videoUrl={}", videoUrl, e);
+            return new VideoAuditResult(false, "审核服务异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 提交视频审核任务
+     *
+     * @param videoUrl 视频URL
+     * @return 任务ID
+     */
+    private String submitVideo(String videoUrl) {
+        try {
+            // 构建 form-data 请求头
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.MULTIPART_FORM_DATA);
+
+            // 构建 form-data 请求体,兼容服务端可能的三种参数名
+            MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
+            formData.add("videoUrl", videoUrl);
+            formData.add("video_path", videoUrl);
+            formData.add("video_url", videoUrl);
+
+            HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(formData, headers);
+
+            log.info("提交视频审核任务:url={}, videoUrl={}", videoSubmitUrl, videoUrl);
+
+            // 发送请求
+            ResponseEntity<String> response = restTemplate.postForEntity(videoSubmitUrl, requestEntity, String.class);
+
+            if (response.getStatusCode() == HttpStatus.OK) {
+                String responseBody = response.getBody();
+                log.info("视频提交接口响应:{}", responseBody);
+
+                if (StringUtils.hasText(responseBody)) {
+                    JSONObject jsonResponse = JSONObject.parseObject(responseBody);
+                    String taskId = jsonResponse.getString("task_id");
+                    if (StringUtils.hasText(taskId)) {
+                        return taskId;
+                    } else {
+                        log.error("视频提交接口返回的task_id为空");
+                        return null;
+                    }
+                } else {
+                    log.error("视频提交接口返回空响应");
+                    return null;
+                }
+            } else {
+                log.error("视频提交接口调用失败,状态码:{}", response.getStatusCode());
+                return null;
+            }
+
+        } catch (org.springframework.web.client.HttpClientErrorException e) {
+            log.error("调用视频提交接口异常,status={}, body={}", e.getStatusCode(), e.getResponseBodyAsString(), e);
+            return null;
+        } catch (Exception e) {
+            log.error("调用视频提交接口异常", e);
+            return null;
+        }
+    }
+
+    /**
+     * 轮询视频审核状态
+     *
+     * @param taskId 任务ID
+     * @param videoUrl 视频URL(用于日志)
+     * @return 审核结果
+     */
+    private VideoAuditResult pollVideoStatus(String taskId, String videoUrl) {
+        int pollCount = 0;
+        while (pollCount < maxPollCount) {
+            try {
+                // 等待一段时间再查询
+                if (pollCount > 0) {
+                    Thread.sleep(pollInterval);
+                }
+
+                String statusUrl = videoStatusUrl + "/" + taskId;
+                log.info("查询视频审核状态:url={}, taskId={}, pollCount={}", statusUrl, taskId, pollCount + 1);
+
+                ResponseEntity<String> response = restTemplate.getForEntity(statusUrl, String.class);
+
+                if (response.getStatusCode() == HttpStatus.OK) {
+                    String responseBody = response.getBody();
+                    log.info("视频审核状态接口响应:{}", responseBody);
+
+                    if (StringUtils.hasText(responseBody)) {
+                        JSONObject jsonResponse = JSONObject.parseObject(responseBody);
+                        String status = jsonResponse.getString("status");
+
+                        if ("completed".equals(status)) {
+                            // 审核完成,解析结果
+                            return parseVideoAuditResult(jsonResponse, taskId);
+                        } else if ("failed".equals(status) || "error".equals(status)) {
+                            // 审核失败
+                            String error = jsonResponse.getString("error");
+                            log.error("视频审核失败,taskId={}, error={}", taskId, error);
+                            return new VideoAuditResult(false, "视频审核失败:" + (error != null ? error : "未知错误"), taskId, jsonResponse);
+                        } else {
+                            // 审核中,继续轮询
+                            log.info("视频审核中,taskId={}, status={}, 继续等待...", taskId, status);
+                            pollCount++;
+                        }
+                    } else {
+                        log.error("视频审核状态接口返回空响应");
+                        return new VideoAuditResult(false, "审核状态接口返回空响应");
+                    }
+                } else {
+                    log.error("视频审核状态接口调用失败,状态码:{}", response.getStatusCode());
+                    return new VideoAuditResult(false, "审核状态接口调用失败,状态码:" + response.getStatusCode());
+                }
+
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                log.error("轮询视频审核状态被中断,taskId={}", taskId, e);
+                return new VideoAuditResult(false, "审核被中断");
+            } catch (Exception e) {
+                log.error("查询视频审核状态异常,taskId={}", taskId, e);
+                pollCount++;
+                // 继续重试
+            }
+        }
+
+        log.error("视频审核超时,taskId={}, maxPollCount={}", taskId, maxPollCount);
+        return new VideoAuditResult(false, "视频审核超时");
+    }
+
+    /**
+     * 解析视频审核结果
+     *
+     * @param jsonResponse API响应JSON
+     * @param taskId 任务ID
+     * @return 审核结果
+     */
+    private VideoAuditResult parseVideoAuditResult(JSONObject jsonResponse, String taskId) {
+        try {
+            JSONObject result = jsonResponse.getJSONObject("result");
+            if (result == null) {
+                log.warn("视频审核结果中result字段为空");
+                return new VideoAuditResult(false, "审核结果格式错误:result字段为空", taskId, jsonResponse);
+            }
+
+            Boolean flagged = result.getBoolean("flagged");
+            String riskLevel = result.getString("risk_level");
+            String summary = result.getString("summary");
+
+            if (flagged != null && flagged) {
+                // 有违规内容,审核失败
+                List<String> violationReasons = new ArrayList<>();
+                if (StringUtils.hasText(riskLevel)) {
+                    violationReasons.add("风险等级:" + riskLevel);
+                }
+                if (StringUtils.hasText(summary)) {
+                    violationReasons.add(summary);
+                }
+
+                // 解析违规证据
+                com.alibaba.fastjson2.JSONArray evidence = result.getJSONArray("evidence");
+                if (evidence != null && !evidence.isEmpty()) {
+                    List<String> evidenceReasons = new ArrayList<>();
+                    for (int i = 0; i < evidence.size(); i++) {
+                        JSONObject item = evidence.getJSONObject(i);
+                        if (item != null) {
+                            String reason = item.getString("reason");
+                            String timeOffset = item.getString("time_offset");
+                            if (StringUtils.hasText(reason)) {
+                                String evidenceReason = StringUtils.hasText(timeOffset)
+                                    ? String.format("时间点%s: %s", timeOffset, reason)
+                                    : reason;
+                                evidenceReasons.add(evidenceReason);
+                            }
+                        }
+                    }
+                    if (!evidenceReasons.isEmpty()) {
+                        violationReasons.add("违规证据:" + String.join("; ", evidenceReasons));
+                    }
+                }
+
+                String failureReason = String.join("; ", violationReasons);
+                if (failureReason.isEmpty()) {
+                    failureReason = "视频内容违规";
+                }
+
+                log.warn("视频审核失败:{}", failureReason);
+                return new VideoAuditResult(false, failureReason, taskId, jsonResponse);
+            } else {
+                // 无违规内容,审核通过
+                log.info("视频审核通过:taskId={}", taskId);
+                return new VideoAuditResult(true, null, taskId, jsonResponse);
+            }
+
+        } catch (Exception e) {
+            log.error("解析视频审核结果异常", e);
+            return new VideoAuditResult(false, "解析审核结果异常:" + e.getMessage(), taskId, jsonResponse);
+        }
+    }
+}
+