Ver Fonte

移除无用代码

jintao.geng há 2 semanas atrás
pai
commit
7309990f51
22 ficheiros alterados com 2 adições e 1854 exclusões
  1. 0 71
      mango-application/src/main/java/com/mangoo/rating/recommend/app/controller/AiChatController.java
  2. 2 2
      mango-application/src/main/resources/application.yml
  3. 0 162
      mango-application/src/test/java/com/mangoo/rating/recommend/cart/CardDetailServiceImplTest.java
  4. 0 98
      mango-application/src/test/java/com/mangoo/rating/recommend/cart/EfficiencyPriceProviderImplTest.java
  5. 0 95
      mango-application/src/test/java/com/mangoo/rating/recommend/cart/OrderGrouperTest.java
  6. 0 306
      mango-application/src/test/java/com/mangoo/rating/recommend/cart/RecommendServiceImplTest.java
  7. 0 156
      mango-application/src/test/java/com/mangoo/rating/recommend/cart/RuleMatcherTest.java
  8. 0 130
      mango-application/src/test/java/com/mangoo/rating/recommend/cart/RuleSyncServiceImplTest.java
  9. 0 64
      mango-application/src/test/java/com/mangoo/rating/recommend/test/ai/AgentLoopExecutorTest.java
  10. 0 46
      mango-application/src/test/java/com/mangoo/rating/recommend/test/ai/AiChatServiceTest.java
  11. 0 56
      mango-application/src/test/java/com/mangoo/rating/recommend/test/ai/ClaudeSseParserTest.java
  12. 0 56
      mango-application/src/test/java/com/mangoo/rating/recommend/test/ai/OrderLogisticsQueryToolTest.java
  13. 0 55
      mango-application/src/test/java/com/mangoo/rating/recommend/test/ai/SessionContextStoreTest.java
  14. 0 34
      mango-application/src/test/java/com/mangoo/rating/recommend/test/ai/ToolRegistryTest.java
  15. 0 44
      mango-domain/src/main/java/com/mangoo/rating/recommend/ai/agent/AgentLoopExecutor.java
  16. 0 13
      mango-domain/src/main/java/com/mangoo/rating/recommend/ai/provider/ChatModelProvider.java
  17. 0 190
      mango-domain/src/main/java/com/mangoo/rating/recommend/ai/provider/ClaudeChatProvider.java
  18. 0 50
      mango-domain/src/main/java/com/mangoo/rating/recommend/ai/service/AiChatService.java
  19. 0 57
      mango-domain/src/main/java/com/mangoo/rating/recommend/ai/session/SessionContextStore.java
  20. 0 16
      mango-domain/src/main/java/com/mangoo/rating/recommend/ai/tool/AiTool.java
  21. 0 111
      mango-domain/src/main/java/com/mangoo/rating/recommend/ai/tool/OrderLogisticsQueryTool.java
  22. 0 42
      mango-domain/src/main/java/com/mangoo/rating/recommend/ai/tool/ToolRegistry.java

+ 0 - 71
mango-application/src/main/java/com/mangoo/rating/recommend/app/controller/AiChatController.java

@@ -1,71 +0,0 @@
-package com.mangoo.rating.recommend.app.controller;
-
-import com.alibaba.fastjson.JSONObject;
-import com.mangoo.rating.recommend.ai.service.AiChatService;
-import com.mangoo.rating.recommend.request.ai.AiChatRequest;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.web.bind.annotation.*;
-import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
-
-import javax.annotation.Resource;
-import java.util.UUID;
-import java.util.concurrent.Executor;
-
-/** 小程序 AI 会话入口(SSE 流式打字机输出) */
-@Slf4j
-@RestController
-@RequestMapping("/api/ai")
-@RequiredArgsConstructor
-public class AiChatController {
-
-    private final AiChatService aiChatService;
-
-    @Resource(name = "aiChatExecutor")
-    private Executor aiChatExecutor;
-
-    /**
-     * 发起一轮 AI 会话,SSE 流式返回。
-     * 用户身份:主线程取 X-USER-BASE64 原始头,透传给下游工具 Feign 调 ratingApp(由其解析隔离)。
-     */
-    @PostMapping("/chat/stream")
-    public SseEmitter chatStream(@RequestBody AiChatRequest req,
-                                 @RequestHeader(value = "X-USER-BASE64", required = false) String userHeader) {
-        final String sessionId = (req.getSessionId() == null || req.getSessionId().isEmpty())
-            ? UUID.randomUUID().toString().replace("-", "") : req.getSessionId();
-        final String message = req.getMessage();
-        final String header = userHeader;
-
-        SseEmitter emitter = new SseEmitter(600000L); // 10 分钟超时
-        aiChatExecutor.execute(() -> {
-            try {
-                aiChatService.chat(header, sessionId, message, delta -> {
-                    try {
-                        JSONObject d = new JSONObject();
-                        d.put("text", delta);
-                        emitter.send(SseEmitter.event().name("delta").data(d.toJSONString()));
-                    } catch (Exception se) {
-                        throw new RuntimeException(se);
-                    }
-                });
-                JSONObject done = new JSONObject();
-                done.put("sessionId", sessionId);
-                done.put("finishReason", "end_turn");
-                emitter.send(SseEmitter.event().name("done").data(done.toJSONString()));
-                emitter.complete();
-            } catch (Exception e) {
-                log.warn("AI 会话处理失败 sessionId={}", sessionId, e);
-                try {
-                    JSONObject err = new JSONObject();
-                    err.put("code", -1);
-                    err.put("msg", "服务繁忙,请稍后再试");
-                    emitter.send(SseEmitter.event().name("error").data(err.toJSONString()));
-                    emitter.complete();
-                } catch (Exception ignore) {
-                    emitter.completeWithError(e);
-                }
-            }
-        });
-        return emitter;
-    }
-}

+ 2 - 2
mango-application/src/main/resources/application.yml

@@ -271,11 +271,11 @@ pay-service:
   url: http://192.168.77.249:7756
 
 order-service:
-  url: ${ORDER_URL:192.168.31.111:8090}
+  url: ${ORDER_URL:192.168.31.101:8090}
 
 # ratingApp
 rating-app:
-  url: ${RATING_APP_URL:192.168.31.111:8091}
+  url: ${RATING_APP_URL:192.168.31.101:8091}
 
 # Feign配置
 feign:

+ 0 - 162
mango-application/src/test/java/com/mangoo/rating/recommend/cart/CardDetailServiceImplTest.java

@@ -1,162 +0,0 @@
-package com.mangoo.rating.recommend.cart;
-
-import com.mangoo.rating.recommend.AjaxResult;
-import com.mangoo.rating.recommend.manager.ActiveBatchManager;
-import com.mangoo.rating.recommend.manager.CardPopManager;
-import com.mangoo.rating.recommend.manager.CardValueHistoryManager;
-import com.mangoo.rating.recommend.manager.RatingRecommendRuleManager;
-import com.mangoo.rating.recommend.po.CardPopPO;
-import com.mangoo.rating.recommend.po.CardValueHistoryPO;
-import com.mangoo.rating.recommend.po.RatingRecommendRulePO;
-import com.mangoo.rating.recommend.response.recommend.CardDetailVO;
-import com.mangoo.rating.recommend.request.recommend.CardDetailRequest;
-import com.mangoo.rating.recommend.service.impl.CardDetailServiceImpl;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import java.math.BigDecimal;
-import java.time.LocalDate;
-import java.util.Arrays;
-import java.util.Collections;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.Mockito.lenient;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-/**
- * CardDetailServiceImpl Mockito 单测
- *
- * @author gengjintao
- * @date 2026/06/25
- */
-@ExtendWith(MockitoExtension.class)
-class CardDetailServiceImplTest {
-
-    @Mock
-    private RatingRecommendRuleManager ruleManager;
-    @Mock
-    private CardPopManager cardPopManager;
-    @Mock
-    private CardValueHistoryManager cardValueHistoryManager;
-    @Mock
-    private ActiveBatchManager activeBatchManager;
-    @InjectMocks
-    private CardDetailServiceImpl service;
-
-    @Captor
-    private ArgumentCaptor<LocalDate> dateCaptor;
-
-    @BeforeEach
-    void setUp() {
-        // 默认桩:有生效批次 B1,POP 与趋势默认空(lenient 避免未使用桩报错)
-        lenient().when(activeBatchManager.selectActiveBatchNo()).thenReturn("B1");
-        lenient().when(cardPopManager.selectByCardId(eq("C1"), eq("B1"))).thenReturn(Collections.emptyList());
-        lenient().when(cardValueHistoryManager.selectTrend(eq("C1"), org.mockito.ArgumentMatchers.any())).thenReturn(Collections.emptyList());
-    }
-
-    /** 构造一条标准规则 PO */
-    private RatingRecommendRulePO rule() {
-        RatingRecommendRulePO r = new RatingRecommendRulePO();
-        r.setCardId("C1");
-        r.setCardNo("066/080");
-        r.setPlayer("AIPOM");
-        r.setCardYear("2026");
-        r.setSeries("pokemon.M2.JNP");
-        r.setCardSet("BASE");
-        r.setCurrentValue(new BigDecimal("15"));
-        r.setValueChangePct(new BigDecimal("7.05"));
-        r.setValueMin(new BigDecimal("10"));
-        r.setValueMax(new BigDecimal("45"));
-        return r;
-    }
-
-    /** 取状态码(Map 风格,成功 0) */
-    private int code(AjaxResult r) {
-        return ((Number) r.get("code")).intValue();
-    }
-
-    /** 取 data(Map 风格) */
-    private CardDetailVO data(AjaxResult r) {
-        return (CardDetailVO) r.get("data");
-    }
-
-    /** 构造详情查询请求对象 */
-    private CardDetailRequest reqOf(String cardId, String period) {
-        CardDetailRequest req = new CardDetailRequest();
-        req.setCardId(cardId);
-        req.setPeriod(period);
-        return req;
-    }
-
-    @Test
-    void 正常_组装详情() {
-        when(ruleManager.selectByCardId("C1", "B1")).thenReturn(rule());
-        CardPopPO pop = new CardPopPO();
-        pop.setCardId("C1");
-        pop.setAgency("PSA");
-        pop.setGradeAll(660);
-        pop.setGrade10(25);
-        when(cardPopManager.selectByCardId("C1", "B1")).thenReturn(Collections.singletonList(pop));
-        CardValueHistoryPO h = new CardValueHistoryPO();
-        h.setCardId("C1");
-        h.setPrice(new BigDecimal("12"));
-        h.setRecordDate(LocalDate.now().minusDays(1));
-        when(cardValueHistoryManager.selectTrend(eq("C1"), org.mockito.ArgumentMatchers.any())).thenReturn(Arrays.asList(h));
-
-        AjaxResult result = service.getDetail(reqOf("C1", "30d"));
-        assertEquals(0, code(result));
-        CardDetailVO d = data(result);
-        assertEquals("AIPOM", d.getName());
-        assertEquals("066/080", d.getCardNo());
-        assertEquals(new BigDecimal("15"), d.getValue().getCurrent());
-        assertEquals(1, d.getPops().size());
-        assertEquals("PSA", d.getPops().get(0).getAgency());
-        assertEquals(1, d.getTrend().size());
-    }
-
-    @Test
-    void 无生效批次_返回error() {
-        when(activeBatchManager.selectActiveBatchNo()).thenReturn(null);
-        AjaxResult result = service.getDetail(reqOf("C1", "30d"));
-        assertNotEquals(0, code(result));
-    }
-
-    @Test
-    void 卡片不存在_返回error() {
-        when(ruleManager.selectByCardId("C1", "B1")).thenReturn(null);
-        AjaxResult result = service.getDetail(reqOf("C1", "30d"));
-        assertNotEquals(0, code(result));
-    }
-
-    @Test
-    void cardId为空_返回error() {
-        AjaxResult result = service.getDetail(reqOf("  ", "30d"));
-        assertNotEquals(0, code(result));
-    }
-
-    @Test
-    void period_7d_起始为7天前() {
-        when(ruleManager.selectByCardId("C1", "B1")).thenReturn(rule());
-        service.getDetail(reqOf("C1", "7d"));
-        verify(cardValueHistoryManager).selectTrend(eq("C1"), dateCaptor.capture());
-        assertEquals(LocalDate.now().minusDays(7), dateCaptor.getValue());
-    }
-
-    @Test
-    void period_all_起始为null() {
-        when(ruleManager.selectByCardId("C1", "B1")).thenReturn(rule());
-        service.getDetail(reqOf("C1", "all"));
-        verify(cardValueHistoryManager).selectTrend(eq("C1"), isNull());
-    }
-}

