Kaynağa Gözat

feat(backend): S-cfc-68 实现团队分润管理端 team.admin.manage 能力键

契约:TeamProfitAdminManageContractTest(20/20 绿)。
- TeamProfitAdminController(/api/admin/team-profit/*,全 @PostMapping,role=admin 门禁)
- TeamProfitConfigService(星级档位/分佣比例/service_config 配置)
- TeamProfitFlowService(team_share_records/team_award_records 流水分页)
- ServiceCostConfigService(service_cost_config 成本价 get/save,适配远端实体 costCent Long)
- WithdrawalService 4 参 getAdminList 重载(提现审核分页)
- API_REFERENCE 4.66 管理端接口文档

DatabaseInitializer/schema.sql 迁移 461/462 由 S-cfc-61/62 并入,本任务不再重复建表。
Sisyphus Agent 6 gün önce
ebeveyn
işleme
753dc16ec8

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

@@ -0,0 +1,234 @@
+package com.etotem.cfc.controller.admin;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+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 org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+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/团队门槛/团队奖分档配置 + 分润流水查询 + 提现审核,复用
+ * 现有管理端佣金模式)。统一 {@code /api/admin} 前缀 + 全部 {@code @PostMapping} +
+ * {@code JwtInterceptor} 拦截 + 管理员角色门禁(role=admin)。</p>
+ */
+@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;
+
+    // ==================== 配置:星级档位(§11-#12 后台可调) ====================
+
+    @PostMapping("/config/star-tiers")
+    public Result<int[]> getStarTiers(@RequestAttribute("role") String role) {
+        if (!isAdmin(role)) {
+            return Result.error("无权限");
+        }
+        return Result.success(teamProfitConfigService.getStarTiers());
+    }
+
+    @PostMapping("/config/default-star-tiers")
+    public Result<int[]> getDefaultStarTiers(@RequestAttribute("role") String role) {
+        if (!isAdmin(role)) {
+            return Result.error("无权限");
+        }
+        return Result.success(teamProfitConfigService.defaultStarTiers());
+    }
+
+    @PostMapping("/config/save-star-tiers")
+    public Result<String> saveStarTiers(@RequestBody Map<String, Object> params,
+                                        @RequestAttribute("role") String role) {
+        if (!isAdmin(role)) {
+            return Result.error("无权限");
+        }
+        Object raw = params == null ? null : params.get("tiers");
+        if (!(raw instanceof java.util.List)) {
+            return Result.error("tiers 必须为 6 档整数数组");
+        }
+        java.util.List<?> list = (java.util.List<?>) raw;
+        if (list.size() != 6) {
+            return Result.error("星级档位必须为 6 档(1-6 星一一对应)");
+        }
+        int[] tiers = new int[6];
+        for (int i = 0; i < 6; i++) {
+            Object item = list.get(i);
+            if (item == null) {
+                return Result.error("星级档位不能为空");
+            }
+            try {
+                tiers[i] = Integer.parseInt(String.valueOf(item).trim());
+            } catch (NumberFormatException e) {
+                return Result.error("星级档位必须为整数");
+            }
+        }
+        try {
+            teamProfitConfigService.saveStarTiers(tiers);
+        } catch (IllegalArgumentException e) {
+            return Result.error(e.getMessage());
+        }
+        return Result.success("保存成功");
+    }
+
+    // ==================== 配置:比率 / 份额 / 归属 / 权重(§7.8) ====================
+
+    @PostMapping("/config/ratios")
+    public Result<Map<String, Object>> getRatios(@RequestAttribute("role") String role) {
+        if (!isAdmin(role)) {
+            return Result.error("无权限");
+        }
+        Map<String, Object> data = new HashMap<>();
+        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);
+    }
+
+    @PostMapping("/config/save")
+    public Result<String> saveConfig(@RequestBody Map<String, Object> params,
+                                     @RequestAttribute("role") String role) {
+        if (!isAdmin(role)) {
+            return Result.error("无权限");
+        }
+        String key = params == null ? null : (String) params.get("key");
+        Object rawValue = params == null ? null : params.get("value");
+        if (key == null || key.trim().isEmpty() || rawValue == null) {
+            return Result.error("key 与 value 不能为空");
+        }
+        try {
+            teamProfitConfigService.saveConfig(key, String.valueOf(rawValue));
+        } catch (IllegalArgumentException e) {
+            return Result.error(e.getMessage());
+        }
+        return Result.success("保存成功");
+    }
+
+    // ==================== 配置:服务成本价(§7.5 + §11-#8) ====================
+
+    @PostMapping("/cost/get")
+    public Result<Integer> getCost(@RequestBody Map<String, Object> params,
+                                   @RequestAttribute("role") String role) {
+        if (!isAdmin(role)) {
+            return Result.error("无权限");
+        }
+        String serviceType = params == null ? null : (String) params.get("serviceType");
+        if (serviceType == null || serviceType.trim().isEmpty()) {
+            return Result.error("serviceType 不能为空");
+        }
+        return Result.success(serviceCostConfigService.getCostCent(serviceType));
+    }
+
+    @PostMapping("/cost/save")
+    public Result<Integer> saveCost(@RequestBody Map<String, Object> params,
+                                    @RequestAttribute("role") String role) {
+        if (!isAdmin(role)) {
+            return Result.error("无权限");
+        }
+        String serviceType = params == null ? null : (String) params.get("serviceType");
+        String serviceName = params == null ? null : (String) params.get("serviceName");
+        Integer costCent = null;
+        Object costRaw = params == null ? null : params.get("costCent");
+        if (costRaw != null) {
+            try {
+                costCent = Integer.parseInt(String.valueOf(costRaw).trim());
+            } catch (NumberFormatException e) {
+                costCent = null;
+            }
+        }
+        if (serviceType == null || serviceType.trim().isEmpty()) {
+            return Result.error("serviceType 不能为空");
+        }
+        if (costCent == null || costCent < 0) {
+            return Result.error("costCent 必须为非负整数(分)");
+        }
+        return Result.success(serviceCostConfigService.saveCostCent(serviceType, serviceName, costCent));
+    }
+
+    // ==================== 分润流水查询(§7.3 / §7.4) ====================
+
+    @PostMapping("/flow/share-records")
+    public Result<Page<TeamShareRecord>> shareRecords(@RequestBody Map<String, Object> params,
+                                                      @RequestAttribute("role") String role) {
+        if (!isAdmin(role)) {
+            return Result.error("无权限");
+        }
+        Long recipientUserId = ParamUtils.getLong(params == null ? null : params.get("recipientUserId"));
+        String status = params == null ? null : (String) params.get("status");
+        int page = getInt(params, "page", 1);
+        int size = getInt(params, "size", 20);
+        return Result.success(teamProfitFlowService.queryTeamShareRecords(recipientUserId, status, page, size));
+    }
+
+    @PostMapping("/flow/award-records")
+    public Result<Page<TeamAwardRecord>> awardRecords(@RequestBody Map<String, Object> params,
+                                                      @RequestAttribute("role") String role) {
+        if (!isAdmin(role)) {
+            return Result.error("无权限");
+        }
+        Long ownerUserId = ParamUtils.getLong(params == null ? null : params.get("ownerUserId"));
+        String status = params == null ? null : (String) params.get("status");
+        int page = getInt(params, "page", 1);
+        int size = getInt(params, "size", 20);
+        return Result.success(teamProfitFlowService.queryTeamAwardRecords(ownerUserId, status, page, size));
+    }
+
+    // ==================== 提现审核(复用 withdrawal_requests,§11-#7) ====================
+
+    @PostMapping("/withdrawals")
+    public Result<Page<WithdrawalRequest>> withdrawals(@RequestBody Map<String, Object> params,
+                                                       @RequestAttribute("role") String role) {
+        if (!isAdmin(role)) {
+            return Result.error("无权限");
+        }
+        String status = params == null ? null : (String) params.get("status");
+        String type = params == null ? null : (String) params.get("type");
+        int page = getInt(params, "page", 1);
+        int size = getInt(params, "size", 20);
+        return Result.success(withdrawalService.getAdminList(status, type, page, size));
+    }
+
+    // ==================== helpers ====================
+
+    private static boolean isAdmin(String role) {
+        return "admin".equals(role);
+    }
+
+    private static int getInt(Map<String, Object> params, String key, int defaultValue) {
+        if (params == null || params.get(key) == null) {
+            return defaultValue;
+        }
+        try {
+            return ((Number) params.get(key)).intValue();
+        } catch (ClassCastException e) {
+            return defaultValue;
+        }
+    }
+}

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

