Browse Source

feat(cfc-backend): S-cfc-68 能力键 team.admin.manage 管理端团队分润配置/成本价/流水查询/提现审核

TDD-GREEN: TeamProfitAdminManageContractTest 20/20 转绿(§12-T5 + §7.8 + §11-#12 + §11-#7,规格 2026-11-07-referral-team-profit-sharing.md v0.7)。

新增:
- TeamProfitConfigService: defaultStarTiers {0,8千,2.5万,7万,14万,25万}/getStarTiers(service_config team_profit.star_tiers 回退,非6档拒止)/比率 80/20/8/award_distribute=platform_rule/权重 50-40-10/saveStarTiers(6档校验)/saveConfig(group_key=team_profit)
- TeamProfitFlowService: queryTeamShareRecords (§7.3)/queryTeamAwardRecords (§7.4) 分页查询
- ServiceCostConfigService: getCostCent/saveCostCent (§7.5 成本价 upsert)
- TeamProfitAdminController: /api/admin/team-profit/* 8 个 @PostMapping(配置/流水/成本价/提现审核) + 平台角色门禁

修改:
- WithdrawalService: 新增 type 过滤重载 getAdminList(status,type,page,size) (§11-#7 team_share/team_award)
- DatabaseInitializer 迁移463 + schema.sql: service_cost_config 补 service_name/enabled 列(§7.5 规格列,实体已声明表缺列)

验证: mvn test -DskipTests=false -Dtest=TeamProfitAdminManageContractTest → BUILD SUCCESS 20/20(先前一次并发 cfc-web build 导致 surefire JVM 中断,独占重跑通过); cfc-web npm run build 通过
E2E Test Bot 4 days ago
parent
commit
17b6d6bfb

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

@@ -3485,6 +3485,9 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         createTeamStarSnapshotTable();
         // 迁移462: 服务成本价配置表(§7.5,uk_service_cost 幂等;成本价回填通道,S-cfc-62 team.profit.hook)
         createServiceCostConfigTable();
+        // 迁移463: service_cost_config 补 §7.5 规格列 service_name/enabled(S-cfc-68 team.admin.manage 成本价管理端维护载体)
+        ensureColumn("service_cost_config", "service_name", "VARCHAR(64) NULL COMMENT '服务名称'");
+        ensureColumn("service_cost_config", "enabled", "TINYINT DEFAULT 1 COMMENT '是否启用: 1=启用 0=停用'");
     }
 
     /**

+ 276 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/TeamProfitAdminController.java

@@ -0,0 +1,276 @@
+package com.etotem.cfc.controller.admin;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.AdminRoles;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.TeamAwardRecord;
+import com.etotem.cfc.entity.TeamShareRecord;
+import com.etotem.cfc.entity.WithdrawalRequest;
+import com.etotem.cfc.service.ServiceCostConfigService;
+import com.etotem.cfc.service.TeamProfitConfigService;
+import com.etotem.cfc.service.TeamProfitFlowService;
+import com.etotem.cfc.service.WithdrawalService;
+import com.etotem.cfc.util.ParamUtils;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 管理端团队分润(能力键 {@code team.admin.manage},单元 admin,§12-T5)。
+ *
+ * <p>契约溯源:{@code docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md} v0.7
+ * §12-T5:成本价/星级档位(后台配置 #12)/团队门槛/团队奖分档配置 + 分润流水查询与提现审核
+ * (复用现有管理端佣金模式)。</p>
+ *
+ * <p><b>接口约定(项目规范)</b>:{@code /api/admin} 前缀统一 {@code @PostMapping},
+ * JwtInterceptor 拦截 + 控制器内管理员角色门禁(admin/operator/finance 平台角色)。</p>
+ *
+ * <p><b>规格:docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md v0.7
+ * §7.5 + §7.8 + §11-#12 + §12-T5。</b></p>
+ */
+@Tag(name = "管理端-团队分润")
+@RestController
+@RequestMapping("/api/admin/team-profit")
+public class TeamProfitAdminController {
+
+    @Resource
+    private TeamProfitConfigService teamProfitConfigService;
+
+    @Resource
+    private TeamProfitFlowService teamProfitFlowService;
+
+    @Resource
+    private ServiceCostConfigService serviceCostConfigService;
+
+    @Resource
+    private WithdrawalService withdrawalService;
+
+    // ==================== 配置(§7.8 + §11-#12) ====================
+
+    @Operation(summary = "团队分润配置总览(星级档位/比率/份额/归属/权重)")
+    @PostMapping("/config")
+    public Result<Map<String, Object>> config(@RequestAttribute("role") String role,
+                                              @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        Result<Map<String, Object>> gate = requirePlatformRole(role, roles);
+        if (gate != null) {
+            return gate;
+        }
+        Map<String, Object> data = new LinkedHashMap<>();
+        data.put("starTiers", teamProfitConfigService.getStarTiers());
+        data.put("eightShareRatio", teamProfitConfigService.getEightShareRatio());
+        data.put("teamAwardRatio", teamProfitConfigService.getTeamAwardRatio());
+        data.put("shareTotal", teamProfitConfigService.getShareTotal());
+        data.put("awardDistribute", teamProfitConfigService.getAwardDistribute());
+        data.put("starWeight", teamProfitConfigService.getStarWeight());
+        return Result.success(data);
+    }
+
+    @Operation(summary = "保存星级档位(后台配置 #12,非 6 档拒收)")
+    @PostMapping("/config/star-tiers")
+    public Result<String> saveStarTiers(@RequestBody(required = false) Map<String, Object> body,
+                                        @RequestAttribute("role") String role,
+                                        @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        Result<Map<String, Object>> gate = requirePlatformRole(role, roles);
+        if (gate != null) {
+            return Result.error(gate.getCode(), gate.getMessage());
+        }
+        if (body == null) {
+            return Result.badRequest("请求体不能为空");
+        }
+        int[] tiers = toIntArray(body.get("starTiers"));
+        if (tiers == null) {
+            return Result.badRequest("starTiers 不能为空");
+        }
+        try {
+            teamProfitConfigService.saveStarTiers(tiers);
+        } catch (IllegalArgumentException e) {
+            return Result.badRequest(e.getMessage());
+        }
+        return Result.success("星级档位已保存");
+    }
+
+    @Operation(summary = "保存团队分润配置项(写入 service_config group_key='team_profit')")
+    @PostMapping("/config/save")
+    public Result<String> saveConfig(@RequestBody(required = false) Map<String, Object> body,
+                                     @RequestAttribute("role") String role,
+                                     @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        Result<Map<String, Object>> gate = requirePlatformRole(role, roles);
+        if (gate != null) {
+            return Result.error(gate.getCode(), gate.getMessage());
+        }
+        if (body == null) {
+            return Result.badRequest("请求体不能为空");
+        }
+        String key = (String) body.get("key");
+        String value = body.get("value") != null ? String.valueOf(body.get("value")) : null;
+        if (key == null || key.trim().isEmpty()) {
+            return Result.badRequest("配置键不能为空");
+        }
+        try {
+            teamProfitConfigService.saveConfig(key, value);
+        } catch (IllegalArgumentException e) {
+            return Result.badRequest(e.getMessage());
+        }
+        return Result.success("配置已保存");
+    }
+
+    // ==================== 分润流水查询(§7.3/§7.4) ====================
+
+    @Operation(summary = "8 份分佣流水分页查询(§7.3 team_share_record)")
+    @PostMapping("/flow/share-records")
+    public Result<Page<TeamShareRecord>> shareRecords(@RequestBody(required = false) Map<String, Object> body,
+                                                      @RequestAttribute("role") String role,
+                                                      @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        Result<Map<String, Object>> gate = requirePlatformRole(role, roles);
+        if (gate != null) {
+            return Result.error(gate.getCode(), gate.getMessage());
+        }
+        Long recipientUserId = ParamUtils.getLong(body != null ? body.get("recipientUserId") : null);
+        String status = body != null ? (String) body.get("status") : null;
+        int page = (int) ParamUtils.getLong(body != null ? body.get("page") : null, 1L);
+        int size = (int) ParamUtils.getLong(body != null ? body.get("size") : null, 10L);
+        return Result.success(teamProfitFlowService.queryTeamShareRecords(recipientUserId, status, page, size));
+    }
+
+    @Operation(summary = "团队奖流水分页查询(§7.4 team_award_record)")
+    @PostMapping("/flow/award-records")
+    public Result<Page<TeamAwardRecord>> awardRecords(@RequestBody(required = false) Map<String, Object> body,
+                                                      @RequestAttribute("role") String role,
+                                                      @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        Result<Map<String, Object>> gate = requirePlatformRole(role, roles);
+        if (gate != null) {
+            return Result.error(gate.getCode(), gate.getMessage());
+        }
+        Long teamId = ParamUtils.getLong(body != null ? body.get("teamId") : null);
+        String status = body != null ? (String) body.get("status") : null;
+        int page = (int) ParamUtils.getLong(body != null ? body.get("page") : null, 1L);
+        int size = (int) ParamUtils.getLong(body != null ? body.get("size") : null, 10L);
+        return Result.success(teamProfitFlowService.queryTeamAwardRecords(teamId, status, page, size));
+    }
+
+    // ==================== 成本价配置(§7.5) ====================
+
+    @Operation(summary = "读取服务成本价 C(分)")
+    @PostMapping("/cost/get")
+    public Result<Integer> getCost(@RequestBody(required = false) Map<String, Object> body,
+                                   @RequestAttribute("role") String role,
+                                   @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        Result<Map<String, Object>> gate = requirePlatformRole(role, roles);
+        if (gate != null) {
+            return Result.error(gate.getCode(), gate.getMessage());
+        }
+        String serviceType = body != null ? (String) body.get("serviceType") : null;
+        return Result.success(serviceCostConfigService.getCostCent(serviceType));
+    }
+
+    @Operation(summary = "保存服务成本价 C(分,管理端维护)")
+    @PostMapping("/cost/save")
+    public Result<Integer> saveCost(@RequestBody(required = false) Map<String, Object> body,
+                                    @RequestAttribute("role") String role,
+                                    @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        Result<Map<String, Object>> gate = requirePlatformRole(role, roles);
+        if (gate != null) {
+            return Result.error(gate.getCode(), gate.getMessage());
+        }
+        if (body == null) {
+            return Result.badRequest("请求体不能为空");
+        }
+        String serviceType = (String) body.get("serviceType");
+        String serviceName = (String) body.get("serviceName");
+        int costCent = (int) ParamUtils.getLong(body.get("costCent"), -1L);
+        if (serviceType == null || serviceType.trim().isEmpty()) {
+            return Result.badRequest("serviceType 不能为空");
+        }
+        if (costCent < 0) {
+            return Result.badRequest("costCent 不能为负数");
+        }
+        try {
+            return Result.success(serviceCostConfigService.saveCostCent(serviceType, serviceName, costCent));
+        } catch (IllegalArgumentException e) {
+            return Result.badRequest(e.getMessage());
+        }
+    }
+
+    // ==================== 提现审核(复用 withdrawal_requests,type=team_share/team_award,§11-#7) ====================
+
+    @Operation(summary = "提现审核列表(按来源 type=team_share/team_award 过滤,§11-#7)")
+    @PostMapping("/withdrawal/list")
+    public Result<Page<WithdrawalRequest>> withdrawalList(@RequestBody(required = false) Map<String, Object> body,
+                                                          @RequestAttribute("role") String role,
+                                                          @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        Result<Map<String, Object>> gate = requirePlatformRole(role, roles);
+        if (gate != null) {
+            return Result.error(gate.getCode(), gate.getMessage());
+        }
+        String status = body != null ? (String) body.get("status") : null;
+        String type = body != null ? (String) body.get("type") : null;
+        int page = (int) ParamUtils.getLong(body != null ? body.get("page") : null, 1L);
+        int size = (int) ParamUtils.getLong(body != null ? body.get("size") : null, 10L);
+        return Result.success(withdrawalService.getAdminList(status, type, page, size));
+    }
+
+    // ==================== 内部辅助 ====================
+
+    /** 管理员角色门禁:平台角色(admin/operator/finance)直通;否则 403。 */
+    private Result<Map<String, Object>> requirePlatformRole(String role, List<String> roles) {
+        if ("admin".equals(role) || AdminRoles.hasAnyPlatformRole(roles)) {
+            return null;
+        }
+        return Result.forbidden("无权限:需要管理员角色");
+    }
+
+    /** 解析请求体中的 int[](接受 List<Number>/List<String>/int[]),失败返回 null。 */
+    private int[] toIntArray(Object value) {
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof int[]) {
+            return ((int[]) value).clone();
+        }
+        if (value instanceof List) {
+            List<?> list = (List<?>) value;
+            int[] result = new int[list.size()];
+            for (int i = 0; i < list.size(); i++) {
+                Object item = list.get(i);
+                if (item instanceof Number) {
+                    result[i] = ((Number) item).intValue();
+                } else if (item instanceof String) {
+                    try {
+                        result[i] = Integer.parseInt(((String) item).trim());
+                    } catch (NumberFormatException e) {
+                        return null;
+                    }
+                } else {
+                    return null;
+                }
+            }
+            return result;
+        }
+        if (value instanceof String) {
+            String raw = ((String) value).replace('[', ' ').replace(']', ' ').trim();
+            if (raw.isEmpty()) {
+                return null;
+            }
+            String[] parts = raw.split("[,/]");
+            int[] result = new int[parts.length];
+            for (int i = 0; i < parts.length; i++) {
+                try {
+                    result[i] = Integer.parseInt(parts[i].trim());
+                } catch (NumberFormatException e) {
+                    return null;
+                }
+            }
+            return result;
+        }
+        return null;
+    }
+}