+ 0 - 98
mango-application/src/test/java/com/mangoo/rating/recommend/cart/EfficiencyPriceProviderImplTest.java

@@ -1,98 +0,0 @@
-package com.mangoo.rating.recommend.cart;
-
-import com.mangoo.rating.recommend.client.OrderApiClient;
-import com.mangoo.rating.recommend.client.feign.dto.ProductServiceLevelDTO;
-import com.mangoo.rating.recommend.config.RecommendProperties;
-import com.mangoo.rating.recommend.dto.recommend.EfficiencyDictItem;
-import com.mangoo.rating.recommend.service.recommend.impl.EfficiencyPriceProviderImpl;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import java.math.BigDecimal;
-import java.util.Arrays;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.when;
-
-/**
- * EfficiencyPriceProviderImpl 字典桥接单测
- *
- * @author gengjintao
- */
-@ExtendWith(MockitoExtension.class)
-class EfficiencyPriceProviderImplTest {
-
-    @Mock
-    private RecommendProperties recommendProperties;
-    @Mock
-    private OrderApiClient orderApiClient;
-    @InjectMocks
-    private EfficiencyPriceProviderImpl provider;
-
-    private ProductServiceLevelDTO level(long id, String name, String timeLimit, String price) {
-        ProductServiceLevelDTO d = new ProductServiceLevelDTO();
-        d.setId(id);
-        d.setLevelName(name);
-        d.setTimeLimit(timeLimit);
-        d.setPrice(new BigDecimal(price));
-        return d;
-    }
-
-    @Test
-    void 桥接_timeLimit映射到语义档code() {
-        Map<String, Long> mapping = new LinkedHashMap<>();
-        mapping.put("普通", 1L);
-        mapping.put("闪评", 3L);
-        when(recommendProperties.getTimeLimitMapping()).thenReturn(mapping);
-        when(orderApiClient.serviceList(any())).thenReturn(Arrays.asList(
-                level(11L, "普通档", "普通", "50.00"),
-                level(33L, "闪评档", "闪评", "200.00")));
-
-        Map<Long, EfficiencyDictItem> dict = provider.loadEfficiencyDict();
-        assertEquals("普通档", dict.get(1L).getName());
-        assertEquals(new BigDecimal("50.00"), dict.get(1L).getPrice());
-        assertEquals("闪评档", dict.get(3L).getName());
-    }
-
-    @Test
-    void 订单服务空_返回空字典() {
-        // serviceList 返回 null 时代码提前 return,不会访问 timeLimitMapping,故此处无需打桩
-        when(orderApiClient.serviceList(any())).thenReturn(null);
-        assertTrue(provider.loadEfficiencyDict().isEmpty());
-    }
-
-    @Test
-    void 同档多条_取首条() {
-        Map<String, Long> mapping = new LinkedHashMap<>();
-        mapping.put("闪评", 3L);
-        when(recommendProperties.getTimeLimitMapping()).thenReturn(mapping);
-        // 两条都映射到档 3,putIfAbsent 应保留首条
-        when(orderApiClient.serviceList(any())).thenReturn(Arrays.asList(
-                level(31L, "闪评首条", "闪评", "200.00"),
-                level(32L, "闪评次条", "闪评", "999.00")));
-        Map<Long, EfficiencyDictItem> dict = provider.loadEfficiencyDict();
-        assertEquals(1, dict.size());
-        assertEquals("闪评首条", dict.get(3L).getName());
-        assertEquals(new BigDecimal("200.00"), dict.get(3L).getPrice());
-    }
-
-    @Test
-    void timeLimit未在映射表_跳过该档() {
-        Map<String, Long> mapping = new LinkedHashMap<>();
-        mapping.put("普通", 1L);
-        when(recommendProperties.getTimeLimitMapping()).thenReturn(mapping);
-        when(orderApiClient.serviceList(any())).thenReturn(Arrays.asList(
-                level(11L, "普通档", "普通", "50.00"),
-                level(99L, "未知档", "超急速", "999.00")));
-        Map<Long, EfficiencyDictItem> dict = provider.loadEfficiencyDict();
-        assertEquals(1, dict.size());
-        assertTrue(dict.containsKey(1L));
-    }
-}

+ 0 - 95
mango-application/src/test/java/com/mangoo/rating/recommend/cart/OrderGrouperTest.java

@@ -1,95 +0,0 @@
-package com.mangoo.rating.recommend.cart;
-
-import com.mangoo.rating.recommend.response.recommend.RecommendCardVO;
-import com.mangoo.rating.recommend.service.recommend.OrderGrouper;
-import com.mangoo.rating.recommend.response.recommend.OrderSuggestionVO;
-import org.junit.jupiter.api.Test;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * OrderGrouper TDD 单测
- *
- * @author gengjintao
- * @date 2026/06/24
- */
-class OrderGrouperTest {
-
-    /**
-     * 构造测试用卡 VO,仅设置 clientCardId 与 evaluateEfficiencyId(其他字段不参与分组)
-     * 说明:VO 已由旧 recommendEfficiency(int) 迁移到 evaluateEfficiencyId(Long)。
-     */
-    private RecommendCardVO c(String no, long eff) {
-        RecommendCardVO v = new RecommendCardVO();
-        v.setClientCardId(no);
-        v.setEvaluateEfficiencyId(eff);
-        return v;
-    }
-
-    @Test
-    void 空输入_返回空订单列表() {
-        List<OrderSuggestionVO> orders = OrderGrouper.group(new ArrayList<>());
-        assertTrue(orders.isEmpty());
-    }
-
-    @Test
-    void null输入_返回空订单列表() {
-        List<OrderSuggestionVO> orders = OrderGrouper.group(null);
-        assertTrue(orders.isEmpty());
-    }
-
-    @Test
-    void 单卡_返回单订单_组号1() {
-        List<OrderSuggestionVO> orders = OrderGrouper.group(Arrays.asList(c("a", 2)));
-        assertEquals(1, orders.size());
-        assertEquals(1, orders.get(0).getGroupNo());
-        assertEquals(2, orders.get(0).getEvaluateEfficiencyId());
-        assertEquals(1, orders.get(0).getCardCount());
-        assertEquals("a", orders.get(0).getCards().get(0).getClientCardId());
-    }
-
-    @Test
-    void 全同档_合并为一单() {
-        List<OrderSuggestionVO> orders = OrderGrouper.group(Arrays.asList(c("a", 1), c("b", 1), c("c", 1)));
-        assertEquals(1, orders.size());
-        assertEquals(1, orders.get(0).getEvaluateEfficiencyId());
-        assertEquals(3, orders.get(0).getCardCount());
-    }
-
-    @Test
-    void 两档_按档位升序输出_组号从1递增() {
-        // 输入 闪评/普通 乱序,期望输出 普通(1) 在前
-        List<OrderSuggestionVO> orders = OrderGrouper.group(Arrays.asList(c("x", 3), c("y", 1), c("z", 1)));
-        assertEquals(2, orders.size());
-        assertEquals(1, orders.get(0).getGroupNo());
-        assertEquals(1, orders.get(0).getEvaluateEfficiencyId());
-        assertEquals(2, orders.get(0).getCardCount());
-        assertEquals(2, orders.get(1).getGroupNo());
-        assertEquals(3, orders.get(1).getEvaluateEfficiencyId());
-        assertEquals(1, orders.get(1).getCardCount());
-    }
-
-    @Test
-    void 三档同时存在_最多3个订单_按档位升序() {
-        List<OrderSuggestionVO> orders = OrderGrouper.group(Arrays.asList(c("a", 2), c("b", 3), c("c", 1)));
-        assertEquals(3, orders.size());
-        assertEquals(1, orders.get(0).getEvaluateEfficiencyId()); // 普通
-        assertEquals(2, orders.get(1).getEvaluateEfficiencyId()); // 快速
-        assertEquals(3, orders.get(2).getEvaluateEfficiencyId()); // 闪评
-    }
-
-    @Test
-    void 组内卡列表保持原顺序() {
-        // 同档卡 a/b/c 输入顺序 a c b,期望分组内顺序 a c b
-        List<OrderSuggestionVO> orders = OrderGrouper.group(Arrays.asList(c("a", 1), c("c", 1), c("b", 1)));
-        assertEquals(1, orders.size());
-        assertEquals("a", orders.get(0).getCards().get(0).getClientCardId());
-        assertEquals("c", orders.get(0).getCards().get(1).getClientCardId());
-        assertEquals("b", orders.get(0).getCards().get(2).getClientCardId());
-    }
-}

+ 0 - 306
mango-application/src/test/java/com/mangoo/rating/recommend/cart/RecommendServiceImplTest.java

