Răsfoiți Sursa

Merge branch 'cfclub' of http://git.iwintrue.com/liaoxg/cfc into cfclub

# Conflicts:
#	cfc-frontend/pages/body-detail/index.vue
Sisyphus Agent 3 zile în urmă
părinte
comite
26995b9de3
100 a modificat fișierele cu 2645 adăugiri și 715 ștergeri
  1. 27 244
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  2. 22 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java
  3. 18 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductController.java
  4. 76 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/SupplyTeamController.java
  5. 3 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/cart/CartController.java
  6. 16 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/CartItemDTO.java
  7. 12 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/CartListResultDTO.java
  8. 22 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/SupplyCustomerRelation.java
  9. 27 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/SupplyCustomerRelationMapper.java
  10. 97 2
      cfc-backend/src/main/java/com/etotem/cfc/service/CartService.java
  11. 42 31
      cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java
  12. 71 2
      cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java
  13. 38 3
      cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java
  14. 26 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java
  15. 181 0
      cfc-backend/src/main/java/com/etotem/cfc/service/SupplyCustomerRelationService.java
  16. 16 0
      cfc-backend/src/main/resources/schema.sql
  17. 155 0
      cfc-backend/src/test/java/com/etotem/cfc/service/HealthReportServiceTest.java
  18. 128 2
      cfc-frontend/components/ContentSharePoster.vue
  19. 12 0
      cfc-frontend/pages.json
  20. 5 5
      cfc-frontend/pages/action-detail/index.vue
  21. 3 3
      cfc-frontend/pages/action-detail/member-action-detail.vue
  22. 1 1
      cfc-frontend/pages/activity/activity-detail/activity-detail.vue
  23. 2 2
      cfc-frontend/pages/activity/index.vue
  24. 17 17
      cfc-frontend/pages/ai/chat.vue
  25. 1 1
      cfc-frontend/pages/article-center/article-detail.vue
  26. 1 1
      cfc-frontend/pages/article-center/article-edit.vue
  27. 5 5
      cfc-frontend/pages/assessment/apply.vue
  28. 2 2
      cfc-frontend/pages/assessment/purchase.vue
  29. 1 1
      cfc-frontend/pages/assessment/results.vue
  30. 2 2
      cfc-frontend/pages/assessment/upload-report.vue
  31. 1 1
      cfc-frontend/pages/body-detail/checkin.vue
  32. 28 6
      cfc-frontend/pages/body-detail/index.vue
  33. 1 1
      cfc-frontend/pages/body-detail/member-body-detail.vue
  34. 1 1
      cfc-frontend/pages/child/tasks.vue
  35. 1 1
      cfc-frontend/pages/cognitive-test/language-test.vue
  36. 1 1
      cfc-frontend/pages/cognitive-test/perception-test.vue
  37. 1 1
      cfc-frontend/pages/cognitive-test/speed-test.vue
  38. 1 1
      cfc-frontend/pages/games/1a2b.vue
  39. 1 1
      cfc-frontend/pages/games/schulte.vue
  40. 1 1
      cfc-frontend/pages/games/sudoku.vue
  41. 4 4
      cfc-frontend/pages/growth/create.vue
  42. 1 1
      cfc-frontend/pages/growth/diet-checkin.vue
  43. 1 1
      cfc-frontend/pages/growth/exercise-checkin.vue
  44. 1 1
      cfc-frontend/pages/growth/sleep-checkin.vue
  45. 2 2
      cfc-frontend/pages/growth/upload.vue
  46. 3 3
      cfc-frontend/pages/health-main/index.vue
  47. 4 4
      cfc-frontend/pages/health/challenge-manage.vue
  48. 1 1
      cfc-frontend/pages/health/daily-checkin.vue
  49. 1 1
      cfc-frontend/pages/health/diet-index.vue
  50. 1 1
      cfc-frontend/pages/health/exercise-index.vue
  51. 1 1
      cfc-frontend/pages/health/gut-index.vue
  52. 1 4
      cfc-frontend/pages/health/health-plan-summary.vue
  53. 1 1
      cfc-frontend/pages/health/meditation-index.vue
  54. 6 22
      cfc-frontend/pages/health/report-list.vue
  55. 11 11
      cfc-frontend/pages/health/report-survey.vue
  56. 2 2
      cfc-frontend/pages/health/sleep-index.vue
  57. 1 1
      cfc-frontend/pages/health/water-index.vue
  58. 1 1
      cfc-frontend/pages/home-pages/components/MicroActionCard.vue
  59. 1 1
      cfc-frontend/pages/home-pages/member-home-detail.vue
  60. 0 2
      cfc-frontend/pages/index-home/index.vue
  61. 1 1
      cfc-frontend/pages/member-detail/member-detail.vue
  62. 1 1
      cfc-frontend/pages/membership/index.vue
  63. 1 1
      cfc-frontend/pages/membership/upgrade.vue
  64. 2 2
      cfc-frontend/pages/mind-detail/index.vue
  65. 1 1
      cfc-frontend/pages/mind-detail/member-mind-detail.vue
  66. 4 4
      cfc-frontend/pages/parent/child-detail.vue
  67. 1 1
      cfc-frontend/pages/parent/plans/detail.vue
  68. 1 1
      cfc-frontend/pages/parent/plans/index.vue
  69. 2 2
      cfc-frontend/pages/parent/wishes/index.vue
  70. 1 1
      cfc-frontend/pages/profile-extra/children.vue
  71. 2 2
      cfc-frontend/pages/profile-extra/create-child.vue
  72. 1 1
      cfc-frontend/pages/profile-extra/edit-child.vue
  73. 166 235
      cfc-frontend/pages/profile-main/profile.vue
  74. 1 1
      cfc-frontend/pages/profile/components/ProfileHeader.vue
  75. 3 3
      cfc-frontend/pages/profile/portrait-edit.vue
  76. 1 0
      cfc-frontend/pages/promotion/index.vue
  77. 1 1
      cfc-frontend/pages/promotion/team.vue
  78. 1 1
      cfc-frontend/pages/rewards/badge.vue
  79. 4 4
      cfc-frontend/pages/rewards/rewards.vue
  80. 92 19
      cfc-frontend/pages/shop/cart/cart.vue
  81. 58 3
      cfc-frontend/pages/shop/checkout/checkout.vue
  82. 3 3
      cfc-frontend/pages/stats/completion-rate.vue
  83. 4 4
      cfc-frontend/pages/tasks/create-task.vue
  84. 2 2
      cfc-frontend/pages/tasks/review.vue
  85. 1 1
      cfc-frontend/pages/teacher/families.vue
  86. 1 1
      cfc-frontend/pages/tianpan/member-detail.vue
  87. 1 1
      cfc-frontend/pages/tianpan/relation-detail.vue
  88. 1 1
      cfc-frontend/pages/tianpan/traditional.vue
  89. 2 2
      cfc-frontend/pages/user-edit/user-edit.vue
  90. 423 0
      cfc-frontend/pages/vendor/team-customers/team-customers.vue
  91. 616 0
      cfc-frontend/pages/vendor/team-system/team-system.vue
  92. 1 1
      cfc-frontend/pages/wealth-detail/member-wealth-detail.vue
  93. 1 1
      cfc-frontend/pages/wealth-sub/insurance-planning.vue
  94. 1 1
      cfc-frontend/pages/wealth/index.vue
  95. 1 1
      cfc-frontend/pages/wisdom-detail/member-wisdom-detail.vue
  96. 12 0
      cfc-frontend/utils/api.js
  97. 1 1
      cfc-web/.last_build_commit
  98. 2 2
      cfc-web/package-lock.json
  99. 1 1
      cfc-web/package.json
  100. 116 0
      cfc-web/public/CHANGELOG-v1.0.md

+ 27 - 244
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -1055,8 +1055,10 @@ public class DatabaseInitializer implements CommandLineRunner {
             insertSysConfigSeed("trial_days", "7", "试用天数");
             insertSysConfigSeed("trial_features", "[\"free_activities\",\"free_courses\",\"teacher_consultation\"]", "试用期间可用功能");
             insertSysConfigSeed("member_pay_virtual_enabled", "1", "会员支付是否使用虚拟支付通道(1=虚拟,0=普通微信)");
-            insertSysConfigSeed("gift_membership_threshold", "198000", "购买送会员商品累计达到该金额(分)赠送会员");
-            insertSysConfigSeed("gift_membership_level", "FAMILY", "达到送会员额度后赠送的会员等级");
+insertSysConfigSeed("gift_membership_threshold", "198000", "购买送会员商品累计达到该金额(分)赠送会员");
+insertSysConfigSeed("gift_membership_level", "FAMILY", "达到送会员额度后赠送的会员等级");
+insertSysConfigSeed("gift_membership_original_threshold", "1000000", "送会员商品原价达标阈值(分),达到此金额后送会员商品部分实付一口价");
+insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达标后一口实付价(分)");
             insertSysConfigSeed("health_dimension_weights", "{\"growth\":0.20,\"sleep\":0.15,\"vision\":0.10,\"immunity\":0.15,\"nutrition\":0.15,\"gut\":0.10,\"exercise\":0.15}", "7维健康→身能量权重配置");
             insertSysConfigSeed("health_dimension_sync_factor", "0.5", "7维健康同步至身能量系数");
             log.info("SysConfig 种子数据已加载");
@@ -2213,248 +2215,6 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("创建 cart 表失败: {}", e.getMessage());
         }
 
-        // ==================== DanShop 电商模块建表 =============        // 1. danshop_categories (商品分类表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_categories (" +
-                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                    "parent_id BIGINT DEFAULT NULL COMMENT '父分类ID(null=一级)', " +
-                    "name VARCHAR(64) NOT NULL COMMENT '分类名称', " +
-                    "image VARCHAR(512) DEFAULT NULL COMMENT '分类图标URL', " +
-                    "sort_order INT DEFAULT 0 COMMENT '排序(越小越靠前)', " +
-                    "energy_dimension_id BIGINT DEFAULT NULL COMMENT '关联五维维度ID', " +
-                    "status INT DEFAULT 1 COMMENT '1启用/0禁用', " +
-                    "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                    "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-                    "INDEX idx_parent (parent_id), " +
-                    "INDEX idx_status (status)" +
-                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品分类表'");
-            log.info("已创建 danshop_categories 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_categories 表失败: {}", e.getMessage());
-        }
-
-        // 2. danshop_products (商品表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_products (" +
-                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                    "category_id BIGINT NOT NULL COMMENT '所属分类ID', " +
-                    "name VARCHAR(128) NOT NULL COMMENT '商品名称', " +
-                    "description TEXT COMMENT '商品详细描述', " +
-                    "cover_image VARCHAR(512) COMMENT '封面图URL', " +
-                    "images TEXT COMMENT '多图(JSON数组)', " +
-                    "price INT NOT NULL COMMENT '售价(分)', " +
-                    "member_price INT DEFAULT NULL COMMENT '会员价(分)', " +
-                    "cost_price INT DEFAULT NULL COMMENT '成本价(分)', " +
-                    "stock INT DEFAULT 0 COMMENT '库存数量', " +
-                    "sales_count INT DEFAULT 0 COMMENT '销量', " +
-                    "product_type VARCHAR(32) NOT NULL COMMENT '类型: physical/virtual/coupon/course', " +
-                    "vendor_id BIGINT DEFAULT NULL COMMENT '供应商ID', " +
-                    "vendor_name VARCHAR(64) COMMENT '供应商名称', " +
-                    "status VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/approved/rejected/on_shelf/off_shelf', " +
-                    "reject_reason VARCHAR(255) DEFAULT NULL COMMENT '审核拒绝原因', " +
-                    "profit_rate INT DEFAULT 0 COMMENT '平台利润率(bps,100=1%)', " +
-                    "external_source VARCHAR(32) COMMENT '外部商品来源', " +
-                    "external_id VARCHAR(64) COMMENT '外部商品ID', " +
-                    "external_data TEXT COMMENT '外部商品完整数据(JSON)', " +
-                    "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                    "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-                    "INDEX idx_category (category_id), " +
-                    "INDEX idx_vendor (vendor_id), " +
-                    "INDEX idx_status (status)" +
-                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表'");
-            log.info("已创建 danshop_products 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_products 表失败: {}", e.getMessage());
-        }
-
-        // 3. danshop_orders (主订单表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_orders (" +
-                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                    "order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号(DS+时间戳+6位随机)', " +
-                    "buyer_id BIGINT NOT NULL COMMENT '买家ID', " +
-                    "family_id BIGINT DEFAULT NULL COMMENT '家庭ID', " +
-                    "total_amount INT NOT NULL COMMENT '订单总金额(分)', " +
-                    "discount_amount INT DEFAULT 0 COMMENT '优惠金额(分)', " +
-                    "actual_amount INT NOT NULL COMMENT '实付金额(分)', " +
-                    "status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/paid/shipped/completed/refunding/refunded/cancelled', " +
-                    "payment_method VARCHAR(16) DEFAULT 'wechat' COMMENT 'wechat/alipay', " +
-                    "transaction_id VARCHAR(64) DEFAULT NULL COMMENT '微信/支付宝交易号', " +
-                    "paid_at DATETIME DEFAULT NULL COMMENT '支付时间', " +
-                    "shipped_at DATETIME DEFAULT NULL COMMENT '发货时间', " +
-                    "completed_at DATETIME DEFAULT NULL COMMENT '完成时间', " +
-                    "remark VARCHAR(255) DEFAULT NULL COMMENT '买家备注', " +
-                    "cancel_reason VARCHAR(255) DEFAULT NULL COMMENT '取消原因', " +
-                    "receiver_name VARCHAR(32) DEFAULT NULL COMMENT '收件人姓名', " +
-                    "receiver_phone VARCHAR(16) DEFAULT NULL COMMENT '收件人电话', " +
-                    "receiver_address VARCHAR(256) DEFAULT NULL COMMENT '收件人地址', " +
-                    "freight INT DEFAULT 0 COMMENT '运费(分)', " +
-                    "profit_amount INT DEFAULT 0 COMMENT '返利金额(分)', " +
-                    "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                    "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-                    "INDEX idx_buyer (buyer_id), " +
-                    "INDEX idx_family (family_id), " +
-                    "INDEX idx_status (status), " +
-                    "INDEX idx_order_no (order_no)" +
-                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='主订单表'");
-            log.info("已创建 danshop_orders 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_orders 表失败: {}", e.getMessage());
-        }
-
-        // 4. danshop_order_items (订单明细表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_order_items (" +
-                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                    "order_id BIGINT NOT NULL COMMENT '所属订单ID', " +
-                    "product_id BIGINT NOT NULL COMMENT '商品ID', " +
-                    "product_name VARCHAR(128) NOT NULL COMMENT '商品名称(快照)', " +
-                    "cover_image VARCHAR(512) COMMENT '封面图(快照)', " +
-                    "unit_price INT NOT NULL COMMENT '单价(分,快照)', " +
-                    "price INT NOT NULL COMMENT '单价(分,别名兼容)', " +
-                    "quantity INT NOT NULL DEFAULT 1 COMMENT '购买数量', " +
-                    "subtotal INT NOT NULL COMMENT '小计(分)', " +
-                    "product_type VARCHAR(32) COMMENT '商品类型(快照)', " +
-                    "vendor_id BIGINT COMMENT '供应商ID(快照)', " +
-                    "vendor_name VARCHAR(64) COMMENT '供应商名称(快照)', " +
-                    "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                    "INDEX idx_order (order_id), " +
-                    "INDEX idx_product (product_id)" +
-                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表'");
-            log.info("已创建 danshop_order_items 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_order_items 表失败: {}", e.getMessage());
-        }
-
-        // 5. danshop_order_logs (订单操作日志表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_order_logs (" +
-                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                    "order_id BIGINT NOT NULL COMMENT '订单ID', " +
-                    "action VARCHAR(32) NOT NULL COMMENT '操作: create/pay/ship/confirm/refund/cancel', " +
-                    "operator_id BIGINT DEFAULT NULL COMMENT '操作人ID', " +
-                    "operator_type VARCHAR(16) DEFAULT NULL COMMENT 'buyer/vendor/admin/system', " +
-                    "detail TEXT DEFAULT NULL COMMENT '操作详情(JSON)', " +
-                    "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                    "INDEX idx_order (order_id)" +
-                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单操作日志表'");
-            log.info("已创建 danshop_order_logs 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_order_logs 表失败: {}", e.getMessage());
-        }
-
-        // 6. danshop_logistics (物流信息表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_logistics (" +
-                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                    "order_id BIGINT NOT NULL COMMENT '订单ID', " +
-                    "order_item_id BIGINT DEFAULT NULL COMMENT '订单明细ID', " +
-                    "logistics_no VARCHAR(64) NOT NULL COMMENT '物流单号', " +
-                    "logistics_company VARCHAR(32) NOT NULL COMMENT '物流公司编码: sf/yto/zt/sto/yd/ttkd', " +
-                    "logistics_name VARCHAR(32) DEFAULT NULL COMMENT '物流公司名称', " +
-                    "sender_name VARCHAR(32) DEFAULT NULL COMMENT '发货人姓名', " +
-                    "sender_phone VARCHAR(16) DEFAULT NULL COMMENT '发货人电话', " +
-                    "sender_address VARCHAR(256) DEFAULT NULL COMMENT '发货人地址', " +
-                    "receiver_name VARCHAR(32) NOT NULL COMMENT '收件人姓名', " +
-                    "receiver_phone VARCHAR(16) NOT NULL COMMENT '收件人电话', " +
-                    "receiver_address VARCHAR(256) NOT NULL COMMENT '收件人地址', " +
-                    "status VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/shipped/signing/completed', " +
-                    "tracking_data TEXT DEFAULT NULL COMMENT '物流轨迹(JSON数组)', " +
-                    "shipped_at DATETIME DEFAULT NULL COMMENT '发货时间', " +
-                    "signed_at DATETIME DEFAULT NULL COMMENT '签收时间', " +
-                    "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                    "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-                    "INDEX idx_order (order_id), " +
-                    "INDEX idx_logistics_no (logistics_no), " +
-                    "INDEX idx_status (status)" +
-                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物流信息表'");
-            log.info("已创建 danshop_logistics 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_logistics 表失败: {}", e.getMessage());
-        }
-
-        // 7. danshop_profit_rules (返利规则配置表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_profit_rules (" +
-                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                    "rule_name VARCHAR(64) NOT NULL COMMENT '规则名称', " +
-                    "rule_type VARCHAR(32) NOT NULL COMMENT '规则类型: product_category/vendor_level/global', " +
-                    "target_id BIGINT DEFAULT NULL COMMENT '关联目标ID', " +
-                    "platform_profit_rate INT NOT NULL COMMENT '平台利润率(bps,100=1%)', " +
-                    "referrer_l1_rate INT DEFAULT 0 COMMENT '直接推荐人返利比例(bps,占利润%)', " +
-                    "referrer_l2_rate INT DEFAULT 0 COMMENT '间接推荐人返利比例(bps,占利润%)', " +
-                    "platform_service_rate INT DEFAULT 0 COMMENT '平台服务费率(bps,占订单金额%)', " +
-                    "is_active INT DEFAULT 1 COMMENT '1启用/0禁用', " +
-                    "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                    "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-                    "INDEX idx_type (rule_type, target_id), " +
-                    "INDEX idx_active (is_active)" +
-                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='返利规则配置表'");
-            log.info("已创建 danshop_profit_rules 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_profit_rules 表失败: {}", e.getMessage());
-        }
-
-        // 8. danshop_profit_logs (返利流水表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_profit_logs (" +
-                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                    "order_id BIGINT NOT NULL COMMENT '订单ID', " +
-                    "order_no VARCHAR(32) NOT NULL COMMENT '订单号', " +
-                    "order_amount INT NOT NULL COMMENT '订单实付金额(分)', " +
-                    "cost_amount INT DEFAULT 0 COMMENT '成本金额(分)', " +
-                    "profit_amount INT NOT NULL COMMENT '利润金额(分)', " +
-                    "platform_retained INT NOT NULL COMMENT '平台留存金额(分)', " +
-                    "vendor_income INT NOT NULL COMMENT '供应商实收金额(分)', " +
-                    "commission_l1 INT DEFAULT 0 COMMENT 'L1推荐人佣金(分)', " +
-                    "commission_l2 INT DEFAULT 0 COMMENT 'L2推荐人佣金(分)', " +
-                    "rule_id BIGINT DEFAULT NULL COMMENT '使用的返利规则ID', " +
-                    "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                    "INDEX idx_order (order_id), " +
-                    "INDEX idx_order_no (order_no)" +
-                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='返利流水表'");
-            log.info("已创建 danshop_profit_logs 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_profit_logs 表失败: {}", e.getMessage());
-        }
-
-        // 9. danshop_favorites (商品收藏表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_favorites (" +
-                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                    "user_id BIGINT NOT NULL COMMENT '用户ID', " +
-                    "product_id BIGINT NOT NULL COMMENT '商品ID', " +
-                    "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                    "UNIQUE INDEX idx_user_product (user_id, product_id), " +
-                    "INDEX idx_user (user_id)" +
-                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品收藏表'");
-            log.info("已创建 danshop_favorites 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_favorites 表失败: {}", e.getMessage());
-        }
-
-        // 10. danshop_addresses (收货地址表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_addresses (" +
-                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                    "user_id BIGINT NOT NULL COMMENT '用户ID', " +
-                    "receiver_name VARCHAR(32) NOT NULL COMMENT '收货人姓名', " +
-                    "phone VARCHAR(16) NOT NULL COMMENT '收货人电话', " +
-                    "province VARCHAR(32) NOT NULL COMMENT '省份', " +
-                    "city VARCHAR(32) NOT NULL COMMENT '城市', " +
-                    "district VARCHAR(32) NOT NULL COMMENT '区县', " +
-                    "street VARCHAR(256) NOT NULL COMMENT '详细地址', " +
-                    "is_default INT DEFAULT 0 COMMENT '是否默认地址 1=是 0=否', " +
-                    "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                    "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-                    "INDEX idx_user (user_id), " +
-                    "INDEX idx_user_default (user_id, is_default)" +
-                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收货地址表'");
-            log.info("已创建 danshop_addresses 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_addresses 表失败: {}", e.getMessage());
-        }
-
         // ==================== 迁移: activities表添加visible_scope列 =============        
         try {
             Integer colExists = jdbcTemplate.queryForObject(
@@ -11753,4 +11513,27 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
                 "BIGINT DEFAULT NULL COMMENT '报告归属成员ID(family_members.id),A2/DAN自动采集必需'");
         log.info("迁移343: health_report_drafts.member_id 已确保存在");
     }
+
+    // 迁移344: 创建 supply_customer_relation 表(供应商团队客户归属记录)
+    private void createSupplyCustomerRelationTable() {
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS supply_customer_relation (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'," +
+                    "system_id BIGINT NOT NULL COMMENT '供应商体系ID(supply_system.id)'," +
+                    "parent_user_id BIGINT NOT NULL COMMENT '上级供应商用户ID(supply_hierarchy.parent_id)'," +
+                    "family_id BIGINT NOT NULL COMMENT '客户家庭ID(family_members.family_id)'," +
+                    "child_user_id BIGINT DEFAULT NULL COMMENT '下单用户ID(users.id),可为空'," +
+                    "relation_type VARCHAR(32) NOT NULL DEFAULT 'family' COMMENT '归属类型: family=家庭归属 order=订单关联'," +
+                    "source VARCHAR(32) NOT NULL DEFAULT 'manual' COMMENT '来源: manual=人工绑定 order=下单自动 referral=推荐码'," +
+                    "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
+                    "INDEX idx_system_family (system_id, family_id)," +
+                    "INDEX idx_parent_user (parent_user_id)," +
+                    "INDEX idx_family_id (family_id)," +
+                    "UNIQUE KEY uk_system_family (system_id, family_id)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商团队客户归属关系表'");
+            log.info("迁移344: 已创建 supply_customer_relation 表");
+        } catch (Exception e) {
+            log.warn("迁移344: 创建 supply_customer_relation 表失败: {}", e.getMessage());
+        }
+    }
 }

