|
|
@@ -19,14 +19,10 @@ import shop.alien.mapper.*;
|
|
|
import shop.alien.store.config.WebSocketProcess;
|
|
|
import shop.alien.store.service.StoreRenovationRequirementService;
|
|
|
import shop.alien.store.util.ai.AiContentModerationUtil;
|
|
|
-import shop.alien.store.util.ai.AiVideoModerationUtil;
|
|
|
import shop.alien.util.common.JwtUtil;
|
|
|
|
|
|
-import javax.annotation.PostConstruct;
|
|
|
-import javax.annotation.PreDestroy;
|
|
|
import java.text.SimpleDateFormat;
|
|
|
import java.util.*;
|
|
|
-import java.util.concurrent.*;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
/**
|
|
|
@@ -41,9 +37,6 @@ import java.util.stream.Collectors;
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
public class StoreRenovationRequirementServiceImpl extends ServiceImpl<StoreRenovationRequirementMapper, StoreRenovationRequirement> implements StoreRenovationRequirementService {
|
|
|
|
|
|
- // 1. 自定义视频审核线程池(全局复用,避免频繁创建线程)
|
|
|
- private ExecutorService videoAuditExecutor;
|
|
|
-
|
|
|
private final StoreInfoMapper storeInfoMapper;
|
|
|
|
|
|
private final LifeMessageMapper lifeMessageMapper;
|
|
|
@@ -55,7 +48,7 @@ public class StoreRenovationRequirementServiceImpl extends ServiceImpl<StoreReno
|
|
|
private final StoreUserMapper storeUserMapper;
|
|
|
|
|
|
private final AiContentModerationUtil aiContentModerationUtil;
|
|
|
- private final AiVideoModerationUtil aiVideoModerationUtil;
|
|
|
+ // 供 CommonRatingServiceImpl 等复用的 URL 分类;本服务保存需求时不再对图/视频做审核
|
|
|
// 定义图片后缀常量(不可修改,保证线程安全)
|
|
|
private static final Set<String> IMAGE_SUFFIXES = Collections.unmodifiableSet(
|
|
|
new HashSet<>(Arrays.asList(".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"))
|
|
|
@@ -66,55 +59,6 @@ public class StoreRenovationRequirementServiceImpl extends ServiceImpl<StoreReno
|
|
|
new HashSet<>(Arrays.asList(".mp4", ".avi", ".mov", ".flv", ".mkv", ".wmv", ".mpeg"))
|
|
|
);
|
|
|
|
|
|
- // 初始化线程池
|
|
|
- @PostConstruct
|
|
|
- public void initExecutor() {
|
|
|
- // 核心参数根据业务调整,IO密集型任务线程数可设为CPU核心数*2~4
|
|
|
- int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
|
|
|
- int maxPoolSize = Runtime.getRuntime().availableProcessors() * 4;
|
|
|
- videoAuditExecutor = 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("video-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(videoAuditExecutor)) {
|
|
|
- videoAuditExecutor.shutdown();
|
|
|
- try {
|
|
|
- if (!videoAuditExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
|
|
|
- videoAuditExecutor.shutdownNow();
|
|
|
- }
|
|
|
- } catch (InterruptedException e) {
|
|
|
- videoAuditExecutor.shutdownNow();
|
|
|
- Thread.currentThread().interrupt();
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
@Override
|
|
|
public boolean saveOrUpdateRequirement(StoreRenovationRequirementDto dto) {
|
|
|
log.info("StoreRenovationRequirementServiceImpl.saveOrUpdateRequirement?dto={}", dto);
|
|
|
@@ -183,89 +127,27 @@ public class StoreRenovationRequirementServiceImpl extends ServiceImpl<StoreReno
|
|
|
websocketVo.setText(JSONObject.from(lifeMessage).toJSONString());
|
|
|
webSocketProcess.sendMessage("store_" + storeUser.getPhone(), JSONObject.from(websocketVo).toJSONString());
|
|
|
|
|
|
-
|
|
|
- // 附件图/视频由前端直连 AI;服务端仅审 detailedRequirement 文本(图片 URL 在 util 内不传 moderate)
|
|
|
- Map<String, List<String>> urlCategoryMap = classifyUrls(dto.getAttachmentUrls());
|
|
|
- List<String> videoUrls = urlCategoryMap.get("video");
|
|
|
- AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(requirement.getDetailedRequirement(), null);
|
|
|
- if (!auditResult.isPassed()) {
|
|
|
- requirement.setAuditStatus(2);
|
|
|
- requirement.setAuditReason(auditResult.getFailureReason());
|
|
|
- // 发送审核失败通知(文本图片)
|
|
|
+ // 仅审核 detailedRequirement 文本;无文字则整体直接审核通过;附件原样存 attachment_urls
|
|
|
+ if (!StringUtils.hasText(requirement.getDetailedRequirement())) {
|
|
|
+ requirement.setAuditStatus(1);
|
|
|
+ requirement.setAuditReason(null);
|
|
|
if (storeUser != null && storeUser.getPhone() != null) {
|
|
|
- sendAuditNotification(storeUser.getPhone(), 2, auditResult.getFailureReason(), "text_image");
|
|
|
+ sendAuditNotification(storeUser.getPhone(), 1, null);
|
|
|
}
|
|
|
-
|
|
|
} else {
|
|
|
- // 如果文本/图片审核通过,且没有视频需要审核,直接设置为审核通过
|
|
|
- if (videoUrls == null || videoUrls.isEmpty()) {
|
|
|
- requirement.setAuditStatus(1);
|
|
|
- requirement.setAuditReason(null);
|
|
|
- // 发送审核通过通知(文本图片,无视频)
|
|
|
+ AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(requirement.getDetailedRequirement(), null);
|
|
|
+ if (!auditResult.isPassed()) {
|
|
|
+ requirement.setAuditStatus(2);
|
|
|
+ requirement.setAuditReason(auditResult.getFailureReason());
|
|
|
if (storeUser != null && storeUser.getPhone() != null) {
|
|
|
- sendAuditNotification(storeUser.getPhone(), 1, null, "text_image");
|
|
|
+ sendAuditNotification(storeUser.getPhone(), 2, auditResult.getFailureReason());
|
|
|
}
|
|
|
} else {
|
|
|
- // 有视频时走异步分支;视频不在服务端审(前端已审),auditVideos 恒通过
|
|
|
- // 保存storeUser信息供异步任务使用
|
|
|
- final StoreUser finalStoreUser = storeUser;
|
|
|
- CompletableFuture.runAsync(() -> {
|
|
|
- AiVideoModerationUtil.VideoAuditResult videoAuditResult = null;
|
|
|
- try {
|
|
|
- // 调用审核接口,增加超时控制(避免接口挂死)
|
|
|
- videoAuditResult = CompletableFuture.supplyAsync(
|
|
|
- () -> aiVideoModerationUtil.auditVideos(urlCategoryMap.get("video")),
|
|
|
- videoAuditExecutor
|
|
|
- ).get();
|
|
|
-
|
|
|
- // 审核不通过则更新状态和原因
|
|
|
- if (Objects.nonNull(videoAuditResult) && !videoAuditResult.isPassed()) {
|
|
|
- // 重新查询最新的requirement,避免并发覆盖
|
|
|
- StoreRenovationRequirement latestRequirement = this.getById(requirement.getId());
|
|
|
- if (Objects.isNull(latestRequirement)) {
|
|
|
- log.error("视频审核后更新失败,requirement不存在,ID:{}", requirement.getId());
|
|
|
- return;
|
|
|
- }
|
|
|
- latestRequirement.setAuditStatus(2);
|
|
|
- latestRequirement.setAuditReason(videoAuditResult.getFailureReason());
|
|
|
- this.saveOrUpdate(latestRequirement);
|
|
|
- log.info("视频审核不通过,已更新状态,requirementID:{},原因:{}",
|
|
|
- requirement.getId(), videoAuditResult.getFailureReason());
|
|
|
- // 发送审核不通过通知(视频)
|
|
|
- if (finalStoreUser != null && finalStoreUser.getPhone() != null) {
|
|
|
- sendAuditNotification(finalStoreUser.getPhone(), 2, videoAuditResult.getFailureReason(), "video");
|
|
|
- }
|
|
|
-
|
|
|
- } else if (Objects.nonNull(videoAuditResult) && videoAuditResult.isPassed()) {
|
|
|
- // 审核通过也更新状态(可选,根据业务需求)
|
|
|
- StoreRenovationRequirement latestRequirement = this.getById(requirement.getId());
|
|
|
- if (Objects.nonNull(latestRequirement)) {
|
|
|
- latestRequirement.setAuditStatus(1);
|
|
|
- latestRequirement.setAuditReason(null);
|
|
|
- this.saveOrUpdate(latestRequirement);
|
|
|
- // 发送审核通过通知(视频)
|
|
|
- if (finalStoreUser != null && finalStoreUser.getPhone() != null) {
|
|
|
- sendAuditNotification(finalStoreUser.getPhone(), 1, null, "video");
|
|
|
- }
|
|
|
-
|
|
|
- log.info("视频审核通过,已更新状态,requirementID:{}", requirement.getId());
|
|
|
- }
|
|
|
- }
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("视频审核接口调用异常,requirementID:{}", requirement.getId(), e);
|
|
|
- StoreRenovationRequirement latestRequirement = this.getById(requirement.getId());
|
|
|
- if (Objects.nonNull(latestRequirement)) {
|
|
|
- String exceptionMessage = "视频审核接口调用异常:" + e.getMessage();
|
|
|
- latestRequirement.setAuditStatus(2);
|
|
|
- latestRequirement.setAuditReason(exceptionMessage);
|
|
|
- this.saveOrUpdate(latestRequirement);
|
|
|
- // 发送审核异常通知(视频)
|
|
|
- if (finalStoreUser != null && finalStoreUser.getPhone() != null) {
|
|
|
- sendAuditNotification(finalStoreUser.getPhone(), 2, exceptionMessage, "video");
|
|
|
- }
|
|
|
- }
|
|
|
+ requirement.setAuditStatus(1);
|
|
|
+ requirement.setAuditReason(null);
|
|
|
+ if (storeUser != null && storeUser.getPhone() != null) {
|
|
|
+ sendAuditNotification(storeUser.getPhone(), 1, null);
|
|
|
}
|
|
|
- }, videoAuditExecutor);
|
|
|
}
|
|
|
}
|
|
|
requirement.setAuditTime(new Date());
|
|
|
@@ -332,43 +214,21 @@ public class StoreRenovationRequirementServiceImpl extends ServiceImpl<StoreReno
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 发送审核结果通知
|
|
|
- * @param storePhone 商铺电话
|
|
|
- * @param auditStatus 审核状态:1-通过,2-不通过
|
|
|
- * @param auditReason 审核原因(不通过时提供)
|
|
|
- * @param auditType 审核类型:text_image-文本图片审核,video-视频审核
|
|
|
+ * 发送文本审核结果通知(图/视频不参与审核,仅与需求一并保存)
|
|
|
*/
|
|
|
- private void sendAuditNotification(String storePhone, Integer auditStatus, String auditReason, String auditType) {
|
|
|
+ private void sendAuditNotification(String storePhone, Integer auditStatus, String auditReason) {
|
|
|
try {
|
|
|
if (storePhone == null || storePhone.trim().isEmpty()) {
|
|
|
log.warn("发送审核通知失败,商铺电话为空");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");
|
|
|
- String commonDate = sdf.format(new Date());
|
|
|
-
|
|
|
- String title;
|
|
|
+ String title = "审核通知";
|
|
|
String message;
|
|
|
if (auditStatus == 1) {
|
|
|
- // 审核通过
|
|
|
- title = "审核通知";
|
|
|
- if ("video".equals(auditType)) {
|
|
|
- message = "在" + commonDate + ",您发布的装修动态视频审核已通过。";
|
|
|
- } else {
|
|
|
- message = "您发布的装修需求已通过审核,平台已将此需求发布,有意向的装修公司会与您联系";
|
|
|
- }
|
|
|
+ message = "您发布的装修需求已通过审核,平台已将此需求发布,有意向的装修公司会与您联系";
|
|
|
} else {
|
|
|
- // 审核不通过
|
|
|
- title = "审核通知";
|
|
|
-// String reasonText = auditReason != null && !auditReason.trim().isEmpty()
|
|
|
-// ? ",原因:" + auditReason
|
|
|
-// : "";
|
|
|
- if ("video".equals(auditType)) {
|
|
|
- message = "在" + commonDate + ",您发布的装修动态视频审核未通过" + auditReason + ",请修改后重新提交。";
|
|
|
- } else {
|
|
|
- message = "您发布的装修需求未通过审核.驳回原因:"+auditReason+"请您重新发布.";
|
|
|
- }
|
|
|
+ message = "您发布的装修需求未通过审核.驳回原因:" + (auditReason != null ? auditReason : "") + "请您重新发布.";
|
|
|
}
|
|
|
|
|
|
LifeNotice lifeNotice = new LifeNotice();
|
|
|
@@ -391,9 +251,9 @@ public class StoreRenovationRequirementServiceImpl extends ServiceImpl<StoreReno
|
|
|
websocketVo.setText(JSONObject.from(lifeNotice).toJSONString());
|
|
|
webSocketProcess.sendMessage("store_" + storePhone, JSONObject.from(websocketVo).toJSONString());
|
|
|
|
|
|
- log.info("审核通知发送成功,商铺电话:{},审核状态:{},审核类型:{}", storePhone, auditStatus, auditType);
|
|
|
+ log.info("审核通知发送成功,商铺电话:{},审核状态:{}", storePhone, auditStatus);
|
|
|
} catch (Exception e) {
|
|
|
- log.error("发送审核通知异常,商铺电话:{},审核状态:{},审核类型:{}", storePhone, auditStatus, auditType, e);
|
|
|
+ log.error("发送审核通知异常,商铺电话:{},审核状态:{}", storePhone, auditStatus, e);
|
|
|
}
|
|
|
}
|
|
|
|