|
@@ -8,17 +8,25 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
import org.springframework.stereotype.Service;
|
|
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.*;
|
|
|
import shop.alien.entity.store.vo.StoreClockInVo;
|
|
import shop.alien.entity.store.vo.StoreClockInVo;
|
|
|
|
|
+import shop.alien.entity.store.vo.WebSocketVo;
|
|
|
import shop.alien.mapper.*;
|
|
import shop.alien.mapper.*;
|
|
|
|
|
+import shop.alien.store.config.WebSocketProcess;
|
|
|
import shop.alien.store.service.StoreClockInService;
|
|
import shop.alien.store.service.StoreClockInService;
|
|
|
import shop.alien.store.service.StoreCommentService;
|
|
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;
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -29,10 +37,15 @@ import java.util.stream.Collectors;
|
|
|
* @author ssk
|
|
* @author ssk
|
|
|
* @since 2025-05-15
|
|
* @since 2025-05-15
|
|
|
*/
|
|
*/
|
|
|
|
|
+@Slf4j
|
|
|
@Service
|
|
@Service
|
|
|
@RequiredArgsConstructor
|
|
@RequiredArgsConstructor
|
|
|
|
|
+@Transactional(rollbackFor = Exception.class)
|
|
|
public class StoreClockInServiceImpl extends ServiceImpl<StoreClockInMapper, StoreClockIn> implements StoreClockInService {
|
|
public class StoreClockInServiceImpl extends ServiceImpl<StoreClockInMapper, StoreClockIn> implements StoreClockInService {
|
|
|
|
|
|
|
|
|
|
+ // 1. 自定义图片审核线程池(全局复用,避免频繁创建线程)
|
|
|
|
|
+ private ExecutorService imgAuditExecutor;
|
|
|
|
|
+
|
|
|
private final StoreClockInMapper storeClockInMapper;
|
|
private final StoreClockInMapper storeClockInMapper;
|
|
|
|
|
|
|
|
private final LifeUserMapper lifeUserMapper;
|
|
private final LifeUserMapper lifeUserMapper;
|
|
@@ -49,6 +62,62 @@ public class StoreClockInServiceImpl extends ServiceImpl<StoreClockInMapper, Sto
|
|
|
|
|
|
|
|
private final StoreImgMapper storeImgMapper;
|
|
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
|
|
@Override
|
|
|
public StoreClockIn addStoreClockIn(StoreClockIn storeClockIn) {
|
|
public StoreClockIn addStoreClockIn(StoreClockIn storeClockIn) {
|
|
|
LifeUser user = lifeUserMapper.selectById(storeClockIn.getUserId());
|
|
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>()
|
|
IPage<StoreClockInVo> storeClockInIPage1 = storeClockInMapper.getStoreClockInList(iPage, new QueryWrapper<StoreClockIn>()
|
|
|
.eq("clock.store_id", storeId)
|
|
.eq("clock.store_id", storeId)
|
|
|
.eq("clock.delete_flag", 0)
|
|
.eq("clock.delete_flag", 0)
|
|
|
|
|
+ .ne("clock.check_flag", 3)
|
|
|
.eq("user.delete_flag", 0)
|
|
.eq("user.delete_flag", 0)
|
|
|
.eq("store.delete_flag", 0)
|
|
.eq("store.delete_flag", 0)
|
|
|
.isNotNull(0 == mySelf, "clock.img_url")
|
|
.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.eq(1 == mySelf, "clock.user_id", userId);
|
|
|
wrapper.isNotNull(0 == mySelf, "clock.img_url");
|
|
wrapper.isNotNull(0 == mySelf, "clock.img_url");
|
|
|
wrapper.eq("clock.delete_flag", 0);
|
|
wrapper.eq("clock.delete_flag", 0);
|
|
|
|
|
+ wrapper.ne("clock.check_flag", 3);
|
|
|
wrapper.eq("user.delete_flag", 0);
|
|
wrapper.eq("user.delete_flag", 0);
|
|
|
wrapper.eq("store.delete_flag", 0);
|
|
wrapper.eq("store.delete_flag", 0);
|
|
|
wrapper.and(wrapper1 ->
|
|
wrapper.and(wrapper1 ->
|
|
@@ -212,11 +283,129 @@ public class StoreClockInServiceImpl extends ServiceImpl<StoreClockInMapper, Sto
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
|
public int setImgAndAiContent(int id, String img,String aiContent) {
|
|
public int setImgAndAiContent(int id, String img,String aiContent) {
|
|
|
|
|
+ // 1. 先更新打卡状态为"审核中",然后返回
|
|
|
LambdaUpdateWrapper<StoreClockIn> wrapper = new LambdaUpdateWrapper<>();
|
|
LambdaUpdateWrapper<StoreClockIn> wrapper = new LambdaUpdateWrapper<>();
|
|
|
wrapper.set(StoreClockIn::getImgUrl, img);
|
|
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);
|
|
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);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|