+ 22 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java

@@ -1368,10 +1368,31 @@ public class HealthReportController {
         return Result.success("报告绑定成功");
     }
 
+    /**
+     * 重新分配报告归属
+     *
+     * <p>将报告从旧归属切换到新归属,或解除归属(subjectId 设为 null)。
+     * 归属变更后自动重新计算五维能量维度和食材推荐指数。</p>
+     *
+     * <p>权限检查:仅家庭成员或管理员可操作,通过 familyId 验证报告归属。</p>
+     *
+     * @param params 请求体:reportId (必填), subjectId (可为 null 表示解绑)
+     * @param familyId 当前登录用户的家庭ID(由 JwtInterceptor 注入)
+     * @return 操作结果
+     */
+    @PostMapping("/report/reassign")
+    public Result<String> reassignReport(@RequestBody Map<String, Object> params,
+                                          @RequestAttribute(value = "familyId", required = false) Long familyId) {
+        Long reportId = ParamUtils.getLong(params.get("reportId"));
+        Long newSubjectId = ParamUtils.getLong(params.get("subjectId")); // null = unbind
+        
+        healthReportService.reassignReport(reportId, newSubjectId, familyId);
+        return Result.success("归属已更新");
+    }
+
     /**
      * 获取某家庭中所有未绑定成员的报告列表
      */