+ 92 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ServiceCostConfigService.java

@@ -0,0 +1,92 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.ServiceCostConfig;
+import com.etotem.cfc.mapper.ServiceCostConfigMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+
+/**
+ * 服务成本价配置服务(能力键 {@code team.admin.manage},单元 admin,§12-T5 成本价配置)。
+ *
+ * <p>契约溯源:{@code docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md} v0.7
+ * §7.5 + §12-T5。</p>
+ *
+ * <p><b>宿主契约方法</b>:
+ * <ul>
+ *   <li>{@code int getCostCent(String serviceType)} — 读服务成本价 C(分),未配置返回 0(§2.1 默认平台自担)</li>
+ *   <li>{@code int saveCostCent(String serviceType, String serviceName, int costCent)}
+ *       — 管理端维护成本价(upsert service_cost_config,返回生效成本价)</li>
+ * </ul>
+ *
+ * <p>成本价 C 是 §2.1 利润漏斗的输入(P = S − C),由平台运营真实填写(§10#3 合规红线),
+ * 供分润计算读取。纯反射契约上下文(无 Spring)下 Mapper 未注入时读取返回 0。</p>
+ *
+ * <p><b>规格:docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md v0.7
+ * §7.5 + §12-T5 + §10#3。</b></p>
+ */
+@Service
+public class ServiceCostConfigService {
+
+    @Resource
+    private ServiceCostConfigMapper serviceCostConfigMapper;
+
+    /** 未配置的成本价 C → 0(平台自担成本,分润按 S−C 计算,C=0 时 P=S)。 */
+    public int getCostCent(String serviceType) {
+        if (serviceType == null || serviceType.trim().isEmpty()) {
+            return 0;
+        }
+        if (serviceCostConfigMapper == null) {
+            return 0;
+        }
+        try {
+            ServiceCostConfig cfg = serviceCostConfigMapper.selectOne(
+                    new LambdaQueryWrapper<ServiceCostConfig>()
+                            .eq(ServiceCostConfig::getServiceType, serviceType.trim())
+                            .last("LIMIT 1"));
+            if (cfg != null && cfg.getCostCent() != null) {
+                return cfg.getCostCent().intValue();
+            }
+        } catch (Exception e) {
+            // 读取失败视为未配置(不抛异常使管理端崩溃),返回 0
+        }
+        return 0;
+    }
+
+    /**
+     * 保存服务成本价(管理端维护,upsert):已存在的 serviceType 更新成本价/服务名,
+     * 不存在则新增(enabled=1)。返回生效的成本价(分)。
+     *
+     * @throws IllegalArgumentException serviceType 为空或 costCent 为负数
+     */
+    public int saveCostCent(String serviceType, String serviceName, int costCent) {
+        if (serviceType == null || serviceType.trim().isEmpty()) {
+            throw new IllegalArgumentException("serviceType 不能为空");
+        }
+        if (costCent < 0) {
+            throw new IllegalArgumentException("成本价不能为负数");
+        }
+        String type = serviceType.trim();
+        ServiceCostConfig cfg = serviceCostConfigMapper.selectOne(
+                new LambdaQueryWrapper<ServiceCostConfig>()
+                        .eq(ServiceCostConfig::getServiceType, type)
+                        .last("LIMIT 1"));
+        if (cfg == null) {
+            cfg = new ServiceCostConfig();
+            cfg.setServiceType(type);
+            cfg.setServiceName(serviceName);
+            cfg.setCostCent((long) costCent);
+            cfg.setEnabled(1);
+            serviceCostConfigMapper.insert(cfg);
+        } else {
+            cfg.setServiceName(serviceName);
+            cfg.setCostCent((long) costCent);
+            if (cfg.getEnabled() == null) {
+                cfg.setEnabled(1);
+            }
+            serviceCostConfigMapper.updateById(cfg);
+        }
+        return costCent;
+    }
+}

