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.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.junit.jupiter.api.Assertions.assertTrue; 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) 落库包字段映射:10 个识别字段 + userId/totalCards/packageId 全透传 * * @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; } /** * 构造一条"合法且能通过 isLegalRule 校验"的 EXACT 命中规则。 * 注意 evaluateEfficiencyName 在 PO 中是 Long(非 String),此处按真实签名给 3L。 */ private RatingRecommendRulePO exactRule() { RatingRecommendRulePO po = new RatingRecommendRulePO(); po.setEvaluateEfficiencyId(3L); po.setEvaluateEfficiencyName(3L); po.setCardId("DW-CARD-1"); return po; } /** 构造 Feign 成功返回:data 里带 preOrderNo */ private AjaxResult okWith(String preOrderNo) { Map data = new HashMap<>(); data.put("preOrderNo", preOrderNo); return AjaxResult.success(data); } /** 默认让匹配命中、价格可取、用户已登录(各用例可覆盖) */ private void defaultHappyStubs() { lenient().when(recommendProperties.getBatchMax()).thenReturn(50); lenient().when(activeBatchManager.selectActiveBatchNo()).thenReturn("B1"); lenient().when(ratingRecommendRuleManager.selectByExact(any(), any(), any(), any(), any())) .thenReturn(exactRule()); Map prices = new HashMap<>(); prices.put(3L, new BigDecimal("200.00")); lenient().when(efficiencyPriceProvider.loadEfficiencyPrices()).thenReturn(prices); 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(); 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("金箔"); c.setId(88L); c.setPrice(new BigDecimal("120")); recommendService.recommendEfficiency(req(c)); // 抓取实际传给 Feign 的 PreOrderSaveDTO ArgumentCaptor 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()); // 时效价可取到时应大于 0 assertTrue(card.getEfficiencyPrice().compareTo(BigDecimal.ZERO) > 0); } }