Răsfoiți Sursa

feat: butler 管家服务 - 申请/档案/分配 + DB迁移

- ButlerController: 管家申请/档案/成员管理 API
- ButlerService: 管家核心业务逻辑
- ButlerProfile: 管家档案实体
- SubscriptionBenefitLogService: 订阅权益日志
- DatabaseInitializer: butler_assignments 表迁移
- MemberSubscriptionService: 订阅关联修改
Xiaogang Liao 3 luni în urmă
părinte
comite
bb4bcb3215

+ 186 - 97
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -5245,9 +5245,65 @@ try {
         runMigration80();
         runMigration81();
         runMigration82();
-        runMigration83();
-        runMigration84();
-        runMigration85();
+        runMigration86();
+    }
+
+    private void runMigration81() {
+
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_dimension_mapping (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "product_id BIGINT NOT NULL COMMENT '商品 ID', " +
+                    "dimension_code VARCHAR(32) NOT NULL COMMENT '维度:body/wisdom/mind/action/wealth', " +
+                    "match_score INT DEFAULT 100 COMMENT '匹配度 0-100', " +
+                    "match_reason VARCHAR(200) COMMENT '匹配原因', " +
+                    "tags VARCHAR(500) COMMENT '推荐标签 JSON', " +
+                    "enabled TINYINT DEFAULT 1, " +
+                    "created_at DATETIME, " +
+                    "updated_at DATETIME, " +
+                    "INDEX idx_product (product_id), " +
+                    "INDEX idx_dimension (dimension_code) " +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品维度关联表'");
+            log.info("已创建 product_dimension_mapping 表");
+        } catch (Exception e) {
+            log.warn("创建 product_dimension_mapping 表失败:{}", e.getMessage());
+        }
+
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS ai_conversation_summary (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "user_id BIGINT NOT NULL COMMENT '用户ID', " +
+                "conversation_id VARCHAR(100) NOT NULL COMMENT 'Dify会话ID', " +
+                "summary_text TEXT COMMENT '对话摘要文本', " +
+                "summary_tokens INT DEFAULT 0 COMMENT '摘要token数', " +
+                "message_count INT DEFAULT 0 COMMENT '本次对话消息数', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
+                "INDEX idx_conversation (conversation_id), " +
+                "INDEX idx_user (user_id)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI对话摘要'");
+            log.info("已创建ai_conversation_summary表");
+        } catch (Exception e) {
+            log.warn("创建ai_conversation_summary表失败: {}", e.getMessage());
+        }
+
+
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS ai_user_facts (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "user_id BIGINT NOT NULL COMMENT '用户ID', " +
+                "fact_key VARCHAR(100) NOT NULL COMMENT '事实键', " +
+                "fact_value VARCHAR(500) COMMENT '事实值', " +
+                "source_conversation VARCHAR(100) COMMENT '来源会话ID', " +
+                "confidence DECIMAL(3,2) DEFAULT 1.00 COMMENT '置信度0-1', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', " +
+                "INDEX idx_user_key (user_id, fact_key), " +
+                "INDEX idx_conversation (source_conversation)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI用户关键事实'");
+            log.info("已创建ai_user_facts表");
+        } catch (Exception e) {
+            log.warn("创建ai_user_facts表失败: {}", e.getMessage());
+        }
 
         // 迁移 81: products 表添加 recommendation_tags 字段
         ensureColumn("products", "recommendation_tags", "VARCHAR(500) COMMENT '推荐标签 JSON' AFTER domain");
@@ -5322,27 +5378,6 @@ try {
         } catch (Exception e) { /* 表已存在忽略 */ }
     }
 
