|
|
@@ -0,0 +1,431 @@
|
|
|
+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.constant.CfConfigKeys;
|
|
|
+import com.etotem.cfc.dto.CfBalanceResult;
|
|
|
+import com.etotem.cfc.dto.CfEarnResult;
|
|
|
+import com.etotem.cfc.entity.CfBalanceLog;
|
|
|
+import com.etotem.cfc.entity.CfEarnRule;
|
|
|
+import com.etotem.cfc.entity.DanAssessmentResult;
|
|
|
+import com.etotem.cfc.mapper.CfBalanceLogMapper;
|
|
|
+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.InjectMocks;
|
|
|
+import org.mockito.Mock;
|
|
|
+import org.mockito.MockitoAnnotations;
|
|
|
+import org.mockito.Spy;
|
|
|
+
|
|
|
+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.assertNotNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
|
+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.never;
|
|
|
+import static org.mockito.Mockito.times;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * F-3b1 发放途径接入服务(CfPathwayEarnService)单元测试:
|
|
|
+ * - F07 文章答题 / F08 AI 阅读答题 / F09 测评建档 / F10 家庭挑战 / F11 邀请里程碑 /
|
|
|
+ * F12 成长任务 / F13 分享阅读 统一改投 earnByRule,数量与幂等标识(refId/ctx)按规则组装;
|
|
|
+ * - F09 首份报告 50 / 后续 10 的判定(first_report)与重复报告同一 ref_id(N10 去重);
|
|
|
+ * - F13 数量:cf_earn_rule 优先,缺数量回退 service_config(不再读 sys_config);
|
|
|
+ * - F19 管理员调整:正数走 earnByRule、负数走 adjust,家长密码校验、操作人+原因留痕、幂等键。
|
|
|
+ */
|
|
|
+class CfPathwayEarnServiceTest {
|
|
|
+
|
|
|
+ @Mock
|
|
|
+ private CfBalanceService cfBalanceService;
|
|
|
+ @Mock
|
|
|
+ private CfEarnRuleService cfEarnRuleService;
|
|
|
+ @Mock
|
|
|
+ private CfBalanceLogMapper cfBalanceLogMapper;
|
|
|
+ @Mock
|
|
|
+ private ConfigService configService;
|
|
|
+ @Mock
|
|
|
+ private UserService userService;
|
|
|
+ @Spy
|
|
|
+ private ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+
|
|
|
+ @InjectMocks
|
|
|
+ private CfPathwayEarnService service;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ MockitoAnnotations.openMocks(this);
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), CfBalanceLog.class);
|
|
|
+ // 默认:无已有测评奖励流水(F09 首份报告判定)
|
|
|
+ when(cfBalanceLogMapper.selectCount(any())).thenReturn(0L);
|
|
|
+ }
|
|
|
+
|
|
|
+ private CfEarnRule rule(String code, String ruleType, Integer amount, String formula, String paramsJson,
|
|
|
+ String dedupRefType) {
|
|
|
+ CfEarnRule r = new CfEarnRule();
|
|
|
+ r.setCode(code);
|
|
|
+ r.setName(code + "-规则");
|
|
|
+ r.setCategory("behavior");
|
|
|
+ r.setEnabled(1);
|
|
|
+ r.setRuleType(ruleType);
|
|
|
+ r.setAmount(amount);
|
|
|
+ r.setFormula(formula);
|
|
|
+ r.setParamsJson(paramsJson);
|
|
|
+ r.setDedupRefType(dedupRefType);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ private CfEarnResult earned(String ruleCode, String refType, int amount, boolean duplicated) {
|
|
|
+ CfEarnResult r = new CfEarnResult();
|
|
|
+ r.setRuleCode(ruleCode);
|
|
|
+ r.setRefType(refType);
|
|
|
+ r.setEarned(true);
|
|
|
+ r.setAmount(amount);
|
|
|
+ r.setDuplicated(duplicated);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private Map<String, Object> capturedCtx() {
|
|
|
+ ArgumentCaptor<Map<String, Object>> ctxCaptor = ArgumentCaptor.forClass(Map.class);
|
|
|
+ verify(cfBalanceService).earnByRule(any(Long.class), any(String.class), any(Long.class), ctxCaptor.capture());
|
|
|
+ return ctxCaptor.getValue();
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- F07 文章答题 ----------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F07_答题发放_ctx带分数与全对标识() {
|
|
|
+ when(cfBalanceService.earnByRule(eq(1L), eq("F07"), eq(100L), any()))
|
|
|
+ .thenReturn(earned("F07", "article_answer", 3, false));
|
|
|
+
|
|
|
+ CfEarnResult r = service.earnArticleQuizCf(1L, 100L, 3, 3);
|
|
|
+
|
|
|
+ assertTrue(Boolean.TRUE.equals(r.getEarned()));
|
|
|
+ Map<String, Object> ctx = capturedCtx();
|
|
|
+ assertEquals(3, ctx.get("score"));
|
|
|
+ assertEquals(Boolean.TRUE, ctx.get("all_correct"));
|
|
|
+ assertEquals("文章答题奖励", ctx.get("remark"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F07_未全对_ctx全对为false() {
|
|
|
+ when(cfBalanceService.earnByRule(eq(1L), eq("F07"), eq(100L), any()))
|
|
|
+ .thenReturn(earned("F07", "article_answer", 2, false));
|
|
|
+
|
|
|
+ service.earnArticleQuizCf(1L, 100L, 2, 5);
|
|
|
+
|
|
|
+ Map<String, Object> ctx = capturedCtx();
|
|
|
+ assertEquals(Boolean.FALSE, ctx.get("all_correct"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F07_同一答题记录重复提交_幂等命中不重复发放() {
|
|
|
+ when(cfBalanceService.earnByRule(eq(1L), eq("F07"), eq(100L), any()))
|
|
|
+ .thenReturn(earned("F07", "article_answer", 3, true));
|
|
|
+
|
|
|
+ CfEarnResult r = service.earnArticleQuizCf(1L, 100L, 3, 3);
|
|
|
+
|
|
|
+ assertTrue(Boolean.TRUE.equals(r.getDuplicated()));
|
|
|
+ verify(cfBalanceService).earnByRule(eq(1L), eq("F07"), eq(100L), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- F08 AI 阅读答题 ----------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F08_未全部答对_不发放也不调用统一入口() {
|
|
|
+ CfEarnResult r = service.earnAiReadingQuizCf(1L, 200L, false);
|
|
|
+
|
|
|
+ assertFalse(Boolean.TRUE.equals(r.getEarned()));
|
|
|
+ assertNotNull(r.getReason());
|
|
|
+ verify(cfBalanceService, never()).earnByRule(any(Long.class), any(String.class), any(Long.class), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F08_全部答对_数量不硬编码由规则决定() {
|
|
|
+ // F08 固定 5 已外置到 cf_earn_rule;ctx 不带 amount → earnByRule 按 fixed 规则取数
|
|
|
+ when(cfEarnRuleService.getByCode("F08"))
|
|
|
+ .thenReturn(rule("F08", "fixed", 5, null, "{\"require_all_correct\":true}", "ai_reading_answer"));
|
|
|
+ when(cfBalanceService.earnByRule(eq(1L), eq("F08"), eq(200L), any()))
|
|
|
+ .thenReturn(earned("F08", "ai_reading_answer", 5, false));
|
|
|
+
|
|
|
+ CfEarnResult r = service.earnAiReadingQuizCf(1L, 200L, true);
|
|
|
+
|
|
|
+ assertEquals(5, r.getAmount());
|
|
|
+ Map<String, Object> ctx = capturedCtx();
|
|
|
+ assertEquals(Boolean.TRUE, ctx.get("all_correct"));
|
|
|
+ // 数量由规则配置,代码不得传入 amount
|
|
|
+ assertNull(ctx.get("amount"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F08_同一答题记录重复提交_幂等命中() {
|
|
|
+ when(cfEarnRuleService.getByCode("F08"))
|
|
|
+ .thenReturn(rule("F08", "fixed", 5, null, "{\"require_all_correct\":true}", "ai_reading_answer"));
|
|
|
+ when(cfBalanceService.earnByRule(eq(1L), eq("F08"), eq(200L), any()))
|
|
|
+ .thenReturn(earned("F08", "ai_reading_answer", 5, true));
|
|
|
+
|
|
|
+ CfEarnResult r = service.earnAiReadingQuizCf(1L, 200L, true);
|
|
|
+
|
|
|
+ assertTrue(Boolean.TRUE.equals(r.getDuplicated()));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- F09 测评建档 ----------
|
|
|
+
|
|
|
+ private DanAssessmentResult report(Long id, Long sourceReportId, String reportFileId) {
|
|
|
+ DanAssessmentResult r = new DanAssessmentResult();
|
|
|
+ r.setId(id);
|
|
|
+ r.setSourceReportId(sourceReportId);
|
|
|
+ r.setReportFileId(reportFileId);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F09_首份报告_ctx首份为true() {
|
|
|
+ when(cfEarnRuleService.getByCode("F09"))
|
|
|
+ .thenReturn(rule("F09", "formula", null, "first_report ? 50 : 10", "{}", "assessment_report"));
|
|
|
+ when(cfBalanceLogMapper.selectCount(any())).thenReturn(0L);
|
|
|
+ when(cfBalanceService.earnByRule(eq(1L), eq("F09"), eq(555L), any()))
|
|
|
+ .thenReturn(earned("F09", "assessment_report", 50, false));
|
|
|
+
|
|
|
+ DanAssessmentResult result = report(11L, 555L, null);
|
|
|
+ CfEarnResult r = service.earnAssessmentReportCf(1L, result);
|
|
|
+
|
|
|
+ assertEquals(50, r.getAmount());
|
|
|
+ ArgumentCaptor<Long> refIdCaptor = ArgumentCaptor.forClass(Long.class);
|
|
|
+ verify(cfBalanceService).earnByRule(eq(1L), eq("F09"), refIdCaptor.capture(), any());
|
|
|
+ assertEquals(555L, refIdCaptor.getValue());
|
|
|
+ Map<String, Object> ctx = capturedCtx();
|
|
|
+ assertEquals(Boolean.TRUE, ctx.get("first_report"));
|
|
|
+ assertEquals(555L, ctx.get("report_ref"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F09_已有报告_ctx首份为false_发后续档() {
|
|
|
+ when(cfEarnRuleService.getByCode("F09"))
|
|
|
+ .thenReturn(rule("F09", "formula", null, "first_report ? 50 : 10", "{}", "assessment_report"));
|
|
|
+ when(cfBalanceLogMapper.selectCount(any())).thenReturn(3L);
|
|
|
+ when(cfBalanceService.earnByRule(eq(1L), eq("F09"), eq(556L), any()))
|
|
|
+ .thenReturn(earned("F09", "assessment_report", 10, false));
|
|
|
+
|
|
|
+ CfEarnResult r = service.earnAssessmentReportCf(1L, report(12L, 556L, null));
|
|
|
+
|
|
|
+ assertEquals(10, r.getAmount());
|
|
|
+ Map<String, Object> ctx = capturedCtx();
|
|
|
+ assertEquals(Boolean.FALSE, ctx.get("first_report"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F09_同一份报告重复提交_同一refId_幂等只发一次() {
|
|
|
+ when(cfEarnRuleService.getByCode("F09"))
|
|
|
+ .thenReturn(rule("F09", "formula", null, "first_report ? 50 : 10", "{}", "assessment_report"));
|
|
|
+ when(cfBalanceService.earnByRule(eq(1L), eq("F09"), any(Long.class), any()))
|
|
|
+ .thenReturn(earned("F09", "assessment_report", 10, true));
|
|
|
+
|
|
|
+ // 报告文件标识去重(无上传报告编号):同一份报告重复提交 → 同一 refId → F-2 幂等只发一次
|
|
|
+ DanAssessmentResult result = report(13L, null, "file-abc");
|
|
|
+ service.earnAssessmentReportCf(1L, result);
|
|
|
+ service.earnAssessmentReportCf(1L, report(14L, null, "file-abc"));
|
|
|
+
|
|
|
+ ArgumentCaptor<Long> refIdCaptor = ArgumentCaptor.forClass(Long.class);
|
|
|
+ verify(cfBalanceService, times(2)).earnByRule(eq(1L), eq("F09"), refIdCaptor.capture(), any());
|
|
|
+ List<Long> refIds = refIdCaptor.getAllValues();
|
|
|
+ assertEquals(refIds.get(0), refIds.get(1));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F09_缺成员或记录_不发() {
|
|
|
+ CfEarnResult r = service.earnAssessmentReportCf(null, null);
|
|
|
+
|
|
|
+ assertFalse(Boolean.TRUE.equals(r.getEarned()));
|
|
|
+ verify(cfBalanceService, never()).earnByRule(any(Long.class), any(String.class), any(Long.class), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- F10 家庭挑战 ----------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F10_按成员各发一次_数量取挑战配置() {
|
|
|
+ when(cfBalanceService.earnByRule(eq(5L), eq("F10"), eq(700L), any()))
|
|
|
+ .thenReturn(earned("F10", "family_challenge", 30, false));
|
|
|
+
|
|
|
+ CfEarnResult r = service.earnFamilyChallengeCf(5L, 700L, 30, "全家跳绳");
|
|
|
+
|
|
|
+ assertEquals(30, r.getAmount());
|
|
|
+ Map<String, Object> ctx = capturedCtx();
|
|
|
+ assertEquals(30, ctx.get("amount"));
|
|
|
+ assertEquals(700L, ctx.get("challenge_id"));
|
|
|
+ assertEquals("家庭挑战完成奖励: 全家跳绳", ctx.get("remark"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- F11 邀请里程碑 ----------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F11_数量取里程碑配置_refId为里程碑记录() {
|
|
|
+ when(cfBalanceService.earnByRule(eq(5L), eq("F11"), eq(88L), any()))
|
|
|
+ .thenReturn(earned("F11", "invite_milestone", 20, false));
|
|
|
+
|
|
|
+ CfEarnResult r = service.earnInviteMilestoneCf(5L, 88L, 20, "INVITE_5");
|
|
|
+
|
|
|
+ assertEquals(20, r.getAmount());
|
|
|
+ Map<String, Object> ctx = capturedCtx();
|
|
|
+ assertEquals(20, ctx.get("amount"));
|
|
|
+ assertEquals("INVITE_5", ctx.get("milestone"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- F12 成长任务 ----------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F12_数量取任务模板配置_refId为任务记录() {
|
|
|
+ when(cfBalanceService.earnByRule(eq(5L), eq("F12"), eq(99L), any()))
|
|
|
+ .thenReturn(earned("F12", "growth_task", 15, false));
|
|
|
+
|
|
|
+ CfEarnResult r = service.earnGrowthTaskCf(5L, 99L, 15, "sleep_early");
|
|
|
+
|
|
|
+ assertEquals(15, r.getAmount());
|
|
|
+ Map<String, Object> ctx = capturedCtx();
|
|
|
+ assertEquals(15, ctx.get("amount"));
|
|
|
+ assertEquals("sleep_early", ctx.get("task_key"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- F13 分享阅读 ----------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F13_规则数量优先() {
|
|
|
+ when(cfEarnRuleService.getByCode("F13"))
|
|
|
+ .thenReturn(rule("F13", "fixed", 5, null, "{}", "share_read"));
|
|
|
+ when(cfBalanceService.earnByRule(eq(5L), eq("F13"), eq(300L), any()))
|
|
|
+ .thenReturn(earned("F13", "share_read", 5, false));
|
|
|
+
|
|
|
+ CfEarnResult r = service.earnShareReadCf(5L, 300L);
|
|
|
+
|
|
|
+ assertEquals(5, r.getAmount());
|
|
|
+ Map<String, Object> ctx = capturedCtx();
|
|
|
+ assertEquals(5, ctx.get("amount"));
|
|
|
+ // 规则有数量时不回退 service_config
|
|
|
+ verify(configService, never()).getIntConfig(eq(CfConfigKeys.RATE_SHARE_READ_REWARD_POINTS), any(Integer.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F13_规则缺数量_回退service_config() {
|
|
|
+ when(cfEarnRuleService.getByCode("F13"))
|
|
|
+ .thenReturn(rule("F13", "fixed", null, null, "{}", "share_read"));
|
|
|
+ when(configService.getIntConfig(CfConfigKeys.RATE_SHARE_READ_REWARD_POINTS, 0)).thenReturn(7);
|
|
|
+ when(cfBalanceService.earnByRule(eq(5L), eq("F13"), eq(301L), any()))
|
|
|
+ .thenReturn(earned("F13", "share_read", 7, false));
|
|
|
+
|
|
|
+ CfEarnResult r = service.earnShareReadCf(5L, 301L);
|
|
|
+
|
|
|
+ assertEquals(7, r.getAmount());
|
|
|
+ Map<String, Object> ctx = capturedCtx();
|
|
|
+ assertEquals(7, ctx.get("amount"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F13_规则缺失_回退service_config() {
|
|
|
+ when(cfEarnRuleService.getByCode("F13")).thenReturn(null);
|
|
|
+ when(configService.getIntConfig(CfConfigKeys.RATE_SHARE_READ_REWARD_POINTS, 0)).thenReturn(5);
|
|
|
+ when(cfBalanceService.earnByRule(eq(5L), eq("F13"), eq(302L), any()))
|
|
|
+ .thenReturn(earned("F13", "share_read", 5, false));
|
|
|
+
|
|
|
+ CfEarnResult r = service.earnShareReadCf(5L, 302L);
|
|
|
+
|
|
|
+ assertEquals(5, r.getAmount());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- F19 管理员手动调整 ----------
|
|
|
+
|
|
|
+ private CfEarnRule f19Rule() {
|
|
|
+ return rule("F19", "per_item", null, null,
|
|
|
+ "{\"require_parent_password\":true,\"allow_negative\":true}", "admin_adjust");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F19_正向调整_走统一发放入口_留操作人原因() {
|
|
|
+ when(cfEarnRuleService.getByCode("F19")).thenReturn(f19Rule());
|
|
|
+ when(userService.verifyPassword(9L, "secret")).thenReturn(true);
|
|
|
+ when(cfBalanceService.earnByRule(eq(1L), eq("F19"), any(Long.class), any()))
|
|
|
+ .thenReturn(earned("F19", "admin_adjust", 20, false));
|
|
|
+
|
|
|
+ CfEarnResult r = service.adjustByAdmin(1L, 20, "家长误扣,补发", "secret", 9L, "key-1");
|
|
|
+
|
|
|
+ assertTrue(Boolean.TRUE.equals(r.getEarned()));
|
|
|
+ assertEquals(20, r.getAmount());
|
|
|
+ Map<String, Object> ctx = capturedCtx();
|
|
|
+ assertEquals(20, ctx.get("amount"));
|
|
|
+ assertEquals(9L, ctx.get("operator"));
|
|
|
+ assertEquals("家长误扣,补发", ctx.get("reason"));
|
|
|
+ assertEquals("家长误扣,补发", ctx.get("remark"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F19_负向调整_走adjust_负数金额() {
|
|
|
+ when(cfEarnRuleService.getByCode("F19")).thenReturn(f19Rule());
|
|
|
+ when(userService.verifyPassword(9L, "secret")).thenReturn(true);
|
|
|
+ CfBalanceResult balanceResult = new CfBalanceResult();
|
|
|
+ balanceResult.setAmount(-20);
|
|
|
+ balanceResult.setDuplicated(false);
|
|
|
+ when(cfBalanceService.adjust(eq(1L), eq(-20), any(Long.class), any(String.class), eq(9L)))
|
|
|
+ .thenReturn(balanceResult);
|
|
|
+
|
|
|
+ CfEarnResult r = service.adjustByAdmin(1L, -20, "重复发放,回扣", "secret", 9L, "key-2");
|
|
|
+
|
|
|
+ assertTrue(Boolean.TRUE.equals(r.getEarned()));
|
|
|
+ assertEquals(-20, r.getAmount());
|
|
|
+ assertEquals("admin_adjust", r.getRefType());
|
|
|
+ verify(cfBalanceService).adjust(1L, -20, r.getRefId(), "重复发放,回扣", 9L);
|
|
|
+ verify(cfBalanceService, never()).earnByRule(any(Long.class), any(String.class), any(Long.class), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F19_密码错误_拒绝调整() {
|
|
|
+ when(cfEarnRuleService.getByCode("F19")).thenReturn(f19Rule());
|
|
|
+ when(userService.verifyPassword(9L, "wrong")).thenReturn(false);
|
|
|
+
|
|
|
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
|
|
|
+ () -> service.adjustByAdmin(1L, 20, "补发", "wrong", 9L, "key-3"));
|
|
|
+ assertTrue(e.getMessage().contains("密码"));
|
|
|
+ verify(cfBalanceService, never()).earnByRule(any(Long.class), any(String.class), any(Long.class), any());
|
|
|
+ verify(cfBalanceService, never()).adjust(any(Long.class), any(Integer.class), any(Long.class), any(String.class), any(Long.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F19_同一幂等键_同一refId_不同键不同refId() {
|
|
|
+ when(cfEarnRuleService.getByCode("F19")).thenReturn(f19Rule());
|
|
|
+ when(userService.verifyPassword(9L, "secret")).thenReturn(true);
|
|
|
+ when(cfBalanceService.earnByRule(eq(1L), eq("F19"), any(Long.class), any()))
|
|
|
+ .thenReturn(earned("F19", "admin_adjust", 20, false));
|
|
|
+
|
|
|
+ service.adjustByAdmin(1L, 20, "补发", "secret", 9L, "same-key");
|
|
|
+ service.adjustByAdmin(1L, 20, "补发", "secret", 9L, "same-key");
|
|
|
+ service.adjustByAdmin(1L, 20, "补发", "secret", 9L, "other-key");
|
|
|
+
|
|
|
+ ArgumentCaptor<Long> refIdCaptor = ArgumentCaptor.forClass(Long.class);
|
|
|
+ verify(cfBalanceService, times(3)).earnByRule(eq(1L), eq("F19"), refIdCaptor.capture(), any());
|
|
|
+ List<Long> refIds = refIdCaptor.getAllValues();
|
|
|
+ // 同一幂等键 → 同一 refId(F-2 幂等键拦截重复提交);不同幂等键 → 不同 refId
|
|
|
+ assertEquals(refIds.get(0), refIds.get(1));
|
|
|
+ assertTrue(refIds.get(0).longValue() != refIds.get(2).longValue());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void F19_无幂等键_refId为空_每次独立调整() {
|
|
|
+ when(cfEarnRuleService.getByCode("F19")).thenReturn(f19Rule());
|
|
|
+ when(userService.verifyPassword(9L, "secret")).thenReturn(true);
|
|
|
+ when(cfBalanceService.earnByRule(eq(1L), eq("F19"), any(), any()))
|
|
|
+ .thenReturn(earned("F19", "admin_adjust", 20, false));
|
|
|
+
|
|
|
+ CfEarnResult r = service.adjustByAdmin(1L, 20, "补发", "secret", 9L, null);
|
|
|
+
|
|
|
+ assertNull(r.getRefId());
|
|
|
+ }
|
|
|
+}
|