|
@@ -27,6 +27,7 @@ import shop.alien.entity.store.vo.WebSocketVo;
|
|
|
import shop.alien.entity.storePlatform.StoreOperationalActivity;
|
|
import shop.alien.entity.storePlatform.StoreOperationalActivity;
|
|
|
import shop.alien.mapper.*;
|
|
import shop.alien.mapper.*;
|
|
|
import shop.alien.mapper.storePlantform.StoreOperationalActivityMapper;
|
|
import shop.alien.mapper.storePlantform.StoreOperationalActivityMapper;
|
|
|
|
|
+import shop.alien.util.common.Constants;
|
|
|
import shop.alien.store.config.WebSocketProcess;
|
|
import shop.alien.store.config.WebSocketProcess;
|
|
|
import shop.alien.store.service.CommonCommentService;
|
|
import shop.alien.store.service.CommonCommentService;
|
|
|
import shop.alien.store.service.CommonRatingService;
|
|
import shop.alien.store.service.CommonRatingService;
|
|
@@ -34,6 +35,7 @@ import shop.alien.store.service.LifeDiscountCouponStoreFriendService;
|
|
|
import shop.alien.store.util.CommonConstant;
|
|
import shop.alien.store.util.CommonConstant;
|
|
|
import shop.alien.store.util.ai.AiContentModerationUtil;
|
|
import shop.alien.store.util.ai.AiContentModerationUtil;
|
|
|
import shop.alien.store.util.ai.AiVideoModerationUtil;
|
|
import shop.alien.store.util.ai.AiVideoModerationUtil;
|
|
|
|
|
+import shop.alien.store.util.ai.ReceiptAuditUtil;
|
|
|
import shop.alien.util.common.DateUtils;
|
|
import shop.alien.util.common.DateUtils;
|
|
|
import shop.alien.util.common.constant.CommentSourceTypeEnum;
|
|
import shop.alien.util.common.constant.CommentSourceTypeEnum;
|
|
|
import shop.alien.util.common.constant.RatingBusinessTypeEnum;
|
|
import shop.alien.util.common.constant.RatingBusinessTypeEnum;
|
|
@@ -84,6 +86,8 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
|
|
|
private final AiVideoModerationUtil aiVideoModerationUtil;
|
|
private final AiVideoModerationUtil aiVideoModerationUtil;
|
|
|
private final LifeDiscountCouponStoreFriendService lifeDiscountCouponStoreFriendService;
|
|
private final LifeDiscountCouponStoreFriendService lifeDiscountCouponStoreFriendService;
|
|
|
private final StoreOperationalActivityMapper storeOperationalActivityMapper;
|
|
private final StoreOperationalActivityMapper storeOperationalActivityMapper;
|
|
|
|
|
+ private final ReceiptAuditUtil receiptAuditUtil;
|
|
|
|
|
+ private final StoreClockInMapper storeClockInMapper;
|
|
|
|
|
|
|
|
public static final List<String> SERVICES_LIST = ImmutableList.of(
|
|
public static final List<String> SERVICES_LIST = ImmutableList.of(
|
|
|
TextReviewServiceEnum.COMMENT_DETECTION_PRO.getService(),
|
|
TextReviewServiceEnum.COMMENT_DETECTION_PRO.getService(),
|
|
@@ -125,8 +129,7 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
|
|
|
commonRating.setScoreThree(parse.getDouble("scoreThree"));
|
|
commonRating.setScoreThree(parse.getDouble("scoreThree"));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 2. 先进行文本和图片审核(同步审核,必须通过才能保存)
|
|
|
|
|
- // 处理imageUrls可能为null的情况
|
|
|
|
|
|
|
+ // 2. 处理图片URL列表
|
|
|
String imageUrlsStr = commonRating.getImageUrls();
|
|
String imageUrlsStr = commonRating.getImageUrls();
|
|
|
List<String> imageUrlList = new ArrayList<>();
|
|
List<String> imageUrlList = new ArrayList<>();
|
|
|
if (StringUtils.isNotEmpty(imageUrlsStr)) {
|
|
if (StringUtils.isNotEmpty(imageUrlsStr)) {
|
|
@@ -135,18 +138,113 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
|
|
|
|
|
|
|
|
// 一次遍历完成分类,避免多次流式处理
|
|
// 一次遍历完成分类,避免多次流式处理
|
|
|
Map<String, List<String>> urlCategoryMap = StoreRenovationRequirementServiceImpl.classifyUrls(imageUrlList);
|
|
Map<String, List<String>> urlCategoryMap = StoreRenovationRequirementServiceImpl.classifyUrls(imageUrlList);
|
|
|
|
|
+ List<String> imageUrls = urlCategoryMap.get("image");
|
|
|
|
|
+ List<String> videoUrls = urlCategoryMap.get("video");
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 内容审核(基础审核,必须通过)
|
|
|
|
|
+ // 文本、图片、视频必须符合法律法规要求
|
|
|
|
|
+ AiContentModerationUtil.AuditResult contentAuditResult = new AiContentModerationUtil.AuditResult(true, "");
|
|
|
|
|
+ // 只要有内容或图片,就必须进行内容审核
|
|
|
|
|
+ if (StringUtils.isNotEmpty(commonRating.getContent()) || !imageUrls.isEmpty()) {
|
|
|
|
|
+ contentAuditResult = aiContentModerationUtil.auditContent(commonRating.getContent(), imageUrls);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 内容审核不通过,直接判定为审核不通过
|
|
|
|
|
+ if (!contentAuditResult.isPassed()) {
|
|
|
|
|
+ log.warn("评论审核失败:内容审核不通过,不保存评论。原因:{}", contentAuditResult.getFailureReason());
|
|
|
|
|
+ return 2; // 返回2表示内容审核不通过
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ log.info("评论审核:内容审核通过,继续检查其他条件");
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 内容审核通过后,检查是否满足以下条件之一(满足任意一个即可):
|
|
|
|
|
+ // 条件1:当前用户必须在这个店铺打过卡
|
|
|
|
|
+ // 条件2:当前用户上传的图片中包含该店铺的小票(调用AI审核)
|
|
|
|
|
+ // 注意:小票审核和打卡审核不通过时,允许创建评论,但审核状态设为不通过并记录审核原因
|
|
|
|
|
+
|
|
|
|
|
+ boolean condition1Passed = false; // 条件1:打过卡
|
|
|
|
|
+ boolean condition2Passed = false; // 条件2:有小票
|
|
|
|
|
+ String auditReason = null; // 审核原因
|
|
|
|
|
+
|
|
|
|
|
+ // 检查条件1:用户是否在该店铺打过卡
|
|
|
|
|
+ try {
|
|
|
|
|
+ LambdaQueryWrapper<StoreClockIn> clockInWrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+ clockInWrapper.eq(StoreClockIn::getUserId, commonRating.getUserId())
|
|
|
|
|
+ .eq(StoreClockIn::getStoreId, commonRating.getBusinessId())
|
|
|
|
|
+ .eq(StoreClockIn::getDeleteFlag, 0)
|
|
|
|
|
+ .last("LIMIT 1");
|
|
|
|
|
+ Integer clockInCount = storeClockInMapper.selectCount(clockInWrapper);
|
|
|
|
|
+ condition1Passed = clockInCount != null && clockInCount > 0;
|
|
|
|
|
+ if (condition1Passed) {
|
|
|
|
|
+ log.info("评论审核条件1通过:用户在该店铺打过卡,userId={}, storeId={}",
|
|
|
|
|
+ commonRating.getUserId(), commonRating.getBusinessId());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 打卡审核不通过,记录审核原因
|
|
|
|
|
+ auditReason = "用户未在该店铺打过卡";
|
|
|
|
|
+ log.info("评论审核条件1未通过:用户未在该店铺打过卡,userId={}, storeId={}",
|
|
|
|
|
+ commonRating.getUserId(), commonRating.getBusinessId());
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("检查用户打卡记录失败:userId={}, storeId={}",
|
|
|
|
|
+ commonRating.getUserId(), commonRating.getBusinessId(), e);
|
|
|
|
|
+ // 检查异常时,也记录为未通过
|
|
|
|
|
+ if (auditReason == null) {
|
|
|
|
|
+ auditReason = "检查用户打卡记录失败";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 3. 文本和图片审核(同步,必须通过)
|
|
|
|
|
- AiContentModerationUtil.AuditResult auditResult = new AiContentModerationUtil.AuditResult(true, "");
|
|
|
|
|
- // 只要有内容或图片,就必须审核
|
|
|
|
|
- if (StringUtils.isNotEmpty(commonRating.getContent()) || !urlCategoryMap.get("image").isEmpty()) {
|
|
|
|
|
- auditResult = aiContentModerationUtil.auditContent(commonRating.getContent(), urlCategoryMap.get("image"));
|
|
|
|
|
|
|
+ // 检查条件2:图片中包含该店铺的小票(如果有图片)
|
|
|
|
|
+ if (!condition1Passed && !imageUrls.isEmpty()) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 获取店铺名称用于小票审核
|
|
|
|
|
+ String storeName = null;
|
|
|
|
|
+ StoreInfo storeInfo = storeInfoMapper.selectById(commonRating.getBusinessId());
|
|
|
|
|
+ if (storeInfo != null && StringUtils.isNotEmpty(storeInfo.getStoreName())) {
|
|
|
|
|
+ storeName = storeInfo.getStoreName();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 调用小票审核接口
|
|
|
|
|
+ ReceiptAuditUtil.ReceiptAuditResult receiptAuditResult = receiptAuditUtil.auditReceipt(imageUrls, storeName);
|
|
|
|
|
+ condition2Passed = receiptAuditResult.isPassed();
|
|
|
|
|
+
|
|
|
|
|
+ if (condition2Passed) {
|
|
|
|
|
+ log.info("评论审核条件2通过:图片中包含该店铺的小票,isValidProof={}, proofType={}",
|
|
|
|
|
+ receiptAuditResult.isValidProof(), receiptAuditResult.getProofType());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 小票审核不通过,记录审核原因
|
|
|
|
|
+ String receiptReason = receiptAuditResult.getFailureReason();
|
|
|
|
|
+ if (StringUtils.isNotEmpty(receiptReason)) {
|
|
|
|
|
+ auditReason = "小票审核不通过:" + receiptReason;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ auditReason = "小票审核不通过:上传的图片不是有效的消费凭证(小票、支付记录等)";
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info("评论审核条件2未通过:小票审核失败,reason={}", receiptReason);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("小票审核异常:userId={}, storeId={}",
|
|
|
|
|
+ commonRating.getUserId(), commonRating.getBusinessId(), e);
|
|
|
|
|
+ // 小票审核异常时,也记录为未通过
|
|
|
|
|
+ if (auditReason == null) {
|
|
|
|
|
+ auditReason = "小票审核异常";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 4. 文本/图片审核不通过,直接返回,不保存
|
|
|
|
|
- if (!auditResult.isPassed()) {
|
|
|
|
|
- log.warn("评价审核不通过,不保存评论。原因:{}", auditResult.getFailureReason());
|
|
|
|
|
- return 2; // 返回2表示文本内容异常
|
|
|
|
|
|
|
+ // 如果两个条件都不满足,设置审核状态为不通过并记录审核原因
|
|
|
|
|
+ if (!condition1Passed && !condition2Passed) {
|
|
|
|
|
+ // 如果还没有审核原因,设置默认原因
|
|
|
|
|
+ if (auditReason == null) {
|
|
|
|
|
+ auditReason = "不满足审核条件:1) 用户未在该店铺打过卡;2) 上传的图片不包含该店铺的小票";
|
|
|
|
|
+ }
|
|
|
|
|
+ // 设置审核状态为不通过(2-驳回)
|
|
|
|
|
+ commonRating.setAuditStatus(2);
|
|
|
|
|
+ commonRating.setAuditReason(auditReason);
|
|
|
|
|
+ log.warn("评论审核不通过:内容审核通过,但不满足其他条件。条件1(打过卡)={}, 条件2(有小票)={}, 审核原因={}",
|
|
|
|
|
+ condition1Passed, condition2Passed, auditReason);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 满足条件,审核状态保持为待审核(0),等待视频审核
|
|
|
|
|
+ log.info("评论审核通过:内容审核通过,且满足条件1(打过卡)={} 或 条件2(有小票)={}",
|
|
|
|
|
+ condition1Passed, condition2Passed);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 5. 文本/图片审核通过,保存评论(状态为待审核,等待视频审核)
|
|
// 5. 文本/图片审核通过,保存评论(状态为待审核,等待视频审核)
|
|
@@ -156,8 +254,54 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
|
|
|
return 1;
|
|
return 1;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 6. 如果有视频,进行异步视频审核
|
|
|
|
|
- List<String> videoUrls = urlCategoryMap.get("video");
|
|
|
|
|
|
|
+ // 6. 如果审核不通过(小票审核或打卡审核不通过),发送websocket通知给用户
|
|
|
|
|
+ if (commonRating.getAuditStatus() != null && commonRating.getAuditStatus() == 2 && StringUtils.isNotEmpty(auditReason)) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 获取用户信息
|
|
|
|
|
+ LifeUser lifeUser = lifeUserMapper.selectById(commonRating.getUserId());
|
|
|
|
|
+ if (lifeUser != null && StringUtils.isNotEmpty(lifeUser.getUserPhone())) {
|
|
|
|
|
+ String receiverId = "user_" + lifeUser.getUserPhone();
|
|
|
|
|
+
|
|
|
|
|
+ // 构建通知内容
|
|
|
|
|
+ JSONObject contextJson = new JSONObject();
|
|
|
|
|
+ contextJson.put("ratingId", commonRating.getId());
|
|
|
|
|
+ contextJson.put("storeId", commonRating.getBusinessId());
|
|
|
|
|
+ contextJson.put("message", "您的评价审核未通过:" + auditReason);
|
|
|
|
|
+ contextJson.put("auditReason", auditReason);
|
|
|
|
|
+
|
|
|
|
|
+ // 保存通知到数据库
|
|
|
|
|
+ LifeNotice lifeNotice = new LifeNotice();
|
|
|
|
|
+ lifeNotice.setSenderId("system");
|
|
|
|
|
+ lifeNotice.setReceiverId(receiverId);
|
|
|
|
|
+ lifeNotice.setBusinessId(commonRating.getBusinessId());
|
|
|
|
|
+ lifeNotice.setTitle("评价审核通知");
|
|
|
|
|
+ lifeNotice.setContext(contextJson.toJSONString());
|
|
|
|
|
+ lifeNotice.setNoticeType(Constants.Notice.SYSTEM_NOTICE); // 系统通知
|
|
|
|
|
+ lifeNotice.setIsRead(0);
|
|
|
|
|
+ lifeNoticeMapper.insert(lifeNotice);
|
|
|
|
|
+
|
|
|
|
|
+ // 通过WebSocket发送实时通知
|
|
|
|
|
+ WebSocketVo webSocketVo = new WebSocketVo();
|
|
|
|
|
+ webSocketVo.setSenderId("system");
|
|
|
|
|
+ webSocketVo.setReceiverId(receiverId);
|
|
|
|
|
+ webSocketVo.setCategory("notice");
|
|
|
|
|
+ webSocketVo.setNoticeType("1");
|
|
|
|
|
+ webSocketVo.setIsRead(0);
|
|
|
|
|
+ webSocketVo.setText(JSONObject.from(lifeNotice).toJSONString());
|
|
|
|
|
+
|
|
|
|
|
+ webSocketProcess.sendMessage(receiverId, JSONObject.from(webSocketVo).toJSONString());
|
|
|
|
|
+ log.info("评价审核不通过通知发送成功,ratingId={}, receiverId={}, auditReason={}",
|
|
|
|
|
+ commonRating.getId(), receiverId, auditReason);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.warn("无法发送评价审核通知:用户信息不存在或手机号为空,userId={}", commonRating.getUserId());
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("发送评价审核不通过通知失败,ratingId={}, error={}",
|
|
|
|
|
+ commonRating.getId(), e.getMessage(), e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 7. 如果有视频,进行异步视频审核
|
|
|
if (!videoUrls.isEmpty()) {
|
|
if (!videoUrls.isEmpty()) {
|
|
|
CompletableFuture.runAsync(() -> {
|
|
CompletableFuture.runAsync(() -> {
|
|
|
AiVideoModerationUtil.VideoAuditResult videoAuditResult = null;
|
|
AiVideoModerationUtil.VideoAuditResult videoAuditResult = null;
|