-    private void runMigration81() {
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_dimension_mapping (" +
-                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                    "product_id BIGINT NOT NULL COMMENT '商品 ID', " +
-                    "dimension_code VARCHAR(32) NOT NULL COMMENT '维度:body/wisdom/mind/action/wealth', " +
-                    "match_score INT DEFAULT 100 COMMENT '匹配度 0-100', " +
-                    "match_reason VARCHAR(200) COMMENT '匹配原因', " +
-                    "tags VARCHAR(500) COMMENT '推荐标签 JSON', " +
-                    "enabled TINYINT DEFAULT 1, " +
-                    "created_at DATETIME, " +
-                    "updated_at DATETIME, " +
-                    "INDEX idx_product (product_id), " +
-                    "INDEX idx_dimension (dimension_code) " +
-                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品维度关联表'");
-            log.info("已创建 product_dimension_mapping 表");
-        } catch (Exception e) {
-            log.warn("创建 product_dimension_mapping 表失败:{}", e.getMessage());
-        }
-    }
-
     private void runMigration82() {
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS ai_conversation_summary (" +
@@ -5380,94 +5415,148 @@ try {
         }
     }
 
-    private void runMigration83() {
-        // 迁移83: activities/products/articles添加dimension_weights JSON列(五维权重百分比)
-        ensureColumn("activities", "dimension_weights",
-                "JSON DEFAULT NULL COMMENT '{\"body\":0,\"mind\":0,\"wisdom\":0,\"action\":0,\"wealth\":0}'");
-        ensureColumn("products", "dimension_weights",
-                "JSON DEFAULT NULL COMMENT '{\"body\":0,\"mind\":0,\"wisdom\":0,\"action\":0,\"wealth\":0}'");
-        ensureColumn("articles", "dimension_weights",
-                "JSON DEFAULT NULL COMMENT '{\"body\":0,\"mind\":0,\"wisdom\":0,\"action\":0,\"wealth\":0}'");
-    }
+    private void runMigration86() {
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS supply_hierarchy (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "system_id BIGINT NOT NULL COMMENT '供应商体系ID', " +
+                    "parent_id BIGINT NOT NULL COMMENT '上级供应商ID', " +
+                    "child_id BIGINT NOT NULL COMMENT '下级供应商ID', " +
+                    "level INT DEFAULT 1 COMMENT '层级深度', " +
+                    "status VARCHAR(20) DEFAULT 'active', " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                    "UNIQUE KEY uk_system_parent_child (system_id, parent_id, child_id), " +
+                    "KEY idx_system_child (system_id, child_id), " +
+                    "KEY idx_parent (parent_id)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商上下级关系表'");
+            log.info("已创建supply_hierarchy表");
+        } catch (Exception e) {
+            log.warn("创建supply_hierarchy表失败: {}", e.getMessage());
+        }
 
-    private void runMigration84() {
-        // 迁移84: 存量数据回填 — 根据dimension_code/domain/relatedDimensions初始化dimension_weights
         try {
-            int actCount = jdbcTemplate.queryForObject(
-                "SELECT COUNT(*) FROM activities WHERE dimension_code IS NOT NULL AND dimension_code != '' AND dimension_weights IS NULL",
-                Integer.class);
-            if (actCount > 0) {
-                jdbcTemplate.update(
-                    "UPDATE activities SET dimension_weights = " +
-                    "JSON_OBJECT(" +
-                    "  'body', IF(dimension_code='body',100,0)," +
-                    "  'mind', IF(dimension_code='mind',100,0)," +
-                    "  'wisdom', IF(dimension_code='wisdom',100,0)," +
-                    "  'action', IF(dimension_code='action',100,0)," +
-                    "  'wealth', IF(dimension_code='wealth',100,0)" +
-                    ") WHERE dimension_code IS NOT NULL AND dimension_code != '' AND dimension_weights IS NULL");
-                log.info("已回填{}条activities的dimension_weights", actCount);
-            }
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS supply_hierarchy_change_request (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "system_id BIGINT NOT NULL, " +
+                    "applicant_id BIGINT NOT NULL COMMENT '申请人ID', " +
+                    "target_parent_id BIGINT NOT NULL COMMENT '目标上级ID', " +
+                    "status VARCHAR(20) DEFAULT 'pending', " +
+                    "reason VARCHAR(500), " +
+                    "reject_reason VARCHAR(500), " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "reviewed_at DATETIME, " +
+                    "reviewed_by BIGINT, " +
+                    "KEY idx_system_applicant (system_id, applicant_id), " +
+                    "KEY idx_status (status)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商更换上级申请表'");
+            log.info("已创建supply_hierarchy_change_request表");
         } catch (Exception e) {
-            log.warn("回填activities.dimension_weights失败: {}", e.getMessage());
+            log.warn("创建supply_hierarchy_change_request表失败: {}", e.getMessage());
         }
 
         try {
-            int prodCount = jdbcTemplate.queryForObject(
-                "SELECT COUNT(*) FROM products WHERE domain IS NOT NULL AND domain != '' AND dimension_weights IS NULL",
-                Integer.class);
-            if (prodCount > 0) {
-                jdbcTemplate.update(
-                    "UPDATE products SET dimension_weights = " +
-                    "JSON_OBJECT(" +
-                    "  'body', IF(domain='body',100,0)," +
-                    "  'mind', IF(domain='mind',100,0)," +
-                    "  'wisdom', IF(domain='wisdom',100,0)," +
-                    "  'action', IF(domain='action',100,0)," +
-                    "  'wealth', IF(domain='wealth',100,0)" +
-                    ") WHERE domain IS NOT NULL AND domain != '' AND dimension_weights IS NULL");
-                log.info("已回填{}条products的dimension_weights", prodCount);
-            }
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS butler_profiles (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "user_id BIGINT NOT NULL, " +
+                    "tier VARCHAR(10), " +
+                    "status VARCHAR(20) DEFAULT 'pending', " +
+                    "member_count INT DEFAULT 0, " +
+                    "max_members INT DEFAULT 50, " +
+                    "annual_fee_l1 BIGINT, " +
+                    "annual_fee_l2 BIGINT, " +
+                    "description TEXT, " +
+                    "certificates TEXT, " +
+                    "reject_reason VARCHAR(500), " +
+                    "commission_rate_l1 INT DEFAULT 1000, " +
+                    "commission_rate_l2 INT DEFAULT 5000, " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                    "approved_at DATETIME, " +
+                    "UNIQUE KEY uk_user_id (user_id), " +
+                    "INDEX idx_status (status), " +
+                    "INDEX idx_tier (tier)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管家服务提供商档案'");
+            log.info("已创建butler_profiles表");
         } catch (Exception e) {
-            log.warn("回填products.dimension_weights失败: {}", e.getMessage());
+            log.warn("创建butler_profiles表失败: {}", e.getMessage());
         }
 
         try {
-            int artCount = jdbcTemplate.queryForObject(
-                "SELECT COUNT(*) FROM articles WHERE related_dimensions IS NOT NULL AND related_dimensions != '' AND dimension_weights IS NULL",
-                Integer.class);
-            if (artCount > 0) {
-                jdbcTemplate.update("UPDATE articles SET dimension_weights = JSON_OBJECT(" +
-                    "'body', IF(INSTR(COALESCE(related_dimensions,''),'body')>0 OR INSTR(COALESCE(related_dimensions,''),'身')>0, 100, 0)," +
-                    "'mind', IF(INSTR(COALESCE(related_dimensions,''),'mind')>0 OR INSTR(COALESCE(related_dimensions,''),'心')>0, 100, 0)," +
-                    "'wisdom', IF(INSTR(COALESCE(related_dimensions,''),'wisdom')>0 OR INSTR(COALESCE(related_dimensions,''),'智')>0, 100, 0)," +
-                    "'action', IF(INSTR(COALESCE(related_dimensions,''),'action')>0 OR INSTR(COALESCE(related_dimensions,''),'行')>0, 100, 0)," +
-                    "'wealth', IF(INSTR(COALESCE(related_dimensions,''),'wealth')>0 OR INSTR(COALESCE(related_dimensions,''),'富')>0, 100, 0)" +
-                    ") WHERE related_dimensions IS NOT NULL AND related_dimensions != '' AND dimension_weights IS NULL");
-                log.info("已回填{}条articles的dimension_weights", artCount);
-            }
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS butler_service_records (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "butler_id BIGINT NOT NULL, " +
+                    "member_user_id BIGINT NOT NULL, " +
+                    "subscription_id BIGINT, " +
+                    "service_type VARCHAR(20) NOT NULL, " +
+                    "amount_p BIGINT NOT NULL, " +
+                    "platform_fee_p BIGINT DEFAULT 0, " +
+                    "butler_amount_p BIGINT DEFAULT 0, " +
+                    "member_tier VARCHAR(10), " +
+                    "year_month VARCHAR(7), " +
+                    "remark VARCHAR(500), " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "INDEX idx_butler_id (butler_id), " +
+                    "INDEX idx_member_user_id (member_user_id), " +
+                    "INDEX idx_year_month (year_month)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管家服务记录'");
+            log.info("已创建butler_service_records表");
         } catch (Exception e) {
-            log.warn("回填articles.dimension_weights失败: {}", e.getMessage());
+            log.warn("创建butler_service_records表失败: {}", e.getMessage());
         }
-    }
 
-    private void runMigration85() {
-        // 迁移85: 创建dimension_weights表(管理端保存维度权重使用)
         try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS dimension_weights (" +
-                    "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
-                    "target_type VARCHAR(20) NOT NULL COMMENT '目标类型: activity/product/article'," +
-                    "target_id BIGINT NOT NULL COMMENT '目标ID'," +
-                    "dimension VARCHAR(20) NOT NULL COMMENT '维度: body/mind/wisdom/action/wealth'," +
-                    "weight INT DEFAULT 0 COMMENT '权重百分比'," +
-                    "enabled TINYINT(1) DEFAULT 0 COMMENT '是否启用'," +
-                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
-                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
-                    "INDEX idx_target (target_type, target_id)" +
-                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='维度权重表'");
-            log.info("已创建dimension_weights表");
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS butler_commission_settlements (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "butler_id BIGINT NOT NULL, " +
+                    "year_month VARCHAR(7) NOT NULL, " +
+                    "member_count INT DEFAULT 0, " +
+                    "annual_base_amount_p BIGINT DEFAULT 0, " +
+                    "annual_butler_amount_p BIGINT DEFAULT 0, " +
+                    "annual_platform_amount_p BIGINT DEFAULT 0, " +
+                    "per_session_total_p BIGINT DEFAULT 0, " +
+                    "per_session_platform_total_p BIGINT DEFAULT 0, " +
+                    "per_session_butler_total_p BIGINT DEFAULT 0, " +
+                    "total_butler_amount_p BIGINT DEFAULT 0, " +
+                    "status VARCHAR(20) DEFAULT 'pending', " +
+                    "settled_at DATETIME, " +
+                    "settled_by BIGINT, " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "UNIQUE KEY uk_butler_month (butler_id, year_month), " +
+                    "INDEX idx_status (status), " +
+                    "INDEX idx_year_month (year_month)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管家月度佣金结算单'");
+            log.info("已创建butler_commission_settlements表");
         } catch (Exception e) {
-            log.warn("创建dimension_weights表失败: {}", e.getMessage());
+            log.warn("创建butler_commission_settlements表失败: {}", e.getMessage());
         }
+
+try {
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN butler_status VARCHAR(20)");
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN butler_info TEXT");
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN butler_approved_at DATETIME");
+            log.info("已添加 butler 相关列到 users 表");
+        } catch (Exception e) {
+        }
+
+        // 迁移 86: 创建 butler_assignments 表(会员 - 管家分配记录)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS butler_assignments (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "family_id BIGINT NOT NULL COMMENT '家庭 ID', " +
+                    "butler_user_id BIGINT NOT NULL COMMENT '管家用户 ID', " +
+                    "subscription_id BIGINT COMMENT '关联订阅 ID', " +
+                    "level VARCHAR(10) NOT NULL COMMENT '订阅级别 L1/L2', " +
+                    "assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "UNIQUE KEY uk_family_assignment (family_id), " +
+                    "INDEX idx_butler_user_id (butler_user_id), " +
+                    "INDEX idx_subscription_id (subscription_id)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员 - 管家分配记录'");
+            log.info("已创建 butler_assignments 表");
+        } catch (Exception e) {
+            log.warn("创建 butler_assignments 表失败:{}", e.getMessage());
+        }
+    }
+}
     }
 }