@@ -0,0 +1,79 @@
+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;T5 成本价配置)。
+ *
+ * <p>契约溯源:{@code docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md} v0.7
+ * §7.5({@code service_cost_config} 载体,管理端维护成本价 C 供分润计算读取)+ §11-#8
+ * (首批 4 类核心服务 planner_consult/assessor_dan/菌群检测/工作坊)。</p>
+ *
+ * <p><b>契约方法(按 javadoc 签名落地,勿改名——proxy 字符串契约)</b>:</p>
+ * <ul>
+ *   <li>{@code int getCostCent(String serviceType)} — 读取成本价(分),未配置返回 0</li>
+ *   <li>{@code int saveCostCent(String serviceType, String serviceName, int costCent)} — upsert(按 service_type 唯一键)</li>
+ * </ul>
+ *
+ * <p>反射直连(无 Spring / 无 DB)时 Mapper 为空:getCostCent 返回 0、saveCostCent 直接返回入参(纯契约上下文不落库)。</p>
+ */
+@Service
+public class ServiceCostConfigService {
+
+    @Resource
+    private ServiceCostConfigMapper serviceCostConfigMapper;
+
+    public ServiceCostConfigService() {
+        // no-arg constructor for reflection instantiation
+    }
+
+    /**
+     * 读取服务成本价 C(分,§7.5):未配置返回 0(视为零成本,分润按 S 计)。
+     */
+    public int getCostCent(String serviceType) {
+        if (serviceType == null || serviceType.trim().isEmpty() || serviceCostConfigMapper == null) {
+            return 0;
+        }
+        try {
+            ServiceCostConfig cfg = serviceCostConfigMapper.selectOne(
+                    new LambdaQueryWrapper<ServiceCostConfig>()
+                            .eq(ServiceCostConfig::getServiceType, serviceType.trim())
+                            .last("LIMIT 1"));
+            return (cfg != null && cfg.getCostCent() != null) ? cfg.getCostCent().intValue() : 0;
+        } catch (Exception e) {
+            return 0;
+        }
+    }
+
+    /**
+     * 保存服务成本价 C(分,§7.5):按 service_type upsert;返回保存后的成本价(分)。
+     */
+    public int saveCostCent(String serviceType, String serviceName, int costCent) {
+        if (serviceType == null || serviceType.trim().isEmpty() || serviceCostConfigMapper == null) {
+            return costCent;
+        }
+        String type = serviceType.trim();
+        ServiceCostConfig existing = serviceCostConfigMapper.selectOne(
+                new LambdaQueryWrapper<ServiceCostConfig>()
+                        .eq(ServiceCostConfig::getServiceType, type)
+                        .last("LIMIT 1"));
+        if (existing == null) {
+            ServiceCostConfig cfg = new ServiceCostConfig();
+            cfg.setServiceType(type);
+            cfg.setServiceName(serviceName);
+            cfg.setCostCent((long) costCent);
+            cfg.setEnabled(1);
+            serviceCostConfigMapper.insert(cfg);
+        } else {
+            existing.setServiceName(serviceName != null ? serviceName : existing.getServiceName());
+            existing.setCostCent((long) costCent);
+            serviceCostConfigMapper.updateById(existing);
+        }
+        return costCent;
+    }
+}

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

@@ -0,0 +1,201 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.config.ConfigService;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+
+/**
+ * 团队分润配置服务(能力键 {@code team.admin.manage},单元 admin;T5 管理端配置)。
+ *
+ * <p>契约溯源:{@code docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md} v0.7
+ * §7.8(配置全部入 {@code service_config},{@code group_key='team_profit'})+ §11-#12
+ * (星级档位推荐值 {0, 8千, 2.5万, 7万, 14万, 25万} 放后台配置)+ §4.2(8 份分母 8)+
+ * §4.4 #3 拍板(团队奖归属 platform_rule)+ §4.3(业绩分权重 50/40/10)。</p>
+ *
+ * <p><b>契约方法(按 javadoc 签名落地,勿改名——proxy 字符串契约)</b>:</p>
+ * <ul>
+ *   <li>{@code int[] defaultStarTiers()} — §11-#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 档拒收)</li>
+ *   <li>{@code void saveConfig(String,String)} — 写 service_config(group_key='team_profit')</li>
+ * </ul>
+ *
+ * <p><b>纯契约可用性</b>:{@code ConfigService} 为 Spring 注入;反射直连(无 Spring / 无 DB)
+ * 时为空,所有读取回退规格默认值,保证契约级单测与生产可用双态成立。</p>
+ */
+@Service
+public class TeamProfitConfigService {
+
+    /** §7.8 配置组 */
+    public static final String GROUP_TEAM_PROFIT = "team_profit";
+
+    // ==================== §7.8 配置键(service_config group_key='team_profit') ====================
+
+    /** 8 份分佣池占 T 比例(默认 80%) */
+    public static final String KEY_EIGHT_SHARE_RATIO = "team_profit.eight_share_ratio";
+    /** 团队奖池占 T 比例(默认 20%) */
+    public static final String KEY_TEAM_AWARD_RATIO = "team_profit.team_award_ratio";
+    /** 星级业绩分门槛表(6 档 JSON) */
+    public static final String KEY_STAR_TIERS = "team_profit.star_tiers";
+    /** 各服务团队年度业绩门槛(JSON,§11-#6 默认 0) */
+    public static final String KEY_TEAM_THRESHOLD = "team_profit.team_threshold";
+    /** 8 份制分母(默认 8) */
+    public static final String KEY_SHARE_TOTAL = "team_profit.share_total";
+    /** 团队奖分档(TA0-TA3 门槛与比例 JSON) */
+    public static final String KEY_AWARD_TIERS = "team_profit.award_tiers";
+    /** 团队奖归属(§4.4 #3 拍板:platform_rule 默认) */
+    public static final String KEY_AWARD_DISTRIBUTE = "team_profit.award_distribute";
+    /** 星级业绩分权重 50/40/10 */
+    public static final String KEY_STAR_WEIGHT = "team_profit.star_weight";
+
+    // ==================== §11-#12 默认值(后台可调) ====================
+
+    /** §11-#12 星级档位推荐默认值(0 / 8千 / 2.5万 / 7万 / 14万 / 25万) */
+    private static final int[] DEFAULT_STAR_TIERS = {0, 8000, 25000, 70000, 140000, 250000};
+
+    /** §4.3 星级业绩分权重(星 / 团队 / 推荐) */
+    private static final int[] DEFAULT_STAR_WEIGHT = {50, 40, 10};
+
+    /** L2 配置中心(service_config);反射直连(无 Spring)时为空,读取回退默认 */
+    @Resource
+    private ConfigService configService;
+
+    public TeamProfitConfigService() {
+        // no-arg constructor for reflection instantiation
+    }
+
+    // ==================== §11-#12 星级档位 ====================
+
+    /**
+     * §11-#12 默认星级档位六档(0 / 8千 / 2.5万 / 7万 / 14万 / 25万)。
+     */
+    public int[] defaultStarTiers() {
+        return DEFAULT_STAR_TIERS.clone();
+    }
+
+    /**
+     * 生效中的星级档位:读 {@code service_config.team_profit.star_tiers}(JSON,后台可调),
+     * 缺省/解析失败回退 §11-#12 默认值。
+     */
+    public int[] getStarTiers() {
+        int[] parsed = parseTiers(readConfig(KEY_STAR_TIERS));
+        return parsed != null ? parsed : DEFAULT_STAR_TIERS.clone();
+    }
+
+    /**
+     * 管理端保存星级档位(§11-#12 后台配置):非 6 档(1-6 星一一对应)拒收。
+     */
+    public void saveStarTiers(int[] tiers) {
+        if (tiers == null || tiers.length != 6) {
+            throw new IllegalArgumentException("星级档位必须为 6 档(1-6 星一一对应)");
+        }
+        saveConfig(KEY_STAR_TIERS, joinTiers(tiers));
+    }
+
+    /**
+     * 写 {@code service_config}(group_key='team_profit'):任意 team_profit 配置键的通用保存入口。
+     */
+    public void saveConfig(String key, String value) {
+        if (key == null || key.trim().isEmpty()) {
+            throw new IllegalArgumentException("配置键不能为空");
+        }
+        if (configService != null) {
+            configService.setConfig(key, value, null);
+        }
+        // 反射直连(无 Spring)时静默跳过(纯契约上下文无需落库)
+    }
+
+    // ==================== §7.8 比率 / 份额 / 归属 / 权重 ====================
+
+    /** 8 份分佣池占 T 比例(默认 80,§11-#2 拍板 80/20 拆分) */
+    public int getEightShareRatio() {
+        return readInt(KEY_EIGHT_SHARE_RATIO, 80);
+    }
+
+    /** 团队奖池占 T 比例(默认 20) */
+    public int getTeamAwardRatio() {
+        return readInt(KEY_TEAM_AWARD_RATIO, 20);
+    }
+
+    /** 8 份制分母(默认 8,§4.2) */
+    public int getShareTotal() {
+        return readInt(KEY_SHARE_TOTAL, 8);
+    }
+
+    /** 团队奖归属(§4.4 #3 拍板:platform_rule 默认,平台统一规则) */
+    public String getAwardDistribute() {
+        return readString(KEY_AWARD_DISTRIBUTE, "platform_rule");
+    }
+
+    /** 星级业绩分权重(默认 50/40/10,§4.3 加权公式) */
+    public int[] getStarWeight() {
+        int[] parsed = parseTiers(readConfig(KEY_STAR_WEIGHT));
+        return parsed != null ? parsed : DEFAULT_STAR_WEIGHT.clone();
+    }
+
+    // ==================== 配置读取(反射直连回退默认) ====================
+
+    private String readConfig(String key) {
+        if (configService == null) {
+            return null;
+        }
+        try {
+            return configService.getConfig(key);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private int readInt(String key, int defaultValue) {
+        String raw = readConfig(key);
+        if (raw == null || raw.trim().isEmpty()) {
+            return defaultValue;
+        }
+        try {
+            return Integer.parseInt(raw.trim());
+        } catch (NumberFormatException e) {
+            return defaultValue;
+        }
+    }
+
+    private String readString(String key, String defaultValue) {
+        String raw = readConfig(key);
+        return (raw == null || raw.trim().isEmpty()) ? defaultValue : raw.trim();
+    }
+
+    /** 解析 "0,8000,25000" 或 JSON "[0,8000,25000]" 形式的整数数组;失败返回 null。 */
+    private int[] parseTiers(String raw) {
+        if (raw == null || raw.trim().isEmpty()) {
+            return null;
+        }
+        String s = raw.trim();
+        if (s.startsWith("[") && s.endsWith("]")) {
+            s = s.substring(1, s.length() - 1);
+        }
+        String[] parts = s.split(",");
+        int[] result = new int[parts.length];
+        try {
+            for (int i = 0; i < parts.length; i++) {
+                result[i] = Integer.parseInt(parts[i].trim());
+            }
+            return result;
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    private String joinTiers(int[] tiers) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < tiers.length; i++) {
+            if (i > 0) {
+                sb.append(',');
+            }
+            sb.append(tiers[i]);
+        }
+        return sb.toString();
+    }
+}

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

@@ -0,0 +1,78 @@
+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;T5 流水查询)。
+ *
+ * <p>契约溯源:{@code docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md} v0.7
+ * §7.3({@code team_share_record} 8 份分佣流水)+ §7.4({@code team_award_record} 团队奖流水),
+ * 管理端按接收人/团队/状态分页查询。</p>
+ *
+ * <p><b>契约方法(按 javadoc 签名落地,勿改名——proxy 字符串契约)</b>:</p>
+ * <ul>
+ *   <li>{@code Page<TeamShareRecord> queryTeamShareRecords(Long recipientUserId, String status, int page, int size)} — §7.3</li>
+ *   <li>{@code Page<TeamAwardRecord> queryTeamAwardRecords(Long ownerUserId, String status, int page, int size)} — §7.4</li>
+ * </ul>
+ *
+ * <p>反射直连(无 Spring / 无 DB)时 Mapper 为空,返回空分页(纯契约上下文不落库)。</p>
+ */
+@Service
+public class TeamProfitFlowService {
+
+    @Resource
+    private TeamShareRecordMapper teamShareRecordMapper;
+
+    @Resource
+    private TeamAwardRecordMapper teamAwardRecordMapper;
+
+    public TeamProfitFlowService() {
+        // no-arg constructor for reflection instantiation
+    }
+
+    /**
+     * §7.3 8 份分佣流水查询:按接收人(可选)+ 状态(可选)分页,创建时间倒序。
+     */
+    public Page<TeamShareRecord> queryTeamShareRecords(Long recipientUserId, String status, int page, int size) {
+        Page<TeamShareRecord> pageParam = new Page<>(page, size);
+        if (teamShareRecordMapper == null) {
+            return pageParam;
+        }
+        LambdaQueryWrapper<TeamShareRecord> wrapper = new LambdaQueryWrapper<TeamShareRecord>()
+                .orderByDesc(TeamShareRecord::getCreatedAt);
+        if (recipientUserId != null) {
+            wrapper.eq(TeamShareRecord::getRecipientUserId, recipientUserId);
+        }
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(TeamShareRecord::getStatus, status);
+        }
+        return teamShareRecordMapper.selectPage(pageParam, wrapper);
+    }
+
+    /**
+     * §7.4 团队奖流水查询:按团队负责人(可选)+ 状态(可选)分页,创建时间倒序。
+     */
+    public Page<TeamAwardRecord> queryTeamAwardRecords(Long ownerUserId, String status, int page, int size) {
+        Page<TeamAwardRecord> pageParam = new Page<>(page, size);
+        if (teamAwardRecordMapper == null) {
+            return pageParam;
+        }
+        LambdaQueryWrapper<TeamAwardRecord> wrapper = new LambdaQueryWrapper<TeamAwardRecord>()
+                .orderByDesc(TeamAwardRecord::getCreatedAt);
+        if (ownerUserId != null) {
+            wrapper.eq(TeamAwardRecord::getOwnerUserId, ownerUserId);
+        }
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(TeamAwardRecord::getStatus, status);
+        }
+        return teamAwardRecordMapper.selectPage(pageParam, wrapper);
+    }
+}

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

