浏览代码

Merge remote-tracking branch 'origin/sit-eight-categories' into sit-eight-categories

panzhilin 2 天之前
父节点
当前提交
ee248d9b09

+ 6 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreProductItemDto.java

@@ -1,10 +1,12 @@
 package shop.alien.entity.store.dto;
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
 import java.io.Serializable;
+import java.math.BigDecimal;
 import java.util.List;
 
 @Data
@@ -25,6 +27,10 @@ public class StoreProductItemDto implements Serializable {
     @ApiModelProperty("商品名称")
     private String prodName;
 
+    @ApiModelProperty(value = "总价")
+    @TableField("total_price")
+    private BigDecimal totalPrice;
+
     @ApiModelProperty("商品类型,整型枚举:1酒吧-酒水 2酒吧-餐食 3酒吧-套餐  4美食-餐食 5运动健身-单次 6运动健身-多次")
     private Integer prodType;
 

+ 27 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreStaffPositionCountVo.java

@@ -0,0 +1,27 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 员工职位统计VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "StoreStaffPositionCountVo对象", description = "员工职位统计")
+public class StoreStaffPositionCountVo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "员工职位")
+    private String staffPosition;
+
+    @ApiModelProperty(value = "该职位对应的员工数量")
+    private Long count;
+}
+

+ 1 - 6
alien-store/src/main/java/shop/alien/store/controller/StoreProductBarController.java

@@ -39,12 +39,7 @@ public class StoreProductBarController {
         if (bar.getPrice() == null) {
             return R.fail("价格不能为空");
         }
-        if (bar.getCostPrice() == null) {
-            return R.fail("成本价不能为空");
-        }
-        if (bar.getCategory() == null || bar.getCategory().trim().isEmpty()) {
-            return R.fail("品类不能为空");
-        }
+
 
         R<StoreProductBar> result = storeProductBarService.addStoreProductBar(bar);
         if (result.getCode() == 200) {

+ 4 - 1
alien-store/src/main/java/shop/alien/store/controller/StoreProductItemController.java

@@ -57,7 +57,10 @@ public class StoreProductItemController {
     @ApiOperation("根据ID查询商品")
     @ApiOperationSupport(order = 4)
     @GetMapping("/getById")
-    public R<List<StoreProductItemDto>> getStoreProductItemById(@RequestParam(value = "id") Integer id,@RequestParam(value = "id")Integer modelType) {
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "modelType", value = "1酒吧2美食3运动", dataType = "int", paramType = "query"),
+    })
+    public R<List<StoreProductItemDto>> getStoreProductItemById(@RequestParam(value = "id") Integer id,@RequestParam(value = "modelType")Integer modelType) {
         log.info("StoreProductItemController.getStoreProductItemById?id={}", id);
         return storeProductItemService.getStoreProductItemById(id, modelType);
     }

+ 42 - 4
alien-store/src/main/java/shop/alien/store/controller/StoreStaffConfigController.java

@@ -12,6 +12,7 @@ import shop.alien.entity.store.StoreStaffConfig;
 import shop.alien.entity.store.dto.StoreStaffConfigListQueryDto;
 import shop.alien.entity.store.vo.StoreStaffDetailVo;
 import shop.alien.entity.store.vo.StoreStaffFitnessDetailVo;
+import shop.alien.entity.store.vo.StoreStaffPositionCountVo;
 import shop.alien.mapper.StoreDictionaryMapper;
 import shop.alien.store.service.StoreStaffConfigService;
 
@@ -150,15 +151,17 @@ public class StoreStaffConfigController {
             @ApiImplicitParam(name = "page", value = "分页页数", dataType = "Integer", paramType = "query", required = false),
             @ApiImplicitParam(name = "size", value = "分页条数", dataType = "Integer", paramType = "query", required = false),
             @ApiImplicitParam(name = "storeId", value = "店铺ID", dataType = "Integer", paramType = "query", required = true),
-            @ApiImplicitParam(name = "status", value = "员工状态(0-待审核 1-审核通过 2-审核拒绝)", dataType = "String", paramType = "query", required = false)
+            @ApiImplicitParam(name = "status", value = "员工状态(0-待审核 1-审核通过 2-审核拒绝)", dataType = "String", paramType = "query", required = false),
+            @ApiImplicitParam(name = "staffPosition", value = "员工职位", dataType = "String", paramType = "query", required = false)
     })
     @GetMapping("/queryStaffList")
     public R<IPage<StoreStaffConfig>> queryStaffList(
             @RequestParam(value = "page", defaultValue = "1") Integer page,
             @RequestParam(value = "size", defaultValue = "10") Integer size,
             @RequestParam(value = "storeId") Integer storeId,
-            @RequestParam(value = "status", required = false) String status) {
-        log.info("查询员工列表,参数:page={}, size={}, storeId={}, status={}", page, size, storeId, status);
+            @RequestParam(value = "status", required = false) String status,
+            @RequestParam(value = "staffPosition", required = false) String staffPosition) {
+        log.info("查询员工列表,参数:page={}, size={}, storeId={}, status={}, staffPosition={}", page, size, storeId, status, staffPosition);
 
         // 参数校验
         if (storeId == null || storeId <= 0) {
@@ -176,7 +179,7 @@ public class StoreStaffConfigController {
             size = 100;
         }
 
-        IPage<StoreStaffConfig> result = storeStaffConfigService.queryStaffList(page, size, storeId, status);
+        IPage<StoreStaffConfig> result = storeStaffConfigService.queryStaffList(page, size, storeId, status, staffPosition);
         log.info("查询员工列表成功,共{}条记录", result.getTotal());
         return R.data(result);
     }
@@ -373,4 +376,39 @@ public class StoreStaffConfigController {
         }
     }
 
