Explorar o código

Merge remote-tracking branch 'origin/cfclub' into cfclub

liaoxg hai 1 mes
pai
achega
437d9434c8
Modificáronse 50 ficheiros con 1338 adicións e 207 borrados
  1. 6 1
      AGENTS.md
  2. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthExerciseController.java
  3. 8 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthMealController.java
  4. 4 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java
  5. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthSleepController.java
  6. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/InviteController.java
  7. 25 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java
  8. 8 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideTeamController.java
  9. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/LoginResultDTO.java
  10. 15 2
      cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java
  11. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/service/CouponService.java
  12. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/service/EmotionCheckinService.java
  13. 5 9
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyJoinRequestService.java
  14. 10 0
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java
  15. 31 30
      cfc-backend/src/main/java/com/etotem/cfc/service/InviteCardService.java
  16. 41 0
      cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java
  17. 34 0
      cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
  18. 2 0
      cfc-backend/src/main/resources/application.yml
  19. 95 0
      cfc-backend/src/test/java/com/etotem/cfc/service/AiGatewayTimeoutTest.java
  20. 15 17
      cfc-frontend/App.vue
  21. 1 1
      cfc-frontend/components/ProfileMenu.vue
  22. 9 6
      cfc-frontend/components/share-mixin.js
  23. 3 3
      cfc-frontend/pages/activity/activity-detail/activity-detail.vue
  24. 3 3
      cfc-frontend/pages/article-center/article-detail.vue
  25. 8 69
      cfc-frontend/pages/discover-detail/product-detail/product-detail.vue
  26. 4 1
      cfc-frontend/pages/growth-main/index.vue
  27. 1 0
      cfc-frontend/pages/growth/diet-checkin.vue
  28. 2 1
      cfc-frontend/pages/growth/exercise-checkin.vue
  29. 1 0
      cfc-frontend/pages/growth/growth-emotion-checkin.vue
  30. 1 0
      cfc-frontend/pages/growth/sleep-checkin.vue
  31. 80 5
      cfc-frontend/pages/health/gut-flora-foods-detail.vue
  32. 12 5
      cfc-frontend/pages/index-home/index.vue
  33. 15 9
      cfc-frontend/pages/login/login.vue
  34. 8 2
      cfc-frontend/pages/profile-extra/family-members.vue
  35. 4 4
      cfc-frontend/pages/profile/index.vue
  36. 2 2
      cfc-frontend/pages/profile/report-detail.vue
  37. 2 2
      cfc-frontend/pages/promotion/index.vue
  38. 20 6
      cfc-frontend/pages/shop/checkout/checkout.vue
  39. 15 9
      cfc-frontend/pages/user-edit/user-edit.vue
  40. 2 2
      cfc-frontend/scripts/audit-duplicate-api-calls.js
  41. 16 8
      cfc-frontend/utils/api.js
  42. 1 1
      cfc-web/.last_build_commit
  43. 2 2
      cfc-web/package-lock.json
  44. 1 1
      cfc-web/package.json
  45. 65 0
      cfc-web/public/CHANGELOG-v1.0.md
  46. 66 1
      cfc-web/public/CHANGELOG.md
  47. 181 0
      运营文案/官方号搜索收割内容-第一批.md
  48. 57 0
      运营文案/小红书内容/小红书发布指南.md
  49. 171 0
      运营文案/小红书同类账号探索报告.md
  50. 274 0
      运营文案/浠艾福小红书运营方略.md

+ 6 - 1
AGENTS.md

@@ -363,4 +363,9 @@ curl -X POST http://localhost:9082/api/migration/run
 | `cfc-backend/AGENTS.md` | 后端详细规范 + 数据库迁移工作流 |
 | `cfc-backend/AGENTS.md` | 后端详细规范 + 数据库迁移工作流 |
 | `cfc-frontend/AGENTS.md` | 小程序前端规范 |
 | `cfc-frontend/AGENTS.md` | 小程序前端规范 |
 | `cfc-web/AGENTS.md` | Web管理端规范 |
 | `cfc-web/AGENTS.md` | Web管理端规范 |
-| `tests/AGENTS.md` | 分层测试策略 |
+| `tests/AGENTS.md` | 分层测试策略 |
+
+## opencode自动启动
+当用户要求启动本地opencode时:自动在cli命令行执行如下命令:
+$env:OPENCODE_ADMIN_PASSWORD = "IwinTrue@123"
+opencode web --port 4090 --hostname 127.0.0.1

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthExerciseController.java