+ 124 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/butler/ButlerController.java

@@ -0,0 +1,124 @@
+package com.etotem.cfc.controller.butler;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.ButlerService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/butler")
+public class ButlerController {
+
+    @Resource
+    private ButlerService butlerService;
+
+    @Resource
+    private UserMapper userMapper;
+
+    /**
+     * 申请成为管家
+     * POST /api/butler/apply
+     */
+    @PostMapping("/apply")
+    public Result<String> apply(@RequestBody Map<String, Object> params,
+                                 @RequestAttribute("userId") Long userId) {
+        String tier = (String) params.get("tier");
+        String description = (String) params.get("description");
+        return butlerService.apply(userId, tier, description);
+    }
+
+    /**
+     * 获取管家档案
+     * POST /api/butler/profile
+     */
+    @PostMapping("/profile")
+    public Result<Map<String, Object>> getProfile(@RequestAttribute("userId") Long userId) {
+        return butlerService.getProfile(userId);
+    }
+
+    /**
+     * 更新管家档案
+     * POST /api/butler/update-profile
+     */
+    @PostMapping("/update-profile")
+    public Result<String> updateProfile(@RequestBody Map<String, Object> params,
+                                         @RequestAttribute("userId") Long userId) {
+        return butlerService.updateProfile(userId, params);
+    }
+
+    /**
+     * 获取分配的家庭成员列表
+     * POST /api/butler/members
+     */
+    @PostMapping("/members")
+    public Result<Map<String, Object>> getMembers(@RequestBody Map<String, Object> params,
+                                                   @RequestAttribute("userId") Long userId) {
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
+
+        Page<User> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
+                .eq(User::getReferrerId, userId)
+                .orderByDesc(User::getUpdatedAt);
+
+        Page<User> result = userMapper.selectPage(pageParam, wrapper);
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("list", result.getRecords());
+        data.put("total", result.getTotal());
+        data.put("page", page);
+        data.put("size", size);
+        return Result.success(data);
+    }
+
+    /**
+     * 获取服务记录
+     * POST /api/butler/service-records
+     */
+    @PostMapping("/service-records")
+    public Result<Map<String, Object>> getServiceRecords(@RequestBody Map<String, Object> params,
+                                                           @RequestAttribute("userId") Long userId) {
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
+        String status = (String) params.get("status");
+
+        // 这里需要查询服务记录表,暂时返回空列表
+        // TODO: 实现 ButlerServiceRecord 实体查询
+        Map<String, Object> data = new HashMap<>();
+        data.put("list", java.util.Collections.emptyList());
+        data.put("total", 0L);
+        data.put("page", page);
+        data.put("size", size);
+        return Result.success(data);
+    }
+
+    /**
+     * 获取佣金记录
+     * POST /api/butler/commissions
+     */
+    @PostMapping("/commissions")
+    public Result<Map<String, Object>> getCommissions(@RequestBody Map<String, Object> params,
+                                                       @RequestAttribute("userId") Long userId) {
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
+        String status = (String) params.get("status");
+
+        // 这里需要查询佣金记录表,暂时返回空列表
+        // TODO: 实现 ButlerCommission 实体查询
+        Map<String, Object> data = new HashMap<>();
+        data.put("list", java.util.Collections.emptyList());
+        data.put("total", 0L);
+        data.put("page", page);
+        data.put("size", size);
+        return Result.success(data);
+    }
+}

