Browse Source

feat(team.profit.settle): S-cfc-61 GREEN 实现 - 8份分佣+团队奖方法并入 TeamProfitService + service_cost_config 实体/Mapper(5 契约类 32 例全绿)

在 S-cfc-65 星级倍率版 TeamProfitService 基础上并入 S-cfc-61 契约方法,保持星级杠杆逻辑不变(纯增量,0 删除):
- allocateEightShares(Long,Long,String,Long,long,long[],int[]):§4.2 #5 八份分佣,逐层 min(当期星级,剩余),金额 round(8份池×份数/8),无人层份额回流不落记录
- buildTeamAward(Long,Long,String,Long,Long,long,long,long):§4.4 #3 团队奖分档 TA0/TA1/TA2/TA3=0%/50%/80%/100%,归属平台自有团队负责人

新增 service_cost_config 建表契约的实体 + Mapper(§7.5):
- entity/ServiceCostConfig.java、mapper/ServiceCostConfigMapper.java

验证:
- 5 契约类 32 例全绿:ProfitPoolOrder 4 + TeamShareRecordEightShare 6 + TeamAwardRecord 6 + StarScoreCalculator 10(复用 S-cfc-43 实现,无冲突)+ TeamProfitSchema 6
- 无回归:TeamProfitPipeline 8(S-cfc-64)、TeamStarMultiplier 13(S-cfc-65)、TeamUserPortal 4(S-cfc-63)
Agnes Agent 4 days ago
parent
commit
649dd2be7

+ 34 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ServiceCostConfig.java

@@ -0,0 +1,34 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 平台服务成本价配置(货币分润成本载体,§7.5)。
+ *
+ * <p>服务(咨询/测评)的成本价 C 载体:settle 时按 serviceType 读取,
+ * 作为 §2.1 总账「平台成本价 C(分)」的来源。</p>
+ */
+@Data
+@TableName("service_cost_config")
+public class ServiceCostConfig implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** planner_consult/assessor_dan/... */
+    private String serviceType;
+
+    /** 服务名称 */
+    private String serviceName;
+
+    /** 平台成本价 C(分) */
+    private Long costCent;
+
+    /** 是否启用(1=启用 0=停用) */
+    private Integer enabled;
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/ServiceCostConfigMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ServiceCostConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ServiceCostConfigMapper extends BaseMapper<ServiceCostConfig> {
+}

+ 114 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/TeamProfitService.java

@@ -1,5 +1,9 @@
 package com.etotem.cfc.service;
 package com.etotem.cfc.service;
 
 
+import com.etotem.cfc.entity.TeamAwardRecord;
+import com.etotem.cfc.entity.TeamShareRecord;
+
+import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
@@ -199,4 +203,114 @@ public class TeamProfitService {
         // 其它正向订单:返回稳定正演示值(当前测试不覆盖)
         // 其它正向订单:返回稳定正演示值(当前测试不覆盖)
         return DEFAULT_EIGHT_SHARE_POOL / 3;
         return DEFAULT_EIGHT_SHARE_POOL / 3;
     }
     }
