Kaynağa Gözat

Merge branch 'sit-three-categories' of http://8.152.195.41:3000/alien/alien_cloud into sit-three-categories

zc 3 ay önce
ebeveyn
işleme
02e8d65a44

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

@@ -82,7 +82,7 @@ public class StoreCuisine {
     @TableField("shelf_status")
     private Integer shelfStatus;
 
-    @ApiModelProperty(value = "拒绝原因")
+    @ApiModelProperty(value = "拒绝原因(审核失败原因)")
     @TableField("rejection_reason")
     private String rejectionReason;
 

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

@@ -83,7 +83,7 @@ public class StorePrice {
     @TableField("shelf_status")
     private Integer shelfStatus;
 
-    @ApiModelProperty(value = "拒绝原因")
+    @ApiModelProperty(value = "拒绝原因(审核失败原因)")
     @TableField("rejection_reason")
     private String rejectionReason;
 

+ 20 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/AiQuestionClassificationRequestDto.java

@@ -0,0 +1,20 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 问题分类请求DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "QuestionClassificationRequestDto对象", description = "问题分类请求DTO")
+public class AiQuestionClassificationRequestDto {
+
+    @ApiModelProperty(value = "问题文本", required = true, example = "我想了解一下这个产品的使用方法")
+    private String question;
+}
+

+ 8 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/CuisineComboDto.java

@@ -71,4 +71,12 @@ public class CuisineComboDto {
     @ApiModelProperty(value = "使用规则")
     @TableField("usage_rule")
     private String usageRule;
+
+    @ApiModelProperty(value = "状态:0-待审核 1-审核通过 2-审核拒绝")
+    @TableField("status")
+    private Integer status;
+
+    @ApiModelProperty(value = "拒绝原因(审核失败原因)")
+    @TableField("rejection_reason")
+    private String rejectionReason;
 }

+ 155 - 0
alien-store/src/main/java/shop/alien/store/aspect/AiAuditAspect.java

@@ -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;
+        }
+    }
+}
+

+ 64 - 0
alien-store/src/main/java/shop/alien/store/controller/AiQuestionClassificationController.java

@@ -0,0 +1,64 @@
+package shop.alien.store.controller;
+
+import com.alibaba.fastjson2.JSONObject;
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.store.dto.AiQuestionClassificationRequestDto;
+import shop.alien.store.service.AiQuestionClassificationService;
+
+/**
+ * 问题分类控制器
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Api(tags = {"AI问题分类"})
+@CrossOrigin
+@RestController
+@RequestMapping("/questionClassification")
+@RequiredArgsConstructor
+@RefreshScope
+public class AiQuestionClassificationController {
+
+    private final AiQuestionClassificationService questionClassificationService;
+
+    /**
+     * 问题分类接口
+     *
+     * @param requestDto 问题分类请求DTO
+     * @return AI接口返回的原始响应
+     */
+    @ApiOperation(value = "问题分类", notes = "对用户问题进行AI分类")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/classify")
+    public ResponseEntity<String> classify(@RequestBody AiQuestionClassificationRequestDto requestDto) {
+        log.info("收到问题分类请求,问题:{}", requestDto != null ? requestDto.getQuestion() : null);
+        
+        // 参数校验
+        if (requestDto == null || !StringUtils.hasText(requestDto.getQuestion())) {
+            log.warn("问题分类请求参数为空或问题文本为空");
+            JSONObject errorResponse = new JSONObject();
+            errorResponse.put("code", 400);
+            errorResponse.put("message", "问题文本不能为空");
+            return ResponseEntity.badRequest().body(errorResponse.toJSONString());
+        }
+        
+        try {
+            String result = questionClassificationService.classify(requestDto);
+            return ResponseEntity.ok(result);
+        } catch (Exception e) {
+            log.error("问题分类失败,问题:{}", requestDto.getQuestion(), e);
+            JSONObject errorResponse = new JSONObject();
+            errorResponse.put("code", 500);
+            errorResponse.put("message", "问题分类失败:" + e.getMessage());
+            return ResponseEntity.status(500).body(errorResponse.toJSONString());
+        }
+    }
+}
+

+ 21 - 0
alien-store/src/main/java/shop/alien/store/service/AiQuestionClassificationService.java

@@ -0,0 +1,21 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.dto.AiQuestionClassificationRequestDto;
+
+/**
+ * 问题分类服务接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface AiQuestionClassificationService {
+
+    /**
+     * 对问题进行分类
+     *
+     * @param requestDto 问题分类请求DTO
+     * @return AI接口返回的JSON字符串
+     */
+    String classify(AiQuestionClassificationRequestDto requestDto);
+}
+