@@ -124,4 +124,25 @@ public class WithdrawalService {
         SortUtil.applySort(wrapper);
         return withdrawalRequestMapper.selectPage(pageParam, wrapper);
     }
+
+    /**
+     * 管理端提现审核列表(按 status + type 过滤重载,§11-#7)。
+     *
+     * <p>团队货币分润提现复用 {@code withdrawal_requests},type 区分 {@code team_share}(8 份分佣)
+     * / {@code team_award}(团队奖);管理端(T5 {@code team.admin.manage})按来源 type 过滤,
+     * 现有佣金提现继续走 {@link #getAdminList(String, int, int)}。</p>
+     */
+    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);
+    }
 }

+ 31 - 0
docs/superpowers/api/API_REFERENCE.md

@@ -3678,3 +3678,34 @@ W0.2 双配置中心合并:`product_config` 已并入 `service_config`(唯
 | SC-X-18 | 推送跨端送达 | 公众号模板消息 + 微信待办 webhook 已投产;小程序订阅消息(requestSubscribeMessage/subscribeMessage)前后端 0 命中,`WechatNotificationChannel` 预留禁用;cfc-langgraph 无推送 | 缺口 X-G-3,需产品侧确认 |
 > 本批仅登记缺口不实现,转产品/运维侧确认后由后续批次落地。
 *文档更新:2026-11-07(EVD-15 跨角色域剩余 8 SC 契约核对增补「跨角色域剩余」段落 4.65:双角色数据隔离越权 / 文章发布闭环 / 消息通知闭环 / 解绑重分配 / 注销数据导出 / 活动名额并发 / 多端登录互斥 / 推送跨端送达 + 命名回填 2 处 + 契约缺口 3 项。完整核验表见 `docs/superpowers/specs/2026-11-07-cross-role-contract.md`)*
+
+## 4.66 团队分润管理端(`/api/admin/team-profit/*`,能力键 `team.admin.manage`,T5 管理端)
+> 契约溯源:`docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md` v0.7 §12-T5(管理端:成本价 / 星级档位 §11-#12 / 团队门槛 / 团队奖分档配置 + 分润流水查询 + 提现审核,复用现有管理端佣金模式)。
+> 约定:全部 `@PostMapping`;`TeamProfitAdminController`,`/api/admin` 前缀经 `JwtInterceptor` 拦截;控制器内校验 `@RequestAttribute("role")` == `admin`(非管理员返回「无权限」)。
+> 配置载体:`service_config`(`group_key='team_profit'`)+ `service_cost_config`(迁移 462,entity `ServiceCostConfig`)。
+### 4.66.1 星级档位(§11-#12 后台可调)
+| 路径 | 说明 |
+|------|------|
+| `POST /api/admin/team-profit/config/star-tiers` | 读取生效中 6 档星级门槛(`TeamProfitConfigService.getStarTiers()`,缺省回退默认 `{0,8000,25000,70000,140000,250000}`,单位分) |
+| `POST /api/admin/team-profit/config/default-star-tiers` | 读取出厂默认 6 档(`defaultStarTiers()`) |
+| `POST /api/admin/team-profit/config/save-star-tiers` | 保存 6 档(`tiers` 必须为长度 6 的整数数组,非 6 档拒收,`saveStarTiers(int[])`) |
+### 4.66.2 比率 / 份额 / 归属 / 权重(§7.8)
+| 路径 | 说明 |
+|------|------|
+| `POST /api/admin/team-profit/config/ratios` | 读取 8 份分佣池占比(默认 80)/ 团队奖池占比(默认 20)/ 8 份制分母(默认 8)/ 团队奖归属(默认 `platform_rule`)/ 星级业绩分权重(默认 `{50,40,10}`) |
+| `POST /api/admin/team-profit/config/save` | 按 `key` + `value` 写 `service_config`(`group_key='team_profit'`,`saveConfig(String,String)`) |
+### 4.66.3 服务成本价(§7.5 + §11-#8)
+| 路径 | 说明 |
+|------|------|
+| `POST /api/admin/team-profit/cost/get` | 读取服务成本价(分,参数 `serviceType`,`ServiceCostConfigService.getCostCent(String)`,未配置返回 0) |
+| `POST /api/admin/team-profit/cost/save` | 保存服务成本价(参数 `serviceType` / `serviceName` / `costCent` 非负整数分,按 `service_type` upsert,`saveCostCent(String,String,int)`) |
+### 4.66.4 分润流水查询(§7.3 / §7.4)
+| 路径 | 说明 |
+|------|------|
+| `POST /api/admin/team-profit/flow/share-records` | 团队分润流水分页(`team_share_records`,参数 `recipientUserId`/`status`/`page`/`size`,`TeamProfitFlowService.queryTeamShareRecords(Long,String,int,int)`) |
+| `POST /api/admin/team-profit/flow/award-records` | 团队奖流水分页(`team_award_records`,参数 `ownerUserId`/`status`/`page`/`size`,`TeamProfitFlowService.queryTeamAwardRecords(Long,String,int,int)`) |
+### 4.66.5 提现审核(§11-#7,复用 `withdrawal_requests`)
+| 路径 | 说明 |
+|------|------|
+| `POST /api/admin/team-profit/withdrawals` | 提现申请分页(参数 `status`/`type`/`page`/`size`,`WithdrawalService.getAdminList(String,String,int,int)` 4 参重载) |
+*文档更新:2026-11-08(S-cfc-68 团队分润管理端实现落绿,API_REFERENCE 增补段 4.66:星级档位 / 比率份额归属权重 / 服务成本价 / 分润流水查询 / 提现审核)*