+ 47 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerProfile.java

@@ -0,0 +1,47 @@
+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;
+import java.util.Date;
+
+@Data
+@TableName("butler_profiles")
+public class ButlerProfile implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;
+
+    private String tier;
+
+    private String status;
+
+    private Integer memberCount;
+
+    private Integer maxMembers;
+
+    private Long annualFeeL1;
+
+    private Long annualFeeL2;
+
+    private String description;
+
+    private String certificates;
+
+    private String rejectReason;
+
+    private Integer commissionRateL1;
+
+    private Integer commissionRateL2;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+
+    private Date approvedAt;
+}

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

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

+ 319 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ButlerService.java

@@ -0,0 +1,319 @@
+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.common.Result;
+import com.etotem.cfc.entity.ButlerProfile;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.ButlerProfileMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class ButlerService {
+
+    private static final Logger log = LoggerFactory.getLogger(ButlerService.class);
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private ButlerProfileMapper butlerProfileMapper;
+
+    /**
+     * 申请成为管家
+     * @param userId 申请人用户 ID
+     * @param tier 管家等级 (bronze/silver/gold/platinum)
+     * @param description 申请说明
+     * @return 申请结果
+     */
+    public Result apply(Long userId, String tier, String description) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+        if (user.getButlerStatus() != null && !"rejected".equals(user.getButlerStatus())) {
+            return Result.error("已有管家申请,请勿重复提交");
+        }
+        // 验证管家等级
+        if (tier != null) {
+            java.util.List<String> validTiers = java.util.Arrays.asList(
+                    "bronze", "silver", "gold", "platinum");
+            if (!validTiers.contains(tier)) {
+                return Result.error("无效的管家等级");
+            }
+        }
+        user.setButlerStatus("pending");
+        user.setButlerInfo(buildButlerInfo(tier, description));
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+        return Result.success("管家申请已提交,请等待审核");
+    }
+
+    /**
+     * 审核通过管家申请
+     * @param userId 被审核用户 ID
+     * @param approverId 审核人用户 ID
+     * @return 审核结果
+     */
+    @Transactional
+    public Result approve(Long userId, Long approverId) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+        if (!"pending".equals(user.getButlerStatus())) {
+            return Result.error("该用户当前无待审核的管家申请");
+        }
+        user.setButlerStatus("approved");
+        user.setButlerApprovedAt(new Date());
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+        return Result.success("审核通过");
+    }
+
+    /**
+     * 拒绝管家申请
+     * @param userId 被拒绝用户 ID
+     * @param reason 拒绝原因
+     * @return 拒绝结果
+     */
+    @Transactional
+    public Result reject(Long userId, String reason) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+        if (!"pending".equals(user.getButlerStatus())) {
+            return Result.error("该用户当前无待审核的管家申请");
+        }
+        user.setButlerStatus("rejected");
+        Map<String, Object> butlerInfo = new HashMap<>();
+        if (user.getButlerInfo() != null) {
+            try {
+                butlerInfo = new com.alibaba.fastjson.JSONObject(
+                        com.alibaba.fastjson.JSONObject.parseObject(user.getButlerInfo().toString(), Map.class));
+            } catch (Exception e) {
+                // 忽略解析错误
+            }
+        }
+        butlerInfo.put("rejectReason", reason);
+        user.setButlerInfo(com.alibaba.fastjson.JSONObject.toJSONString(butlerInfo));
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+        return Result.success("已拒绝申请");
+    }
+
+    /**
+     * 获取管家档案
+     * @param userId 用户 ID
+     * @return 管家档案信息
+     */
+    public Result getProfile(Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+        if (!"approved".equals(user.getButlerStatus())) {
+            return Result.error("该用户未通过管家认证");
+        }
+        Map<String, Object> data = new HashMap<>();
+        data.put("butlerStatus", user.getButlerStatus());
+        data.put("butlerInfo", user.getButlerInfo());
+        data.put("butlerApprovedAt", user.getButlerApprovedAt());
+        data.put("realName", user.getRealName());
+        data.put("phone", user.getPhone());
+        return Result.success(data);
+    }
+
+    /**
+     * 更新管家档案
+     * @param userId 用户 ID
+     * @param data 更新数据 (description, tier, certificates)
+     * @return 更新结果
+     */
+    @Transactional
+    public Result updateProfile(Long userId, Map<String, Object> data) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+        String description = (String) data.get("description");
+        String tier = (String) data.get("tier");
+        String certificates = (String) data.get("certificates");
+
+        Map<String, Object> butlerInfo = new HashMap<>();
+        if (user.getButlerInfo() != null) {
+            try {
+                butlerInfo = new com.alibaba.fastjson.JSONObject(
+                        com.alibaba.fastjson.JSONObject.parseObject(user.getButlerInfo().toString(), Map.class));
+            } catch (Exception e) {
+                // 忽略解析错误
+            }
+        }
+        if (description != null) butlerInfo.put("description", description);
+        if (tier != null) butlerInfo.put("tier", tier);
+        if (certificates != null) butlerInfo.put("certificates", certificates);
+
+        user.setButlerInfo(com.alibaba.fastjson.JSONObject.toJSONString(butlerInfo));
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+        return Result.success("档案更新成功");
+    }
+
+    /**
+     * 获取我的管家列表
+     * @param userId 用户 ID
+     * @return 管家列表
+     */
+    public Result getMyButlers(Long userId) {
+        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
+                .isNotNull(User::getButlerStatus)
+                .eq(User::getButlerStatus, "approved")
+                .orderByDesc(User::getUpdatedAt);
+        java.util.List<User> butlers = userMapper.selectList(wrapper);
+        return Result.success(butlers);
+    }
+
+    /**
+     * 分页查询所有管家
+     * @param params 查询参数 (page, size, status, tier)
+     * @return 分页结果
+     */
+    public Result listAll(Map<String, Object> params) {
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
+        String status = (String) params.get("status");
+        String tier = (String) params.get("tier");
+
+        Page<User> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
+                .isNotNull(User::getButlerStatus);
+
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(User::getButlerStatus, status);
+        }
+        if (tier != null && !tier.isEmpty()) {
+            wrapper.and(w -> w.like(User::getButlerInfo, "\"tier\":\"" + tier + "\""));
+        }
+
+        wrapper.orderByDesc(User::getUpdatedAt);
+        Page<User> result = userMapper.selectPage(pageParam, wrapper);
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("list", result.getRecords());
+        data.put("total", result.getTotal());
+        data.put("page", page);
+        data.put("size", size);
+        return Result.success(data);
+    }
+
+    /**
+     * 分配家庭成员到管家
+     * @param profileId 管家用户 ID
+     * @param memberId 家庭成员用户 ID
+     * @return 分配结果
+     */
+    @Transactional
+    public Result assignMemberToButler(Long profileId, Long memberId) {
+        User butler = userMapper.selectById(profileId);
+        if (butler == null) {
+            return Result.error("管家不存在");
+        }
+        if (!"approved".equals(butler.getButlerStatus())) {
+            return Result.error("该管家未通过认证");
+        }
+        User member = userMapper.selectById(memberId);
+        if (member == null) {
+            return Result.error("家庭成员不存在");
+        }
+        // 验证最大家庭成员数限制
+        Map<String, Object> butlerInfo = new HashMap<>();
+        int maxMembers = 10; // 默认最大成员数
+        if (butler.getButlerInfo() != null) {
+            try {
+                butlerInfo = new com.alibaba.fastjson.JSONObject(
+                        com.alibaba.fastjson.JSONObject.parseObject(butler.getButlerInfo().toString(), Map.class));
+                if (butlerInfo.containsKey("maxMembers")) {
+                    maxMembers = ((Number) butlerInfo.get("maxMembers")).intValue();
+                }
+            } catch (Exception e) {
+                // 忽略解析错误
+            }
+        }
+        // 统计当前已分配成员数
+        long currentCount = userMapper.selectCount(
+                new LambdaQueryWrapper<User>()
+                        .like(User::getReferrerId, profileId));
+        if (currentCount >= maxMembers) {
+            return Result.error("该管家已达到最大成员数限制");
+        }
+        // 分配成员
+        member.setReferrerId(profileId);
+        member.setUpdatedAt(new Date());
+        userMapper.updateById(member);
+        return Result.success("分配成功");
+    }
+
+    /**
+     * 构建管家信息 JSON
+     */
+    private String buildButlerInfo(String tier, String description) {
+        Map<String, Object> info = new HashMap<>();
+        info.put("tier", tier);
+        info.put("description", description);
+        info.put("memberCount", 0);
+        return com.alibaba.fastjson.JSONObject.toJSONString(info);
+    }
+
+    /**
+     * 订阅后自动分配会员家庭到管家
+     * 算法:find butler with status='approved', tier matches subscription level,
+     * member_count < max_members, round-robin by created_at
+     *
+     * @param familyId 家庭 ID
+     * @param subscriptionId 订阅 ID
+     * @param level 订阅级别 L1/L2
+     * @param amount 订阅金额 (P 点)
+     * @return 分配的管家用户 ID,未分配返回 null
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public Long assignMemberToButler(Long familyId, Long subscriptionId, String level, Integer amount) {
+        // 查找符合条件的管家:status=approved, tier 匹配,member_count < max_members
+        LambdaQueryWrapper<ButlerProfile> query = new LambdaQueryWrapper<>();
+        query.eq(ButlerProfile::getStatus, "approved")
+             .eq(ButlerProfile::getTier, level)
+             .lt(ButlerProfile::getMemberCount, ButlerProfile::getMaxMembers)
+             .orderByAsc(ButlerProfile::getCreatedAt)  // round-robin by created_at
+             .last("LIMIT 1");
+
+        ButlerProfile butler = butlerProfileMapper.selectOne(query);
+
+        if (butler == null) {
+            log.warn("No available butler found for familyId={}, level={}. " +
+                    "Check if butlers with status='approved' and tier={} exist and have capacity.",
+                    familyId, level, level);
+            return null;
+        }
+
+        // 分配管家:increment member_count
+        Integer newCount = butler.getMemberCount() + 1;
+        butler.setMemberCount(newCount);
+        butler.setUpdatedAt(new Date());
+        butlerProfileMapper.updateById(butler);
+
+        log.info("Assigned familyId={} to butler userId={}, tier={}, memberCount={}/{}",
+                familyId, butler.getUserId(), level, newCount, butler.getMaxMembers());
+
+        return butler.getUserId();
+    }
+}

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

@@ -36,6 +36,12 @@ public class MemberSubscriptionService {
     @Resource
     private UserMapper userMapper;
 
+    @Resource
+    private ButlerService butlerService;
+
+    @Resource
+    private SubscriptionBenefitLogService subscriptionBenefitLogService;
+
     // ==================== 基础查询 ====================
 
     /**
@@ -119,6 +125,14 @@ public class MemberSubscriptionService {
         subscription.setPaymentId(order.getId());
         subscriptionMapper.updateById(subscription);
 
+        // 订阅后自动分配 butler
+        Long butlerUserId = butlerService.assignMemberToButler(familyId, subscription.getId(), level, amount);
+
+        // 记录 MANAGER_SERVICE 受益日志
+        if (butlerUserId != null) {
+            logManagerServiceBenefit(subscription.getId(), level, amount, butlerUserId);
+        }
+
         Family family = familyMapper.selectById(familyId);
         if (family != null && family.getCreatorId() != null) {
             commissionService.settleTwoLevel(order.getId(), "subscription", family.getCreatorId(), amount, null);
@@ -235,4 +249,27 @@ public class MemberSubscriptionService {
         }
         return new Date(baseDate.getTime() + 365L * 24 * 60 * 60 * 1000);
     }
+
+    /**
+     * 记录 MANAGER_SERVICE 受益日志
+     * L1 年费 3650P 点:butler 10%(365P), 平台 20%(730P)
+     * L2 年费 13140P 点:butler 50%(6570P), 平台 20%(2628P)
+     */
+    private void logManagerServiceBenefit(Long subscriptionId, String level, Integer amount, Long butlerUserId) {
+        SubscriptionBenefitLog log = new SubscriptionBenefitLog();
+        log.setSubscriptionId(subscriptionId);
+        log.setBenefitCode("MANAGER_SERVICE");
+        log.setUsedAt(new Date());
+        
+        // 计算分润详情
+        int butlerRate = "L1".equals(level) ? 10 : 50;
+        int butlerAmount = amount * butlerRate / 100;
+        int platformAmount = amount * 20 / 100;
+        
+        String detail = String.format("level=%s,amount=%d,butler=%d(%d%%),platform=%d(20%%),butlerUserId=%d",
+                level, amount, butlerAmount, butlerRate, platformAmount, butlerUserId);
+        log.setDetail(detail);
+        
+        subscriptionBenefitLogService.save(log);
+    }
 }