@@ -1,306 +0,0 @@
-package com.mangoo.rating.recommend.cart;
-
-import com.mangoo.rating.recommend.AjaxResult;
-import com.mangoo.rating.recommend.client.feign.PreOrderFeignClient;
-import com.mangoo.rating.recommend.config.RecommendProperties;
-import com.mangoo.rating.recommend.dto.recommend.EfficiencyDictItem;
-import com.mangoo.rating.recommend.dto.recommend.SeriesCardSetKey;
-import com.mangoo.rating.recommend.manager.ActiveBatchManager;
-import com.mangoo.rating.recommend.manager.RatingRecommendRuleManager;
-import com.mangoo.rating.recommend.po.RatingRecommendRulePO;
-import com.mangoo.rating.recommend.request.recommend.PreOrderCardDTO;
-import com.mangoo.rating.recommend.request.recommend.PreOrderSaveDTO;
-import com.mangoo.rating.recommend.request.recommend.RecommendCardDTO;
-import com.mangoo.rating.recommend.request.recommend.RecommendRequest;
-import com.mangoo.rating.recommend.service.impl.RecommendServiceImpl;
-import com.mangoo.rating.recommend.service.recommend.EfficiencyPriceProvider;
-import com.mangoo.rating.recommend.service.recommend.LoginUserProvider;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.lenient;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-/**
- * RecommendServiceImpl 单测(预订单落库改造后:返回 preOrderNo + Feign 落库)
- *
- * 覆盖的目标场景:
- *   1) 空入参 / null                        → 非 0 code
- *   2) 超上限                                → 非 0 code
- *   3) 正常,Feign 返回 preOrderNo          → code=0,data=preOrderNo
- *   4) userId 为 null(未登录)              → 非 0 code,且不调用 Feign
- *   5) Feign 抛异常                          → 非 0 code
- *   6) Feign 返回 code!=0                    → 非 0 code(下游 msg 不再透传,改为固定文案)
- *   7) 落库包字段映射:识别字段 + userId/totalCards/packageId 全透传
- *   8) 未命中 → 按 cardType 兜底默认档(落库卡 evaluateEfficiencyId 取兜底档)
- *   9) 批量查询:同 (series,cardSet) 卡去重后每次请求只查一次规则
- *
- * @author gengjintao
- */
-@ExtendWith(MockitoExtension.class)
-class RecommendServiceImplTest {
-
-    @Mock
-    private RatingRecommendRuleManager ratingRecommendRuleManager;
-    @Mock
-    private EfficiencyPriceProvider efficiencyPriceProvider;
-    @Mock
-    private RecommendProperties recommendProperties;
-    @Mock
-    private ActiveBatchManager activeBatchManager;
-    @Mock
-    private PreOrderFeignClient preOrderFeignClient;
-    @Mock
-    private LoginUserProvider loginUserProvider;
-    @InjectMocks
-    private RecommendServiceImpl recommendService;
-
-    /** 落库失败对外统一文案(生产代码不再透传下游 msg) */
-    private static final String FAIL_MSG = "预订单生成失败,请稍后重试";
-
-    /** 造最简卡入参:仅带识别 4 字段 */
-    private RecommendCardDTO card(String player, String year, String series, String cardSet) {
-        RecommendCardDTO d = new RecommendCardDTO();
-        d.setPlayer(player);
-        d.setYear(year);
-        d.setSeries(series);
-        d.setCardSet(cardSet);
-        return d;
-    }
-
-    /** 造一个 request(浅拷贝入参列表,避免测试之间共享同一 List) */
-    private RecommendRequest req(RecommendCardDTO... cards) {
-        RecommendRequest r = new RecommendRequest();
-        r.setCards(new ArrayList<>(Arrays.asList(cards)));
-        return r;
-    }
-
-    /** 造一条 EXACT 命中规则(放入池) */
-    private RatingRecommendRulePO poolRule(long id, int eff, String player, String year,
-                                           String series, String cardSet) {
-        RatingRecommendRulePO po = new RatingRecommendRulePO();
-        po.setId(id);
-        // PO 字段为 recommendEfficiencyId(Long);这里将 int 参数窄化装箱
-        po.setRecommendEfficiencyId((long) eff);
-        po.setPlayer(player);
-        po.setCardYear(year);
-        po.setSeries(series);
-        po.setCardSet(cardSet);
-        po.setCardId("DW-CARD-1");
-        return po;
-    }
-
-    /** 构造 Feign 成功返回:data 里带 preOrderNo */
-    private AjaxResult okWith(String preOrderNo) {
-        Map<String, Object> data = new HashMap<>();
-        data.put("preOrderNo", preOrderNo);
-        return AjaxResult.success(data);
-    }
-
-    /** 默认:批量池含一条 EXACT 命中、字典有价、用户已登录 */
-    private void defaultHappyStubs() {
-        lenient().when(recommendProperties.getBatchMax()).thenReturn(50);
-        lenient().when(activeBatchManager.selectActiveBatchNo()).thenReturn("B1");
-        lenient().when(ratingRecommendRuleManager.selectByBatchAndSeriesSets(anyString(), any()))
-                .thenReturn(new ArrayList<>(Arrays.asList(
-                        poolRule(1L, 3, "Jordan", "1986", "Fleer", "Base"))));
-        Map<Long, EfficiencyDictItem> dict = new HashMap<>();
-        dict.put(3L, new EfficiencyDictItem("闪评", new BigDecimal("200.00")));
-        lenient().when(efficiencyPriceProvider.loadEfficiencyDict()).thenReturn(dict);
-        lenient().when(loginUserProvider.currentUserId()).thenReturn(1001L);
-        lenient().when(loginUserProvider.currentUserHeader()).thenReturn("base64header");
-    }
-
-    private int code(AjaxResult r) {
-        return r.getCode();
-    }
-
-    @Test
-    void 空入参_返回error() {
-        // null / 空列表:直接拦截,不调用 Feign
-        assertNotEquals(0, code(recommendService.recommendEfficiency(null)));
-        assertNotEquals(0, code(recommendService.recommendEfficiency(new RecommendRequest())));
-    }
-
-    @Test
-    void 超限_返回error() {
-        // 上限=1,传 2 张 → 拦截
-        when(recommendProperties.getBatchMax()).thenReturn(1);
-        AjaxResult result = recommendService.recommendEfficiency(
-                req(card("p", "y", "s", "cs"), card("p", "y", "s", "cs")));
-        assertNotEquals(0, code(result));
-    }
-
-    @Test
-    void 正常_落库成功_返回preOrderNo() {
-        defaultHappyStubs();
-        when(preOrderFeignClient.savePreOrder(anyString(), any(PreOrderSaveDTO.class)))
-                .thenReturn(okWith("PRE123456"));
-        AjaxResult result = recommendService.recommendEfficiency(req(card("Jordan", "1986", "Fleer", "Base")));
-        assertEquals(0, code(result));
-        // 生产代码:成功时 data 为 String preOrderNo
-        assertEquals("PRE123456", result.get("data"));
-    }
-
-    @Test
-    void 用户未登录_不调用Feign_返回error() {
-        defaultHappyStubs();
-        // 覆盖 defaultHappyStubs 里的登录用户
-        when(loginUserProvider.currentUserId()).thenReturn(null);
-        AjaxResult result = recommendService.recommendEfficiency(req(card("p", "y", "s", "cs")));
-        assertNotEquals(0, code(result));
-        // 关键:未登录时不得触发远程调用
-        verify(preOrderFeignClient, never()).savePreOrder(any(), any());
-    }
-
-    @Test
-    void Feign抛异常_返回生成失败() {
-        defaultHappyStubs();
-        when(preOrderFeignClient.savePreOrder(anyString(), any(PreOrderSaveDTO.class)))
-                .thenThrow(new RuntimeException("timeout"));
-        AjaxResult result = recommendService.recommendEfficiency(req(card("p", "y", "s", "cs")));
-        assertNotEquals(0, code(result));
-        assertEquals(FAIL_MSG, result.getMsg());
-    }
-
-    @Test
-    void Feign返回非0_透传错误() {
-        defaultHappyStubs();
-        // 下游返回 error(含具体 msg "下游库满"),生产代码经质量审查后已不再透传下游 msg,
-        // 对外统一为固定文案 FAIL_MSG,避免下游信息泄露。故只断言 code 非 0 + 固定文案。
-        when(preOrderFeignClient.savePreOrder(anyString(), any(PreOrderSaveDTO.class)))
-                .thenReturn(AjaxResult.error("下游库满"));
-        AjaxResult result = recommendService.recommendEfficiency(req(card("p", "y", "s", "cs")));
-        assertNotEquals(0, code(result));
-        assertEquals(FAIL_MSG, result.getMsg());
-    }
-
-    @Test
-    void 落库包_识别字段与关键字段全部透传() {
-        defaultHappyStubs();
-        // 入参卡设满了全部分类前提字段(cardType/sportEvent/ipName/team/role/rule),
-        // 规则须在这些前提字段上同值才能通过前提过滤命中(否则命中不了、matchedCardId 取不到)
-        RatingRecommendRulePO ruleWithType = poolRule(1L, 3, "Jordan", "1986", "Fleer", "Base");
-        ruleWithType.setCardType(2);
-        ruleWithType.setSportEvent("篮球");
-        ruleWithType.setIpName("NBA");
-        ruleWithType.setTeam("Bulls");
-        ruleWithType.setRole("SG");
-        ruleWithType.setRule("MVP");
-        when(ratingRecommendRuleManager.selectByBatchAndSeriesSets(anyString(), any()))
-                .thenReturn(new ArrayList<>(Arrays.asList(ruleWithType)));
-        when(preOrderFeignClient.savePreOrder(anyString(), any(PreOrderSaveDTO.class)))
-                .thenReturn(okWith("PRE999"));
-
-        // 构造含 10 个补齐识别字段 + id/price 的入参卡
-        RecommendCardDTO c = card("Jordan", "1986", "Fleer", "Base");
-        c.setSportEvent("篮球");
-        c.setCardType(2);
-        c.setRole("SG");
-        c.setCardNo("NO-1");
-        c.setTeam("Bulls");
-        c.setLimitId("/99");
-        c.setIpName("NBA");
-        c.setRule("MVP");
-        c.setRarity("SSR");
-        c.setMaterial("金箔");
-        // DTO 字段已重命名为 packageId/packagePrice(原 id/price),对齐真实签名
-        c.setPackageId(88L);
-        c.setPackagePrice(new BigDecimal("120"));
-
-        recommendService.recommendEfficiency(req(c));
-
-        // 抓取实际传给 Feign 的 PreOrderSaveResponseDTO
-        ArgumentCaptor<PreOrderSaveDTO> captor = ArgumentCaptor.forClass(PreOrderSaveDTO.class);
-        verify(preOrderFeignClient).savePreOrder(anyString(), captor.capture());
-        PreOrderSaveDTO sent = captor.getValue();
-
-        // 主表汇总字段
-        assertEquals(1001L, sent.getUserId());
-        assertEquals(1, sent.getTotalCards());
-
-        // 卡片明细:10 识别字段全部透传(sportEvent/cardType/role/cardNo/team/limitId/ipName/rule/rarity/material)
-        PreOrderCardDTO card = sent.getGroups().get(0).getCards().get(0);
-        assertEquals("篮球", card.getSportEvent());
-        assertEquals(2, card.getCardType());
-        assertEquals("SG", card.getRole());
-        assertEquals("NO-1", card.getCardNo());
-        assertEquals("Bulls", card.getTeam());
-        assertEquals("/99", card.getLimitId());
-        assertEquals("NBA", card.getIpName());
-        // 入参 rule -> VO roleName -> PreOrderCardDTO.rule(往返映射)
-        assertEquals("MVP", card.getRule());
-        assertEquals("SSR", card.getRarity());
-        assertEquals("金箔", card.getMaterial());
-        // 套餐/命中:入参 id -> packageId;规则 cardId -> matchedCardId
-        assertEquals(88L, card.getPackageId());
-        assertEquals("DW-CARD-1", card.getMatchedCardId());
-        // 命中档位 3(闪评)应装入卡的 evaluateEfficiencyId
-        assertEquals(3L, card.getEvaluateEfficiencyId());
-        // 时效价精确断言:字典档 3 → 200.00(compareTo 规避 scale 差异)
-        assertEquals(0, card.getEfficiencyPrice().compareTo(new BigDecimal("200.00")));
-    }
-
-    @Test
-    void 未命中_按cardType兜底默认档() {
-        defaultHappyStubs();
-        // 池为空 → 全部兜底;配置 cardType=1 默认档=2
-        when(ratingRecommendRuleManager.selectByBatchAndSeriesSets(anyString(), any()))
-                .thenReturn(new ArrayList<>());
-        Map<Integer, Long> byType = new HashMap<>();
-        byType.put(1, 2L);
-        when(recommendProperties.getDefaultEfficiencyByCardType()).thenReturn(byType);
-        // 字典含档 2 的价,保证能报价、能落库
-        Map<Long, EfficiencyDictItem> dict = new HashMap<>();
-        dict.put(2L, new EfficiencyDictItem("快速", new BigDecimal("100.00")));
-        when(efficiencyPriceProvider.loadEfficiencyDict()).thenReturn(dict);
-        when(preOrderFeignClient.savePreOrder(anyString(), any(PreOrderSaveDTO.class)))
-                .thenReturn(okWith("PRE-DEF"));
-
-        RecommendCardDTO c = card("Nobody", "2099", "NoSeries", "NoSet");
-        c.setCardType(1);
-        recommendService.recommendEfficiency(req(c));
-
-        ArgumentCaptor<PreOrderSaveDTO> captor = ArgumentCaptor.forClass(PreOrderSaveDTO.class);
-        verify(preOrderFeignClient).savePreOrder(anyString(), captor.capture());
-        PreOrderCardDTO sent = captor.getValue().getGroups().get(0).getCards().get(0);
-        // 兜底档 2 应装入卡的 evaluateEfficiencyId
-        assertEquals(2L, sent.getEvaluateEfficiencyId());
-    }
-
-    @Test
-    void 批量查询_每次请求只查一次规则_且按seriesSet去重() {
-        defaultHappyStubs();
-        when(preOrderFeignClient.savePreOrder(anyString(), any(PreOrderSaveDTO.class)))
-                .thenReturn(okWith("PRE-ONCE"));
-        // 3 张同 (series,cardSet) 卡 → 去重后只有 1 个 key,且只调 1 次批量查询
-        recommendService.recommendEfficiency(req(
-                card("Jordan", "1986", "Fleer", "Base"),
-                card("Pippen", "1986", "Fleer", "Base"),
-                card("Rodman", "1986", "Fleer", "Base")));
-        // 抓取批量查询的 keys 入参,锁住 distinct 去重行为(删掉生产的 .distinct() 本用例会失败)
-        @SuppressWarnings("unchecked")
-        ArgumentCaptor<java.util.List<SeriesCardSetKey>> keysCaptor =
-                ArgumentCaptor.forClass(java.util.List.class);
-        verify(ratingRecommendRuleManager, org.mockito.Mockito.times(1))
-                .selectByBatchAndSeriesSets(anyString(), keysCaptor.capture());
-        // 3 张同系列+卡种 → 去重后 keys 只剩 1 个
-        assertEquals(1, keysCaptor.getValue().size());
-    }
-}

