|
|
@@ -0,0 +1,447 @@
|
|
|
+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.CfEarnResult;
|
|
|
+import com.etotem.cfc.dto.MembershipLevelDTO;
|
|
|
+import com.etotem.cfc.entity.CfBalance;
|
|
|
+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.mapper.CfBalanceLogMapper;
|
|
|
+import com.etotem.cfc.mapper.CfBalanceMapper;
|
|
|
+import com.etotem.cfc.mapper.CfEarnDailyLimitMapper;
|
|
|
+import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
|
+import com.etotem.cfc.service.impl.CfBalanceServiceImpl;
|
|
|
+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.ArgumentCaptor;
|
|
|
+import org.mockito.Mock;
|
|
|
+import org.mockito.MockitoAnnotations;
|
|
|
+import org.springframework.test.util.ReflectionTestUtils;
|
|
|
+
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
+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.eq;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * F-3b2 成长行为类发放接入服务(CfGrowthPathwayEarnService)单元测试:
|
|
|
+ * - F14 喝水打卡 / F15 饮食打卡 / F16 每日健康打卡 固定数量(5/5/10)全部来自 cf_earn_rule,
|
|
|
+ * 代码 ctx 不携带 amount(规则改数量即改发放值);
|
|
|
+ * - F14-F16 日限按会员等级(N11):达上限跳过并写 skip 流水;
|
|
|
+ * - F17 挑战赛子任务完成:数量取任务配置 progress.reward_cf,ref_id = 挑战进度记录ID;
|
|
|
+ * - F18 健康启航计划付费:公式 floor(实付分/100)×100(与 F02 同口径 1 元=100CF),硬编码 300 已删除;
|
|
|
+ * - 五条途径 refType(health_water / health_meal / daily_health_checkin / challenge_sub_task /
|
|
|
+ * health_start_plan)互不相同、幂等键(member_id, type, ref_type, ref_id)生效。
|
|
|
+ */
|
|
|
+class CfGrowthPathwayEarnServiceTest {
|
|
|
+
|
|
|
+ private static final long MEMBER_ID = 1L;
|
|
|
+ private static final long USER_ID = 100L;
|
|
|
+ private static final long FAMILY_ID = 7L;
|
|
|
+
|
|
|
+ @Mock
|
|
|
+ private CfBalanceMapper cfBalanceMapper;
|
|
|
+ @Mock
|
|
|
+ private CfBalanceLogMapper cfBalanceLogMapper;
|
|
|
+ @Mock
|
|
|
+ private CfEarnDailyLimitMapper cfEarnDailyLimitMapper;
|
|
|
+ @Mock
|
|
|
+ private CfEarnRuleService cfEarnRuleService;
|
|
|
+ @Mock
|
|
|
+ private FamilyMemberMapper familyMemberMapper;
|
|
|
+ @Mock
|
|
|
+ private MembershipService membershipService;
|
|
|
+ @Mock
|
|
|
+ private ConfigService configService;
|
|
|
+
|
|
|
+ private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+ private final List<CfBalanceLog> insertedLogs = new ArrayList<>();
|
|
|
+
|
|
|
+ private final CfBalanceServiceImpl cfBalanceService = new CfBalanceServiceImpl();
|
|
|
+ private final CfGrowthPathwayEarnService service = new CfGrowthPathwayEarnService();
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ MockitoAnnotations.openMocks(this);
|
|
|
+ // MyBatis-Plus LambdaQueryWrapper 需要实体元数据(参照 CfEarnDispatchServiceTest)
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), CfBalance.class);
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), CfBalanceLog.class);
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
|
|
|
+ CfEarnDailyLimit.class);
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), FamilyMember.class);
|
|
|
+
|
|
|
+ CfBalance balance = balance(MEMBER_ID, 0);
|
|
|
+ when(cfBalanceMapper.selectForUpdate(MEMBER_ID)).thenReturn(balance);
|
|
|
+ when(cfBalanceMapper.selectOne(any())).thenReturn(balance);
|
|
|
+ when(cfBalanceLogMapper.selectOne(any())).thenAnswer(inv ->
|
|
|
+ insertedLogs.isEmpty() ? null : insertedLogs.get(insertedLogs.size() - 1));
|
|
|
+ when(cfBalanceLogMapper.selectCount(any())).thenReturn(0L);
|
|
|
+ when(cfBalanceLogMapper.insert(any())).thenAnswer(inv -> {
|
|
|
+ insertedLogs.add(inv.getArgument(0));
|
|
|
+ return 1;
|
|
|
+ });
|
|
|
+
|
|
|
+ FamilyMember member = new FamilyMember();
|
|
|
+ member.setId(MEMBER_ID);
|
|
|
+ member.setUserId(USER_ID);
|
|
|
+ member.setFamilyId(FAMILY_ID);
|
|
|
+ when(familyMemberMapper.selectById(MEMBER_ID)).thenReturn(member);
|
|
|
+ // 会员等级:BASIC(N11 日限 3 次),规则未配 daily_limit_per_member 时按等级表取值
|
|
|
+ MembershipLevelDTO level = new MembershipLevelDTO();
|
|
|
+ level.setLevelCode("BASIC");
|
|
|
+ when(membershipService.getCurrentLevel(FAMILY_ID)).thenReturn(level);
|
|
|
+ when(configService.getConfig("cf.rate.cf_per_yuan")).thenReturn("100");
|
|
|
+
|
|
|
+ // 规则种子(与迁移408 口径一致)
|
|
|
+ when(cfEarnRuleService.getByCode("F14")).thenReturn(rule("F14", "behavior", "fixed", 5, null,
|
|
|
+ "{\"daily_limit_by_tier\":\"cf_earn_daily_limit\"}", null, null, "health_water"));
|
|
|
+ when(cfEarnRuleService.getByCode("F15")).thenReturn(rule("F15", "behavior", "fixed", 5, null,
|
|
|
+ "{\"daily_limit_by_tier\":\"cf_earn_daily_limit\"}", null, null, "health_meal"));
|
|
|
+ when(cfEarnRuleService.getByCode("F16")).thenReturn(rule("F16", "behavior", "fixed", 10, null,
|
|
|
+ "{\"daily_limit_by_tier\":\"cf_earn_daily_limit\"}", null, null, "daily_health_checkin"));
|
|
|
+ when(cfEarnRuleService.getByCode("F17")).thenReturn(rule("F17", "behavior", "per_item", null, null,
|
|
|
+ "{\"source\":\"challenge_task.reward_cf\"}", null, null, "challenge_sub_task"));
|
|
|
+ when(cfEarnRuleService.getByCode("F18")).thenReturn(rule("F18", "consume", "formula", null, "floor(amt)",
|
|
|
+ "{\"cf_per_yuan\":100,\"amt_unit\":\"cent\"}", 1, 1, "health_start_plan"));
|
|
|
+
|
|
|
+ ReflectionTestUtils.setField(cfBalanceService, "cfBalanceMapper", cfBalanceMapper);
|
|
|
+ ReflectionTestUtils.setField(cfBalanceService, "cfBalanceLogMapper", cfBalanceLogMapper);
|
|
|
+ ReflectionTestUtils.setField(cfBalanceService, "cfEarnDailyLimitMapper", cfEarnDailyLimitMapper);
|
|
|
+ ReflectionTestUtils.setField(cfBalanceService, "cfEarnRuleService", cfEarnRuleService);
|
|
|
+ ReflectionTestUtils.setField(cfBalanceService, "familyMemberMapper", familyMemberMapper);
|
|
|
+ ReflectionTestUtils.setField(cfBalanceService, "membershipService", membershipService);
|
|
|
+ ReflectionTestUtils.setField(cfBalanceService, "configService", configService);
|
|
|
+ ReflectionTestUtils.setField(cfBalanceService, "objectMapper", objectMapper);
|
|
|
+ ReflectionTestUtils.setField(service, "cfBalanceService", cfBalanceService);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- F14 喝水打卡 ----------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F14_喝水打卡_数量取规则固定5_refType为health_water() {
|
|
|
+ CfEarnResult r = service.earnWaterCheckinCf(MEMBER_ID, 1001L);
|
|
|
+
|
|
|
+ assertTrue(r.getEarned());
|
|
|
+ assertEquals("F14", r.getRuleCode());
|
|
|
+ assertEquals("health_water", r.getRefType());
|
|
|
+ assertEquals(1001L, r.getRefId());
|
|
|
+ // 5 来自 cf_earn_rule F14(fixed),代码不写死
|
|
|
+ assertEquals(5, r.getAmount());
|
|
|
+ assertEquals(5, cfBalanceService.getBalance(MEMBER_ID).getAvailable());
|
|
|
+
|
|
|
+ CfBalanceLog log = insertedLogs.get(0);
|
|
|
+ assertEquals("earn", log.getType());
|
|
|
+ assertEquals("health_water", log.getRefType());
|
|
|
+ assertEquals(1001L, log.getRefId());
|
|
|
+ assertEquals(5, log.getAmount());
|
|
|
+ assertEquals(MEMBER_ID, log.getMemberId());
|
|
|
+ assertEquals("喝水打卡", log.getRemark());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F14_会员等级日限达上限_跳过发放并写skip流水() {
|
|
|
+ // N11:BASIC 会员日限 3 次/天(cf_earn_daily_limit)
|
|
|
+ CfEarnDailyLimit limit = new CfEarnDailyLimit();
|
|
|
+ limit.setRuleCode("F14");
|
|
|
+ limit.setLevelCode("BASIC");
|
|
|
+ limit.setDailyLimit(3);
|
|
|
+ when(cfEarnDailyLimitMapper.selectOne(any())).thenReturn(limit);
|
|
|
+ when(cfBalanceLogMapper.selectCount(any())).thenReturn(3L);
|
|
|
+
|
|
|
+ CfEarnResult r = service.earnWaterCheckinCf(MEMBER_ID, 1002L);
|
|
|
+
|
|
|
+ assertFalse(r.getEarned());
|
|
|
+ assertTrue(r.getReason().contains("日限"));
|
|
|
+ assertEquals(1002L, r.getRefId());
|
|
|
+ assertTrue(earnLogs().isEmpty(), "日限命中不得入账");
|
|
|
+ assertEquals(1, insertedLogs.size());
|
|
|
+ assertEquals("skip", insertedLogs.get(0).getType());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F14_规则数量调整_发放数量随规则变化_代码无硬编码() {
|
|
|
+ when(cfEarnRuleService.getByCode("F14")).thenReturn(
|
|
|
+ rule("F14", "behavior", "fixed", 8, null, "{}", null, null, "health_water"));
|
|
|
+
|
|
|
+ CfEarnResult r = service.earnWaterCheckinCf(MEMBER_ID, 1003L);
|
|
|
+
|
|
|
+ assertEquals(8, r.getAmount());
|
|
|
+ assertEquals(8, cfBalanceService.getBalance(MEMBER_ID).getAvailable());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F14_同一打卡记录重复提交_幂等只发一次() {
|
|
|
+ service.earnWaterCheckinCf(MEMBER_ID, 1004L);
|
|
|
+ CfEarnResult second = service.earnWaterCheckinCf(MEMBER_ID, 1004L);
|
|
|
+
|
|
|
+ assertTrue(second.getDuplicated());
|
|
|
+ assertEquals(1, insertedLogs.size());
|
|
|
+ assertEquals(5, cfBalanceService.getBalance(MEMBER_ID).getAvailable());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- F15 饮食打卡 ----------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F15_饮食打卡_数量取规则固定5_refType为health_meal() {
|
|
|
+ CfEarnResult r = service.earnMealCheckinCf(MEMBER_ID, 2001L);
|
|
|
+
|
|
|
+ assertTrue(r.getEarned());
|
|
|
+ assertEquals("F15", r.getRuleCode());
|
|
|
+ assertEquals("health_meal", r.getRefType());
|
|
|
+ assertEquals(2001L, r.getRefId());
|
|
|
+ assertEquals(5, r.getAmount());
|
|
|
+ assertEquals(5, cfBalanceService.getBalance(MEMBER_ID).getAvailable());
|
|
|
+
|
|
|
+ CfBalanceLog log = insertedLogs.get(0);
|
|
|
+ assertEquals("health_meal", log.getRefType());
|
|
|
+ assertEquals(5, log.getAmount());
|
|
|
+ assertEquals("饮食打卡", log.getRemark());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F15_会员等级日限达上限_跳过发放() {
|
|
|
+ CfEarnDailyLimit limit = new CfEarnDailyLimit();
|
|
|
+ limit.setRuleCode("F15");
|
|
|
+ limit.setLevelCode("BASIC");
|
|
|
+ limit.setDailyLimit(3);
|
|
|
+ when(cfEarnDailyLimitMapper.selectOne(any())).thenReturn(limit);
|
|
|
+ when(cfBalanceLogMapper.selectCount(any())).thenReturn(3L);
|
|
|
+
|
|
|
+ CfEarnResult r = service.earnMealCheckinCf(MEMBER_ID, 2002L);
|
|
|
+
|
|
|
+ assertFalse(r.getEarned());
|
|
|
+ assertTrue(r.getReason().contains("日限"));
|
|
|
+ assertTrue(earnLogs().isEmpty(), "日限命中不得入账");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- F16 每日健康打卡 ----------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F16_每日健康打卡_数量取规则固定10_refType为daily_health_checkin() {
|
|
|
+ CfEarnResult r = service.earnDailyHealthCheckinCf(MEMBER_ID, 3001L);
|
|
|
+
|
|
|
+ assertTrue(r.getEarned());
|
|
|
+ assertEquals("F16", r.getRuleCode());
|
|
|
+ assertEquals("daily_health_checkin", r.getRefType());
|
|
|
+ assertEquals(3001L, r.getRefId());
|
|
|
+ // 10 来自 cf_earn_rule F16(fixed)
|
|
|
+ assertEquals(10, r.getAmount());
|
|
|
+ assertEquals(10, cfBalanceService.getBalance(MEMBER_ID).getAvailable());
|
|
|
+
|
|
|
+ CfBalanceLog log = insertedLogs.get(0);
|
|
|
+ assertEquals("daily_health_checkin", log.getRefType());
|
|
|
+ assertEquals(10, log.getAmount());
|
|
|
+ assertEquals("每日健康打卡", log.getRemark());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F16_同一打卡记录重复提交_幂等只发一次() {
|
|
|
+ service.earnDailyHealthCheckinCf(MEMBER_ID, 3002L);
|
|
|
+ CfEarnResult second = service.earnDailyHealthCheckinCf(MEMBER_ID, 3002L);
|
|
|
+
|
|
|
+ assertTrue(second.getDuplicated());
|
|
|
+ assertEquals(10, cfBalanceService.getBalance(MEMBER_ID).getAvailable());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- F17 挑战赛子任务完成 ----------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F17_挑战赛子任务_数量取任务配置rewardCf_refType为challenge_sub_task() {
|
|
|
+ CfEarnResult r = service.earnChallengeSubTaskCf(MEMBER_ID, 500L, 100, "streak_7days", "连续7天打卡");
|
|
|
+
|
|
|
+ assertTrue(r.getEarned());
|
|
|
+ assertEquals("F17", r.getRuleCode());
|
|
|
+ assertEquals("challenge_sub_task", r.getRefType());
|
|
|
+ assertEquals(500L, r.getRefId());
|
|
|
+ assertEquals(100, r.getAmount());
|
|
|
+ assertEquals(100, cfBalanceService.getBalance(MEMBER_ID).getAvailable());
|
|
|
+
|
|
|
+ CfBalanceLog log = insertedLogs.get(0);
|
|
|
+ assertEquals("challenge_sub_task", log.getRefType());
|
|
|
+ assertEquals(500L, log.getRefId());
|
|
|
+ assertEquals(100, log.getAmount());
|
|
|
+ assertEquals("挑战赛子任务完成奖励: 连续7天打卡", log.getRemark());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F17_同一子任务重复触发_幂等只发一次() {
|
|
|
+ service.earnChallengeSubTaskCf(MEMBER_ID, 500L, 100, "streak_7days", "连续7天打卡");
|
|
|
+ CfEarnResult second = service.earnChallengeSubTaskCf(MEMBER_ID, 500L, 100, "streak_7days", "连续7天打卡");
|
|
|
+
|
|
|
+ assertTrue(second.getDuplicated());
|
|
|
+ assertEquals(1, insertedLogs.size());
|
|
|
+ assertEquals(100, cfBalanceService.getBalance(MEMBER_ID).getAvailable());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- F18 健康启航计划付费 ----------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F18_健康启航付费_199元得19900CF_与F02同口径() {
|
|
|
+ // 199 元实付 = 19900 分 → 19900 CF(1 元 = 100 CF)
|
|
|
+ CfEarnResult r = service.earnHealthStartPlanCf(MEMBER_ID, 900L, 19900, "HQ20260001");
|
|
|
+
|
|
|
+ assertTrue(r.getEarned());
|
|
|
+ assertEquals("F18", r.getRuleCode());
|
|
|
+ assertEquals("health_start_plan", r.getRefType());
|
|
|
+ assertEquals(900L, r.getRefId());
|
|
|
+ assertEquals(19900, r.getAmount());
|
|
|
+ assertEquals(19900, cfBalanceService.getBalance(MEMBER_ID).getAvailable());
|
|
|
+
|
|
|
+ CfBalanceLog log = insertedLogs.get(0);
|
|
|
+ assertEquals("health_start_plan", log.getRefType());
|
|
|
+ assertEquals(900L, log.getRefId());
|
|
|
+ assertEquals(19900, log.getAmount());
|
|
|
+ assertEquals("健康启航计划付费奖励", log.getRemark());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F18_同一订单重复触发_幂等不重复发放() {
|
|
|
+ service.earnHealthStartPlanCf(MEMBER_ID, 900L, 19900, "HQ20260001");
|
|
|
+ CfEarnResult second = service.earnHealthStartPlanCf(MEMBER_ID, 900L, 19900, "HQ20260001");
|
|
|
+
|
|
|
+ assertTrue(second.getDuplicated());
|
|
|
+ assertEquals(1, insertedLogs.size());
|
|
|
+ assertEquals(19900, cfBalanceService.getBalance(MEMBER_ID).getAvailable());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F18_规则累计上限1次_第二笔按规则跳过() {
|
|
|
+ when(cfEarnRuleService.getByCode("F18")).thenReturn(rule("F18", "consume", "formula", null, "floor(amt)",
|
|
|
+ "{\"cf_per_yuan\":100,\"amt_unit\":\"cent\"}", null, 1, "health_start_plan"));
|
|
|
+ when(cfBalanceLogMapper.selectCount(any())).thenReturn(0L, 1L);
|
|
|
+
|
|
|
+ service.earnHealthStartPlanCf(MEMBER_ID, 900L, 19900, "HQ1");
|
|
|
+ CfEarnResult second = service.earnHealthStartPlanCf(MEMBER_ID, 901L, 19900, "HQ2");
|
|
|
+
|
|
|
+ assertFalse(second.getEarned());
|
|
|
+ assertTrue(second.getReason().contains("累计上限"));
|
|
|
+ assertEquals(1, earnLogs().size(), "第一笔入账,第二笔命中累计上限只写 skip 流水");
|
|
|
+ assertEquals(19900, cfBalanceService.getBalance(MEMBER_ID).getAvailable());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- ctx 组装校验(数量一律由规则决定,代码不硬编码) ----------
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private Map<String, Object> captureCtx(CfGrowthPathwayEarnService s, long memberId, String ruleCode,
|
|
|
+ long refId, Runnable call) {
|
|
|
+ CfBalanceService stub = mock(CfBalanceService.class);
|
|
|
+ ReflectionTestUtils.setField(s, "cfBalanceService", stub);
|
|
|
+ call.run();
|
|
|
+ ArgumentCaptor<String> ruleCaptor = ArgumentCaptor.forClass(String.class);
|
|
|
+ ArgumentCaptor<Long> refIdCaptor = ArgumentCaptor.forClass(Long.class);
|
|
|
+ ArgumentCaptor<Map<String, Object>> ctxCaptor = ArgumentCaptor.forClass(Map.class);
|
|
|
+ verify(stub).earnByRule(eq(memberId), ruleCaptor.capture(), refIdCaptor.capture(), ctxCaptor.capture());
|
|
|
+ assertEquals(ruleCode, ruleCaptor.getValue(), "规则编码必须与途径一致");
|
|
|
+ assertEquals(Long.valueOf(refId), refIdCaptor.getValue(), "幂等 refId 必须用业务记录ID");
|
|
|
+ return ctxCaptor.getValue();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F14_F15_F16_ctx不携带数量_数量全由规则决定() {
|
|
|
+ CfGrowthPathwayEarnService s = new CfGrowthPathwayEarnService();
|
|
|
+
|
|
|
+ Map<String, Object> ctxF14 = captureCtx(s, MEMBER_ID, "F14", 11L,
|
|
|
+ () -> s.earnWaterCheckinCf(MEMBER_ID, 11L));
|
|
|
+ assertNull(ctxF14.get("amount"), "固定规则 F14 的数量必须来自 cf_earn_rule,代码不得传 amount");
|
|
|
+ assertNull(ctxF14.get("amt"));
|
|
|
+ assertEquals(11L, ctxF14.get("checkin_id"));
|
|
|
+ assertEquals("喝水打卡", ctxF14.get("remark"));
|
|
|
+
|
|
|
+ Map<String, Object> ctxF15 = captureCtx(s, MEMBER_ID, "F15", 22L,
|
|
|
+ () -> s.earnMealCheckinCf(MEMBER_ID, 22L));
|
|
|
+ assertNull(ctxF15.get("amount"), "固定规则 F15 的数量必须来自 cf_earn_rule,代码不得传 amount");
|
|
|
+ assertEquals(22L, ctxF15.get("checkin_id"));
|
|
|
+ assertEquals("饮食打卡", ctxF15.get("remark"));
|
|
|
+
|
|
|
+ Map<String, Object> ctxF16 = captureCtx(s, MEMBER_ID, "F16", 33L,
|
|
|
+ () -> s.earnDailyHealthCheckinCf(MEMBER_ID, 33L));
|
|
|
+ assertNull(ctxF16.get("amount"), "固定规则 F16 的数量必须来自 cf_earn_rule,代码不得传 amount");
|
|
|
+ assertEquals(33L, ctxF16.get("checkin_id"));
|
|
|
+ assertEquals("每日健康打卡", ctxF16.get("remark"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F17_ctx携带任务配置数量与任务标识() {
|
|
|
+ CfGrowthPathwayEarnService s = new CfGrowthPathwayEarnService();
|
|
|
+
|
|
|
+ Map<String, Object> ctx = captureCtx(s, MEMBER_ID, "F17", 500L,
|
|
|
+ () -> s.earnChallengeSubTaskCf(MEMBER_ID, 500L, 100, "streak_7days", "连续7天打卡"));
|
|
|
+
|
|
|
+ assertEquals(100, ctx.get("amount"), "F17 数量必须来自任务配置 progress.reward_cf");
|
|
|
+ assertEquals("streak_7days", ctx.get("task_key"));
|
|
|
+ assertEquals("挑战赛子任务完成奖励: 连续7天打卡", ctx.get("remark"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F18_ctx携带实付金额分_数量由规则公式计算() {
|
|
|
+ CfGrowthPathwayEarnService s = new CfGrowthPathwayEarnService();
|
|
|
+
|
|
|
+ Map<String, Object> ctx = captureCtx(s, MEMBER_ID, "F18", 900L,
|
|
|
+ () -> s.earnHealthStartPlanCf(MEMBER_ID, 900L, 19900, "HQ20260001"));
|
|
|
+
|
|
|
+ assertEquals(19900, ctx.get("amt"), "F18 只传实付金额(分),数量由规则公式 floor(amt) 计算");
|
|
|
+ assertNull(ctx.get("amount"), "F18 不得在代码中写死 CF 数量");
|
|
|
+ assertEquals("HQ20260001", ctx.get("order_no"));
|
|
|
+ assertEquals("健康启航计划付费奖励", ctx.get("remark"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- 构造 ----------
|
|
|
+
|
|
|
+ /** 已入账的 earn 流水(排除 skip 流水) */
|
|
|
+ private List<CfBalanceLog> earnLogs() {
|
|
|
+ List<CfBalanceLog> earn = new ArrayList<>();
|
|
|
+ for (CfBalanceLog logEntry : insertedLogs) {
|
|
|
+ if ("earn".equals(logEntry.getType())) {
|
|
|
+ earn.add(logEntry);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return earn;
|
|
|
+ }
|
|
|
+
|
|
|
+ private CfBalance balance(long memberId, int available) {
|
|
|
+ CfBalance b = new CfBalance();
|
|
|
+ b.setId(memberId);
|
|
|
+ b.setMemberId(memberId);
|
|
|
+ b.setUserId(USER_ID);
|
|
|
+ b.setFamilyId(FAMILY_ID);
|
|
|
+ b.setAvailable(available);
|
|
|
+ b.setFrozen(0);
|
|
|
+ b.setTotalEarned(available);
|
|
|
+ b.setWithdrawn(0);
|
|
|
+ b.setExchanged(0);
|
|
|
+ return b;
|
|
|
+ }
|
|
|
+
|
|
|
+ private CfEarnRule rule(String code, String category, String ruleType, Integer amount, String formula,
|
|
|
+ String paramsJson, Integer dailyLimit, Integer totalLimit, String dedupRefType) {
|
|
|
+ CfEarnRule r = new CfEarnRule();
|
|
|
+ r.setCode(code);
|
|
|
+ r.setName(code + "-规则");
|
|
|
+ r.setCategory(category);
|
|
|
+ r.setEnabled(1);
|
|
|
+ r.setRuleType(ruleType);
|
|
|
+ r.setAmount(amount);
|
|
|
+ r.setFormula(formula);
|
|
|
+ r.setParamsJson(paramsJson);
|
|
|
+ r.setDailyLimitPerMember(dailyLimit);
|
|
|
+ r.setTotalLimitPerMember(totalLimit);
|
|
|
+ r.setCooldownSeconds(null);
|
|
|
+ r.setDedupRefType(dedupRefType);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+}
|