+ 259 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/TeamProfitConfigService.java

@@ -0,0 +1,259 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.config.ConfigService;
+import com.etotem.cfc.entity.ServiceConfigItem;
+import com.etotem.cfc.mapper.ServiceConfigItemMapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+
+/**
+ * 团队分润管理端配置服务(能力键 {@code team.admin.manage},单元 admin)。
+ *
+ * <p>契约溯源:{@code docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md} v0.7
+ * §12-T5 + §7.8 + §11-#12 + §4.4 #3。</p>
+ *
+ * <p><b>宿主契约方法(按规格落地)</b>:
+ * <ul>
+ *   <li>{@code int[] defaultStarTiers()} — #12 默认六档 {0,8000,25000,70000,140000,250000}</li>
+ *   <li>{@code int[] getStarTiers()} — 读 service_config(team_profit.star_tiers),缺省回退默认;档位异常拒止</li>
+ *   <li>{@code int getEightShareRatio() / getTeamAwardRatio() / getShareTotal()} — 80 / 20 / 8</li>
+ *   <li>{@code String getAwardDistribute()} — "platform_rule"(§4.4 #3 拍板)</li>
+ *   <li>{@code int[] getStarWeight()} — {50,40,10}(§4.3 加权)</li>
+ *   <li>{@code void saveStarTiers(int[])} — 非 6 档拒收,落 service_config(后台配置 #12)</li>
+ *   <li>{@code void saveConfig(String,String)} — 写入 service_config,group_key='team_profit'(§7.8)</li>
+ * </ul>
+ *
+ * <p>读取链路:{@link ConfigService}(缓存 + 兜底)→ {@link ServiceConfigItemMapper} 直读 →
+ * §7.8/§11-#12 默认值。纯反射契约上下文(无 Spring)下三者均缺失时一律回退 §11-#12 已拍板默认值。</p>
+ *
+ * <p><b>规格:docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md v0.7
+ * §7.8 + §11-#12 + §12-T5。</b></p>
+ */
+@Service
+public class TeamProfitConfigService {
+
+    private static final Logger log = LoggerFactory.getLogger(TeamProfitConfigService.class);
+
+    /** §7.8 配置组(全部入 service_config,group_key='team_profit')。 */
+    public static final String GROUP_TEAM_PROFIT = "team_profit";
+
+    /** §7.8 配置键。 */
+    public static final String KEY_STAR_TIERS = "team_profit.star_tiers";
+    public static final String KEY_EIGHT_SHARE_RATIO = "team_profit.eight_share_ratio";
+    public static final String KEY_TEAM_AWARD_RATIO = "team_profit.team_award_ratio";
+    public static final String KEY_SHARE_TOTAL = "team_profit.share_total";
+    public static final String KEY_AWARD_DISTRIBUTE = "team_profit.award_distribute";
+    public static final String KEY_STAR_WEIGHT = "team_profit.star_weight";
+
+    /** §11-#12 星级档位推荐默认值(0 / 8千 / 2.5万 / 7万 / 14万 / 25万,后台可调)。 */
+    private static final int[] DEFAULT_STAR_TIERS = {0, 8000, 25000, 70000, 140000, 250000};
+
+    /** §7.8 默认比率/分母/归属/权重。 */
+    private static final int DEFAULT_EIGHT_SHARE_RATIO = 80;
+    private static final int DEFAULT_TEAM_AWARD_RATIO = 20;
+    private static final int DEFAULT_SHARE_TOTAL = 8;
+    private static final String DEFAULT_AWARD_DISTRIBUTE = "platform_rule";
+    private static final int[] DEFAULT_STAR_WEIGHT = {50, 40, 10};
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    @Resource
+    private ConfigService configService;
+
+    @Resource
+    private ServiceConfigItemMapper serviceConfigItemMapper;
+
+    // ==================== 星级档位(§11-#12,后台配置) ====================
+
+    /** #12 默认六档星级门槛(1-6 星一一对应)。 */
+    public int[] defaultStarTiers() {
+        return DEFAULT_STAR_TIERS.clone();
+    }
+
+    /**
+     * 生效中的星级档位:读 service_config(team_profit.star_tiers),缺省回退 #12 默认。
+     *
+     * <p>档位语义为 1-6 星六档,配置异常(非 6 档)不得静默放行——直接抛
+     * {@link IllegalArgumentException} 由管理端入口转 400,避免错误档位进入分润结算。</p>
+     */
+    public int[] getStarTiers() {
+        String raw = readConfig(KEY_STAR_TIERS);
+        if (raw != null && !raw.trim().isEmpty()) {
+            int[] tiers = parseJsonIntArray(raw.trim());
+            if (tiers != null) {
+                if (tiers.length != 6) {
+                    throw new IllegalArgumentException(
+                            "星级档位必须为 6 档(1-6 星一一对应),当前配置 " + tiers.length + " 档");
+                }
+                return tiers;
+            }
+            log.warn("团队分润配置 {} 解析失败,回退默认六档:{}", KEY_STAR_TIERS, raw);
+        }
+        return DEFAULT_STAR_TIERS.clone();
+    }
+
+    /**
+     * 保存星级档位(后台配置 #12):非 6 档拒收,合法六档写入 service_config(team_profit.star_tiers)。
+     */
+    public void saveStarTiers(int[] tiers) {
+        if (tiers == null || tiers.length != 6) {
+            throw new IllegalArgumentException("星级档位必须为 6 档(1-6 星一一对应),当前 "
+                    + (tiers == null ? 0 : tiers.length) + " 档");
+        }
+        saveConfig(KEY_STAR_TIERS, toJson(tiers));
+    }
+
+    // ==================== §7.8 比率/份额/归属/权重 ====================
+
+    /** 8 份分佣池占 T 比率(默认 80,team_profit.eight_share_ratio)。 */
+    public int getEightShareRatio() {
+        return readInt(KEY_EIGHT_SHARE_RATIO, DEFAULT_EIGHT_SHARE_RATIO);
+    }
+
+    /** 团队奖池占 T 比率(默认 20,team_profit.team_award_ratio)。 */
+    public int getTeamAwardRatio() {
+        return readInt(KEY_TEAM_AWARD_RATIO, DEFAULT_TEAM_AWARD_RATIO);
+    }
+
+    /** 8 份制分母(默认 8,team_profit.share_total,§4.2)。 */
+    public int getShareTotal() {
+        return readInt(KEY_SHARE_TOTAL, DEFAULT_SHARE_TOTAL);
+    }
+
+    /** 团队奖归属(默认 platform_rule,§4.4 #3 拍板)。 */
+    public String getAwardDistribute() {
+        String v = readConfig(KEY_AWARD_DISTRIBUTE);
+        return (v != null && !v.trim().isEmpty()) ? v.trim() : DEFAULT_AWARD_DISTRIBUTE;
+    }
+
+    /** 星级业绩分权重(默认 50/40/10,team_profit.star_weight,§4.3)。 */
+    public int[] getStarWeight() {
+        String raw = readConfig(KEY_STAR_WEIGHT);
+        if (raw != null && !raw.trim().isEmpty()) {
+            int[] weight = parseJsonIntArray(raw.trim());
+            if (weight == null) {
+                weight = parseSlashIntArray(raw.trim());
+            }
+            if (weight != null) {
+                return weight;
+            }
+            log.warn("团队分润配置 {} 解析失败,回退默认权重:{}", KEY_STAR_WEIGHT, raw);
+        }
+        return DEFAULT_STAR_WEIGHT.clone();
+    }
+
+    // ==================== 配置保存(§7.8,group_key='team_profit') ====================
+
+    /**
+     * 将配置写入 service_config(§7.8:全部入 service_config,group_key='team_profit')。
+     *
+     * <p>键未带 {@code team_profit.} 前缀时自动补齐,保证配置组归口一致;
+     * 优先走 {@link ConfigService#setConfig}(缓存失效 + cf.version 递增),兜底直写 Mapper。</p>
+     */
+    public void saveConfig(String key, String value) {
+        if (key == null || key.trim().isEmpty()) {
+            throw new IllegalArgumentException("配置键不能为空");
+        }
+        String cfgKey = key.startsWith(GROUP_TEAM_PROFIT + ".") ? key.trim() : GROUP_TEAM_PROFIT + "." + key.trim();
+        if (configService != null) {
+            configService.setConfig(cfgKey, value, null);
+            return;
+        }
+        if (serviceConfigItemMapper == null) {
+            throw new IllegalStateException("配置写入通道不可用(ConfigService/ServiceConfigItemMapper 均未注入)");
+        }
+        ServiceConfigItem item = serviceConfigItemMapper.selectOne(
+                new LambdaQueryWrapper<ServiceConfigItem>()
+                        .eq(ServiceConfigItem::getConfigKey, cfgKey)
+                        .last("LIMIT 1"));
+        Date now = new Date();
+        if (item == null) {
+            item = new ServiceConfigItem();
+            item.setConfigKey(cfgKey);
+            item.setGroupKey(GROUP_TEAM_PROFIT);
+            item.setConfigValue(value);
+            item.setValueType("string");
+            item.setEditable(1);
+            item.setCreatedAt(now);
+            item.setUpdatedAt(now);
+            serviceConfigItemMapper.insert(item);
+        } else {
+            item.setConfigValue(value);
+            item.setUpdatedAt(now);
+            serviceConfigItemMapper.updateById(item);
+        }
+    }
+
+    // ==================== 内部读取辅助(ConfigService 优先 → Mapper 直读 → 默认值) ====================
+
+    private String readConfig(String key) {
+        if (configService != null) {
+            return configService.getConfig(key);
+        }
+        if (serviceConfigItemMapper != null) {
+            try {
+                ServiceConfigItem item = serviceConfigItemMapper.selectOne(
+                        new LambdaQueryWrapper<ServiceConfigItem>()
+                                .eq(ServiceConfigItem::getConfigKey, key)
+                                .last("LIMIT 1"));
+                if (item != null) {
+                    return item.getConfigValue();
+                }
+            } catch (Exception e) {
+                log.warn("直读 service_config 失败 key={},回退默认值:{}", key, e.getMessage());
+            }
+        }
+        return null;
+    }
+
+    private int readInt(String key, int def) {
+        String v = readConfig(key);
+        if (v == null || v.trim().isEmpty()) {
+            return def;
+        }
+        try {
+            return Integer.parseInt(v.trim());
+        } catch (NumberFormatException e) {
+            log.warn("配置 {} 非整数:{},使用默认值 {}", key, v, def);
+            return def;
+        }
+    }
+
+    /** JSON 数组解析([1,2,...]),失败返回 null。 */
+    private int[] parseJsonIntArray(String raw) {
+        try {
+            return OBJECT_MAPPER.readValue(raw, int[].class);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /** 斜杠/逗号分隔解析("50/40/10" 或 "50,40,10"),失败返回 null。 */
+    private int[] parseSlashIntArray(String raw) {
+        try {
+            String cleaned = raw.replace('[', ' ').replace(']', ' ').replace(',', '/').trim();
+            String[] parts = cleaned.split("/");
+            int[] result = new int[parts.length];
+            for (int i = 0; i < parts.length; i++) {
+                result[i] = Integer.parseInt(parts[i].trim());
+            }
+            return result;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private String toJson(int[] values) {
+        try {
+            return OBJECT_MAPPER.writeValueAsString(values);
+        } catch (Exception e) {
+            throw new IllegalArgumentException("配置序列化失败:" + e.getMessage());
+        }
+    }
+}

+ 92 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/TeamProfitFlowService.java

@@ -0,0 +1,92 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.entity.TeamAwardRecord;
+import com.etotem.cfc.entity.TeamShareRecord;
+import com.etotem.cfc.mapper.TeamAwardRecordMapper;
+import com.etotem.cfc.mapper.TeamShareRecordMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+
+/**
+ * 团队分润流水查询服务(能力键 {@code team.admin.manage},单元 admin,§12-T5 流水查询)。
+ *
+ * <p>契约溯源:{@code docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md} v0.7
+ * §7.3 + §7.4 + §12-T5。</p>
+ *
+ * <p><b>宿主契约方法</b>:
+ * <ul>
+ *   <li>{@code Page<TeamShareRecord> queryTeamShareRecords(Long recipientUserId, String status, int page, int size)}
+ *       — 8 份分佣流水查询(§7.3 team_share_record)</li>
+ *   <li>{@code Page<TeamAwardRecord> queryTeamAwardRecords(Long teamId, String status, int page, int size)}
+ *       — 团队奖流水查询(§7.4 team_award_record)</li>
+ * </ul>
+ *
+ * <p>纯反射契约上下文(无 Spring)下 Mapper 未注入时返回空页,不抛异常。</p>
+ *
+ * <p><b>规格:docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md v0.7
+ * §7.3 + §7.4 + §12-T5。</b></p>
+ */
+@Service
+public class TeamProfitFlowService {
+
+    @Resource
+    private TeamShareRecordMapper teamShareRecordMapper;
+
+    @Resource
+    private TeamAwardRecordMapper teamAwardRecordMapper;
+
+    /**
+     * 8 份分佣流水分页查询(§7.3 team_share_record),按接收人/状态过滤。
+     *
+     * @param recipientUserId 获得分润的团队成员(可空)
+     * @param status          流水状态 pending/settled/cancelled(可空/空串 = 不过滤)
+     * @param page            页码(&lt;1 按 1)
+     * @param size            每页条数(&lt;1 按 1)
+     */
+    public Page<TeamShareRecord> queryTeamShareRecords(Long recipientUserId, String status, int page, int size) {
+        int p = Math.max(page, 1);
+        int s = Math.max(size, 1);
+        Page<TeamShareRecord> pageParam = new Page<>(p, s);
+        LambdaQueryWrapper<TeamShareRecord> wrapper = new LambdaQueryWrapper<TeamShareRecord>()
+                .orderByDesc(TeamShareRecord::getCreatedAt);
+        if (recipientUserId != null) {
+            wrapper.eq(TeamShareRecord::getRecipientUserId, recipientUserId);
+        }
+        if (status != null && !status.trim().isEmpty()) {
+            wrapper.eq(TeamShareRecord::getStatus, status.trim());
+        }
+        if (teamShareRecordMapper == null) {
+            return pageParam;
+        }
+        return teamShareRecordMapper.selectPage(pageParam, wrapper);
+    }
+
+    /**
+     * 团队奖流水分页查询(§7.4 team_award_record),按团队/状态过滤。
+     *
+     * @param teamId 归属服务团队(distribution_systems.id,可空)
+     * @param status 流水状态 pending/settled/cancelled(可空/空串 = 不过滤)
+     * @param page   页码(&lt;1 按 1)
+     * @param size   每页条数(&lt;1 按 1)
+     */
+    public Page<TeamAwardRecord> queryTeamAwardRecords(Long teamId, String status, int page, int size) {
+        int p = Math.max(page, 1);
+        int s = Math.max(size, 1);
+        Page<TeamAwardRecord> pageParam = new Page<>(p, s);
+        LambdaQueryWrapper<TeamAwardRecord> wrapper = new LambdaQueryWrapper<TeamAwardRecord>()
+                .orderByDesc(TeamAwardRecord::getCreatedAt);
+        if (teamId != null) {
+            wrapper.eq(TeamAwardRecord::getTeamId, teamId);
+        }
+        if (status != null && !status.trim().isEmpty()) {
+            wrapper.eq(TeamAwardRecord::getStatus, status.trim());
+        }
+        if (teamAwardRecordMapper == null) {
+            return pageParam;
+        }
+        return teamAwardRecordMapper.selectPage(pageParam, wrapper);
+    }
+}

+ 17 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/WithdrawalService.java

@@ -115,12 +115,29 @@ public class WithdrawalService {
     }
 
     public Page<WithdrawalRequest> getAdminList(String status, int page, int size) {
+        return getAdminList(status, null, page, size);
+    }
+
+    /**
+     * 提现审核列表(type 过滤重载,§11-#7 团队分润提现审核)。
+     *
+     * <p>管理口径:复用 {@code withdrawal_requests},type 区分 {@code team_share} / {@code team_award}
+     * (团队货币分润提现)与既有 {@code cf_balance} / {@code commission};status 与 type 可组合过滤
+     * (能力键 {@code team.admin.manage},S-cfc-68)。</p>
+     *
+     * @param status 提现状态 pending/approved/rejected(可空 = 不过滤)
+     * @param type   提现来源 team_share/team_award/cf_balance/commission(可空 = 不过滤)
+     */
+    public Page<WithdrawalRequest> getAdminList(String status, String type, int page, int size) {
         Page<WithdrawalRequest> pageParam = new Page<>(page, size);
         LambdaQueryWrapper<WithdrawalRequest> wrapper = new LambdaQueryWrapper<WithdrawalRequest>()
                 .orderByDesc(WithdrawalRequest::getCreatedAt);
         if (status != null && !status.isEmpty()) {
             wrapper.eq(WithdrawalRequest::getStatus, status);
         }
+        if (type != null && !type.isEmpty()) {
+            wrapper.eq(WithdrawalRequest::getType, type);
+        }
         SortUtil.applySort(wrapper);
         return withdrawalRequestMapper.selectPage(pageParam, wrapper);
     }

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

@@ -7182,8 +7182,10 @@ CREATE TABLE IF NOT EXISTS team_star_snapshot (
 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',
+    service_name VARCHAR(64) NULL COMMENT '服务名称',
     product_id BIGINT NULL COMMENT '商品/服务ID(NULL=对该类型通配)',
     cost_cent INT NOT NULL COMMENT '平台成本价 C(分)',
+    enabled TINYINT DEFAULT 1 COMMENT '是否启用: 1=启用 0=停用',
     source VARCHAR(16) NOT NULL DEFAULT 'manual' COMMENT '成本来源: manual=人工维护/auto=自动测算',
     remark VARCHAR(255) NULL COMMENT '备注',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',