|
@@ -0,0 +1,423 @@
|
|
|
|
|
+package com.etotem.cfc.unit;
|
|
|
|
|
+
|
|
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
|
|
+import com.etotem.cfc.controller.EnergyController;
|
|
|
|
|
+import com.etotem.cfc.controller.wish.WishController;
|
|
|
|
|
+import com.etotem.cfc.dto.CreateWishDTO;
|
|
|
|
|
+import com.etotem.cfc.entity.EnergyBalance;
|
|
|
|
|
+import com.etotem.cfc.entity.EnergyDimension;
|
|
|
|
|
+import com.etotem.cfc.entity.EnergyLog;
|
|
|
|
|
+import com.etotem.cfc.entity.FamilyMember;
|
|
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
|
|
+import com.etotem.cfc.entity.Wish;
|
|
|
|
|
+import com.etotem.cfc.mapper.EnergyBalanceMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.EnergyDimensionMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.EnergyLogMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.WishMapper;
|
|
|
|
|
+import com.etotem.cfc.service.CfSpendDispatchService;
|
|
|
|
|
+import com.etotem.cfc.service.EnergyService;
|
|
|
|
|
+import com.etotem.cfc.service.NotificationDispatchService;
|
|
|
|
|
+import com.etotem.cfc.service.WishService;
|
|
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+import org.springframework.web.bind.annotation.PostMapping;
|
|
|
|
|
+import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
|
|
+
|
|
|
|
|
+import java.lang.reflect.Method;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.Arrays;
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+import java.util.HashSet;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+import java.util.Set;
|
|
|
|
|
+
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.fail;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 契约级单测:能力键 {@code child.wish.manage}(SC-C-04 提心愿)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>溯源·需求</b>:{@code tests/user-scenarios/应用场景总览.md} SC-C-04(提心愿)——
|
|
|
|
|
+ * 建心愿(名称+积分)→ 进家长待审批;边界:积分超额提示;被驳回不可兑。</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>溯源·设计</b>:API_REFERENCE §4.4 心愿(/api/wishes):create / set-price / reject /
|
|
|
|
|
+ * exchange / approve-exchange / fulfill(任务书引 §4.6,实际心愿章节在 §4.4,已按实锚定);
|
|
|
|
|
+ * EnergyController /deduct-for-wish(积分扣减)。</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>承担端点</b>:{@link WishController} /api/wishes(create/set-price/reject/exchange/
|
|
|
|
|
+ * approve-exchange/fulfill)+ {@link EnergyController} /deduct-for-wish。</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>实测结论(2026-11-13)</b>:本能力<b>实现已落地</b> —— 6 个契约端点、WishService 状态机
|
|
|
|
|
+ * (draft→active→pending_exchange→approved→exchanging→fulfilled/cancelled/rejected)、
|
|
|
|
|
+ * EnergyService.deductForWish(扣 action 维度)与 CfSpendDispatchService.spendForMember
|
|
|
|
|
+ * (refType=wish_exchange)均已在 origin/cfclub 存在,且已被 {@code WishFlowScP13ContractTest}
|
|
|
|
|
+ * (SC-C-04/SC-P-13/14/88,15 用例全绿)等既有测试覆盖。按任务交付纪律(任务要求 #2:
|
|
|
|
|
+ * 「若实测发现实现已存在(测试意外全绿),在汇报中注明并交编排侧转测试登记,不私改实现」),
|
|
|
|
|
+ * 本类作为「契约登记验证」新增:预期 <b>全绿(非红)</b>,供编排侧转测试登记;
|
|
|
|
|
+ * 不修改 src/main 实现。</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>测试范围</b>:纯 Mockito / 反射,无 Spring / 无 DB / 无外部服务。
|
|
|
|
|
+ * 覆盖:① 6 个契约端点 + /deduct-for-wish 的 @PostMapping 路由契约(反射);
|
|
|
|
|
+ * ② SC-C-04 WishService 提心愿状态机(正常流 / 积分不足拦截 / 被驳回不可兑 / 越权拒绝);
|
|
|
|
|
+ * ③ EnergyService.deductForWish 真实扣减行为(余额充足/不足/非正金额);
|
|
|
|
|
+ * ④ EnergyController /deduct-for-wish 参数契约。</p>
|
|
|
|
|
+ */
|
|
|
|
|
+class ChildWishManageContractTest extends EnergyContractTestBase {
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 契约锚点常量(API_REFERENCE §4.4) ====================
|
|
|
|
|
+
|
|
|
|
|
+ /** WishController 6 个契约端点(任务书设计锚点 create/set-price/reject/exchange/approve-exchange/fulfill) */
|
|
|
|
|
+ private static final String[] WISH_CONTRACT_PATHS = {
|
|
|
|
|
+ "/create", "/{id}/set-price", "/{id}/reject",
|
|
|
|
|
+ "/{id}/exchange", "/{id}/approve-exchange", "/{id}/fulfill"
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ private static final String ENERGY_DEDUCT_FOR_WISH_PATH = "/deduct-for-wish";
|
|
|
|
|
+
|
|
|
|
|
+ /** 心愿宿主类全名(反射锚定) */
|
|
|
|
|
+ private static final String WISH_CONTROLLER_CLASS = "com.etotem.cfc.controller.wish.WishController";
|
|
|
|
|
+ private static final String ENERGY_CONTROLLER_CLASS = "com.etotem.cfc.controller.EnergyController";
|
|
|
|
|
+
|
|
|
|
|
+ // ---- SC-C-04 服务层(WishService)----
|
|
|
|
|
+ private WishService wishService;
|
|
|
|
|
+ private WishMapper wishMapper;
|
|
|
|
|
+ private FamilyMemberMapper familyMemberMapper;
|
|
|
|
|
+ private UserMapper userMapper;
|
|
|
|
|
+ private CfSpendDispatchService cfSpendDispatchService;
|
|
|
|
|
+
|
|
|
|
|
+ /** 捕获 DB 中已插入/更新的心愿 */
|
|
|
|
|
+ private final List<Wish> wishStore = new ArrayList<>();
|
|
|
|
|
+ private long idSeq = 5000L;
|
|
|
|
|
+
|
|
|
|
|
+ // ---- /deduct-for-wish 服务层(EnergyService)----
|
|
|
|
|
+ private EnergyService energyService;
|
|
|
|
|
+ private EnergyDimensionMapper dimensionMapper;
|
|
|
|
|
+ private EnergyBalanceMapper balanceMapper;
|
|
|
|
|
+ private EnergyLogMapper energyLogMapper;
|
|
|
|
|
+ private NotificationDispatchService notificationDispatchService;
|
|
|
|
|
+
|
|
|
|
|
+ @BeforeEach
|
|
|
|
|
+ void setUp() throws Exception {
|
|
|
|
|
+ // ---------- WishService 装配(仿 WishFlowScP13ContractTest) ----------
|
|
|
|
|
+ wishService = new WishService();
|
|
|
|
|
+ wishMapper = mock(WishMapper.class);
|
|
|
|
|
+ familyMemberMapper = mock(FamilyMemberMapper.class);
|
|
|
|
|
+ userMapper = mock(UserMapper.class);
|
|
|
|
|
+ cfSpendDispatchService = mock(CfSpendDispatchService.class);
|
|
|
|
|
+
|
|
|
|
|
+ inject(wishService, "wishMapper", wishMapper);
|
|
|
|
|
+ inject(wishService, "familyMemberMapper", familyMemberMapper);
|
|
|
|
|
+ inject(wishService, "userMapper", userMapper);
|
|
|
|
|
+ inject(wishService, "cfSpendDispatchService", cfSpendDispatchService);
|
|
|
|
|
+
|
|
|
|
|
+ when(wishMapper.insert(any(Wish.class))).thenAnswer(inv -> {
|
|
|
|
|
+ Wish w = inv.getArgument(0);
|
|
|
|
|
+ if (w.getId() == null) w.setId(++idSeq);
|
|
|
|
|
+ wishStore.add(w);
|
|
|
|
|
+ return 1;
|
|
|
|
|
+ });
|
|
|
|
|
+ when(wishMapper.selectById(any(Long.class))).thenAnswer(inv -> {
|
|
|
|
|
+ Long id = inv.getArgument(0);
|
|
|
|
|
+ return wishStore.stream().filter(w -> w.getId().equals(id)).findFirst().orElse(null);
|
|
|
|
|
+ });
|
|
|
|
|
+ when(wishMapper.updateById(any(Wish.class))).thenAnswer(inv -> {
|
|
|
|
|
+ Wish w = inv.getArgument(0);
|
|
|
|
|
+ for (int i = 0; i < wishStore.size(); i++) {
|
|
|
|
|
+ if (wishStore.get(i).getId().equals(w.getId())) wishStore.set(i, w);
|
|
|
|
|
+ }
|
|
|
|
|
+ return 1;
|
|
|
|
|
+ });
|
|
|
|
|
+ // 默认:家庭1、成员B 归属,余额 1000,无重复待审批
|
|
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user(1L, 1L));
|
|
|
|
|
+ when(familyMemberMapper.selectById(100L)).thenReturn(member(100L, 1L, 1L, "孩子"));
|
|
|
|
|
+ when(cfSpendDispatchService.getAvailableForMember(100L)).thenReturn(1000);
|
|
|
|
|
+
|
|
|
|
|
+ // ---------- EnergyService 装配(仿 EnergyGrantContractTest) ----------
|
|
|
|
|
+ energyService = new EnergyService();
|
|
|
|
|
+ dimensionMapper = mock(EnergyDimensionMapper.class);
|
|
|
|
|
+ balanceMapper = mock(EnergyBalanceMapper.class);
|
|
|
|
|
+ energyLogMapper = mock(EnergyLogMapper.class);
|
|
|
|
|
+ notificationDispatchService = mock(NotificationDispatchService.class);
|
|
|
|
|
+
|
|
|
|
|
+ inject(energyService, "energyDimensionMapper", dimensionMapper);
|
|
|
|
|
+ inject(energyService, "energyBehaviorConfigMapper",
|
|
|
|
|
+ mock(com.etotem.cfc.mapper.EnergyBehaviorConfigMapper.class));
|
|
|
|
|
+ inject(energyService, "energyLogMapper", energyLogMapper);
|
|
|
|
|
+ inject(energyService, "energyBalanceMapper", balanceMapper);
|
|
|
|
|
+ inject(energyService, "familyMemberMapper", familyMemberMapper);
|
|
|
|
|
+ inject(energyService, "notificationDispatchService", notificationDispatchService);
|
|
|
|
|
+ inject(energyService, "energySourceConfigMapper",
|
|
|
|
|
+ mock(com.etotem.cfc.mapper.EnergySourceConfigMapper.class));
|
|
|
|
|
+ inject(energyService, "wishMapper", mock(WishMapper.class));
|
|
|
|
|
+
|
|
|
|
|
+ when(dimensionMapper.selectList(any())).thenReturn(Arrays.asList(fiveDims()));
|
|
|
|
|
+ when(familyMemberMapper.selectById(any(Long.class))).thenReturn(member(100L, 1L, 1L, "孩子"));
|
|
|
|
|
+
|
|
|
|
|
+ initTable(EnergyDimension.class);
|
|
|
|
|
+ initTable(EnergyLog.class);
|
|
|
|
|
+ initTable(EnergyBalance.class);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // =====================================================================
|
|
|
|
|
+ // ① 端点契约(反射):6 个 /api/wishes 端点 + /deduct-for-wish
|
|
|
|
|
+ // =====================================================================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("端点契约:WishController /api/wishes 暴露 6 个契约端点")
|
|
|
|
|
+ void wishController_exposesSixContractEndpoints() {
|
|
|
|
|
+ Class<?> clazz = loadClass(WISH_CONTROLLER_CLASS);
|
|
|
|
|
+ RequestMapping classMapping = clazz.getAnnotation(RequestMapping.class);
|
|
|
|
|
+ assertNotNull(classMapping, "WishController 应声明类级 @RequestMapping");
|
|
|
|
|
+ assertTrue(Arrays.asList(classMapping.value()).contains("/api/wishes"),
|
|
|
|
|
+ "WishController 类级路由应为 /api/wishes,实际: " + Arrays.toString(classMapping.value()));
|
|
|
|
|
+
|
|
|
|
|
+ Set<String> postPaths = postMappingPaths(clazz);
|
|
|
|
|
+ for (String path : WISH_CONTRACT_PATHS) {
|
|
|
|
|
+ assertTrue(postPaths.contains(path),
|
|
|
|
|
+ "契约端点 POST /api/wishes" + path + " 应已注册(API_REFERENCE §4.4),实际已注册: " + postPaths);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("端点契约:EnergyController 暴露 /deduct-for-wish 积分扣减端点")
|
|
|
|
|
+ void energyController_exposesDeductForWishEndpoint() {
|
|
|
|
|
+ Class<?> clazz = loadClass(ENERGY_CONTROLLER_CLASS);
|
|
|
|
|
+ Set<String> postPaths = postMappingPaths(clazz);
|
|
|
|
|
+ assertTrue(postPaths.contains(ENERGY_DEDUCT_FOR_WISH_PATH),
|
|
|
|
|
+ "契约端点 POST /api/energy/deduct-for-wish 应已注册,实际已注册: " + postPaths);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // =====================================================================
|
|
|
|
|
+ // ② SC-C-04 提心愿(WishService 状态机)
|
|
|
|
|
+ // =====================================================================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-C-04 正常流:孩子提心愿 → draft → 家长定价 → active")
|
|
|
|
|
+ void c04_正常流_提心愿_定价_active() {
|
|
|
|
|
+ Long id = wishService.createByChild(100L, childWish("买乐高"));
|
|
|
|
|
+ Wish w = current(id);
|
|
|
|
|
+ assertNotNull(w, "提心愿应落库");
|
|
|
|
|
+ assertEquals("draft", w.getStatus(), "孩子建心愿初始为 draft(进家长待审批)");
|
|
|
|
|
+ assertEquals(100L, w.getMemberId().longValue(), "心愿归属成员应为发起者");
|
|
|
|
|
+ assertEquals("child", w.getCreatorType());
|
|
|
|
|
+ assertEquals(0, w.getRejectCount().intValue());
|
|
|
|
|
+
|
|
|
|
|
+ boolean ok = wishService.setPrice(id, 1L, 500);
|
|
|
|
|
+ assertTrue(ok, "家长定价应成功");
|
|
|
|
|
+ assertEquals("active", current(id).getStatus(), "定价后应 active");
|
|
|
|
|
+ assertEquals(500, current(id).getPointsRequired().intValue());
|
|
|
|
|
+ assertEquals(1L, current(id).getPointsSetBy().longValue());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-C-04 正常流:active 心愿完整兑换-兑现交付闭环")
|
|
|
|
|
+ void c04_正常流_兑换审批兑现闭环() {
|
|
|
|
|
+ Long id = wishService.createByChild(100L, childWish("玩具"));
|
|
|
|
|
+ wishService.setPrice(id, 1L, 300);
|
|
|
|
|
+
|
|
|
|
|
+ assertTrue(wishService.requestExchange(id, 100L), "申请兑换应成功");
|
|
|
|
|
+ assertEquals("pending_exchange", current(id).getStatus());
|
|
|
|
|
+
|
|
|
|
|
+ assertTrue(wishService.approveExchange(id, 1L, true), "家长批准兑换应成功");
|
|
|
|
|
+ assertEquals("approved", current(id).getStatus(), "批准后应 approved");
|
|
|
|
|
+ verify(cfSpendDispatchService).spendForMember(100L, 300, "wish_exchange", id, "兑换心愿: 玩具");
|
|
|
|
|
+
|
|
|
|
|
+ assertTrue(wishService.markFulfilled(id, 1L), "家长标记已购买应成功");
|
|
|
|
|
+ assertEquals("exchanging", current(id).getStatus());
|
|
|
|
|
+
|
|
|
|
|
+ assertTrue(wishService.confirmReceived(id, 100L), "确认收货应成功");
|
|
|
|
|
+ assertEquals("fulfilled", current(id).getStatus());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-C-04 边界:积分不足申请兑换被拦截(积分超额提示)")
|
|
|
|
|
+ void c04_边界_积分不足拦截_状态保持active() {
|
|
|
|
|
+ when(cfSpendDispatchService.getAvailableForMember(100L)).thenReturn(10);
|
|
|
|
|
+ Long id = wishService.createByChild(100L, childWish("高价心愿"));
|
|
|
|
|
+ wishService.setPrice(id, 1L, 300);
|
|
|
|
|
+
|
|
|
|
|
+ RuntimeException e = assertThrows(RuntimeException.class, () -> wishService.requestExchange(id, 100L));
|
|
|
|
|
+ assertTrue(e.getMessage().contains("积分不足"), "实际: " + e.getMessage());
|
|
|
|
|
+ assertEquals("active", current(id).getStatus(), "积分不足不应改变状态");
|
|
|
|
|
+ verify(cfSpendDispatchService, never()).spendForMember(any(), any(Integer.class), any(), any(), any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-C-04 边界:被驳回不可兑(rejected 后申请兑换被拒)")
|
|
|
|
|
+ void c04_边界_被驳回不可兑() {
|
|
|
|
|
+ Long id = wishService.createByChild(100L, childWish("被驳回心愿"));
|
|
|
|
|
+ assertTrue(wishService.rejectWish(id, 1L, "积分超额"), "家长驳回应成功");
|
|
|
|
|
+ assertEquals("rejected", current(id).getStatus());
|
|
|
|
|
+ assertEquals(1, current(id).getRejectCount().intValue());
|
|
|
|
|
+
|
|
|
|
|
+ assertFalse(wishService.requestExchange(id, 100L), "被驳回心愿不可再申请兑换(被驳回不可兑)");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-C-04 权限流:非心愿归属成员申请兑换被拒")
|
|
|
|
|
+ void c04_权限流_非归属成员申请兑换_拒绝() {
|
|
|
|
|
+ Long id = wishService.createByChild(100L, childWish("他人心愿"));
|
|
|
|
|
+ wishService.setPrice(id, 1L, 10);
|
|
|
|
|
+ assertFalse(wishService.requestExchange(id, 200L), "非归属成员申请兑换应被拒");
|
|
|
|
|
+ verify(cfSpendDispatchService, never()).spendForMember(any(), any(Integer.class), any(), any(), any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // =====================================================================
|
|
|
|
|
+ // ③ EnergyService.deductForWish 真实扣减行为(/deduct-for-wish 服务契约)
|
|
|
|
|
+ // =====================================================================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("能量扣减正常流:余额充足 → 扣 action 维度并返回 true")
|
|
|
|
|
+ void deductForWish_余额充足_扣减成功() {
|
|
|
|
|
+ EnergyBalance balance = balance(100L, 3L, 100);
|
|
|
|
|
+ when(balanceMapper.selectOne(any())).thenReturn(balance);
|
|
|
|
|
+
|
|
|
|
|
+ Boolean result = energyService.deductForWish(100L, 5000L, 30L);
|
|
|
|
|
+
|
|
|
|
|
+ assertTrue(result, "余额充足应扣减成功");
|
|
|
|
|
+ assertEquals(70, balance.getBalance().intValue(), "余额应扣减 30");
|
|
|
|
|
+ verify(balanceMapper).updateById(any(EnergyBalance.class));
|
|
|
|
|
+ verify(energyLogMapper).insert(any(EnergyLog.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("能量扣减异常流:余额不足 → 返回 false 且不写流水")
|
|
|
|
|
+ void deductForWish_余额不足_返回false() {
|
|
|
|
|
+ EnergyBalance balance = balance(100L, 3L, 10);
|
|
|
|
|
+ when(balanceMapper.selectOne(any())).thenReturn(balance);
|
|
|
|
|
+
|
|
|
|
|
+ Boolean result = energyService.deductForWish(100L, 5001L, 30L);
|
|
|
|
|
+
|
|
|
|
|
+ assertFalse(result, "余额不足应返回 false");
|
|
|
|
|
+ assertEquals(10, balance.getBalance().intValue(), "余额不应变化");
|
|
|
|
|
+ verify(balanceMapper, never()).updateById(any(EnergyBalance.class));
|
|
|
|
|
+ verify(energyLogMapper, never()).insert(any(EnergyLog.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("能量扣减异常流:金额非正抛非法参数")
|
|
|
|
|
+ void deductForWish_金额非正_抛非法参数() {
|
|
|
|
|
+ when(balanceMapper.selectOne(any())).thenReturn(balance(100L, 3L, 100));
|
|
|
|
|
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
|
|
|
|
|
+ () -> energyService.deductForWish(100L, 5002L, 0L));
|
|
|
|
|
+ assertTrue(e.getMessage().contains("扣除数量必须为正数"), "实际: " + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // =====================================================================
|
|
|
|
|
+ // ④ EnergyController /deduct-for-wish 参数契约
|
|
|
|
|
+ // =====================================================================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("deduct-for-wish 参数契约:必填缺失返回业务错误")
|
|
|
|
|
+ void deductForWish_api_必填缺失_返回错误() throws Exception {
|
|
|
|
|
+ EnergyController controller = new EnergyController();
|
|
|
|
|
+ inject(controller, "energyService", energyService);
|
|
|
|
|
+ inject(controller, "userMapper", mock(UserMapper.class));
|
|
|
|
|
+ inject(controller, "unlockGateService", mock(com.etotem.cfc.service.UnlockGateService.class));
|
|
|
|
|
+ inject(controller, "fiveDimensionScoreService",
|
|
|
|
|
+ mock(com.etotem.cfc.service.FiveDimensionScoreService.class));
|
|
|
|
|
+
|
|
|
|
|
+ Result<Boolean> result = controller.deductForWish(new HashMap<>(), 1L, null);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(500, result.getCode().intValue());
|
|
|
|
|
+ assertTrue(result.getMessage().contains("不能为空"), "实际: " + result.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("deduct-for-wish 参数契约:无家庭返回 5001")
|
|
|
|
|
+ void deductForWish_api_无家庭_返回5001() throws Exception {
|
|
|
|
|
+ EnergyController controller = new EnergyController();
|
|
|
|
|
+ inject(controller, "energyService", energyService);
|
|
|
|
|
+ inject(controller, "userMapper", mock(UserMapper.class));
|
|
|
|
|
+ inject(controller, "unlockGateService", mock(com.etotem.cfc.service.UnlockGateService.class));
|
|
|
|
|
+ inject(controller, "fiveDimensionScoreService",
|
|
|
|
|
+ mock(com.etotem.cfc.service.FiveDimensionScoreService.class));
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> params = new HashMap<>();
|
|
|
|
|
+ params.put("memberId", 100L);
|
|
|
|
|
+ params.put("wishId", 5000L);
|
|
|
|
|
+ params.put("amount", 30L);
|
|
|
|
|
+
|
|
|
|
|
+ Result<Boolean> result = controller.deductForWish(params, null, null);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(5001, result.getCode().intValue(), "无家庭应返回 5001 noFamily");
|
|
|
|
|
+ assertTrue(result.getMessage().contains("家庭"), "实际: " + result.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // =====================================================================
|
|
|
|
|
+ // 辅助构造
|
|
|
|
|
+ // =====================================================================
|
|
|
|
|
+
|
|
|
|
|
+ private static Class<?> loadClass(String name) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return Class.forName(name);
|
|
|
|
|
+ } catch (ClassNotFoundException e) {
|
|
|
|
|
+ fail("宿主类 " + name + " 不存在: " + e.getMessage());
|
|
|
|
|
+ throw new IllegalStateException("unreachable");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 收集类上所有 @PostMapping 的 value 路径 */
|
|
|
|
|
+ private static Set<String> postMappingPaths(Class<?> clazz) {
|
|
|
|
|
+ Set<String> paths = new HashSet<>();
|
|
|
|
|
+ for (Method m : clazz.getDeclaredMethods()) {
|
|
|
|
|
+ PostMapping pm = m.getAnnotation(PostMapping.class);
|
|
|
|
|
+ if (pm != null) {
|
|
|
|
|
+ paths.addAll(Arrays.asList(pm.value()));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return paths;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static User user(Long id, Long familyId) {
|
|
|
|
|
+ User u = new User();
|
|
|
|
|
+ u.setId(id);
|
|
|
|
|
+ u.setFamilyId(familyId);
|
|
|
|
|
+ return u;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static FamilyMember member(Long id, Long familyId, Long userId, String nickname) {
|
|
|
|
|
+ FamilyMember m = new FamilyMember();
|
|
|
|
|
+ m.setId(id);
|
|
|
|
|
+ m.setFamilyId(familyId);
|
|
|
|
|
+ m.setUserId(userId);
|
|
|
|
|
+ m.setNickname(nickname);
|
|
|
|
|
+ return m;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static CreateWishDTO childWish(String title) {
|
|
|
|
|
+ CreateWishDTO dto = new CreateWishDTO();
|
|
|
|
|
+ dto.setTitle(title);
|
|
|
|
|
+ return dto;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 能量余额构造(totalEarned/totalSpent 必须非空,deductEnergy 会做 totalSpent + amount 自增) */
|
|
|
|
|
+ private static EnergyBalance balance(Long memberId, Long dimId, int balance) {
|
|
|
|
|
+ EnergyBalance b = new EnergyBalance();
|
|
|
|
|
+ b.setMemberId(memberId);
|
|
|
|
|
+ b.setDimensionId(dimId);
|
|
|
|
|
+ b.setBalance(balance);
|
|
|
|
|
+ b.setTotalEarned(0);
|
|
|
|
|
+ b.setTotalSpent(0);
|
|
|
|
|
+ return b;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 在 store 中检索指定 id 的当前心愿对象 */
|
|
|
|
|
+ private Wish current(Long id) {
|
|
|
|
|
+ return wishStore.stream().filter(w -> w.getId().equals(id)).findFirst().orElse(null);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|