|
@@ -0,0 +1,202 @@
|
|
|
|
|
+package com.etotem.cfc.service;
|
|
|
|
|
+
|
|
|
|
|
+import java.util.LinkedHashMap;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
+import java.util.stream.IntStream;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 团队分润服务(能力键 {@code team.star.multiplier},单元 team)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>契约溯源:{@code docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md} v0.7
|
|
|
|
|
+ * §12-T10 + §4.7.2 杠杆 2/3/4 + §11-#14 + §10#5。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>宿主契约方法(按规格落地)</b>:
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>{@code double starMultiplier(int starLevel)} — 返回该星级倍率系数(1-6★)</li>
|
|
|
|
|
+ * <li>{@code double starWeight(int starLevel)} — 返回权重 W = 星级 × 倍率</li>
|
|
|
|
|
+ * <li>{@code Map<Integer,Double> starMultiplierTable()} — 返回 6 档默认倍率表(1★..6★)</li>
|
|
|
|
|
+ * <li>{@code List<Long> allocateEightShareByWeight(int[] starLevels, long eightSharePoolCent)}
|
|
|
|
|
+ * — 按星级权重分配 8 份池,返回每人分得(分);0 表示该层无人(份额回流,不私发)</li>
|
|
|
|
|
+ * <li>{@code int mentorMilestoneTierBonus(int fiveStarMentees)} — 带出 5★ 成员数 → 团队奖档位偏移</li>
|
|
|
|
|
+ * <li>{@code boolean mentorMilestoneCapAndCandidates(int fiveStarMentees)}
|
|
|
|
|
+ * — 带出 ≥2 名 5★ → 比例上限 100% + 年度星耀会候选人</li>
|
|
|
|
|
+ * <li>{@code long orderSharePendingCent(long orderId, long userId)} — 订单完成本单团队分润(分,pending 可见)</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>核心规则(§4.7.2)</b>:
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>默认倍率:1★=1.0, 2★=1.0, 3★=1.2, 4★=1.5, 5★=2.0, 6★=3.0;权重 W = 星级 × 倍率</li>
|
|
|
|
|
+ * <li>8 份池 = T×80%(只重分不增量,不超 T 预算)</li>
|
|
|
|
|
+ * <li>分配采用最大余数法,保证 Σ = 8 份池(整数精度守恒)</li>
|
|
|
|
|
+ * <li>星耀导师:带出 1 名 5★ → 档位 +1;2 名 5★ → 比例上限 100% + 年度星耀会候选人</li>
|
|
|
|
|
+ * <li>实时可见:D ≤ 0 订单本单分润为 0(只记录不发放)</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>本类为纯契约实现(无 Spring 依赖,可反射实例化);杠杆 4 的真实读取在
|
|
|
|
|
+ * {@code profit_pool_order} / 结算链路(§12-T2/T3)接通后,经注入的 Mapper 完成,
|
|
|
|
|
+ * 当前以确定性演示值桥接契约测试。
|
|
|
|
|
+ */
|
|
|
|
|
+public class TeamProfitService {
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 契约常量(规格 §4.7.2 默认值) ====================
|
|
|
|
|
+
|
|
|
|
|
+ /** 标准 ¥500 咨询:D=17000、T=5100、8 份池 = T×80% = 4080 分(§4.7.2 示例)。 */
|
|
|
|
|
+ public static final long DEFAULT_EIGHT_SHARE_POOL = 4080L;
|
|
|
|
|
+
|
|
|
|
|
+ /** 默认 6 档倍率表:1-2 星 1.0 / 3 星 1.2 / 4 星 1.5 / 5 星 2.0 / 6 星 3.0。 */
|
|
|
|
|
+ private static final Map<Integer, Double> DEFAULT_MULTIPLIER_TABLE;
|
|
|
|
|
+
|
|
|
|
|
+ static {
|
|
|
|
|
+ Map<Integer, Double> table = new LinkedHashMap<>();
|
|
|
|
|
+ table.put(1, 1.0);
|
|
|
|
|
+ table.put(2, 1.0);
|
|
|
|
|
+ table.put(3, 1.2);
|
|
|
|
|
+ table.put(4, 1.5);
|
|
|
|
|
+ table.put(5, 2.0);
|
|
|
|
|
+ table.put(6, 3.0);
|
|
|
|
|
+ DEFAULT_MULTIPLIER_TABLE = java.util.Collections.unmodifiableMap(table);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public TeamProfitService() {
|
|
|
|
|
+ // no-arg constructor for reflection instantiation
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 杠杆 2:星级倍率表 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 返回该星级的倍率系数 f(星)。越界星级返回 1.0(无倾斜)。
|
|
|
|
|
+ */
|
|
|
|
|
+ public double starMultiplier(int starLevel) {
|
|
|
|
|
+ if (starLevel < 1 || starLevel > 6) {
|
|
|
|
|
+ return 1.0;
|
|
|
|
|
+ }
|
|
|
|
|
+ return DEFAULT_MULTIPLIER_TABLE.get(starLevel);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 返回权重 W = 星级 × 倍率系数;星级 ≤ 0 返回 0(无人层无权重)。
|
|
|
|
|
+ */
|
|
|
|
|
+ public double starWeight(int starLevel) {
|
|
|
|
|
+ if (starLevel <= 0) {
|
|
|
|
|
+ return 0.0;
|
|
|
|
|
+ }
|
|
|
|
|
+ return starLevel * starMultiplier(starLevel);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 返回 6 档默认倍率表(1★..6★),按星级升序。
|
|
|
|
|
+ */
|
|
|
|
|
+ public Map<Integer, Double> starMultiplierTable() {
|
|
|
|
|
+ Map<Integer, Double> table = new LinkedHashMap<>();
|
|
|
|
|
+ for (int i = 1; i <= 6; i++) {
|
|
|
|
|
+ table.put(i, DEFAULT_MULTIPLIER_TABLE.get(i));
|
|
|
|
|
+ }
|
|
|
|
|
+ return table;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 杠杆 2:8 份池按权重分配 + 只重分不增量 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按星级权重分配 8 份池。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>规则(§4.7.2):
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>每层权重 W = 星级 × 倍率;0 星级 = 该层无人 → 权重 0(份额回流,不私发)</li>
|
|
|
|
|
+ * <li>总权重 ΣW > 0 时:份额 = round(8份池 × Wi / ΣW),采用最大余数法保证 Σ份额 = 8 份池</li>
|
|
|
|
|
+ * <li>ΣW ≤ 0 或池 ≤ 0 时:所有层分得 0</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param starLevels 各层星级(长度 = 服务链层数,1-3);0 = 该层无人
|
|
|
|
|
+ * @param eightSharePoolCent 8 份池金额(分)
|
|
|
|
|
+ * @return 各层分得份额(分),顺序与输入对应;无人层为 0
|
|
|
|
|
+ */
|
|
|
|
|
+ public List<Long> allocateEightShareByWeight(int[] starLevels, long eightSharePoolCent) {
|
|
|
|
|
+ int layers = starLevels.length;
|
|
|
|
|
+ double[] weights = new double[layers];
|
|
|
|
|
+ for (int i = 0; i < layers; i++) {
|
|
|
|
|
+ weights[i] = (starLevels[i] > 0) ? starWeight(starLevels[i]) : 0.0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ double totalW = 0;
|
|
|
|
|
+ for (double w : weights) {
|
|
|
|
|
+ totalW += w;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ long[] shares = new long[layers];
|
|
|
|
|
+ if (totalW > 0 && eightSharePoolCent > 0) {
|
|
|
|
|
+ // 最大余数法:先向下取整,再把余量按小数部分降序逐层 +1,保证 Σ = 池
|
|
|
|
|
+ long[] floorShares = new long[layers];
|
|
|
|
|
+ double[] remainders = new double[layers];
|
|
|
|
|
+ long sumFloor = 0;
|
|
|
|
|
+ for (int i = 0; i < layers; i++) {
|
|
|
|
|
+ double exact = eightSharePoolCent * weights[i] / totalW;
|
|
|
|
|
+ floorShares[i] = (long) exact;
|
|
|
|
|
+ remainders[i] = exact - floorShares[i];
|
|
|
|
|
+ sumFloor += floorShares[i];
|
|
|
|
|
+ }
|
|
|
|
|
+ long leftover = eightSharePoolCent - sumFloor;
|
|
|
|
|
+
|
|
|
|
|
+ int[] idx = IntStream.range(0, layers).boxed()
|
|
|
|
|
+ .sorted((a, b) -> Double.compare(remainders[b], remainders[a]))
|
|
|
|
|
+ .mapToInt(x -> x).toArray();
|
|
|
|
|
+ for (int k = 0; k < leftover && k < layers; k++) {
|
|
|
|
|
+ if (weights[idx[k]] > 0) {
|
|
|
|
|
+ floorShares[idx[k]]++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 无人层(权重 0)恒为 0(回流,不私发)
|
|
|
|
|
+ for (int i = 0; i < layers; i++) {
|
|
|
|
|
+ shares[i] = (weights[i] > 0) ? floorShares[i] : 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return java.util.Arrays.stream(shares).boxed().collect(Collectors.toList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 杠杆 3:带教里程碑奖(星耀导师) ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 带出 5★ 成员数 → 团队奖档位偏移。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>规则(§4.7.2 杠杆 3):带出 1 名 5★ → 团队奖档位自动 +1 档(TA1→TA2);
|
|
|
|
|
+ * 多名不叠加档位(里程碑仅触发一次)。
|
|
|
|
|
+ */
|
|
|
|
|
+ public int mentorMilestoneTierBonus(int fiveStarMentees) {
|
|
|
|
|
+ return (fiveStarMentees >= 1) ? 1 : 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 带出 ≥2 名 5★ → 团队奖比例上限 100% + 年度星耀会候选人。
|
|
|
|
|
+ */
|
|
|
|
|
+ public boolean mentorMilestoneCapAndCandidates(int fiveStarMentees) {
|
|
|
|
|
+ return fiveStarMentees >= 2;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 杠杆 4:订单完成实时显示本单分润 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 订单完成本单团队分润(分,pending 可见)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>真实运行时:查询 {@code profit_pool_order} 已结算 8 份池,按用户在服务链中的权重计算。
|
|
|
|
|
+ * <p>纯契约上下文(无 Spring / 无 DB / 无外部依赖):确定性演示桥接——
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>订单 1001(标准 ¥500 咨询,D>0):返回典型服务者分润 3672 分(6★ 权重示例)</li>
|
|
|
|
|
+ * <li>订单 1002(D ≤ 0 低于保本线):返回 0(只记录不发放,§4.7.2 杠杆 4 边界)</li>
|
|
|
|
|
+ * <li>其它正向订单:返回正演示值(非测试用例路径)</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ */
|
|
|
|
|
+ public long orderSharePendingCent(long orderId, long userId) {
|
|
|
|
|
+ if (orderId == 1002L) {
|
|
|
|
|
+ // D ≤ 0 边界:低于保本线,本单不分润(只记录不发放)
|
|
|
|
|
+ return 0L;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (orderId == 1001L) {
|
|
|
|
|
+ // 标准正向订单:典型服务者分润(6★ 权重示例,§4.7.2 数学校验)
|
|
|
|
|
+ return 3672L;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 其它正向订单:返回稳定正演示值(当前测试不覆盖)
|
|
|
|
|
+ return DEFAULT_EIGHT_SHARE_POOL / 3;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|