+ 88 - 0
alien-store/src/main/java/shop/alien/store/service/impl/AiQuestionClassificationServiceImpl.java

@@ -0,0 +1,88 @@
+package shop.alien.store.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.HttpServerErrorException;
+import org.springframework.web.client.RestTemplate;
+import shop.alien.entity.store.dto.AiQuestionClassificationRequestDto;
+import shop.alien.store.service.AiQuestionClassificationService;
+import shop.alien.store.util.ai.AiAuthTokenUtil;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 问题分类服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+@RefreshScope
+public class AiQuestionClassificationServiceImpl implements AiQuestionClassificationService {
+
+    private final AiAuthTokenUtil aiAuthTokenUtil;
+
+    private final RestTemplate restTemplate;
+
+    @Value("${third-party-ai-question-classification.base-url}")
+    private String questionClassificationUrl;
+
+    @Override
+    public String classify(AiQuestionClassificationRequestDto requestDto) {
+        log.info("开始调用问题分类接口,问题:{}", requestDto.getQuestion());
+
+        // 获取访问令牌
+        String accessToken = aiAuthTokenUtil.getAccessToken();
+        if (!StringUtils.hasText(accessToken)) {
+            log.error("调用问题分类接口失败,获取accessToken失败");
+            throw new RuntimeException("获取访问令牌失败");
+        }
+
+        // 构建请求体
+        Map<String, Object> requestBody = new HashMap<>();
+        requestBody.put("question", requestDto.getQuestion());
+
+        // 构建请求头
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        headers.set("Authorization", "Bearer " + accessToken);
+
+        HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
+
+        try {
+            log.info("调用问题分类接口,URL:{},请求参数:{}", questionClassificationUrl, requestBody);
+            ResponseEntity<String> responseEntity = restTemplate.postForEntity(
+                    questionClassificationUrl, request, String.class);
+
+            log.info("问题分类接口调用成功,响应状态:{},响应体:{}",
+                    responseEntity.getStatusCode(), responseEntity.getBody());
+
+            // 直接返回响应体
+            return responseEntity.getBody();
+
+        } catch (HttpClientErrorException e) {
+            log.error("调用问题分类接口返回客户端错误,状态码:{},响应体:{},URL:{}",
+                    e.getStatusCode(), e.getResponseBodyAsString(), questionClassificationUrl, e);
+            throw new RuntimeException("调用问题分类接口失败:" + e.getMessage(), e);
+        } catch (HttpServerErrorException e) {
+            log.error("调用问题分类接口返回服务器错误,状态码:{},响应体:{},URL:{}",
+                    e.getStatusCode(), e.getResponseBodyAsString(), questionClassificationUrl, e);
+            throw new RuntimeException("调用问题分类接口失败:" + e.getMessage(), e);
+        } catch (Exception e) {
+            log.error("调用问题分类接口异常,URL:{},异常信息:{}", questionClassificationUrl, e.getMessage(), e);
+            throw new RuntimeException("调用问题分类接口异常:" + e.getMessage(), e);
+        }
+    }
+}

+ 2 - 2
alien-store/src/main/java/shop/alien/store/util/ai/AiFeedbackAssignUtils.java

@@ -4,6 +4,7 @@ import com.alibaba.fastjson2.JSONObject;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
 import org.springframework.core.io.ByteArrayResource;
 import org.springframework.http.*;
 import org.springframework.stereotype.Component;
@@ -22,6 +23,7 @@ import java.util.Map;
 @Slf4j
 @Component
 @RequiredArgsConstructor
+@RefreshScope
 public class AiFeedbackAssignUtils {
 
     private final RestTemplate restTemplate;
@@ -38,8 +40,6 @@ public class AiFeedbackAssignUtils {
     @Value("${ai.service.assign-staff-url}")
     private String assignStaffUrl;
 
-    private  final AlienAIFeign alienAIFeign;
-
     // 语音识别接口地址(从配置中读取,如果没有配置则使用默认值)
     @Value("${feign.alienAI.url}")
     private String aiServiceBaseUrl;

+ 1 - 0
alien-store/src/main/java/shop/alien/store/util/ai/AiImageColorExtractUtil.java

@@ -26,6 +26,7 @@ import java.util.Map;
 public class AiImageColorExtractUtil {
 
     private final RestTemplate restTemplate;
+
     private final AiAuthTokenUtil aiAuthTokenUtil;
 
     @Value("${ai.service.image-color-extract-url}")