|
|
@@ -0,0 +1,463 @@
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
|
|
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
|
+import com.etotem.cfc.config.ConfigService;
|
|
|
+import com.etotem.cfc.dto.RewardEstimateVO;
|
|
|
+import com.etotem.cfc.entity.CfBalanceLog;
|
|
|
+import com.etotem.cfc.entity.CfEarnDailyLimit;
|
|
|
+import com.etotem.cfc.entity.CfEarnRule;
|
|
|
+import com.etotem.cfc.entity.FamilyMember;
|
|
|
+import com.etotem.cfc.entity.GrowthTask;
|
|
|
+import com.etotem.cfc.entity.Product;
|
|
|
+import com.etotem.cfc.entity.Reward;
|
|
|
+import com.etotem.cfc.entity.Task;
|
|
|
+import com.etotem.cfc.mapper.CfBalanceLogMapper;
|
|
|
+import com.etotem.cfc.mapper.CfEarnDailyLimitMapper;
|
|
|
+import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
|
+import com.etotem.cfc.mapper.GrowthTaskMapper;
|
|
|
+import com.etotem.cfc.mapper.ProductMapper;
|
|
|
+import com.etotem.cfc.mapper.RewardMapper;
|
|
|
+import com.etotem.cfc.mapper.TaskMapper;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import org.apache.ibatis.builder.MapperBuilderAssistant;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.Mock;
|
|
|
+import org.mockito.MockitoAnnotations;
|
|
|
+import org.springframework.test.util.ReflectionTestUtils;
|
|
|
+
|
|
|
+import java.sql.Date;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
|
|
+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.assertNull;
|
|
|
+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.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * SC-X-22 · {@code reward.cfEstimate.expectedCf} 契约级单测({@link RewardRuleService})。
|
|
|
+ *
|
|
|
+ * <p>对应契约 docs/superpowers/specs/2026-11-05-sc-x22-cf-estimate-contract.md:
|
|
|
+ * <ul>
|
|
|
+ * <li>§2.1 服务端唯一计算方:按当日有效规则算 expectedCf(正=获得、负=消耗、0=无奖励);</li>
|
|
|
+ * <li>§3 产出点三类文案(获得型 / 消耗型 / 零值禁「+0 CF」);</li>
|
|
|
+ * <li>§5.2 日限触顶按剩余额度、不超上限;</li>
|
|
|
+ * <li>§8 规则未配置 / 未生效 / 内部异常 → 降级「CF 奖励以实际为准」,不抛 500。</li>
|
|
|
+ * </ul>
|
|
|
+ * 纯 Mockito 单测:不依赖 DB / 真实微信 / Spring 容器;不改 src/main。
|
|
|
+ * 溯源:TC-X-22 / TC-X-22-E / TC-X-22-E2(tests/test-cases/cross-role-e2e.md)。
|
|
|
+ */
|
|
|
+class RewardRuleServiceEstimateTest {
|
|
|
+
|
|
|
+ private static final long MEMBER_ID = 7L;
|
|
|
+ private static final long FAMILY_ID = 1L;
|
|
|
+ private static final long USER_ID = 100L;
|
|
|
+
|
|
|
+ @Mock
|
|
|
+ private CfEarnRuleService cfEarnRuleService;
|
|
|
+ @Mock
|
|
|
+ private CfBalanceLogMapper cfBalanceLogMapper;
|
|
|
+ @Mock
|
|
|
+ private CfEarnDailyLimitMapper cfEarnDailyLimitMapper;
|
|
|
+ @Mock
|
|
|
+ private ConfigService configService;
|
|
|
+ @Mock
|
|
|
+ private FamilyMemberMapper familyMemberMapper;
|
|
|
+ @Mock
|
|
|
+ private TaskMapper taskMapper;
|
|
|
+ @Mock
|
|
|
+ private GrowthTaskMapper growthTaskMapper;
|
|
|
+ @Mock
|
|
|
+ private ProductMapper productMapper;
|
|
|
+ @Mock
|
|
|
+ private RewardMapper rewardMapper;
|
|
|
+ @Mock
|
|
|
+ private MembershipService membershipService;
|
|
|
+
|
|
|
+ private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+ private final RewardRuleService rewardRuleService = new RewardRuleService();
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ MockitoAnnotations.openMocks(this);
|
|
|
+ // MyBatis-Plus LambdaQueryWrapper 需要实体元数据(参照 CfEarnDispatchServiceTest)
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), CfBalanceLog.class);
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), CfEarnDailyLimit.class);
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), FamilyMember.class);
|
|
|
+
|
|
|
+ ReflectionTestUtils.setField(rewardRuleService, "cfEarnRuleService", cfEarnRuleService);
|
|
|
+ ReflectionTestUtils.setField(rewardRuleService, "cfBalanceLogMapper", cfBalanceLogMapper);
|
|
|
+ ReflectionTestUtils.setField(rewardRuleService, "cfEarnDailyLimitMapper", cfEarnDailyLimitMapper);
|
|
|
+ ReflectionTestUtils.setField(rewardRuleService, "configService", configService);
|
|
|
+ ReflectionTestUtils.setField(rewardRuleService, "familyMemberMapper", familyMemberMapper);
|
|
|
+ ReflectionTestUtils.setField(rewardRuleService, "taskMapper", taskMapper);
|
|
|
+ ReflectionTestUtils.setField(rewardRuleService, "growthTaskMapper", growthTaskMapper);
|
|
|
+ ReflectionTestUtils.setField(rewardRuleService, "productMapper", productMapper);
|
|
|
+ ReflectionTestUtils.setField(rewardRuleService, "rewardMapper", rewardMapper);
|
|
|
+ ReflectionTestUtils.setField(rewardRuleService, "objectMapper", objectMapper);
|
|
|
+ ReflectionTestUtils.setField(rewardRuleService, "membershipService", membershipService);
|
|
|
+
|
|
|
+ // 默认:无日限、无会员等级、无今日已发放;1 元 = 100 CF
|
|
|
+ when(cfBalanceLogMapper.selectCount(any())).thenReturn(0L);
|
|
|
+ when(cfBalanceLogMapper.selectObjs(any())).thenReturn(new ArrayList<>());
|
|
|
+ when(cfEarnDailyLimitMapper.selectOne(any())).thenReturn(null);
|
|
|
+ when(membershipService.getCurrentLevel(any())).thenReturn(null);
|
|
|
+ when(configService.getConfig(anyString())).thenReturn(null);
|
|
|
+ when(configService.getConfig("cf.rate.cf_per_yuan")).thenReturn("100");
|
|
|
+ when(familyMemberMapper.selectById(MEMBER_ID)).thenReturn(member(MEMBER_ID, FAMILY_ID));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 一、获得型正常路径(TC-X-22) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void taskComplete_固定规则20_返回获得型预估与统一文案() {
|
|
|
+ when(cfEarnRuleService.getByCode("F22")).thenReturn(rule("F22", "fixed", null, null, null, 20));
|
|
|
+ when(taskMapper.selectById(101L)).thenReturn(task(101L, FAMILY_ID, 20));
|
|
|
+
|
|
|
+ RewardEstimateVO vo = rewardRuleService.estimate(MEMBER_ID, "TASK_COMPLETE", "TASK", "101", "key-1");
|
|
|
+
|
|
|
+ assertEquals(20, vo.getExpectedCf());
|
|
|
+ assertNull(vo.getActualCf());
|
|
|
+ assertNull(vo.getReason());
|
|
|
+ assertEquals("CF", vo.getCurrency());
|
|
|
+ assertEquals("TASK_COMPLETE", vo.getOperationType());
|
|
|
+ assertEquals("TASK", vo.getResourceType());
|
|
|
+ assertEquals("101", vo.getResourceId());
|
|
|
+ assertEquals("key-1", vo.getIdempotencyKey());
|
|
|
+ assertEquals("EARN", vo.getDisplay().getType());
|
|
|
+ assertEquals("本操作预计获得 20 CF", vo.getDisplay().getTemplate());
|
|
|
+ assertEquals(20, vo.getDisplay().getCfValue());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void audit_与任务完成共享F22规则_审核发分预估() {
|
|
|
+ when(cfEarnRuleService.getByCode("F22")).thenReturn(rule("F22", "fixed", null, null, null, 20));
|
|
|
+ when(taskMapper.selectById(102L)).thenReturn(task(102L, FAMILY_ID, 20));
|
|
|
+
|
|
|
+ RewardEstimateVO vo = rewardRuleService.estimate(MEMBER_ID, "AUDIT", "TASK", "102", null);
|
|
|
+
|
|
|
+ assertEquals(20, vo.getExpectedCf());
|
|
|
+ assertEquals("EARN", vo.getDisplay().getType());
|
|
|
+ assertEquals("本操作预计获得 20 CF", vo.getDisplay().getTemplate());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void purchase_公式F02_99元商品按1元100CF估9900() {
|
|
|
+ when(cfEarnRuleService.getByCode("F02")).thenReturn(rule("F02", "formula", "floor(amt)",
|
|
|
+ "{\"cf_per_yuan\":100,\"amt_unit\":\"cent\"}", null, null));
|
|
|
+ // price 单位为分:9900 分 = 99 元 → 9900 CF
|
|
|
+ when(productMapper.selectById(77L)).thenReturn(product(77L, 9900));
|
|
|
+
|
|
|
+ RewardEstimateVO vo = rewardRuleService.estimate(MEMBER_ID, "PURCHASE", "PRODUCT", "77", null);
|
|
|
+
|
|
|
+ assertEquals(9900, vo.getExpectedCf());
|
|
|
+ assertEquals("EARN", vo.getDisplay().getType());
|
|
|
+ assertEquals("本操作预计获得 9900 CF", vo.getDisplay().getTemplate());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void growthTask_按任务奖励积分perItem预估() {
|
|
|
+ when(cfEarnRuleService.getByCode("F12")).thenReturn(rule("F12", "per_item", null, null, null, null));
|
|
|
+ when(growthTaskMapper.selectById(55L)).thenReturn(growthTask(55L, 10));
|
|
|
+
|
|
|
+ RewardEstimateVO vo = rewardRuleService.estimate(MEMBER_ID, "GROWTH_TASK", "GROWTH_TASK", "55", null);
|
|
|
+
|
|
|
+ assertEquals(10, vo.getExpectedCf());
|
|
|
+ assertEquals("EARN", vo.getDisplay().getType());
|
|
|
+ assertEquals("本操作预计获得 10 CF", vo.getDisplay().getTemplate());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 二、消耗型(TC-X-22-E2 第 2 步) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void wish_心愿兑换_负向消耗文案不误用获得型() {
|
|
|
+ when(rewardMapper.selectById(9L)).thenReturn(reward(9L, FAMILY_ID, 50));
|
|
|
+
|
|
|
+ RewardEstimateVO vo = rewardRuleService.estimate(MEMBER_ID, "WISH", "REWARD", "9", null);
|
|
|
+
|
|
|
+ assertEquals(-50, vo.getExpectedCf());
|
|
|
+ assertEquals("SPEND", vo.getDisplay().getType());
|
|
|
+ assertEquals("本操作将消耗 50 CF", vo.getDisplay().getTemplate());
|
|
|
+ assertEquals(-50, vo.getDisplay().getCfValue());
|
|
|
+ assertFalse(vo.getDisplay().getTemplate().contains("获得"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void wish_心愿无消耗值_零值省略文案不展示加0CF() {
|
|
|
+ when(rewardMapper.selectById(10L)).thenReturn(reward(10L, FAMILY_ID, null));
|
|
|
+
|
|
|
+ RewardEstimateVO vo = rewardRuleService.estimate(MEMBER_ID, "WISH", "REWARD", "10", null);
|
|
|
+
|
|
|
+ assertEquals(0, vo.getExpectedCf());
|
|
|
+ assertEquals("ZERO", vo.getDisplay().getType());
|
|
|
+ assertNull(vo.getDisplay().getTemplate());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 三、零值(TC-X-22-E2 第 1 步,禁止「+0 CF」) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 规则值为0_返回零值展示且不出现加0CF文案() {
|
|
|
+ when(cfEarnRuleService.getByCode("F22")).thenReturn(rule("F22", "fixed", null, null, null, 0));
|
|
|
+ when(taskMapper.selectById(103L)).thenReturn(task(103L, FAMILY_ID, 20));
|
|
|
+
|
|
|
+ RewardEstimateVO vo = rewardRuleService.estimate(MEMBER_ID, "TASK_COMPLETE", "TASK", "103", null);
|
|
|
+
|
|
|
+ assertEquals(0, vo.getExpectedCf());
|
|
|
+ assertEquals("ZERO", vo.getDisplay().getType());
|
|
|
+ assertNull(vo.getDisplay().getTemplate(), "零值必须省略预估文案,禁止展示「+0 CF」");
|
|
|
+ assertEquals(0, vo.getDisplay().getCfValue());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 四、降级(TC-X-22-E 第 4 步 / 契约 §8) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 规则未配置_降级为以实际为准且不抛异常() {
|
|
|
+ when(cfEarnRuleService.getByCode("F22")).thenReturn(null);
|
|
|
+
|
|
|
+ RewardEstimateVO vo = assertDoesNotThrow(
|
|
|
+ () -> rewardRuleService.estimate(MEMBER_ID, "TASK_COMPLETE", "TASK", "101", null));
|
|
|
+
|
|
|
+ assertNull(vo.getExpectedCf());
|
|
|
+ assertEquals("DEGRADED", vo.getDisplay().getType());
|
|
|
+ assertEquals("CF 奖励以实际为准", vo.getDisplay().getTemplate());
|
|
|
+ assertEquals("CF", vo.getCurrency());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 规则未启用_降级为以实际为准() {
|
|
|
+ CfEarnRule disabled = rule("F22", "fixed", null, null, null, 20);
|
|
|
+ disabled.setEnabled(0);
|
|
|
+ when(cfEarnRuleService.getByCode("F22")).thenReturn(disabled);
|
|
|
+
|
|
|
+ RewardEstimateVO vo = rewardRuleService.estimate(MEMBER_ID, "TASK_COMPLETE", "TASK", "101", null);
|
|
|
+
|
|
|
+ assertNull(vo.getExpectedCf());
|
|
|
+ assertEquals("DEGRADED", vo.getDisplay().getType());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 规则不在生效期_降级为以实际为准() {
|
|
|
+ CfEarnRule future = rule("F22", "fixed", null, null, null, 20);
|
|
|
+ future.setEffectiveFrom(Date.valueOf(LocalDate.now().plusDays(1)));
|
|
|
+ when(cfEarnRuleService.getByCode("F22")).thenReturn(future);
|
|
|
+
|
|
|
+ RewardEstimateVO vo = rewardRuleService.estimate(MEMBER_ID, "TASK_COMPLETE", "TASK", "101", null);
|
|
|
+
|
|
|
+ assertNull(vo.getExpectedCf());
|
|
|
+ assertEquals("DEGRADED", vo.getDisplay().getType());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 任务不存在_降级不泄露规则数值() {
|
|
|
+ when(cfEarnRuleService.getByCode("F22")).thenReturn(rule("F22", "fixed", null, null, null, 20));
|
|
|
+ when(taskMapper.selectById(404L)).thenReturn(null);
|
|
|
+
|
|
|
+ RewardEstimateVO vo = rewardRuleService.estimate(MEMBER_ID, "TASK_COMPLETE", "TASK", "404", null);
|
|
|
+
|
|
|
+ assertNull(vo.getExpectedCf());
|
|
|
+ assertEquals("DEGRADED", vo.getDisplay().getType());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 任务积分为0_降级不返回虚假预估() {
|
|
|
+ when(cfEarnRuleService.getByCode("F22")).thenReturn(rule("F22", "fixed", null, null, null, 20));
|
|
|
+ when(taskMapper.selectById(105L)).thenReturn(task(105L, FAMILY_ID, 0));
|
|
|
+
|
|
|
+ RewardEstimateVO vo = rewardRuleService.estimate(MEMBER_ID, "TASK_COMPLETE", "TASK", "105", null);
|
|
|
+
|
|
|
+ assertNull(vo.getExpectedCf());
|
|
|
+ assertEquals("DEGRADED", vo.getDisplay().getType());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 规则读取内部异常_降级不向调用方抛500() {
|
|
|
+ when(cfEarnRuleService.getByCode("F22")).thenThrow(new RuntimeException("db down"));
|
|
|
+
|
|
|
+ RewardEstimateVO vo = assertDoesNotThrow(
|
|
|
+ () -> rewardRuleService.estimate(MEMBER_ID, "TASK_COMPLETE", "TASK", "101", null));
|
|
|
+
|
|
|
+ assertNull(vo.getExpectedCf());
|
|
|
+ assertEquals("DEGRADED", vo.getDisplay().getType());
|
|
|
+ assertEquals("CF 奖励以实际为准", vo.getDisplay().getTemplate());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 五、日上限(TC-X-22-E 第 2 步 / 契约 §5.2) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 次数日限已触顶_预估按剩余额度0展示为无奖励() {
|
|
|
+ CfEarnRule limited = rule("F22", "fixed", null, null, 1, 20);
|
|
|
+ when(cfEarnRuleService.getByCode("F22")).thenReturn(limited);
|
|
|
+ when(taskMapper.selectById(106L)).thenReturn(task(106L, FAMILY_ID, 20));
|
|
|
+ when(cfBalanceLogMapper.selectCount(any())).thenReturn(1L); // 今日已发放 1 次 = 上限
|
|
|
+
|
|
|
+ RewardEstimateVO vo = rewardRuleService.estimate(MEMBER_ID, "TASK_COMPLETE", "TASK", "106", null);
|
|
|
+
|
|
|
+ assertEquals(0, vo.getExpectedCf());
|
|
|
+ assertEquals("ZERO", vo.getDisplay().getType());
|
|
|
+ assertNull(vo.getDisplay().getTemplate());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 金额日限未触顶_按剩余额度缩小预估不超上限() {
|
|
|
+ CfEarnRule limited = rule("F22", "fixed", null, "{\"daily_limit_amount\":100}", null, 20);
|
|
|
+ when(cfEarnRuleService.getByCode("F22")).thenReturn(limited);
|
|
|
+ when(taskMapper.selectById(107L)).thenReturn(task(107L, FAMILY_ID, 20));
|
|
|
+ when(cfBalanceLogMapper.selectObjs(any())).thenReturn(Arrays.asList(90L)); // 今日已得 90
|
|
|
+
|
|
|
+ RewardEstimateVO vo = rewardRuleService.estimate(MEMBER_ID, "TASK_COMPLETE", "TASK", "107", null);
|
|
|
+
|
|
|
+ assertEquals(10, vo.getExpectedCf());
|
|
|
+ assertEquals("EARN", vo.getDisplay().getType());
|
|
|
+ assertEquals("本操作预计获得 10 CF", vo.getDisplay().getTemplate());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 六、幂等(契约 §5.1) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 同一幂等键重放_预估值稳定且回显幂等键() {
|
|
|
+ when(cfEarnRuleService.getByCode("F22")).thenReturn(rule("F22", "fixed", null, null, null, 20));
|
|
|
+ when(taskMapper.selectById(108L)).thenReturn(task(108L, FAMILY_ID, 20));
|
|
|
+
|
|
|
+ RewardEstimateVO first = rewardRuleService.estimate(MEMBER_ID, "TASK_COMPLETE", "TASK", "108", "same-key");
|
|
|
+ RewardEstimateVO second = rewardRuleService.estimate(MEMBER_ID, "TASK_COMPLETE", "TASK", "108", "same-key");
|
|
|
+
|
|
|
+ assertEquals(first.getExpectedCf(), second.getExpectedCf());
|
|
|
+ assertEquals(20, second.getExpectedCf());
|
|
|
+ assertEquals("same-key", second.getIdempotencyKey());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 七、纯数值取数入口 estimateCf ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void estimateCf_正常返回整数_降级返回0() {
|
|
|
+ when(cfEarnRuleService.getByCode("F22")).thenReturn(rule("F22", "fixed", null, null, null, 20));
|
|
|
+ when(taskMapper.selectById(109L)).thenReturn(task(109L, FAMILY_ID, 20));
|
|
|
+
|
|
|
+ assertEquals(20, rewardRuleService.estimateCf(MEMBER_ID, "TASK_COMPLETE", "TASK", "109"));
|
|
|
+
|
|
|
+ when(cfEarnRuleService.getByCode("F22")).thenReturn(null);
|
|
|
+ assertEquals(0, rewardRuleService.estimateCf(MEMBER_ID, "TASK_COMPLETE", "TASK", "109"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 八、产出点内嵌挂载(契约 §4.1 / §4.3) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void attachTaskEstimate_为任务挂载expectedCf与display() {
|
|
|
+ when(cfEarnRuleService.getByCode("F22")).thenReturn(rule("F22", "fixed", null, null, null, 20));
|
|
|
+ when(taskMapper.selectById(110L)).thenReturn(task(110L, FAMILY_ID, 20));
|
|
|
+ Task task = task(110L, FAMILY_ID, 20);
|
|
|
+
|
|
|
+ rewardRuleService.attachTaskEstimate(task, MEMBER_ID);
|
|
|
+
|
|
|
+ assertEquals(20, task.getExpectedCf());
|
|
|
+ assertNotNull(task.getDisplay());
|
|
|
+ assertEquals("EARN", task.getDisplay().getType());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void attachTaskListEstimate_空值安全不抛异常() {
|
|
|
+ assertDoesNotThrow(() -> rewardRuleService.attachTaskListEstimate(null, MEMBER_ID));
|
|
|
+ assertDoesNotThrow(() -> rewardRuleService.attachTaskListEstimate(new ArrayList<>(), null));
|
|
|
+
|
|
|
+ Task task = task(111L, FAMILY_ID, 20);
|
|
|
+ assertDoesNotThrow(() -> rewardRuleService.attachTaskListEstimate(Arrays.asList(task), null));
|
|
|
+ assertNull(task.getExpectedCf(), "memberId 为空不挂载预估");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void attachGrowthTaskMapEstimate_写回map的expectedCf与display() {
|
|
|
+ when(cfEarnRuleService.getByCode("F12")).thenReturn(rule("F12", "per_item", null, null, null, null));
|
|
|
+ when(growthTaskMapper.selectById(55L)).thenReturn(growthTask(55L, 10));
|
|
|
+ Map<String, Object> item = new HashMap<>();
|
|
|
+ item.put("id", 55L);
|
|
|
+ List<Map<String, Object>> items = new ArrayList<>();
|
|
|
+ items.add(item);
|
|
|
+
|
|
|
+ rewardRuleService.attachGrowthTaskMapEstimate(items, MEMBER_ID);
|
|
|
+
|
|
|
+ assertEquals(10, item.get("expectedCf"));
|
|
|
+ assertNotNull(item.get("display"));
|
|
|
+ assertEquals("EARN", ((com.etotem.cfc.dto.RewardDisplayVO) item.get("display")).getType());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 九、成员解析 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void resolveMemberIdByUser_按userId反查家庭成员_无记录返回null() {
|
|
|
+ when(familyMemberMapper.selectOne(any())).thenReturn(member(MEMBER_ID, FAMILY_ID));
|
|
|
+ assertEquals(Long.valueOf(MEMBER_ID), rewardRuleService.resolveMemberIdByUser(USER_ID));
|
|
|
+
|
|
|
+ when(familyMemberMapper.selectOne(any())).thenReturn(null);
|
|
|
+ assertNull(rewardRuleService.resolveMemberIdByUser(USER_ID));
|
|
|
+ assertNull(rewardRuleService.resolveMemberIdByUser(null));
|
|
|
+ assertNull(rewardRuleService.resolveMemberIdByUser(0L));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 构造辅助 ====================
|
|
|
+
|
|
|
+ private FamilyMember member(long id, long familyId) {
|
|
|
+ FamilyMember m = new FamilyMember();
|
|
|
+ m.setId(id);
|
|
|
+ m.setFamilyId(familyId);
|
|
|
+ m.setUserId(USER_ID);
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Task task(long id, long familyId, int points) {
|
|
|
+ Task t = new Task();
|
|
|
+ t.setId(id);
|
|
|
+ t.setFamilyId(familyId);
|
|
|
+ t.setPoints(points);
|
|
|
+ return t;
|
|
|
+ }
|
|
|
+
|
|
|
+ private GrowthTask growthTask(long id, int rewardPoints) {
|
|
|
+ GrowthTask gt = new GrowthTask();
|
|
|
+ gt.setId(id);
|
|
|
+ gt.setRewardPoints(rewardPoints);
|
|
|
+ gt.setTaskKey("key-" + id);
|
|
|
+ return gt;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Product product(long id, int price) {
|
|
|
+ Product p = new Product();
|
|
|
+ p.setId(id);
|
|
|
+ p.setPrice(price);
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Reward reward(long id, long familyId, Integer pointsRequired) {
|
|
|
+ Reward r = new Reward();
|
|
|
+ r.setId(id);
|
|
|
+ r.setFamilyId(familyId);
|
|
|
+ r.setPointsRequired(pointsRequired);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ private CfEarnRule rule(String code, String ruleType, String formula, String paramsJson,
|
|
|
+ Integer dailyLimitPerMember, Integer amount) {
|
|
|
+ CfEarnRule r = new CfEarnRule();
|
|
|
+ r.setCode(code);
|
|
|
+ r.setName(code + "-规则");
|
|
|
+ r.setCategory("earn");
|
|
|
+ r.setEnabled(1);
|
|
|
+ r.setRuleType(ruleType);
|
|
|
+ r.setFormula(formula);
|
|
|
+ r.setParamsJson(paramsJson);
|
|
|
+ r.setDailyLimitPerMember(dailyLimitPerMember);
|
|
|
+ r.setAmount(amount);
|
|
|
+ r.setDedupRefType(null);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+}
|