+ 0 - 156
mango-application/src/test/java/com/mangoo/rating/recommend/cart/RuleMatcherTest.java

@@ -1,156 +0,0 @@
-package com.mangoo.rating.recommend.cart;
-
-import com.mangoo.rating.recommend.enums.MatchLevelEnum;
-import com.mangoo.rating.recommend.po.RatingRecommendRulePO;
-import com.mangoo.rating.recommend.request.recommend.RecommendCardDTO;
-import com.mangoo.rating.recommend.service.recommend.RuleMatcher;
-import org.junit.jupiter.api.Test;
-
-import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-
-/**
- * RuleMatcher 纯算法单测:分类前提 + L0~L3 降级 + 多候选取舍
- *
- * @author gengjintao
- */
-class RuleMatcherTest {
-
-    private RatingRecommendRulePO rule(long id, Integer eff, String player, String year,
-                                       String series, String cardSet) {
-        RatingRecommendRulePO po = new RatingRecommendRulePO();
-        po.setId(id);
-        // PO 字段为 recommendEfficiencyId(Long);测试参数是 Integer,此处窄化装箱
-        po.setRecommendEfficiencyId(eff == null ? null : eff.longValue());
-        po.setPlayer(player);
-        po.setCardYear(year);
-        po.setSeries(series);
-        po.setCardSet(cardSet);
-        return po;
-    }
-
-    private RecommendCardDTO card(String player, String year, String series, String cardSet) {
-        RecommendCardDTO d = new RecommendCardDTO();
-        d.setPlayer(player);
-        d.setYear(year);
-        d.setSeries(series);
-        d.setCardSet(cardSet);
-        return d;
-    }
-
-    @Test
-    void 空池_返回null() {
-        assertNull(RuleMatcher.match(card("p", "y", "s", "cs"), new ArrayList<>()));
-    }
-
-    @Test
-    void EXACT层_四字段全等命中() {
-        RatingRecommendRulePO r = rule(1, 2, "Jordan", "1986", "Fleer", "Base");
-        RuleMatcher.MatchResult res = RuleMatcher.match(card("Jordan", "1986", "Fleer", "Base"),
-                Arrays.asList(r));
-        assertNotNull(res);
-        assertEquals(MatchLevelEnum.EXACT, res.getLevel());
-        assertEquals(2L, res.getRule().getRecommendEfficiencyId());
-    }
-
-    @Test
-    void L3层_仅系列卡种命中() {
-        RatingRecommendRulePO r = rule(1, 1, "Other", "1990", "Fleer", "Base");
-        RecommendCardDTO c = card("Jordan", "1986", "Fleer", "Base"); // player/year 不同 → 降到 L3
-        RuleMatcher.MatchResult res = RuleMatcher.match(c, Arrays.asList(r));
-        assertNotNull(res);
-        assertEquals(MatchLevelEnum.L3, res.getLevel());
-    }
-
-    @Test
-    void L0层_含cardNo等本体精化命中() {
-        RatingRecommendRulePO r = rule(1, 3, "Jordan", "1986", "Fleer", "Base");
-        r.setCardNo("023");
-        r.setLimitId("/99");
-        r.setRarity("SSR");
-        r.setMaterial("金箔");
-        RecommendCardDTO c = card("Jordan", "1986", "Fleer", "Base");
-        c.setCardNo("023");
-        c.setLimitId("/99");
-        c.setRarity("SSR");
-        c.setMaterial("金箔");
-        RuleMatcher.MatchResult res = RuleMatcher.match(c, Arrays.asList(r));
-        assertNotNull(res);
-        assertEquals(MatchLevelEnum.L0, res.getLevel());
-    }
-
-    @Test
-    void 分类前提_cardType不吻合则不命中() {
-        RatingRecommendRulePO r = rule(1, 2, "Jordan", "1986", "Fleer", "Base");
-        r.setCardType(2); // 球星卡
-        RecommendCardDTO c = card("Jordan", "1986", "Fleer", "Base");
-        c.setCardType(1); // 入参宝可梦卡 → 前提不吻合 → 不命中
-        assertNull(RuleMatcher.match(c, Arrays.asList(r)));
-    }
-
-    @Test
-    void 分类前提_入参为空则不约束() {
-        RatingRecommendRulePO r = rule(1, 2, "Jordan", "1986", "Fleer", "Base");
-        r.setCardType(2);
-        RecommendCardDTO c = card("Jordan", "1986", "Fleer", "Base");
-        // c.cardType 为 null → 不约束 → 仍命中 EXACT
-        RuleMatcher.MatchResult res = RuleMatcher.match(c, Arrays.asList(r));
-        assertNotNull(res);
-        assertEquals(MatchLevelEnum.EXACT, res.getLevel());
-    }
-
-    @Test
-    void 多候选取舍_生效时间优先同时间取高档() {
-        // 反向对照锁死主序:older 的 eff 更高(5)但无生效时间;newer 的 eff 更低(1)但生效时间更新。
-        // 仅当 effective_time 是主序时,newer 才会胜出;若被错改成 eff 主序,本用例会 FAIL。
-        RatingRecommendRulePO older = rule(10, 5, "Jordan", "1986", "Fleer", "Base");
-        RatingRecommendRulePO newer = rule(2, 1, "Jordan", "1986", "Fleer", "Base");
-        newer.setEffectiveTime(LocalDateTime.of(2026, 7, 1, 0, 0));
-        RuleMatcher.MatchResult res = RuleMatcher.match(card("Jordan", "1986", "Fleer", "Base"),
-                Arrays.asList(older, newer));
-        assertNotNull(res);
-        // 断言取 effective_time 更新的 newer(eff=1),验证 time 是主序
-        assertEquals(1L, res.getRule().getRecommendEfficiencyId());
-        assertEquals(2L, res.getRule().getId());
-    }
-
-    @Test
-    void L1层_含cardNo命中() {
-        // 规则齐备至 L1 键(player+year+series+cardSet+cardNo),不含 limitId/rarity/material;
-        // 入参同样只到 cardNo → L0 因缺 limitId/rarity/material 落空 → 命中 L1
-        RatingRecommendRulePO r = rule(1, 2, "Jordan", "1986", "Fleer", "Base");
-        r.setCardNo("023");
-        RecommendCardDTO c = card("Jordan", "1986", "Fleer", "Base");
-        c.setCardNo("023");
-        RuleMatcher.MatchResult res = RuleMatcher.match(c, Arrays.asList(r));
-        assertNotNull(res);
-        assertEquals(MatchLevelEnum.L1, res.getLevel());
-    }
-
-    @Test
-    void L2层_系列卡种年份命中() {
-        // player 与入参不同(触发 L0/L1/EXACT 落空),但 series+cardSet+cardYear 一致 → 命中 L2
-        RatingRecommendRulePO r = rule(1, 2, "OtherPlayer", "1986", "Fleer", "Base");
-        RecommendCardDTO c = card("Jordan", "1986", "Fleer", "Base");
-        RuleMatcher.MatchResult res = RuleMatcher.match(c, Arrays.asList(r));
-        assertNotNull(res);
-        assertEquals(MatchLevelEnum.L2, res.getLevel());
-    }
-
-    @Test
-    void 多候选取舍_同生效时间取高档() {
-        RatingRecommendRulePO low = rule(9, 1, "Jordan", "1986", "Fleer", "Base");
-        RatingRecommendRulePO high = rule(8, 3, "Jordan", "1986", "Fleer", "Base");
-        // 两条都无 effectiveTime(并列)→ 取 recommend_efficiency 高档(3)
-        RuleMatcher.MatchResult res = RuleMatcher.match(card("Jordan", "1986", "Fleer", "Base"),
-                Arrays.asList(low, high));
-        assertNotNull(res);
-        assertEquals(3L, res.getRule().getRecommendEfficiencyId());
-    }
-}

