Просмотр исходного кода

feat(cf): CfCommissionService 统一分佣核心(双返/只返推荐人/同家庭上溯/阶梯比例)

Sisyphus 2 недель назад
Родитель
Сommit
50be1533da

+ 191 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CfCommissionService.java

@@ -0,0 +1,191 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.CfRateTier;
+import com.etotem.cfc.entity.CfTransferRecord;
+import com.etotem.cfc.entity.Product;
+import com.etotem.cfc.entity.ReferralTree;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.CfTransferRecordMapper;
+import com.etotem.cfc.mapper.ProductMapper;
+import com.etotem.cfc.mapper.ReferralTreeMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 统一 CF 值分佣服务。
+ * 规则:
+ *  - 普通订单(商品/套餐/测评):当前消费人返 CF + 推荐人按团队规模阶梯比例分润
+ *  - 会员/订阅订单:只返推荐人,不返当前人
+ *  - 同家庭互推:跳过本人,上溯到第一个非同家庭引荐人
+ */
+@Service
+public class CfCommissionService {
+
+    private static final Logger log = LoggerFactory.getLogger(CfCommissionService.class);
+
+    @Resource
+    private ReferralTreeMapper referralTreeMapper;
+    @Resource
+    private UserMapper userMapper;
+    @Resource
+    private CfReferralService cfReferralService;
+    @Resource
+    private PlatformPointsService platformPointsService;
+    @Resource
+    private CfTransferRecordMapper cfTransferRecordMapper;
+    @Resource
+    private PpointConfigService ppointConfigService;
+    @Resource
+    private SysConfigService sysConfigService;
+    @Resource
+    private ProductMapper productMapper;
+
+    /**
+     * 通用分佣(双返):当前人 + 推荐人
+     */
+    @Transactional
+    public void settle(Long orderId, String orderType, Long buyerUserId, Long buyerFamilyId,
+                       Integer orderAmountCent, Long productId) {
+        // 当前消费人返 CF(个人钱包)
+        int buyerReturn = calcBuyerReturn(orderType, orderAmountCent, productId, buyerUserId);
+        if (buyerReturn > 0) {
+            try {
+                platformPointsService.earn(buyerUserId, buyerReturn, "order_consume", orderId,
+                        "消费返CF:" + orderType);
+                record(buyerUserId, null, null, buyerReturn, "allocate", "order_consume", orderId, "消费返CF");
+            } catch (Exception e) {
+                log.error("当前人返CF失败: buyer={}, orderId={}, err={}", buyerUserId, orderId, e.getMessage());
+            }
+        }
+        // 推荐人分润
+        distributeToReferrers(orderId, orderType, buyerUserId, buyerFamilyId, orderAmountCent, productId);
+    }
+
+    /**
+     * 会员/订阅专用:只返推荐人
+     */
+    @Transactional
+    public void settleReferrerOnly(Long orderId, String orderType, Long buyerUserId, Long buyerFamilyId,
+                                   Integer orderAmountCent) {
+        distributeToReferrers(orderId, orderType, buyerUserId, buyerFamilyId, orderAmountCent, null);
+    }
+
+    /**
+     * 计算当前消费人返 CF。
+     * 商品:floor(orderAmount/100) × P点 × shareBps/10000
+     * 非商品:floor(orderAmount/100) × serviceRateBps/10000
+     */
+    private int calcBuyerReturn(String orderType, Integer orderAmountCent, Long productId, Long buyerUserId) {
+        if (orderAmountCent == null || orderAmountCent <= 0) return 0;
+        int amountYuan = orderAmountCent / 100;
+        if ("product".equals(orderType) && productId != null) {
+            try {
+                Product p = productMapper.selectById(productId);
+                if (p == null) return 0;
+                int effectivePpoint = ppointConfigService.getEffectivePpoint(productId, p.getCategoryId());
+                if (effectivePpoint <= 0) return 0;
+                int shareBps = getSysBps("product_platform_points_share", 1000);
+                return amountYuan * effectivePpoint / 100 * shareBps / 10000;
+            } catch (Exception e) {
+                return 0;
+            }
+        }
+        int serviceRateBps = getSysBps("commission_service_rate", 1000);
+        return amountYuan * serviceRateBps / 10000;
+    }
+
+    /**
+     * 推荐人分润:查全链路 → 同家庭跳过上溯 → 按阶梯比例
+     */
+    private void distributeToReferrers(Long orderId, String orderType, Long buyerUserId, Long buyerFamilyId,
+                                       Integer orderAmountCent, Long productId) {
+        if (orderAmountCent == null || orderAmountCent <= 0) return;
+        int amountYuan = orderAmountCent / 100;
+
+        // 取买家全链路推荐人(referral_tree,含全部层级)
+        List<ReferralTree> referrals = referralTreeMapper.selectList(
+                new LambdaQueryWrapper<ReferralTree>()
+                        .eq(ReferralTree::getChildId, buyerUserId)
+                        .orderByAsc(ReferralTree::getLevel));
+        if (referrals == null || referrals.isEmpty()) return;
+
+        List<Long> processed = new ArrayList<>();
+        for (ReferralTree ref : referrals) {
+            Long referrerId = ref.getParentId();
+            if (processed.contains(referrerId)) continue;
+            processed.add(referrerId);
+
+            User referrer = userMapper.selectById(referrerId);
+            if (referrer == null) continue;
+            // 同家庭跳过(D5):不返,继续上溯(循环继续)
+            if (buyerFamilyId != null && buyerFamilyId.equals(referrer.getFamilyId())) {
+                continue;
+            }
+            // 按团队规模匹配阶梯比例
+            int teamSize = cfReferralService.getTotalTeamSize(referrerId);
+            CfRateTier tier = cfReferralService.matchRateTier(teamSize);
+            int ratePercent = tier == null ? 0 : tier.getRatePercent();
+            if (ratePercent <= 0) continue;
+
+            // 分润基数:商品按 P点,非商品按服务费率
+            int base = calcReferrerBase(orderType, amountYuan, productId);
+            if (base <= 0) continue;
+            int share = base * ratePercent / 100;
+            if (share <= 0) continue;
+
+            try {
+                platformPointsService.earn(referrerId, share, "referral_dist", orderId,
+                        "推荐分润:" + orderType);
+                record(referrerId, null, referrer.getFamilyId(), share, "allocate", "referral_dist", orderId, "推荐分润");
+            } catch (Exception e) {
+                log.error("推荐人分润失败: referrer={}, orderId={}, err={}", referrerId, orderId, e.getMessage());
+            }
+        }
+    }
+
+    private int calcReferrerBase(String orderType, int amountYuan, Long productId) {
+        if ("product".equals(orderType) && productId != null) {
+            try {
+                Product p = productMapper.selectById(productId);
+                if (p == null) return 0;
+                return ppointConfigService.getEffectivePpoint(productId, p.getCategoryId());
+            } catch (Exception e) {
+                return 0;
+            }
+        }
+        int serviceRateBps = getSysBps("commission_service_rate", 1000);
+        return amountYuan * serviceRateBps / 10000;
+    }
+
+    private int getSysBps(String key, int def) {
+        try {
+            String v = sysConfigService.getValue(key);
+            if (v != null && !v.isEmpty()) return Integer.parseInt(v);
+        } catch (Exception ignored) {}
+        return def;
+    }
+
+    private void record(Long fromUserId, Long toUserId, Long familyId, int amount, String type,
+                        String refType, Long refId, String remark) {
+        CfTransferRecord r = new CfTransferRecord();
+        r.setFromUserId(fromUserId);
+        r.setToUserId(toUserId);
+        r.setFamilyId(familyId);
+        r.setAmount(amount);
+        r.setType(type);
+        r.setRefType(refType);
+        r.setRefId(refId);
+        r.setRemark(remark);
+        r.setCreatedAt(new Date());
+        try { cfTransferRecordMapper.insert(r); } catch (Exception e) { log.warn("写流转记录失败: {}", e.getMessage()); }
+    }
+}