@@ -45,6 +45,9 @@ public class HealthExerciseController {
         if (record.getMemberId() == null) {
         if (record.getMemberId() == null) {
             record.setMemberId(currentMemberId);
             record.setMemberId(currentMemberId);
         }
         }
+        if (record.getMemberId() == null) return Result.error("memberId不能为空");
+        if (record.getExerciseType() == null || record.getExerciseType().trim().isEmpty()) return Result.error("请选择运动类型");
+        if (record.getDurationMinutes() == null || record.getDurationMinutes() <= 0) return Result.error("运动时长需大于0分钟");
         record.setCreatedAt(new Date());
         record.setCreatedAt(new Date());
         healthExerciseRecordMapper.insert(record);
         healthExerciseRecordMapper.insert(record);
                 // 触发画像重算
                 // 触发画像重算

+ 8 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthMealController.java

@@ -45,7 +45,14 @@ public class HealthMealController {
 
 
     @Operation(summary = "创建饮食打卡")
     @Operation(summary = "创建饮食打卡")
     @PostMapping("/create")
     @PostMapping("/create")
-    public Result<HealthMealRecord> create(@RequestBody HealthMealRecord record) {
+    public Result<HealthMealRecord> create(@RequestBody HealthMealRecord record,
+                                           @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+        if (record.getMemberId() == null) {
+            record.setMemberId(currentMemberId);
+        }
+        if (record.getMemberId() == null) return Result.error("memberId不能为空");
+        if (record.getMealType() == null || record.getMealType().trim().isEmpty()) return Result.error("请选择餐次");
+        if (record.getFoodItems() == null || record.getFoodItems().trim().isEmpty()) return Result.error("请填写食物清单");
         record.setCreatedAt(new Date());
         record.setCreatedAt(new Date());
         healthMealRecordMapper.insert(record);
         healthMealRecordMapper.insert(record);
         // 饮食打卡返积分:每次记录奖励 5 积分(不影响打卡主流程)
         // 饮食打卡返积分:每次记录奖励 5 积分(不影响打卡主流程)

+ 4 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java

@@ -1463,8 +1463,10 @@ public class HealthReportController {
             // 指定报告:返回该报告的食材推荐
             // 指定报告:返回该报告的食材推荐
             foods = foodService.listByReportId(reportId);
             foods = foodService.listByReportId(reportId);
         } else {
         } else {
-            // 未指定报告:返回维护的"当前食材"推荐(food_recommend_idx,有新报告时 upsert 更新)
-            Long uid = userId != null ? userId : -1L;
+            // 未指定报告:优先用 memberId(孩子视角),其次 JWT userId(家长视角)
+            // food_recommend_idx 以 subjectId 维度写入,家长 JWT userId 查不到孩子的推荐
+            Long memberId = params != null ? ParamUtils.getLong(params.get("memberId")) : null;
+            Long uid = memberId != null ? memberId : (userId != null ? userId : -1L);
             foods = foodRecommendService.getCurrentFoodsAsFoodList(uid);
             foods = foodRecommendService.getCurrentFoodsAsFoodList(uid);
         }
         }
         return Result.success(foods);
         return Result.success(foods);

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthSleepController.java

@@ -45,6 +45,9 @@ public class HealthSleepController {
         if (record.getMemberId() == null) {
         if (record.getMemberId() == null) {
             record.setMemberId(currentMemberId);
             record.setMemberId(currentMemberId);
         }
         }
+        if (record.getMemberId() == null) return Result.error("memberId不能为空");
+        if (record.getSleepTime() == null) return Result.error("请选择入睡时间");
+        if (record.getWakeTime() == null) return Result.error("请选择醒来时间");
         record.setCreatedAt(new Date());
         record.setCreatedAt(new Date());
         healthSleepRecordMapper.insert(record);
         healthSleepRecordMapper.insert(record);
                 // 触发画像重算
                 // 触发画像重算

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

@@ -66,7 +66,7 @@ public class InviteController {
     public Result<Map<String, Object>> getQrCode(@RequestAttribute("userId") Long userId) {
     public Result<Map<String, Object>> getQrCode(@RequestAttribute("userId") Long userId) {
         try {
         try {
             String referralCode = commissionService.getOrCreateReferralCode(userId);
             String referralCode = commissionService.getOrCreateReferralCode(userId);
-            String qrBase64 = wechatService.generateWxacode(referralCode, "pages/invite/join");
+            String qrBase64 = wechatService.generateWxacode("ref=" + referralCode, "pages/invite/join");
             Map<String, Object> result = new HashMap<>();
             Map<String, Object> result = new HashMap<>();
             result.put("qrCodeBase64", "data:image/png;base64," + qrBase64);
             result.put("qrCodeBase64", "data:image/png;base64," + qrBase64);
             result.put("referralCode", referralCode);
             result.put("referralCode", referralCode);

+ 25 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java

@@ -165,7 +165,11 @@ public class AdminController {
         List<Map<String, String>> sortSpecs = params.get("sort") != null
         List<Map<String, String>> sortSpecs = params.get("sort") != null
                 ? (List<Map<String, String>>) params.get("sort")
                 ? (List<Map<String, String>>) params.get("sort")
                 : null;
                 : null;
-        SortUtil.applySort(wrapper, sortSpecs, null);
+        if (sortSpecs != null && !sortSpecs.isEmpty()) {
+            SortUtil.applySort(wrapper, sortSpecs, null);
+        } else {
+            wrapper.orderByDesc(User::getCreatedAt);
+        }
         Page<User> result = userMapper.selectPage(pageParam, wrapper);
         Page<User> result = userMapper.selectPage(pageParam, wrapper);
         return Result.success(result);
         return Result.success(result);
     }
     }
@@ -1398,6 +1402,9 @@ public class AdminController {
     @Resource
     @Resource
     private com.etotem.cfc.service.ProductOrderService productOrderService;
     private com.etotem.cfc.service.ProductOrderService productOrderService;
 
 
+    @Resource
+    private com.etotem.cfc.service.CouponService couponService;
+
     @PostMapping("/product/pending")
     @PostMapping("/product/pending")
     public Result<java.util.List<com.etotem.cfc.dto.ProductDTO>> productPending() {
     public Result<java.util.List<com.etotem.cfc.dto.ProductDTO>> productPending() {
         return productService.pendingReview();
         return productService.pendingReview();
@@ -1730,6 +1737,23 @@ public class AdminController {
                 record.setApproveRemark("管理员会员维护");
                 record.setApproveRemark("管理员会员维护");
                 record.setCreatedAt(new Date());
                 record.setCreatedAt(new Date());
                 memberUpgradeRecordMapper.insert(record);
                 memberUpgradeRecordMapper.insert(record);
+
+                // 升级到付费等级(非 FREE)时自动发放 JOIN 赠券(幂等:family_coupon 唯一键防重)
+                if (!"FREE".equals(newLevel)) {
+                    User changedUser = userMapper.selectById(targetUserId);
+                    if (changedUser != null && changedUser.getFamilyId() != null) {
+                        try {
+                            couponService.grantFamilyJoinCoupons(
+                                    changedUser.getFamilyId(), newLevel,
+                                    "admin:" + adminUserId);
+                            changes.append("发放").append(newLevel).append("JOIN券; ");
+                        } catch (Exception ex) {
+                            changes.append("发放").append(newLevel).append("JOIN券异常:").append(ex.getMessage()).append("; ");
+                        }
+                    } else {
+                        changes.append("升级").append(newLevel).append("(无家庭,跳过发券); ");
+                    }
+                }
             }
             }
         }
         }
 
 

+ 8 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideTeamController.java

@@ -155,6 +155,14 @@ public class GuideTeamController {
             return Result.error("您已经在该团队中");
             return Result.error("您已经在该团队中");
         }
         }
 
 
+        // 检查是否已有其他上级:每个规划师只能有一个直接上级
+        GuideHierarchy existingHierarchy = guideHierarchyService.getOne(new LambdaQueryWrapper<GuideHierarchy>()
+                .eq(GuideHierarchy::getChildId, userId)
+                .eq(GuideHierarchy::getStatus, "active"));
+        if (existingHierarchy != null) {
+            return Result.error("您已有上级规划师,不能加入此团队。如需更换团队,请先离开当前团队。");
+        }
+
         // 建立团队关系
         // 建立团队关系
         guideHierarchyService.addChildGuide(guideId, userId);
         guideHierarchyService.addChildGuide(guideId, userId);
         guideInviteCodeService.useInviteCode(inviteCode, userId);
         guideInviteCodeService.useInviteCode(inviteCode, userId);

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/LoginResultDTO.java

@@ -10,6 +10,7 @@ public class LoginResultDTO {
     private java.util.List<String> roles;
     private java.util.List<String> roles;
     private Long familyId;
     private Long familyId;
     private String nickname;
     private String nickname;
+    private String phone;
     private Long currentChildId; // 当前切换到的孩子ID(如果切换了)
     private Long currentChildId; // 当前切换到的孩子ID(如果切换了)
     private Boolean isNewUser; // 是否新用户
     private Boolean isNewUser; // 是否新用户
     private String openid; // 微信openid,用于前端缓存实现自动登录
     private String openid; // 微信openid,用于前端缓存实现自动登录

+ 15 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java

@@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.HttpEntity;
 import org.springframework.http.HttpEntity;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.web.client.RestTemplate;
 import org.springframework.web.client.RestTemplate;
 import java.util.*;
 import java.util.*;
@@ -37,6 +38,11 @@ public class AiGateway {
     @Value("${python.timeout-ms:15000}")
     @Value("${python.timeout-ms:15000}")
     private int timeoutMs;
     private int timeoutMs;
 
 
+    // LLM 推理可达 30-60s(画像/问卷更长),read 超时需覆盖推理耗时,
+    // 否则 AI 长任务永远被超时截断退化到 mock 兜底
+    @Value("${python.read-timeout-ms:120000}")
+    private int readTimeoutMs;
+
     @Value("${python.circuit-breaker.failure-threshold:3}")
     @Value("${python.circuit-breaker.failure-threshold:3}")
     private int failureThreshold;
     private int failureThreshold;
 
 
@@ -56,9 +62,16 @@ public class AiGateway {
 
 
     @PostConstruct
     @PostConstruct
     public void init() {
     public void init() {
+        // 应用超时到 HTTP 客户端:
+        // connect 用 python.timeout-ms(快速失败),read 用 python.read-timeout-ms(覆盖 LLM 推理耗时)。
+        // 之前用 `new RestTemplate()` 默认无限阻塞,LangGraph 服务挂起时后端线程被拖死,前端永远等不到响应
+        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+        factory.setConnectTimeout(timeoutMs);
+        factory.setReadTimeout(readTimeoutMs);
+        restTemplate.setRequestFactory(factory);
         if (enabled) {
         if (enabled) {
-            log.info("AiGateway 已启用: baseUrl={}, timeout={}ms",
-                    baseUrl, timeoutMs);
+            log.info("AiGateway 已启用: baseUrl={}, connectTimeout={}ms, readTimeout={}ms",
+                    baseUrl, timeoutMs, readTimeoutMs);
         } else {
         } else {
             log.info("AiGateway 已禁用, 所有请求走 LangGraph");
             log.info("AiGateway 已禁用, 所有请求走 LangGraph");
         }
         }

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CouponService.java

@@ -86,6 +86,7 @@ public class CouponService {
         item.put("receivedAt", fc.getReceivedAt());
         item.put("receivedAt", fc.getReceivedAt());
         item.put("usedAt", fc.getUsedAt());
         item.put("usedAt", fc.getUsedAt());
         item.put("orderId", fc.getOrderId());
         item.put("orderId", fc.getOrderId());
+        item.put("productId", coupon.getProductId());
         return item;
         return item;
     }
     }
 
 

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/EmotionCheckinService.java

@@ -49,6 +49,9 @@ public class EmotionCheckinService {
         if (child == null) {
         if (child == null) {
             throw new RuntimeException("孩子不存在");
             throw new RuntimeException("孩子不存在");
         }
         }
+        if (dto.getEmotionType() == null || dto.getEmotionType().trim().isEmpty()) {
+            throw new RuntimeException("请选择情绪类型");
+        }
 
 
         EmotionCheckin checkin = new EmotionCheckin();
         EmotionCheckin checkin = new EmotionCheckin();
         checkin.setChildId(dto.getMemberId());
         checkin.setChildId(dto.getMemberId());

+ 5 - 9
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyJoinRequestService.java

@@ -303,7 +303,7 @@ public class FamilyJoinRequestService {
      * 平台推荐人绑定(需求:未注册/已注册但无推荐人 → 绑定为邀请人的上级推荐人;已有推荐人 → 不动):
      * 平台推荐人绑定(需求:未注册/已注册但无推荐人 → 绑定为邀请人的上级推荐人;已有推荐人 → 不动):
      * - 仅 token 路径(inviterUserId 非空)有明确邀请人,短码路径无邀请人则不处理;
      * - 仅 token 路径(inviterUserId 非空)有明确邀请人,短码路径无邀请人则不处理;
      * - 被邀请者已有推荐人则跳过(需求:已有推荐人不动);
      * - 被邀请者已有推荐人则跳过(需求:已有推荐人不动);
-     * - 邀请人自身无推荐人时,将邀请人作为直接推荐人(fallback,避免新用户无推荐人);
+     * - 邀请人自身无推荐人则跳过(被邀请者保持无推荐人);
      * - 幂等性由 commissionService.bindReferrerByUserId 双保险(已有推荐人忽略);
      * - 幂等性由 commissionService.bindReferrerByUserId 双保险(已有推荐人忽略);
      * - 绑定失败不阻断审批(仅记录日志)。
      * - 绑定失败不阻断审批(仅记录日志)。
      */
      */
@@ -317,16 +317,12 @@ public class FamilyJoinRequestService {
         }
         }
         try {
         try {
             User inviter = userMapper.selectById(inviterUserId);
             User inviter = userMapper.selectById(inviterUserId);
-            if (inviter == null) {
+            if (inviter == null || inviter.getReferrerId() == null) {
                 return;
                 return;
             }
             }
-            // 邀请人有上级推荐人 → 绑定到上级(保持推荐链不跨家庭)
-            // 邀请人无上级推荐人 → 将邀请人作为直接推荐人(避免新用户无推荐人)
-            Long targetReferrerId = (inviter.getReferrerId() != null)
-                    ? inviter.getReferrerId() : inviter.getId();
-            commissionService.bindReferrerByUserId(requester.getId(), targetReferrerId);
-            log.info("家庭加入已绑定平台推荐人: userId={}, targetReferrerId={}, inviterId={}, requestId={}",
-                    requester.getId(), targetReferrerId, inviter.getId(), request.getId());
+            commissionService.bindReferrerByUserId(requester.getId(), inviter.getReferrerId());
+            log.info("家庭加入已绑定平台推荐人: userId={}, referrerId={}, requestId={}",
+                    requester.getId(), inviter.getReferrerId(), request.getId());
         } catch (Exception e) {
         } catch (Exception e) {
             log.warn("家庭加入绑定平台推荐人失败(不阻断审批): requestId={}, error={}", request.getId(), e.getMessage());
             log.warn("家庭加入绑定平台推荐人失败(不阻断审批): requestId={}, error={}", request.getId(), e.getMessage());
         }
         }

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

@@ -73,6 +73,9 @@ public class FamilyMemberService {
     @Resource
     @Resource
     private HealthReportMapper healthReportMapper;
     private HealthReportMapper healthReportMapper;
 
 
+    @Resource
+    private MembershipService membershipService;
+
     /**
     /**
      * 添加家庭成员
      * 添加家庭成员
      */
      */
@@ -615,6 +618,13 @@ public class FamilyMemberService {
         member.setStatus("active");
         member.setStatus("active");
         familyMemberMapper.updateById(member);
         familyMemberMapper.updateById(member);
 
 
+        // 原家庭是付费会员时,将会员权益合并到邀请人家庭
+        try {
+            membershipService.mergeMembershipToTargetFamily(oldFamilyId, targetFamilyId);
+        } catch (Exception e) {
+            log.warn("合并会员权益异常: oldFamilyId={}, targetFamilyId={}", oldFamilyId, targetFamilyId, e);
+        }
+
         user.setFamilyId(targetFamilyId);
         user.setFamilyId(targetFamilyId);
         user.setUpdatedAt(new Date());
         user.setUpdatedAt(new Date());
         userMapper.updateById(user);
         userMapper.updateById(user);

+ 31 - 30
cfc-backend/src/main/java/com/etotem/cfc/service/InviteCardService.java

@@ -217,40 +217,41 @@ private String generateCode(String type) {
         result.put("familyId", card.getRefId());
         result.put("familyId", card.getRefId());
         result.put("familyName", family.getName());
         result.put("familyName", family.getName());
         result.put("type", "guide");
         result.put("type", "guide");
-        result.put("needsTeacherRegister", true);
-
-        if (user != null && "teacher".equals(user.getRole())) {
-            // 将规划师绑定到家庭
-            family.setTeacherId(user.getId());
-            family.setUpdatedAt(new Date());
-            familyMapper.updateById(family);
-
-            // 更新规划师的 teacherFamilyIds
-            String currentIds = user.getTeacherFamilyIds();
-            String newFamilyId = String.valueOf(family.getId());
-            String updatedIds;
-            if (currentIds == null || currentIds.isEmpty()) {
-                updatedIds = newFamilyId;
-            } else if (!currentIds.contains(newFamilyId)) {
-                updatedIds = currentIds + "," + newFamilyId;
-            } else {
-                updatedIds = currentIds;
-            }
-            user.setTeacherFamilyIds(updatedIds);
-            user.setUpdatedAt(new Date());
-            userMapper.updateById(user);
 
 
-            card.setUsed(1);
-            card.setUpdatedAt(LocalDateTime.now());
-            inviteCardMapper.updateById(card);
+        if (user == null || !"teacher".equals(user.getRole())) {
+            // 接受者必须已是规划师才能通过邀请绑定家庭;非规划师需先自行申请成为规划师
+            return Result.error("您不是成长规划师,无法接受此邀请。请先自行申请成为规划师。");
+        }
 
 
-            result.put("needsTeacherRegister", false);
-            result.put("token", jwtConfig.generateToken(user.getId(), user.getRole()));
-            result.put("userId", user.getId());
-            result.put("role", "teacher");
-            result.put("teacherFamilyIds", updatedIds);
+        // 将规划师绑定到家庭
+        family.setTeacherId(user.getId());
+        family.setUpdatedAt(new Date());
+        familyMapper.updateById(family);
+
+        // 更新规划师的 teacherFamilyIds
+        String currentIds = user.getTeacherFamilyIds();
+        String newFamilyId = String.valueOf(family.getId());
+        String updatedIds;
+        if (currentIds == null || currentIds.isEmpty()) {
+            updatedIds = newFamilyId;
+        } else if (!currentIds.contains(newFamilyId)) {
+            updatedIds = currentIds + "," + newFamilyId;
+        } else {
+            updatedIds = currentIds;
         }
         }
+        user.setTeacherFamilyIds(updatedIds);
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
 
 
+        card.setUsed(1);
+        card.setUpdatedAt(LocalDateTime.now());
+        inviteCardMapper.updateById(card);
+
+        result.put("needsTeacherRegister", false);
+        result.put("token", jwtConfig.generateToken(user.getId(), user.getRole()));
+        result.put("userId", user.getId());
+        result.put("role", "teacher");
+        result.put("teacherFamilyIds", updatedIds);
         return Result.success(result);
         return Result.success(result);
     }
     }
 
 

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

@@ -161,6 +161,47 @@ public class MembershipService implements MembershipServiceInterface {
         return level != null ? toLevelDTO(level) : null;
         return level != null ? toLevelDTO(level) : null;
     }
     }
 
 
+    /**
+     * 将原家庭的活跃付费会员合并到目标家庭(成员被邀请走时调用)
+     * 规则:
+     * - 原家庭存在活跃付费会员(paymentStatus=paid 且未过期)时,把该会员记录转移到目标家庭
+     * - 若目标家庭已有到期日晚于原家庭会员的活跃付费会员,则跳过(目标家庭权益不低于原家庭)
+     * @return 是否发生了合并
+     */
+    public boolean mergeMembershipToTargetFamily(Long oldFamilyId, Long targetFamilyId) {
+        if (oldFamilyId == null || targetFamilyId == null || oldFamilyId.equals(targetFamilyId)) {
+            return false;
+        }
+        Date now = new Date();
+        FamilyMembership oldMembership = membershipMapper.selectOne(
+                new LambdaQueryWrapper<FamilyMembership>()
+                        .eq(FamilyMembership::getFamilyId, oldFamilyId)
+                        .eq(FamilyMembership::getPaymentStatus, "paid")
+                        .gt(FamilyMembership::getEndDate, now)
+                        .orderByDesc(FamilyMembership::getEndDate)
+                        .last("LIMIT 1"));
+        if (oldMembership == null) {
+            return false;
+        }
+        // 目标家庭已有更晚到期的活跃会员则不覆盖
+        FamilyMembership targetMembership = membershipMapper.selectOne(
+                new LambdaQueryWrapper<FamilyMembership>()
+                        .eq(FamilyMembership::getFamilyId, targetFamilyId)
+                        .eq(FamilyMembership::getPaymentStatus, "paid")
+                        .gt(FamilyMembership::getEndDate, now)
+                        .orderByDesc(FamilyMembership::getEndDate)
+                        .last("LIMIT 1"));
+        if (targetMembership != null && targetMembership.getEndDate().after(oldMembership.getEndDate())) {
+            return false;
+        }
+        oldMembership.setFamilyId(targetFamilyId);
+        oldMembership.setUpdatedAt(now);
+        membershipMapper.updateById(oldMembership);
+        log.info("会员合并: 原家庭{}的{}会员(至{})合并到目标家庭{}",
+                oldFamilyId, oldMembership.getLevelCode(), oldMembership.getEndDate(), targetFamilyId);
+        return true;
+    }
+
     /**
     /**
      * 检查功能权限
      * 检查功能权限
      */
      */

+ 34 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java

@@ -160,6 +160,7 @@ private FamilyInvitationService familyInvitationService;
         result.setRoles(user.getRoles());
         result.setRoles(user.getRoles());
         result.setFamilyId(user.getFamilyId());
         result.setFamilyId(user.getFamilyId());
         result.setNickname(user.getNickname());
         result.setNickname(user.getNickname());
+        result.setPhone(user.getPhone());
         result.setIsNewUser(isNewUser);
         result.setIsNewUser(isNewUser);
         result.setOpenid(openid);
         result.setOpenid(openid);
 
 
@@ -237,6 +238,7 @@ private FamilyInvitationService familyInvitationService;
   result.setRoles(user.getRoles());
   result.setRoles(user.getRoles());
   result.setFamilyId(user.getFamilyId());
   result.setFamilyId(user.getFamilyId());
   result.setNickname(user.getNickname());
   result.setNickname(user.getNickname());
+  result.setPhone(user.getPhone());
   result.setIsNewUser(isNewUser);
   result.setIsNewUser(isNewUser);
   result.setNeedsRoleSelect(isNewUser);
   result.setNeedsRoleSelect(isNewUser);
   result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
   result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
@@ -295,6 +297,7 @@ private FamilyInvitationService familyInvitationService;
         result.setRoles(user.getRoles());
         result.setRoles(user.getRoles());
         result.setFamilyId(user.getFamilyId());
         result.setFamilyId(user.getFamilyId());
         result.setNickname(user.getNickname());
         result.setNickname(user.getNickname());
+        result.setPhone(user.getPhone());
         result.setIsNewUser(false);
         result.setIsNewUser(false);
         result.setOpenid(openid);
         result.setOpenid(openid);
         result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
         result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
@@ -328,6 +331,7 @@ private FamilyInvitationService familyInvitationService;
         result.setRole(user.getRole());
         result.setRole(user.getRole());
         result.setFamilyId(user.getFamilyId());
         result.setFamilyId(user.getFamilyId());
         result.setNickname(user.getNickname());
         result.setNickname(user.getNickname());
+        result.setPhone(user.getPhone());
         result.setIsNewUser(false);
         result.setIsNewUser(false);
         result.setOpenid(openid);
         result.setOpenid(openid);
         result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
         result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
@@ -556,6 +560,7 @@ private FamilyInvitationService familyInvitationService;
         result.setRoles(user.getRoles());
         result.setRoles(user.getRoles());
         result.setFamilyId(user.getFamilyId());
         result.setFamilyId(user.getFamilyId());
         result.setNickname(user.getNickname());
         result.setNickname(user.getNickname());
+        result.setPhone(user.getPhone());
         result.setIsNewUser(true);
         result.setIsNewUser(true);
         result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
         result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
         populateTeacherFields(result, user);
         populateTeacherFields(result, user);
@@ -632,6 +637,7 @@ private FamilyInvitationService familyInvitationService;
         result.setRoles(user.getRoles());
         result.setRoles(user.getRoles());
         result.setFamilyId(user.getFamilyId());
         result.setFamilyId(user.getFamilyId());
         result.setNickname(user.getNickname());
         result.setNickname(user.getNickname());
+        result.setPhone(user.getPhone());
         result.setIsNewUser(false);
         result.setIsNewUser(false);
         result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
         result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
         populateTeacherFields(result, user);
         populateTeacherFields(result, user);
@@ -788,6 +794,17 @@ private FamilyInvitationService familyInvitationService;
         List<User> parents = userMapper.selectList(new LambdaQueryWrapper<User>()
         List<User> parents = userMapper.selectList(new LambdaQueryWrapper<User>()
                 .eq(User::getFamilyId, familyId)
                 .eq(User::getFamilyId, familyId)
                 .eq(User::getRole, "parent"));
                 .eq(User::getRole, "parent"));
+        // 查询 family_members 以便找到当前用户记录,用于计算相对关系标签
+        List<FamilyMember> familyMembersForVisible = familyMemberMapper.selectList(
+                new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getFamilyId, familyId));
+        // 查找当前用户在 family_members 中的记录(用于计算相对关系标签)
+        FamilyMember viewerFm = null;
+        for (FamilyMember fm : familyMembersForVisible) {
+            if (userId.equals(fm.getUserId())) {
+                viewerFm = fm;
+                break;
+            }
+        }
         for (User p : parents) {
         for (User p : parents) {
             if (p.getId().equals(userId) || (p.getShowToFamily() != null && p.getShowToFamily() == 1)) {
             if (p.getId().equals(userId) || (p.getShowToFamily() != null && p.getShowToFamily() == 1)) {
                 FamilyMemberVO vo = new FamilyMemberVO();
                 FamilyMemberVO vo = new FamilyMemberVO();
@@ -807,6 +824,20 @@ private FamilyInvitationService familyInvitationService;
                 vo.setShowToFamily(p.getShowToFamily() != null ? p.getShowToFamily() : 1);
                 vo.setShowToFamily(p.getShowToFamily() != null ? p.getShowToFamily() : 1);
                 vo.setIsAdmin(isAdmin);
                 vo.setIsAdmin(isAdmin);
                 vo.setIsSelf(p.getId().equals(userId));
                 vo.setIsSelf(p.getId().equals(userId));
+                // 计算 relativeLabel:与当前用户的辈分关系
+                if (viewerFm != null && viewerFm.getGeneration() != null) {
+                    int viewerGen = viewerFm.getGeneration();
+                    // 同家庭的其他家长(非创建者)generation 通常为 0,配偶/同辈
+                    int genDiff = 0 - viewerGen; // 简化:假定 parent 在同辈(generation=0)
+                    if (genDiff < -1) {
+                        // target 是晚辈
+                        vo.setRelativeLabel("male".equals(p.getGender()) ? "儿子" : "女儿");
+                    } else if (genDiff > 1) {
+                        vo.setRelativeLabel("male".equals(p.getGender()) ? "父亲" : "母亲");
+                    } else {
+                        vo.setRelativeLabel("male".equals(p.getGender()) ? "丈夫" : "妻子");
+                    }
+                }
                 seenUserIds.add(p.getId());
                 seenUserIds.add(p.getId());
                 result.add(vo);
                 result.add(vo);
             }
             }
@@ -916,6 +947,7 @@ private FamilyInvitationService familyInvitationService;
         result.setRole(user.getRole());
         result.setRole(user.getRole());
         result.setFamilyId(user.getFamilyId());
         result.setFamilyId(user.getFamilyId());
         result.setNickname(user.getNickname());
         result.setNickname(user.getNickname());
+        result.setPhone(user.getPhone());
         result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
         result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
         populateTeacherFields(result, user);
         populateTeacherFields(result, user);
 
 
@@ -1486,6 +1518,7 @@ FamilyMember child = familyMemberMapper.selectById(memberId);
             result.setRoles(user.getRoles());
             result.setRoles(user.getRoles());
             result.setFamilyId(user.getFamilyId());
             result.setFamilyId(user.getFamilyId());
             result.setNickname(user.getNickname());
             result.setNickname(user.getNickname());
+            result.setPhone(user.getPhone());
             result.setIsNewUser(true);
             result.setIsNewUser(true);
             result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
             result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
             populateTeacherFields(result, user);
             populateTeacherFields(result, user);
@@ -1552,6 +1585,7 @@ FamilyMember child = familyMemberMapper.selectById(memberId);
         result.setRoles(user.getRoles());
         result.setRoles(user.getRoles());
         result.setFamilyId(user.getFamilyId());
         result.setFamilyId(user.getFamilyId());
         result.setNickname(user.getNickname());
         result.setNickname(user.getNickname());
+        result.setPhone(user.getPhone());
         result.setIsNewUser(isNewUser);
         result.setIsNewUser(isNewUser);
         result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
         result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
         populateTeacherFields(result, user);
         populateTeacherFields(result, user);

+ 2 - 0
cfc-backend/src/main/resources/application.yml

@@ -104,6 +104,8 @@ python:
   enabled: true
   enabled: true
   base-url: https://ai.etotem.com.cn
   base-url: https://ai.etotem.com.cn
   timeout-ms: 15000
   timeout-ms: 15000
+  # LLM 推理可达 30-60s(画像/问卷更长),read 超时必须覆盖推理耗时,connect 超时才用 timeout-ms
+  read-timeout-ms: 120000
   circuit-breaker:
   circuit-breaker:
     failure-threshold: 3
     failure-threshold: 3
     reset-timeout-ms: 30000
     reset-timeout-ms: 30000

+ 95 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/AiGatewayTimeoutTest.java

@@ -0,0 +1,95 @@
+package com.etotem.cfc.service;
+
+import com.sun.net.httpserver.HttpServer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.InetSocketAddress;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.when;
+
+/**
+ * 回归测试:LangGraph 服务挂起(accept 连接但永不响应)时,
+ * AiGateway 必须在 python.timeout-ms 内返回 null(走 mock 兜底),
+ * 而不是无限阻塞拖死后端线程。
+ *
+ * 背景:AiGateway 曾使用 `new RestTemplate()`(默认无限 connect/read 超时),
+ * 配置的 python.timeout-ms 从未应用,LangGraph 挂起时问卷生成永远转圈。
+ */
+@ExtendWith(MockitoExtension.class)
+class AiGatewayTimeoutTest {
+
+    private HttpServer server;
+    private AiGateway gateway;
+
+    @Mock
+    private PiisService piisService;
+
+    private void startHungServer() throws IOException {
+        server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
+        server.setExecutor(runnable -> {
+            Thread t = new Thread(runnable);
+            t.setDaemon(true);
+            t.start();
+        });
+        server.createContext("/", exchange -> {
+            // 不写响应,模拟挂起/卡死的上游服务
+            try {
+                Thread.sleep(TimeUnit.HOURS.toMillis(1));
+            } catch (InterruptedException ignored) {
+            }
+        });
+        server.start();
+    }
+
+    private void buildGateway(int timeoutMs) throws Exception {
+        gateway = new AiGateway();
+        ReflectionTestUtils.setField(gateway, "enabled", true);
+        ReflectionTestUtils.setField(gateway, "baseUrl",
+                "http://127.0.0.1:" + server.getAddress().getPort());
+        ReflectionTestUtils.setField(gateway, "timeoutMs", timeoutMs);
+        ReflectionTestUtils.setField(gateway, "readTimeoutMs", timeoutMs);
+        ReflectionTestUtils.setField(gateway, "failureThreshold", 3);
+        ReflectionTestUtils.setField(gateway, "resetTimeoutMs", 30000);
+        when(piisService.sanitizeContext(any(), isNull())).thenAnswer(inv -> inv.getArgument(0));
+        ReflectionTestUtils.setField(gateway, "piisService", piisService);
+        gateway.init();
+    }
+
+    @AfterEach
+    void tearDown() {
+        if (server != null) server.stop(0);
+        if (gateway != null) gateway = null;
+    }
+
+    @Test
+    void generateQuestionnaire_upstreamHangs_returnsNullWithinTimeout() throws Exception {
+        startHungServer();
+        int timeoutMs = 500;
+        buildGateway(timeoutMs);
+
+        Map<String, Object> inputs = new HashMap<>();
+        inputs.put("member_name", "测试");
+        inputs.put("relationship_type", "child");
+
+        long start = System.currentTimeMillis();
+        Map<String, Object> result = gateway.generateQuestionnaire(inputs);
+        long elapsed = System.currentTimeMillis() - start;
+
+        assertNull(result, "上游挂起时应返回 null 触发 mock 兜底");
+        assertTrue(elapsed < TimeUnit.SECONDS.toMillis(5),
+                "应在 ~" + timeoutMs + "ms 超时返回,实际耗时 " + elapsed + "ms");
+    }
+}

+ 15 - 17
cfc-frontend/App.vue

@@ -9,10 +9,12 @@ export default {
     console.log('App Launch', options)
     console.log('App Launch', options)
     // 捕获分享/邀请参数(普通分享链接 ?inviteCode=xxx):未登录写入 storage 供注册登录后绑定推荐人,已登录直接绑定
     // 捕获分享/邀请参数(普通分享链接 ?inviteCode=xxx):未登录写入 storage 供注册登录后绑定推荐人,已登录直接绑定
     this.captureReferralParams(options)
     this.captureReferralParams(options)
-    // 分享/深链冷启动(携带分享参数)放行到落地页,不套 splash,避免分享的文章/小程序页面打不开。
-    // 家庭邀请分享卡为 ?token=xxx(family-members 分享)或 ?familyInviteCode=xxx(user-edit 直达),
-    // 必须一并识别,否则冷启动会被 redirectTo splash 劫持导致邀请页打不开(空白/闪到首页)
-    var hasDeepLink = options && options.query && (options.query.inviteCode || options.query.id || options.query.token || options.query.familyInviteCode)
+// 分享/深链冷启动(携带分享参数)放行到落地页,不套 splash,避免分享的文章/小程序页面打不开
+    // inviteCode=推荐码(文章/活动/商品分享)| refCode=推广码 | token/familyInviteCode=家庭邀请令牌 | id=内容详情
+    var hasDeepLink = options && options.query && (
+      options.query.inviteCode || options.query.refCode || options.query.token ||
+      options.query.familyInviteCode || options.query.id
+    )
     // 冷启动:跳转到 splash 页展示过渡动画,2秒后自动进入首页
     // 冷启动:跳转到 splash 页展示过渡动画,2秒后自动进入首页
     if (options.scene !== 1047 && options.scene !== 1048 && options.scene !== 1049 && !hasDeepLink) {
     if (options.scene !== 1047 && options.scene !== 1048 && options.scene !== 1049 && !hasDeepLink) {
       uni.setStorageSync('_isFirstLaunch', '1')
       uni.setStorageSync('_isFirstLaunch', '1')
@@ -59,33 +61,29 @@ export default {
     console.log('App Hide')
     console.log('App Hide')
   },
   },
   methods: {
   methods: {
-    // 捕获分享/邀请参数:普通分享链接 /pages/xxx?inviteCode=yyy 冷/热启动进入时,
+    // 捕获分享/邀请参数:普通分享链接 /pages/xxx?refCode=yyy 冷/热启动进入时,
     // 新用户(未登录)写入 storage,登录注册后由 login / user-edit 消费绑定推荐人;
     // 新用户(未登录)写入 storage,登录注册后由 login / user-edit 消费绑定推荐人;
     // 已登录用户直接绑定(boundInviteCode 防重,避免重复请求)
     // 已登录用户直接绑定(boundInviteCode 防重,避免重复请求)
     captureReferralParams(options) {
     captureReferralParams(options) {
       if (!options || !options.query) return
       if (!options || !options.query) return
-      var code = options.query.inviteCode
+      var query = options.query
+      // 推荐码统一为 refCode;inviteCode 兼容历史分享链接(旧 share-mixin 用 inviteCode 承载推荐码)
+      var code = query.refCode || query.inviteCode
       if (!code) return
       if (!code) return
       if (uni.getStorageSync('token')) {
       if (uni.getStorageSync('token')) {
         var bound = uni.getStorageSync('boundInviteCode')
         var bound = uni.getStorageSync('boundInviteCode')
         if (bound === code) return
         if (bound === code) return
-        bindReferralIfNotSelf(code).then(function(res) {
-          if (res && res.code === 200) {
+bindReferralIfNotSelf(code).then(function(res) {
+          if (res && (res.code === 200 || res.code === 400)) {
             uni.setStorageSync('boundInviteCode', code)
             uni.setStorageSync('boundInviteCode', code)
-            console.log('分享参数推荐人绑定成功', code)
+            console.log('分享参数推荐人绑定结果', res.code, code)
           }
           }
         }).catch(function(e) {
         }).catch(function(e) {
-          // 后端已绑定(code=500 message=已绑定推荐人)→ 记录防重,避免每次冷/热启动重复请求/重复提示
-          if (e && e.code !== undefined && e.code !== 401) {
-            uni.setStorageSync('boundInviteCode', code)
-            console.log('分享参数推荐人绑定失败(业务拒绝,已防重)', code, e.message || '')
-          } else {
-            console.log('分享参数推荐人绑定失败(网络/登录态)', e)
-          }
+          console.log('分享参数推荐人绑定失败', e)
         })
         })
         return
         return
       }
       }
-      uni.setStorageSync('inviteCode', code)
+      uni.setStorageSync('refCode', code)
       if (options.query.inviteType) {
       if (options.query.inviteType) {
         uni.setStorageSync('inviteType', options.query.inviteType)
         uni.setStorageSync('inviteType', options.query.inviteType)
       }
       }

+ 1 - 1
cfc-frontend/components/ProfileMenu.vue

@@ -251,7 +251,7 @@ var res = await getReferralCode()
       const config = inviteTypes[type] || inviteTypes.family
       const config = inviteTypes[type] || inviteTypes.family
       var path = config.path
       var path = config.path
       if (this.referralCode) {
       if (this.referralCode) {
-        path = path + '&inviteCode=' + this.referralCode
+        path = path + '&refCode=' + this.referralCode
       }
       }
       this.$emit('invite-generate', {
       this.$emit('invite-generate', {
         title: config.title,
         title: config.title,

+ 9 - 6
cfc-frontend/components/share-mixin.js

@@ -18,9 +18,12 @@ export default {
     }
     }
   },
   },
   onLoad(options) {
   onLoad(options) {
-    // 自动绑定:从分享链接进入时,绑定邀请码
-    if (options && options.inviteCode) {
-      this.autoBindInviteCode(options.inviteCode)
+    // 自动绑定:从分享链接进入时,绑定推荐人(推荐码统一为 refCode,兼容老 inviteCode 链接)
+    if (options) {
+      var code = options.refCode || options.inviteCode
+      if (code) {
+        this.autoBindInviteCode(code)
+      }
     }
     }
   },
   },
   methods: {
   methods: {
@@ -53,9 +56,9 @@ export default {
      * 规则:如果用户已有邀请人,则忽略此邀请码
      * 规则:如果用户已有邀请人,则忽略此邀请码
      */
      */
     async autoBindInviteCode(code) {
     async autoBindInviteCode(code) {
-      // 未登录时存 storage,登录后由 login 页面处理
+      // 未登录时存 storage,登录后由 login 页面处理(推荐码统一为 refCode)
       if (!uni.getStorageSync('token')) {
       if (!uni.getStorageSync('token')) {
-        uni.setStorageSync('inviteCode', code)
+        uni.setStorageSync('refCode', code)
         return
         return
       }
       }
 
 
@@ -87,7 +90,7 @@ export default {
       if (!this.sharePath) return ''
       if (!this.sharePath) return ''
       if (this.referralCode) {
       if (this.referralCode) {
         var sep = this.sharePath.indexOf('?') === -1 ? '?' : '&'
         var sep = this.sharePath.indexOf('?') === -1 ? '?' : '&'
-        return this.sharePath + sep + 'inviteCode=' + this.referralCode
+        return this.sharePath + sep + 'refCode=' + this.referralCode
       }
       }
       return this.sharePath
       return this.sharePath
     },
     },

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

@@ -314,9 +314,9 @@ export default {
     }
     }
   },
   },
   onLoad: function(options) {
   onLoad: function(options) {
-    // 兼容旧分享链接:inviteCode 参数(shareMixin onLoad 也会处理,此处显式兜底)
-    if (options && options.inviteCode) {
-      this.autoBindInviteCode(options.inviteCode)
+    // 兼容旧分享链接:refCode / inviteCode 参数(shareMixin onLoad 也会处理,此处显式兜底)
+    if (options && (options.refCode || options.inviteCode)) {
+      this.autoBindInviteCode(options.refCode || options.inviteCode)
     }
     }
     var aid = ''
     var aid = ''
     if (options && options.id) {
     if (options && options.id) {

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

@@ -108,9 +108,9 @@ export default {
   onLoad(options) {
   onLoad(options) {
     this.mascotIcon = this.mascotList[Math.floor(Math.random() * 2)].icon
     this.mascotIcon = this.mascotList[Math.floor(Math.random() * 2)].icon
     this.mascotName = this.mascotList[Math.floor(Math.random() * 2)].name
     this.mascotName = this.mascotList[Math.floor(Math.random() * 2)].name
-    // 兼容旧分享链接:inviteCode 参数(shareMixin onLoad 也会处理,此处显式兜底)
-    if (options && options.inviteCode) {
-      this.autoBindInviteCode(options.inviteCode)
+    // 兼容旧分享链接:refCode / inviteCode 参数(shareMixin onLoad 也会处理,此处显式兜底)
+    if (options && (options.refCode || options.inviteCode)) {
+      this.autoBindInviteCode(options.refCode || options.inviteCode)
     }
     }
     var aid = ''
     var aid = ''
     if (options && options.id) {
     if (options && options.id) {

+ 8 - 69
cfc-frontend/pages/discover-detail/product-detail/product-detail.vue

@@ -99,19 +99,6 @@
         </view>
         </view>
       </view>
       </view>
 
 
-      <!-- 积分兑换优惠券 -->
-      <view class="coupon-exchange-section" v-if="productCoupons.length > 0">
-        <text class="section-title">积分兑换优惠券</text>
-        <view class="coupon-item" v-for="(item, index) in productCoupons" :key="getCouponKey(item, index)">
-          <view class="coupon-info">
-            <text class="coupon-name">{{ item.name }}</text>
-            <text class="coupon-desc">{{ getCouponDesc(item) }}</text>
-          </view>
-          <view class="coupon-price">{{ item.pointsPrice }}积分</view>
-          <button class="coupon-exchange-btn" @click="doExchangeCoupon(item)">兑换</button>
-        </view>
-      </view>
-
       <view class="intro-section" v-if="product.intro">
       <view class="intro-section" v-if="product.intro">
         <text class="section-title">商品介绍</text>
         <text class="section-title">商品介绍</text>
         <mp-html :content="product.intro" :domain="apiBaseUrl" />
         <mp-html :content="product.intro" :domain="apiBaseUrl" />
@@ -160,7 +147,7 @@
 
 
 <script>
 <script>
 import config from '@/config.js'
 import config from '@/config.js'
-import { productDetail, productOrderCreate, productOrderPay, cartAdd, getShareQrCode, productSpecMap, getProductCoupons, exchangeCoupon, getMyMembership } from '@/utils/api.js'
+import { productDetail, productOrderCreate, productOrderPay, cartAdd, getShareQrCode, productSpecMap, getMyMembership } from '@/utils/api.js'
 import MpHtml from '@/components/mp-html/mp-html.vue'
 import MpHtml from '@/components/mp-html/mp-html.vue'
 import ContentSharePoster from '@/components/ContentSharePoster.vue'
 import ContentSharePoster from '@/components/ContentSharePoster.vue'
 import shareMixin from '../../../components/share-mixin.js'
 import shareMixin from '../../../components/share-mixin.js'
@@ -180,7 +167,6 @@ export default {
       specGroups: [],
       specGroups: [],
       selectedSpecs: {},
       selectedSpecs: {},
       selectedSku: null,
       selectedSku: null,
-      productCoupons: [],
       cfEarnedValue: 0,
       cfEarnedValue: 0,
       // 会员身份(实时调 getMyMembership,FREE=非会员)
       // 会员身份(实时调 getMyMembership,FREE=非会员)
       isMember: false,
       isMember: false,
@@ -317,9 +303,9 @@ export default {
     }
     }
   },
   },
   onLoad(options) {
   onLoad(options) {
-    // 兼容旧分享链接:inviteCode 参数(shareMixin onLoad 也会处理,此处显式兜底)
-    if (options && options.inviteCode) {
-      this.autoBindInviteCode(options.inviteCode)
+    // 兼容旧分享链接:refCode / inviteCode 参数(shareMixin onLoad 也会处理,此处显式兜底)
+    if (options && (options.refCode || options.inviteCode)) {
+      this.autoBindInviteCode(options.refCode || options.inviteCode)
     }
     }
     var pid = options && options.id ? options.id : ''
     var pid = options && options.id ? options.id : ''
     if (options && options.scene) {
     if (options && options.scene) {
@@ -379,6 +365,10 @@ export default {
         if (res.code === 200 && res.data) {
         if (res.code === 200 && res.data) {
           that.product = res.data
           that.product = res.data
           that.bundleItems = (res.data && res.data.bundleItems) || []
           that.bundleItems = (res.data && res.data.bundleItems) || []
+          // 计算可得CF值:floor(原价/100) × pointsMultiplier
+          if (res.data.price && res.data.pointsMultiplier) {
+            that.cfEarnedValue = Math.floor(res.data.price / 100) * res.data.pointsMultiplier;
+          }
           // 赠品项初始化选择状态(响应式)
           // 赠品项初始化选择状态(响应式)
           var cfgList = res.data.giftConfigs
           var cfgList = res.data.giftConfigs
           if (cfgList && cfgList.length > 0) {
           if (cfgList && cfgList.length > 0) {
@@ -394,7 +384,6 @@ export default {
             '/pages/discover-detail/product-detail/product-detail?id=' + that.product.id
             '/pages/discover-detail/product-detail/product-detail?id=' + that.product.id
           )
           )
           that.loadSkuData(id)
           that.loadSkuData(id)
-          that.loadProductCoupons(parseInt(id))
         }
         }
       }).catch(function() {
       }).catch(function() {
         that.loading = false
         that.loading = false
@@ -418,60 +407,10 @@ export default {
       })
       })
     },
     },
 
 
-    loadProductCoupons: function(productId) {
-      if (!productId) return
-      var that = this
-      // 计算可得CF值:floor(原价/100) × pointsMultiplier
-      if (that.product && that.product.price && that.product.pointsMultiplier) {
-        that.cfEarnedValue = Math.floor(that.product.price / 100) * that.product.pointsMultiplier;
-      }
-      // 未登录(如从分享链接进入)时跳过优惠券查询,避免 /api/coupon/product 返回 401
-      // 触发"登录已过期"并强制跳登录页;优惠券兑换是登录用户功能,未登录无需展示
-      if (!uni.getStorageSync('token')) return
-      getProductCoupons({ productId: productId }).then(function(res) {
-        if (res.code === 200) {
-          that.productCoupons = res.data || []
-        }
-      }).catch(function() {
-        // 兑换券加载失败不阻塞页面
-      })
-    },
-
-    getCouponKey: function(item, index) {
-      return item.id + '-' + index
-    },
     getBundleKey: function(item, idx) {
     getBundleKey: function(item, idx) {
       return item ? (item.childProductId || idx) : idx
       return item ? (item.childProductId || idx) : idx
     },
     },
 
 
-    getCouponDesc: function(item) {
-      if (item.type === 'DISCOUNT') {
-        return '折扣券 ' + (item.discountRate / 100).toFixed(1) + '折'
-      }
-      return '立减 ' + (item.value / 100).toFixed(2) + '元'
-    },
-
-    doExchangeCoupon: function(item) {
-      var that = this
-      uni.showModal({
-        title: '确认兑换',
-        content: '消耗 ' + item.pointsPrice + ' 积分兑换「' + item.name + '」?',
-        success: function(r) {
-          if (!r.confirm) return
-          exchangeCoupon({ couponId: item.id }).then(function(res) {
-            if (res.code === 200) {
-              uni.showToast({ title: '兑换成功', icon: 'success' })
-              that.loadProductCoupons(that.product.id)
-            } else {
-              uni.showToast({ title: res.message || '兑换失败', icon: 'none' })
-            }
-          }).catch(function(e) {
-            uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
-          })
-        }
-      })
-    },
-
     selectSpec: function(groupId, option) {
     selectSpec: function(groupId, option) {
       if (this.selectedSpecs[groupId] === option.id) {
       if (this.selectedSpecs[groupId] === option.id) {
         this.$delete(this.selectedSpecs, groupId)
         this.$delete(this.selectedSpecs, groupId)

+ 4 - 1
cfc-frontend/pages/growth-main/index.vue

@@ -309,7 +309,10 @@ export default {
     goToTasks: function() { uni.navigateTo({ url: '/pages/child/tasks' }) },
     goToTasks: function() { uni.navigateTo({ url: '/pages/child/tasks' }) },
     handleGuestFeatureTap: function() { uni.showToast({ title: '登录后即可体验该功能', icon: 'none' }) },
     handleGuestFeatureTap: function() { uni.showToast({ title: '登录后即可体验该功能', icon: 'none' }) },
     goToCaution: function() { uni.navigateTo({ url: '/pages/growth/caution-detail' }) },
     goToCaution: function() { uni.navigateTo({ url: '/pages/growth/caution-detail' }) },
-    goToFoods: function() { uni.navigateTo({ url: '/pages/health/gut-flora-foods-detail' }) },
+    goToFoods: function() {
+      var mid = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || ''
+      uni.navigateTo({ url: '/pages/health/gut-flora-foods-detail' + (mid ? '?memberId=' + mid : '') })
+    },
     goToArticle: function(article) { var id = article && article.id ? article.id : article; if (!id) return; uni.navigateTo({ url: '/pages/article-center/article-detail?id=' + id }) },
     goToArticle: function(article) { var id = article && article.id ? article.id : article; if (!id) return; uni.navigateTo({ url: '/pages/article-center/article-detail?id=' + id }) },
     goToProduct: function(prod) { var id = prod && prod.id ? prod.id : prod; if (!id) return; uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + id }) },
     goToProduct: function(prod) { var id = prod && prod.id ? prod.id : prod; if (!id) return; uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + id }) },
     goMoreProducts: function() { uni.navigateTo({ url: '/pages/shop/index/index' }) },
     goMoreProducts: function() { uni.navigateTo({ url: '/pages/shop/index/index' }) },

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

@@ -298,6 +298,7 @@ export default {
     async doCheckin() {
     async doCheckin() {
       var self = this
       var self = this
       if (!self.form.mealType) { uni.showToast({ title: '请选择餐次', icon: 'none' }); return }
       if (!self.form.mealType) { uni.showToast({ title: '请选择餐次', icon: 'none' }); return }
+      if (!self.form.foodItems || !self.form.foodItems.trim()) { 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
       self.submitting = true
       try {
       try {

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

@@ -311,7 +311,8 @@ export default {
     async doCheckin() {
     async doCheckin() {
       var self = this
       var self = this
       if (!self.form.exerciseType) { uni.showToast({ title: '请选择运动类型', icon: 'none' }); return }
       if (!self.form.exerciseType) { uni.showToast({ title: '请选择运动类型', icon: 'none' }); return }
-      if (!self.form.durationMinutes) { uni.showToast({ title: '请填写运动时长', icon: 'none' }); return }
+      var durationNum = parseInt(self.form.durationMinutes)
+      if (!self.form.durationMinutes || isNaN(durationNum) || durationNum <= 0) { 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
       self.submitting = true
       try {
       try {

+ 1 - 0
cfc-frontend/pages/growth/growth-emotion-checkin.vue

@@ -280,6 +280,7 @@ export default {
     },
     },
     async doCheckin() {
     async doCheckin() {
       var self = this
       var self = this
+      if (!self.selectedEmotion) { 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
       self.submitting = true
       try {
       try {

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

@@ -143,6 +143,7 @@ export default {
       var self = this
       var self = this
       if (!self.form.sleepTime) { uni.showToast({ title: '请选择入睡时间', icon: 'none' }); return }
       if (!self.form.sleepTime) { uni.showToast({ title: '请选择入睡时间', icon: 'none' }); return }
       if (!self.form.wakeTime) { 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
       self.submitting = true
       try {
       try {

+ 80 - 5
cfc-frontend/pages/health/gut-flora-foods-detail.vue

@@ -9,6 +9,33 @@
       </view>
       </view>
     </view>
     </view>
 
 
+    <!-- 分类筛选 + 排序 -->
+    <view class="filter-bar">
+      <scroll-view class="cat-scroll" scroll-x>
+        <view class="cat-list">
+          <view
+            class="cat-chip"
+            :class="{'cat-chip-active': activeCategory === 'all'}"
+            @tap="switchCategory('all')"
+          >
+            <text class="cat-chip-text">全部</text>
+          </view>
+          <view
+            class="cat-chip"
+            v-for="cat in categories"
+            :key="cat"
+            :class="{'cat-chip-active': activeCategory === cat}"
+            @tap="switchCategory(cat)"
+          >
+            <text class="cat-chip-text">{{ cat }}</text>
+          </view>
+        </view>
+      </scroll-view>
+      <view class="sort-btn" @tap="toggleSort">
+        <text class="sort-btn-text">{{ sortDesc ? '↓ 推荐值从高到低' : '↑ 推荐值从低到高' }}</text>
+      </view>
+    </view>
+
     <scroll-view class="content" scroll-y>
     <scroll-view class="content" scroll-y>
       <view
       <view
         class="food-card"
         class="food-card"
@@ -92,38 +119,74 @@ export default {
   data() {
   data() {
     return {
     return {
       reportId: null,
       reportId: null,
+      memberId: null,
       reportData: null,
       reportData: null,
       foods: [],
       foods: [],
       isEditing: false,
       isEditing: false,
       editFoods: [],
       editFoods: [],
       searchText: '',
       searchText: '',
+      activeCategory: 'all',
+      sortDesc: true,
       knowledgeVisible: false,
       knowledgeVisible: false,
       knowledgeData: {},
       knowledgeData: {},
       _kbCache: {}
       _kbCache: {}
     }
     }
   },
   },
   computed: {
   computed: {
+    categories: function() {
+      var seen = {}
+      var cats = []
+      var list = this.isEditing ? this.editFoods : this.foods
+      for (var i = 0; i < list.length; i++) {
+        var c = list[i].category || '其它'
+        if (!seen[c]) {
+          seen[c] = true
+          cats.push(c)
+        }
+      }
+      return cats
+    },
     filteredFoods: function() {
     filteredFoods: function() {
       var list = this.isEditing ? this.editFoods : this.foods
       var list = this.isEditing ? this.editFoods : this.foods
       var kw = (this.searchText || '').trim()
       var kw = (this.searchText || '').trim()
+      var self = this
       if (kw) {
       if (kw) {
         list = list.filter(function(item) {
         list = list.filter(function(item) {
           var name = item.foodName || item.name || ''
           var name = item.foodName || item.name || ''
           return name.indexOf(kw) >= 0
           return name.indexOf(kw) >= 0
         })
         })
       }
       }
+      if (this.activeCategory !== 'all') {
+        list = list.filter(function(item) {
+          return (item.category || '其它') === self.activeCategory
+        })
+      }
+      // 按推荐值排序(默认降序)
+      list = list.slice()
+      list.sort(function(a, b) {
+        var sa = parseFloat(a.score) || 0
+        var sb = parseFloat(b.score) || 0
+        return self.sortDesc ? sb - sa : sa - sb
+      })
       return list
       return list
     }
     }
   },
   },
   onLoad: function(options) {
   onLoad: function(options) {
     // 不传 reportId 时取当前用户最新报告的食材推荐
     // 不传 reportId 时取当前用户最新报告的食材推荐
     this.reportId = options.reportId ? parseInt(options.reportId) : null
     this.reportId = options.reportId ? parseInt(options.reportId) : null
+    this.memberId = options.memberId ? parseInt(options.memberId) : null
     this.loadData()
     this.loadData()
   },
   },
   methods: {
   methods: {
     goBack: function() {
     goBack: function() {
       uni.navigateBack()
       uni.navigateBack()
     },
     },
+    switchCategory: function(cat) {
+      this.activeCategory = cat
+    },
+    toggleSort: function() {
+      this.sortDesc = !this.sortDesc
+    },
     /** 推荐值 → 背景色分级:<-30 红,>30 绿,中间每 10 换一档 */
     /** 推荐值 → 背景色分级:<-30 红,>30 绿,中间每 10 换一档 */
     _scoreBand: function(score) {
     _scoreBand: function(score) {
       var s = parseFloat(score)
       var s = parseFloat(score)
@@ -139,7 +202,7 @@ export default {
     },
     },
     loadData: function() {
     loadData: function() {
       var self = this
       var self = this
-      getHealthFoods(self.reportId).then(function(res) {
+      getHealthFoods(self.reportId, self.memberId).then(function(res) {
         if (res.code === 200 && res.data) {
         if (res.code === 200 && res.data) {
           // 后端 Food 实体字段:name, category, score;兼容 foodName(旧 draft)
           // 后端 Food 实体字段:name, category, score;兼容 foodName(旧 draft)
           self.foods = (Array.isArray(res.data) ? res.data : []).map(function(item) {
           self.foods = (Array.isArray(res.data) ? res.data : []).map(function(item) {
@@ -242,7 +305,7 @@ export default {
 .page { min-height: 100vh; background: #f8f8f8; }
 .page { min-height: 100vh; background: #f8f8f8; }
 
 
 /* 搜索框 */
 /* 搜索框 */
-.search-bar { padding: 20rpx 30rpx; background: #fff; }
+.search-bar { padding: 20rpx 30rpx 8rpx; background: #fff; }
 .search-input-wrap {
 .search-input-wrap {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
@@ -254,7 +317,18 @@ export default {
 .search-input { flex: 1; font-size: 28rpx; }
 .search-input { flex: 1; font-size: 28rpx; }
 .search-clear { font-size: 28rpx; color: #999; padding: 0 8rpx; }
 .search-clear { font-size: 28rpx; color: #999; padding: 0 8rpx; }
 
 
-.content { padding: 20rpx 30rpx; height: calc(100vh - 120rpx); }
+/* 分类筛选 + 排序 */
+.filter-bar { display: flex; align-items: center; background: #fff; padding: 8rpx 30rpx 16rpx; box-sizing: border-box; width: 100%; }
+.cat-scroll { flex: 1; white-space: nowrap; min-width: 0; }
+.cat-list { display: inline-flex; padding-right: 8rpx; }
+.cat-chip { display: inline-flex; align-items: center; padding: 10rpx 24rpx; margin-right: 12rpx; background: #f5f5f5; border-radius: 30rpx; flex-shrink: 0; }
+.cat-chip-active { background: #4A9BD7; }
+.cat-chip-text { font-size: 24rpx; color: #666; }
+.cat-chip-active .cat-chip-text { color: #fff; }
+.sort-btn { flex-shrink: 0; margin-left: 16rpx; padding: 10rpx 20rpx; background: #FFF1E6; border-radius: 30rpx; }
+.sort-btn-text { font-size: 24rpx; color: #C2410C; }
+
+.content { padding: 20rpx 30rpx; height: calc(100vh - 200rpx); }
 
 
 .food-card {
 .food-card {
   border-radius: 16rpx;
   border-radius: 16rpx;
@@ -274,8 +348,9 @@ export default {
 .score-band-null { background: #f0f0f0; }
 .score-band-null { background: #f0f0f0; }
 
 
 .food-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8rpx; }
 .food-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8rpx; }
-.food-name { font-size: 30rpx; font-weight: 600; color: #fff; flex: 1; }
-.food-cat-tag { padding: 4rpx 16rpx; background: rgba(255,255,255,0.3); border-radius: 20rpx; font-size: 22rpx; color: #fff; }
+.food-name { font-size: 30rpx; font-weight: 600; color: #fff; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.food-cat-tag { padding: 4rpx 16rpx; background: rgba(255,255,255,0.3); border-radius: 20rpx; font-size: 22rpx; color: #fff; flex-shrink: 0; margin-left: 12rpx; max-width: 40%; box-sizing: border-box; }
+.food-cat-tag text { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
 
 
 .food-score-row { display: flex; align-items: baseline; gap: 12rpx; }
 .food-score-row { display: flex; align-items: baseline; gap: 12rpx; }
 .food-score-label { font-size: 22rpx; color: rgba(255,255,255,0.85); }
 .food-score-label { font-size: 22rpx; color: rgba(255,255,255,0.85); }

+ 12 - 5
cfc-frontend/pages/index-home/index.vue

@@ -669,9 +669,16 @@ membershipDays: 0,
   onLoad(options) {
   onLoad(options) {
     this._onLoadFired = true
     this._onLoadFired = true
     // 兜底:分享卡直达首页(热启动直通等 App 级捕获未覆盖到的路径)时,
     // 兜底:分享卡直达首页(热启动直通等 App 级捕获未覆盖到的路径)时,
-    // 新用户把 inviteCode 写入 storage,登录注册后由 login / user-edit 消费绑定推荐人
-    if (options && options.inviteCode && !uni.getStorageSync('token')) {
-      uni.setStorageSync('inviteCode', options.inviteCode)
+    // 新用户把推荐码写入 storage,登录注册后由 login / user-edit 消费绑定推荐人
+    // 注意:type=parent_invite 的家庭邀请码不存推荐码 storage,避免后续误绑
+    if (options && !uni.getStorageSync('token')) {
+      var refCode = options.refCode
+      if (!refCode && options.inviteCode && options.type !== 'parent_invite') {
+        refCode = options.inviteCode  // 兼容老推荐码链接
+      }
+      if (refCode) {
+        uni.setStorageSync('refCode', refCode)
+      }
     }
     }
     this._doInit(options)
     this._doInit(options)
   },
   },
@@ -1386,7 +1393,7 @@ membershipDays: 0,
     onShareAppMessage: function() {
     onShareAppMessage: function() {
       var path = '/pages/index-home/index'
       var path = '/pages/index-home/index'
       if (this.referralCode) {
       if (this.referralCode) {
-        path = path + '?inviteCode=' + this.referralCode
+        path = path + '?refCode=' + this.referralCode
       }
       }
       return {
       return {
         title: '浠艾福 · 家庭五维幸福成长',
         title: '浠艾福 · 家庭五维幸福成长',
@@ -1398,7 +1405,7 @@ membershipDays: 0,
     onShareTimeline: function() {
     onShareTimeline: function() {
       var query = ''
       var query = ''
       if (this.referralCode) {
       if (this.referralCode) {
-        query = 'inviteCode=' + this.referralCode
+        query = 'refCode=' + this.referralCode
       }
       }
       return {
       return {
         title: '浠艾福 · 家庭五维幸福成长',
         title: '浠艾福 · 家庭五维幸福成长',

+ 15 - 9
cfc-frontend/pages/login/login.vue

@@ -39,6 +39,8 @@
           class="phone-btn"
           class="phone-btn"
           open-type="getPhoneNumber"
           open-type="getPhoneNumber"
           @getphonenumber="getPhoneNumber"
           @getphonenumber="getPhoneNumber"
+          :loading="loading"
+          :disabled="loading"
         >
         >
           <text class="phone-btn-icon">📱</text>
           <text class="phone-btn-icon">📱</text>
           <text>手机号快捷登录</text>
           <text>手机号快捷登录</text>
@@ -84,10 +86,9 @@ export default {
         uni.setStorageSync('inviteCode', options.inviteCode)
         uni.setStorageSync('inviteCode', options.inviteCode)
         uni.setStorageSync('inviteType', options.inviteType || 'family')
         uni.setStorageSync('inviteType', options.inviteType || 'family')
       }
       }
-      // 处理推广码(refCode):来自 join.vue 的推广绑定流程
+      // 处理推荐码(refCode):来自 join.vue 的推广绑定流程,推荐码统一存 refCode storage
       if (options.refCode) {
       if (options.refCode) {
-        uni.setStorageSync('inviteCode', options.refCode)
-        uni.setStorageSync('inviteType', '')
+        uni.setStorageSync('refCode', options.refCode)
       }
       }
       // 存储登录后跳转地址
       // 存储登录后跳转地址
       if (options.redirect) {
       if (options.redirect) {
@@ -123,6 +124,7 @@ export default {
         uni.showToast({ title: '请先同意用户协议', icon: 'none' })
         uni.showToast({ title: '请先同意用户协议', icon: 'none' })
         return
         return
       }
       }
+      if (this.loading) return
       if (!e.detail || !e.detail.code) {
       if (!e.detail || !e.detail.code) {
         uni.showToast({ title: '授权失败,请重试', icon: 'none' })
         uni.showToast({ title: '授权失败,请重试', icon: 'none' })
         return
         return
@@ -180,30 +182,34 @@ export default {
     handlePostLoginRouting() {
     handlePostLoginRouting() {
       const inviteCode = uni.getStorageSync('inviteCode')
       const inviteCode = uni.getStorageSync('inviteCode')
       const inviteType = uni.getStorageSync('inviteType')
       const inviteType = uni.getStorageSync('inviteType')
+      const refCode = uni.getStorageSync('refCode')
 
 
       if (inviteCode && inviteType === 'team') {
       if (inviteCode && inviteType === 'team') {
         this.handleTeamInviteFlow(inviteCode)
         this.handleTeamInviteFlow(inviteCode)
+      } else if (refCode) {
+        this.handleReferralBind(refCode)
       } else if (inviteCode) {
       } else if (inviteCode) {
-        // 非 team 邀请码(如分享文章/商品/活动的邀请码),绑定推荐关系
+        // 兼容:历史 storage inviteCode 承载推荐码的残留
         this.handleReferralBind(inviteCode)
         this.handleReferralBind(inviteCode)
       } else {
       } else {
         this.navigateToHome()
         this.navigateToHome()
       }
       }
     },
     },
-    async handleReferralBind(inviteCode) {
+    async handleReferralBind(refCode) {
       try {
       try {
-        var res = await bindReferralIfNotSelf(inviteCode)
+var res = await bindReferralIfNotSelf(refCode)
         if (res && res.code === 200) {
         if (res && res.code === 200) {
-          console.log('登录后绑定邀请码成功', inviteCode)
+          console.log('登录后绑定推荐人成功', refCode)
         }
         }
       } catch (e) {
       } catch (e) {
         // 后端 Result.error(code=500) → request() 已 toast「已绑定推荐人,不可修改」等业务消息
         // 后端 Result.error(code=500) → request() 已 toast「已绑定推荐人,不可修改」等业务消息
         if (e && e.code !== undefined && e.code !== 401) {
         if (e && e.code !== undefined && e.code !== 401) {
-          console.log('登录后邀请码业务拒绝', inviteCode, e.message || '')
+          console.log('登录后推荐码业务拒绝', refCode, e.message || '')
         } else {
         } else {
-          console.log('登录后邀请码绑定失败', e)
+          console.log('登录后推荐码绑定失败', e)
         }
         }
       }
       }
+      uni.removeStorageSync('refCode')
       uni.removeStorageSync('inviteCode')
       uni.removeStorageSync('inviteCode')
       uni.removeStorageSync('inviteType')
       uni.removeStorageSync('inviteType')
       this.navigateToHome()
       this.navigateToHome()

+ 8 - 2
cfc-frontend/pages/profile-extra/family-members.vue

@@ -32,7 +32,7 @@
 
 
     <!-- ===== 成员卡片列表 ===== -->
     <!-- ===== 成员卡片列表 ===== -->
     <view class="member-list" v-if="filteredMembers.length > 0">
     <view class="member-list" v-if="filteredMembers.length > 0">
-      <view class="member-card" v-for="m in filteredMembers" :key="m.id" @click="showMemberDetail(m)">
+<view class="member-card" v-for="m in filteredMembers" :key="m.id" @click="showMemberDetail(m)">
         <image class="member-avatar" :src="m.avatar || '/static/default-avatar.png'" mode="aspectFill"></image>
         <image class="member-avatar" :src="m.avatar || '/static/default-avatar.png'" mode="aspectFill"></image>
         <view class="member-info">
         <view class="member-info">
           <text class="member-name">{{ m.nickname || '未设置' }}</text>
           <text class="member-name">{{ m.nickname || '未设置' }}</text>
@@ -158,9 +158,15 @@ export default {
   computed: {
   computed: {
     // 列表过滤掉"我"(当前登录用户对应的成员),避免自己和自己互动
     // 列表过滤掉"我"(当前登录用户对应的成员),避免自己和自己互动
     filteredMembers() {
     filteredMembers() {
+// 过滤掉自己,只展示其他家庭成员
       var self = this
       var self = this
       return (this.members || []).filter(function(m) {
       return (this.members || []).filter(function(m) {
-        return !(m && m.userId && m.userId === self.userId)
+        var isSelf = m.isSelf === true
+        if (!isSelf && m.userId && m.userId === self.userId) {
+          isSelf = true
+        }
+        return !isSelf
+      })
       })
       })
     },
     },
     detailMemberScores() {
     detailMemberScores() {

+ 4 - 4
cfc-frontend/pages/profile/index.vue

@@ -58,9 +58,9 @@ export default {
     onShareAppMessage() {
     onShareAppMessage() {
     if (this.shareData) {
     if (this.shareData) {
       var path = this.shareData.path
       var path = this.shareData.path
-      if (this.referralCode && path.indexOf('inviteCode=') === -1) {
+      if (this.referralCode && path.indexOf('refCode=') === -1 && path.indexOf('inviteCode=') === -1) {
         var sep = path.indexOf('?') === -1 ? '?' : '&'
         var sep = path.indexOf('?') === -1 ? '?' : '&'
-        path = path + sep + 'inviteCode=' + this.referralCode
+        path = path + sep + 'refCode=' + this.referralCode
       }
       }
       return {
       return {
         title: this.shareData.title,
         title: this.shareData.title,
@@ -75,8 +75,8 @@ export default {
       var qi = this.shareData.path.indexOf('?')
       var qi = this.shareData.path.indexOf('?')
       if (qi !== -1) query = this.shareData.path.substring(qi + 1)
       if (qi !== -1) query = this.shareData.path.substring(qi + 1)
     }
     }
-    if (this.referralCode && query.indexOf('inviteCode=') === -1) {
-      query = query + (query ? '&' : '') + 'inviteCode=' + this.referralCode
+    if (this.referralCode && query.indexOf('refCode=') === -1 && query.indexOf('inviteCode=') === -1) {
+      query = query + (query ? '&' : '') + 'refCode=' + this.referralCode
     }
     }
     return {
     return {
       title: this.shareData ? this.shareData.title : '浠艾福 — 给全家的一站式幸福提案',
       title: this.shareData ? this.shareData.title : '浠艾福 — 给全家的一站式幸福提案',

+ 2 - 2
cfc-frontend/pages/profile/report-detail.vue

@@ -53,7 +53,7 @@ export default {
   onShareAppMessage() {
   onShareAppMessage() {
     var path = '/pages/profile/report-detail?id=' + this.reportId
     var path = '/pages/profile/report-detail?id=' + this.reportId
     if (this.referralCode) {
     if (this.referralCode) {
-      path = path + '&inviteCode=' + this.referralCode
+      path = path + '&refCode=' + this.referralCode
     }
     }
     return {
     return {
       title: this.detail.title || '浠艾福报告',
       title: this.detail.title || '浠艾福报告',
@@ -63,7 +63,7 @@ export default {
   onShareTimeline() {
   onShareTimeline() {
     var query = 'id=' + this.reportId
     var query = 'id=' + this.reportId
     if (this.referralCode) {
     if (this.referralCode) {
-      query = query + '&inviteCode=' + this.referralCode
+      query = query + '&refCode=' + this.referralCode
     }
     }
     return {
     return {
       title: this.detail.title || '浠艾福报告',
       title: this.detail.title || '浠艾福报告',

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

@@ -274,7 +274,7 @@ export default {
   onShareAppMessage() {
   onShareAppMessage() {
     var path = '/pages/invite/join'
     var path = '/pages/invite/join'
     if (this.shareReferralCode) {
     if (this.shareReferralCode) {
-      path = path + '?inviteCode=' + this.shareReferralCode
+      path = path + '?refCode=' + this.shareReferralCode
     }
     }
     return {
     return {
       title: '我在浠艾福发现了好用的家庭成长工具,快来加入!',
       title: '我在浠艾福发现了好用的家庭成长工具,快来加入!',
@@ -285,7 +285,7 @@ export default {
   onShareTimeline() {
   onShareTimeline() {
     var query = ''
     var query = ''
     if (this.shareReferralCode) {
     if (this.shareReferralCode) {
-      query = 'inviteCode=' + this.shareReferralCode
+      query = 'refCode=' + this.shareReferralCode
     }
     }
     return {
     return {
       title: '我在浠艾福发现了好用的家庭成长工具,快来加入!',
       title: '我在浠艾福发现了好用的家庭成长工具,快来加入!',

+ 20 - 6
cfc-frontend/pages/shop/checkout/checkout.vue

@@ -254,12 +254,12 @@
               <text class="check-icon">✓</text>
               <text class="check-icon">✓</text>
             </view>
             </view>
           </view>
           </view>
-          <!-- 可兑换优惠券(需CF值) -->
-          <view class="picker-section-title" v-if="exchangeableCoupons.length > 0">
+          <!-- 可兑换优惠券(需CF值):仅在 CF 足够兑换其中一张券时才提醒 -->
+          <view class="picker-section-title" v-if="affordableExchangeCoupons.length > 0">
             可用CF值兑换(当前余额 {{ cfBalance }} CF)
             可用CF值兑换(当前余额 {{ cfBalance }} CF)
           </view>
           </view>
           <view
           <view
-            v-for="coupon in exchangeableCoupons"
+            v-for="coupon in affordableExchangeCoupons"
             :key="coupon.id"
             :key="coupon.id"
             class="picker-item picker-item-exchange"
             class="picker-item picker-item-exchange"
           >
           >
@@ -278,7 +278,7 @@
               {{ coupon.exchanging ? '兑换中...' : '兑换' }}
               {{ coupon.exchanging ? '兑换中...' : '兑换' }}
             </button>
             </button>
           </view>
           </view>
-          <view class="picker-empty" v-if="ownedCoupons.length === 0 && exchangeableCoupons.length === 0">
+          <view class="picker-empty" v-if="ownedCoupons.length === 0 && affordableExchangeCoupons.length === 0">
             <text>暂无可用优惠券</text>
             <text>暂无可用优惠券</text>
           </view>
           </view>
         </scroll-view>
         </scroll-view>
@@ -480,22 +480,36 @@ export default {
     },
     },
     ownedCoupons() {
     ownedCoupons() {
       var self = this
       var self = this
+      var currentProductId = (self.items && self.items.length > 0) ? self.items[0].productId : null
       return this.coupons.filter(function(c) {
       return this.coupons.filter(function(c) {
         if (c.status && c.status !== 'AVAILABLE') return false
         if (c.status && c.status !== 'AVAILABLE') return false
-        if (c.applicableTo && c.applicableTo !== 'ALL') return false
         if (c.minSpend && c.minSpend > self.actualAmount) return false
         if (c.minSpend && c.minSpend > self.actualAmount) return false
+        if (c.applicableTo === 'PRODUCT' && c.productId != null) {
+          return c.productId === currentProductId
+        }
+        if (c.applicableTo && c.applicableTo !== 'ALL' && c.applicableTo !== 'PRODUCT') return false
         return true
         return true
       })
       })
     },
     },
     availableCoupons() {
     availableCoupons() {
       var self = this
       var self = this
+      var currentProductId = (self.items && self.items.length > 0) ? self.items[0].productId : null
       return this.coupons.filter(function(c) {
       return this.coupons.filter(function(c) {
         if (c.status && c.status !== 'AVAILABLE') return false
         if (c.status && c.status !== 'AVAILABLE') return false
-        if (c.applicableTo && c.applicableTo !== 'ALL') return false
         if (c.minSpend && c.minSpend > self.actualAmount) return false
         if (c.minSpend && c.minSpend > self.actualAmount) return false
+        if (c.applicableTo === 'PRODUCT' && c.productId != null) {
+          return c.productId === currentProductId
+        }
+        if (c.applicableTo && c.applicableTo !== 'ALL' && c.applicableTo !== 'PRODUCT') return false
         return true
         return true
       })
       })
     },
     },
+    affordableExchangeCoupons() {
+      var self = this
+      return (this.exchangeableCoupons || []).filter(function(c) {
+        return c.pointsPrice > 0 && self.cfBalance >= c.pointsPrice
+      })
+    },
     onlineTimeSlots() {
     onlineTimeSlots() {
       if (this.deliveryMethod === 4 && this.selectedServicePerson) {
       if (this.deliveryMethod === 4 && this.selectedServicePerson) {
         return this.selectedServicePerson.slots || []
         return this.selectedServicePerson.slots || []

+ 15 - 9
cfc-frontend/pages/user-edit/user-edit.vue

@@ -233,7 +233,7 @@
 		<!-- 新用户流程:提交申请 / 跳过(接受邀请时按钮固定在页面底部悬浮) -->
 		<!-- 新用户流程:提交申请 / 跳过(接受邀请时按钮固定在页面底部悬浮) -->
 		<view class="family-section" v-if="isEdit && form.role !== 'teacher' && isNewUserFlow">
 		<view class="family-section" v-if="isEdit && form.role !== 'teacher' && isNewUserFlow">
 			<view class="family-actions family-actions-fixed">
 			<view class="family-actions family-actions-fixed">
-				<button class="btn btn-primary" @click="saveUserInfo" :loading="loading">
+				<button class="btn btn-primary" @click="saveUserInfo" :loading="loading" :disabled="loading">
 					{{ hasPendingFamilyInvite ? '提交申请' : '保存资料' }}
 					{{ hasPendingFamilyInvite ? '提交申请' : '保存资料' }}
 				</button>
 				</button>
 				<button class="btn btn-skip" @click="skip">
 				<button class="btn btn-skip" @click="skip">
@@ -253,7 +253,7 @@
 		<!-- 修改个人资料保存按钮(非新用户流程时显示) -->
 		<!-- 修改个人资料保存按钮(非新用户流程时显示) -->
 		<view class="family-section" v-if="isEdit && !isNewUserFlow && form.role !== 'teacher'">
 		<view class="family-section" v-if="isEdit && !isNewUserFlow && form.role !== 'teacher'">
 			<view class="family-actions">
 			<view class="family-actions">
-				<button class="btn btn-primary" @click="saveUserInfo" :loading="loading">
+				<button class="btn btn-primary" @click="saveUserInfo" :loading="loading" :disabled="loading">
 					保存修改
 					保存修改
 				</button>
 				</button>
 			</view>
 			</view>
@@ -262,7 +262,7 @@
 		<!-- 成长规划师提交按钮 -->
 		<!-- 成长规划师提交按钮 -->
 		<view class="family-section" v-if="isEdit && form.role === 'teacher' && isNewUserFlow">
 		<view class="family-section" v-if="isEdit && form.role === 'teacher' && isNewUserFlow">
 			<view class="family-actions">
 			<view class="family-actions">
-				<button class="btn btn-primary" @click="submitTeacherInfo" :loading="loading">
+				<button class="btn btn-primary" @click="submitTeacherInfo" :loading="loading" :disabled="loading">
 					提交申请
 					提交申请
 				</button>
 				</button>
 			</view>
 			</view>
@@ -713,6 +713,7 @@ export default {
 		},
 		},
 		// 保存修改
 		// 保存修改
 		async saveUserInfo() {
 		async saveUserInfo() {
+			if (this.loading) return
 			// 新用户流程:昵称可跳过(跳过时用默认昵称);老用户流程:昵称必填
 			// 新用户流程:昵称可跳过(跳过时用默认昵称);老用户流程:昵称必填
 			if (!this.isNewUserFlow && !this.form.nickname) {
 			if (!this.isNewUserFlow && !this.form.nickname) {
 				uni.showToast({ title: '请输入昵称', icon: 'none' })
 				uni.showToast({ title: '请输入昵称', icon: 'none' })
@@ -782,6 +783,7 @@ export default {
 		}
 		}
 	},
 	},
 		async submitTeacherInfo() {
 		async submitTeacherInfo() {
+			if (this.loading) return
 			// 验证必填字段
 			// 验证必填字段
 			if (!this.form.nickname) {
 			if (!this.form.nickname) {
 				uni.showToast({ title: '请输入昵称', icon: 'none' })
 				uni.showToast({ title: '请输入昵称', icon: 'none' })
@@ -825,6 +827,7 @@ export default {
 			}
 			}
 		},
 		},
 		async joinFamilyAction() {
 		async joinFamilyAction() {
+			if (this.loading) return
 			if (!this.form.inviteCode) {
 			if (!this.form.inviteCode) {
 				uni.showToast({ title: '请输入邀请码', icon: 'none' })
 				uni.showToast({ title: '请输入邀请码', icon: 'none' })
 				return
 				return
@@ -933,26 +936,29 @@ export default {
 					console.error('保存 mascot 失败', e)
 					console.error('保存 mascot 失败', e)
 				}
 				}
 			}
 			}
-			// 新用户流程:消费分享者推荐码,绑定推荐人(非 team 类型)
+			// 新用户流程:消费分享者推荐码,绑定推荐人(推荐码统一为 refCode storage)
 			// 登录页 completeLogin 在 needsRoleSelect=true 时直接跳本页,跳过了 handleReferralBind,
 			// 登录页 completeLogin 在 needsRoleSelect=true 时直接跳本页,跳过了 handleReferralBind,
-			// 因此在此处统一消费 storage 中的 inviteCode,避免推荐人绑定丢失
+			// 因此在此处统一消费 storage 中的 refCode/inviteCode,避免推荐人绑定丢失
+			var refCode = uni.getStorageSync('refCode')
 			var inviteCode = uni.getStorageSync('inviteCode')
 			var inviteCode = uni.getStorageSync('inviteCode')
 			var inviteType = uni.getStorageSync('inviteType')
 			var inviteType = uni.getStorageSync('inviteType')
-			if (inviteCode && inviteType !== 'team') {
+			var code = refCode || (inviteCode && inviteType !== 'team' ? inviteCode : '')
+			if (code) {
 				var self = this
 				var self = this
-				bindReferralIfNotSelf(inviteCode).then(function(res) {
+bindReferralIfNotSelf(code).then(function(res) {
 					if (res && res.code === 200) {
 					if (res && res.code === 200) {
-						console.log('新用户绑定推荐人成功', inviteCode)
+						console.log('新用户绑定推荐人成功', code)
 					}
 					}
 				}).catch(function(e) {
 				}).catch(function(e) {
 					// 后端 Result.error(code=500) → request() 已 toast 业务消息
 					// 后端 Result.error(code=500) → request() 已 toast 业务消息
 					if (e && e.code !== undefined && e.code !== 401) {
 					if (e && e.code !== undefined && e.code !== 401) {
-						console.log('新用户绑定推荐人业务拒绝', inviteCode, e.message || '')
+						console.log('新用户绑定推荐人业务拒绝', code, e.message || '')
 					} else {
 					} else {
 						console.log('新用户绑定推荐人失败', e)
 						console.log('新用户绑定推荐人失败', e)
 					}
 					}
 				})
 				})
 			}
 			}
+			uni.removeStorageSync('refCode')
 			uni.removeStorageSync('inviteCode')
 			uni.removeStorageSync('inviteCode')
 			uni.removeStorageSync('inviteType')
 			uni.removeStorageSync('inviteType')
 			// 用 switchTab 跳转带底部 TabBar 的首页(行/身/智/心/富)
 			// 用 switchTab 跳转带底部 TabBar 的首页(行/身/智/心/富)

+ 2 - 2
cfc-frontend/scripts/audit-duplicate-api-calls.js

@@ -122,12 +122,12 @@ for (var f = 0; f < files.length; f++) {
       var guarded = false
       var guarded = false
       shared.forEach(function (m) {
       shared.forEach(function (m) {
         var mbody = hookBody(script, m)
         var mbody = hookBody(script, m)
-        if (mbody && /(?:_initDone|_inited|_loaded|_hasInit|loggedInInitDone|_firstLoad|_onLoadFired|_initialLoadDone|_mounted)\b/.test(mbody)) guarded = true
+        if (mbody && /(?:_initDone|_inited|_loaded|_hasInit|loggedInInitDone|_firstLoad|_onLoadFired|_initialLoadDone|_mounted|_pageShown)\b/.test(mbody)) guarded = true
       })
       })
       // 也检查生命周期钩子本身是否含守卫标志(_onLoadFired 通常写在 onLoad/onShow 里)
       // 也检查生命周期钩子本身是否含守卫标志(_onLoadFired 通常写在 onLoad/onShow 里)
       ;[a, b].forEach(function (h) {
       ;[a, b].forEach(function (h) {
         var hbody = hookBody(script, h)
         var hbody = hookBody(script, h)
-        if (hbody && /(?:_initDone|_inited|_loaded|_hasInit|loggedInInitDone|_firstLoad|_onLoadFired|_initialLoadDone|_mounted)\b/.test(hbody)) guarded = true
+        if (hbody && /(?:_initDone|_inited|_loaded|_hasInit|loggedInInitDone|_firstLoad|_onLoadFired|_initialLoadDone|_mounted|_pageShown)\b/.test(hbody)) guarded = true
       })
       })
       violations.push({
       violations.push({
         type: 'LIFECYCLE_DOUBLE',
         type: 'LIFECYCLE_DOUBLE',

+ 16 - 8
cfc-frontend/utils/api.js

@@ -137,10 +137,11 @@ function _handle401(url, method, data, options, resolve, reject, resData) {
 
 
 const request = (url, method = 'POST', data = {}, options = {}) => {
 const request = (url, method = 'POST', data = {}, options = {}) => {
   // Append query params from options to URL if provided (e.g., { guideId } for GET requests)
   // Append query params from options to URL if provided (e.g., { guideId } for GET requests)
+  // `timeout`/`_skipDedup` 是请求配置项,不是 query 参数,需排除
   if (options && Object.keys(options).length > 0) {
   if (options && Object.keys(options).length > 0) {
     const separator = url.includes('?') ? '&' : '?'
     const separator = url.includes('?') ? '&' : '?'
     const query = Object.entries(options)
     const query = Object.entries(options)
-      .filter(([_, v]) => v !== undefined && v !== null && v !== '')
+      .filter(([k, v]) => k !== 'timeout' && k !== '_skipDedup' && v !== undefined && v !== null && v !== '')
       .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
       .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
       .join('&')
       .join('&')
     if (query) url = url + separator + query
     if (query) url = url + separator + query
@@ -176,7 +177,7 @@ const request = (url, method = 'POST', data = {}, options = {}) => {
   }
   }
 
 
   var _promise = new Promise((resolve, reject) => {
   var _promise = new Promise((resolve, reject) => {
-    uni.request({
+    var _reqOpts = {
       url: fullUrl,
       url: fullUrl,
       method: method,
       method: method,
       data: body,
       data: body,
@@ -222,7 +223,9 @@ const request = (url, method = 'POST', data = {}, options = {}) => {
         })
         })
         reject(err)
         reject(err)
       }
       }
-    })
+    }
+    if (options && options.timeout) _reqOpts.timeout = options.timeout
+    uni.request(_reqOpts)
   })
   })
 
 
   // 注册去重缓存,3 秒窗口内复用
   // 注册去重缓存,3 秒窗口内复用
@@ -2420,19 +2423,19 @@ export const getInteractionTypes = () => {
 
 
 // ===== 鍏崇郴闂嵎 =====
 // ===== 鍏崇郴闂嵎 =====
 export const generateQuestionnaire = (memberId) => {
 export const generateQuestionnaire = (memberId) => {
-  return request('/api/family/questionnaire/generate', 'POST', { memberId })
+  return request('/api/family/questionnaire/generate', 'POST', { memberId }, { timeout: 130000 })
 }
 }
 
 
 export const submitQuestionnaire = (data) => {
 export const submitQuestionnaire = (data) => {
-  return request('/api/family/questionnaire/submit', 'POST', data)
+  return request('/api/family/questionnaire/submit', 'POST', data, { timeout: 130000 })
 }
 }
 
 
 export const getQuestionnaireSnapshot = (memberId) => {
 export const getQuestionnaireSnapshot = (memberId) => {
-  return request('/api/family/questionnaire/snapshot/' + memberId, 'POST', {})
+  return request('/api/family/questionnaire/snapshot/' + memberId, 'POST', {}, { timeout: 130000 })
 }
 }
 
 
 export const getQuestionnaireHistory = (memberId) => {
 export const getQuestionnaireHistory = (memberId) => {
-  return request('/api/family/questionnaire/history/' + memberId, 'POST', {})
+  return request('/api/family/questionnaire/history/' + memberId, 'POST', {}, { timeout: 130000 })
 }
 }
 
 
 // ===== 五维家庭自检(P0-1)=====
 // ===== 五维家庭自检(P0-1)=====
@@ -2749,7 +2752,12 @@ export const getGrowthRecommendations = (category = 'health', limit = 3) =>
 export const getFoodCautionList = () => request('/api/food/recommendation/caution', 'POST', {})
 export const getFoodCautionList = () => request('/api/food/recommendation/caution', 'POST', {})
 
 
 // ===== 健康报告食材推荐(共用 /api/health/foods)=====
 // ===== 健康报告食材推荐(共用 /api/health/foods)=====
-export const getHealthFoods = (reportId) => request('/api/health/foods', 'POST', { reportId })
+export const getHealthFoods = (reportId, memberId) => {
+  var data = {}
+  if (reportId) data.reportId = reportId
+  if (memberId) data.memberId = memberId
+  return request('/api/health/foods', 'POST', data)
+}
 
 
 // ===== 家庭平台积分(CF值)=====
 // ===== 家庭平台积分(CF值)=====
 export const getFamilyPlatformBalance = () => request('/api/family-platform/balance', 'POST', {})
 export const getFamilyPlatformBalance = () => request('/api/family-platform/balance', 'POST', {})

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-44e2e274c02e0f81600dd6430517c004bd560316
+9cc05f6d

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

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

+ 1 - 1
cfc-web/package.json

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

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

@@ -4,6 +4,71 @@
 
 
 ---
 ---
 
 
+## v1.0.1333 (2026-09-08)
+
+### Bug 修复
+- 家庭成员管理页过滤本人、展示与当前用户的关系标签
+
+### 其他
+- - family-members.vue: 成员数/空态改用 filteredMembers
+- - UserService.getVisibleMembers: 查询 viewerFm,为 parent 源成员补算 relativeLabel(丈夫/妻子/儿子/女儿等)
+- 
+
+
+## v1.0.1332 (2026-09-08)
+
+### 新功能
+- 成长页跳转带 memberId + 食材页分类/排序/溢出修复
+- 健康报告食材推荐接口支持 memberId 查询孩子视角
+
+### 其他
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+
+## v1.0.1331 (2026-09-08)
+
+### 文档
+- 新增运营方略(1+N矩阵)与同类账号探索报告 - 品牌定位与竞品依据
+- 新增官方号搜索收割内容第一批 - 5篇用户视角SEO布局
+- 发布指南新增内容创作第一原则 - 讲用户想听的(4信号源+主语转换法+自检清单)
+
+
+## v1.0.1330 (2026-09-08)
+
+### 新功能
+- 成员被邀请走时原家庭付费会员权益合并到邀请人家庭
+
+
+## v1.0.1329 (2026-09-08)
+
+### Bug 修复
+- AiGateway 超时分离 - connect 快速失败 + read 覆盖 LLM 推理耗时
+- 会员升级自动发JOIN券/用户列表倒序/商品券支付可选/兑换提醒移至支付
+
+### 其他
+- - application.yml: 新增 python.read-timeout-ms: 120000
+- - 前端 api.js: request 支持 timeout 选项(不混入 query),问卷接口加 130s 超时
+- 
+-   grantFamilyJoinCoupons 发放 JOIN 赠券(幂等防重), 记录发券结果
+- - AdminController /users: 无 sort 参数时默认 orderByDesc(createdAt) 倒序
+- - CouponService.toCouponVO: 补 productId 字段, 前端可按商品匹配券
+- - checkout.vue: ownedCoupons/availableCoupons 对 PRODUCT 券按当前商品
+-   productId 匹配(原来一刀切过滤非 ALL 券导致商品券选不到); 新增
+-   affordableExchangeCoupons - CF 不足时不再显示兑换提醒
+- - product-detail.vue: 移除商品页积分兑换优惠券区块, 兑换提醒移至支付时
+-   (无对应券+CF 足够才提示)
+- 
+
+
+## v1.0.1328 (2026-09-07)
+
+### Bug 修复
+- 修复已绑定推荐人重复提示与新用户信息补充页问题(boundInviteCode防重+新用户流程隐藏浠宝福宝选择+家庭身份选择回显)
+
+
 ## v1.0.1327 (2026-09-07)
 ## v1.0.1327 (2026-09-07)
 
 
 ### 新功能
 ### 新功能

+ 66 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 # 更新日志
 
 
-> 当前版本: v1.0.1327
+> 当前版本: v1.0.1333
 
 
 ## 历史版本
 ## 历史版本
 
 
@@ -8,6 +8,71 @@
 
 
 ---
 ---
 
 
+## v1.0.1333 (2026-09-08)
+
+### Bug 修复
+- 家庭成员管理页过滤本人、展示与当前用户的关系标签
+
+### 其他
+- - family-members.vue: 成员数/空态改用 filteredMembers
+- - UserService.getVisibleMembers: 查询 viewerFm,为 parent 源成员补算 relativeLabel(丈夫/妻子/儿子/女儿等)
+- 
+
+
+## v1.0.1332 (2026-09-08)
+
+### 新功能
+- 成长页跳转带 memberId + 食材页分类/排序/溢出修复
+- 健康报告食材推荐接口支持 memberId 查询孩子视角
+
+### 其他
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+
+## v1.0.1331 (2026-09-08)
+
+### 文档
+- 新增运营方略(1+N矩阵)与同类账号探索报告 - 品牌定位与竞品依据
+- 新增官方号搜索收割内容第一批 - 5篇用户视角SEO布局
+- 发布指南新增内容创作第一原则 - 讲用户想听的(4信号源+主语转换法+自检清单)
+
+
+## v1.0.1330 (2026-09-08)
+
+### 新功能
+- 成员被邀请走时原家庭付费会员权益合并到邀请人家庭
+
+
+## v1.0.1329 (2026-09-08)
+
+### Bug 修复
+- AiGateway 超时分离 - connect 快速失败 + read 覆盖 LLM 推理耗时
+- 会员升级自动发JOIN券/用户列表倒序/商品券支付可选/兑换提醒移至支付
+
+### 其他
+- - application.yml: 新增 python.read-timeout-ms: 120000
+- - 前端 api.js: request 支持 timeout 选项(不混入 query),问卷接口加 130s 超时
+- 
+-   grantFamilyJoinCoupons 发放 JOIN 赠券(幂等防重), 记录发券结果
+- - AdminController /users: 无 sort 参数时默认 orderByDesc(createdAt) 倒序
+- - CouponService.toCouponVO: 补 productId 字段, 前端可按商品匹配券
+- - checkout.vue: ownedCoupons/availableCoupons 对 PRODUCT 券按当前商品
+-   productId 匹配(原来一刀切过滤非 ALL 券导致商品券选不到); 新增
+-   affordableExchangeCoupons - CF 不足时不再显示兑换提醒
+- - product-detail.vue: 移除商品页积分兑换优惠券区块, 兑换提醒移至支付时
+-   (无对应券+CF 足够才提示)
+- 
+
+
+## v1.0.1328 (2026-09-07)
+
+### Bug 修复
+- 修复已绑定推荐人重复提示与新用户信息补充页问题(boundInviteCode防重+新用户流程隐藏浠宝福宝选择+家庭身份选择回显)
+
+
 ## v1.0.1327 (2026-09-07)
 ## v1.0.1327 (2026-09-07)
 
 
 ### 新功能
 ### 新功能

+ 181 - 0
运营文案/官方号搜索收割内容-第一批.md

@@ -0,0 +1,181 @@
+# 官方号搜索收割内容 · 第一批(5 篇)· 用户视角重写版
+
+> 用途:官方号「浠艾福家庭幸福俱乐部」搜索 SEO 布局,承接个人号种草流量
+> 原则:**讲用户想听的,不讲我想说的**(见《小红书发布指南》⭐ 内容创作第一原则)
+> 方法:用户搜什么,标题就含什么;开头第一句回应用户顾虑;正文主语是"您家/您孩子/您的钱";不介绍"我们",只回答"您"
+> 口吻:创始人 IP(54 岁冻龄妈妈)第一人称,直面疑虑,不硬广
+> 规范:标题 ≤20 字 / 正文 600-750 字 / 无 Markdown 语法 / 含 20 秒动作 / 含评论区钩子
+
+---
+
+## S1 | 搜索词:浠艾福测评靠谱吗
+
+**标题**:浠艾福测评靠谱吗?我先劝退3类人
+
+**正文**:
+
+您搜"靠谱吗",我懂。不是怕花那几百块,是怕花了钱还添堵:测完拿一堆看不懂的报告,然后被追着买课。
+
+所以我不劝您买,先劝退三类人。
+
+第一类,指望测一次孩子就脱胎换骨的,别买。测评不是药,它只干一件事——把您"总觉得孩子哪有问题"的模糊焦虑,变成"具体差在哪、差多少、往哪使劲"的一张地图。指望它直接出结果,几百块白花。
+
+第二类,孩子还没到 3 岁的,先别买。太小了,测出来的数据参考意义有限,等上了小学再说。
+
+第三类,您自己压根没时间看报告的,别买。测评最值钱的部分,不是测,是测完之后那 28 页报告怎么读、怎么用。您要是不打算花半小时看,这钱省着给孩子买书吧。
+
+如果您不在上面三类里,那我说说您花钱到底买到什么。
+
+就一样东西:知道该管哪里,不用管哪里。
+
+说个真事。有个妈妈,一直觉得孩子专注力差,砸了两万块报专注力训练班。测评一测,孩子专注力在同龄人里是前 30%,根本不差;真正垫底的是"归属感"——他在学校没什么朋友,上课走神是因为心里慌,不是坐不住。
+
+方向错了,两万块等于扔水里。测评帮她省下的,是接下来还要砸进去的钱。
+
+那分数怎么来的,准不准?您孩子测出来的分数,不是我们随便打的,是跟几十万个同龄孩子比出来的位置。6 分不代表"及格",代表"超过了 60% 的同龄人"。您要盯的不是数字大小,是跟同龄人比,他落后在哪一项。
+
+至于"会不会被追着买课"——报告解读是免费的,您拿着报告走也行,找我们规划师聊也行,不买任何东西也能问清楚每一行数字什么意思。话放这儿。
+
+今晚 20 秒能做的:别急着掏钱,私信我领一份 A2 的样题,您自己先看题目问得在不在点子上。觉得扯,划走;觉得问到您心坎里了,再聊下一步。
+
+评论区告诉我:您最想搞清楚孩子哪一项?专注力、情绪,还是社交?
+
+下期我算笔账:核心素养测评值不值那个钱——不聊我们,聊您家的钱该往哪投。
+
+#主动健康 #五维能量 #妈妈成长 #核心素养测评 #浠艾福 #孩子测评
+
+---
+
+## S2 | 搜索词:核心素养测评值不值
+
+**标题**:核心素养测评值不值?先回您3个问题
+
+**正文**:
+
+您搜"值不值",说明已经在犹豫了。我先回答您心里最可能的三句话。
+
+第一句:"我家孩子成绩还行,有必要测吗?"
+
+有必要,但测的目的不是看成绩。成绩是结果,测评看的是底子——自驱力三个维度(自主感、胜任感、归属感)加情绪稳定度。您见过那种孩子:小学成绩挺好,到了初中突然塌了。不是变笨了,是小学靠"听话+刷题"撑着的,底子里的自主感和胜任感一直没长出来。测评就是提前看看这块底子。
+
+第二句:"几百块测一次,会不会测完就那样?"
+
+问您一句:您给孩子报的班,一年下来有没有几千块没见效的?测评几百块,干的事是——先看清楚钱该往哪投。有个妈妈砸了两万块报专注力班,一测孩子专注力根本不差,差的是归属感。方向错了,两万块是学费;方向对了,测评的钱比哪个班都便宜。
+
+第三句:"和网上那种免费测试有啥区别?"
+
+区别就一个:免费测试测的是"感觉",正经测评测的是"位置"。网上那种二三十道题的,答案都是"您孩子很聪明,只是需要多陪伴"——说了等于没说。正经的测评,是拿几十万个同龄孩子做参照,告诉您孩子在哪个百分位、哪一项是真短板。这才是能拿来定方向的东西。
+
+如果您看完这三句,觉得家里用不上,那就不值,别买。这是实话。
+
+今晚 20 秒:数一下您今年给孩子报的班,有几个是"看到别人报才报的"?
+
+评论区告诉我这个数字。下期教您:拿到测评报告,第一步该看哪一页。
+
+#主动健康 #五维能量 #妈妈成长 #核心素养测评 #教育避坑 #浠艾福
+
+---
+
+## S3 | 搜索词:测评报告怎么看
+
+**标题**:拿到测评报告,先看这一页就够了
+
+**正文**:
+
+28 页的报告摊开,第一反应是懵,太正常了。我不让您都看完,只带您看一页。
+
+翻到"自驱力"那一页,上面有三个分数:自主感、胜任感、归属感。
+
+这三个数,就是您孩子"愿不愿意自己往前走"的三个开关。
+
+自主感低,孩子是这样:您让他干啥他干啥,但您不问,他就没想法,做什么都要先看您脸色。
+
+胜任感低:一遇到难一点的事就说"我不行""算了吧",还没试就打退堂鼓。
+
+归属感低:话越来越少,放学回家关房门,您问啥都是"还行""没事"。
+
+哪个分最低,您家的劲儿就该往哪使。三个都低?别慌,往下看。
+
+先说清楚:分数不是判决书。6 分不代表"及格",代表"超过了 60% 的同龄人"。您要看的不是"低不低",是"跟同龄人比,哪一项最落后"。
+
+最怕的不是分数低,是您拿着报告干着急,不知道该干嘛。给您一个原则:哪个低,就先补哪个,一次只补一个。
+
+自主感低 → 今天起,每天给孩子一个"您说了算"的小事:吃什么、穿哪件、周末去哪,让他自己定,定了就别反悔。
+
+胜任感低 → 把目标拆小。别再说"期末考进前十",改说"这周口算错题少三道"。小胜仗打多了,"我能行"才长得出来。
+
+归属感低 → 最难也最管用:睡前放下手机,问一句"今天有什么想跟我说的",然后闭嘴听。别评判,别纠正,坚持两周。
+
+今晚 20 秒:把报告翻到自驱力那一页,找出三个分里最低的那个。
+
+评论区告诉我哪一项,我告诉您先从哪一步补起。
+
+#主动健康 #五维能量 #妈妈成长 #测评报告 #核心素养 #亲子
+
+---
+
+## S4 | 搜索词:孩子自驱力怎么提升
+
+**标题**:孩子不推不动?先分清是这三个里的哪个
+
+**正文**:
+
+您家孩子是哪种"不推不动"?
+
+第一种,催了才动,不催就躺平。作业要您坐旁边才写,练琴要您盯着才碰。他其实会做,就是不想主动开始。
+
+第二种,一遇到难的就缩。新题不会就空着、比赛没赢过就不想参加、有点难度的书翻两页就扔。
+
+第三种,不爱说话,越来越闷。放学回家关房门,您问"今天怎么样",回答永远是"还行"。
+
+这三种,看着都叫"没自驱力",但病根完全不一样,补法也完全不一样。
+
+第一种,是自主感缺了。他习惯了"您指挥、他执行",从来没尝过"我自己决定"的滋味。补法:从今天起,每天给他一个您说了不算的小事——吃什么、穿哪件、周末干嘛,让他定,定了就不许反悔。他得先尝到"我说了算"的甜头,才会想主动。
+
+第二种,是胜任感缺了。他不是不想,是不相信自己行。原因通常是:您夸得太少,或者要求太高。补法:把目标拆到他够得着。别再说"期末进前十",改说"这周口算错三道以内"。赢几次小仗,"我能行"才长得出来。
+
+第三种,是归属感缺了。他在家里感觉不到被接纳,话就越来越少了。补法:睡前放下手机,问一句"今天有什么想跟我说的",然后闭嘴听。坚持两周,他话匣子会开的。前提是——他说的您别评判、别纠正、别急着给建议。
+
+一句话:别急着催"你怎么不主动",先分清他是哪一种。补对了,自驱力自己会长。
+
+这三个洞怎么判断是哪个?拿不准的,私信我,我给您一份简单的判断清单,自己对照就行。
+
+今晚 20 秒:想一下您家娃最像上面哪一种,评论区打"1、2 或 3",我告诉您从哪一步开始。
+
+#主动健康 #五维能量 #妈妈成长 #自驱力 #核心素养测评 #赋能未来
+
+---
+
+## S5 | 搜索词:肠道菌群检测有没有用
+
+**标题**:孩子睡不好老感冒,查肠道可能比查脑子管用
+
+**正文**:
+
+先别管"有没有用"这个说法。您先回答我一个问题:您家最近半年,有没有这种反复——
+
+孩子睡不踏实,翻来覆去,早上起不来;或者一到换季就感冒,好了又犯;再或者全家总有人便秘、胀气、胃口差。
+
+如果有,我告诉您一个可能从没想过的地方:肠道。
+
+肠子和脑子是连着的,医学上叫"肠脑轴"。您有没有发现:孩子一考试就胃疼、一紧张就拉肚子?那不是装的,是肠子在替脑子报警。反过来也一样——肠道菌群失衡,会影响睡眠、情绪、免疫力。
+
+市面上那个"肠道菌群检测",查的就是这个:您肠道里住着哪些菌、哪类多了哪类少了、整体生态平不平衡。用的是一个叫 16S rRNA 测序的技术,通俗说就是把您肠道里的"菌口普查"做一遍。
+
+那测出来有什么用?举个最典型的例子。
+
+孩子注意力不集中,您第一反应是报专注力班。但有一种情况是:他肠道里跟多巴胺合成有关的菌明显偏少——多巴胺是大脑"想做事"的燃料,燃料不够,他当然坐不住、没精神。这时候补脑不如先调肠。测一次,把这个方向照出来了,您就知道该往哪使劲,而不是病急乱投医。
+
+测完不是让您买一堆保健品。营养师会根据结果给您配一份全家人的食谱——多吃哪几类、少碰哪几类,都是菜市场买得到的东西。
+
+但我也说实话:它不解决所有问题。它就是一张"身体内部的生态报告"。值不值,看您家有没有上面那些"反复"。
+
+今晚 20 秒:回想一下,全家最近半年有没有人反复感冒、睡不踏实、肠胃闹脾气?
+
+评论区告诉我占了几样,我看看从哪讲起。
+
+#主动健康 #五维能量 #妈妈成长 #肠道菌群 #精准营养 #浠艾福
+
+---
+
+*第一批搜索收割内容(用户视角版)完成。发布前按《小红书发布指南》QA 清单自查,重点自查「⭐ 内容创作第一原则」:删掉品牌名内容还成立吗?主语是"您"吗?*

+ 57 - 0
运营文案/小红书内容/小红书发布指南.md

@@ -59,6 +59,63 @@
 
 
 ---
 ---
 
 
+## ⭐ 内容创作第一原则:讲用户想听的,不讲我想说的
+
+> 2026-09-07 新增。**本原则优先于本文档所有其他规范**。选题角度、正文组织、每句话的主语,全部从用户出发。
+
+### 原则一句话
+
+**内容的起点永远是用户的顾虑和搜索意图,不是品牌想传达的信息。**
+
+"我想说的" = 品牌视角(我们是什么、我们多专业、我们有多好)——用户不关心,划过。
+"用户想听的" = 用户视角(我花了钱能得到什么、会不会被坑、我家这种情况怎么办)——用户停下来。
+
+### 怎么判断"用户想听的"——4 个信号源(按真实度排序)
+
+| 信号源 | 怎么看 | 例子 |
+|--------|--------|------|
+| 搜索联想词 | 小红书搜索框打前几个字,看自动补全 | 搜"核心素养测评"→ 联想"是不是智商税""值不值" |
+| 评论区 | 同类高赞内容下,用户在问/吐槽什么 | "测完报告看不懂""会不会被推销" |
+| 收藏率 | 收藏 > 点赞的内容 = 用户"想留着以后用" | 教程类、清单类 |
+| 竞品爆款标题 | 用户用点赞投过票的句式 | "过来人建议:不要……" |
+
+### 一条检验标准(每句写完问自己)
+
+> **把品牌名删掉,这句话还对用户有用吗?**
+> - 还有用 → 用户想听的
+> - 删了就垮 → 我在自说自话
+
+### 主语转换法(写的时候直接套)
+
+把主语从"我们 / 产品 / 测评"换成"您 / 您孩子 / 您的钱"。
+
+| 品牌视角(❌ 自说自话) | 用户视角(✅ 想听的) |
+|--------------------------|------------------------|
+| "测的不是玄学,是能力" | "我花几百块,和网上免费测评差在哪" |
+| "数据不是拍脑袋" | "我孩子的分数怎么算的,准不准" |
+| "用的人不是小白鼠" | "别人测完到底有没有用,别给我看宣传" |
+| "我们平台跟踪发现……" | "这对我家孩子意味着什么" |
+
+### 常见"我想说的"信号(写完自查,出现就改)
+
+- ❌ 开头第一句在介绍我们/产品("浠艾福是……""我们测评基于……")
+- ❌ 正文出现"我们平台""我们测评""我们的体系"等主语
+- ❌ 罗列品牌数据当卖点("12万家庭在用"→ 要改成"12万家庭里,有多少跟您家情况一样")
+- ❌ 解释产品是什么(用户不关心品类定义,关心自己的问题)
+- ✅ 开头第一句在回应一个具体的顾虑
+- ✅ 正文主语是"您家""您孩子""您的钱"
+- ✅ 每段都回答一个用户"会不会问"的问题
+
+### 对搜索收割内容的特殊要求
+
+搜索收割内容最容易犯"品牌视角"的错——一写"我们是什么"就停不下来。记住:
+
+**用户搜"靠谱吗",你要回答的是"花了钱能得到什么、会不会被坑",不是"我们多专业"。**
+**用户搜"值不值",你要回答的是"我家这种情况该不该买",不是"我们帮你省了多少"。**
+**用户搜"怎么看报告",你要回答的是"拿到手第一步看哪、红线项别慌",不是"这 5 个指标的定义"。**
+
+---
+
 ## 二、内容规范(硬性标准)
 ## 二、内容规范(硬性标准)
 
 
 ### 2.1 标题
 ### 2.1 标题

+ 171 - 0
运营文案/小红书同类账号探索报告.md

@@ -0,0 +1,171 @@
+# 小红书同类账号探索报告 · 浠艾福
+
+> 探索日期:2026-09-07
+> 方法:小红书"家庭教育"搜索页实时抓取 + 7月底竞品单篇数据分析 + 全网头部账号检索
+> 目的:为浠艾福 1+N 矩阵定位提供竞品依据
+
+---
+
+## 一、结论先行:三个关键判断
+
+1. **直接竞品(测评/成长规划/健康报告类)在小红书几乎空白** —— 这是浠艾福最大的机会窗口
+2. **泛家庭教育/育儿类账号高度饱和** —— 情绪管理、内驱力、亲子沟通是红海,纯内容号难突围
+3. **头部账号靠"专业背书 + 强人设"立足** —— 单靠干货科普已经卷不动,必须有差异化壁垒
+
+---
+
+## 二、小红书"家庭教育"搜索页实时账号(2026-09-07 抓取)
+
+在"家庭教育"关键词下抓到的活跃账号(按出现顺序):
+
+| 账号 | 内容特征 | 判断 |
+|------|---------|------|
+| 少年墨翎 | 亲子关系/情绪(8.2万赞爆款) | 泛育儿情感号 |
+| 行遥家庭教育 | 家庭教育知识 | 泛知识号 |
+| 张老师懂孩子 | 儿童心理 | 专家人设号 |
+| 铭爸来了 | 爸爸视角育儿 | 差异化人设 |
+| 家庭认知局 | 家庭认知/教育观点 | 观点号 |
+| 亲子说 | 亲子关系 | 泛育儿号 |
+| 曹老师讲家庭智慧 | 家庭智慧/教育 | 专家人设号 |
+| 小猪聊育儿 | 育儿知识 | 泛育儿号 |
+| 琳感力场 | 家庭教育 | 泛知识号 |
+| 温温提倡温柔自育 | 温柔育儿(8464赞) | 情感共鸣号 |
+| 一口蛋卷儿 | 反向育儿(2.7万赞) | 观点号 |
+| 奶牛猫一家 | 好好说话/亲子沟通 | 泛育儿号 |
+
+**特征总结**:
+- 全部是**泛家庭教育内容号**,无一做"测评/数据/成长规划"这类工具型产品
+- 人设集中在"老师/专家/妈妈"三类,靠观点和情感共鸣吸粉
+- 没有看到任何账号用"真实测评数据 + 前后对比"做内容——这正是浠艾福的差异化空间
+
+---
+
+## 三、7月底竞品单篇分析(39篇高赞文章)
+
+来源:`小红书发布\_过程文件\xhs_hot_articles_analysis.txt`
+
+### 3.1 高赞内容 TOP 特征
+
+| 排名 | 标题 | 赞 | 藏 | 评 | 转 |
+|------|------|-----|-----|-----|-----|
+| 1 | 拒绝"散养"如何高情商与孩子对话 | 32,517 | 33,786 | 119 | 6,276 |
+| 2 | 亲子沟通绘本《妈妈凶你不是不爱你》 | 10,101 | 5,033 | 111 | 806 |
+| 3 | 恢复孩子内驱力最快方法:马斯洛定律 | 8,373 | 13,378 | 249 | 5,179 |
+| 4 | 情绪暴躁的妈妈,每天抄写一遍21天 | 5,871 | 8,325 | 346 | 1,312 |
+| 5 | 过来人建议:不要过度和孩子共情 | 4,355 | 4,278 | 181 | 1,763 |
+
+### 3.2 高赞内容三大主题
+
+| 主题 | 占比 | 代表 | 竞争度 |
+|------|:----:|------|:------:|
+| **情绪/妈妈** | 12/39 | 情绪暴躁的妈妈抄写、情绪稳定是福气 | 🔴 红海 |
+| **内驱力/专注** | 13/39 | 马斯洛定律、内驱力爆棚的孩子 | 🔴 红海 |
+| **沟通/亲子** | 2/39 | 高情商对话、绘本故事 | 🟠 中 |
+
+**关键洞察**:
+- 高赞内容**收藏/点赞比普遍 >1**(如马斯洛定律 1.60、情绪抄写 1.42)——说明用户"收藏了但没行动",是典型的"干货焦虑"内容
+- 这些内容**没有数据支撑、没有真实案例、没有产品承接**——用户看完收藏就完了,无法转化
+- 浠艾福的"真实测评数据 + 前后对比 + 可执行方案"恰好补上这个缺口
+
+---
+
+## 四、全网头部同类账号(web 检索)
+
+### 4.1 家庭教育/育儿头部账号
+
+| 账号 | 粉丝量级 | 定位 | 变现方式 |
+|------|---------|------|---------|
+| 下沙育儿 | 头部 | 家庭教育学会理事+心理咨询师,15年经验 | 知识付费/咨询 |
+| 牛妈彭小蹦 | 1.9万 | 阿德勒学派心理咨询师,10年育儿干货 | 咨询/课程 |
+| 黄老师科学育儿早教 | 头部 | 北大早教特聘专家,央视提名 | 课程/品牌 |
+| 年糕妈妈 | 72.7万 | 浙大医学硕士,0-6岁喂养护理 | 电商/课程 |
+| 君君辅食记 | 244万 | 儿童营养师+育婴师,辅食 | 电商/广告 |
+| 育婴师系列(安安米琪/Aimee等) | 10-215万 | 育婴师+营养师 | 电商/广告 |
+
+### 4.2 关键发现
+
+- **头部账号全部靠"专业资质背书"**(学会理事、心理咨询师、医学硕士、育婴师证)——这是信任的底层
+- **变现路径清晰**:知识付费课程(99-399元)+ 一对一咨询(500-2000元)+ 电商
+- **没有一个账号做"家庭测评/成长规划/健康报告"这类工具型产品**——浠艾福的测评体系是独有壁垒
+
+---
+
+## 五、矩阵玩法参考(教培/机构类)
+
+来源:全网矩阵运营方法论检索
+
+### 5.1 成熟矩阵结构(教培机构已验证)
+
+| 账号层级 | 定位 | 内容方向 | 粉丝目标 |
+|---------|------|---------|---------|
+| **主账号** | 品牌官方形象 | 品牌故事、产品测评、KOL合作 | 5万+ |
+| **老师个人号** | 专业人设 | 教学日常、专业知识 | 1-3万 |
+| **家长素人号** | 真实用户 | 使用体验、痛点共鸣 | 5000-2万 |
+| **引流号** | 销售承接 | 评论区/私信转化 | 小号 |
+
+### 5.2 矩阵运营核心规则(2025-2026 平台风控)
+
+| 规则 | 说明 |
+|------|------|
+| ❌ 矩阵号互赞互评 | 系统识别为刷量 |
+| ❌ 同一素材复用多号 | 相似度>40% 触发去重 |
+| ❌ 正文标注"官方/姐妹号" | 触发平台审查 |
+| ❌ 同设备同IP切换多号 | 账号关联暴露 |
+| ✅ 内容差异化 | 主号精修图文,子号真实视频/随拍 |
+| ✅ 关键词差异化 | 主号高搜索词,子号长尾词 |
+| ✅ 发布时间错开 | 避免同屏竞争 |
+
+---
+
+## 六、对浠艾福矩阵的启示
+
+### 6.1 机会窗口(竞品空白)
+
+| 空白领域 | 说明 | 浠艾福打法 |
+|---------|------|-----------|
+| **测评/数据类内容** | 无竞品用真实测评数据做内容 | 官方号主打"测评报告解读+前后对比" |
+| **成长规划工具** | 无竞品做"规划服务" | 官方号承接"规划咨询"转化 |
+| **健康报告/菌群检测** | 无竞品做家庭健康数据 | 官方号做"健康报告解读"搜索收割 |
+| **五维体系** | 无竞品有完整理论体系 | 官方号做"五维能量"品牌心智 |
+
+### 6.2 红海警示(不要硬碰)
+
+| 红海领域 | 说明 | 浠艾福策略 |
+|---------|------|-----------|
+| 情绪管理 | 12/39 高赞内容,饱和 | 个人号可做,但必须带数据/案例差异化 |
+| 内驱力 | 13/39 高赞内容,饱和 | 个人号做,但用"测评前后对比"切入 |
+| 亲子沟通 | 竞争激烈 | 个人号做,用"真实家庭案例"切入 |
+
+### 6.3 矩阵定位校准(基于竞品)
+
+```
+官方号(浠艾福家庭幸福俱乐部)
+  → 定位:测评/规划/健康报告 搜索收割 + 品牌背书
+  → 差异化:真实数据 + 五维体系 + 专业服务商背书
+  → 竞品对标:无直接竞品,抢占"家庭测评"心智
+
+个人号A(54岁冻龄妈妈)
+  → 定位:主动健康生活化种草
+  → 差异化:真实人设 + 健康数据(体检/睡眠/菌群)
+  → 竞品对标:泛健康号,但用"冻龄+数据"差异化
+
+个人号B(职场二娃妈)
+  → 定位:教育方法论种草
+  → 差异化:真实测评数据 + 不吼不骂
+  → 竞品对标:内驱力红海,但用"测评前后对比"切入
+```
+
+---
+
+## 七、数据来源说明
+
+| 来源 | 内容 | 时间 |
+|------|------|------|
+| 小红书"家庭教育"搜索页实时抓取 | 20+ 活跃账号 | 2026-09-07 |
+| `xhs_hot_articles_analysis.txt` | 39篇高赞文章 | 2026-07-27 |
+| `search_results_readable.txt` | 20篇高赞文章+作者 | 2026-07-27 |
+| 全网 web 检索 | 头部账号粉丝量/定位 | 2026-09-07 |
+
+---
+
+*探索完成。下一步:基于竞品空白,细化官方号搜索收割内容选题 + 个人号人设差异化定位。*

+ 274 - 0
运营文案/浠艾福小红书运营方略.md

@@ -0,0 +1,274 @@
+# 浠艾福(Care Family)小红书运营方略
+
+> 制定日期:2026-09-07
+> 依据:远程同步《小红书运营方略与起号逻辑》系列 + 浠艾福 25 篇已发布数据复盘 + 品牌现状
+> 核心命题:**1 个官方账号 + N 个个人引流号的矩阵打法**
+
+---
+
+## 一、现状诊断:25 篇内容为什么没涨粉
+
+先看数据事实(2026-08-12 创作者中心 3 页完整导出):
+
+| 指标 | 当前值 | 健康值 | 判断 |
+|------|--------|--------|------|
+| 平均 CTR | 8.1% | ≥8% | ✅ 标题+封面有效 |
+| 单篇评论 | 0.48 | ≥3 | ❌ 评论区钩子失效 |
+| 单篇收藏 | 0.8 | ≥5 | ❌ 无收藏价值感 |
+| 单篇涨粉 | 0.12 | ≥1 | ❌ 几乎不涨粉 |
+| 观看时长中位数 | 9s | ≥20s | ❌ 内容没接住标题 |
+| 累计粉丝 | 10+ | — | ❌ 冷启动失败 |
+
+**四个根因(不是内容质量问题,是账号定位问题):**
+
+1. **官方账号干了个人号的活**:用一个"官方号"发"54岁冻龄妈妈"的第一人称内容。小红书用户对官方号有天然信任门槛,官方号发个人叙事既丢了官方背书力,又没人设可信度——两头不靠。
+2. **种草与搜索不分家**:所有内容都是"种草向",没有任何一篇在布局搜索词。而浠艾福的测评产品(A2/B4/健康报告)恰恰是高决策产品,用户一定会搜"测评靠谱吗""值不值""有没有用"——搜索场是空的。
+3. **矩阵缺失,单号天花板极低**:一个号一天一条,曝光天花板就在那儿(25篇总曝光仅1.2万)。没有个人号矩阵在评论区、私信、转发层造势。
+4. **涨粉机制为零**:内容没有"关注理由"(系列感、下期预告、资料领取都没有形成闭环),用户看完即走。
+
+**结论:内容产能和质量已经够用(CTR 8.1% 证明标题封面体系是成立的),缺的是"结构"——账号矩阵结构、内容双层结构、导流承接结构。**
+
+---
+
+## 二、战略总纲:1+N 账号矩阵
+
+### 2.1 为什么必须 1+N
+
+远程方略的核心结论:**小红书的种草发生在"个人信任"里,收割发生在"搜索答案"里。**
+
+- **种草内容**:起点是用户兴趣,终点是唤起购买欲 → 需要"真实的人"讲述,官方号讲没人信
+- **搜索内容**:起点是用户对答案的预期,终点是打消顾虑 → 需要"权威的答案",个人号答不了
+
+单一账号无法同时扮演"可信的人"和"权威的官方"。所以:
+
+```
+┌─────────────────────────────────────────────────────────┐
+│                   浠艾福小红书矩阵                        │
+│                                                         │
+│  官方号 × 1(信任场 / 收割场)                          │
+│  ────────────────────────────────                       │
+│  · 品牌背书 · 案例报告 · 搜索SEO · 私信转化             │
+│                                                         │
+│  个人号 × N(种草场 / 流量场)                          │
+│  ────────────────────────────────                       │
+│  · 真实人设 · 痛点共鸣 · 生活场景 · 评论区造势          │
+│                                                         │
+│  协同:个人号种草 → 官方号承接 → 私域转化               │
+└─────────────────────────────────────────────────────────┘
+```
+
+### 2.2 矩阵架构
+
+| 账号 | 角色 | 人设 | 主打维度 | 核心使命 |
+|------|------|------|---------|---------|
+| **官方号** | 信任中枢 | 浠艾福官方(创始人IP口吻) | 全五维 | 搜索收割 + 案例背书 + 转化承接 |
+| **个人号A** | 种草号 | 54岁冻龄妈妈 | 身 | 主动健康生活化种草 |
+| **个人号B** | 种草号 | 职场二娃妈 | 智 | 教育方法论种草 |
+| **个人号C** | 种草号 | 青春期孩子妈妈 | 行·心 | 亲子关系情感种草 |
+| **个人号D** | 种草号 | 家庭理财妈妈 | 富 | 财商种草 |
+
+> 冷启动先做官方号 + 2 个个人号(A+B),跑通后再扩到 4 个。每个号对应 1-2 个维度,避免内容内耗。
+
+---
+
+## 三、官方账号运营方略
+
+### 3.1 定位:品牌信任中枢(不是流量号)
+
+官方号唯一目标是**承接转化**,不追求爆款。用户看完个人号种草后,会搜"浠艾福测评怎么样"——官方号要保证搜到的全是答案。
+
+### 3.2 内容配比(每月 12 篇)
+
+| 内容类型 | 占比 | 作用 | 具体形式 |
+|---------|:----:|------|---------|
+| 搜索收割型 | 40% | 打消顾虑 | "浠艾福测评值不值""A2测评是什么""测评报告怎么看" |
+| 案例背书型 | 30% | 建立信任 | 脱敏报告前后对比、家庭改善实录 |
+| 品牌心智型 | 20% | 强化主张 | 五维能量科普、品牌理念、官方活动 |
+| 官方动态型 | 10% | 展示实力 | 服务商入驻、产品更新、线下活动 |
+
+### 3.3 搜索收割内容怎么做(当前完全空白,最高优先级)
+
+远程方略核心方法——**用户决策前会搜 200 个搜索词看 600 篇笔记,必须让顾虑有答案**:
+
+| 用户会搜的词 | 官方号要铺的内容 |
+|-------------|-----------------|
+| 浠艾福测评靠谱吗 | 测评体系科普:A2/B4 是什么、基于什么常模、多少家庭验证 |
+| 核心素养测评值不值 | ROI 拆解:一个报告能解决什么、省下什么冤枉钱 |
+| 测评报告怎么看 | 保姆级解读教程:红灯指标什么意思、怎么改善 |
+| 孩子自驱力怎么提升 | 案例型:脱敏前后对比(自驱力 4.07→5.30) |
+| 肠道菌群检测有没有用 | 原理科普:16S rRNA 测序是什么、怎么指导饮食 |
+
+**做法**:标题直接含搜索词,正文第一屏给答案,用"浠艾福家庭健康报告"标注数据来源。铺满 20 篇搜索词笔记后,搜索场就守住了。
+
+### 3.4 官方号维护规范
+
+- **账号资料**:按《账号资料更新速查卡》完成(昵称"浠艾福家庭幸福俱乐部"、简介含"官方账号"字样、类目选"亲子/教育/家庭教育/儿童心理")
+- **合集体系**:按五维建 5 个合集,每篇挂载对应合集(现有 25 篇全部补挂)
+- **私信承接**:设置自动回复关键词("测评""报告""咨询"),所有个人号导流都落到官方号私信
+- **评论区**:官方号每条评论必回,专业问题给 2-3 句有信息量的回答再引导私信
+- **发布频率**:每周 3 篇固定(周二/周四/周日 19:00),配合个人号节奏
+- **搜索SEO 自查**:每月搜一次 15 个核心关键词,检查前 10 篇是否有自己内容,没有就补
+
+---
+
+## 四、个人引流号矩阵:起号与维护(核心)
+
+### 4.1 起号三阶段
+
+#### 阶段一:养号期(3-7 天,不发内容)
+
+新号权重低,直接发内容容易进小黑屋。先模拟真实用户:
+
+| 动作 | 频率 | 目的 |
+|------|------|------|
+| 完善资料(头像/昵称/简介/背景图) | 1 次 | 真实感,头像不用品牌 logo |
+| 浏览垂直领域内容 | 每天 30 分钟 | 让算法识别账号兴趣标签 |
+| 点赞/收藏/评论同类博主 | 每天 10-15 条 | 建立账号活跃度 |
+| 关注 20-30 个同类账号 | 分 3 天完成 | 关注过多会触发异常 |
+
+**养号禁忌**:不批量关注、不秒关秒取关、不一天发多条、不用 WiFi 同时登录多号、新号 7 天内不挂任何导流信息。
+
+#### 阶段二:定调期(前 10 篇,决定账号标签)
+
+前 10 篇决定算法给账号贴什么标签,必须垂直:
+
+- **第 1-3 篇**:人设亮相 + 价值承诺
+  - 例(个人号A):"54岁看起来像40,我把这5个习惯坚持了10年"
+  - 例(个人号B):"两个娃都养出了自驱力,我的方法可能跟你想的不一样"
+- **第 4-6 篇**:垂直内容测试(3 个选题方向各 1 篇,看数据反应)
+- **第 7-10 篇**:放大表现最好的方向,形成风格
+
+**定调期铁律**:
+- 一个号只讲 1-2 个维度,绝不跨维度
+- 前 10 篇不出现任何品牌名/导流信息(先立人设,后谈产品)
+- 每篇必含"个人经历"叙事(我做了、我试了、我踩过坑),不是知识搬运
+- 标题沿用已验证公式(反常识/数字/俗语钩子)
+
+#### 阶段三:放量期(第 11 篇起)
+
+- 频率:日更或隔日更(至少每周 4 篇)
+- 复制爆款:数据好的选题方向横向延展,形成系列
+- 开始轻量提及产品:"上次说的那个测评,我带孩子做了,结果挺意外……"
+- 第 20 篇后:评论区引导"完整方法我整理在 @浠艾福 官方号了"
+
+### 4.2 人设设计(每个号必须真实、垂直、有记忆点)
+
+个人号的核心是"让用户相信你是真人",参考远程方略的**创始人 IP 专家路线**——放低功利心,真诚解决问题,信任是成交前提:
+
+| 要素 | 个人号A | 个人号B | 个人号C | 个人号D |
+|------|---------|---------|---------|---------|
+| 一句话人设 | 冻龄妈妈 | 职场二娃妈 | 青春期妈妈 | 家庭理财妈妈 |
+| 年龄感 | 50+ | 35 左右 | 40 左右 | 35+ |
+| 身份细节 | 退休、儿子留学 | 大厂/公司上班 | 家有初中生 | 全职/半全职 |
+| 记忆点 | "看起来像40" | "不吼不骂养娃" | "从剑拔弩张到无话不谈" | "记账14年" |
+| 差异化内容 | 健康习惯、体检、睡眠 | 学习方法、专注力 | 亲子沟通、情绪 | 记账、定投、财商 |
+| 与产品关系 | 健康报告/菌群检测 | A2测评 | 亲子关系测评 | 家庭财务规划 |
+| 引流话术 | "测评我用的浠艾福" | "报告解读我发官方号了" | "私信我发你量表" | "模板在官方号" |
+
+**人设真实性铁律**:
+- 每个号绑不同手机号注册,头像/昵称/简介完全不同
+- 人设细节要能自洽(年龄、家庭、职业、城市),运营时保持一致
+- 不用同一张脸/同一个文案模板发多个号(判搬运/营销号)
+- 个人号禁止出现"官方""浠艾福官方""亿图腾"等字样
+
+### 4.3 维护规范(日常 SOP)
+
+| 动作 | 频率 | 说明 |
+|------|------|------|
+| 发布 | 每周 4-7 篇 | 固定时间(个人号建议 20:00-21:30) |
+| 前 1 小时回评 | 每篇 | 发布后 1 小时内回复所有评论并点赞,算法加权 |
+| 评论区自问自答 | 每篇 2-3 条 | 用小号/自己留言"这个我也遇到过""具体怎么做",引导真实用户跟评 |
+| 浏览互动 | 每天 20 分钟 | 保持账号活跃(点赞/评论同类内容) |
+| 数据周复盘 | 每周日 | 曝光/CTR/互动/涨粉,砍掉数据差的选题方向 |
+| 私信承接 | 每天 | 收到私信当日回,引导关注官方号或加微信 |
+
+**防关联红线(矩阵号最容易踩的坑)**:
+- ❌ 同一台手机登录多个账号(至少一机一号,用不同手机/电脑分身)
+- ❌ 同一 WiFi/网络环境同时发多个号的内容
+- ❌ 多个号互相点赞/评论/转发(同 IP 互动的营销信号最强)
+- ❌ 内容重复发布(同一篇换标题发两个号 = 判搬运)
+- ✅ 多个号内容要"同主题不同人设":同一案例,A号从健康角度讲,B号从教育角度讲
+
+### 4.4 个人号内容素材从哪来
+
+现有素材库直接复用,不用从零创作:
+
+| 素材源 | 怎么用 |
+|--------|--------|
+| 25 篇已发布内容 | 按人设改写成个人号版本(官方号发过的案例,个人号换第一人称重讲) |
+| 六阶模型内容方法论 | 每篇套"20秒动作 + 3天/14天变化 + 评论区打卡"模板 |
+| 双培强基知识库索引 | 心/智维度直接引用专家观点(刘亮/陶沙/郭晶等)+ 脱敏案例 |
+| 双培强基工程文章库 | 70+ 篇公众号旧文拆成小红书短文 |
+| 真实报告素材 | `temp_images\` 脱敏报告截图(个人号用体感叙述,官方号用数据背书) |
+
+---
+
+## 五、矩阵协同机制
+
+### 5.1 内容双轮驱动(远程方略核心落地)
+
+```
+个人号(种草)              官方号(收割)
+──────────────              ──────────────
+起点:用户兴趣              起点:用户搜索"值不值"
+终点:唤起购买欲            终点:打消顾虑做选择
+内容:生活场景+体感         内容:答案+数据+原理
+例:54岁妈妈的体检报告      例:浠艾福健康报告怎么看
+```
+
+**协同节奏**:
+1. 个人号先发种草笔记(某家庭做测评的体感故事)
+2. 3-7 天后官方号发收割笔记(对应搜索词 + 数据背书)
+3. 个人号在评论区被问到时回"测评是在官方号做的,搜浠艾福"
+4. 官方号私信承接,完成转化
+
+### 5.2 导流路径(公域 → 私域)
+
+```
+小红书公域                   私域
+─────────                   ─────
+个人号种草笔记               官方号私信
+  → 用户搜索"浠艾福"        → 自动回复(关键词触发)
+  → 官方号搜索收割笔记       → 引导加微信/公众号
+  → 官方号主页/简介          → 公众号"浠艾福"
+  → 私信咨询                 → 小程序测评转化
+```
+
+### 5.3 数据复盘与迭代
+
+沿用现有《发布数据复盘清单》指标,但增加矩阵维度:
+
+| 指标 | 健康值 | 预警线 |
+|------|--------|--------|
+| 官方号搜索词覆盖 | 核心 15 词全占 | <8 词即补内容 |
+| 个人号单篇涨粉 | ≥1% | <0.3% 换方向 |
+| 个人号→官方号导流量 | 每周 ≥20 次 | <5 次检查话术 |
+| 私信→微信转化率 | ≥30% | <15% 优化承接话术 |
+| 矩阵总涨粉/周 | 官方 20 + 个人 100 | 总 <50 复盘定位 |
+
+---
+
+## 六、冷启动 90 天执行计划
+
+| 阶段 | 时间 | 动作 | 里程碑 |
+|------|------|------|--------|
+| 第 1 阶段 | 第 1-2 周 | 官方号资料完善 + 5 篇搜索收割内容;个人号A/B 注册养号 | 官方号搜索词覆盖 5 个 |
+| 第 2 阶段 | 第 3-6 周 | 个人号A/B 定调期(前 10 篇);官方号每周 3 篇;开始合集挂载 | 个人号各破 100 粉 |
+| 第 3 阶段 | 第 7-10 周 | 个人号放量期(日更);建立"种草→收割"协同;评论区导流话术跑通 | 个人号 500 粉/号,官方号私信 10+/周 |
+| 第 4 阶段 | 第 11-13 周 | 扩个人号C/D;搜索词覆盖到 15 个;数据复盘定下一季度方向 | 矩阵总粉 2000+,私信→微信转化闭环 |
+
+---
+
+## 七、红线清单(沿用 + 矩阵新增)
+
+1. ❌ 个人号出现"官方/浠艾福官方"字样(个人号永远以真实个人出现)
+2. ❌ 暴露客户隐私(化名、学校不提及、日期模糊、数据打码)
+3. ❌ 虚构案例(所有案例数据来自真实 A2/B4/健康报告,截图为证)
+4. ❌ 夸大承诺("保证提升"类话术零容忍)
+5. ❌ 个人号与官方号内容重复发布(同主题不同人设角度)
+6. ❌ 矩阵号互赞互评互转(防关联营销判定)
+7. ❌ 官方号过度商业化(每 3-4 篇最多 1 篇硬广)
+
+---
+
+*方略制定完成。第一步行动:完成官方号搜索收割内容 5 篇 + 注册个人号A/B 开始养号。*