+ 0 - 130
mango-application/src/test/java/com/mangoo/rating/recommend/cart/RuleSyncServiceImplTest.java

@@ -1,130 +0,0 @@
-package com.mangoo.rating.recommend.cart;
-
-import com.mangoo.rating.recommend.AjaxResult;
-import com.mangoo.rating.recommend.manager.ActiveBatchManager;
-import com.mangoo.rating.recommend.manager.CardPopManager;
-import com.mangoo.rating.recommend.manager.CardValueHistoryManager;
-import com.mangoo.rating.recommend.manager.RatingRecommendRuleManager;
-import com.mangoo.rating.recommend.po.RatingRecommendRulePO;
-import com.mangoo.rating.recommend.request.recommend.RuleSyncCardDTO;
-import com.mangoo.rating.recommend.request.recommend.RuleSyncRequest;
-import com.mangoo.rating.recommend.service.impl.RuleSyncServiceImpl;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.mockito.Mockito.lenient;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-/**
- * RuleSyncServiceImpl Mockito 单测
- *
- * @author gengjintao
- * @date 2026/06/25
- */
-@ExtendWith(MockitoExtension.class)
-class RuleSyncServiceImplTest {
-
-    @Mock
-    private RatingRecommendRuleManager ruleManager;
-    @Mock
-    private CardPopManager cardPopManager;
-    @Mock
-    private CardValueHistoryManager cardValueHistoryManager;
-    @Mock
-    private ActiveBatchManager activeBatchManager;
-    @InjectMocks
-    private RuleSyncServiceImpl ruleSyncService;
-
-    @Captor
-    private ArgumentCaptor<List<RatingRecommendRulePO>> ruleListCaptor;
-
-    @BeforeEach
-    void setUp() {
-        // 默认无生效批次 + 清旧批次查询返回空,避免 NPE
-        lenient().when(activeBatchManager.selectActiveBatchNo()).thenReturn(null);
-        lenient().when(ruleManager.selectDistinctBatchNos()).thenReturn(Collections.emptyList());
-    }
-
-    private RuleSyncCardDTO card(String cardId) {
-        RuleSyncCardDTO c = new RuleSyncCardDTO();
-        c.setCardId(cardId);
-        c.setPlayer("p");
-        c.setYear("2026");
-        c.setSeries("s");
-        c.setCardSet("BASE");
-        // 评级时效字段已重构:旧 recommendEfficiency(int) -> 新 evaluateEfficiencyId(Long)
-        c.setEvaluateEfficiencyId(2L);
-        return c;
-    }
-
-    private RuleSyncRequest req(String batchNo, RuleSyncCardDTO... cards) {
-        RuleSyncRequest r = new RuleSyncRequest();
-        r.setBatchNo(batchNo);
-        r.setCards(new ArrayList<>(Arrays.asList(cards)));
-        return r;
-    }
-
-    private int code(AjaxResult r) {
-        return ((Number) r.get("code")).intValue();
-    }
-
-    @Test
-    void 正常_全量替换并切批次() {
-        AjaxResult result = ruleSyncService.sync(req("2026062501", card("CARD-1")));
-        assertEquals(0, code(result)); // 成功码 0
-        // 切指针到新批次
-        verify(activeBatchManager).updateActiveBatchNo("2026062501");
-        // 规则批量插入被调用
-        verify(ruleManager).batchInsert(ruleListCaptor.capture());
-        assertEquals(1, ruleListCaptor.getValue().size());
-    }
-
-    @Test
-    void 空batchNo_拒绝且不写库() {
-        AjaxResult result = ruleSyncService.sync(req("", card("CARD-1")));
-        assertNotEquals(0, code(result));
-        verify(ruleManager, never()).batchInsert(org.mockito.ArgumentMatchers.anyList());
-        verify(activeBatchManager, never()).updateActiveBatchNo(org.mockito.ArgumentMatchers.anyString());
-    }
-
-    @Test
-    void 空cards_拒绝() {
-        RuleSyncRequest r = new RuleSyncRequest();
-        r.setBatchNo("2026062501");
-        r.setCards(new ArrayList<>());
-        AjaxResult result = ruleSyncService.sync(r);
-        assertNotEquals(0, code(result));
-        verify(ruleManager, never()).batchInsert(org.mockito.ArgumentMatchers.anyList());
-    }
-
-    @Test
-    void 旧批次_拒绝回灌() {
-        when(activeBatchManager.selectActiveBatchNo()).thenReturn("2026062502");
-        AjaxResult result = ruleSyncService.sync(req("2026062501", card("CARD-1")));
-        assertNotEquals(0, code(result)); // 新批次 <= 生效批次,拒绝
-        verify(ruleManager, never()).batchInsert(org.mockito.ArgumentMatchers.anyList());
-    }
-
-    @Test
-    void cardId去重_重复只插一条() {
-        AjaxResult result = ruleSyncService.sync(req("2026062501", card("DUP"), card("DUP")));
-        assertEquals(0, code(result));
-        verify(ruleManager).batchInsert(ruleListCaptor.capture());
-        assertEquals(1, ruleListCaptor.getValue().size()); // 同 cardId 去重为 1
-    }
-}

+ 0 - 64
mango-application/src/test/java/com/mangoo/rating/recommend/test/ai/AgentLoopExecutorTest.java

@@ -1,64 +0,0 @@
-package com.mangoo.rating.recommend.test.ai;
-
-import com.mangoo.rating.recommend.ai.agent.AgentLoopExecutor;
-import com.mangoo.rating.recommend.ai.model.*;
-import com.mangoo.rating.recommend.ai.provider.ChatModelProvider;
-import com.mangoo.rating.recommend.ai.tool.ToolRegistry;
-import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
-
-import java.util.*;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
-
-class AgentLoopExecutorTest {
-
-    @Test
-    void runs_tool_then_finishes_and_streams_text() {
-        ChatModelProvider provider = Mockito.mock(ChatModelProvider.class);
-        ToolRegistry registry = Mockito.mock(ToolRegistry.class);
-        when(registry.execute(eq("order_logistics_query"), anyString(), anyString()))
-            .thenReturn("订单RO1状态:送评中");
-
-        ChatResult r1 = ChatResult.builder().stopReason("tool_use")
-            .toolCalls(Collections.singletonList(
-                ToolCall.builder().id("tu1").name("order_logistics_query").argumentsJson("{}").build())).build();
-        ChatResult r2 = ChatResult.builder().stopReason("end_turn").text("您的订单正在送评中").build();
-        when(provider.chat(anyString(), anyList(), anyList(), any()))
-            .thenAnswer(inv -> { ((StreamCallback) inv.getArgument(3)).onTextDelta(""); return r1; })
-            .thenAnswer(inv -> { ((StreamCallback) inv.getArgument(3)).onTextDelta("您的订单正在送评中"); return r2; });
-
-        AgentLoopExecutor exec = new AgentLoopExecutor(provider, registry);
-        StringBuilder streamed = new StringBuilder();
-        List<ChatMessage> msgs = new ArrayList<>();
-        msgs.add(ChatMessage.builder().role(Role.USER).content("我的订单到哪了").build());
-
-        String finalText = exec.run("dummy-header", "sys", msgs, Collections.emptyList(), 5, streamed::append);
-
-        assertEquals("您的订单正在送评中", finalText);
-        assertEquals("您的订单正在送评中", streamed.toString());
-        verify(registry).execute(eq("order_logistics_query"), eq("{}"), anyString());
-        assertTrue(msgs.stream().anyMatch(m -> m.getRole() == Role.TOOL));
-    }
-
-    @Test
-    void stops_at_max_iterations() {
-        ChatModelProvider provider = Mockito.mock(ChatModelProvider.class);
-        ToolRegistry registry = Mockito.mock(ToolRegistry.class);
-        when(registry.execute(anyString(), anyString(), anyString())).thenReturn("x");
-        ChatResult loop = ChatResult.builder().stopReason("tool_use")
-            .toolCalls(Collections.singletonList(
-                ToolCall.builder().id("t").name("any").argumentsJson("{}").build())).build();
-        when(provider.chat(anyString(), anyList(), anyList(), any())).thenReturn(loop);
-
-        AgentLoopExecutor exec = new AgentLoopExecutor(provider, registry);
-        List<ChatMessage> msgs = new ArrayList<>();
-        msgs.add(ChatMessage.builder().role(Role.USER).content("hi").build());
-
-        String text = exec.run("dummy-header", "sys", msgs, Collections.emptyList(), 3, s -> {});
-        assertNotNull(text);
-        verify(provider, times(3)).chat(anyString(), anyList(), anyList(), any());
-    }
-}

+ 0 - 46
mango-application/src/test/java/com/mangoo/rating/recommend/test/ai/AiChatServiceTest.java

@@ -1,46 +0,0 @@
-package com.mangoo.rating.recommend.test.ai;
-
-import com.mangoo.rating.recommend.ai.agent.AgentLoopExecutor;
-import com.mangoo.rating.recommend.ai.model.ChatMessage;
-import com.mangoo.rating.recommend.ai.model.Role;
-import com.mangoo.rating.recommend.ai.service.AiChatService;
-import com.mangoo.rating.recommend.ai.session.SessionContextStore;
-import com.mangoo.rating.recommend.ai.tool.ToolRegistry;
-import com.mangoo.rating.recommend.config.AiAssistantProperties;
-import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mockito;
-
-import java.util.*;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
-
-class AiChatServiceTest {
-
-    @Test
-    void loads_runs_and_saves_trimmed_context() {
-        SessionContextStore store = Mockito.mock(SessionContextStore.class);
-        AgentLoopExecutor exec = Mockito.mock(AgentLoopExecutor.class);
-        ToolRegistry registry = Mockito.mock(ToolRegistry.class);
-        AiAssistantProperties props = new AiAssistantProperties();
-        when(store.load("s1")).thenReturn(new ArrayList<>());
-        when(registry.specs()).thenReturn(Collections.emptyList());
-        when(exec.run(anyString(), anyString(), anyList(), anyList(), anyInt(), any()))
-            .thenReturn("您的订单正在送评中");
-
-        AiChatService svc = new AiChatService(store, exec, registry, props);
-        StringBuilder streamed = new StringBuilder();
-        svc.chat("dummy-header", "s1", "我的订单到哪了", streamed::append);
-
-        ArgumentCaptor<List<ChatMessage>> cap = ArgumentCaptor.forClass(List.class);
-        verify(store).save(eq("s1"), cap.capture(), anyInt());
-        List<ChatMessage> saved = cap.getValue();
-        assertEquals(2, saved.size());
-        assertEquals(Role.USER, saved.get(0).getRole());
-        assertEquals(Role.ASSISTANT, saved.get(1).getRole());
-        assertEquals("您的订单正在送评中", saved.get(1).getContent());
-        assertTrue(saved.stream().noneMatch(m -> m.getRole() == Role.TOOL));
-    }
-}

+ 0 - 56
mango-application/src/test/java/com/mangoo/rating/recommend/test/ai/ClaudeSseParserTest.java

