Browse Source

chore: auto bump version and changelog [skip ci]

iwt 2 weeks ago
parent
commit
b470393701

+ 22 - 2
cfc-backend/src/test/java/com/etotem/cfc/service/CfCommissionServiceSettleTest.java

@@ -44,6 +44,8 @@ class CfCommissionServiceSettleTest {
     @Mock
     private PlatformPointsService platformPointsService;
     @Mock
+    private CfEarnDispatchService cfEarnDispatchService;
+    @Mock
     private CfTransferRecordMapper cfTransferRecordMapper;
     @Mock
     private PpointConfigService ppointConfigService;
@@ -114,8 +116,11 @@ class CfCommissionServiceSettleTest {
 
         cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
 
-        // 当前人返 CF:100元 × 100P点/100 × 10% = 10
-        verify(platformPointsService).earn(eq(BUYER_ID), eq(10), eq("order_consume"), eq(ORDER_ID), anyString());
+        // 当前人返 CF 走 F02 统一账本(不再直写旧账本):
+        // 实付 100 元 = 10000 分 → F02 发放
+        verify(cfEarnDispatchService).earnConsumeCf(eq(BUYER_ID), eq("product"), eq(ORDER_ID), eq(10000));
+        // 旧 order_consume 直写彻底移除
+        verify(platformPointsService, never()).earn(eq(BUYER_ID), anyInt(), eq("order_consume"), any(), anyString());
         // 只返 level2:100P点 × 10% = 10,一次
         verify(platformPointsService).earn(eq(20L), eq(10), eq("referral_dist"), eq(ORDER_ID), anyString());
         // level1 同家庭、level3 更远推荐人均不分润
@@ -151,4 +156,19 @@ class CfCommissionServiceSettleTest {
 
         verify(platformPointsService, never()).earn(anyLong(), anyInt(), eq("referral_dist"), any(), anyString());
     }
+
+    @Test
+    void settleReferrerOnly_会员订阅订单_当前人也返F02() {
+        // 会员/订阅仅做推荐人分润,但当前消费人仍按 N6 口径返 F02(所有消费订单都返)
+        mockReferralChain(ref(10L, 1));
+        when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
+        when(cfReferralService.getTotalTeamSize(10L)).thenReturn(5);
+        when(cfReferralService.matchRateTier(5)).thenReturn(tier(10));
+
+        cfCommissionService.settleReferrerOnly(ORDER_ID, "membership", BUYER_ID, BUYER_FAMILY, 19900);
+
+        verify(cfEarnDispatchService).earnConsumeCf(eq(BUYER_ID), eq("membership"), eq(ORDER_ID), eq(19900));
+        // 推荐人分润仍走旧口径(F-4 再收口)
+        verify(platformPointsService).earn(eq(10L), anyInt(), eq("referral_dist"), eq(ORDER_ID), anyString());
+    }
 }

+ 272 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/CfEarnDispatchServiceTest.java

@@ -0,0 +1,272 @@
+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.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.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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.Mockito.when;
+
+/**
+ * 统一发放分派服务(CfEarnDispatchService)单元测试:
+ *  - F02 消费返 CF / F04 商品赠品 / F05 活动签到 / F06 菌群调研返还 四条途径发放;
+ *  - 数量口径:F02/F06 实付分→CF 1:1(amt_unit=cent + cf_per_yuan=100),F04/F05 取 per_item 配置;
+ *  - 幂等键(member_id, type, ref_type, ref_id)生效:重复触发不重复入账;
+ *  - F02 跨订单类型 refId 命名空间隔离(不同业务表自增 ID 会重复);
+ *  - 用户未映射到家庭成员时不发放。
+ */
+class CfEarnDispatchServiceTest {
+
+    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 CfEarnDispatchService dispatchService = new CfEarnDispatchService();
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+        // MyBatis-Plus LambdaQueryWrapper 需要实体元数据(参照 CfBalanceServiceTest)
+        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, 0, 0, 0, 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;
+        });
+        when(familyMemberMapper.selectById(MEMBER_ID)).thenReturn(member(MEMBER_ID, FAMILY_ID));
+        when(configService.getConfig("cf.rate.cf_per_yuan")).thenReturn("100");
+        when(membershipService.getCurrentLevel(FAMILY_ID)).thenReturn(null);
+
+        // 规则种子 stub(与迁移408一致的口径):F02/F06 formula floor(amt)+amt_unit=cent;F04/F05 per_item
+        when(cfEarnRuleService.getByCode("F02"))
+                .thenReturn(rule("F02", "formula", "floor(amt)",
+                        "{\"cf_per_yuan\":100,\"amt_unit\":\"cent\"}", null, null, null, "cf_consume_order"));
+        when(cfEarnRuleService.getByCode("F04"))
+                .thenReturn(rule("F04", "per_item", null,
+                        "{\"source\":\"product_order_gift.cf_value\"}", null, null, null, "product_order_gift"));
+        when(cfEarnRuleService.getByCode("F05"))
+                .thenReturn(rule("F05", "per_item", null,
+                        "{\"source\":\"activities.platform_points\"}", null, null, null, "activity_checkin"));
+        when(cfEarnRuleService.getByCode("F06"))
+                .thenReturn(rule("F06", "formula", "floor(amt)",
+                        "{\"cf_per_yuan\":100,\"amt_unit\":\"cent\"}", null, null, null, "gut_health_refund"));
+
+        // 装配真实账本实现 + 分派服务
+        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(dispatchService, "cfBalanceService", cfBalanceService);
+        ReflectionTestUtils.setField(dispatchService, "familyMemberMapper", familyMemberMapper);
+
+        // 默认:用户 100 → 成员 1(登录账号自己的家庭成员记录)
+        FamilyMember resolved = new FamilyMember();
+        resolved.setId(MEMBER_ID);
+        resolved.setUserId(USER_ID);
+        resolved.setFamilyId(FAMILY_ID);
+        when(familyMemberMapper.selectOne(any())).thenReturn(resolved);
+    }
+
+    // ---------- F02 消费返 CF ----------
+
+    @Test
+    void earnConsumeCf_商品订单_按实付分1比1换算CF() {
+        // 199 元实付 = 19900 分 → 19900 CF(1 元 = 100 CF)
+        CfEarnResult r = dispatchService.earnConsumeCf(USER_ID, "product", 7L, 19900);
+
+        assertTrue(r.getEarned());
+        assertEquals(19900, r.getAmount());
+        assertEquals("cf_consume_order", r.getRefType());
+        assertEquals(701L, r.getRefId()); // 7×100 + product(1)
+        assertEquals(19900, cfBalanceService.getBalance(MEMBER_ID).getAvailable());
+
+        CfBalanceLog log = insertedLogs.get(0);
+        assertEquals("earn", log.getType());
+        assertEquals("cf_consume_order", log.getRefType());
+        assertEquals(701L, log.getRefId());
+        assertEquals(19900, log.getAmount());
+        assertEquals(MEMBER_ID, log.getMemberId());
+    }
+
+    @Test
+    void earnConsumeCf_重复触发_幂等键生效不重复发放() {
+        dispatchService.earnConsumeCf(USER_ID, "product", 7L, 19900);
+        CfEarnResult second = dispatchService.earnConsumeCf(USER_ID, "product", 7L, 19900);
+
+        assertTrue(second.getEarned());
+        assertTrue(second.getDuplicated());
+        assertEquals(1, insertedLogs.size());
+        assertEquals(19900, cfBalanceService.getBalance(MEMBER_ID).getAvailable());
+    }
+
+    @Test
+    void composeConsumeRefId_跨订单类型不冲突() {
+        // 商品订单 #1 与套餐订单 #1 分属不同业务表,refId 必须隔离,避免幂等误判
+        assertNotEquals(CfEarnDispatchService.composeConsumeRefId("product", 1L),
+                CfEarnDispatchService.composeConsumeRefId("package", 1L));
+        assertEquals(Long.valueOf(101L), CfEarnDispatchService.composeConsumeRefId("product", 1L));
+        assertEquals(Long.valueOf(102L), CfEarnDispatchService.composeConsumeRefId("package", 1L));
+        assertEquals(Long.valueOf(106L), CfEarnDispatchService.composeConsumeRefId("gut_health", 1L));
+    }
+
+    // ---------- F04 商品赠品 CF ----------
+
+    @Test
+    void earnGiftCf_赠品CF_取赠品配置数量() {
+        CfEarnResult r = dispatchService.earnGiftCf(USER_ID, 55L, 500);
+
+        assertTrue(r.getEarned());
+        assertEquals(500, r.getAmount());
+        assertEquals("product_order_gift", r.getRefType());
+        assertEquals(55L, r.getRefId());
+        assertEquals(500, cfBalanceService.getBalance(MEMBER_ID).getAvailable());
+    }
+
+    // ---------- F05 活动签到 CF ----------
+
+    @Test
+    void earnActivityCheckinCf_签到CF_取活动配置数量() {
+        CfEarnResult r = dispatchService.earnActivityCheckinCf(USER_ID, 88L, 300);
+
+        assertTrue(r.getEarned());
+        assertEquals(300, r.getAmount());
+        assertEquals("activity_checkin", r.getRefType());
+        assertEquals(88L, r.getRefId());
+        assertEquals(300, cfBalanceService.getBalance(MEMBER_ID).getAvailable());
+    }
+
+    // ---------- F06 菌群健康调研打卡返还 ----------
+
+    @Test
+    void earnGutHealthRefundCf_调研返还_项目价19_9元得1990CF() {
+        CfEarnResult r = dispatchService.earnGutHealthRefundCf(USER_ID, 66L, 1990);
+
+        assertTrue(r.getEarned());
+        assertEquals(1990, r.getAmount());
+        assertEquals("gut_health_refund", r.getRefType());
+        assertEquals(66L, r.getRefId());
+        assertEquals(1990, cfBalanceService.getBalance(MEMBER_ID).getAvailable());
+    }
+
+    @Test
+    void earnGutHealthRefundCf_重复触发_幂等不重复返还() {
+        dispatchService.earnGutHealthRefundCf(USER_ID, 66L, 1990);
+        CfEarnResult second = dispatchService.earnGutHealthRefundCf(USER_ID, 66L, 1990);
+
+        assertTrue(second.getDuplicated());
+        assertEquals(1, insertedLogs.size());
+        assertEquals(1990, cfBalanceService.getBalance(MEMBER_ID).getAvailable());
+    }
+
+    // ---------- 成员解析 ----------
+
+    @Test
+    void earnForUser_未找到家庭成员_不发放() {
+        when(familyMemberMapper.selectOne(any())).thenReturn(null);
+
+        CfEarnResult r = dispatchService.earnConsumeCf(999L, "product", 1L, 100);
+
+        assertFalse(r.getEarned());
+        assertTrue(r.getReason().contains("家庭成员"));
+        assertEquals(0, insertedLogs.size());
+    }
+
+    // ---------- 构造 ----------
+
+    private CfBalance balance(long memberId, int available, int frozen, int totalEarned, int withdrawn, int exchanged) {
+        CfBalance b = new CfBalance();
+        b.setId(memberId);
+        b.setMemberId(memberId);
+        b.setUserId(USER_ID);
+        b.setFamilyId(FAMILY_ID);
+        b.setAvailable(available);
+        b.setFrozen(frozen);
+        b.setTotalEarned(totalEarned);
+        b.setWithdrawn(withdrawn);
+        b.setExchanged(exchanged);
+        return b;
+    }
+
+    private FamilyMember member(long memberId, long familyId) {
+        FamilyMember m = new FamilyMember();
+        m.setId(memberId);
+        m.setFamilyId(familyId);
+        m.setUserId(USER_ID);
+        return m;
+    }
+
+    private CfEarnRule rule(String code, String ruleType, String formula, String paramsJson,
+                            Integer dailyLimit, Integer totalLimit, Integer cooldownSeconds, String dedupRefType) {
+        CfEarnRule r = new CfEarnRule();
+        r.setCode(code);
+        r.setName(code + "-规则");
+        r.setCategory("consume");
+        r.setEnabled(1);
+        r.setRuleType(ruleType);
+        r.setFormula(formula);
+        r.setParamsJson(paramsJson);
+        r.setDailyLimitPerMember(dailyLimit);
+        r.setTotalLimitPerMember(totalLimit);
+        r.setCooldownSeconds(cooldownSeconds);
+        r.setDedupRefType(dedupRefType);
+        return r;
+    }
+}

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-777f7da6d40ae71e7d75e70e1d397e3bd83caf66
+08be1b9577144b5f9a5349e5715cfc60ed2cfb47