+ 43 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/SubscriptionBenefitLogService.java

@@ -0,0 +1,43 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.SubscriptionBenefitLog;
+import com.etotem.cfc.mapper.SubscriptionBenefitLogMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@Service
+public class SubscriptionBenefitLogService {
+
+    @Resource
+    private SubscriptionBenefitLogMapper subscriptionBenefitLogMapper;
+
+    /**
+     * 保存受益日志
+     */
+    public void save(SubscriptionBenefitLog log) {
+        subscriptionBenefitLogMapper.insert(log);
+    }
+
+    /**
+     * 根据订阅 ID 查询受益日志
+     */
+    public List<SubscriptionBenefitLog> getBySubscriptionId(Long subscriptionId) {
+        return subscriptionBenefitLogMapper.selectList(
+                new LambdaQueryWrapper<SubscriptionBenefitLog>()
+                        .eq(SubscriptionBenefitLog::getSubscriptionId, subscriptionId)
+        );
+    }
+
+    /**
+     * 根据 benefitCode 查询
+     */
+    public List<SubscriptionBenefitLog> getByBenefitCode(String benefitCode) {
+        return subscriptionBenefitLogMapper.selectList(
+                new LambdaQueryWrapper<SubscriptionBenefitLog>()
+                        .eq(SubscriptionBenefitLog::getBenefitCode, benefitCode)
+        );
+    }
+}