@@ -1,56 +0,0 @@
-package com.mangoo.rating.recommend.test.ai;
-
-import com.mangoo.rating.recommend.ai.provider.ClaudeChatProvider;
-import com.mangoo.rating.recommend.ai.model.ChatResult;
-import org.junit.jupiter.api.Test;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-/**
- * Claude SSE 解析纯逻辑单测(不触网,仅验证 feedLine + SseAccumulator)。
- * 覆盖两类场景:纯文本 end_turn、tool_use 工具调用。
- */
-class ClaudeSseParserTest {
-
-    /** 文本增量逐段累积,message_delta 携带 end_turn */
-    @Test
-    void parses_text_delta_and_end_turn() {
-        List<String> lines = new ArrayList<>();
-        lines.add("data: {\"type\":\"content_block_delta\",\"delta\":{\"type\":\"text_delta\",\"text\":\"您好\"}}");
-        lines.add("data: {\"type\":\"content_block_delta\",\"delta\":{\"type\":\"text_delta\",\"text\":\",订单送评中\"}}");
-        lines.add("data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"}}");
-
-        // 同时校验流式回调推送内容与最终结果一致
-        StringBuilder streamed = new StringBuilder();
-        ClaudeChatProvider.SseAccumulator acc = new ClaudeChatProvider.SseAccumulator(streamed::append);
-        for (String l : lines) { ClaudeChatProvider.feedLine(l, acc); }
-        ChatResult r = acc.toResult();
-
-        assertEquals("end_turn", r.getStopReason());
-        assertEquals("您好,订单送评中", r.getText());
-        assertEquals("您好,订单送评中", streamed.toString());
-        assertTrue(r.getToolCalls() == null || r.getToolCalls().isEmpty());
-    }
-
-    /** 工具调用:content_block_start 起一个 tool_use,input_json_delta 拼参数,end 为 tool_use */
-    @Test
-    void parses_tool_use_block() {
-        List<String> lines = new ArrayList<>();
-        lines.add("data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"tu_1\",\"name\":\"order_logistics_query\"}}");
-        lines.add("data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"orderNo\\\":\\\"RO1\\\"}\"}}");
-        lines.add("data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"}}");
-
-        ClaudeChatProvider.SseAccumulator acc = new ClaudeChatProvider.SseAccumulator(s -> {});
-        for (String l : lines) { ClaudeChatProvider.feedLine(l, acc); }
-        ChatResult r = acc.toResult();
-
-        assertEquals("tool_use", r.getStopReason());
-        assertEquals(1, r.getToolCalls().size());
-        assertEquals("order_logistics_query", r.getToolCalls().get(0).getName());
-        assertEquals("tu_1", r.getToolCalls().get(0).getId());
-        assertTrue(r.getToolCalls().get(0).getArgumentsJson().contains("RO1"));
-    }
-}

+ 0 - 56
mango-application/src/test/java/com/mangoo/rating/recommend/test/ai/OrderLogisticsQueryToolTest.java

@@ -1,56 +0,0 @@
-package com.mangoo.rating.recommend.test.ai;
-
-import com.mangoo.rating.recommend.AjaxResult;
-import com.mangoo.rating.recommend.ai.tool.OrderLogisticsQueryTool;
-import com.mangoo.rating.recommend.client.feign.RatingOrderFeignClient;
-import com.mangoo.rating.recommend.page.TableDataInfo;
-import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
-
-import java.util.*;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.when;
-
-/**
- * 订单物流查询工具单元测试(Feign 远程调用,Mockito 打桩)。
- * 验证:① 有订单时组装出含订单号/状态的文本;② 无订单时返回友好提示。
- */
-class OrderLogisticsQueryToolTest {
-
-    /** 有最近订单:应组装出包含订单号与状态描述的结果文本 */
-    @Test
-    void query_recent_order_assembles_text() {
-        RatingOrderFeignClient feign = Mockito.mock(RatingOrderFeignClient.class);
-        // 打桩:我的订单列表返回一条记录
-        Map<String, Object> row = new HashMap<>();
-        row.put("orderId", 1001L); row.put("orderNo", "RO20260617001"); row.put("statusText", "送评中");
-        TableDataInfo page = new TableDataInfo();
-        page.setRows(Collections.singletonList(row));
-        when(feign.myOrderList(anyString(), any())).thenReturn(page);
-
-        // 打桩:订单详情成功(code=0),data 含物流状态描述
-        Map<String, Object> data = new HashMap<>();
-        data.put("orderNo", "RO20260617001"); data.put("orderStatusDesc", "送评中");
-        AjaxResult detail = new AjaxResult();
-        detail.put("code", 0); detail.put("data", data);
-        when(feign.orderDetail(anyString(), any())).thenReturn(detail);
-
-        OrderLogisticsQueryTool tool = new OrderLogisticsQueryTool(feign);
-        String text = tool.execute("{}", "dummy-header");
-        assertTrue(text.contains("RO20260617001"));
-        assertTrue(text.contains("送评中"));
-    }
-
-    /** 无订单:应返回包含「未查询到」的友好提示 */
-    @Test
-    void no_order_returns_friendly_text() {
-        RatingOrderFeignClient feign = Mockito.mock(RatingOrderFeignClient.class);
-        TableDataInfo empty = new TableDataInfo();
-        empty.setRows(Collections.emptyList());
-        when(feign.myOrderList(anyString(), any())).thenReturn(empty);
-        OrderLogisticsQueryTool tool = new OrderLogisticsQueryTool(feign);
-        assertTrue(tool.execute("{}", "dummy-header").contains("未查询到"));
-    }
-}

+ 0 - 55
mango-application/src/test/java/com/mangoo/rating/recommend/test/ai/SessionContextStoreTest.java

@@ -1,55 +0,0 @@
-package com.mangoo.rating.recommend.test.ai;
-
-import com.alibaba.fastjson.JSON;
-import com.mangoo.rating.recommend.ai.model.ChatMessage;
-import com.mangoo.rating.recommend.ai.model.Role;
-import com.mangoo.rating.recommend.ai.session.SessionContextStore;
-import com.mangoo.rating.recommend.utils.RedisUtils;
-import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mockito;
-
-import java.util.*;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
-
-class SessionContextStoreTest {
-
-    @Test
-    void save_writes_json_with_ttl_seconds() {
-        RedisUtils redis = Mockito.mock(RedisUtils.class);
-        SessionContextStore store = new SessionContextStore(redis);
-        List<ChatMessage> msgs = Collections.singletonList(
-            ChatMessage.builder().role(Role.USER).content("hi").build());
-        store.save("s1", msgs, 30);
-        ArgumentCaptor<String> key = ArgumentCaptor.forClass(String.class);
-        ArgumentCaptor<Object> val = ArgumentCaptor.forClass(Object.class);
-        ArgumentCaptor<Long> ttl = ArgumentCaptor.forClass(Long.class);
-        verify(redis).set(key.capture(), val.capture(), ttl.capture());
-        assertEquals("ai:chat:session:s1", key.getValue());
-        assertEquals(30L * 60, ttl.getValue());
-        assertTrue(((String) val.getValue()).contains("hi"));
-    }
-
-    @Test
-    void load_parses_json() {
-        RedisUtils redis = Mockito.mock(RedisUtils.class);
-        List<ChatMessage> seed = Collections.singletonList(
-            ChatMessage.builder().role(Role.ASSISTANT).content("hello").build());
-        when(redis.get("ai:chat:session:s1")).thenReturn(JSON.toJSONString(seed));
-        SessionContextStore store = new SessionContextStore(redis);
-        List<ChatMessage> got = store.load("s1");
-        assertEquals(1, got.size());
-        assertEquals("hello", got.get(0).getContent());
-    }
-
-    @Test
-    void load_missing_returns_empty() {
-        RedisUtils redis = Mockito.mock(RedisUtils.class);
-        when(redis.get(anyString())).thenReturn(null);
-        SessionContextStore store = new SessionContextStore(redis);
-        assertTrue(store.load("none").isEmpty());
-    }
-}

+ 0 - 34
mango-application/src/test/java/com/mangoo/rating/recommend/test/ai/ToolRegistryTest.java

@@ -1,34 +0,0 @@
-package com.mangoo.rating.recommend.test.ai;
-
-import com.mangoo.rating.recommend.ai.model.ToolSpec;
-import com.mangoo.rating.recommend.ai.tool.AiTool;
-import com.mangoo.rating.recommend.ai.tool.ToolRegistry;
-import org.junit.jupiter.api.Test;
-import java.util.Collections;
-import java.util.List;
-import static org.junit.jupiter.api.Assertions.*;
-
-class ToolRegistryTest {
-    static class EchoTool implements AiTool {
-        public String name() { return "echo"; }
-        public String description() { return "回显入参"; }
-        public String inputSchemaJson() { return "{\"type\":\"object\",\"properties\":{}}"; }
-        public String execute(String argumentsJson, String userHeader) { return "echo:" + argumentsJson; }
-    }
-
-    @Test
-    void specs_and_route_execute() {
-        ToolRegistry reg = new ToolRegistry(Collections.singletonList(new EchoTool()));
-        List<ToolSpec> specs = reg.specs();
-        assertEquals(1, specs.size());
-        assertEquals("echo", specs.get(0).getName());
-        assertEquals("echo:{}", reg.execute("echo", "{}", "dummy-header"));
-    }
-
-    @Test
-    void unknown_tool_returns_error_text_not_throw() {
-        ToolRegistry reg = new ToolRegistry(Collections.singletonList(new EchoTool()));
-        String r = reg.execute("not_exist", "{}", "dummy-header");
-        assertTrue(r.contains("未知工具"));
-    }
-}

+ 0 - 44
mango-domain/src/main/java/com/mangoo/rating/recommend/ai/agent/AgentLoopExecutor.java

@@ -1,44 +0,0 @@
-package com.mangoo.rating.recommend.ai.agent;
-
-import com.mangoo.rating.recommend.ai.model.*;
-import com.mangoo.rating.recommend.ai.provider.ChatModelProvider;
-import com.mangoo.rating.recommend.ai.tool.ToolRegistry;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-
-import java.util.Collections;
-import java.util.List;
-
-/** tool use 循环:调模型→若 tool_use 则执行工具并回填→再调→直到 end_turn 或 maxIterations。纯编排,可单测。 */
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class AgentLoopExecutor {
-
-    private final ChatModelProvider provider;
-    private final ToolRegistry toolRegistry;
-
-    /** @param userHeader X-USER-BASE64 原始值,透传给工具;messages 会被就地追加中间消息;@return 最终文本 */
-    public String run(String userHeader, String systemPrompt, List<ChatMessage> messages,
-                      List<ToolSpec> tools, int maxIterations, StreamCallback cb) {
-        String lastText = "";
-        for (int i = 0; i < maxIterations; i++) {
-            ChatResult r = provider.chat(systemPrompt, messages, tools, cb);
-            lastText = r.getText() == null ? "" : r.getText();
-            if (!"tool_use".equals(r.getStopReason())) {
-                return lastText; // end_turn:文本已流式推完
-            }
-            messages.add(ChatMessage.builder()
-                .role(Role.ASSISTANT).content(lastText).toolCalls(r.getToolCalls()).build());
-            List<ToolCall> calls = r.getToolCalls() == null ? Collections.emptyList() : r.getToolCalls();
-            for (ToolCall call : calls) {
-                String result = toolRegistry.execute(call.getName(), call.getArgumentsJson(), userHeader);
-                messages.add(ChatMessage.builder()
-                    .role(Role.TOOL).toolCallId(call.getId()).content(result).build());
-            }
-        }
-        log.warn("Agent loop 达到 maxIterations={},强制收尾", maxIterations);
-        return lastText.isEmpty() ? "抱歉,本次未能完成查询,请稍后再试。" : lastText;
-    }
-}

