|
@@ -0,0 +1,155 @@
|
|
|
|
|
+package shop.alien.store.aspect;
|
|
|
|
|
+
|
|
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.aspectj.lang.ProceedingJoinPoint;
|
|
|
|
|
+import org.aspectj.lang.annotation.Around;
|
|
|
|
|
+import org.aspectj.lang.annotation.Aspect;
|
|
|
|
|
+import org.aspectj.lang.annotation.Pointcut;
|
|
|
|
|
+import org.springframework.core.annotation.Order;
|
|
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
|
|
+import shop.alien.entity.store.StorePrice;
|
|
|
|
|
+import shop.alien.entity.store.dto.CuisineComboDto;
|
|
|
|
|
+import shop.alien.store.util.ai.AiContentModerationUtil;
|
|
|
|
|
+
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * AI 审核切面:拦截美食与通用价目新增/修改接口
|
|
|
|
|
+ */
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Aspect
|
|
|
|
|
+@Component
|
|
|
|
|
+@Order(2)
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
|
|
+public class AiAuditAspect {
|
|
|
|
|
+
|
|
|
|
|
+ private final AiContentModerationUtil aiContentModerationUtil;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 仅拦截美食新增/修改与通用价目新增/修改接口
|
|
|
|
|
+ */
|
|
|
|
|
+ @Pointcut(
|
|
|
|
|
+ "execution(* shop.alien.store.controller.StoreCuisineController.addCuisineCombo(..))"
|
|
|
|
|
+ + " || execution(* shop.alien.store.controller.StoreCuisineController.updateCuisineCombo(..))"
|
|
|
|
|
+ + " || execution(* shop.alien.store.controller.StorePriceController.save(..))"
|
|
|
|
|
+ + " || execution(* shop.alien.store.controller.StorePriceController.update(..))")
|
|
|
|
|
+ public void aiAuditPointcut() {
|
|
|
|
|
+ // pointcut definition
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 环绕通知:组合请求参数,获取AI token,并设置审核状态
|
|
|
|
|
+ */
|
|
|
|
|
+ @Around("aiAuditPointcut()")
|
|
|
|
|
+ public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
|
|
|
|
|
+ Object[] args = joinPoint.getArgs();
|
|
|
|
|
+
|
|
|
|
|
+ String payload = JSON.toJSONString(args);
|
|
|
|
|
+ log.info("AI审核切面拦截方法: {}, payload={}", joinPoint.getSignature().toShortString(), payload);
|
|
|
|
|
+
|
|
|
|
|
+ List<String> imageUrls = extractImageUrls(args);
|
|
|
|
|
+
|
|
|
|
|
+ // AI审核:文本采用入参JSON,图片列表来自入参的 images 字符串字段
|
|
|
|
|
+ boolean auditPassed = performAiAudit( payload, imageUrls, args);
|
|
|
|
|
+
|
|
|
|
|
+ // 将审核结果写入请求对象
|
|
|
|
|
+ applyStatus(args, auditPassed);
|
|
|
|
|
+
|
|
|
|
|
+ return joinPoint.proceed(args);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * AI审核调用:使用文本审核(图片列表暂为空)
|
|
|
|
|
+ */
|
|
|
|
|
+ private boolean performAiAudit(String payload, List<String> imageUrls, Object[] args) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // token 目前仅预留,如后续需要可添加到header或payload
|
|
|
|
|
+ AiContentModerationUtil.AuditResult result = aiContentModerationUtil.auditContent(payload, imageUrls);
|
|
|
|
|
+ if (result == null) {
|
|
|
|
|
+ log.warn("AI审核返回为空,视为未通过");
|
|
|
|
|
+ applyFailureReason(args, "审核异常");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ boolean passed = result.isPassed();
|
|
|
|
|
+ if (!passed) {
|
|
|
|
|
+ String reason = result.getFailureReason();
|
|
|
|
|
+ log.warn("AI审核不通过,原因: {}", reason);
|
|
|
|
|
+ applyFailureReason(args, reason);
|
|
|
|
|
+ }
|
|
|
|
|
+ return passed;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("AI审核调用异常", e);
|
|
|
|
|
+ applyFailureReason(args, "审核异常");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将审核结果写入入参的status字段(通过=1,不通过=0)
|
|
|
|
|
+ */
|
|
|
|
|
+ private void applyStatus(Object[] args, boolean passed) {
|
|
|
|
|
+ int status = passed ? 1 : 0;
|
|
|
|
|
+ for (Object arg : args) {
|
|
|
|
|
+ if (arg == null) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (arg instanceof CuisineComboDto) {
|
|
|
|
|
+ ((CuisineComboDto) arg).setStatus(status);
|
|
|
|
|
+ } else if (arg instanceof StorePrice) {
|
|
|
|
|
+ ((StorePrice) arg).setStatus(status);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将审核失败原因写入入参
|
|
|
|
|
+ */
|
|
|
|
|
+ private void applyFailureReason(Object[] args, String reason) {
|
|
|
|
|
+ String safeReason = reason != null ? reason : "审核未通过";
|
|
|
|
|
+ for (Object arg : args) {
|
|
|
|
|
+ if (arg == null) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (arg instanceof CuisineComboDto) {
|
|
|
|
|
+ ((CuisineComboDto) arg).setRejectionReason(safeReason);
|
|
|
|
|
+ } else if (arg instanceof StorePrice) {
|
|
|
|
|
+ ((StorePrice) arg).setRejectionReason(safeReason);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从入参中提取图片URL列表;images字段为JSON字符串数组
|
|
|
|
|
+ */
|
|
|
|
|
+ private List<String> extractImageUrls(Object[] args) {
|
|
|
|
|
+ for (Object arg : args) {
|
|
|
|
|
+ if (arg instanceof CuisineComboDto) {
|
|
|
|
|
+ return parseImages(((CuisineComboDto) arg).getImages());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (arg instanceof StorePrice) {
|
|
|
|
|
+ return parseImages(((StorePrice) arg).getImages());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return Collections.emptyList();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<String> parseImages(String images) {
|
|
|
|
|
+ if (images == null || images.trim().isEmpty()) {
|
|
|
|
|
+ return Collections.emptyList();
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 期望格式为 JSON 数组字符串
|
|
|
|
|
+ return JSON.parseArray(images, String.class);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("解析图片列表失败,images={}", images, e);
|
|
|
|
|
+ List<String> single = new ArrayList<>();
|
|
|
|
|
+ single.add(images);
|
|
|
|
|
+ return single;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|