فهرست منبع

feat(store): 添加打卡图片审核功能

- 新增审核拒绝状态码3,并更新相关注释
- 在打卡查询中过滤掉审核拒绝的记录
- 集成AI内容审核工具进行图片审核
- 实现异步图片审核线程池管理
- 添加审核状态更新逻辑(审核中、审核通过、审核拒绝)
- 实现审核拒绝时的通知机制
- 添加WebSocket实时消息推送支持
- 优化线程池配置和资源释放机制
fcw 2 ماه پیش
والد
کامیت
4619e99d3b

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

@@ -80,7 +80,7 @@ public class StoreClockIn extends Model<StoreClockIn> {
     @TableField(value = "updated_user_id", fill = FieldFill.INSERT_UPDATE)
     private Integer updatedUserId;
 
-    @ApiModelProperty(value = "是否审核(未审核:0,审核中:1,审核完成:2)")
+    @ApiModelProperty(value = "是否审核(未审核:0,审核中:1,审核通过:2,审核拒绝:3)")
     @TableField("check_flag")
     private Integer checkFlag;
 

+ 1 - 1
alien-entity/src/main/java/shop/alien/mapper/StoreClockInMapper.java

@@ -48,7 +48,7 @@ public interface StoreClockInMapper extends BaseMapper<StoreClockIn> {
      *
      * @return list
      */
-    @Select("SELECT store_id as storeId, count(store_id) as count FROM `store_clock_in` where delete_flag = 0 GROUP BY store_id ")
+    @Select("SELECT store_id as storeId, count(store_id) as count FROM `store_clock_in` where delete_flag = 0 and check_flag != 3 GROUP BY store_id ")
     List<Map<Integer, Integer>> getStoreClockInCount();
      /**
       * 获取所有店铺打卡次数(有图片并且设置为可见的)

+ 194 - 5
alien-store/src/main/java/shop/alien/store/service/impl/StoreClockInServiceImpl.java

@@ -8,17 +8,25 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import com.alibaba.fastjson2.JSONObject;
 import shop.alien.entity.store.*;
 import shop.alien.entity.store.vo.StoreClockInVo;
+import shop.alien.entity.store.vo.WebSocketVo;
 import shop.alien.mapper.*;
+import shop.alien.store.config.WebSocketProcess;
 import shop.alien.store.service.StoreClockInService;
 import shop.alien.store.service.StoreCommentService;
+import shop.alien.store.util.ai.AiContentModerationUtil;
+import shop.alien.store.util.ai.AiVideoModerationUtil;
 
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.*;
+import java.util.concurrent.*;
 import java.util.stream.Collectors;
 
 /**
@@ -29,10 +37,15 @@ import java.util.stream.Collectors;
  * @author ssk
  * @since 2025-05-15
  */
+@Slf4j
 @Service
 @RequiredArgsConstructor
+@Transactional(rollbackFor = Exception.class)
 public class StoreClockInServiceImpl extends ServiceImpl<StoreClockInMapper, StoreClockIn> implements StoreClockInService {
 
+    // 1. 自定义图片审核线程池(全局复用,避免频繁创建线程)
+    private ExecutorService imgAuditExecutor;
+
     private final StoreClockInMapper storeClockInMapper;
 
     private final LifeUserMapper lifeUserMapper;
@@ -49,6 +62,62 @@ public class StoreClockInServiceImpl extends ServiceImpl<StoreClockInMapper, Sto
 
      private final StoreImgMapper storeImgMapper;
 
+    private final AiContentModerationUtil aiContentModerationUtil;
+
+    private final LifeNoticeMapper lifeNoticeMapper;
+
+    private final WebSocketProcess webSocketProcess;
+
+    // 初始化线程池
+    @PostConstruct
+    public void initExecutor() {
+        // 核心参数根据业务调整,IO密集型任务线程数可设为CPU核心数*2~4
+        int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
+        int maxPoolSize = Runtime.getRuntime().availableProcessors() * 4;
+        imgAuditExecutor = new ThreadPoolExecutor(
+                corePoolSize,
+                maxPoolSize,
+                60L,
+                TimeUnit.SECONDS,
+                new LinkedBlockingQueue<>(500), // 任务队列,避免无界队列溢出
+                new ThreadFactory() {
+                    private int count = 0;
+                    @Override
+                    public Thread newThread(Runnable r) {
+                        Thread t = new Thread(r);
+                        t.setName("img-audit-" + count++); // 自定义线程名,便于排查
+                        return t;
+                    }
+                },
+                // 任务拒绝策略:记录日志+调用者线程兜底(避免任务丢失)
+                new ThreadPoolExecutor.CallerRunsPolicy() {
+                    @Override
+                    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
+                        log.error("图片审核任务被拒绝,队列已满!当前队列大小:{},活跃线程数:{}",
+                                e.getQueue().size(), e.getActiveCount());
+                        super.rejectedExecution(r, e);
+                    }
+                }
+        );
+
+    }
+
+    // 优雅关闭线程池
+    @PreDestroy
+    public void destroyExecutor() {
+        if (Objects.nonNull(imgAuditExecutor)) {
+            imgAuditExecutor.shutdown();
+            try {
+                if (!imgAuditExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
+                    imgAuditExecutor.shutdownNow();
+                }
+            } catch (InterruptedException e) {
+                imgAuditExecutor.shutdownNow();
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
     @Override
     public StoreClockIn addStoreClockIn(StoreClockIn storeClockIn) {
         LifeUser user = lifeUserMapper.selectById(storeClockIn.getUserId());
@@ -72,6 +141,7 @@ public class StoreClockInServiceImpl extends ServiceImpl<StoreClockInMapper, Sto
             IPage<StoreClockInVo> storeClockInIPage1 = storeClockInMapper.getStoreClockInList(iPage, new QueryWrapper<StoreClockIn>()
                     .eq("clock.store_id", storeId)
                     .eq("clock.delete_flag", 0)
+                    .ne("clock.check_flag", 3)
                     .eq("user.delete_flag", 0)
                     .eq("store.delete_flag", 0)
                     .isNotNull(0 == mySelf, "clock.img_url")
@@ -95,6 +165,7 @@ public class StoreClockInServiceImpl extends ServiceImpl<StoreClockInMapper, Sto
         wrapper.eq(1 == mySelf, "clock.user_id", userId);
         wrapper.isNotNull(0 == mySelf, "clock.img_url");
         wrapper.eq("clock.delete_flag", 0);
+        wrapper.ne("clock.check_flag", 3);
         wrapper.eq("user.delete_flag", 0);
         wrapper.eq("store.delete_flag", 0);
         wrapper.and(wrapper1 ->
@@ -212,11 +283,129 @@ public class StoreClockInServiceImpl extends ServiceImpl<StoreClockInMapper, Sto
 
     @Override
     public int setImgAndAiContent(int id, String img,String aiContent) {
+        // 1. 先更新打卡状态为"审核中",然后返回
         LambdaUpdateWrapper<StoreClockIn> wrapper = new LambdaUpdateWrapper<>();
         wrapper.set(StoreClockIn::getImgUrl, img);
-        wrapper.set(StoreClockIn::getMaybeAiContent,aiContent);
+        wrapper.set(StoreClockIn::getMaybeAiContent, aiContent);
+        wrapper.set(StoreClockIn::getCheckFlag, 1); // 1-审核中
         wrapper.eq(StoreClockIn::getId, id);
-        return storeClockInMapper.update(null, wrapper);
+        int updateResult = storeClockInMapper.update(null, wrapper);
+        
+        // 2. 异步调用AI接口审核图片
+        StoreClockIn storeClockIn = storeClockInMapper.selectById(id);
+        if (storeClockIn == null) {
+            log.warn("打卡记录不存在,id={}", id);
+            return updateResult;
+        }
+        
+        LifeUser lifeUser = lifeUserMapper.selectById(storeClockIn.getUserId());
+        if (lifeUser == null) {
+            log.warn("用户不存在,userId={}", storeClockIn.getUserId());
+            return updateResult;
+        }
+        
+        final String phoneId = "user_" + lifeUser.getUserPhone();
+        final Integer clockInId = id;
+        final Integer userId = storeClockIn.getUserId();
+        
+        List<String> imgList = new ArrayList<>();
+        if (StringUtils.isNotBlank(img)) {
+            String[] imgArray = img.split(",");
+            for (String imgUrl : imgArray) {
+                String trimmed = imgUrl.trim();
+                if (StringUtils.isNotBlank(trimmed)) {
+                    imgList.add(trimmed);
+                }
+            }
+        }
+        
+        // 异步执行AI审核任务
+        CompletableFuture.runAsync(() -> {
+            AiContentModerationUtil.AuditResult imgAuditResult = null;
+            try {
+                imgAuditResult = CompletableFuture.supplyAsync(
+                        () -> aiContentModerationUtil.auditContent(null, imgList),
+                        imgAuditExecutor
+                ).get();
+
+                // 3. 审核后按照注释修改状态
+                LambdaUpdateWrapper<StoreClockIn> updateWrapper = new LambdaUpdateWrapper<>();
+                updateWrapper.eq(StoreClockIn::getId, clockInId);
+                
+                if (imgAuditResult != null && imgAuditResult.isPassed()) {
+                    // 审核通过,修改状态,打卡可显示在列表中
+                    updateWrapper.set(StoreClockIn::getCheckFlag, 2); // 2-审核通过
+                    updateWrapper.set(StoreClockIn::getReason, null); // 清除拒绝原因
+                    storeClockInMapper.update(null, updateWrapper);
+                    log.info("打卡图片审核通过,打卡ID:{}", clockInId);
+                } else {
+                    // 审核拒绝,修改状态,打卡不可显示在列表中,通知用户,打卡审核不通过
+                    String rejectReason = (imgAuditResult != null && StringUtils.isNotEmpty(imgAuditResult.getFailureReason()))
+                            ? imgAuditResult.getFailureReason()
+                            : "图片内容不符合规范";
+                    updateWrapper.set(StoreClockIn::getCheckFlag, 3); // 2-审核完成(但审核未通过)
+                    updateWrapper.set(StoreClockIn::getReason, rejectReason); // 记录拒绝原因
+                    storeClockInMapper.update(null, updateWrapper);
+                    log.warn("打卡图片审核拒绝,打卡ID:{},原因:{}", clockInId, rejectReason);
+                    
+                    // 通知用户打卡审核不通过
+                    sendAuditRejectNotification(clockInId, userId, phoneId, rejectReason);
+                }
+            } catch (Exception e) {
+                log.error("图片审核接口调用异常,打卡ID:{}", clockInId, e);
+                // 审核异常时,保持审核中状态,记录错误原因
+                LambdaUpdateWrapper<StoreClockIn> updateWrapper = new LambdaUpdateWrapper<>();
+                updateWrapper.eq(StoreClockIn::getId, clockInId);
+                updateWrapper.set(StoreClockIn::getCheckFlag, 1); // 保持审核中状态
+                updateWrapper.set(StoreClockIn::getReason, "审核异常,请稍后重试");
+                storeClockInMapper.update(null, updateWrapper);
+            }
+        }, imgAuditExecutor);
+        
+        return updateResult;
+    }
+
+    /**
+     * 发送审核拒绝通知
+     *
+     * @param clockInId 打卡ID
+     * @param userId    用户ID
+     * @param phoneId   用户phoneId
+     * @param reason    拒绝原因
+     */
+    private void sendAuditRejectNotification(Integer clockInId, Integer userId, String phoneId, String reason) {
+        try {
+            LifeNotice lifeNotice = new LifeNotice();
+            lifeNotice.setSenderId("system");
+            lifeNotice.setBusinessId(clockInId);
+            lifeNotice.setReceiverId(phoneId);
+            lifeNotice.setTitle("打卡审核通知");
+            lifeNotice.setNoticeType(1); // 1-系统通知
+            lifeNotice.setIsRead(0);
+
+            JSONObject jsonObject = new JSONObject();
+            jsonObject.put("message", "您的打卡审核未通过,原因:" + reason);
+            jsonObject.put("clockInId", clockInId);
+            jsonObject.put("status", "rejected");
+            lifeNotice.setContext(jsonObject.toJSONString());
+
+            // 保存通知
+            lifeNoticeMapper.insert(lifeNotice);
+
+            // 发送WebSocket消息
+            WebSocketVo webSocketVo = new WebSocketVo();
+            webSocketVo.setSenderId("system");
+            webSocketVo.setReceiverId(phoneId);
+            webSocketVo.setCategory("notice");
+            webSocketVo.setNoticeType("1");
+            webSocketVo.setIsRead(0);
+            webSocketVo.setText(JSONObject.from(lifeNotice).toJSONString());
+            webSocketProcess.sendMessage(phoneId, JSONObject.from(webSocketVo).toJSONString());
+
+            log.info("打卡审核拒绝通知发送成功,打卡ID:{},接收人:{}", clockInId, phoneId);
+        } catch (Exception e) {
+            log.error("发送打卡审核拒绝通知失败,打卡ID:{},异常信息:{}", clockInId, e.getMessage(), e);
+        }
     }
 
     /**