+ 0 - 13
mango-domain/src/main/java/com/mangoo/rating/recommend/ai/provider/ChatModelProvider.java

@@ -1,13 +0,0 @@
-package com.mangoo.rating.recommend.ai.provider;
-
-import com.mangoo.rating.recommend.ai.model.ChatMessage;
-import com.mangoo.rating.recommend.ai.model.ChatResult;
-import com.mangoo.rating.recommend.ai.model.StreamCallback;
-import com.mangoo.rating.recommend.ai.model.ToolSpec;
-import java.util.List;
-
-/** 模型抽象(可插拔):一轮调用,流式文本经 callback 推送 */
-public interface ChatModelProvider {
-    ChatResult chat(String systemPrompt, List<ChatMessage> messages,
-                    List<ToolSpec> tools, StreamCallback callback);
-}

+ 0 - 190
mango-domain/src/main/java/com/mangoo/rating/recommend/ai/provider/ClaudeChatProvider.java

@@ -1,190 +0,0 @@
-package com.mangoo.rating.recommend.ai.provider;
-
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONArray;
-import com.alibaba.fastjson.JSONObject;
-import com.mangoo.rating.recommend.ai.model.*;
-import com.mangoo.rating.recommend.config.AiAssistantProperties;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import okhttp3.*;
-import okio.BufferedSource;
-import org.springframework.stereotype.Component;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Claude 模型实现(Anthropic 原生 /v1/messages,stream=true)。OkHttp 手动读 SSE;
- * 报文映射内聚本类,未来切国内模型新增 Provider 即可。
- */
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class ClaudeChatProvider implements ChatModelProvider {
-
-    private final AiAssistantProperties props;
-
-    @Override
-    public ChatResult chat(String systemPrompt, List<ChatMessage> messages,
-                           List<ToolSpec> tools, StreamCallback callback) {
-        String body = buildRequestBody(systemPrompt, messages, tools);
-        OkHttpClient client = new OkHttpClient.Builder()
-            .connectTimeout(props.getConnectTimeoutMs(), TimeUnit.MILLISECONDS)
-            .readTimeout(props.getReadTimeoutMs(), TimeUnit.MILLISECONDS)
-            .build();
-        Request req = new Request.Builder()
-            .url(props.getBaseUrl() + "/v1/messages")
-//            .addHeader("Authorization", "Bearer " + props.getApiKey())
-            .addHeader("Authorization", "Bearer sk-bd900939754858f55372b32f0617f71dbf3f850cae9d1d1d112643b307c9356a")
-            .addHeader("anthropic-version", "2023-06-01")
-            .addHeader("content-type", "application/json")
-            .post(RequestBody.create(MediaType.parse("application/json"), body))
-            .build();
-
-        SseAccumulator acc = new SseAccumulator(callback);
-        try (Response resp = client.newCall(req).execute()) {
-            if (!resp.isSuccessful() || resp.body() == null) {
-                throw new RuntimeException("Claude 网关返回异常: HTTP " + resp.code());
-            }
-            BufferedSource source = resp.body().source();
-            String line;
-            while ((line = source.readUtf8Line()) != null) {
-                feedLine(line, acc);
-            }
-        } catch (Exception e) {
-            log.warn("调用 Claude 网关失败", e);
-            throw new RuntimeException("调用模型失败: " + e.getMessage(), e);
-        }
-        return acc.toResult();
-    }
-
-    /** 组装 Anthropic 原生请求体 */
-    private String buildRequestBody(String systemPrompt, List<ChatMessage> messages, List<ToolSpec> tools) {
-        JSONObject root = new JSONObject(true);
-        root.put("model", props.getModel());
-        root.put("max_tokens", props.getMaxTokens());
-        root.put("stream", true);
-        if (systemPrompt != null && !systemPrompt.isEmpty()) { root.put("system", systemPrompt); }
-        if (tools != null && !tools.isEmpty()) {
-            JSONArray ts = new JSONArray();
-            for (ToolSpec t : tools) {
-                JSONObject to = new JSONObject(true);
-                to.put("name", t.getName());
-                to.put("description", t.getDescription());
-                to.put("input_schema", JSON.parseObject(t.getInputSchemaJson()));
-                ts.add(to);
-            }
-            root.put("tools", ts);
-        }
-        JSONArray ms = new JSONArray();
-        for (ChatMessage m : messages) {
-            JSONObject mo = new JSONObject(true);
-            switch (m.getRole()) {
-                case USER:
-                    mo.put("role", "user");
-                    mo.put("content", m.getContent());
-                    break;
-                case ASSISTANT:
-                    mo.put("role", "assistant");
-                    if (m.getToolCalls() != null && !m.getToolCalls().isEmpty()) {
-                        JSONArray blocks = new JSONArray();
-                        if (m.getContent() != null && !m.getContent().isEmpty()) {
-                            JSONObject tb = new JSONObject(true);
-                            tb.put("type", "text"); tb.put("text", m.getContent()); blocks.add(tb);
-                        }
-                        for (ToolCall c : m.getToolCalls()) {
-                            JSONObject ub = new JSONObject(true);
-                            ub.put("type", "tool_use"); ub.put("id", c.getId()); ub.put("name", c.getName());
-                            ub.put("input", JSON.parseObject(c.getArgumentsJson() == null ? "{}" : c.getArgumentsJson()));
-                            blocks.add(ub);
-                        }
-                        mo.put("content", blocks);
-                    } else {
-                        mo.put("content", m.getContent());
-                    }
-                    break;
-                case TOOL:
-                    mo.put("role", "user");
-                    JSONArray rb = new JSONArray();
-                    JSONObject tr = new JSONObject(true);
-                    tr.put("type", "tool_result"); tr.put("tool_use_id", m.getToolCallId());
-                    tr.put("content", m.getContent());
-                    rb.add(tr);
-                    mo.put("content", rb);
-                    break;
-                default: continue;
-            }
-            ms.add(mo);
-        }
-        root.put("messages", ms);
-        return root.toJSONString();
-    }
-
-    /** 累积一次流式响应(纯逻辑,可单测) */
-    public static class SseAccumulator {
-        private final StreamCallback cb;
-        private final StringBuilder text = new StringBuilder();
-        private String stopReason = "end_turn";
-        private final List<ToolCall> toolCalls = new ArrayList<>();
-        private String curToolId, curToolName;
-        private final StringBuilder curToolArgs = new StringBuilder();
-
-        public SseAccumulator(StreamCallback cb) { this.cb = cb; }
-
-        void appendText(String t) { if (t != null) { text.append(t); if (cb != null) cb.onTextDelta(t); } }
-        void startTool(String id, String name) { flushTool(); curToolId = id; curToolName = name; curToolArgs.setLength(0); }
-        void appendToolArgs(String partial) { if (partial != null) curToolArgs.append(partial); }
-        void setStop(String s) { if (s != null) stopReason = s; }
-        void flushTool() {
-            if (curToolId != null) {
-                toolCalls.add(ToolCall.builder().id(curToolId).name(curToolName)
-                    .argumentsJson(curToolArgs.length() == 0 ? "{}" : curToolArgs.toString()).build());
-                curToolId = null; curToolName = null; curToolArgs.setLength(0);
-            }
-        }
-        public ChatResult toResult() {
-            flushTool();
-            return ChatResult.builder().stopReason(stopReason).text(text.toString()).toolCalls(toolCalls).build();
-        }
-    }
-
-    /** 处理一行 SSE(仅关心 data: 行 JSON)。Anthropic 原生事件。 */
-    public static void feedLine(String line, SseAccumulator acc) {
-        if (line == null) return;
-        line = line.trim();
-        if (line.isEmpty() || !line.startsWith("data:")) return;
-        String json = line.substring("data:".length()).trim();
-        if (json.equals("[DONE]") || json.isEmpty()) return;
-        JSONObject ev;
-        try { ev = JSON.parseObject(json); } catch (Exception e) { return; }
-        String type = ev.getString("type");
-        if (type == null) return;
-        switch (type) {
-            case "content_block_start": {
-                JSONObject cbk = ev.getJSONObject("content_block");
-                if (cbk != null && "tool_use".equals(cbk.getString("type"))) {
-                    acc.startTool(cbk.getString("id"), cbk.getString("name"));
-                }
-                break;
-            }
-            case "content_block_delta": {
-                JSONObject d = ev.getJSONObject("delta");
-                if (d == null) break;
-                if ("text_delta".equals(d.getString("type"))) {
-                    acc.appendText(d.getString("text"));
-                } else if ("input_json_delta".equals(d.getString("type"))) {
-                    acc.appendToolArgs(d.getString("partial_json"));
-                }
-                break;
-            }
-            case "message_delta": {
-                JSONObject d = ev.getJSONObject("delta");
-                if (d != null && d.getString("stop_reason") != null) { acc.setStop(d.getString("stop_reason")); }
-                break;
-            }
-            default: break;
-        }
-    }
-}

+ 0 - 50
mango-domain/src/main/java/com/mangoo/rating/recommend/ai/service/AiChatService.java

@@ -1,50 +0,0 @@
-package com.mangoo.rating.recommend.ai.service;
-
-import com.mangoo.rating.recommend.ai.agent.AgentLoopExecutor;
-import com.mangoo.rating.recommend.ai.model.ChatMessage;
-import com.mangoo.rating.recommend.ai.model.Role;
-import com.mangoo.rating.recommend.ai.model.StreamCallback;
-import com.mangoo.rating.recommend.ai.session.SessionContextStore;
-import com.mangoo.rating.recommend.ai.tool.ToolRegistry;
-import com.mangoo.rating.recommend.config.AiAssistantProperties;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/** 助手门面:取上下文 → 驱动 loop → 流式回写 → 回存精简上下文。 */
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class AiChatService {
-
-    private static final String SYSTEM_PROMPT =
-        "你是卡牌评级小程序的业务助手,只回答评级、订单、物流、卡片相关问题。"
-      + "需要查询用户数据时调用提供的工具;用简洁中文友好作答;"
-      + "遇到与评级业务无关或越界的请求,礼貌拒绝。";
-
-    private final SessionContextStore store;
-    private final AgentLoopExecutor executor;
-    private final ToolRegistry toolRegistry;
-    private final AiAssistantProperties props;
-
-    /** @param userHeader X-USER-BASE64 原始值,透传给工具 Feign 调用 */
-    public void chat(String userHeader, String sessionId, String message, StreamCallback cb) {
-        // 1. 历史精简上下文
-        List<ChatMessage> history = store.load(sessionId);
-        // 2. 工作消息 = 历史 + 本轮用户消息(system 单独传,不入 messages)
-        List<ChatMessage> working = new ArrayList<>(history);
-        ChatMessage userMsg = ChatMessage.builder().role(Role.USER).content(message).build();
-        working.add(userMsg);
-        // 3. 驱动 loop(working 会被就地追加 tool_use/tool_result 中间态)
-        String finalText = executor.run(userHeader, SYSTEM_PROMPT, working,
-            toolRegistry.specs(), props.getMaxIterations(), cb);
-        // 4. 回存精简上下文:历史 + 用户问 + 助手终答(剔除 TOOL 中间态)
-        List<ChatMessage> trimmed = new ArrayList<>(history);
-        trimmed.add(userMsg);
-        trimmed.add(ChatMessage.builder().role(Role.ASSISTANT).content(finalText).build());
-        store.save(sessionId, trimmed, props.getSessionTtlMinutes());
-    }
-}

