Browse Source

feat(team.profit.hook): S-cfc-62 支付成功钩子接入 5 订单服务 6 调用点 + service_cost_config 表(契约单测转绿)

Agnes Agent 4 ngày trước cách đây
mục cha
commit
b037fe71b

+ 31 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -3483,6 +3483,8 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         createTeamAwardRecordTable();
         // 迁移461: 团队成员星级月度快照表(§7.6,uk_user_period 幂等)
         createTeamStarSnapshotTable();
+        // 迁移462: 服务成本价配置表(§7.5,uk_service_cost 幂等;成本价回填通道,S-cfc-62 team.profit.hook)
+        createServiceCostConfigTable();
     }
 
     /**
@@ -3603,6 +3605,35 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         }
     }
 
+    /**
+     * 迁移462: 创建服务成本价配置表(§7.5)。
+     *
+     * <p>{@code uk_service_cost(service_type, product_id)} 唯一键(NULL=对该类型通配);
+     * 供 §2.1 利润漏斗平台成本价 C 取值回填通道(默认 0,由配置键/管理端维护)。幂等:
+     * CREATE TABLE IF NOT EXISTS。</p>
+     *
+     * <p>口径来源:docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md §7.5,
+     * 能力键 S-cfc-62 team.profit.hook 第 4 张表。</p>
+     */
+    private void createServiceCostConfigTable() {
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS service_cost_config ("
+                    + "id BIGINT AUTO_INCREMENT PRIMARY KEY,"
+                    + "service_type VARCHAR(32) NOT NULL COMMENT '触发源',"
+                    + "product_id BIGINT NULL COMMENT '商品/服务ID(NULL=对该类型通配)',"
+                    + "cost_cent INT NOT NULL COMMENT '平台成本价 C(分)',"
+                    + "source VARCHAR(16) NOT NULL DEFAULT 'manual' COMMENT '成本来源: manual=人工维护/auto=自动测算',"
+                    + "remark VARCHAR(255) NULL COMMENT '备注',"
+                    + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',"
+                    + "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',"
+                    + "UNIQUE KEY uk_service_cost (service_type, product_id)"
+                    + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务成本价配置'");
+            log.info("迁移462: service_cost_config 表已就绪");
+        } catch (Exception e) {
+            log.warn("迁移462: service_cost_config 创建失败: {}", e.getMessage());
+        }
+    }
+
     /**
      * 迁移450: 会员权益文案与实际成交口径对齐(DML,幂等)。
      *

+ 10 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentOrderService.java

@@ -34,6 +34,9 @@ public class AssessmentOrderService extends ServiceImpl<AssessmentOrderMapper, A
     @Resource
     private CfCommissionService cfCommissionService;
 
+    @Resource
+    private ProfitPoolService profitPoolService;
+
     @Resource
     private UserMapper userMapper;
 
@@ -82,6 +85,13 @@ public class AssessmentOrderService extends ServiceImpl<AssessmentOrderMapper, A
             cfCommissionService.settle(order.getId(), "assessment", order.getUserId(),
                     buyerFamilyId,
                     order.getActualPrice() != null ? order.getActualPrice().intValue() : 0, null);
+            // 团队分润池钩子(§12-T3 同点位并行调用,S-cfc-62 team.profit.hook)
+            try {
+                profitPoolService.settle(order.getId(), "assessment", null,
+                        (long) (order.getActualPrice() != null ? order.getActualPrice().intValue() : 0), 0L);
+            } catch (Exception e) {
+                log.error("团队分润池核算失败: orderId={}, type=assessment, error={}", order.getId(), e.getMessage());
+            }
         }
         return updated;
     }

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/MemberSubscriptionService.java

@@ -32,6 +32,9 @@ public class MemberSubscriptionService {
     @Resource
     private CfCommissionService cfCommissionService;
 
+    @Resource
+    private ProfitPoolService profitPoolService;
+
     @Resource
     private ButlerService butlerService;
 
@@ -165,6 +168,12 @@ public class MemberSubscriptionService {
         if (family != null && family.getCreatorId() != null) {
             cfCommissionService.settleReferrerOnly(order.getId(), "subscription", family.getCreatorId(),
                     family.getId(), amount);
+            // 团队分润池钩子(§12-T3 同点位并行调用,S-cfc-62 team.profit.hook)
+            try {
+                profitPoolService.settle(order.getId(), "subscription", null, amount, 0L);
+            } catch (Exception e) {
+                log.error("团队分润池核算失败: orderId={}, type=subscription, error={}", order.getId(), e.getMessage());
+            }
         }
 
         return subscription;
@@ -237,6 +246,12 @@ public class MemberSubscriptionService {
         if (family != null && family.getCreatorId() != null) {
             cfCommissionService.settleReferrerOnly(order.getId(), "subscription", family.getCreatorId(),
                     family.getId(), amount);
+            // 团队分润池钩子(§12-T3 同点位并行调用,S-cfc-62 team.profit.hook)
+            try {
+                profitPoolService.settle(order.getId(), "subscription", null, amount, 0L);
+            } catch (Exception e) {
+                log.error("团队分润池核算失败: orderId={}, type=subscription, error={}", order.getId(), e.getMessage());
+            }
         }
 
         return current;

+ 11 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java

@@ -52,6 +52,9 @@ public class MembershipService implements MembershipServiceInterface {
     @Lazy
     private CfCommissionService cfCommissionService;
 
+    @Resource
+    private ProfitPoolService profitPoolService;
+
     @Resource
     private SysConfigService sysConfigService;
 
@@ -843,6 +846,14 @@ public class MembershipService implements MembershipServiceInterface {
             Long buyerFamilyId = (buyer != null) ? buyer.getFamilyId() : null;
             cfCommissionService.settleReferrerOnly(order.getId(), "membership", adminUserId, buyerFamilyId, order.getAmount());
 
+            // 团队分润池钩子(§12-T3 同点位并行调用,S-cfc-62 team.profit.hook)
+            try {
+                profitPoolService.settle(order.getId(), "membership", null,
+                        order.getAmount() != null ? order.getAmount() : 0, 0L);
+            } catch (Exception e) {
+                log.error("团队分润池核算失败: orderId={}, type=membership, error={}", order.getId(), e.getMessage());
+            }
+
             log.info("会员开通完成: orderNo={}, levelCode={}, amount={}, fromLevel={}",
                     order.getOrderNo(), order.getLevelCode(), order.getAmount(), fromLevel);
             return true;

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/PaymentService.java

@@ -63,6 +63,9 @@ public class PaymentService implements PaymentServiceInterface {
     @Resource
     private CfCommissionService cfCommissionService;
 
+    @Resource
+    private ProfitPoolService profitPoolService;
+
     @Lazy
     @Resource
     private ProductOrderService productOrderService;
@@ -262,6 +265,12 @@ public class PaymentService implements PaymentServiceInterface {
         if (updated) {
             cfCommissionService.settle(order.getId(), "package", order.getUserId(),
                     order.getFamilyId(), order.getPrice(), null);
+            // 团队分润池钩子(§12-T3 同点位并行调用,S-cfc-62 team.profit.hook)
+            try {
+                profitPoolService.settle(order.getId(), "package", null, order.getPrice(), 0L);
+            } catch (Exception e) {
+                log.error("团队分润池核算失败: orderId={}, type=package, error={}", order.getId(), e.getMessage());
+            }
         }
         return updated;
     }

+ 19 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -100,6 +100,9 @@ public class ProductOrderService {
     @Resource
     private CfCommissionService cfCommissionService;
 
+    @Resource
+    private ProfitPoolService profitPoolService;
+
     @Resource
     private PaymentService paymentService;
 
@@ -1068,6 +1071,22 @@ public class ProductOrderService {
             log.error("CF值分佣失败: orderId={}, error={}", order.getId(), e.getMessage());
         }
 
+        // 团队分润池钩子(§12-T3 同点位并行调用,S-cfc-62 team.profit.hook)
+        try {
+            Integer paidCent = order.getMoneyAmount() != null ? order.getMoneyAmount() : order.getTotalAmount();
+            Integer costCent = 0;
+            if (order.getProductId() != null) {
+                Product hp = productMapper.selectById(order.getProductId());
+                if (hp != null && hp.getPurchasePrice() != null) {
+                    costCent = hp.getPurchasePrice();
+                }
+            }
+            profitPoolService.settle(order.getId(), "product", order.getProductId(),
+                    (long) (paidCent != null ? paidCent : 0), (long) (costCent != null ? costCent : 0));
+        } catch (Exception e) {
+            log.error("团队分润池核算失败: orderId={}, type=product, error={}", order.getId(), e.getMessage());
+        }
+
         // 原佣金结算标记为废弃,保留兼容
         // commissionService.settle(order.getId(), "product", order.getBuyerId(),
         //        order.getTotalAmount(), order.getProductId());

+ 13 - 0
cfc-backend/src/main/resources/schema.sql

@@ -7177,3 +7177,16 @@ CREATE TABLE IF NOT EXISTS team_star_snapshot (
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     UNIQUE KEY uk_user_period (user_id, period)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='团队成员星级月度快照';
+
+-- 迁移462: 服务成本价配置表(§7.5,uk_service_cost 幂等;成本价回填通道,供 §2.1 利润漏斗 C 取值)
+CREATE TABLE IF NOT EXISTS service_cost_config (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    service_type VARCHAR(32) NOT NULL COMMENT '触发源: package/product/service/assessment/assessment_retest/subscription/membership',
+    product_id BIGINT NULL COMMENT '商品/服务ID(NULL=对该类型通配)',
+    cost_cent INT NOT NULL COMMENT '平台成本价 C(分)',
+    source VARCHAR(16) NOT NULL DEFAULT 'manual' COMMENT '成本来源: manual=人工维护/auto=自动测算',
+    remark VARCHAR(255) NULL COMMENT '备注',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    UNIQUE KEY uk_service_cost (service_type, product_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务成本价配置';