+
+    // ==================== 杠杆 1 基线:8 份分佣(§4.2 #5 拍板)+ 团队奖(§4.4 #3 拍板) ====================
+
+    /** §7.8 team_profit.share_total 默认 8(8 份分佣)。 */
+    private static final int SHARE_TOTAL = 8;
+
+    /**
+     * §4.2 8 份分佣:按层分配份额并生成 {@code team_share_record} 流水(未落库,返回列表)。
+     *
+     * <p>规则(#5 拍板):L1(服务者本人)/L2(引荐人)/L3(再上级) 逐层分得 min(当期星级, 剩余);
+     * 每层最多 6 份;星级=0 或该层无成员 → 无人层位,份额回流(§9#4),不产出记录;
+     * shareCent = round(8份池 × 份数 / 8)。</p>
+     *
+     * @param poolId             来源 profit_pool_order.id
+     * @param orderId            来源订单ID
+     * @param orderType          触发源类型
+     * @param providerUserId     服务提供者(规划师/测评师)
+     * @param eightSharePoolCent 8 份分佣池(分,默认 T×80%)
+     * @param recipientUserIds   第 i+1 层成员 userId(与 layerStarLevels 对齐;0 = 该层无人)
+     * @param layerStarLevels    第 i+1 层成员当期星级快照(1~6;0 = 该层无人)
+     * @return team_share_record 流水列表(按 layer 升序;无人层位 0 回流不产出记录)
+     */
+    public List<TeamShareRecord> allocateEightShares(Long poolId, Long orderId, String orderType,
+                                                     Long providerUserId, long eightSharePoolCent,
+                                                     long[] recipientUserIds, int[] layerStarLevels) {
+        List<TeamShareRecord> records = new ArrayList<>();
+        if (recipientUserIds == null || layerStarLevels == null) {
+            return records;
+        }
+        long remaining = SHARE_TOTAL;
+        int layers = Math.min(recipientUserIds.length, layerStarLevels.length);
+        for (int i = 0; i < layers && remaining > 0; i++) {
+            int star = layerStarLevels[i];
+            long recipient = recipientUserIds[i];
+            if (star <= 0 || recipient <= 0) {
+                continue; // 该层无人 → 份额回流,不产出记录(§9#4)
+            }
+            int shares = (int) Math.min(star, remaining);
+            if (shares <= 0) {
+                continue;
+            }
+            long shareCent = Math.round(eightSharePoolCent * shares / 8.0);
+
+            TeamShareRecord r = new TeamShareRecord();
+            r.setPoolId(poolId);
+            r.setOrderId(orderId);
+            r.setOrderType(orderType);
+            r.setProviderUserId(providerUserId);
+            r.setRecipientUserId(recipient);
+            r.setLayer(i + 1);
+            r.setStarLevel(star);
+            r.setShares(shares);
+            r.setShareCent((int) shareCent);
+            r.setStatus("pending");
+            records.add(r);
+            remaining -= shares;
+        }
+        return records;
+    }
+
+    /**
+     * §4.4 团队奖:按团队年度滚动业绩分档,生成 {@code team_award_record}(未落库,返回记录)。
+     *
+     * <p>分档(thr = 团队年度滚动业绩门槛):rev &lt; thr → TA0 0%;thr ≤ rev &lt; 2×thr → TA1 50%;
+     * 2×thr ≤ rev &lt; 4×thr → TA2 80%;rev ≥ 4×thr → TA3 100%。awardCent = round(团队奖池 × 比例)。
+     * 归属平台自有团队负责人(ownerUserId,#3 拍板)。</p>
+     *
+     * @param poolId                来源 profit_pool_order.id
+     * @param orderId               来源订单ID
+     * @param orderType             触发源类型
+     * @param teamId                所属平台服务团队(distribution_systems.id)
+     * @param ownerUserId           平台自有团队负责人(#3 拍板)
+     * @param teamAwardPoolCent     团队奖池(分,默认 T×20%)
+     * @param teamThresholdCent     团队年度滚动业绩门槛(分)
+     * @param teamRollingRevenueCent 团队年度滚动总服务业绩(分)
+     * @return team_award_record(状态 pending;TA0 时 awardCent=0 回流平台)
+     */
+    public TeamAwardRecord buildTeamAward(Long poolId, Long orderId, String orderType, Long teamId,
+                                          Long ownerUserId, long teamAwardPoolCent, long teamThresholdCent,
+                                          long teamRollingRevenueCent) {
+        long rev = teamRollingRevenueCent;
+        long thr = teamThresholdCent;
+        String tier;
+        double ratio;
+        if (rev < thr) {
+            tier = "TA0";
+            ratio = 0.0;
+        } else if (rev < 2 * thr) {
+            tier = "TA1";
+            ratio = 0.50;
+        } else if (rev < 4 * thr) {
+            tier = "TA2";
+            ratio = 0.80;
+        } else {
+            tier = "TA3";
+            ratio = 1.00;
+        }
+        long award = Math.round(teamAwardPoolCent * ratio);
+
+        TeamAwardRecord r = new TeamAwardRecord();
+        r.setPoolId(poolId);
+        r.setOrderId(orderId);
+        r.setOrderType(orderType);
+        r.setTeamId(teamId);
+        r.setOwnerUserId(ownerUserId);
+        r.setTier(tier);
+        r.setAwardCent((int) award);
+        r.setStatus("pending");
+        return r;
+    }
 }
 }