+ 0 - 57
mango-domain/src/main/java/com/mangoo/rating/recommend/ai/session/SessionContextStore.java

@@ -1,57 +0,0 @@
-package com.mangoo.rating.recommend.ai.session;
-
-import com.alibaba.fastjson.JSON;
-import com.mangoo.rating.recommend.ai.model.ChatMessage;
-import com.mangoo.rating.recommend.utils.RedisUtils;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/** 会话短期上下文:Redis 存精简对话(user问+assistant终答),带 TTL。 */
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class SessionContextStore {
-
-    /** 会话上下文 key 前缀,与 ratingApp 共用同一套 Redis,故加业务命名空间隔离 */
-    private static final String KEY_PREFIX = "ai:chat:session:";
-    private final RedisUtils redisUtils;
-
-    /**
-     * 读取会话上下文;缺失或解析失败均降级为空列表(不影响主流程)。
-     *
-     * @param sessionId 会话标识
-     * @return 历史对话消息;无则返回空列表(非 null)
-     */
-    public List<ChatMessage> load(String sessionId) {
-        try {
-            Object v = redisUtils.get(KEY_PREFIX + sessionId);
-            if (v == null) { return new ArrayList<>(); }
-            // fastjson 反序列化为内部模型列表
-            List<ChatMessage> list = JSON.parseArray(v.toString(), ChatMessage.class);
-            return list == null ? new ArrayList<>() : list;
-        } catch (Exception e) {
-            log.warn("读取会话上下文失败 sessionId={},降级为空", sessionId, e);
-            return new ArrayList<>();
-        }
-    }
-
-    /**
-     * 写入会话上下文(覆盖);失败仅记录告警,不抛出(短期缓存丢失可容忍)。
-     *
-     * @param sessionId  会话标识
-     * @param messages   待持久化的精简对话
-     * @param ttlMinutes 过期时间(分钟)→ 转为 RedisUtils.set 所需的秒
-     */
-    public void save(String sessionId, List<ChatMessage> messages, int ttlMinutes) {
-        try {
-            // ttlMinutes 分钟 → RedisUtils.set 的秒(time>0 才会设过期)
-            redisUtils.set(KEY_PREFIX + sessionId, JSON.toJSONString(messages), (long) ttlMinutes * 60);
-        } catch (Exception e) {
-            log.warn("写入会话上下文失败 sessionId={}", sessionId, e);
-        }
-    }
-}

+ 0 - 16
mango-domain/src/main/java/com/mangoo/rating/recommend/ai/tool/AiTool.java

@@ -1,16 +0,0 @@
-package com.mangoo.rating.recommend.ai.tool;
-
-/** AI 工具抽象:name/description 供模型决定何时调用;execute 透传 userHeader(X-USER-BASE64 原始值)给下游 Feign */
-public interface AiTool {
-    /** 工具名称(模型按此名路由调用) */
-    String name();
-
-    /** 工具用途描述(供模型判断何时调用) */
-    String description();
-
-    /** 入参 JSON Schema 字符串(描述 argumentsJson 的结构) */
-    String inputSchemaJson();                         // JSON Schema
-
-    /** 执行工具;argumentsJson 为模型给出的入参,userHeader 为 X-USER-BASE64 原始值,透传给下游 Feign;返回结果文本 */
-    String execute(String argumentsJson, String userHeader); // 返回结果文本
-}

+ 0 - 111
mango-domain/src/main/java/com/mangoo/rating/recommend/ai/tool/OrderLogisticsQueryTool.java

@@ -1,111 +0,0 @@
-package com.mangoo.rating.recommend.ai.tool;
-
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
-import com.mangoo.rating.recommend.AjaxResult;
-import com.mangoo.rating.recommend.client.feign.RatingOrderFeignClient;
-import com.mangoo.rating.recommend.page.TableDataInfo;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * 订单物流查询工具:通过 Feign 远程调 ratingApp 订单接口组装文本结果。
- * 不写 SQL、不做任何修改操作;用户隔离由透传 X-USER-BASE64 头在 ratingApp 侧完成。
- *
- * @author gengjintao
- */
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class OrderLogisticsQueryTool implements AiTool {
-
-    /** 远程订单/物流 Feign 客户端 */
-    private final RatingOrderFeignClient ratingOrderFeignClient;
-
-    @Override
-    public String name() {
-        return "order_logistics_query";
-    }
-
-    @Override
-    public String description() {
-        return "查询当前用户的订单状态与物流轨迹。当用户问『订单到哪了/物流进度/快递信息/我的送评进度』时调用。"
-             + "可选参数 orderNo 指定订单号;不传则返回最近订单。";
-    }
-
-    @Override
-    public String inputSchemaJson() {
-        return "{\"type\":\"object\",\"properties\":{\"orderNo\":{\"type\":\"string\","
-             + "\"description\":\"订单号,可选;不传查最近订单\"}},\"required\":[]}";
-    }
-
-    @Override
-    public String execute(String argumentsJson, String userHeader) {
-        // 1. 解析可选 orderNo(参数为空时查最近订单)
-        String wantOrderNo = null;
-        if (argumentsJson != null && !argumentsJson.trim().isEmpty()) {
-            JSONObject in = JSON.parseObject(argumentsJson);
-            wantOrderNo = in == null ? null : in.getString("orderNo");
-        }
-        // 2. Feign 取当前用户最近订单(透传 X-USER-BASE64,由 ratingApp 隔离)
-        Map<String, Object> listBody = new HashMap<>();
-        listBody.put("page", 1);
-        listBody.put("rows", 10);
-        TableDataInfo page = ratingOrderFeignClient.myOrderList(userHeader, listBody);
-        List<?> rows = page == null ? null : page.getRows();
-        if (rows == null || rows.isEmpty()) {
-            return "未查询到您的订单。";
-        }
-        // 3. 选定目标订单(rows 元素一般为 Map,统一转 JSONObject 取字段)
-        JSONObject target = null;
-        for (Object o : rows) {
-            JSONObject r = toJson(o);
-            if (r == null) { continue; }
-            if (wantOrderNo == null || wantOrderNo.isEmpty()) { target = r; break; }
-            if (wantOrderNo.equals(r.getString("orderNo"))) { target = r; break; }
-        }
-        if (target == null) {
-            return "未查询到订单号 " + wantOrderNo + " 的物流信息。";
-        }
-        Long orderId = target.getLong("orderId");
-        String orderNo = target.getString("orderNo");
-        String statusText = target.getString("statusText");
-        // 4. Feign 取订单详情(含物流轨迹)
-        Map<String, Object> detailBody = new HashMap<>();
-        detailBody.put("orderId", orderId);
-        AjaxResult detail = ratingOrderFeignClient.orderDetail(userHeader, detailBody);
-        Object code = detail == null ? null : detail.get("code");
-        // 详情失败时降级为列表里的状态(code=0 为成功)
-        if (code == null || !"0".equals(String.valueOf(code))) {
-            return "订单 " + nz(orderNo) + " 当前状态:" + nz(statusText) + "。";
-        }
-        JSONObject data = toJson(detail.get("data"));
-        return buildText(data, orderNo, statusText);
-    }
-
-    /** 依据订单详情 data 组装结果文本(缺字段时回退到列表状态) */
-    private String buildText(JSONObject d, String orderNo, String statusText) {
-        if (d == null) { return "订单 " + nz(orderNo) + " 当前状态:" + nz(statusText) + "。"; }
-        String no = d.getString("orderNo") != null ? d.getString("orderNo") : orderNo;
-        StringBuilder sb = new StringBuilder();
-        sb.append("订单号:").append(nz(no)).append(";状态:").append(nz(d.getString("orderStatusDesc")));
-        Object logistics = d.get("logisticsInfo");
-        if (logistics != null) { sb.append(";物流:").append(JSON.toJSONString(logistics)); }
-        return sb.toString();
-    }
-
-    /** Feign 反序列化结果(通常为 Map)统一转 fastjson JSONObject,便于弱类型取字段 */
-    private JSONObject toJson(Object o) {
-        if (o == null) { return null; }
-        if (o instanceof JSONObject) { return (JSONObject) o; }
-        return JSON.parseObject(JSON.toJSONString(o));
-    }
-
-    /** null 安全:空值返回空串,避免文本里出现 "null" */
-    private String nz(String s) { return s == null ? "" : s; }
-}

+ 0 - 42
mango-domain/src/main/java/com/mangoo/rating/recommend/ai/tool/ToolRegistry.java

@@ -1,42 +0,0 @@
-package com.mangoo.rating.recommend.ai.tool;
-
-import com.mangoo.rating.recommend.ai.model.ToolSpec;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-
-import java.util.*;
-import java.util.stream.Collectors;
-
-/** 收集所有 AiTool(Spring 构造注入 List),向模型提供声明并按 name 路由执行 */
-@Slf4j
-@Component
-public class ToolRegistry {
-    /** 工具按 name 索引;用 LinkedHashMap 保持注入顺序,便于 specs() 输出稳定 */
-    private final Map<String, AiTool> byName;
-
-    public ToolRegistry(List<AiTool> tools) {
-        this.byName = new LinkedHashMap<>();
-        for (AiTool t : tools) { this.byName.put(t.name(), t); }
-    }
-
-    /** 输出所有工具声明,供模型决定调用 */
-    public List<ToolSpec> specs() {
-        return byName.values().stream()
-            .map(t -> ToolSpec.builder()
-                .name(t.name()).description(t.description())
-                .inputSchemaJson(t.inputSchemaJson()).build())
-            .collect(Collectors.toList());
-    }
-
-    /** 路由执行;未知工具/异常都返回文本(不抛硬错,交模型兜底) */
-    public String execute(String name, String argumentsJson, String userHeader) {
-        AiTool tool = byName.get(name);
-        if (tool == null) { return "未知工具: " + name; }
-        try {
-            return tool.execute(argumentsJson, userHeader);
-        } catch (Exception e) {
-            log.warn("工具执行失败 name={}", name, e);
-            return "工具执行失败: " + e.getMessage();
-        }
-    }
-}