+    /**
+     * 查询指定店铺的员工职位统计信息
+     *
+     * @param storeId 店铺ID,必填
+     * @return 员工职位统计列表,每个元素包含职位名称和对应员工数量
+     */
+    @ApiOperation("查询员工职位统计(用户端)")
+    @ApiOperationSupport(order = 10)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "店铺ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/getStaffPositionCount")
+    public R<List<StoreStaffPositionCountVo>> getStaffPositionCount(
+            @RequestParam(value = "storeId") Integer storeId) {
+        log.info("查询员工职位统计,参数:storeId={}", storeId);
+
+        try {
+            // 参数校验
+            if (storeId == null || storeId <= 0) {
+                log.warn("查询员工职位统计失败,店铺ID无效:storeId={}", storeId);
+                return R.fail("店铺ID不能为空且必须大于0");
+            }
+
+            List<StoreStaffPositionCountVo> result = storeStaffConfigService.getStaffPositionCount(storeId);
+            log.info("查询员工职位统计成功,storeId={},职位数量:{}", storeId, result.size());
+            return R.data(result);
+        } catch (IllegalArgumentException e) {
+            log.warn("查询员工职位统计失败,参数错误:{}", e.getMessage());
+            return R.fail(e.getMessage());
+        } catch (Exception e) {
+            log.error("查询员工职位统计异常,storeId={},异常信息:{}", storeId, e.getMessage(), e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
 }

+ 20 - 6
alien-store/src/main/java/shop/alien/store/service/StoreStaffConfigService.java

@@ -3,8 +3,10 @@ package shop.alien.store.service;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import shop.alien.entity.store.StoreStaffConfig;
 import shop.alien.entity.store.dto.StoreStaffConfigListQueryDto;
+import shop.alien.entity.store.vo.StoreStaffPositionCountVo;
 
 import java.io.IOException;
+import java.util.List;
 
 public interface StoreStaffConfigService {
 
@@ -74,17 +76,18 @@ public interface StoreStaffConfigService {
     /**
      * 员工列表查询(用户端)
      * <p>
-     * 查询指定店铺的员工列表,支持按状态筛选
+     * 查询指定店铺的员工列表,支持按状态和职位筛选
      * 排序规则:先按置顶状态降序,再按置顶时间降序,最后按创建时间降序
      * </p>
      *
-     * @param page    分页页数,必须大于0
-     * @param size    分页条数,必须大于0
-     * @param storeId 店铺ID,必须大于0
-     * @param status  员工状态,可选(0-待审核 1-审核通过 2-审核拒绝),为空时查询所有状态
+     * @param page          分页页数,必须大于0
+     * @param size          分页条数,必须大于0
+     * @param storeId       店铺ID,必须大于0
+     * @param status        员工状态,可选(0-待审核 1-审核通过 2-审核拒绝),为空时查询所有状态
+     * @param staffPosition 员工职位,可选,为空时查询所有职位
      * @return 员工列表分页结果
      */
-    IPage<StoreStaffConfig> queryStaffList(Integer page, Integer size, Integer storeId, String status);
+    IPage<StoreStaffConfig> queryStaffList(Integer page, Integer size, Integer storeId, String status, String staffPosition);
 
     /**
      * 员工详情查询(用户端)
@@ -128,4 +131,15 @@ public interface StoreStaffConfigService {
      * @return 健身教练详情(包含员工信息、基本信息和认证/荣誉列表),如果员工不存在则返回null
      */
     shop.alien.entity.store.vo.StoreStaffFitnessDetailVo getFitnessCoachDetail(Integer id);
+
+    /**
+     * 查询指定店铺的员工职位统计信息
+     * <p>
+     * 统计指定店铺下各个职位的员工数量,只统计未删除的员工
+     * </p>
+     *
+     * @param storeId 店铺ID,必须大于0
+     * @return 员工职位统计列表,每个元素包含职位名称和对应员工数量
+     */
+    List<StoreStaffPositionCountVo> getStaffPositionCount(Integer storeId);
 }

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

@@ -170,7 +170,7 @@ public class StoreProductItemServiceImpl extends ServiceImpl<StoreProductItemMap
     public R<List<StoreProductItemDto>> getStoreProductItemById(Integer id,Integer modelType) {
         log.info("StoreProductItemServiceImpl.getStoreProductItemById?id={}", id);
         StoreProductItem storeProductItem = this.getById(id);
-        if (storeProductItem == null) {
+        if (storeProductItem != null) {
             ArrayList<StoreProductItemDto> list = new ArrayList<>();
             StoreProductItemDto storeProductItemDto = new StoreProductItemDto();
             list.add(storeProductItemDto);

+ 119 - 10
alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffConfigServiceImpl.java

@@ -20,6 +20,7 @@ import shop.alien.entity.store.excelVo.StoreStaffConfigExcelVo;
 import shop.alien.entity.store.excelVo.util.ExcelGenerator;
 import shop.alien.entity.store.vo.StoreStaffDetailVo;
 import shop.alien.entity.store.vo.StoreStaffFitnessDetailVo;
+import shop.alien.entity.store.vo.StoreStaffPositionCountVo;
 import shop.alien.mapper.*;
 import shop.alien.store.service.StoreStaffConfigService;
 import shop.alien.store.service.StoreStaffFitnessBaseService;
@@ -35,6 +36,7 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
@@ -73,6 +75,8 @@ public class StoreStaffConfigServiceImpl implements StoreStaffConfigService {
 
     private final AiContentModerationUtil aiContentModerationUtil;
 
+    private final shop.alien.store.util.ai.AiVideoModerationUtil aiVideoModerationUtil;
+
     private final StoreStaffFitnessExperienceService storeStaffFitnessExperienceService;
 
     /**
@@ -189,6 +193,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());
         }
@@ -196,26 +202,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);
         }
@@ -627,6 +668,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
@@ -721,7 +783,7 @@ public class StoreStaffConfigServiceImpl implements StoreStaffConfigService {
     }
 
     @Override
-    public IPage<StoreStaffConfig> queryStaffList(Integer page, Integer size, Integer storeId, String status) {
+    public IPage<StoreStaffConfig> queryStaffList(Integer page, Integer size, Integer storeId, String status, String staffPosition) {
         // 参数校验
         if (page == null || page < 1) {
             page = 1;
@@ -748,6 +810,11 @@ public class StoreStaffConfigServiceImpl implements StoreStaffConfigService {
             queryWrapper.eq(StoreStaffConfig::getStatus, status);
         }
 
+        // 可选条件:员工职位筛选
+        if (StringUtils.isNotEmpty(staffPosition)) {
+            queryWrapper.eq(StoreStaffConfig::getStaffPosition, staffPosition);
+        }
+
         // 排序规则:先按置顶状态降序(置顶的在前),再按置顶时间降序,最后按创建时间降序
         queryWrapper.orderByDesc(StoreStaffConfig::getTopStatus)
                 .orderByDesc(StoreStaffConfig::getTopTime)
@@ -944,4 +1011,46 @@ public class StoreStaffConfigServiceImpl implements StoreStaffConfigService {
             throw new RuntimeException("查询健身教练详情异常:" + e.getMessage(), e);
         }
     }
+
+    @Override
+    public List<StoreStaffPositionCountVo> getStaffPositionCount(Integer storeId) {
+        log.info("查询员工职位统计,storeId={}", storeId);
+
+        // 参数校验
+        if (storeId == null || storeId <= 0) {
+            log.warn("查询员工职位统计失败,店铺ID无效:storeId={}", storeId);
+            throw new IllegalArgumentException("店铺ID不能为空且必须大于0");
+        }
+
+        // 构建查询条件:查询指定店铺下未删除的员工
+        LambdaQueryWrapper<StoreStaffConfig> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(StoreStaffConfig::getStoreId, storeId)
+                .eq(StoreStaffConfig::getDeleteFlag, CommonConstant.DELETE_FLAG_UNDELETE)
+                // 只查询staff_position不为空的记录
+                .isNotNull(StoreStaffConfig::getStaffPosition)
+                .ne(StoreStaffConfig::getStaffPosition, "");
+
+        // 查询所有符合条件的员工
+        List<StoreStaffConfig> staffList = storeStaffConfigMapper.selectList(queryWrapper);
+
+        // 按职位分组统计(查询条件已过滤空值,此处直接分组即可)
+        Map<String, Long> positionCountMap = staffList.stream()
+                .collect(Collectors.groupingBy(
+                        StoreStaffConfig::getStaffPosition,
+                        Collectors.counting()
+                ));
+
+        // 转换为VO列表
+        List<StoreStaffPositionCountVo> result = positionCountMap.entrySet().stream()
+                .map(entry -> {
+                    StoreStaffPositionCountVo vo = new StoreStaffPositionCountVo();
+                    vo.setStaffPosition(entry.getKey());
+                    vo.setCount(entry.getValue());
+                    return vo;
+                })
+                .collect(Collectors.toList());
+
+        log.info("查询员工职位统计成功,storeId={},职位数量:{}", storeId, result.size());
+        return result;
+    }
 }

+ 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);
+        }
+    }
+}
+