-    @Operation(summary = "获取未绑定的报告列表")
     @PostMapping("/report/unlinked")
     public Result<List<HealthReport>> getUnlinkedReports(
             @RequestAttribute(value = "familyId", required = false) Long familyId) {

+ 18 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductController.java

@@ -133,6 +133,24 @@ if (params.get("distributionSystemId") != null) {
         return productService.adminShelve(productId, shelve);
     }
 
+    @Operation(summary = "批量商品上下架")
+    @PostMapping("/batch-shelve")
+    public Result<String> batchShelve(@RequestBody Map<String, Object> params) {
+        List<Long> productIds = (List<Long>) params.get("productIds");
+        Boolean shelve = params.get("shelve") != null
+                ? (Boolean) params.get("shelve")
+                : null;
+
+        if (productIds == null || productIds.isEmpty()) {
+            return Result.error("productIds不能为空");
+        }
+        if (shelve == null) {
+            return Result.error("shelve不能为空");
+        }
+
+        return productService.batchAdminShelve(productIds, shelve);
+    }
+
     @Operation(summary = "商品详情")
     @PostMapping("/detail")
     public Result<ProductDTO> detail(@RequestBody Map<String, Object> params) {

+ 76 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/SupplyTeamController.java

@@ -0,0 +1,76 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.SupplyCustomerRelationService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
+
+@RestController
+@RequestMapping("/api/admin/supply-team")
+public class SupplyTeamController {
+
+    @Resource
+    private SupplyCustomerRelationService supplyCustomerRelationService;
+
+    /**
+     * 查询上级供应商下的客户列表
+     */
+    @PostMapping("/customers")
+    public Result<List<Map<String, Object>>> listCustomers(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId) {
+        Long systemId = ParamUtils.getLong(params.get("systemId"));
+        Long parentId = ParamUtils.getLong(params.get("parentId"));
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
+
+        List<Map<String, Object>> customers = supplyCustomerRelationService.listCustomers(systemId, parentId, page, size);
+        return Result.success(customers);
+    }
+
+    /**
+     * 人工绑定客户归属
+     */
+    @PostMapping("/bind-customer")
+    public Result<Void> bindCustomer(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId) {
+        Long systemId = ParamUtils.getLong(params.get("systemId"));
+        Long parentId = ParamUtils.getLong(params.get("parentId"));
+        Long familyId = ParamUtils.getLong(params.get("familyId"));
+        Long childUserId = ParamUtils.getLong(params.get("childUserId"));
+        String source = (String) params.get("source");
+
+        boolean success = supplyCustomerRelationService.bindCustomer(systemId, parentId, familyId, childUserId, source);
+        return success ? Result.success(null) : Result.error("绑定失败");
+    }
+
+    /**
+     * 查询团队成员列表(含客户数量统计)
+     */
+    @PostMapping("/members")
+    public Result<List<Map<String, Object>>> listTeamMembers(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId) {
+        Long systemId = ParamUtils.getLong(params.get("systemId"));
+        List<Map<String, Object>> members = supplyCustomerRelationService.listTeamMembers(systemId);
+        return Result.success(members);
+    }
+
+    /**
+     * 查询某个家庭的归属关系
+     */
+    @PostMapping("/relation")
+    public Result<Object> getRelation(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId) {
+        Long systemId = ParamUtils.getLong(params.get("systemId"));
+        Long familyId = ParamUtils.getLong(params.get("familyId"));
+        var relation = supplyCustomerRelationService.getRelationByFamily(systemId, familyId);
+        return Result.success(relation);
+    }
+}

+ 3 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/cart/CartController.java

@@ -2,6 +2,7 @@ package com.etotem.cfc.controller.cart;
 
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.CartItemDTO;
+import com.etotem.cfc.dto.CartListResultDTO;
 import com.etotem.cfc.service.CartService;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestAttribute;
@@ -36,8 +37,8 @@ public class CartController {
     }
 
     @PostMapping("/list")
-    public Result<List<CartItemDTO>> list(@RequestAttribute("userId") Long userId) {
-        List<CartItemDTO> list = cartService.getCartList(userId);
+    public Result<CartListResultDTO> list(@RequestAttribute("userId") Long userId) {
+        CartListResultDTO list = cartService.getCartList(userId);
         return Result.success(list);
     }
 

+ 16 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/CartItemDTO.java

@@ -53,4 +53,20 @@ public class CartItemDTO {
         private Long childProductId;
         private Integer quantity;
     }
+
+    @Data
+    public static class GiftPromo {
+        /** 送会员商品原价合计(分) */
+        private Integer giftOriginalTotal;
+        /** 阈值(分) */
+        private Integer threshold;
+        /** 一口实付价(分),达标后送会员商品部分实付价 */
+        private Integer payPrice;
+        /** 折扣金额(分) */
+        private Integer discount;
+        /** 是否达标 */
+        private Boolean active;
+        /** 还差多少到阈值(分) */
+        private Integer remaining;
+    }
 }

+ 12 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/CartListResultDTO.java

@@ -0,0 +1,12 @@
+package com.etotem.cfc.dto;
+
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class CartListResultDTO {
+
+    private List<CartItemDTO> items;
+
+    private CartItemDTO.GiftPromo giftPromo;
+}

+ 22 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/SupplyCustomerRelation.java

@@ -0,0 +1,22 @@
+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("supply_customer_relation")
+public class SupplyCustomerRelation implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long systemId;
+    private Long parentUserId;
+    private Long familyId;
+    private Long childUserId;
+    private String relationType;
+    private String source;
+    private Date createdAt;
+}

+ 27 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/SupplyCustomerRelationMapper.java

@@ -0,0 +1,27 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.SupplyCustomerRelation;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface SupplyCustomerRelationMapper extends BaseMapper<SupplyCustomerRelation> {
+    /** 根据体系ID和上级用户ID查询所有客户 */
+    @Select("SELECT r.* FROM supply_customer_relation r " +
+            "WHERE r.system_id = #{systemId} AND r.parent_user_id = #{parentId}")
+    List<SupplyCustomerRelation> listByParent(@Param("systemId") Long systemId, @Param("parentId") Long parentId);
+
+    /** 根据体系ID查询所有客户归属记录 */
+    @Select("SELECT r.* FROM supply_customer_relation r " +
+            "WHERE r.system_id = #{systemId}")
+    List<SupplyCustomerRelation> listBySystem(@Param("systemId") Long systemId);
+
+    /** 根据家庭ID查询归属关系 */
+    @Select("SELECT r.* FROM supply_customer_relation r " +
+            "WHERE r.family_id = #{familyId}")
+    List<SupplyCustomerRelation> listByFamilyId(@Param("familyId") Long familyId);
+}

+ 97 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/CartService.java

@@ -2,6 +2,7 @@ package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.dto.CartItemDTO;
+import com.etotem.cfc.dto.CartListResultDTO;
 import com.etotem.cfc.entity.Cart;
 import com.etotem.cfc.entity.Product;
 import com.etotem.cfc.entity.ProductBundleItem;
@@ -13,6 +14,7 @@ import com.etotem.cfc.mapper.ProductMapper;
 import com.etotem.cfc.service.PpointService;
 import com.etotem.cfc.service.ProductGiftRuleService;
 import com.etotem.cfc.service.ProductSkuService;
+import com.etotem.cfc.service.SysConfigService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -45,6 +47,9 @@ public class CartService {
     @Resource
     private ProductGiftRuleService productGiftRuleService;
 
+    @Resource
+    private SysConfigService sysConfigService;
+
     @Transactional
     public CartItemDTO addToCart(Long userId, Long productId, Integer quantity, Long skuId) {
         Product product = productMapper.selectById(productId);
@@ -84,7 +89,7 @@ public class CartService {
         return buildCartItemDTO(saved);
     }
 
-    public List<CartItemDTO> getCartList(Long userId) {
+    public CartListResultDTO getCartList(Long userId) {
         List<Cart> cartList = cartMapper.selectList(
             new LambdaQueryWrapper<Cart>()
                 .eq(Cart::getUserId, userId)
@@ -122,7 +127,13 @@ public class CartService {
         // 自动套餐模式:检测购物车子商品组合是否凑成某套餐,命中则挂 bundleMatch 提示
         attachBundleMatches(cartList, result);
 
-        return result;
+        // 计算送会员商品聚合信息
+        CartItemDTO.GiftPromo giftPromo = computeGiftPromo(cartList, productMap);
+
+        CartListResultDTO cartResult = new CartListResultDTO();
+        cartResult.setItems(result);
+        cartResult.setGiftPromo(giftPromo);
+        return cartResult;
     }
 
     /**
@@ -395,4 +406,88 @@ public class CartService {
         }
         return dto;
     }
+
+    /**
+     * 计算购物车中送会员商品的聚合信息:原价合计、是否达标、折扣金额等。
+     * 购物车所有商品默认视为已选中(checked=true)。
+     */
+    private CartItemDTO.GiftPromo computeGiftPromo(List<Cart> cartList, Map<Long, Product> productMap) {
+        if (cartList == null || cartList.isEmpty()) {
+            return buildEmptyGiftPromo();
+        }
+
+        // 查询购物车中所有商品的送会员规则
+        List<Long> giftProductIds = new ArrayList<>();
+        for (Cart cart : cartList) {
+            Long pid = cart.getProductId();
+            if (pid != null && !giftProductIds.contains(pid)) {
+                giftProductIds.add(pid);
+            }
+        }
+        List<ProductGiftRule> giftRules = productGiftRuleService.listActiveByProductIds(giftProductIds);
+
+        // 按 productId 建立 rule 映射
+        Map<Long, ProductGiftRule> ruleMap = new HashMap<>();
+        for (ProductGiftRule rule : giftRules) {
+            if (rule.getProductId() != null) {
+                ruleMap.put(rule.getProductId(), rule);
+            }
+        }
+
+        // 计算送会员商品原价合计
+        int giftOriginalTotal = 0;
+        for (Cart cart : cartList) {
+            if (ruleMap.containsKey(cart.getProductId())) {
+                Product product = productMap.get(cart.getProductId());
+                if (product != null && product.getPrice() != null && cart.getQuantity() != null) {
+                    giftOriginalTotal += product.getPrice() * cart.getQuantity();
+                }
+            }
+        }
+
+        // 读取配置
+        String thresholdStr = sysConfigService.getValue("gift_membership_original_threshold");
+        int threshold;
+        try {
+            threshold = thresholdStr != null ? Integer.parseInt(thresholdStr) : 0;
+        } catch (NumberFormatException e) {
+            threshold = 0;
+        }
+
+        String payPriceStr = sysConfigService.getValue("gift_membership_pay_price");
+        int payPrice;
+        try {
+            payPrice = payPriceStr != null ? Integer.parseInt(payPriceStr) : 0;
+        } catch (NumberFormatException e) {
+            payPrice = 0;
+        }
+
+        if (threshold <= 0) {
+            return buildEmptyGiftPromo();
+        }
+
+        boolean active = giftOriginalTotal >= threshold;
+        int discount = active ? Math.max(0, giftOriginalTotal - payPrice) : 0;
+        int remaining = active ? 0 : Math.max(0, threshold - giftOriginalTotal);
+
+        CartItemDTO.GiftPromo promo = new CartItemDTO.GiftPromo();
+        promo.setGiftOriginalTotal(giftOriginalTotal);
+        promo.setThreshold(threshold);
+        promo.setPayPrice(active ? payPrice : 0);
+        promo.setDiscount(discount);
+        promo.setActive(active);
+        promo.setRemaining(remaining);
+        return promo;
+    }
+
+    private CartItemDTO.GiftPromo buildEmptyGiftPromo() {
+        CartItemDTO.GiftPromo promo = new CartItemDTO.GiftPromo();
+        promo.setGiftOriginalTotal(0);
+        promo.setThreshold(0);
+        promo.setPayPrice(0);
+        promo.setDiscount(0);
+        promo.setActive(false);
+        promo.setRemaining(0);
+        return promo;
+    }
 }

+ 42 - 31
cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java

@@ -478,42 +478,46 @@ public class CommissionService {
         Map<String, Object> result = new HashMap<>();
         List<Map<String, Object>> records = new ArrayList<>();
 
-        LambdaQueryWrapper<CommissionRecord> wrapper = new LambdaQueryWrapper<CommissionRecord>()
-                .eq(CommissionRecord::getReferrerId, userId);
-
-        if ("L1".equals(level)) {
-            wrapper.eq(CommissionRecord::getLevel, 1);
-        } else if ("L2".equals(level)) {
-            wrapper.eq(CommissionRecord::getLevel, 2);
+        // 成员来源:users.referrer_id(与 getTeamStats/getTotalTeamSize 口径一致)
+        // 以前用 commission_records,只包含消费过的成员;现在用 users.referrer_id 包含全部推荐关系
+        List<User> memberUsers = new ArrayList<>();
+        if ("L2".equals(level)) {
+            // L2:先查 L1(直接推荐人),再查 L2(L1 成员的推荐人)
+            List<User> l1Users = userMapper.selectList(
+                    new LambdaQueryWrapper<User>().eq(User::getReferrerId, userId));
+            List<Long> l1Ids = new ArrayList<>();
+            for (User u : l1Users) {
+                if (u.getId() != null) {
+                    l1Ids.add(u.getId());
+                }
+            }
+            if (!l1Ids.isEmpty()) {
+                memberUsers = userMapper.selectList(
+                        new LambdaQueryWrapper<User>().in(User::getReferrerId, l1Ids));
+            }
+        } else {
+            // L1 或 ALL:直接推荐人
+            memberUsers = userMapper.selectList(
+                    new LambdaQueryWrapper<User>().eq(User::getReferrerId, userId));
         }
 
-        SortUtil.applySort(wrapper);
-        List<CommissionRecord> allRecords = commissionRecordMapper.selectList(wrapper);
-
+        // 组装成员基础信息
         Map<Long, Map<String, Object>> memberMap = new LinkedHashMap<>();
-        for (CommissionRecord record : allRecords) {
-            Long buyerId = record.getBuyerId();
-            if (buyerId == null) continue;
-
-            if (!memberMap.containsKey(buyerId)) {
-                User member = userMapper.selectById(buyerId);
-                if (member == null) continue;
-
-                Map<String, Object> memberInfo = new HashMap<>();
-                memberInfo.put("userId", buyerId);
-                memberInfo.put("nickname", member.getNickname() != null ? member.getNickname() : "微信用户");
-                memberInfo.put("avatarUrl", member.getAvatar() != null ? member.getAvatar() : "");
-                memberInfo.put("createdAt", member.getCreatedAt());
-                memberInfo.put("orderCount", 0);
-                memberInfo.put("commissionEarned", 0);
-                memberMap.put(buyerId, memberInfo);
-            }
+        for (User member : memberUsers) {
+            if (member.getId() == null) continue;
+            Map<String, Object> memberInfo = new HashMap<>();
+            memberInfo.put("userId", member.getId());
+            memberInfo.put("nickname", member.getNickname() != null ? member.getNickname() : "微信用户");
+            memberInfo.put("avatarUrl", member.getAvatar() != null ? member.getAvatar() : "");
+            memberInfo.put("createdAt", member.getCreatedAt());
+            memberInfo.put("orderCount", 0);
+            memberInfo.put("commissionEarned", 0);
+            memberMap.put(member.getId(), memberInfo);
         }
 
-        // 批量预取成员汇总(避免 N+1):已完成履约订单总额 + 非cancelled佣金总额,循环内直接取用
+        // 批量预取消费订单总额(已履约)
         List<Long> buyerIds = new ArrayList<>(memberMap.keySet());
         Map<Long, Integer> consumptionMap = new HashMap<>();
-        Map<Long, Integer> cfEarnedMap = new HashMap<>();
         if (!buyerIds.isEmpty()) {
             LambdaQueryWrapper<ProductOrder> consumptionWrapper = new LambdaQueryWrapper<ProductOrder>()
                     .in(ProductOrder::getBuyerId, buyerIds)
@@ -522,7 +526,11 @@ public class CommissionService {
                 if (order.getBuyerId() == null || order.getTotalAmount() == null) continue;
                 consumptionMap.merge(order.getBuyerId(), order.getTotalAmount(), Integer::sum);
             }
+        }
 
+        // 批量预取佣金总额(非 cancel 的)
+        Map<Long, Integer> cfEarnedMap = new HashMap<>();
+        if (!buyerIds.isEmpty()) {
             LambdaQueryWrapper<CommissionRecord> cfWrapper = new LambdaQueryWrapper<CommissionRecord>()
                     .eq(CommissionRecord::getReferrerId, userId)
                     .in(CommissionRecord::getBuyerId, buyerIds)
@@ -533,10 +541,12 @@ public class CommissionService {
             }
         }
 
+        // 为每位成员补充层级过滤后的订单数和佣金总额
         for (Map.Entry<Long, Map<String, Object>> entry : memberMap.entrySet()) {
             Long buyerId = entry.getKey();
             Map<String, Object> memberInfo = entry.getValue();
 
+            // 订单数(按层级过滤 commission_records)
             LambdaQueryWrapper<CommissionRecord> orderWrapper = new LambdaQueryWrapper<CommissionRecord>()
                     .eq(CommissionRecord::getReferrerId, userId)
                     .eq(CommissionRecord::getBuyerId, buyerId);
@@ -545,13 +555,14 @@ public class CommissionService {
             } else if ("L2".equals(level)) {
                 orderWrapper.eq(CommissionRecord::getLevel, 2);
             }
-            SortUtil.applySort(orderWrapper);
             Long orderCount = commissionRecordMapper.selectCount(orderWrapper);
             memberInfo.put("orderCount", orderCount != null ? orderCount.intValue() : 0);
 
+            // 佣金总额(按层级过滤)
             LambdaQueryWrapper<CommissionRecord> commissionWrapper = new LambdaQueryWrapper<CommissionRecord>()
                     .eq(CommissionRecord::getReferrerId, userId)
-                    .eq(CommissionRecord::getBuyerId, buyerId);
+                    .eq(CommissionRecord::getBuyerId, buyerId)
+                    .ne(CommissionRecord::getStatus, "cancelled");
             if ("L1".equals(level)) {
                 commissionWrapper.eq(CommissionRecord::getLevel, 1);
             } else if ("L2".equals(level)) {

+ 71 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java

@@ -1193,10 +1193,79 @@ public class HealthReportService {
         }
     }
 
-    public List<HealthReport> getUnlinkedReports(Long userId) {
+    /**
+     * 重新分配报告归属
+     *
+     * <p>将报告从旧归属切换到新归属,或解除归属(subjectId 设为 null)。
+     * 归属变更后自动重新计算五维能量维度和食材推荐指数。</p>
+     *
+     * <p>流程:</p>
+     * <ol>
+     *   <li>权限检查:确保报告属于当前家庭</li>
+     *   <li>记录旧归属 subjectId</li>
+     *   <li>更新 health_reports.subject_id</li>
+     *   <li>若旧归属不为空:refreshFromGutReport + recalculateForUser</li>
+     *   <li>若新归属不为空:refreshFromGutReport + recalculateForUser</li>
+     *   <li>记录操作日志</li>
+     * </ol>
+     *
+     * @param reportId 报告ID
+     * @param newSubjectId 新的归属成员ID(null 表示解除绑定)
+     * @param familyId 家庭ID,用于权限验证
+     * @throws RuntimeException 当报告不存在或不属于该家庭时抛出
+     */
+    @Transactional
+    public void reassignReport(Long reportId, Long newSubjectId, Long familyId) {
+        HealthReport report = healthReportMapper.selectById(reportId);
+        if (report == null) {
+            throw new RuntimeException("报告不存在");
+        }
+        
+        // 权限检查:确保报告属于该家庭
+        if (!familyId.equals(report.getFamilyId())) {
+            throw new RuntimeException("报告不属于该家庭");
+        }
+        
+        Long oldSubjectId = report.getSubjectId();
+        
+        // 无实际变化则直接返回
+        if (oldSubjectId != null && newSubjectId != null && oldSubjectId.equals(newSubjectId)) {
+            return;
+        }
+        
+        // 1. 更新 subject_id
+        String updateSql = "UPDATE health_reports SET subject_id = ? WHERE id = ?";
+        jdbcTemplate.update(updateSql, newSubjectId, reportId);
+        log.info("报告 {} 归属已更新:oldSubjectId={}, newSubjectId={}", reportId, oldSubjectId, newSubjectId);
+        
+        // 2. 重算旧成员维度(如果原来有绑定)
+        if (oldSubjectId != null) {
+            dimensionScoreService.refreshFromGutReport(oldSubjectId, reportId);
+            foodRecommendService.recalculateForUser(oldSubjectId);
+            log.info("已重算旧归属成员 {} 的五维能量与食材推荐", oldSubjectId);
+        }
+        
+        // 3. 重算新成员维度(如果有新归属)
+        if (newSubjectId != null) {
+            dimensionScoreService.refreshFromGutReport(newSubjectId, reportId);
+            foodRecommendService.recalculateForUser(newSubjectId);
+            log.info("已重算新归属成员 {} 的五维能量与食材推荐", newSubjectId);
+        }
+        
+        log.info("报告归属变更: reportId={}, oldSubjectId={}, newSubjectId={}", 
+                 reportId, oldSubjectId, newSubjectId);
+    }
+
+    /**
+     * 获取家庭内所有未绑定归属的报告
+     *
+     * @param familyId 家庭ID
+     * @return 未绑定归属的报告列表
+     */
+    public List<HealthReport> getUnlinkedReports(Long familyId) {
         return healthReportMapper.selectList(
                 new LambdaQueryWrapper<HealthReport>()
-                        .eq(HealthReport::getUserId, userId)
+                        .eq(HealthReport::getFamilyId, familyId)
                         .isNull(HealthReport::getSubjectId)
                         .eq(HealthReport::getStatus, "active")
                         .orderByDesc(HealthReport::getReportDate)

+ 38 - 3
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -27,7 +27,9 @@ import com.etotem.cfc.service.PlatformPointsService;
 import com.etotem.cfc.util.RedisLockUtil;
 import com.etotem.cfc.mapper.AfterSalesRequestMapper;
 import com.etotem.cfc.entity.SupplySystemMember;
+import com.etotem.cfc.entity.SupplyCustomerRelation;
 import com.etotem.cfc.mapper.SupplySystemMemberMapper;
+import com.etotem.cfc.mapper.SupplyCustomerRelationMapper;
 import com.etotem.cfc.service.SupplyHierarchyService;
 import com.etotem.cfc.mapper.CouponMapper;
 import com.etotem.cfc.mapper.FamilyCouponMapper;
@@ -59,6 +61,7 @@ import com.etotem.cfc.service.EnergyService;
 import com.etotem.cfc.service.UnlockGateService;
 import com.etotem.cfc.service.UserService;
 import com.etotem.cfc.service.GiftMembershipProgressService;
+import com.etotem.cfc.service.SysConfigService;
 import com.alibaba.fastjson.JSON;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -139,12 +142,18 @@ public class ProductOrderService {
     @Resource
     private SupplyHierarchyService supplyHierarchyService;
 
+    @Resource
+    private SupplyCustomerRelationMapper supplyCustomerRelationMapper;
+
     @Resource
     private ProductGiftRuleService productGiftRuleService;
 
     @Resource
     private GiftMembershipProgressService giftMembershipProgressService;
 
+    @Resource
+    private SysConfigService sysConfigService;
+
     @Resource
     private MembershipService membershipService;
 
@@ -773,6 +782,8 @@ public class ProductOrderService {
                     );
                     if (referrerMember != null) {
                         supplyHierarchyService.addRelation(order.getSupplySystemId(), buyer.getReferrerId(), order.getBuyerId());
+                        // 同时记录供应商团队客户归属
+                        supplyCustomerRelationService.autoBindOnOrder(order.getSupplySystemId(), buyer.getReferrerId(), order.getFamilyId(), order.getBuyerId());
                     }
                 }
             }
@@ -812,17 +823,41 @@ public class ProductOrderService {
             log.error("发放测评额度失败: orderNo={}", orderNo, e);
             // 不阻塞支付流程
         }
-        // 赠送会员:原价支付时,送会员商品金额累计到家庭进度,达到阈值自动赠送会员
+        // 赠送会员:单笔订单送会员商品原价合计达标即赠送会员
         try {
             if (order.getGiftAmount() != null && order.getGiftAmount() > 0 && order.getFamilyId() != null) {
-                // 原价判定:无积分抵扣、无优惠券、无折扣
+                String origThresholdStr = sysConfigService.getValue("gift_membership_original_threshold");
+                int origThreshold;
+                try {
+                    origThreshold = origThresholdStr != null ? Integer.parseInt(origThresholdStr) : 0;
+                } catch (NumberFormatException e) {
+                    origThreshold = 0;
+                }
+                // 只要单笔订单送会员商品原价合计>=阈值,即直接赠送会员(不再受是否有折扣/积分/优惠券限制)
+                if (origThreshold > 0 && order.getGiftAmount() >= origThreshold) {
+                    String levelCode = sysConfigService.getValue("gift_membership_level");
+                    if (levelCode == null || levelCode.isEmpty()) {
+                        levelCode = "FAMILY";
+                    }
+                    boolean granted = membershipService.grantMembershipByGift(
+                            order.getFamilyId(), order.getBuyerId(), levelCode, null, null);
+                    if (granted) {
+                        // 同步记录进度(已赠送标记)
+                        giftMembershipProgressService.accumulate(
+                                order.getFamilyId(), order.getBuyerId(), order.getGiftAmount());
+                        log.info("商品订单{}单笔达标送会员: giftAmount={}, threshold={}, granted={}", orderNo, order.getGiftAmount(), origThreshold, granted);
+                    }
+                }
+                // 兼容原有累计进度展示(仅统计无折扣/无券/无积分的订单)
                 boolean fullPrice = (order.getPointsUsed() == null || order.getPointsUsed() == 0)
                         && order.getCouponId() == null
                         && (order.getDiscountAmount() == null || order.getDiscountAmount() == 0);
                 if (fullPrice) {
                     boolean granted = giftMembershipProgressService.accumulate(
                             order.getFamilyId(), order.getBuyerId(), order.getGiftAmount());
-                    log.info("商品订单{}累计送会员金额={}, 触发赠送={}", orderNo, order.getGiftAmount(), granted);
+                    if (granted) {
+                        log.info("商品订单{}累计送会员金额={}, 触发赠送={}", orderNo, order.getGiftAmount(), granted);
+                    }
                 }
             }
         } catch (Exception e) {

+ 26 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java

@@ -370,6 +370,32 @@ wrapper.in(Product::getId, productIds)
         return Result.success(shelve ? "上架成功" : "下架成功");
     }
 
+    /**
+     * 管理员批量上下架商品
+     */
+    public Result<String> batchAdminShelve(List<Long> productIds, Boolean shelve) {
+        if (productIds == null || productIds.isEmpty()) {
+            return Result.error("productIds不能为空");
+        }
+        for (Long productId : productIds) {
+            Product product = productMapper.selectById(productId);
+            if (product == null) {
+                return Result.error("商品不存在: " + productId);
+            }
+            if (!"approved".equals(product.getStatus()) && !"on_shelf".equals(product.getStatus()) && !"off_shelf".equals(product.getStatus())) {
+                return Result.error("仅已审核通过的商品可以上下架,商品 ID: " + productId + " 状态: " + product.getStatus());
+            }
+            if (shelve) {
+                product.setStatus("on_shelf");
+            } else {
+                product.setStatus("off_shelf");
+            }
+            product.setUpdatedAt(new Date());
+            productMapper.updateById(product);
+        }
+        return Result.success(shelve ? "批量上架成功" : "批量下架成功");
+    }
+
     public boolean decreaseStock(Long productId, Integer quantity) {
         if (quantity == null || quantity <= 0) {
             return false;

+ 181 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/SupplyCustomerRelationService.java

@@ -0,0 +1,181 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.SupplyCustomerRelation;
+import com.etotem.cfc.entity.SupplySystem;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.SupplyCustomerRelationMapper;
+import com.etotem.cfc.mapper.SupplySystemMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class SupplyCustomerRelationService {
+
+    @Resource
+    private SupplyCustomerRelationMapper supplyCustomerRelationMapper;
+
+    @Resource
+    private SupplySystemMapper supplySystemMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private FamilyMemberMapper familyMemberMapper;
+
+    /**
+     * 查询某上级供应商下的客户列表
+     * 返回:家庭ID、家庭名称、成员昵称、加入时间、归属来源
+     */
+    public List<Map<String, Object>> listCustomers(Long systemId, Long parentId, Integer page, Integer size) {
+        List<SupplyCustomerRelation> relations = supplyCustomerRelationMapper.listByParent(systemId, parentId);
+        
+        // 分页处理
+        int fromIndex = (page - 1) * size;
+        int toIndex = Math.min(fromIndex + size, relations.size());
+        if (fromIndex >= relations.size()) {
+            return Collections.emptyList();
+        }
+        relations = relations.subList(fromIndex, toIndex);
+
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (SupplyCustomerRelation relation : relations) {
+            Map<String, Object> customer = new HashMap<>();
+            customer.put("id", relation.getId());
+            customer.put("familyId", relation.getFamilyId());
+            customer.put("relationType", relation.getRelationType());
+            customer.put("source", relation.getSource());
+            customer.put("createdAt", relation.getCreatedAt());
+
+            // 查询家庭信息
+            FamilyMember parentMember = familyMemberMapper.selectOne(
+                new LambdaQueryWrapper<FamilyMember>()
+                    .eq(FamilyMember::getFamilyId, relation.getFamilyId())
+                    .eq(FamilyMember::getRole, "parent")
+                    .orderByAsc(FamilyMember::getGeneration)
+                    .last("LIMIT 1")
+            );
+            if (parentMember != null) {
+                customer.put("familyName", parentMember.getNickname());
+                customer.put("memberType", "login");
+                
+                // 查询家庭下能登录的用户数量
+                Long loginCount = familyMemberMapper.selectCount(
+                    new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getFamilyId, relation.getFamilyId())
+                        .isNotNull(FamilyMember::getUserId)
+                );
+                customer.put("loginMemberCount", loginCount);
+            } else {
+                customer.put("familyName", "未知家庭");
+                customer.put("memberType", "local");
+            }
+
+            // 查询下单用户信息(如果有)
+            if (relation.getChildUserId() != null) {
+                User buyer = userMapper.selectById(relation.getChildUserId());
+                if (buyer != null) {
+                    customer.put("buyerNickname", buyer.getNickname());
+                    customer.put("buyerPhone", buyer.getPhone());
+                }
+            }
+
+            result.add(customer);
+        }
+        return result;
+    }
+
+    /**
+     * 绑定客户归属关系(人工绑定)
+     */
+    public boolean bindCustomer(Long systemId, Long parentId, Long familyId, Long childUserId, String source) {
+        // 检查是否已存在绑定关系
+        SupplyCustomerRelation existing = supplyCustomerRelationMapper.selectOne(
+            new LambdaQueryWrapper<SupplyCustomerRelation>()
+                .eq(SupplyCustomerRelation::getSystemId, systemId)
+                .eq(SupplyCustomerRelation::getFamilyId, familyId)
+        );
+        if (existing != null) {
+            // 更新现有记录
+            existing.setParentUserId(parentId);
+            existing.setChildUserId(childUserId);
+            existing.setSource(source != null ? source : "manual");
+            existing.setRelationType("family");
+            return supplyCustomerRelationMapper.updateById(existing) > 0;
+        }
+
+        // 创建新记录
+        SupplyCustomerRelation relation = new SupplyCustomerRelation();
+        relation.setSystemId(systemId);
+        relation.setParentUserId(parentId);
+        relation.setFamilyId(familyId);
+        relation.setChildUserId(childUserId);
+        relation.setRelationType("family");
+        relation.setSource(source != null ? source : "manual");
+        return supplyCustomerRelationMapper.insert(relation) > 0;
+    }
+
+    /**
+     * 系统自动绑定(下单时触发)
+     */
+    public void autoBindOnOrder(Long systemId, Long parentId, Long familyId, Long childUserId) {
+        bindCustomer(systemId, parentId, familyId, childUserId, "order");
+    }
+
+    /**
+     * 查询团队成员列表(含客户数量统计)
+     */
+    public List<Map<String, Object>> listTeamMembers(Long systemId) {
+        // 这里简化实现,实际可扩展
+        List<Map<String, Object>> result = new ArrayList<>();
+        
+        // 查询该体系下的所有上下级关系
+        List<SupplyCustomerRelation> allRelations = supplyCustomerRelationMapper.listBySystem(systemId);
+        
+        // 按上级用户分组统计客户数量
+        Map<Long, Long> customerCountByParent = allRelations.stream()
+            .collect(Collectors.groupingBy(
+                SupplyCustomerRelation::getParentUserId,
+                Collectors.counting()
+            ));
+        
+        // 查询所有上级用户的详细信息
+        Set<Long> parentUserIds = allRelations.stream()
+            .map(SupplyCustomerRelation::getParentUserId)
+            .collect(Collectors.toSet());
+        
+        for (Long parentId : parentUserIds) {
+            Map<String, Object> member = new HashMap<>();
+            member.put("userId", parentId);
+            member.put("customerCount", customerCountByParent.getOrDefault(parentId, 0L));
+            
+            User user = userMapper.selectById(parentId);
+            if (user != null) {
+                member.put("nickname", user.getNickname());
+                member.put("phone", user.getPhone());
+                member.put("avatar", user.getAvatar());
+            }
+            result.add(member);
+        }
+        return result;
+    }
+
+    /**
+     * 查询某个家庭的归属关系
+     */
+    public SupplyCustomerRelation getRelationByFamily(Long systemId, Long familyId) {
+        return supplyCustomerRelationMapper.selectOne(
+            new LambdaQueryWrapper<SupplyCustomerRelation>()
+                .eq(SupplyCustomerRelation::getSystemId, systemId)
+                .eq(SupplyCustomerRelation::getFamilyId, familyId)
+        );
+    }
+}

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

@@ -5959,3 +5959,19 @@ CREATE TABLE IF NOT EXISTS home_banners (
     created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='首页广告栏配置表';
+
+-- 迁移344: 创建 supply_customer_relation 表(供应商团队客户归属记录)
+CREATE TABLE IF NOT EXISTS supply_customer_relation (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
+    system_id BIGINT NOT NULL COMMENT '供应商体系ID(supply_system.id)',
+    parent_user_id BIGINT NOT NULL COMMENT '上级供应商用户ID(supply_hierarchy.parent_id)',
+    family_id BIGINT NOT NULL COMMENT '客户家庭ID(family_members.family_id)',
+    child_user_id BIGINT DEFAULT NULL COMMENT '下单用户ID(users.id),可为空',
+    relation_type VARCHAR(32) NOT NULL DEFAULT 'family' COMMENT '归属类型: family=家庭归属 order=订单关联',
+    source VARCHAR(32) NOT NULL DEFAULT 'manual' COMMENT '来源: manual=人工绑定 order=下单自动 referral=推荐码',
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    INDEX idx_system_family (system_id, family_id),
+    INDEX idx_parent_user (parent_user_id),
+    INDEX idx_family_id (family_id),
+    UNIQUE KEY uk_system_family (system_id, family_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商团队客户归属关系表';

+ 155 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/HealthReportServiceTest.java

@@ -0,0 +1,155 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.HealthReport;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * HealthReportService 单元测试(纯单元测试,无 SpringBootTest,避免 MySQL 连接)。
+ *
+ * 测试范围:
+ * - reassignReport: 重新分配报告归属(无实际变化快路径、权限检查、SQL 更新、维度重算)
+ * - getUnlinkedReports: 获取未绑定归属的报告列表(查询条件修复:familyId → family_id)
+ */
+public class HealthReportServiceTest {
+
+    @Mock
+    private com.etotem.cfc.mapper.HealthReportMapper healthReportMapper;
+
+    @Mock
+    private com.etotem.cfc.mapper.HealthReportPermissionMapper healthReportPermissionMapper;
+
+    @Mock
+    private DimensionScoreService dimensionScoreService;
+
+    @Mock
+    private FoodRecommendService foodRecommendService;
+
+    @Mock
+    private JdbcTemplate jdbcTemplate;
+
+    private HealthReportService service;
+
+    @BeforeEach
+    public void setUp() {
+        MockitoAnnotations.openMocks(this);
+        service = new HealthReportService();
+        setField(service, "healthReportMapper", healthReportMapper);
+        setField(service, "healthReportPermissionMapper", healthReportPermissionMapper);
+        setField(service, "dimensionScoreService", dimensionScoreService);
+        setField(service, "foodRecommendService", foodRecommendService);
+        setField(service, "jdbcTemplate", jdbcTemplate);
+    }
+
+    private void setField(Object target, String fieldName, Object value) {
+        try {
+            java.lang.reflect.Field f = target.getClass().getDeclaredField(fieldName);
+            f.setAccessible(true);
+            f.set(target, value);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private HealthReport report(Long id, Long familyId, Long subjectId, String status) {
+        HealthReport r = new HealthReport();
+        r.setId(id);
+        r.setFamilyId(familyId);
+        r.setSubjectId(subjectId);
+        r.setStatus(status);
+        return r;
+    }
+
+    // ── reassignReport ──────────────────────────────────────────────────
+
+    @Test
+    void reassignReport_noActualChange_returnsEarly() {
+        when(healthReportMapper.selectById(1L)).thenReturn(report(1L, 10L, 20L, "active"));
+
+        service.reassignReport(1L, 20L, 10L);
+
+        verify(jdbcTemplate, never()).update(anyString(), any(), anyLong());
+        verify(dimensionScoreService, never()).refreshFromGutReport(anyLong(), anyLong());
+        verify(foodRecommendService, never()).recalculateForUser(anyLong());
+    }
+
+    @Test
+    void reassignReport_unbind_subjectIdNull() {
+        when(healthReportMapper.selectById(1L)).thenReturn(report(1L, 10L, 20L, "active"));
+
+        service.reassignReport(1L, null, 10L);
+
+        verify(jdbcTemplate).update(eq("UPDATE health_reports SET subject_id = ? WHERE id = ?"),
+                eq(null), eq(1L));
+        verify(dimensionScoreService).refreshFromGutReport(eq(20L), eq(1L));
+        verify(foodRecommendService).recalculateForUser(eq(20L));
+    }
+
+    @Test
+    void reassignReport_bind_newMember() {
+        when(healthReportMapper.selectById(1L)).thenReturn(report(1L, 10L, null, "active"));
+
+        service.reassignReport(1L, 30L, 10L);
+
+        verify(jdbcTemplate).update(eq("UPDATE health_reports SET subject_id = ? WHERE id = ?"),
+                eq(30L), eq(1L));
+        verify(dimensionScoreService).refreshFromGutReport(eq(30L), eq(1L));
+        verify(foodRecommendService).recalculateForUser(eq(30L));
+    }
+
+    @Test
+    void reassignReport_switchMember_recalcBoth() {
+        when(healthReportMapper.selectById(1L)).thenReturn(report(1L, 10L, 20L, "active"));
+
+        service.reassignReport(1L, 30L, 10L);
+
+        verify(jdbcTemplate).update(eq("UPDATE health_reports SET subject_id = ? WHERE id = ?"),
+                eq(30L), eq(1L));
+        verify(dimensionScoreService).refreshFromGutReport(eq(20L), eq(1L));
+        verify(dimensionScoreService).refreshFromGutReport(eq(30L), eq(1L));
+        verify(foodRecommendService).recalculateForUser(eq(20L));
+        verify(foodRecommendService).recalculateForUser(eq(30L));
+    }
+
+    @Test
+    void reassignReport_wrongFamilyId_throws() {
+        when(healthReportMapper.selectById(1L)).thenReturn(report(1L, 99L, 20L, "active"));
+
+        assertThrows(RuntimeException.class, () -> service.reassignReport(1L, 30L, 10L));
+
+        verify(jdbcTemplate, never()).update(anyString(), any(), anyLong());
+        verify(dimensionScoreService, never()).refreshFromGutReport(anyLong(), anyLong());
+    }
+
+    @Test
+    void reassignReport_reportNotFound_throws() {
+        when(healthReportMapper.selectById(1L)).thenReturn(null);
+
+        assertThrows(RuntimeException.class, () -> service.reassignReport(1L, 30L, 10L));
+
+        verify(jdbcTemplate, never()).update(anyString(), any(), anyLong());
+    }
+
+    // ── getUnlinkedReports ──────────────────────────────────────────────
+
+    @Test
+    void getUnlinkedReports_returnsReports() {
+        List<HealthReport> reports = Collections.singletonList(report(1L, 10L, null, "active"));
+        when(healthReportMapper.selectList(any())).thenReturn(reports);
+
+        List<HealthReport> result = service.getUnlinkedReports(10L);
+
+        assertEquals(1, result.size());
+        verify(healthReportMapper).selectList(any());
+    }
+}

+ 128 - 2
cfc-frontend/components/ContentSharePoster.vue

@@ -22,7 +22,12 @@ export default {
     summary: { type: String, default: '' },
     priceText: { type: String, default: '' },
     ctaText: { type: String, default: '长按识别二维码 · 查看详情' },
-    inviteText: { type: String, default: '' }
+    inviteText: { type: String, default: '' },
+    // 五维能量数据卡:{ label?: String, overall?: Number, dims: [{ key, label, score, color }] }
+    // 传 null 时不画数据卡,按 coverImage -> brandFallback -> 占位图 顺序降级(原有行为不变)
+    energyData: { type: Object, default: null },
+    // 无 energyData 且 coverImage 为空时,是否用品牌渐变卡替代"暂无封面图"占位
+    brandFallback: { type: Boolean, default: false }
   },
   watch: {
     show(val) {
@@ -92,7 +97,11 @@ export default {
       var coverH = 220
       var self = this
 
-      if (this.coverImage) {
+      if (this.energyData) {
+        // 方案C:五维能量数据卡,canvas 直接绘制(文字锐利、数据真实、无网络开销)
+        this._drawEnergyCard(ctx, cardX + 20, coverY, coverW, coverH, colors)
+        this._drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, colors)
+      } else if (this.coverImage) {
         uni.downloadFile({
           url: this.coverImage,
           success: function(res) {
@@ -120,6 +129,9 @@ export default {
             self._drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, colors)
           }
         })
+      } else if (this.brandFallback) {
+        this._drawBrandCard(ctx, cardX + 20, coverY, coverW, coverH, colors)
+        this._drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, colors)
       } else {
         this._drawCoverPlaceholder(ctx, cardX + 20, coverY, coverW, coverH)
         this._drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, colors)
@@ -198,6 +210,120 @@ export default {
       ctx.fillText('暂无封面图', x + w / 2, y + h / 2)
     },
 
+    // 五维能量数据卡(方案C):综合分大字 + 五维彩色圆/分数/进度条
+    _drawEnergyCard(ctx, x, y, w, h, colors) {
+      var data = this.energyData || {}
+      var dims = data.dims || []
+
+      ctx.setFillStyle('#FFFFFF')
+      this._roundRect(ctx, x, y, w, h, 8)
+      ctx.fill()
+
+      // 顶部:左侧标题,右侧综合分
+      ctx.setFillStyle('#78716C')
+      ctx.setFontSize(13)
+      ctx.setTextAlign('left')
+      ctx.setTextBaseline('top')
+      ctx.fillText(data.label || '我们家的能量', x + 16, y + 16)
+
+      var hasOverall = data.overall !== null && data.overall !== undefined && !isNaN(data.overall)
+      if (hasOverall) {
+        ctx.setFillStyle(colors.main)
+        ctx.setFontSize(30)
+        ctx.setTextAlign('right')
+        ctx.fillText(String(data.overall), x + w - 28, y + 8)
+        ctx.setFillStyle('#78716C')
+        ctx.setFontSize(13)
+        ctx.fillText('分', x + w - 16, y + 28)
+      }
+
+      ctx.setStrokeStyle('#F5F5F4')
+      ctx.setLineWidth(1)
+      ctx.beginPath()
+      ctx.moveTo(x + 16, y + 54)
+      ctx.lineTo(x + w - 16, y + 54)
+      ctx.stroke()
+
+      // 五维柱:彩色圆 + 单字 + 分数 + 进度条
+      var count = dims.length
+      for (var i = 0; i < count; i++) {
+        var d = dims[i] || {}
+        var cx = x + 16 + ((w - 32) / count) * i + (w - 32) / count / 2
+        var color = d.color || colors.main
+        var score = Number(d.score)
+        if (isNaN(score) || score < 0) score = 0
+        if (score > 100) score = 100
+
+        ctx.setFillStyle(color)
+        ctx.beginPath()
+        ctx.arc(cx, y + 94, 17, 0, 2 * Math.PI)
+        ctx.fill()
+
+        ctx.setFillStyle('#FFFFFF')
+        ctx.setFontSize(15)
+        ctx.setTextAlign('center')
+        ctx.setTextBaseline('middle')
+        ctx.fillText(d.label || '', cx, y + 95)
+
+        ctx.setFillStyle('#1C1917')
+        ctx.setFontSize(18)
+        ctx.setTextBaseline('top')
+        ctx.fillText(String(score), cx, y + 120)
+
+        var barW = 44
+        var barH = 4
+        var barX = cx - barW / 2
+        var barY = y + 148
+        ctx.setFillStyle('#F5F5F4')
+        this._roundRect(ctx, barX, barY, barW, barH, 2)
+        ctx.fill()
+        if (score > 0) {
+          ctx.setFillStyle(color)
+          this._roundRect(ctx, barX, barY, barW * score / 100, barH, 2)
+          ctx.fill()
+        }
+      }
+
+      ctx.setFillStyle('#A8A29E')
+      ctx.setFontSize(11)
+      ctx.setTextAlign('center')
+      ctx.setTextBaseline('top')
+      ctx.fillText('五维能量 · 浠艾福家庭测评', x + w / 2, y + 172)
+    },
+
+    // 品牌渐变卡:用户未解锁全景(无五维数据)且无封面图时的降级方案
+    _drawBrandCard(ctx, x, y, w, h, colors) {
+      var g = ctx.createLinearGradient(x, y, x + w, y + h)
+      g.addColorStop(0, colors.main)
+      g.addColorStop(1, colors.dark)
+      ctx.setFillStyle(g)
+      this._roundRect(ctx, x, y, w, h, 8)
+      ctx.fill()
+
+      var dotColors = ['#FF8C42', '#6366F1', '#F59E0B', '#10B981', '#FF6B9D']
+      var centerX = x + w / 2
+      for (var i = 0; i < 5; i++) {
+        ctx.setFillStyle(dotColors[i])
+        ctx.beginPath()
+        ctx.arc(centerX + (i - 2) * 26, y + 66, 5, 0, 2 * Math.PI)
+        ctx.fill()
+      }
+
+      ctx.setFillStyle('#FFFFFF')
+      ctx.setFontSize(24)
+      ctx.setTextAlign('center')
+      ctx.setTextBaseline('top')
+      ctx.fillText('浠艾福', centerX, y + 86)
+
+      ctx.setFillStyle('rgba(255,255,255,0.85)')
+      ctx.setFontSize(13)
+      ctx.fillText('给全家的一站式幸福提案', centerX, y + 124)
+
+      ctx.setFillStyle('rgba(255,255,255,0.7)')
+      ctx.setFontSize(12)
+      ctx.fillText('测一测你家的五维能量', centerX, y + 152)
+    },
+
     _drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, colors) {
       var self = this
 

+ 12 - 0
cfc-frontend/pages.json

@@ -716,6 +716,12 @@
           "style": {
             "navigationBarTitleText": "发布商品"
           }
+        },
+        {
+          "path": "team-customers/team-customers",
+          "style": {
+            "navigationBarTitleText": "团队客户"
+          }
         }
       ]
     },
@@ -1809,6 +1815,12 @@
             "navigationBarTitleText": "服务角色申请"
           }
         },
+        {
+          "path": "team-system/team-system",
+          "style": {
+            "navigationBarTitleText": "我的团队"
+          }
+        },
         {
           "path": "message-history",
           "style": {

+ 5 - 5
cfc-frontend/pages/action-detail/index.vue

@@ -296,10 +296,10 @@ export default {
       var messages = [
         '一月:新年伊始,增进家庭沟通',
         '二月:春节团聚,珍惜亲子时光',
-        '三月:春暖花开,多陪伴孩子',
+        '三月:春暖花开,多陪伴成员',
         '四月:清明踏青,共享家庭活动',
         '五月:劳动教育,培养家庭责任感',
-        '六月:儿童节,倾听孩子心声',
+        '六月:儿童节,倾听成员心声',
         '七月:暑期陪伴,增进亲子关系',
         '八月:开学准备,共同规划',
         '九月:新学期,建立家庭规则',
@@ -442,14 +442,14 @@ export default {
           for (var i = 0; i < self.children.length; i++) {
             if (self.children[i].memberId === savedChildId) {
               self.activeChildId = savedChildId
-              self.activeChildName = self.children[i].name || self.children[i].nickname || '孩子'
+              self.activeChildName = self.children[i].name || self.children[i].nickname || '成员'
               found = true
               break
             }
           }
           if (!found && self.children.length > 0) {
             self.activeChildId = self.children[0].memberId
-            self.activeChildName = self.children[0].name || self.children[0].nickname || '孩子'
+            self.activeChildName = self.children[0].name || self.children[0].nickname || '成员'
             uni.setStorageSync('currentChildId', self.activeChildId)
           }
           if (self.activeChildId) {
@@ -458,7 +458,7 @@ export default {
         }
         self.childrenChecked = true
       }).catch(function(e) {
-        console.log('获取孩子列表失败', e)
+        console.log('获取成员列表失败', e)
         self.childrenChecked = true
       })
     },

+ 3 - 3
cfc-frontend/pages/action-detail/member-action-detail.vue

@@ -26,7 +26,7 @@
           <text class="avatar-text">{{ memberInfo.name && memberInfo.name.charAt(0) || '孩' }}</text>
         </view>
         <view class="member-info">
-          <text class="member-name">{{ memberInfo.name || '孩子' }}</text>
+          <text class="member-name">{{ memberInfo.name || '成员' }}</text>
           <text class="member-role">行动维度</text>
         </view>
         <view class="member-score" v-if="dualDimension">
@@ -140,10 +140,10 @@ export default {
       var messages = [
         '一月:新年伊始,增进家庭沟通',
         '二月:春节团聚,珍惜亲子时光',
-        '三月:春暖花开,多陪伴孩子',
+        '三月:春暖花开,多陪伴成员',
         '四月:清明踏青,共享家庭活动',
         '五月:劳动教育,培养家庭责任感',
-        '六月:儿童节,倾听孩子心声',
+        '六月:儿童节,倾听成员心声',
         '七月:暑期陪伴,增进亲子关系',
         '八月:开学准备,共同规划',
         '九月:新学期,建立家庭规则',

+ 1 - 1
cfc-frontend/pages/activity/activity-detail/activity-detail.vue

@@ -435,7 +435,7 @@ export default {
 
       var memberId = uni.getStorageSync('currentChildId') || self.registrationChildId
       if (!memberId) {
-        uni.showToast({ title: '请选择孩子身份', icon: 'none' })
+        uni.showToast({ title: '请选择家庭成员身份', icon: 'none' })
         return
       }
 

+ 2 - 2
cfc-frontend/pages/activity/index.vue

@@ -725,7 +725,7 @@ export default {
       var self = this
       var memberId = uni.getStorageSync('currentChildId')
       if (!memberId) {
-        uni.showToast({ title: '请先选择孩子身份', icon: 'none' })
+        uni.showToast({ title: '请先选择家庭成员身份', icon: 'none' })
         return
       }
       activityCheckin({ id: act.activityId || act.id, memberId: memberId }).then(function(res) {
@@ -748,7 +748,7 @@ export default {
           if (res.confirm) {
             var memberId = uni.getStorageSync('currentChildId')
             if (!memberId) {
-              uni.showToast({ title: '请先选择孩子身份', icon: 'none' })
+uni.showToast({ title: '请先选择家庭成员身份', icon: 'none' })
               return
             }
             cancelActivityRegistration({ id: act.activityId || act.id, memberId: memberId }).then(function(res) {

+ 17 - 17
cfc-frontend/pages/ai/chat.vue

@@ -45,41 +45,41 @@
               <text class="suggestion-icon">📋</text>
               <text class="suggestion-text">小明最近任务完成怎么样?</text>
             </view>
-            <view class="welcome-suggestion" v-if="!isNutrition && !isHealthCoach" @click="quickQuestion('我家孩子的能量评分怎么样?')">
+            <view class="welcome-suggestion" v-if="!isNutrition && !isHealthCoach" @click="quickQuestion('我家家庭成员的能量评分怎么样?')">
               <text class="suggestion-icon">⚡</text>
-              <text class="suggestion-text">孩子的能量评分怎么样?</text>
+              <text class="suggestion-text">家庭成员的能量评分怎么样?</text>
             </view>
             <view class="welcome-suggestion" v-if="!isNutrition && !isHealthCoach" @click="quickQuestion('今天有什么需要我做的事情?')">
               <text class="suggestion-icon">✅</text>
               <text class="suggestion-text">今天有什么需要我做的事情?</text>
             </view>
-            <view class="welcome-suggestion" v-if="isNutrition" @click="quickQuestion('根据报告,我的孩子需要补充哪些营养?')">
+            <view class="welcome-suggestion" v-if="isNutrition" @click="quickQuestion('根据报告,家庭成员需要补充哪些营养?')">
               <text class="suggestion-icon">🥦</text>
-              <text class="suggestion-text">根据报告,我的孩子需要补充哪些营养?</text>
+              <text class="suggestion-text">根据报告,家庭成员需要补充哪些营养?</text>
             </view>
             <view class="welcome-suggestion" v-if="isNutrition" @click="quickQuestion('有哪些食物或产品推荐?')">
               <text class="suggestion-icon">🛒</text>
               <text class="suggestion-text">有哪些食物或产品推荐?</text>
             </view>
-            <view class="welcome-suggestion" v-if="isHealthCoach" @click="quickQuestion('孩子挑食怎么办?')">
+            <view class="welcome-suggestion" v-if="isHealthCoach" @click="quickQuestion('家人挑食怎么办?')">
               <text class="suggestion-icon">🍽️</text>
-              <text class="suggestion-text">孩子挑食怎么办?</text>
+              <text class="suggestion-text">家人挑食怎么办?</text>
             </view>
-            <view class="welcome-suggestion" v-if="isHealthCoach" @click="quickQuestion('孩子总生病,是不是抵抗力差?')">
+            <view class="welcome-suggestion" v-if="isHealthCoach" @click="quickQuestion('家人总生病,是不是抵抗力差?')">
               <text class="suggestion-icon">🤧</text>
-              <text class="suggestion-text">孩子总生病,是不是抵抗力差?</text>
+              <text class="suggestion-text">家人总生病,是不是抵抗力差?</text>
             </view>
-            <view class="welcome-suggestion" v-if="isHealthCoach" @click="quickQuestion('怎么知道孩子是什么体质?')">
+            <view class="welcome-suggestion" v-if="isHealthCoach" @click="quickQuestion('怎么知道家人是什么体质?')">
               <text class="suggestion-icon">🔍</text>
-              <text class="suggestion-text">怎么知道孩子是什么体质?</text>
+              <text class="suggestion-text">怎么知道家人是什么体质?</text>
             </view>
-            <view class="welcome-suggestion" v-if="isButler" @click="quickQuestion('我家孩子肠道菌群怎么样?')">
+            <view class="welcome-suggestion" v-if="isButler" @click="quickQuestion('我家成员肠道菌群怎么样?')">
               <text class="suggestion-icon">🦠</text>
-              <text class="suggestion-text">我家孩子肠道菌群怎么样?</text>
+              <text class="suggestion-text">我家成员肠道菌群怎么样?</text>
             </view>
-            <view class="welcome-suggestion" v-if="isButler" @click="quickQuestion('怎么改善孩子挑食问题?')">
+            <view class="welcome-suggestion" v-if="isButler" @click="quickQuestion('怎么改善家人挑食问题?')">
               <text class="suggestion-icon">🥗</text>
-              <text class="suggestion-text">怎么改善孩子挑食问题?</text>
+              <text class="suggestion-text">怎么改善家人挑食问题?</text>
             </view>
             <view class="welcome-suggestion" v-if="isButler" @click="quickQuestion('需要补充什么营养素?')">
               <text class="suggestion-icon">💊</text>
@@ -139,7 +139,7 @@
 <!-- 输入区 -->
      <view class="chat-input-area">
        <view class="chat-input-wrap">
-         <input class="chat-input" v-model="inputText" placeholder="问我关于家庭和孩子的问题..."
+         <input class="chat-input" v-model="inputText" placeholder="问我关于家庭和成员的问题..."
            :disabled="sending" @confirm="sendMessage" confirm-type="send" />
        </view>
        <view class="chat-voice-btn" :class="{ 'voice-recording': isRecording }" @touchstart="startVoiceRecord" @touchend="stopVoiceRecord">
@@ -210,7 +210,7 @@ export default {
         return '我可以基于菌群检测报告,为你提供个性化的营养建议和推荐'
       }
       if (this.isHealthCoach) {
-        return '陪伴孩子健康成长,用提问帮你找到答案'
+        return '陪伴家庭成员健康成长,用提问帮你找到答案'
       }
       if (this.isButler) {
         return '专属健康管家,基于知识库提供个性化健康建议'
@@ -231,7 +231,7 @@ export default {
         var pageName = pageNames[this.fromPage] || this.fromPage
         return '从 ' + pageName + ' 发起,我可以帮你了解:'
       }
-      return '我可以帮你了解孩子的成长情况,比如:'
+      return '我可以帮你了解家庭成员的成长情况,比如:'
     }
   },
   onLoad: function(options) {

+ 1 - 1
cfc-frontend/pages/article-center/article-detail.vue

@@ -137,7 +137,7 @@ export default {
   onShow() {
     var memberId = uni.getStorageSync('currentChildId')
     if (!this.article || this.error) return
-    // 已登录用户(家长/孩子/规划师)均可计时,匿名用户不计时
+    // 已登录用户(家长/成员/规划师)均可计时,匿名用户不计时
     if (this.isLogin) this.startNewReadingSession()
   },
   onHide() { this.pauseReadingTimer() },

+ 1 - 1
cfc-frontend/pages/article-center/article-edit.vue

@@ -57,7 +57,7 @@
         <textarea
           class="form-textarea"
           v-model="content"
-          placeholder="记录孩子的成长点滴..."
+          placeholder="记录成员的成长点滴..."
           maxlength="2000"
         />
         <text class="char-count">{{ content.length }}/2000</text>

+ 5 - 5
cfc-frontend/pages/assessment/apply.vue

@@ -2,7 +2,7 @@
   <view class="container">
     <view class="header">
       <text class="title">申请测评</text>
-      <text class="subtitle">专业的DAN测评,了解孩子的专注力发展</text>
+      <text class="subtitle">专业的DAN测评,助力家庭成员专注力 development</text>
     </view>
 
     <!-- 成长规划师选择(可选) -->
@@ -79,9 +79,9 @@
       </view>
     </view>
 
-    <!-- 孩子选择 -->
+    <!-- 测评人选择 -->
     <view class="section">
-      <view class="section-title">选择孩子</view>
+      <view class="section-title">选择测评人</view>
       <view class="child-selector">
         <view 
           class="child-item" 
@@ -218,7 +218,7 @@ export default {
           }
         }
       } catch (e) {
-        console.error('获取孩子列表失败', e)
+        console.error('获取测评人列表失败', e)
       }
     },
 
@@ -289,7 +289,7 @@ export default {
         return
       }
       if (!this.selectedChildId) {
-        uni.showToast({ title: '请选择孩子', icon: 'none' })
+        uni.showToast({ title: '请先选择测评人', icon: 'none' })
         return
       }
 

+ 2 - 2
cfc-frontend/pages/assessment/purchase.vue

@@ -2,7 +2,7 @@
   <view class="container">
     <view class="header">
       <text class="title">选择测评任务模板</text>
-      <text class="subtitle">选择适合孩子的测评任务模板</text>
+      <text class="subtitle">选择适合测评人的测评任务模板</text>
     </view>
 
     <!-- 家庭数据区 -->
@@ -256,7 +256,7 @@ export default {
       }
       var memberId = uni.getStorageSync('currentChildId')
       if (!memberId) {
-        uni.showToast({ title: '请先选择孩子', icon: 'none' })
+        uni.showToast({ title: '请先选择测评人', icon: 'none' })
         return
       }
 

+ 1 - 1
cfc-frontend/pages/assessment/results.vue

@@ -101,7 +101,7 @@
           <view class="detail-section">
             <text class="detail-title">基本信息</text>
             <view class="detail-row">
-              <text class="detail-label">孩子姓名</text>
+              <text class="detail-label">测评人姓名</text>
               <text class="detail-value">{{ currentReport.childName || '未命名' }}</text>
             </view>
             <view class="detail-row">

+ 2 - 2
cfc-frontend/pages/assessment/upload-report.vue

@@ -6,7 +6,7 @@
     </view>
 
     <view class="section">
-      <view class="section-title">选择孩子</view>
+      <view class="section-title">选择测评人</view>
       <view class="child-selector">
         <view class="child-item"
           :class="{ selected: selectedChildId === child.id }"
@@ -70,7 +70,7 @@ export default {
           }
         }
       } catch (e) {
-        console.error('获取孩子列表失败', e)
+        console.error('获取测评人列表失败', e)
       }
     },
     chooseFile() {

+ 1 - 1
cfc-frontend/pages/body-detail/checkin.vue

@@ -192,7 +192,7 @@ export default {
         return
       }
       if (!this.memberId) {
-        uni.showToast({ title: '孩子ID无效', icon: 'none' })
+        uni.showToast({ title: '成员ID无效', icon: 'none' })
         return
       }
       var self = this

+ 28 - 6
cfc-frontend/pages/body-detail/index.vue

@@ -334,7 +334,7 @@ export default {
         '六月:夏日来临,关注肠胃健康',
         '七月:暑期到来,规律运动不可少',
         '八月:秋前调养,提升免疫力',
-        '九月:开学季,关注孩子生长发育',
+        '九月:开学季,关注家庭成员生长发育',
         '十月:国庆长假,注意饮食控制',
         '十一月:秋冬交替,保暖防寒',
         '十二月:年终总结,规划来年健康'
@@ -387,21 +387,24 @@ export default {
           for (var i = 0; i < self.children.length; i++) {
             if (self.children[i].memberId === savedChildId) {
               self.activeChildId = savedChildId
-              self.activeChildName = self.children[i].name || self.children[i].nickname || '孩子'
+              self.activeChildName = self.children[i].name || self.children[i].nickname || '成员'
               found = true
               break
             }
           }
           if (!found && self.children.length > 0) {
             self.activeChildId = self.children[0].memberId
-            self.activeChildName = self.children[0].name || self.children[0].nickname || '孩子'
+            self.activeChildName = self.children[0].name || self.children[0].nickname || '成员'
             uni.setStorageSync('currentChildId', self.activeChildId)
           }
           if (self.activeChildId) {
             self.loadDashboardData()
           }
         }
-      }).catch(function(e) { console.log('获取孩子列表失败', e) })
+      }).catch(function(e) {
+        console.log('获取成员列表失败', e)
+        self.childrenChecked = true
+      })
     },
     loadDashboardData: function() {
       if (!this.activeChildId) return
@@ -523,10 +526,29 @@ export default {
     },
     onFuncClick: function(item) {
       if (item.needLogin && !this.isLoggedIn) {
-        uni.showToast({ title: '请先登录', icon: 'none' })
+        uni.showModal({
+          title: '提示',
+          content: '请先登录后使用此功能',
+          success: function(res) {
+            if (res.confirm) uni.navigateTo({ url: '/pages/login/login' })
+          }
+        })
+        return
+      }
+      if (item.page === 'health-dimensions') {
+        if (!this.activeChildId) {
+          uni.showToast({ title: '请先选择家庭成员', icon: 'none' })
+          return
+        }
+        uni.navigateTo({ url: '/pages/body-detail/health-dimensions?memberId=' + this.activeChildId })
         return
       }
-      uni.navigateTo({ url: item.page })
+      if (item.page) uni.navigateTo({ url: item.page })
+    },
+    onChildChanged: function(child) {
+      this.activeChildId = child.memberId
+      this.activeChildName = child.name || child.nickname || '成员'
+      this.loadDashboardData()
     },
     onFamilyMemberSelect: function(memberId) {
       this.selectedMemberId = memberId

+ 1 - 1
cfc-frontend/pages/body-detail/member-body-detail.vue

@@ -14,7 +14,7 @@
         <text class="avatar-text">{{ memberInfo.name && memberInfo.name.charAt(0) || '孩' }}</text>
       </view>
       <view class="member-info">
-        <text class="member-name">{{ memberInfo.name || '孩子' }}</text>
+        <text class="member-name">{{ memberInfo.name || '成员' }}</text>
         <text class="member-role">身体维度</text>
       </view>
       <view class="member-score" v-if="dualDimension">

+ 1 - 1
cfc-frontend/pages/child/tasks.vue

@@ -240,7 +240,7 @@ export default {
         res = await getTodayTasks(currentChild.id)
         this.tasks = res.data || []
       } catch (e) {
-        console.error('加载孩子任务失败', e)
+        console.error('加载成员任务失败', e)
       } finally {
         this.loading = false
       }

+ 1 - 1
cfc-frontend/pages/cognitive-test/language-test.vue

@@ -220,7 +220,7 @@ export default {
     submitResult: function() {
       var memberId = this.memberId || uni.getStorageSync('currentChildId') || ''
       if (!memberId) {
-        uni.showToast({ title: '请先选择孩子', icon: 'none' })
+        uni.showToast({ title: '请先选择家庭成员', icon: 'none' })
         return
       }
       submitCognitiveSelfTest(memberId, 'languageScore', this.score)

+ 1 - 1
cfc-frontend/pages/cognitive-test/perception-test.vue

@@ -219,7 +219,7 @@ export default {
     submitResult: function() {
       var memberId = this.memberId || uni.getStorageSync('currentChildId') || ''
       if (!memberId) {
-        uni.showToast({ title: '请先选择孩子', icon: 'none' })
+        uni.showToast({ title: '请先选择家庭成员', icon: 'none' })
         return
       }
       submitCognitiveSelfTest(memberId, 'perceptionScore', this.score)

+ 1 - 1
cfc-frontend/pages/cognitive-test/speed-test.vue

@@ -298,7 +298,7 @@ export default {
       if (this.submitting) return
       var memberId = this.memberId || uni.getStorageSync('currentChildId') || ''
       if (!memberId) {
-        uni.showToast({ title: '请先选择孩子', icon: 'none' })
+        uni.showToast({ title: '请先选择家庭成员', icon: 'none' })
         return
       }
       this.submitting = true

+ 1 - 1
cfc-frontend/pages/games/1a2b.vue

@@ -100,7 +100,7 @@ export default {
           }
         }
       } catch (e) {
-        console.error('获取孩子ID失败', e)
+        console.error('获取成员ID失败', e)
       }
     },
     generateAnswer() {

+ 1 - 1
cfc-frontend/pages/games/schulte.vue

@@ -92,7 +92,7 @@ export default {
         }
       }
     } catch (e) {
-      console.error('获取孩子ID失败', e)
+      console.error('获取成员ID失败', e)
     }
   },
     startGame() {

+ 1 - 1
cfc-frontend/pages/games/sudoku.vue

@@ -204,7 +204,7 @@ export default {
           }
         }
       } catch (e) {
-        console.error('获取孩子ID失败', e)
+        console.error('获取成员ID失败', e)
       }
     },
     isCellSelected(row, col) {

+ 4 - 4
cfc-frontend/pages/growth/create.vue

@@ -9,7 +9,7 @@
         <view class="source-card" :class="sourceType === 'manual' ? 'source-active' : ''" @click="sourceType = 'manual'">
           <text class="source-icon">📝</text>
           <text class="source-name">手动创建</text>
-          <text class="source-desc">填写孩子基本信息</text>
+          <text class="source-desc">填写成员基本信息</text>
         </view>
         <view class="source-card" :class="sourceType === 'upload' ? 'source-active' : ''" @click="sourceType = 'upload'">
           <text class="source-icon">📄</text>
@@ -25,10 +25,10 @@
 
       <view class="form" v-if="sourceType === 'manual'">
       <view class="form-item">
-        <text class="label">孩子</text>
+        <text class="label">成员</text>
         <view class="child-select" @click="selectChild">
           <text class="select-text" v-if="selectedChild">{{ selectedChild.nickname || selectedChild.name }}</text>
-          <text class="select-placeholder" v-else>请选择孩子</text>
+          <text class="select-placeholder" v-else>请选择成员</text>
           <text class="select-arrow">›</text>
         </view>
       </view>
@@ -189,7 +189,7 @@ export default {
     },
     async submitManual() {
       if (!this.formData.memberId) {
-        uni.showToast({ title: '请选择孩子', icon: 'none' })
+        uni.showToast({ title: '请选择成员', icon: 'none' })
         return
       }
 

+ 1 - 1
cfc-frontend/pages/growth/diet-checkin.vue

@@ -338,7 +338,7 @@ export default {
       var hasPhoto = self.form.photos && self.form.photos.length > 0
       var hasVoice = self.form.voicePath || (self.form.voiceText && self.form.voiceText.trim())
       if (!hasText && !hasPhoto && !hasVoice) { uni.showToast({ title: '请填写内容或添加图片/语音', icon: 'none' }); return }
-      if (!self.memberId) { uni.showToast({ title: '孩子ID无效', icon: 'none' }); return }
+      if (!self.memberId) { uni.showToast({ title: '成员ID无效', icon: 'none' }); return }
       self.submitting = true
       try {
         var photoUrls = []

+ 1 - 1
cfc-frontend/pages/growth/exercise-checkin.vue

@@ -425,7 +425,7 @@ export default {
       var hasPhoto = self.form.photos && self.form.photos.length > 0
       var hasVoice = self.form.voicePath || (self.form.voiceText && self.form.voiceText.trim())
       if (!hasDuration && !hasPhoto && !hasVoice) { uni.showToast({ title: '请填写运动时长或添加图片/语音', icon: 'none' }); return }
-      if (!self.memberId) { uni.showToast({ title: '孩子ID无效', icon: 'none' }); return }
+      if (!self.memberId) { uni.showToast({ title: '成员ID无效', icon: 'none' }); return }
       self.submitting = true
       try {
         var photoUrls = []

+ 1 - 1
cfc-frontend/pages/growth/sleep-checkin.vue

@@ -151,7 +151,7 @@ export default {
       if (!self.form.sleepTime) { uni.showToast({ title: '请选择入睡时间', icon: 'none' }); return }
       if (!self.form.wakeTime) { uni.showToast({ title: '请选择醒来时间', icon: 'none' }); return }
       if (self.form.sleepTime === self.form.wakeTime) { uni.showToast({ title: '入睡和醒来时间不能相同', icon: 'none' }); return }
-      if (!self.memberId) { uni.showToast({ title: '孩子ID无效', icon: 'none' }); return }
+      if (!self.memberId) { uni.showToast({ title: '成员ID无效', icon: 'none' }); return }
       self.submitting = true
       try {
         var sleepParts = self.form.sleepTime.split(':')

+ 2 - 2
cfc-frontend/pages/growth/upload.vue

@@ -7,10 +7,10 @@
 
     <view class="form" v-if="step === 1">
       <view class="form-item">
-        <text class="label">孩子</text>
+        <text class="label">成员</text>
         <view class="child-select" @click="selectChild">
           <text class="select-text" v-if="selectedChild">{{ selectedChild.nickname || selectedChild.name }}</text>
-          <text class="select-placeholder" v-else>请选择孩子</text>
+          <text class="select-placeholder" v-else>请选择成员</text>
           <text class="select-arrow">›</text>
         </view>
       </view>

+ 3 - 3
cfc-frontend/pages/health-main/index.vue

@@ -6,7 +6,7 @@
       <!-- 痛点卡 -->
       <view class="pain-card">
         <text class="pain-quote">指标正常 ≠ 健康</text>
-        <text class="pain-desc">孩子反复生病,你扛了多少次了?</text>
+        <text class="pain-desc">家人反复生病,你扛了多少次了?</text>
       </view>
       <!-- 自测入口 -->
       <view class="self-test-card" @click="goSelfTest">
@@ -97,7 +97,7 @@
             <text class="wc-icon">⚡</text>
             <view class="wc-info">
               <text class="wc-name">智 · 赋能未来</text>
-              <text class="wc-text">一次测评,看见孩子真实的专注与潜能</text>
+              <text class="wc-text">一次测评,看见成员真实的专注与潜能</text>
             </view>
           </view>
           <view class="wc-card wc-wealth" @click="goPage('/pages/wealth/index')">
@@ -587,7 +587,7 @@ export default {
             self.relationshipEmpty = true
           }
         }).catch(function() { self.relationshipEmpty = true })
-        // 财富子维度:沙盘里家长用 userId,孩子用 family_members.id,不能混用 currentMemberId
+        // 财富子维度:沙盘里家长用 userId,成员用 family_members.id,不能混用 currentMemberId
         var childId = uni.getStorageSync('currentChildId')
         var wealthMemberId = childId || uni.getStorageSync('userId')
         var wealthMemberType = childId ? 'child' : 'parent'

+ 4 - 4
cfc-frontend/pages/health/challenge-manage.vue

@@ -307,7 +307,7 @@
       </view>
     </view>
 
-    <!-- 免费会员引导添加孩子弹窗 -->
+    <!-- 免费会员引导添加成员弹窗 -->
     <view class="overlay" v-if="showAddChildPrompt" @click="showAddChildPrompt = false">
       <view class="modal prompt-modal" @click.stop>
         <view class="modal-header">
@@ -316,12 +316,12 @@
         </view>
         <view class="prompt-content">
           <text class="prompt-icon">👶</text>
-          <text class="prompt-title">需先添加孩子成员</text>
-          <text class="prompt-desc">免费会员只能添加 1 位孩子成员,添加后即可创建家庭挑战。<br/>挑战创建后,可切换到孩子身份同意参与。</text>
+          <text class="prompt-title">需先添加成员</text>
+          <text class="prompt-desc">免费会员只能添加 1 位成员成员,添加后即可创建家庭挑战。<br/>挑战创建后,可切换到成员身份同意参与。</text>
         </view>
         <view class="modal-footer">
           <button class="btn-cancel" @click="showAddChildPrompt = false">暂不添加</button>
-          <button class="btn-submit" @click="goToAddChild">去添加孩子</button>
+          <button class="btn-submit" @click="goToAddMember">去添加成员</button>
         </view>
       </view>
     </view>

+ 1 - 1
cfc-frontend/pages/health/daily-checkin.vue

@@ -147,7 +147,7 @@ export default {
     doCheckin: function() {
       var self = this
       if (!self.memberId) {
-        uni.showToast({ title: '孩子ID无效', icon: 'none' })
+        uni.showToast({ title: '成员ID无效', icon: 'none' })
         return
       }
       self.submitting = true

+ 1 - 1
cfc-frontend/pages/health/diet-index.vue

@@ -196,7 +196,7 @@ export default {
         return
       }
       if (!self.memberId) {
-        uni.showToast({ title: '孩子ID无效', icon: 'none' })
+        uni.showToast({ title: '成员ID无效', icon: 'none' })
         return
       }
       self.submitting = true

+ 1 - 1
cfc-frontend/pages/health/exercise-index.vue

@@ -210,7 +210,7 @@ export default {
         return
       }
       if (!self.memberId) {
-        uni.showToast({ title: '孩子ID无效', icon: 'none' })
+        uni.showToast({ title: '成员ID无效', icon: 'none' })
         return
       }
       self.submitting = true

+ 1 - 1
cfc-frontend/pages/health/gut-index.vue

@@ -133,7 +133,7 @@ export default {
         memberId = userId
       }
       if (!memberId) {
-        uni.showToast({ title: '未找到孩子信息', icon: 'none' })
+        uni.showToast({ title: '未找到成员信息', icon: 'none' })
         return
       }
       healthRequest('/api/health/report/latest', { memberId: memberId }).then(function(res) {

+ 1 - 4
cfc-frontend/pages/health/health-plan-summary.vue

@@ -25,7 +25,7 @@
       />
       <view class="section-title" style="margin-top:20rpx">健康目标</view>
       <view class="hint-text">描述您希望改善的方向,AI将为您定制方案</view>
-      <textarea class="goal-textarea" v-model="userGoal" placeholder="例如:孩子最近睡眠不好,希望改善睡眠质量,同时提升免疫力..."
+      <textarea class="goal-textarea" v-model="userGoal" placeholder="例如:成员最近睡眠不好,希望改善睡眠质量,同时提升免疫力..."
                 :maxlength="300" auto-height :disabled="loading" @input="onGoalInput"></textarea>
       <view class="goal-footer">
         <text class="goal-count">{{ goalLen }}/300</text>
@@ -232,9 +232,6 @@
 
     <!-- 查看模式 -->
     <view class="section plan-view-section" v-if="viewMode && planDetail">
-      <view class="section-header">
-        <text class="section-title">📊 健康方案</text>
-      </view>
       <view class="plan-meta">
         <text class="plan-meta-item">目标:{{ planDetail.goal || '—' }}</text>
         <text class="plan-meta-item" v-if="planDetail.memberName">成员:{{ planDetail.memberName }}</text>

+ 1 - 1
cfc-frontend/pages/health/meditation-index.vue

@@ -87,7 +87,7 @@ export default {
       getChildren().then(function(res) {
         if (res.code === 200 && res.data && res.data.length > 0) {
           self.memberIds = res.data.map(function(c) { return c.memberId })
-          self.memberOptions = ['我自己'].concat(res.data.map(function(c) { return c.name || c.nickname || '孩子' }))
+          self.memberOptions = ['我自己'].concat(res.data.map(function(c) { return c.name || c.nickname || '成员' }))
         }
       }).catch(function() {})
     },

+ 6 - 22
cfc-frontend/pages/health/report-list.vue

@@ -68,6 +68,10 @@
               <text class="report-arrow">›</text>
             </view>
           </view>
+          <!-- 仅在 subjectId 为空(无归属)时显示调整按钮 -->
+          <view v-if="!item.subjectId" class="action-btns">
+            <button type="primary" size="small" @click.stop="adjustBinding(item)">调整归属</button>
+          </view>
         </view>
       </view>
     </view>
@@ -86,7 +90,7 @@
 </template>
 
 <script>
-import { getReportList, deleteReport, getFamilyMemberList, getCollectingReports } from '../../utils/api.js'
+import { getReportList, deleteReport, getFamilyMemberList, getCollectingReports, reassignReport } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 
 export default {
@@ -291,27 +295,7 @@ export default {
         uni.showToast({ title: '删除失败', icon: 'none' })
       })
     },
-    formatDate(dateStr) {
-      if (!dateStr) return '--'
-      var d = parseDate(dateStr)
-      if (!d) return dateStr
-      var y = d.getFullYear()
-      var m = ('' + (d.getMonth() + 1)).padStart(2, '0')
-      var day = ('' + d.getDate()).padStart(2, '0')
-      return y + '-' + m + '-' + day
-    },
-    formatDateTime(dateStr) {
-      if (!dateStr) return '--'
-      var d = parseDate(dateStr)
-      if (!d) return dateStr
-      var y = d.getFullYear()
-      var m = ('' + (d.getMonth() + 1)).padStart(2, '0')
-      var day = ('' + d.getDate()).padStart(2, '0')
-      var h = ('' + d.getHours()).padStart(2, '0')
-      var min = ('' + d.getMinutes()).padStart(2, '0')
-      return y + '-' + m + '-' + day + ' ' + h + ':' + min
-    }
-  }
+  },
 }
 </script>
 

+ 11 - 11
cfc-frontend/pages/health/report-survey.vue

@@ -10,7 +10,7 @@
     <!-- 提示说明 -->
     <view class="tip-banner">
       <text class="tip-icon">💡</text>
-      <text class="tip-text">请根据孩子和家庭的实际情况填写,帮助AI更准确地解读报告</text>
+      <text class="tip-text">请根据成员和家庭的实际情况填写,帮助AI更准确地解读报告</text>
     </view>
 
     <!-- 表单 -->
@@ -25,7 +25,7 @@
       <!-- Q1: 饮食结构 -->
       <view class="question-card">
         <text class="q-number">1/15</text>
-        <text class="q-title">孩子平时的饮食结构是怎样的?</text>
+        <text class="q-title">成员平时的饮食结构是怎样的?</text>
         <view class="q-options">
           <view class="q-option" :class="{ 'q-selected': survey.diet === 'balanced' }" @click="survey.diet = 'balanced'">
             <text class="q-icon">🥗</text>
@@ -49,7 +49,7 @@
       <!-- Q1b: 绝对忌口(必填) -->
       <view class="question-card">
         <text class="q-number">1b/15</text>
-        <text class="q-title">孩子有哪些绝对不能吃/喝的食物?(必填,含过敏原)</text>
+        <text class="q-title">成员有哪些绝对不能吃/喝的食物?(必填,含过敏原)</text>
         <view class="q-options multi">
           <view class="q-option" :class="{ 'q-selected': survey.absoluteAvoid.indexOf('牛奶') !== -1 }" @click="toggleAbsAvoid('牛奶')">
             <text class="q-text">牛奶/乳制品</text>
@@ -125,7 +125,7 @@
       <!-- Q2: 消化状况 -->
       <view class="question-card">
         <text class="q-number">2/15</text>
-        <text class="q-title">孩子近期消化状况如何?</text>
+        <text class="q-title">成员近期消化状况如何?</text>
         <view class="q-options">
           <view class="q-option" :class="{ 'q-selected': survey.digestion === 'good' }" @click="survey.digestion = 'good'">
             <text>😊</text>
@@ -149,7 +149,7 @@
       <!-- Q3: 睡眠质量 -->
       <view class="question-card">
         <text class="q-number">3/15</text>
-        <text class="q-title">孩子近期的睡眠质量如何?</text>
+        <text class="q-title">成员近期的睡眠质量如何?</text>
         <view class="q-options">
           <view class="q-option" :class="{ 'q-selected': survey.sleep === 'good' }" @click="survey.sleep = 'good'">
             <text>😴</text>
@@ -173,7 +173,7 @@
       <!-- Q4: 运动习惯 -->
       <view class="question-card">
         <text class="q-number">4/15</text>
-        <text class="q-title">孩子每周运动情况?</text>
+        <text class="q-title">成员每周运动情况?</text>
         <view class="q-options">
           <view class="q-option" :class="{ 'q-selected': survey.exercise === 'frequent' }" @click="survey.exercise = 'frequent'">
             <text>🏃</text>
@@ -193,7 +193,7 @@
       <!-- Q5: 饮水习惯 -->
       <view class="question-card">
         <text class="q-number">5/15</text>
-        <text class="q-title">孩子每日饮水量如何?</text>
+        <text class="q-title">成员每日饮水量如何?</text>
         <view class="q-options">
           <view class="q-option" :class="{ 'q-selected': survey.water === 'enough' }" @click="survey.water = 'enough'">
             <text>💧</text>
@@ -213,7 +213,7 @@
       <!-- Q6: 用药情况 -->
       <view class="question-card">
         <text class="q-number">6/15</text>
-        <text class="q-title">孩子近期是否有以下用药情况?(可多选)</text>
+        <text class="q-title">成员近期是否有以下用药情况?(可多选)</text>
         <view class="q-options multi">
           <view class="q-option" :class="{ 'q-selected': hasMedication('antibiotic') }" @click="toggleMedication('antibiotic')">
             <text class="q-text">抗生素(近3个月内使用过)</text>
@@ -242,7 +242,7 @@
       <!-- Q7: 慢性病/系统性疾病 -->
       <view class="question-card">
         <text class="q-number">7/15</text>
-        <text class="q-title">孩子是否有以下慢性或系统性疾病?(可多选)</text>
+        <text class="q-title">成员是否有以下慢性或系统性疾病?(可多选)</text>
         <view class="q-options multi">
           <view class="q-option" :class="{ 'q-selected': hasChronic('none') }" @click="toggleChronic('none')">
             <text class="q-text">无已知疾病</text>
@@ -279,7 +279,7 @@
       <!-- Q8: 过敏史 -->
       <view class="question-card">
         <text class="q-number">8/15</text>
-        <text class="q-title">孩子是否有以下过敏情况?(可多选)</text>
+        <text class="q-title">成员是否有以下过敏情况?(可多选)</text>
         <view class="q-options multi">
           <view class="q-option" :class="{ 'q-selected': survey.allergies.indexOf('none') !== -1 }" @click="toggleAllergy('none')">
             <text class="q-text">无明显过敏</text>
@@ -304,7 +304,7 @@
       <!-- Q9: 手术/住院史 -->
       <view class="question-card">
         <text class="q-number">9/15</text>
-        <text class="q-title">孩子是否有手术或住院史?</text>
+        <text class="q-title">成员是否有手术或住院史?</text>
         <view class="q-options">
           <view class="q-option" :class="{ 'q-selected': survey.surgery === 'none' }" @click="survey.surgery = 'none'">
             <text class="q-text">无</text>

+ 2 - 2
cfc-frontend/pages/health/sleep-index.vue

@@ -61,7 +61,7 @@
             </view>
             <view class="info-item">
               <text class="info-age">过渡期调整</text>
-              <text class="info-desc">节假日前后逐渐调整作息,每次调整 15-30 分钟,让孩子平稳过渡,避免睡眠紊乱。</text>
+              <text class="info-desc">节假日前后逐渐调整作息,每次调整 15-30 分钟,让成员平稳过渡,避免睡眠紊乱。</text>
             </view>
           </view>
         </view>
@@ -196,7 +196,7 @@ export default {
         return
       }
       if (!self.memberId) {
-        uni.showToast({ title: '孩子ID无效', icon: 'none' })
+        uni.showToast({ title: '成员ID无效', icon: 'none' })
         return
       }
       self.submitting = true

+ 1 - 1
cfc-frontend/pages/health/water-index.vue

@@ -166,7 +166,7 @@ export default {
         return
       }
       if (!self.memberId) {
-        uni.showToast({ title: '孩子ID无效', icon: 'none' })
+        uni.showToast({ title: '成员ID无效', icon: 'none' })
         return
       }
       self.submitting = true

+ 1 - 1
cfc-frontend/pages/home-pages/components/MicroActionCard.vue

@@ -71,7 +71,7 @@ export default {
     },
     doComplete: function() {
       if (!this.memberId) {
-        uni.showToast({ title: '请先选择孩子', icon: 'none' })
+        uni.showToast({ title: '请先选择家庭成员', icon: 'none' })
         return
       }
       if (!this.action) return

+ 1 - 1
cfc-frontend/pages/home-pages/member-home-detail.vue

@@ -190,7 +190,7 @@ export default {
             }
           }
         }).catch(function(e) {
-          console.log('获取孩子列表失败', e)
+          console.log('获取成员列表失败', e)
         })
       }
 

+ 0 - 2
cfc-frontend/pages/index-home/index.vue

@@ -1134,8 +1134,6 @@ title: '添加家庭成员',
         }
       }).catch(function() {})
     },
-    }).catch(function() {})
-    },
     // ===== 能量/积分/CF值变更通知 =====
     loadEnergyPointsCFChanges: function() {
       var self = this

+ 1 - 1
cfc-frontend/pages/member-detail/member-detail.vue

@@ -177,7 +177,7 @@ export default {
       // 回退:根据 memberType 推断
       if (this.memberType === 'parent') return '家长'
       if (this.memberType === 'elderly') return '长辈'
-      return '孩子'
+      return '成员'
     },
     overallScore: function() {
       if (!this.memberInfo) return null

+ 1 - 1
cfc-frontend/pages/membership/index.vue

@@ -93,7 +93,7 @@ export default {
       features: ['折扣购物', '家庭管理', '推荐佣金', '专属活动', '任务特权', '成长报告'],
       compareData: [
         { name: '商品折扣', free: '普通价格', basic: '会员折扣', family: '会员折扣', premium: '会员折扣' },
-        { name: '家庭管理', free: '基础功能', basic: '3个孩子', family: '10个孩子', premium: '20个孩子' },
+        { name: '家庭管理', free: '基础功能', basic: '3位成员', family: '10位成员', premium: '20位成员' },
         { name: 'AI健康顾问', free: '—', basic: '3次/月', family: '不限', premium: '不限' },
         { name: '报告解读', free: '—', basic: '1次/月', family: '不限', premium: '深度解读' },
         { name: '测评预约', free: '仅查看', basic: '1次/月', family: '不限', premium: '优先' },

+ 1 - 1
cfc-frontend/pages/membership/upgrade.vue

@@ -114,7 +114,7 @@ export default {
       currentLevelName: '免费用户',
       compareData: [
         { name: '商品折扣', free: '普通价格', basic: '会员折扣', family: '会员折扣', premium: '会员折扣', lifetime: '会员折扣' },
-        { name: '家庭管理', free: '基础功能', basic: '3个孩子', family: '10个孩子', premium: '20个孩子', lifetime: '全部功能+专属管家' },
+        { name: '家庭管理', free: '基础功能', basic: '3位成员', family: '10位成员', premium: '20位成员', lifetime: '全部功能+专属管家' },
         { name: 'AI健康顾问', free: '—', basic: '3次/月', family: '不限', premium: '不限', lifetime: '不限' },
         { name: '报告解读', free: '—', basic: '1次/月', family: '每周+深度', premium: '每日+深度+专家解读', lifetime: '实时+专家深度解读' },
         { name: '健康报告上传', free: '—', basic: '2份/月', family: '不限', premium: '不限', lifetime: '不限' },

+ 2 - 2
cfc-frontend/pages/mind-detail/index.vue

@@ -332,7 +332,7 @@ components: { TabTransition, PageBanner, RadarChart, FamilyEnergyBar, PsychCrisi
         '六月:中考高考季,减压情绪管理',
         '七月:暑期到来,培养情绪调节能力',
         '八月:夏末秋初,回顾情绪成长',
-        '九月:开学季,关注孩子心理适应',
+        '九月:开学季,关注家庭成员心理适应',
         '十月:国庆假期,增进亲子沟通',
         '十一月:秋冬交替,预防情绪低落',
         '十二月:年终总结,规划心理成长'
@@ -824,7 +824,7 @@ var descList = descMap[this.dominantElement] || descMap.wood
           }
         }
       }).catch(function(e) {
-        console.log('获取孩子列表失败', e)
+        console.log('获取成员列表失败', e)
       })
     },
     loadDimensionData: function() {

+ 1 - 1
cfc-frontend/pages/mind-detail/member-mind-detail.vue

@@ -14,7 +14,7 @@
         <text class="avatar-text">{{ memberInfo.name && memberInfo.name.charAt(0) || '孩' }}</text>
       </view>
       <view class="member-info">
-        <text class="member-name">{{ memberInfo.name || '孩子' }}</text>
+        <text class="member-name">{{ memberInfo.name || '成员' }}</text>
         <text class="member-role">心智维度</text>
       </view>
       <view class="member-score" v-if="dualDimension">

+ 4 - 4
cfc-frontend/pages/parent/child-detail.vue

@@ -135,7 +135,7 @@ export default {
   },
   onLoad(options) {
     this.memberId = options.memberId || ''
-    this.childName = options.childName ? decodeURIComponent(options.childName) : '孩子'
+    this.childName = options.childName ? decodeURIComponent(options.childName) : '成员'
     this.loadChildData()
   },
   methods: {
@@ -148,8 +148,8 @@ export default {
         const matched = children.find(c => String(c.id) === String(this.memberId))
         if (matched) {
           this.childInfo = matched
-          if (!this.childName || this.childName === '孩子') {
-            this.childName = matched.nickname || matched.name || '孩子'
+if (!this.childName || this.childName === '成员') {
+    this.childName = matched.nickname || matched.name || '成员'
           }
         }
 
@@ -172,7 +172,7 @@ export default {
         const allWishes = wishRes.data || []
         this.pendingWishes = allWishes.filter(w => String(w.memberId) === String(this.memberId)).slice(0, 5)
       } catch (e) {
-        console.error('加载孩子详情失败', e)
+        console.error('加载成员详情失败', e)
         uni.showToast({ title: '加载失败', icon: 'none' })
       } finally {
         this.loading = false

+ 1 - 1
cfc-frontend/pages/parent/plans/detail.vue

@@ -20,7 +20,7 @@
       </view>
 
       <view class="info-card">
-        <text class="info-label">孩子</text>
+        <text class="info-label">成员</text>
         <text class="info-value">{{ plan.childName || '--' }}</text>
       </view>
 

+ 1 - 1
cfc-frontend/pages/parent/plans/index.vue

@@ -28,7 +28,7 @@
         </view>
         <text class="plan-desc" v-if="plan.description">{{ plan.description }}</text>
         <view class="card-meta">
-          <text class="meta-item">孩子:{{ plan.childName || '--' }}</text>
+          <text class="meta-item">成员:{{ plan.childName || '--' }}</text>
           <text class="meta-item">创建时间:{{ plan.createTime || '--' }}</text>
         </view>
       </view>

+ 2 - 2
cfc-frontend/pages/parent/wishes/index.vue

@@ -183,7 +183,7 @@ export default {
   onLoad(options) {
     if (options.memberId) {
       this.memberId = options.memberId
-      this.childName = options.childName || '该孩子'
+      this.childName = options.childName || '该成员'
     }
   },
   methods: {
@@ -193,7 +193,7 @@ export default {
         const pendingRes = await getPendingWishes()
         let wishes = pendingRes.data || []
 
-        // 如果指定了 memberId,按孩子过滤
+        // 如果指定了 memberId,按成员过滤
         if (this.memberId) {
           wishes = wishes.filter(w => String(w.memberId) === String(this.memberId))
         }

+ 1 - 1
cfc-frontend/pages/profile-extra/children.vue

@@ -3,7 +3,7 @@
     <view class="redirect-card">
       <text class="redirect-icon">👪</text>
       <text class="redirect-title">已将孩子管理整合到家庭成员</text>
-      <text class="redirect-desc">孩子的信息请在「家庭成员」中添加和管理</text>
+      <text class="redirect-desc">成员信息请在「家庭成员」中添加和管理</text>
       <button class="redirect-btn" @click="goToFamilyMembers">前往家庭成员</button>
     </view>
   </view>

+ 2 - 2
cfc-frontend/pages/profile-extra/create-child.vue

@@ -2,8 +2,8 @@
   <view class="redirect-container">
     <view class="redirect-card">
       <text class="redirect-icon">👪</text>
-      <text class="redirect-title">请通过「家庭成员」添加孩子</text>
-      <text class="redirect-desc">现在孩子的信息统一在家庭成员中管理,添加时填写出生日期即可自动识别为孩子</text>
+      <text class="redirect-title">请通过「家庭成员」添加成员</text>
+      <text class="redirect-desc">现在成员信息统一在家庭成员中管理,添加时填写出生日期即可</text>
       <button class="redirect-btn" @click="goToFamilyMembers">前往家庭成员</button>
     </view>
   </view>

+ 1 - 1
cfc-frontend/pages/profile-extra/edit-child.vue

@@ -3,7 +3,7 @@
     <view class="redirect-card">
       <text class="redirect-icon">👪</text>
       <text class="redirect-title">请在「家庭成员」中编辑信息</text>
-      <text class="redirect-desc">孩子的信息编辑已整合到家庭成员管理中</text>
+      <text class="redirect-desc">成员信息编辑已整合到家庭成员管理中</text>
       <button class="redirect-btn" @click="goToFamilyMembers">前往家庭成员</button>
     </view>
   </view>

+ 166 - 235
cfc-frontend/pages/profile-main/profile.vue

@@ -57,9 +57,20 @@
         </view>
       </view>
 
-      <!-- 我的推广码浮动按钮 -->
+      <!-- 我的分享海报浮动按钮(纯 CSS 二维码图标,避免 emoji 渲染成计算器) -->
       <view class="share-qr-float" @click="showShareQr">
-        <text class="share-qr-icon">📱</text>
+        <view class="qr-glyph">
+          <view class="qr-eye qr-eye-tl"></view>
+          <view class="qr-eye qr-eye-tr"></view>
+          <view class="qr-eye qr-eye-bl"></view>
+          <view class="qr-mid"></view>
+          <view class="qr-cluster">
+            <view class="qr-pip"></view>
+            <view class="qr-pip"></view>
+            <view class="qr-pip"></view>
+            <view class="qr-pip"></view>
+          </view>
+        </view>
       </view>
 
       <!-- 家族成员速览 -->
@@ -265,43 +276,26 @@
       </view>
 
       </template>
-    <view class="share-qr-modal" v-if="showShareQrModal" @click="closeShareQrModal">
-      <view class="modal-mask" @click.stop />
-      <view class="modal-qr" @click.stop>
-        <view class="modal-qr-header">
-          <text class="modal-qr-title">我的分享海报</text>
-          <text class="modal-qr-close" @click="closeShareQrModal">✕</text>
-        </view>
-        <view class="modal-qr-content">
-          <view v-if="qrLoading" class="qrcode-loading">
-            <text class="loading-spinner">⏳</text>
-            <text class="loading-text">生成中...</text>
-          </view>
-          <view v-else="!qrBase64" class="qrcode-empty">
-            <text>暂无二维码</text>
-          </view>
-          <view v-else class="qrcode-image">
-            <image class="qrcode-img" :src="qrBase64" mode="aspectFit"></image>
-          </view>
-          <view class="qrcode-info">
-            <text class="qrcode-text">邀请码: {{ referralCode }}</text>
-            <text class="qrcode-desc">扫码注册,好友成为您的推荐人</text>
-          </view>
-        </view>
-        <view class="modal-qr-actions">
-          <view class="action-btn share-btn" @click="shareToWechat">
-            <text class="share-icon">📤</text>
-            <text class="share-text">分享给朋友</text>
-          </view>
-        </view>
-      </view>
-    </view>
+    <!-- 我的分享海报:复用文章/活动/商品同一套 ContentSharePoster 组件(canvas 绘制 + 保存到相册) -->
+    <ContentSharePoster
+      :show="showSharePoster"
+      :title="shareHeadline"
+      :energyData="posterEnergyData"
+      :brandFallback="true"
+      :qrCodeBase64="posterQrCode || ''"
+      typeLabel="邀友加入"
+      :summary="posterSummary"
+      ctaText="长按识别二维码 · 加入浠艾福"
+      :inviteText="posterInviteText"
+      @close="closeSharePoster"
+    />
     <FloatingAvatar />
   </view>
 </template>
 
 <script>
 import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
+import ContentSharePoster from '../../components/ContentSharePoster.vue'
 import {
   getChildren, getFamilyMemberList,
   getTodayTasksByCategory, getChallengeList, getMyMembership,
@@ -309,11 +303,11 @@ import {
   cartCount, getOrderCounts, getReferralSummary,
   getProfileStats, getMyReferrer, getTeamConsumption,
   getTeacherInfo, getTeacherPendingTasks, getTeacherUnreadCount,
-  getReferralCode, extractReferralCode, getShareQrCode
+  getReferralCode, extractReferralCode, getShareQrCode, getEnergySandbox
 } from '../../utils/api.js'
 
 export default {
-  components: { FloatingAvatar },
+  components: { FloatingAvatar, ContentSharePoster },
   data() {
     return {
       nickname: '',
@@ -348,10 +342,10 @@ export default {
       profileProgress: 0,
       myReferrer: null,
       // 分享推广码
-      showShareQrModal: false,
+      showSharePoster: false,
       referralCode: '',
-      qrBase64: '',
-      qrLoading: false,
+      posterQrCode: '',
+      posterEnergyData: null,
       referralCodeLoaded: false,
     }
   },
@@ -360,7 +354,7 @@ export default {
       return !!uni.getStorageSync('token')
     },
     roleLabel() {
-      var map = { parent: '', child: '孩子', teacher: '规划师' }
+      var map = { parent: '', child: '成员', teacher: '规划师' }
       return map[this.role] || '用户'
     },
     memberLevelText() {
@@ -374,7 +368,23 @@ export default {
       return this.memberLevel === 'FREE' || this.memberLevel === 'FAMILY'
     },
     upgradeBtnText() {
-      return this.memberLevel === 'FAMILY' ? '升级' : '立即开通'
+      return this.hasUpgradeSpace ? '升级' : '立即开通'
+    },
+    // 分享海报标题:XXX邀请你关注家庭健康(昵称过长自动截断)
+    shareHeadline() {
+      var name = (this.nickname || '').trim()
+      if (!name) return '邀请你关注家庭健康'
+      if (name.length > 8) name = name.substring(0, 8) + '…'
+      return name + '邀请你关注家庭健康'
+    },
+    posterSummary() {
+      return '身体 · 心理 · 智慧 · 行动 · 财富,一家专注全家健康的成长平台'
+    },
+    posterInviteText() {
+      var name = (this.nickname || '').trim()
+      if (!name) return '扫码加入浠艾福,一起照顾好这个家'
+      if (name.length > 8) name = name.substring(0, 8) + '…'
+      return '「' + name + '」邀请你加入浠艾福'
     }
   },
   onShow() {
@@ -547,72 +557,96 @@ export default {
         console.error('获取推荐人失败', e)
       }
     },
-    // ===== 分享推广码 =====
-    showShareQr() {
-      this.showShareQrModal = true
-      if (!this.referralCodeLoaded) {
-        this.loadReferralCode()
-      } else if (this.referralCode && !this.qrBase64) {
-        this.generateQrcode()
+    // ===== 分享海报(复用 ContentSharePoster,与文章/活动/商品同一套生成逻辑)=====
+    async showShareQr() {
+      uni.showLoading({ title: '生成海报中...' })
+      try {
+        if (!this.referralCodeLoaded) {
+          await this.loadReferralCode()
+        }
+        var scene = 'ref=' + (this.referralCode || '')
+        if (scene.length > 32) {
+          scene = scene.substring(0, 32)
+        }
+        // 首页路径,扫码后进入首页并绑定推荐人
+        // 注意:微信 getwxacodeunlimit 的 page 不能带前导斜杠,否则报 errcode 41030 invalid page rid
+        var res = await getShareQrCode('pages/index-home/index', scene)
+        if (res && res.code === 200 && res.data && res.data.qrCodeBase64) {
+          this.posterQrCode = res.data.qrCodeBase64
+          // 必须在 showSharePoster=true 之前取到数据:组件 watch 到 show 后 300ms 即开始绘制
+          await this.loadPosterEnergy()
+          this.showSharePoster = true
+        } else {
+          uni.showToast({ title: '生成失败,请重试', icon: 'none' })
+        }
+      } catch (e) {
+        console.error('生成分享海报失败', e)
+        uni.showToast({ title: '生成失败,请重试', icon: 'none' })
+      } finally {
+        uni.hideLoading()
       }
     },
-    closeShareQrModal() {
-      this.showShareQrModal = false
+    closeSharePoster() {
+      this.showSharePoster = false
+    },
+    // 五维能量数据卡数据(方案C):家庭聚合视角;未解锁全景/取数失败时置空,海报降级为品牌卡
+    loadPosterEnergy() {
+      var self = this
+      return getEnergySandbox('family').then(function(res) {
+        self.posterEnergyData = self.mapPosterEnergy(res)
+      }).catch(function(e) {
+        console.error('获取五维能量失败,海报降级为品牌卡', e)
+        self.posterEnergyData = null
+      })
+    },
+    mapPosterEnergy(res) {
+      if (!res || res.code !== 200 || !res.data) return null
+      // locked=true 表示用户未解锁全景,此时不展示任何数据,改为品牌卡
+      if (res.data.locked) return null
+      // 只认家庭聚合视角;仅当接口返回的是裸 DTO(无 personalData/familyData 包装)时才直接取 res.data
+      var d = res.data.familyData || null
+      if (!d) {
+        if (res.data.personalData) return null
+        d = res.data
+      }
+      if (!d || typeof d !== 'object') return null
+      var defs = [
+        { key: 'body', label: '身', color: '#FF8C42' },
+        { key: 'mind', label: '心', color: '#FF6B9D' },
+        { key: 'wisdom', label: '智', color: '#6366F1' },
+        { key: 'action', label: '行', color: '#10B981' },
+        { key: 'wealth', label: '富', color: '#F59E0B' }
+      ]
+      var dims = []
+      var sum = 0
+      for (var i = 0; i < defs.length; i++) {
+        var raw = d[defs[i].key + 'Score']
+        if (raw === null || raw === undefined || raw === '' || isNaN(raw)) continue
+        var n = Number(raw)
+        if (n < 0) n = 0
+        if (n > 100) n = 100
+        sum += n
+        dims.push({ key: defs[i].key, label: defs[i].label, score: n, color: defs[i].color })
+      }
+      if (dims.length === 0) return null
+      var overall = d.overallScore
+      if (overall === null || overall === undefined || overall === '' || isNaN(overall)) {
+        overall = Math.round(sum / dims.length)
+      }
+      return { label: '我们家的能量', overall: Number(overall), dims: dims }
     },
     async loadReferralCode() {
       if (this.referralCodeLoaded) return
       var self = this
       try {
         var res = await getReferralCode()
-        var code = extractReferralCode(res)
-        self.referralCode = code || ''
-        self.referralCodeLoaded = true
-        if (code) {
-          self.generateQrcode()
-        }
+        self.referralCode = extractReferralCode(res) || ''
       } catch (e) {
         console.error('获取推广码失败', e)
+      } finally {
         self.referralCodeLoaded = true
       }
     },
-    generateQrcode() {
-      if (!this.referralCode) return
-      this.qrLoading = true
-      var self = this
-      var scene = 'ref=' + this.referralCode
-      // 首页路径,扫码后进入首页并绑定推荐人
-      getShareQrCode('/pages/index-home/index', scene)
-        .then(function(res) {
-          if (res && res.code === 200 && res.data) {
-            self.qrBase64 = res.data.qrCodeBase64
-          }
-        })
-        .catch(function(e) {
-          console.error('生成二维码失败', e)
-          uni.showToast({ title: '生成失败,请重试', icon: 'none' })
-        })
-        .finally(function() {
-          self.qrLoading = false
-        })
-    },
-    shareToWechat() {
-      if (!this.referralCode) return
-      var path = '/pages/index-home/index?refCode=' + this.referralCode
-      uni.share({
-        provider: 'weixin',
-        scene: { type: 0 },
-        type: 5,
-        imageUrl: this.qrBase64,
-        path: path,
-        title: '浠艾福 — 邀请你加入',
-        success: function() {
-          uni.showToast({ title: '分享成功', icon: 'success' })
-        },
-        fail: function() {
-          uni.showToast({ title: '分享失败', icon: 'none' })
-        }
-      })
-    },
     goToServiceCenter() {
       uni.navigateTo({ url: '/pages/service-center/index' })
     },
@@ -688,15 +722,14 @@ export default {
     },
     goToSettings: function() {
       uni.showActionSheet({
-        itemList: ['完善信息', '家庭成员', '邀请加入', '隐私政策', '用户协议', '我的画像'],
+        itemList: ['完善信息', '家庭成员', '隐私政策', '用户协议', '我的画像'],
         success: function(res) {
           var urls = {
             0: '/pages/user-edit/user-edit',
             1: '/pages/profile-extra/family-members',
-            2: '/pages/guide/invite/share',
-            3: '/pages/policy/privacy',
-            4: '/pages/policy/agreement',
-            5: '/pages/profile/portrait-edit'
+            2: '/pages/policy/privacy',
+            3: '/pages/policy/agreement',
+            4: '/pages/profile/portrait-edit'
           }
           if (urls[res.tapIndex]) {
             uni.navigateTo({ url: urls[res.tapIndex] })
@@ -705,7 +738,7 @@ export default {
       })
     },
     memberRoleLabel: function(role) {
-      var map = { parent: '家长', child: '孩子', teacher: '规划师' }
+      var map = { parent: '家长', child: '成员', teacher: '规划师' }
       return map[role] || '成员'
     },
     onExitSwitch: function() {
@@ -1282,7 +1315,7 @@ export default {
 /* 分享推广码浮动按钮 */
 .share-qr-float {
   position: fixed;
-  top: 40rpx;
+  top: 180rpx;
   right: 30rpx;
   width: 80rpx;
   height: 80rpx;
@@ -1294,147 +1327,45 @@ export default {
   justify-content: center;
   z-index: 99;
 }
-.share-qr-icon {
-  font-size: 40rpx;
-  line-height: 1;
+/* 二维码图标(纯 CSS 绘制,不依赖 emoji 字体) */
+.qr-glyph {
+  position: relative;
+  width: 46rpx;
+  height: 46rpx;
 }
-
-/* 分享海报弹窗 */
-.share-qr-modal {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  z-index: 999;
-  display: flex;
-  align-items: center;
-  justify-content: center;
+.qr-eye {
+  position: absolute;
+  width: 16rpx;
+  height: 16rpx;
+  border: 4rpx solid #1E293B;
+  border-radius: 3rpx;
+  box-sizing: border-box;
 }
-.modal-mask {
+.qr-eye-tl { left: 0; top: 0; }
+.qr-eye-tr { right: 0; top: 0; }
+.qr-eye-bl { left: 0; bottom: 0; }
+.qr-mid {
+  position: absolute;
+  left: 20rpx;
+  top: 20rpx;
+  width: 7rpx;
+  height: 7rpx;
+  background: #1E293B;
+  border-radius: 1rpx;
+}
+.qr-cluster {
   position: absolute;
-  top: 0;
-  left: 0;
   right: 0;
   bottom: 0;
-  background: rgba(0, 0, 0, 0.5);
-}
-.modal-qr {
-  position: relative;
-  width: 70%;
-  max-width: 600rpx;
-  background: #fff;
-  border-radius: 24rpx;
-  padding: 30rpx;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-}
-.modal-qr-header {
-  width: 100%;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 20rpx;
-}
-.modal-qr-title {
-  font-size: 30rpx;
-  font-weight: bold;
-  color: #333;
-}
-.modal-qr-close {
-  font-size: 32rpx;
-  color: #999;
-  padding: 10rpx;
-}
-.modal-qr-content {
+  width: 18rpx;
   display: flex;
-  flex-direction: column;
-  align-items: center;
-  width: 100%;
-}
-.qrcode-loading,
-.qrcode-empty {
-  width: 340rpx;
-  height: 340rpx;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  background: #FAFAFA;
-  border-radius: 16rpx;
-  border: 2rpx dashed #E5E7EB;
-}
-.loading-spinner {
-  font-size: 48rpx;
-  margin-bottom: 16rpx;
-}
-.loading-text {
-  font-size: 24rpx;
-  color: #94A3B8;
-}
-.qrcode-empty text {
-  font-size: 24rpx;
-  color: #94A3B8;
-}
-.qrcode-image {
-  width: 340rpx;
-  height: 340rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  background: #fff;
-  border-radius: 16rpx;
-  border: 1rpx solid #E5E7EB;
-  overflow: hidden;
-}
-.qrcode-img {
-  width: 330rpx;
-  height: 330rpx;
-}
-.qrcode-info {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  margin-top: 20rpx;
-  gap: 8rpx;
-}
-.qrcode-text {
-  font-size: 28rpx;
-  font-weight: bold;
-  color: #F97316;
-}
-.qrcode-desc {
-  font-size: 22rpx;
-  color: #94A3B8;
-}
-.modal-qr-actions {
-  width: 100%;
-  display: flex;
-  gap: 20rpx;
-  margin-top: 24rpx;
-}
-.action-btn {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  gap: 8rpx;
-  padding: 24rpx;
-  border-radius: 16rpx;
-  font-size: 24rpx;
-  font-weight: bold;
-}
-.share-btn {
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-  color: #fff;
-  width: 100%;
-}
-.share-icon {
-  font-size: 40rpx;
+  flex-wrap: wrap;
 }
-.share-text {
-  font-size: 24rpx;
-  font-weight: bold;
+.qr-pip {
+  width: 7rpx;
+  height: 7rpx;
+  margin: 1rpx;
+  background: #1E293B;
+  border-radius: 1rpx;
 }
 </style>

+ 1 - 1
cfc-frontend/pages/profile/components/ProfileHeader.vue

@@ -51,7 +51,7 @@ export default {
         const res = await getChildren()
         this.children = res.data || []
       } catch (e) {
-        console.error('获取孩子列表失败', e)
+        console.error('获取成员列表失败', e)
       }
     },
     onSwitchMode() {

+ 3 - 3
cfc-frontend/pages/profile/portrait-edit.vue

@@ -19,10 +19,10 @@
       <view class="section-title">参考指标(自动生成)</view>
       <view class="snapshot-text">{{ renderedSnapshot }}</view>
     </view>
-  </view>
 
-  <view class="save-bar">
-    <button class="save-btn" :loading="saving" @click="onSave">保存</button>
+    <view class="save-bar">
+      <button class="save-btn" :loading="saving" @click="onSave">保存</button>
+    </view>
   </view>
 </template>
 

+ 1 - 0
cfc-frontend/pages/promotion/index.vue

@@ -547,6 +547,7 @@ export default {
   padding: 30rpx;
   min-height: 100vh;
   background: #FFF7ED;
+  box-sizing: border-box;
 }
 .invite-card {
   background: linear-gradient(135deg, #F97316, #EA580C);

+ 1 - 1
cfc-frontend/pages/promotion/team.vue

@@ -111,7 +111,7 @@
 </template>
 
 <script>
-import { getCfRate, getTeamList } from '../../utils/api.js'
+import { getCfRate, getTeamList, getTeamGrowthCurve, getTeamAllMembersConsumptionCurve, getTeamL1ConsumptionCurve } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 
 export default {

+ 1 - 1
cfc-frontend/pages/rewards/badge.vue

@@ -26,7 +26,7 @@
           <text class="ranking-avatar-placeholder" v-else>{{ item.memberName ? item.memberName.substring(0,1) : '?' }}</text>
           <view class="ranking-info">
             <text class="ranking-name">{{ item.memberName || '未知' }}</text>
-            <text class="ranking-role">{{ item.role === 'child' ? '孩子' : '家长' }}</text>
+            <text class="ranking-role">{{ item.role === 'child' ? '成员' : '家长' }}</text>
           </view>
           <view class="ranking-count">
             <text class="ranking-num">{{ item.badgeCount || 0 }}</text>

+ 4 - 4
cfc-frontend/pages/rewards/rewards.vue

@@ -42,7 +42,7 @@
           <input v-model="newReward.title" placeholder="请输入心愿名称" />
         </view>
         
-        <!-- 家长模式可以填写积分,孩子模式不需要填写积分 -->
+        <!-- 家长模式可以填写积分,成员模式不需要填写积分 -->
         <view class="form-item" v-if="currentRole === 'parent'">
           <view class="form-label">所需积分 <text class="required">*</text></view>
           <input v-model="newReward.pointsRequired" type="number" placeholder="请输入所需积分" />
@@ -50,7 +50,7 @@
         
         <!-- 孩子模式提示 -->
         <view class="form-tip" v-if="currentRole === 'child'">
-          <text>孩子创建心愿后,需要家长填写所需积分才能生效</text>
+          <text>成员创建心愿后,需要家长填写所需积分才能生效</text>
         </view>
         
         <view class="modal-btns">
@@ -109,7 +109,7 @@ export default {
           const userInfo = await this.getUserInfo()
           this.currentPoints = userInfo.totalPoints || 0
         } else {
-          // 孩子模式:使用孩子心愿单API
+          // 成员模式:使用成员心愿单API
           const childrenRes = await getChildren()
           const children = childrenRes.data
           if (children && children.length > 0) {
@@ -188,7 +188,7 @@ export default {
           })
           uni.showToast({ title: '添加成功', icon: 'success' })
         } else {
-          // 孩子模式:创建孩子心愿,积分为0,等待家长填写
+          // 成员模式:创建成员心愿,积分为0,等待家长填写
           await createRewardApi({
             title: this.newReward.title,
             memberId: this.currentChildId

+ 92 - 19
cfc-frontend/pages/shop/cart/cart.vue

@@ -73,22 +73,38 @@
     </view>
 
     <!-- Gift Membership Progress -->
-    <view v-if="giftProgress && !giftIsGranted && giftProgress.threshold > 0" class="gift-progress-section">
-      <view class="gift-progress-header">
-        <text class="gift-progress-icon">🎁</text>
-        <text class="gift-progress-title">购会员商品累计进度</text>
-        <text class="gift-progress-percent">{{ giftProgressPercent }}%</text>
-      </view>
-      <view class="gift-progress-bar-bg">
-        <view class="gift-progress-bar-fill" :style="{ width: giftProgressPercent + '%' }"></view>
+    <view v-if="!giftPromo || (giftPromo && !giftPromo.active)" class="gift-progress-section">
+      <view v-if="giftProgress && !giftIsGranted && giftProgress.threshold > 0" class="gift-progress-section">
+        <view class="gift-progress-header">
+          <text class="gift-progress-icon">🎁</text>
+          <text class="gift-progress-title">购会员商品累计进度</text>
+          <text class="gift-progress-percent">{{ giftProgressPercent }}%</text>
+        </view>
+        <view class="gift-progress-bar-bg">
+          <view class="gift-progress-bar-fill" :style="{ width: giftProgressPercent + '%' }"></view>
+        </view>
+        <view class="gift-progress-info">
+          <text class="gift-progress-accumulated">已累 {{ giftAccumulatedYuan }} 元</text>
+          <text class="gift-progress-remaining">还差 {{ giftRemainingYuan }} 元送会员</text>
+        </view>
+        <view v-if="giftProgress.grantedLevel" class="gift-progress-level">
+          <text>达标将赠送 {{ giftProgress.grantedLevel === 'FAMILY' ? '家庭会员' : giftProgress.grantedLevel }} 权益</text>
+        </view>
       </view>
-      <view class="gift-progress-info">
-        <text class="gift-progress-accumulated">已累 {{ giftAccumulatedYuan }} 元</text>
-        <text class="gift-progress-remaining">还差 {{ giftRemainingYuan }} 元送会员</text>
+    </view>
+
+    <!-- 送会员一口价促销区 -->
+    <view v-if="giftPromo && giftPromo.active && !giftIsGranted" class="gift-promo-section">
+      <view class="gift-promo-header">
+        <text class="gift-promo-icon">🎁</text>
+        <text class="gift-promo-title">购会员商品原价满 {{ giftPromo.threshold / 100 }} 元,实付 {{ giftPromo.payPrice / 100 }} 元</text>
       </view>
-      <view v-if="giftProgress.grantedLevel" class="gift-progress-level">
-        <text>达标将赠送 {{ giftProgress.grantedLevel === 'FAMILY' ? '家庭会员' : giftProgress.grantedLevel }} 权益</text>
+      <view class="gift-promo-bar">
+        <view v-if="giftPromo.remaining > 0" class="gift-promo-bar-fill" :style="{ width: Math.max(0, (100 - giftPromo.remaining / giftPromo.threshold * 100)) + '%' }"></view>
+        <view v-else class="gift-promo-bar-fill"></view>
       </view>
+      <view v-if="giftPromo.remaining > 0" class="gift-promo-remaining">还差 {{ giftPromo.remaining / 100 }} 元达标</view>
+      <view v-else class="gift-promo-reached">已达标,支付后自动赠送会员</view>
     </view>
 
     <view v-else-if="giftIsGranted" class="gift-granted-hint">
@@ -135,7 +151,8 @@ export default {
       refreshing: false,
       userId: null,
       isMember: false,
-      giftProgress: null  // 赠会员累计进度
+      giftProgress: null,  // 赠会员累计进度
+      giftPromo: null      // 送会员一口价促销信息
     }
   },
   computed: {
@@ -154,6 +171,9 @@ export default {
       return count
     },
     totalAmount() {
+      if (this.giftPromo && this.giftPromo.active === true && this.giftPromo.discount > 0) {
+        return this.originalTotalAmount - this.giftPromo.discount
+      }
       var total = 0
       for (var i = 0; i < this.items.length; i++) {
         if (this.items[i].checked) {
@@ -181,9 +201,17 @@ export default {
           if (this.isMember && this.items[i].memberPrice && this.items[i].memberPrice > 0 && this.items[i].memberPrice < price) {
             price = this.items[i].memberPrice
           }
+          // 送会员一口价折扣:若达标,从总价中减去 discount
+          if (this.giftPromo && this.giftPromo.active === true && this.giftPromo.discount > 0 && this.items[i].giftLevelName) {
+            // 不在此处单独扣除,由 totalAmount() 已在 top 层减去;这里仅作备选兼容
+          }
           total = total + (price * this.items[i].quantity)
         }
       }
+      // 若 giftPromo 达标,从总价中减去 discount
+      if (this.giftPromo && this.giftPromo.active === true && this.giftPromo.discount > 0) {
+        total = total - this.giftPromo.discount
+      }
       return total
     },
     hasGiftItems() {
@@ -274,12 +302,14 @@ export default {
           that.loading = false
           that.refreshing = false
           if (res.data && res.data.code === 200) {
-            var list = res.data.data || []
-            for (var i = 0; i < list.length; i++) {
-              list[i].checked = true
-              list[i].subtotal = list[i].unitPrice * list[i].quantity
+            var result = res.data.data
+            that.items = result.items || []
+            for (var i = 0; i < that.items.length; i++) {
+              that.items[i].checked = true
+              that.items[i].subtotal = that.items[i].unitPrice * that.items[i].quantity
             }
-            that.items = list
+            that.giftPromo = result.giftPromo || null
+            that.loadGiftProgress()
           }
         },
         fail: function() {
@@ -386,6 +416,7 @@ export default {
         }
       }
       uni.setStorageSync('checkoutItems', selected)
+      uni.setStorageSync('checkoutGiftPromo', this.giftPromo)
       uni.navigateTo({ url: '/pages/shop/checkout/checkout' })
     },
     formatPriceWithSymbol(price) {
@@ -722,6 +753,48 @@ export default {
   font-size: 22rpx;
   color: #F97316;
 }
+/* Gift promo section */
+.gift-promo-section {
+  padding: 20rpx 30rpx;
+  background: linear-gradient(135deg, #FEF3C7, #FDE68A);
+  border-top: 2rpx solid #F59E0B;
+}
+.gift-promo-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 12rpx;
+}
+.gift-promo-icon {
+  font-size: 32rpx;
+  margin-right: 12rpx;
+}
+.gift-promo-title {
+  flex: 1;
+  font-size: 26rpx;
+  color: #92400E;
+  font-weight: 500;
+}
+.gift-promo-bar {
+  height: 16rpx;
+  background: #FCD34D;
+  border-radius: 8rpx;
+  overflow: hidden;
+  margin-bottom: 8rpx;
+}
+.gift-promo-bar-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #F59E0B, #D97706);
+  border-radius: 8rpx;
+}
+.gift-promo-remaining {
+  font-size: 22rpx;
+  color: #92400E;
+}
+.gift-promo-reached {
+  font-size: 22rpx;
+  color: #D97706;
+  font-weight: bold;
+}
 .gift-granted-hint {
   display: flex;
   align-items: center;

+ 58 - 3
cfc-frontend/pages/shop/checkout/checkout.vue

@@ -172,7 +172,7 @@
           <text class="gift-progress-accumulated">已累 {{ giftAccumulatedYuan }} 元</text>
           <text class="gift-progress-remaining">还差 {{ giftRemainingYuan }} 元送会员</text>
         </view>
-        <text class="gift-progress-note">仅原价实付订单计入累计,使用积分、优惠券或折扣的订单不计入</text>
+        <text class="gift-progress-note">单笔订单送会员商品原价达标即送,折扣订单也计入</text>
         <view v-if="giftProgress.grantedLevel" class="gift-progress-level">
           <text>达标将赠送 {{ giftProgress.grantedLevel === 'FAMILY' ? '家庭会员' : giftProgress.grantedLevel }} 权益</text>
         </view>
@@ -212,6 +212,18 @@
         <countdown-tag v-if="discountInfo.endTime" :end-time="discountInfo.endTime" />
       </view>
 
+      <!-- Gift Promo Section -->
+      <view v-if="giftPromo && giftPromo.active" class="section gift-promo-section">
+        <view class="gift-promo-row">
+          <text class="gift-promo-icon">🎁</text>
+          <view class="gift-promo-content">
+            <text class="gift-promo-title">送会员一口价</text>
+            <text class="gift-promo-desc">送会员商品原价 {{ formatPriceWithSymbol(giftPromo.giftOriginalTotal) }} 元 → 实付 {{ formatPriceWithSymbol(giftPromo.payPrice) }} 元</text>
+            <text class="gift-promo-save">已优惠 {{ formatPriceWithSymbol(giftPromoDiscount) }} 元</text>
+          </view>
+        </view>
+      </view>
+
       <!-- Coupon Section -->
       <view class="section coupon-section" @click="openCouponPicker">
         <view class="info-row">
@@ -502,7 +514,8 @@ export default {
        showOnlineSlotModal: false,
       giftItemIds: [],
       giftProgress: null,   // 赠会员累计进度
-      discountInfo: null
+      discountInfo: null,
+      giftPromo: null       // 送会员一口价促销信息
     }
   },
   computed: {
@@ -526,6 +539,10 @@ export default {
     giftIsGranted() {
       return this.giftProgress && this.giftProgress.granted === 1
     },
+    giftPromoDiscount() {
+      if (!this.giftPromo || this.giftPromo.active !== true) return 0
+      return this.giftPromo.discount || 0
+    },
     giftLevelNames() {
       var names = []
       for (var i = 0; i < this.items.length; i++) {
@@ -541,6 +558,7 @@ export default {
         total = total - this.selectedCoupon.value
       }
       total = total - (this.pointsUsed || 0)
+      total = total - this.giftPromoDiscount
       if (total < 0) total = 0
       return total
     },
@@ -604,7 +622,9 @@ export default {
     var that = this
     var stored = uni.getStorageSync('checkoutItems')
     var giftIds = uni.getStorageSync('checkoutGiftItemIds')
+    var giftPromo = uni.getStorageSync('checkoutGiftPromo')
     this.giftItemIds = (giftIds && giftIds.length > 0) ? giftIds : []
+    this.giftPromo = giftPromo || null
     if (stored && stored.length > 0) {
       this.items = stored
       this.calcAmount()
@@ -1005,7 +1025,7 @@ export default {
           addressId: that.selectedAddress.id,
           purchaseInfo: that.purchaseForm,
           pointsUsed: that.pointsUsed,
-          discountAmount: that.discountInfo ? that.discountInfo.discountAmount : 0,
+          discountAmount: (that.discountInfo ? that.discountInfo.discountAmount : 0) + that.giftPromoDiscount,
           deliveryMethod: that.deliveryMethod,
           pickupLocationId: that.selectedPickupLocation ? that.selectedPickupLocation.id : null,
           timeSlotId: that.selectedTimeSlot ? that.selectedTimeSlot.id : null,
@@ -1022,6 +1042,7 @@ export default {
             uni.showToast({ title: '下单成功', icon: 'success' })
             uni.removeStorageSync('checkoutItems')
             uni.removeStorageSync('checkoutGiftItemIds')
+            uni.removeStorageSync('checkoutGiftPromo')
             var moneyAmount = order.moneyAmount != null ? order.moneyAmount : order.totalAmount
             var pointsUsed = order.pointsUsed || 0
             setTimeout(function() {
@@ -1816,4 +1837,38 @@ showOnlineSlotPicker: function() {
   font-size: 26rpx;
   color: #15803D;
 }
+.gift-promo-section {
+  background: #FFF7ED;
+  border-radius: 16rpx;
+  border: 2rpx solid #FED7AA;
+  margin: 20rpx;
+  padding: 20rpx;
+}
+.gift-promo-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.gift-promo-icon {
+  font-size: 32rpx;
+  margin-right: 12rpx;
+}
+.gift-promo-content {
+  flex: 1;
+}
+.gift-promo-title {
+  font-size: 26rpx;
+  color: #C2410C;
+  font-weight: 500;
+}
+.gift-promo-desc {
+  font-size: 22rpx;
+  color: #9A3412;
+  margin-top: 4rpx;
+}
+.gift-promo-save {
+  font-size: 22rpx;
+  color: #F97316;
+}
+/* 保持原有样式向下延伸 */
 </style>

+ 3 - 3
cfc-frontend/pages/stats/completion-rate.vue

@@ -79,14 +79,14 @@
           <text class="alert-icon">⚠️</text>
           <view class="alert-body">
             <text class="alert-title">有 {{ stats.overdueTasks }} 个任务已逾期</text>
-            <text class="alert-desc">请督促孩子尽快完成,避免影响成长积分</text>
+            <text class="alert-desc">请督促成员尽快完成,避免影响成长积分</text>
           </view>
         </view>
 
-        <!-- 按孩子对比 (仅家庭维度) -->
+        <!-- 按成员对比 (仅家庭维度) -->
         <view class="section-block" v-if="stats.byChild && stats.byChild.length > 1">
           <view class="section-header">
-            <text class="section-title">孩子对比</text>
+            <text class="section-title">成员对比</text>
           </view>
           <view class="child-compare">
             <view class="compare-bar-wrapper" v-for="child in stats.byChild" :key="child.memberId">

+ 4 - 4
cfc-frontend/pages/tasks/create-task.vue

@@ -12,7 +12,7 @@
 </view>
 
 <view class="form-item">
-<text class="label">选择孩子(可多选)</text>
+<text class="label">选择成员(可多选)</text>
 <checkbox-group @change="onChildrenChange">
 <label class="child-checkbox" v-for="child in children" :key="child.id">
 <checkbox :value="String(child.id)" :checked="selectedChildIds.includes(String(child.id))" />
@@ -218,7 +218,7 @@ methods: {
       try {
         const res = await getChildren()
         this.children = res.data || []
-        // 如果有 preselect memberId,自动选中该孩子
+        // 如果有 preselect memberId,自动选中该成员
         if (preselectedChildId && this.children.length > 0) {
           const matched = this.children.find(c => String(c.id) === String(preselectedChildId))
           if (matched) {
@@ -408,7 +408,7 @@ onStartRequiredChange(e) {
         return
       }
       if (this.form.memberIds.length === 0) {
-        uni.showToast({ title: '请选择至少一个孩子', icon: 'none' })
+        uni.showToast({ title: '请选择至少一个成员', icon: 'none' })
         return
       }
       if (!this.form.deadline) {
@@ -460,7 +460,7 @@ onStartRequiredChange(e) {
       }
 
       if (failCount === 0) {
-        uni.showToast({ title: `已为${successCount}个孩子创建任务`, icon: 'success' })
+        uni.showToast({ title: `已为${successCount}个成员创建任务`, icon: 'success' })
         setTimeout(() => uni.navigateBack(), 1500)
       } else if (successCount > 0) {
         uni.showToast({ title: `${successCount}个成功,${failCount}个失败`, icon: 'none' })

+ 2 - 2
cfc-frontend/pages/tasks/review.vue

@@ -26,7 +26,7 @@
             <text class="card-title">{{ task.title }}</text>
             <text class="card-desc">{{ task.description }}</text>
             <view class="card-meta-row">
-              <text class="card-provider">提供者: {{ task.creatorRole === 'parent' ? '家长' : '孩子' }}</text>
+              <text class="card-provider">提供者: {{ task.creatorRole === 'parent' ? '家长' : '成员' }}</text>
               <text class="card-points">⭐ +{{ task.points }}</text>
             </view>
           </view>
@@ -47,7 +47,7 @@
             </view>
             <text class="card-desc">{{ task.description }}</text>
             <view class="card-meta-row">
-              <text class="card-provider">提供者: {{ task.creatorRole === 'parent' ? '家长' : '孩子' }}</text>
+              <text class="card-provider">提供者: {{ task.creatorRole === 'parent' ? '家长' : '成员' }}</text>
               <text class="card-points">⭐ +{{ task.points }}</text>
             </view>
             <text class="card-time" v-if="task.reviewTime">审核时间: {{ task.reviewTime }}</text>

+ 1 - 1
cfc-frontend/pages/teacher/families.vue

@@ -18,7 +18,7 @@
           <text class="family-status">{{ family.status === 'active' ? '服务中' : '已暂停' }}</text>
         </view>
         <view class="family-info">
-          <text class="info-item">👶 {{ family.childrenCount || 0 }}个孩子</text>
+          <text class="info-item">👶 {{ family.childrenCount || 0 }}位成员</text>
           <text class="info-item">📍 {{ family.address || '未设置地址' }}</text>
         </view>
         <view class="family-tasks">

+ 1 - 1
cfc-frontend/pages/tianpan/member-detail.vue

@@ -193,7 +193,7 @@ export default {
     getRoleText: function(role) {
       var roleMap = {
         'parent': '家长',
-        'child': '孩子',
+        'child': '成员',
         'teacher': '规划师'
       }
       return roleMap[role] || role

+ 1 - 1
cfc-frontend/pages/tianpan/relation-detail.vue

@@ -384,7 +384,7 @@ export default {
     getRoleText: function(role) {
       var roleMap = {
         'parent': '家长',
-        'child': '孩子',
+        'child': '成员',
         'teacher': '规划师'
       }
       return roleMap[role] || role

+ 1 - 1
cfc-frontend/pages/tianpan/traditional.vue

@@ -158,7 +158,7 @@ export default {
     },
 
     getRoleLabel: function(type) {
-      var labels = { parent: '家长', child: '孩子', teacher: '规划师' }
+      var labels = { parent: '家长', child: '成员', teacher: '规划师' }
       return labels[type] || type
     },
 

+ 2 - 2
cfc-frontend/pages/user-edit/user-edit.vue

@@ -14,7 +14,7 @@
 			>
 				<text class="role-icon">👨‍👩‍👧</text>
 				<text class="role-name">家长</text>
-				<text class="role-desc">管理孩子任务、审核奖励</text>
+				<text class="role-desc">管理家庭成员任务、审核奖励</text>
 			</view>
 			
 			<view 
@@ -23,7 +23,7 @@
 				@click="selectRole('child')"
 			>
 				<text class="role-icon">👶</text>
-				<text class="role-name">孩子</text>
+				<text class="role-name">成员</text>
 				<text class="role-desc">执行任务、赚取积分</text>
 			</view>
 			

+ 423 - 0
cfc-frontend/pages/vendor/team-customers/team-customers.vue

@@ -0,0 +1,423 @@
+<template>
+  <view class="page-team-customers">
+    <view class="page-header">
+      <text class="header-title">团队客户</text>
+      <text class="header-subtitle">查看上级供应商下的客户归属</text>
+    </view>
+
+    <view class="content-area">
+      <view v-if="isLoading" class="loading-indicator">
+        <image class="loading-avatar" src="/static/images/avatar_default.png" mode="aspectFill"></image>
+        <text class="loading-text">加载中...</text>
+      </view>
+
+      <view v-else class="customer-list">
+        <!-- 状态筛选 Tab -->
+        <view class="status-tabs" v-if="statusOptions.length > 0">
+          <view
+            class="status-tab"
+            v-for="(option, index) in statusOptions"
+            :key="index"
+            :class="{ 'status-tab-active': status === option.key }"
+            @click="handleStatusChange(option.key)"
+          >
+            <text>{{ option.label }}</text>
+          </view>
+        </view>
+
+        <!-- 列表区域 -->
+        <view v-else>
+          <view class="no-data" v-if="filteredCustomers.length === 0">
+            <view class="empty-state">
+              <image src="/static/images/icon_empty_state.png" class="empty-icon"></image>
+              <text>暂无客户数据</text>
+            </view>
+          </view>
+
+          <view class="customer-item" v-for="customer in filteredCustomers" :key="customer.id">
+            <view class="customer-avatar-wrap">
+              <image
+                v-if="customer.avatar"
+                class="customer-avatar-img"
+                :src="customer.avatar"
+                mode="aspectFill"></image>
+              <text v-else class="customer-avatar-icon">👤</text>
+            </view>
+
+            <view class="customer-info">
+              <view class="customer-name">{{ customer.nickname || '微信用户' }}</view>
+              <view class="customer-meta">
+                <text class="member-type">{{ customer.memberType || '本地用户' }}</text>
+                <text class="join-time">{{ formatJoinTime(customer.joinTime) }}</text>
+              </view>
+            </view>
+
+            <view class="customer-right">
+              <view class="order-count">
+                <text>{{ customer.orderCount || 0 }}</text>
+                <text>订单</text>
+              </view>
+
+              <view class="source-tag" :class="getSourceTagClass(customer.source)">
+                <text>{{ getSourceTagLabel(customer.source) }}</text>
+              </view>
+            </view>
+          </view>
+
+          <!-- 加载更多 -->
+          <view class="load-more" v-if="hasMore && !loading">
+            <view @click="loadMore" class="load-more-btn">
+              <text>加载更多</text>
+              <image v-if="loadingMore" src="/static/images/arrow_down.png" class="spinner"></image>
+            </view>
+          </view>
+        </view>
+      </view>
+    </content-area>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      // 页面状态
+      status: 'all', // all | referred | order | manual
+      page: 1,
+      hasMore: true,
+      loading: false,
+      refreshing: false,
+
+      // 数据
+      customers: [],         // 所有客户列表
+      filteredCustomers: [], // 筛选后的客户列表
+      statusOptions: [       // 筛选选项
+        { key: 'all', label: '全部' },
+        { key: 'referred', label: '推荐人' },
+        { key: 'order', label: '下单' },
+        { key: 'manual', label: '人工' }
+      ],
+
+      // 查询参数
+      systemId: '',          // 供应商体系ID
+      parentId: ''           // 上级供应商用户ID
+    }
+  },
+
+  onLoad(options) {
+    // 从路由参数获取系统ID和上级用户ID
+    this.systemId = options.systemId || ''
+    this.parentId = options.parentId || ''
+    this.loadCustomers()
+  },
+
+  onShow() {
+    // 切Tab返回时刷新
+    this.page = 1
+    this.customers = []
+    this.hasMore = true
+    this.loadCustomers()
+  },
+
+  methods: {
+    // 加载客户列表
+    async loadCustomers() {
+      if (this.loading || this.refreshing) return
+      this.loading = true
+
+      try {
+        // 并发请求:获取客户列表 + 获取统计概览
+        var res = await this.$http.getTeamCustomers({
+          systemId: this.systemId,
+          parentId: this.parentId,
+          page: this.page,
+          size: 20
+        })
+
+        if (res && res.data) {
+          this.customers = this.page === 1 ? res.data.records || [] : this.customers.concat(res.data.records || [])
+          this.hasMore = (res.data.records || []).length >= 20
+          this.page++
+
+          // 计算筛选后的列表
+          this.filteredCustomers = this.filterByStatus(this.customers, this.status)
+        }
+      } catch (error) {
+        console.error('获取团队客户失败:', error)
+      } finally {
+        this.loading = false
+        this.refreshing = false
+      }
+    },
+
+    // 根据状态筛选
+    filterByStatus(customers, status) {
+      if (status === 'all') return customers
+      return customers.filter(c => c.source === status)
+    },
+
+    // 处理状态筛选切换
+    handleStatusChange(key) {
+      this.status = key
+      this.page = 1
+      this.customers = []
+      this.hasMore = true
+      this.loadCustomers()
+    },
+
+    // 加载更多
+    async loadMore() {
+      if (this.loading || !this.hasMore) return
+      this.loadingMore = true
+      try {
+        await this.loadCustomers()
+      } finally {
+        this.loadingMore = false
+      }
+      this.$nextTick(() => {
+        this.loadingMore = false
+      })
+    },
+
+    // 格式化加入时间
+    formatJoinTime(timeStr) {
+      if (!timeStr) return '-'
+      var d = this.parseDate(timeStr)
+      return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
+    },
+
+    // 获取来源标签样式
+    getSourceTagClass(source) {
+      const classes = {
+        'manual': 'tag-manual',
+        'order': 'tag-order',
+        'referral': 'tag-referral'
+      }
+      return classes[source] || 'tag-default'
+    },
+
+    // 获取来源标签文字
+    getSourceTagLabel(source) {
+      const labels = {
+        'manual': '人工绑定',
+        'order': '下单自动',
+        'referral': '推荐码'
+      }
+      return labels[source] || source
+    }
+  }
+}
+</script>
+
+<style>
+/* 页面整体样式 */
+.page-team-customers {
+  background-color: #FFF7ED;
+  min-height: 100vh;
+}
+
+/* 页面头部 */
+.page-header {
+  padding: 24rpx 30rpx;
+  background-color: #fff;
+  border-bottom: 1rpx solid #FED7AA;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.header-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333333;
+}
+
+.header-subtitle {
+  font-size: 20rpx;
+  color: #94A3B8;
+}
+
+/* 内容区 */
+.content-area {
+  padding: 0 30rpx 30rpx;
+}
+
+/* 状态筛选 Tab */
+.status-tabs {
+  display: flex;
+  background-color: #F5FAFE;
+  border-radius: 20rpx;
+  padding: 6rpx 12rpx;
+  margin-bottom: 24rpx;
+}
+
+.status-tab {
+  padding: 10rpx 20rpx;
+  font-size: 24rpx;
+  color: #666666;
+  text-align: center;
+}
+
+.status-tab-active {
+  color: #F97316;
+  border-bottom: 2rpx solid #F97316;
+}
+
+/* 列表项 */
+.customer-item {
+  background-color: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
+  display: flex;
+  align-items: center;
+}
+
+.customer-avatar-wrap {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 50%;
+  margin-right: 24rpx;
+  position: relative;
+}
+
+.customer-avatar-img {
+  width: 100%;
+  height: 100%;
+  border-radius: 50%;
+  object-fit: cover;
+}
+
+.customer-avatar-icon {
+  width: 100%;
+  height: 100%;
+  border-radius: 50%;
+  background-color: #F5FAFE;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #94A3B8;
+  font-size: 32rpx;
+}
+
+.customer-info {
+  flex: 1;
+}
+
+.customer-name {
+  font-size: 28rpx;
+  color: #333333;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.member-type {
+  font-size: 20rpx;
+  color: #94A3B8;
+  margin-top: 4rpx;
+}
+
+.join-time {
+  font-size: 20rpx;
+  color: #B5B5B5;
+}
+
+/* 右侧信息 */
+.customer-right {
+  flex-shrink: 0;
+  text-align: right;
+  margin-left: 24rpx;
+}
+
+.order-count {
+  margin-bottom: 8rpx;
+}
+
+.order-count .number {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #F97316;
+}
+
+.order-count .label {
+  font-size: 20rpx;
+  color: #666666;
+}
+
+/* 来源标签 */
+.source-tag {
+  padding: 4rpx 12rpx;
+  border-radius: 12rpx;
+  font-size: 16rpx;
+  margin-top: 4rpx;
+}
+
+.tag-manual {
+  background-color: #E6F7EE;
+  color: #65C29A;
+}
+
+.tag-order {
+  background-color: #FFF7EE;
+  color: #F97316;
+}
+
+.tag-referral {
+  background-color: #EEF8FF;
+  color: #2F5496;
+}
+
+.tag-default {
+  background-color: #F5FAFE;
+  color: #666666;
+}
+
+/* 加载更多 */
+.load-more {
+  padding: 24rpx;
+  text-align: center;
+  color: #94A3B8;
+  font-size: 24rpx;
+}
+
+/* 加载更多按钮 */
+.load-more-btn {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 120rpx;
+  height: 40rpx;
+  border: 1rpx solid #FED7AA;
+  border-radius: 20rpx;
+  color: #666666;
+  font-size: 24rpx;
+}
+
+.load-more-btn .spinner {
+  width: 16rpx;
+  height: 16rpx;
+  margin-left: 6rpx;
+  animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+
+/* 空状态 */
+.no-data .empty-state {
+  text-align: center;
+  padding: 80rpx 30rpx;
+  color: #94A3B8;
+}
+
+.empty-icon {
+  width: 64rpx;
+  height: 64rpx;
+  margin: 0 auto 12rpx;
+  display: block;
+}
+</style>

+ 616 - 0
cfc-frontend/pages/vendor/team-system/team-system.vue

@@ -0,0 +1,616 @@
+<template>
+  <view class="page-team-system">
+    <!-- 头部:体系信息 -->
+    <view class="system-header" v-if="systemInfo">
+      <view class="system-info">
+        <view class="system-icon">🏢</view>
+        <view class="system-detail">
+          <text class="system-name">{{ systemInfo.name || '我的供应商体系' }}</text>
+          <text class="system-desc">服务商团队管理</text>
+        </view>
+      </view>
+      <view class="system-status" :class="systemInfo.status">
+        <text>{{ systemInfo.status === 'active' ? '活跃' : '未激活' }}</text>
+      </view>
+    </view>
+
+    <!-- 层级关系 -->
+    <view class="section" v-if="hierarchyInfo">
+      <view class="section-header">
+        <text class="section-title">层级关系</text>
+      </view>
+      <view class="hierarchy-grid">
+        <!-- 上级 -->
+        <view class="hierarchy-item" v-if="hierarchyInfo.parent">
+          <view class="hierarchy-label">上级服务商</view>
+          <view class="hierarchy-person">
+            <image v-if="hierarchyInfo.parent.avatar" class="avatar-img" :src="hierarchyInfo.parent.avatar" mode="aspectFill"></image>
+            <text v-else class="avatar-placeholder">👤</text>
+            <view class="person-info">
+              <text class="person-name">{{ hierarchyInfo.parent.nickname || '服务商' }}</text>
+              <text class="person-level">上级</text>
+            </view>
+          </view>
+        </view>
+
+        <!-- 下级 -->
+        <view class="hierarchy-item" v-else>
+          <view class="hierarchy-label">下级服务商</view>
+          <view class="hierarchy-empty">
+            <text>暂无下级</text>
+          </view>
+        </view>
+
+        <!-- 团队规模 -->
+        <view class="hierarchy-stat">
+          <text class="stat-value">{{ hierarchyInfo.totalChildren || 0 }}</text>
+          <text class="stat-label">团队成员</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 团队统计 -->
+    <view class="section">
+      <view class="section-header">
+        <text class="section-title">团队数据</text>
+      </view>
+      <view class="stats-grid">
+        <view class="stat-card">
+          <text class="stat-num">{{ teamStats.memberCount || 0 }}</text>
+          <text class="stat-name">团队成员</text>
+        </view>
+        <view class="stat-card">
+          <text class="stat-num">{{ teamStats.customerCount || 0 }}</text>
+          <text class="stat-name">客户归属</text>
+        </view>
+        <view class="stat-card">
+          <text class="stat-num">{{ teamStats.orderCount || 0 }}</text>
+          <text class="stat-name">关联订单</text>
+        </view>
+        <view class="stat-card">
+          <text class="stat-num">{{ teamStats.amount || 0 }}</text>
+          <text class="stat-name">关联金额(元)</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 团队成员列表 -->
+    <view class="section">
+      <view class="section-header">
+        <text class="section-title">团队成员</text>
+        <text class="section-action" @click="goToMembers">查看全部</text>
+      </view>
+      <view class="member-list">
+        <view class="member-item" v-for="member in teamMembers.slice(0, 5)" :key="member.userId">
+          <image v-if="member.avatar" class="member-avatar" :src="member.avatar" mode="aspectFill"></image>
+          <text v-else class="member-avatar-placeholder">👤</text>
+          <view class="member-info">
+            <text class="member-name">{{ member.nickname || '微信用户' }}</text>
+            <text class="member-role">{{ member.role || '成员' }}</text>
+          </view>
+          <view class="member-stats">
+            <text class="member-count">{{ member.customerCount || 0 }}</text>
+            <text class="member-label">客户</text>
+          </view>
+        </view>
+        <view class="empty-tip" v-if="teamMembers.length === 0">
+          <text>暂无团队成员</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 团队客户列表 -->
+    <view class="section">
+      <view class="section-header">
+        <text class="section-title">团队客户</text>
+        <text class="section-action" @click="goToCustomers">查看全部</text>
+      </view>
+      <view class="customer-list">
+        <view class="customer-item" v-for="customer in teamCustomers.slice(0, 5)" :key="customer.id">
+          <view class="customer-avatar">
+            <text class="customer-icon">👥</text>
+          </view>
+          <view class="customer-info">
+            <text class="customer-name">{{ customer.familyName || '未知家庭' }}</text>
+            <text class="customer-meta">{{ customer.source === 'manual' ? '人工绑定' : customer.source === 'order' ? '下单自动' : '推荐码' }}</text>
+          </view>
+          <view class="customer-stats">
+            <text class="customer-orders">{{ customer.orderCount || 0 }}</text>
+            <text class="customer-label">订单</text>
+          </view>
+        </view>
+        <view class="empty-tip" v-if="teamCustomers.length === 0">
+          <text>暂无客户数据</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 操作按钮 -->
+    <view class="action-bar">
+      <button class="btn-manage" @click="goToManage">团队管理</button>
+      <button class="btn-customers" @click="goToCustomers">客户列表</button>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      systemInfo: null,
+      hierarchyInfo: null,
+      teamStats: {
+        memberCount: 0,
+        customerCount: 0,
+        orderCount: 0,
+        amount: 0
+      },
+      teamMembers: [],
+      teamCustomers: []
+    }
+  },
+
+  onLoad() {
+    this.loadData()
+  },
+
+  onShow() {
+    this.loadData()
+  },
+
+  methods: {
+    async loadData() {
+      try {
+        // 并发获取所有数据
+        const [systemRes, hierarchyRes, membersRes, customersRes] = await Promise.all([
+          this.fetchSystemInfo(),
+          this.fetchHierarchyInfo(),
+          this.fetchTeamMembers(),
+          this.fetchTeamCustomers()
+        ])
+
+        if (systemRes) {
+          this.systemInfo = systemRes
+        }
+        if (hierarchyRes) {
+          this.hierarchyInfo = hierarchyRes
+        }
+        if (membersRes) {
+          this.teamMembers = membersRes
+        }
+        if (customersRes) {
+          this.teamCustomers = customersRes
+        }
+
+        // 更新统计
+        this.updateStats()
+      } catch (error) {
+        console.error('加载团队数据失败:', error)
+      }
+    },
+
+    // 获取体系信息
+    async fetchSystemInfo() {
+      try {
+        const res = await this.$http.get('/api/admin/supply-system/my-system', {})
+        return res.data || null
+      } catch (e) {
+        return null
+      }
+    },
+
+    // 获取层级关系
+    async fetchHierarchyInfo() {
+      try {
+        const res = await this.$http.get('/api/admin/supply-hierarchy/my', {})
+        return res.data || null
+      } catch (e) {
+        return null
+      }
+    },
+
+    // 获取团队成员
+    async fetchTeamMembers() {
+      try {
+        const res = await this.$http.get('/api/admin/supply-team/members', {
+          systemId: this.systemInfo?.id
+        })
+        return res.data || []
+      } catch (e) {
+        return []
+      }
+    },
+
+    // 获取团队客户
+    async fetchTeamCustomers() {
+      try {
+        const res = await this.$http.get('/api/admin/supply-team/customers', {
+          systemId: this.systemInfo?.id,
+          parentId: null,
+          page: 1,
+          size: 50
+        })
+        return res.data || []
+      } catch (e) {
+        return []
+      }
+    },
+
+    // 更新统计数据
+    updateStats() {
+      this.teamStats.memberCount = this.teamMembers.length
+      this.teamStats.customerCount = this.teamCustomers.length
+    },
+
+    // 跳转管理页面
+    goToManage() {
+      uni.navigateTo({
+        url: '/pages/vendor/team-manage/team-manage'
+      })
+    },
+
+    // 跳转客户列表
+    goToCustomers() {
+      uni.navigateTo({
+        url: '/pages/vendor/team-customers/team-customers'
+      })
+    }
+  }
+}
+</script>
+
+<style>
+.page-team-system {
+  background-color: #FFF7ED;
+  min-height: 100vh;
+  padding-bottom: 100rpx;
+}
+
+/* 体系头部 */
+.system-header {
+  background-color: #fff;
+  padding: 30rpx;
+  margin-bottom: 24rpx;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
+}
+
+.system-info {
+  display: flex;
+  align-items: center;
+}
+
+.system-icon {
+  font-size: 64rpx;
+  margin-right: 20rpx;
+}
+
+.system-name {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.system-desc {
+  font-size: 24rpx;
+  color: #94A3B8;
+  margin-top: 4rpx;
+}
+
+.system-status {
+  padding: 8rpx 20rpx;
+  border-radius: 20rpx;
+  font-size: 24rpx;
+}
+
+.system-status.active {
+  background-color: #E6F7EE;
+  color: #65C29A;
+}
+
+.system-status.inactive {
+  background-color: #FFF7EE;
+  color: #F97316;
+}
+
+/* 区块 */
+.section {
+  background-color: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  margin: 0 30rpx 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
+}
+
+.section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 24rpx;
+}
+
+.section-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.section-action {
+  font-size: 24rpx;
+  color: #F97316;
+}
+
+/* 层级关系 */
+.hierarchy-grid {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.hierarchy-item {
+  flex: 1;
+  min-width: 48%;
+  padding: 20rpx;
+  background-color: #FFF7ED;
+  border-radius: 12rpx;
+  margin-bottom: 16rpx;
+}
+
+.hierarchy-label {
+  font-size: 24rpx;
+  color: #666;
+  margin-bottom: 12rpx;
+}
+
+.hierarchy-person {
+  display: flex;
+  align-items: center;
+}
+
+.avatar-img {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 50%;
+  margin-right: 16rpx;
+}
+
+.avatar-placeholder {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 50%;
+  background-color: #F5FAFE;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 32rpx;
+  margin-right: 16rpx;
+}
+
+.person-info {
+  flex: 1;
+}
+
+.person-name {
+  font-size: 26rpx;
+  color: #333;
+  display: block;
+}
+
+.person-level {
+  font-size: 20rpx;
+  color: #94A3B8;
+}
+
+.hierarchy-empty {
+  font-size: 24rpx;
+  color: #94A3B8;
+}
+
+.hierarchy-stat {
+  flex: 1;
+  min-width: 48%;
+  padding: 20rpx;
+  background-color: #F5FAFE;
+  border-radius: 12rpx;
+  text-align: center;
+}
+
+.stat-value {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #2F5496;
+  display: block;
+}
+
+.stat-label {
+  font-size: 22rpx;
+  color: #666;
+}
+
+/* 统计网格 */
+.stats-grid {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.stat-card {
+  width: 50%;
+  padding: 20rpx;
+  text-align: center;
+  border-right: 1rpx solid #FED7AA;
+  border-bottom: 1rpx solid #FED7AA;
+}
+
+.stat-num {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #F97316;
+  display: block;
+}
+
+.stat-name {
+  font-size: 22rpx;
+  color: #666;
+}
+
+/* 成员列表 */
+.member-item {
+  display: flex;
+  align-items: center;
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #FED7AA;
+}
+
+.member-item:last-child {
+  border-bottom: none;
+}
+
+.member-avatar {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 50%;
+  margin-right: 16rpx;
+}
+
+.member-avatar-placeholder {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 50%;
+  background-color: #F5FAFE;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 32rpx;
+  margin-right: 16rpx;
+}
+
+.member-info {
+  flex: 1;
+}
+
+.member-name {
+  font-size: 26rpx;
+  color: #333;
+  display: block;
+}
+
+.member-role {
+  font-size: 20rpx;
+  color: #94A3B8;
+}
+
+.member-stats {
+  text-align: right;
+}
+
+.member-count {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #F97316;
+  display: block;
+}
+
+.member-label {
+  font-size: 20rpx;
+  color: #666;
+}
+
+.empty-tip {
+  text-align: center;
+  padding: 40rpx;
+  color: #94A3B8;
+  font-size: 26rpx;
+}
+
+/* 客户列表 */
+.customer-item {
+  display: flex;
+  align-items: center;
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #FED7AA;
+}
+
+.customer-item:last-child {
+  border-bottom: none;
+}
+
+.customer-avatar {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 50%;
+  background-color: #F5FAFE;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 16rpx;
+}
+
+.customer-icon {
+  font-size: 32rpx;
+}
+
+.customer-info {
+  flex: 1;
+}
+
+.customer-name {
+  font-size: 26rpx;
+  color: #333;
+  display: block;
+}
+
+.customer-meta {
+  font-size: 20rpx;
+  color: #94A3B8;
+}
+
+.customer-stats {
+  text-align: right;
+}
+
+.customer-orders {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #F97316;
+  display: block;
+}
+
+.customer-label {
+  font-size: 20rpx;
+  color: #666;
+}
+
+/* 操作按钮 */
+.action-bar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background-color: #fff;
+  padding: 20rpx 30rpx;
+  display: flex;
+  gap: 20rpx;
+  box-shadow: 0 -2rpx 12rpx rgba(0,0,0,0.05);
+}
+
+.btn-manage, .btn-customers {
+  flex: 1;
+  height: 80rpx;
+  line-height: 80rpx;
+  border-radius: 40rpx;
+  font-size: 28rpx;
+  border: none;
+}
+
+.btn-manage {
+  background-color: #F97316;
+  color: #fff;
+}
+
+.btn-customers {
+  background-color: #fff;
+  color: #F97316;
+  border: 2rpx solid #F97316;
+}
+</style>

+ 1 - 1
cfc-frontend/pages/wealth-detail/member-wealth-detail.vue

@@ -12,7 +12,7 @@
         <text class="avatar-text">{{ memberInfo.name && memberInfo.name.charAt(0) || '孩' }}</text>
       </view>
       <view class="member-info">
-        <text class="member-name">{{ memberInfo.name || '孩子' }}</text>
+        <text class="member-name">{{ memberInfo.name || '成员' }}</text>
         <text class="member-role">财富维度</text>
       </view>
       <view class="member-score" v-if="dualDimension">

+ 1 - 1
cfc-frontend/pages/wealth-sub/insurance-planning.vue

@@ -83,7 +83,7 @@
               <view class="member-avatar">{{ (m.name || '未知').substring(0, 1) }}</view>
               <view class="member-info">
                 <text class="member-name">{{ m.name || '未知' }}</text>
-                <text class="member-role">{{ m.familyRole || (m.memberType === 'parent' ? '家长' : '孩子') }}</text>
+                <text class="member-role">{{ m.familyRole || (m.memberType === 'parent' ? '家长' : '成员') }}</text>
               </view>
               <view class="member-policy-badge" v-if="m.policyCount > 0">
                 <text>{{ m.policyCount }}份保单</text>

+ 1 - 1
cfc-frontend/pages/wealth/index.vue

@@ -446,7 +446,7 @@ export default {
         var res = await getChildren()
         this.children = res.data || []
       } catch (e) {
-        console.error('获取孩子列表失败', e)
+        console.error('获取成员列表失败', e)
       }
     },
     async loadWealthDetail() {

+ 1 - 1
cfc-frontend/pages/wisdom-detail/member-wisdom-detail.vue

@@ -14,7 +14,7 @@
         <text class="avatar-text">{{ memberInfo.name && memberInfo.name.charAt(0) || '孩' }}</text>
       </view>
       <view class="member-info">
-        <text class="member-name">{{ memberInfo.name || '孩子' }}</text>
+        <text class="member-name">{{ memberInfo.name || '成员' }}</text>
         <text class="member-role">智慧维度</text>
       </view>
       <view class="member-score" v-if="dualDimension">

+ 12 - 0
cfc-frontend/utils/api.js

@@ -1613,6 +1613,16 @@ export const vendorInfo = () => {
   return request('/api/vendor/info', 'POST', {})
 }
 
+/**
+ * 获取供应商团队客户列表
+ * 后端接口:POST /api/admin/supply-team/customers
+ * 参数:systemId, parentId, page, size
+ * 返回:records + hasMore
+ */
+export const getTeamCustomers = (params) => {
+  return request('/api/admin/supply-team/customers', 'POST', params)
+}
+
 //  幸福邻里部落 - 商品系统 
 
 // 获取公开配置(pages/health/report-upload 依赖此函数读取 report_upload_category_id)
@@ -2357,6 +2367,8 @@ export const getReportPayload = (reportId) => request('/api/health/report/payloa
 
 export const editHealthReport = (reportId, payload, subjectId) => request('/api/health/report/edit', 'POST', { reportId, payload, subjectId })
 
+export const reassignReport = (reportId, subjectId) => request('/api/health/report/reassign', 'POST', { reportId, subjectId })
+
 export const deleteReport = (reportId) => request('/api/health/report/delete', 'POST', { reportId })
 
 export const downloadReportFile = (reportId) => request('/api/health/report/download', 'POST', { reportId })

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-242a0ba763f631ca1e298c4e9c4174968bc989f0
+78bc1b329a155e5954d75bf647dd7a7e43feaf67

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1501",
+  "version": "1.0.1510",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1501",
+      "version": "1.0.1510",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1502",
+  "version": "1.0.1511",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 116 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,122 @@
 
 ---
 
+## v1.0.1511 (2026-10-05)
+
+### 新功能
+- add batch shelve + category selection for product management
+
+### 其他
+-   * Backend: POST /api/admin/product/batch-shelve with {productIds, shelve}
+-   * ProductService.batchAdminShelve() validates all products are in approvable status before bulk update
+-   * Frontend: ProductManage.vue - multi-select table row + dropdown '批量上架/下架' with confirmation
+-   * api/admin.js: batchShelveProduct()
+- - Task 2: Category selection on product edit page
+-   * ProductEdit.vue adds categoryId field to form, loaded via getCategoryTree(true)
+-   * UI uses el-option-group grouped by parent category, searchable/filterable
+-   * On save/edit, categoryId is included in payload, enabling frontend mall category filtering
+- - Docs: PROJECT-OVERVIEW.md and API_REFERENCE.md updated with new endpoints
+- 
+
+
+## v1.0.1510 (2026-10-05)
+
+### Bug 修复
+- 删除重复的 methods 块,修复 'Unexpected end of input'
+- 重构模板为单根元素,修复 'should contain exactly one root element'
+
+### 其他
+- 但 L298 又出现了一个相同的 methods: { ... } 块,导致整个 script 语法结构断裂:
+-   L139: methods: {   ← 第一个 methods 块(正确)
+-   ...
+-   L297: },
+-   L298: methods: {   ← 重复,整块内容照抄了 L139–L297
+-   ...
+-   L509: }
+-   L510: }
+-   L511: }
+-   L512: </script>
+- Node 报 'Unexpected end of input'(L422),是因为重复块内部的多层闭括号
+- 把 methods 对象的闭合、export default 的闭合都提前结束了,导致后续 </script>
+- 和 style 被当作 script 的一部分解析。
+- 修复:删除 L298–L510 的重复块,在原 methods 块末尾补充 }; 闭合。
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Vue 单根元素要求,编译报 'Component template should contain exactly one root element'。
+- 修复:将 save-bar 移入条件渲染的 section 内(v-if="renderedSnapshot"),
+- 外层 <view class="portrait-edit-page"> 成为唯一根元素。
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+
+## v1.0.1509 (2026-10-05)
+
+### 重构
+- 移除孤儿 DanShop 模块 DDL(danshop_* 十张空表无代码引用)
+
+
+## v1.0.1508 (2026-10-05)
+
+### 新功能
+- 我的分享海报改为五维能量数据卡
+
+### 其他
+- - 新增五维能量数据卡(canvas 绘制:综合分大字 + 五维彩色圆/分数/进度条)与未解锁全景时的品牌渐变卡
+- - profile.vue 接入 getEnergySandbox('family'),移除旧 share-qr-modal 实现;浮标样式由 emoji 改为 CSS 绘制二维码图形
+- 
+- fix(index-home): 删除 loadNotifications 后遗留的悬空 promise 链
+- }).catch(function() {}) 与 },使整页 script 无法解析:
+-       }).catch(function() {})
+-     },
+-     }).catch(function() {})   <- 悬空,无对应的 Promise 链
+-     },                        <- 多余的方法闭合
+-     // ===== 能量/积分/CF值变更通知 =====
+- 该语法错误在 HEAD 中已存在,且 392480520 的两个父提交同样损坏。此前一直
+- 未暴露,是因为差量编译从未重新解析此文件;本次重新编译才报出:
+- Module build failed ... 语法错误: Unexpected token, expected "," (1137:5)
+- 无任何逻辑缺失,纯语法残留,故仅删除这两行,未改动方法体。
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+
+## v1.0.1507 (2026-10-05)
+
+### Bug 修复
+- ProductEdit.vue addBundleItem 防止 null 报错
+
+### 其他
+- - 防止 TypeError: Cannot read properties of null (reading 'bundleItems')
+- 
+
+
+## v1.0.1506 (2026-10-05)
+
+### 文档
+- 记录健康报告归属重分配设计文档与实现计划 本次变更包括: 1. specs/2026-10-05-health-report-reassign-design.md: 功能设计规格(接口、流程、权限、风险、验收标准) 2. docs/superpowers/plans/2026-10-05-health-report-reassign-plan.md: 7 个任务的实施计划(Controller、Service、查询修复、前端、测试、文档) 3. PROJECT-OVERVIEW.md v2.15: 更新版本号与状态栏,标注健康报告归属重分配已实施(全栈已完成)
+
+### 新功能
+- 管理端报告归属管理页改用 Element UI 组件 该页面从 Arco Design 迁移至 Element UI 规范,新增家庭选择过滤、归属状态标签、归属成员展示、刷新功能等。展示列表新增「调整归属」操作入口,点击后弹出归属成员选择器。
+- replace user-facing '孩子' terminology with '成员'/'家庭成员' across frontend - assessment flow, family member pages, health questionnaires, activity pages, and UI text updates per product decision that system has no 'child' concept; users see only self + family members, child status derived from relationship; internal entity/DB references (Child entity, child_id columns) preserved unchanged
+
+
+## v1.0.1505 (2026-10-04)
+
+### 新功能
+- Add article type and fix search bindings
+
+
+## v1.0.1504 (2026-10-04)
+
+### 新功能
+- Remove custom page title from health plan summary, use pages.json configured title
+
+
+## v1.0.1503 (2026-10-04)
+
+### 新功能
+- portrait-edit UI redesign - remove AI generation, enlarge textarea, fix save button to bottom
+
+
 ## v1.0.1501 (2026-10-04)
 
 <<<<<<< Updated upstream

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff