Просмотр исходного кода

fix: 多控制器支持 familyId 为空 + WeChat token 过期重试 + 服务/产品改进

- 30+ 控制器: @RequestAttribute familyId 改为 required=false,避免未加入家庭时空指针
- JwtInterceptor: 查询 familyId 失败时显式设为 null 而非空置
- WechatService: 新增 invalidateAccessToken() + 过期重试机制(42001/40001)
- UserService: 大幅重构和改进
- MembershipService: 新增逻辑
- FamilyJoinRequestService: 改进
- ProductController: 产品相关改进
- cfc-web: 构建版本同步
Xiaogang Liao 2 месяцев назад
Родитель
Сommit
8129ee6e53
38 измененных файлов с 317 добавлено и 184 удалено
  1. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java
  2. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/BadgeController.java
  3. 6 6
      cfc-backend/src/main/java/com/etotem/cfc/controller/DanAssessmentController.java
  4. 6 6
      cfc-backend/src/main/java/com/etotem/cfc/controller/EnergyController.java
  5. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/FamilyHealthController.java
  6. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/FoodRecommendController.java
  7. 5 5
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthCircleController.java
  8. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthInteractionController.java
  9. 5 5
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java
  10. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/InviteCardController.java
  11. 3 3
      cfc-backend/src/main/java/com/etotem/cfc/controller/PromotionController.java
  12. 2 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/ReportStatsController.java
  13. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/ShareController.java
  14. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/EcomSupplierController.java
  15. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentComparisonController.java
  16. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/content/ArticleController.java
  17. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyInviteController.java
  18. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/InteractionLogController.java
  19. 3 3
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/RelationshipQualityController.java
  20. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/growth/GrowthRecordController.java
  21. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideAssessmentController.java
  22. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideController.java
  23. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/PlanReviewController.java
  24. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/TeacherMessageController.java
  25. 2 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/mind/MindFortuneController.java
  26. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/mind/TraditionalMirrorController.java
  27. 36 7
      cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductController.java
  28. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/recommendation/ProductRecommendationController.java
  29. 7 7
      cfc-backend/src/main/java/com/etotem/cfc/controller/subscription/SubscriptionController.java
  30. 3 3
      cfc-backend/src/main/java/com/etotem/cfc/controller/tianpan/TianpanController.java
  31. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/wish/WishController.java
  32. 15 3
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyJoinRequestService.java
  33. 28 0
      cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java
  34. 88 31
      cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
  35. 75 43
      cfc-backend/src/main/java/com/etotem/cfc/service/WechatService.java
  36. 1 1
      cfc-web/.last_build_commit
  37. 6 19
      cfc-web/public/CHANGELOG-v1.0.md
  38. 6 19
      cfc-web/public/CHANGELOG.md

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java

@@ -141,6 +141,7 @@ public class JwtInterceptor implements HandlerInterceptor {
                 request.setAttribute("familyId", userFamilyId);
             } catch (Exception e) {
                 log.warn("查询用户familyId失败: userId={}, error={}", userId, e.getMessage());
+                request.setAttribute("familyId", null);
             }
 
             // 设置currentMemberId:child角色自动从family_member表查找

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

@@ -123,7 +123,7 @@ public class BadgeController {
     @Operation(summary = "家庭勋章排行")
     @PostMapping("/ranking/family")
     public Result<List<Map<String, Object>>> getFamilyRanking(@RequestBody Map<String, Object> body,
-                                                              @RequestAttribute("familyId") Long familyId) {
+                                                              @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }

+ 6 - 6
cfc-backend/src/main/java/com/etotem/cfc/controller/DanAssessmentController.java

@@ -79,7 +79,7 @@ public class DanAssessmentController {
 
     @Operation(summary = "获取家庭测评配置")
     @PostMapping("/family/config")
-    public Result<FamilyAssessmentConfig> getFamilyConfig(@RequestAttribute("familyId") Long familyId) {
+    public Result<FamilyAssessmentConfig> getFamilyConfig(@RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
@@ -89,7 +89,7 @@ public class DanAssessmentController {
     @Operation(summary = "启用家庭测评")
     @PostMapping("/family/enable")
     public Result<String> enableFamilyAssessment(@RequestBody Map<String, Object> body,
-                                                  @RequestAttribute("familyId") Long familyId) {
+                                                  @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
@@ -105,7 +105,7 @@ public class DanAssessmentController {
 
     @Operation(summary = "禁用家庭测评")
     @PostMapping("/family/disable")
-    public Result<String> disableFamilyAssessment(@RequestAttribute("familyId") Long familyId) {
+    public Result<String> disableFamilyAssessment(@RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
@@ -118,7 +118,7 @@ public class DanAssessmentController {
     @Operation(summary = "创建测评记录")
     @PostMapping("/record/create")
     public Result<AssessmentRecord> createRecord(@RequestBody Map<String, Object> body,
-                                                 @RequestAttribute("familyId") Long familyId,
+                                                 @RequestAttribute(value = "familyId", required = false) Long familyId,
                                                  @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
@@ -274,7 +274,7 @@ public class DanAssessmentController {
     @Operation(summary = "创建测评订单(含预约)")
     @PostMapping("/order/create")
     public Result<AssessmentOrder> createOrder(@RequestBody Map<String, Object> body,
-                                               @RequestAttribute("familyId") Long familyId,
+                                               @RequestAttribute(value = "familyId", required = false) Long familyId,
                                                @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
@@ -326,7 +326,7 @@ public class DanAssessmentController {
     @Operation(summary = "取消订单")
     @PostMapping("/order/cancel")
     public Result<String> cancelOrder(@RequestBody Map<String, Object> body,
-                                      @RequestAttribute("familyId") Long familyId) {
+                                      @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }

+ 6 - 6
cfc-backend/src/main/java/com/etotem/cfc/controller/EnergyController.java

@@ -58,7 +58,7 @@ public class EnergyController {
     @PostMapping("/overview")
     public Result<Map<String, Object>> getEnergyOverview(
             @RequestBody(required = false) Map<String, Object> params,
-            @RequestAttribute("familyId") Long familyId,
+            @RequestAttribute(value = "familyId", required = false) Long familyId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
         if (params == null) {
             return Result.error("请求体不能为空");
@@ -82,7 +82,7 @@ public class EnergyController {
     @PostMapping("/logs")
     public Result<Page<EnergyLog>> getEnergyLogs(
             @RequestBody Map<String, Object> params,
-            @RequestAttribute("familyId") Long familyId,
+            @RequestAttribute(value = "familyId", required = false) Long familyId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
         Long memberId = params.get("memberId") != null
                 ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
@@ -109,7 +109,7 @@ public class EnergyController {
     @PostMapping("/grant")
     public Result<EnergyLog> grantEnergy(
             @RequestBody Map<String, Object> params,
-            @RequestAttribute("familyId") Long familyId,
+            @RequestAttribute(value = "familyId", required = false) Long familyId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
         Long memberId = params.get("memberId") != null
                 ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
@@ -137,7 +137,7 @@ public class EnergyController {
     @PostMapping("/deduct-for-wish")
     public Result<Boolean> deductForWish(
             @RequestBody Map<String, Object> params,
-            @RequestAttribute("familyId") Long familyId,
+            @RequestAttribute(value = "familyId", required = false) Long familyId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
         Long memberId = params.get("memberId") != null
                 ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
@@ -164,7 +164,7 @@ public class EnergyController {
     @PostMapping("/configure")
     public Result<String> configureSourceRatios(
             @RequestBody Map<String, Object> params,
-            @RequestAttribute("familyId") Long familyId,
+            @RequestAttribute(value = "familyId", required = false) Long familyId,
             @RequestAttribute("userId") Long userId) {
         String sourceType = (String) params.get("sourceType");
         Long sourceId = params.get("sourceId") != null
@@ -212,7 +212,7 @@ public class EnergyController {
     @PostMapping("/family-score-history")
     public Result<java.util.Map<String, Integer>> getFamilyScoreHistory(
             @RequestBody Map<String, Object> params,
-            @RequestAttribute("familyId") Long familyId) {
+            @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }

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

@@ -15,7 +15,7 @@ public class FamilyHealthController {
     private FamilyHealthScoreService familyHealthScoreService;
 
     @PostMapping("/ranking")
-    public Result<Map<String, Object>> getRanking(@RequestAttribute("familyId") Long familyId,
+    public Result<Map<String, Object>> getRanking(@RequestAttribute(value = "familyId", required = false) Long familyId,
                                                     @RequestAttribute("userId") Long userId,
                                                     @RequestAttribute("role") String role) {
         if (familyId == null) {

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

@@ -46,7 +46,7 @@ public class FoodRecommendController {
 
     @Operation(summary = "按家庭分组获取食材推荐指数")
     @PostMapping("/by-family")
-    public Result<Map<Long, List<FoodRecommendIndex>>> getByFamily(@RequestAttribute("familyId") Long familyId) {
+    public Result<Map<Long, List<FoodRecommendIndex>>> getByFamily(@RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }

+ 5 - 5
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthCircleController.java

@@ -19,7 +19,7 @@ public class HealthCircleController {
     @PostMapping("/list")
     public Result<List<HealthCircle>> list(
             @RequestBody Map<String, Object> params,
-            @RequestAttribute("familyId") Long familyId) {
+            @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
@@ -29,7 +29,7 @@ public class HealthCircleController {
     @PostMapping("/create")
     public Result<HealthCircle> create(
             @RequestBody Map<String, Object> params,
-            @RequestAttribute("familyId") Long familyId) {
+            @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
@@ -41,7 +41,7 @@ public class HealthCircleController {
     @PostMapping("/join")
     public Result<Void> join(
             @RequestBody Map<String, Object> params,
-            @RequestAttribute("familyId") Long familyId) {
+            @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
@@ -52,7 +52,7 @@ public class HealthCircleController {
     @PostMapping("/leave")
     public Result<Void> leave(
             @RequestBody Map<String, Object> params,
-            @RequestAttribute("familyId") Long familyId) {
+            @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
@@ -64,7 +64,7 @@ public class HealthCircleController {
     @PostMapping("/ranking")
     public Result<Map<String, Object>> ranking(
             @RequestBody Map<String, Object> params,
-            @RequestAttribute("familyId") Long familyId) {
+            @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }

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

@@ -18,7 +18,7 @@ public class HealthInteractionController {
     public Result<Void> like(@RequestBody Map<String, Object> params,
                               @RequestAttribute("userId") Long userId,
                               @RequestAttribute("role") String role,
-                              @RequestAttribute("familyId") Long familyId) {
+                              @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }

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

@@ -135,7 +135,7 @@ public class HealthReportController {
     public Result<HealthReport> createReport(
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId,
-            @RequestAttribute("familyId") Long familyId,
+            @RequestAttribute(value = "familyId", required = false) Long familyId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
 
         if (familyId == null) {
@@ -218,7 +218,7 @@ public class HealthReportController {
     @PostMapping("/report/latest")
     public Result<HealthReport> getLatestReport(
             @RequestBody Map<String, Object> params,
-            @RequestAttribute("familyId") Long familyId,
+            @RequestAttribute(value = "familyId", required = false) Long familyId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
@@ -778,7 +778,7 @@ public class HealthReportController {
     @Operation(summary = "获取未绑定的报告列表")
     @PostMapping("/report/unlinked")
     public Result<List<HealthReport>> getUnlinkedReports(
-            @RequestAttribute("familyId") Long familyId) {
+            @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
@@ -974,7 +974,7 @@ public class HealthReportController {
 
     @Operation(summary = "获取家庭成员食材推荐(含完整营养数据)")
     @PostMapping("/foods/by-family")
-    public Result<Map<String, Object>> getFoodsByFamily(@RequestAttribute("familyId") Long familyId) {
+    public Result<Map<String, Object>> getFoodsByFamily(@RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
@@ -1044,7 +1044,7 @@ public class HealthReportController {
     public Result<Map<String, Object>> autoCreateMember(
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId,
-            @RequestAttribute("familyId") Long familyId) {
+            @RequestAttribute(value = "familyId", required = false) Long familyId) {
 
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");

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

@@ -42,7 +42,7 @@ public class InviteCardController {
     @PostMapping("/generate")
     @Operation(summary = "生成邀请码")
     public Result<Map<String, Object>> generate(@RequestAttribute("userId") Long userId,
-                                                @RequestAttribute("familyId") Long familyId,
+                                                @RequestAttribute(value = "familyId", required = false) Long familyId,
                                                 @RequestBody Map<String, Object> params) {
         String type = params.get("type") != null ? params.get("type").toString() : null;
         if (type == null) {

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

@@ -20,7 +20,7 @@ public class PromotionController {
 
     @PostMapping("/bind")
     public Result<Object> bind(@RequestBody Map<String, Object> params,
-                               @RequestAttribute("familyId") Long familyId) {
+                               @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
@@ -38,7 +38,7 @@ public class PromotionController {
     }
 
     @PostMapping("/commission/list")
-    public Result<Map<String, Object>> commissionList(@RequestAttribute("familyId") Long familyId,
+    public Result<Map<String, Object>> commissionList(@RequestAttribute(value = "familyId", required = false) Long familyId,
                                                       @RequestBody Map<String, Object> params) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
@@ -55,7 +55,7 @@ public class PromotionController {
     }
 
     @PostMapping("/bindings/list")
-    public Result<List> bindingsList(@RequestAttribute("familyId") Long familyId,
+    public Result<List> bindingsList(@RequestAttribute(value = "familyId", required = false) Long familyId,
                                      @RequestBody Map<String, Object> params) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");

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

@@ -148,7 +148,7 @@ public class ReportStatsController {
     @Operation(summary = "家庭汇总统计")
     @PostMapping("/family/summary")
     public Result<Map<String, Object>> familySummary(@RequestBody Map<String, Object> body,
-                                                     @RequestAttribute("familyId") Long familyId) {
+                                                     @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
@@ -195,7 +195,7 @@ public class ReportStatsController {
     @Operation(summary = "积分排行榜(家庭内)")
     @PostMapping("/points/ranking")
     public Result<List<Map<String, Object>>> pointsRanking(@RequestBody Map<String, Object> body,
-                                                           @RequestAttribute("familyId") Long familyId) {
+                                                           @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }

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

@@ -107,7 +107,7 @@ public class ShareController {
 
     @Operation(summary = "获取家庭健康足迹(P3分享页)")
     @PostMapping("/family-footprint")
-    public Result<Map<String, Object>> familyFootprint(@RequestAttribute("familyId") Long familyId) {
+    public Result<Map<String, Object>> familyFootprint(@RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }

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

@@ -13,7 +13,7 @@ import java.util.Map;
 
 @Tag(name = "管理端-电商供应商")
 @RestController
-@RequestMapping("/api/admin/supplier")
+@RequestMapping("/api/admin/ecom-supplier")
 public class EcomSupplierController {
 
     @Resource

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

@@ -67,7 +67,7 @@ public class AssessmentComparisonController {
     @Operation(summary = "对比家庭所有孩子首次与最新测评")
     @PostMapping("/by-family")
     public Result<List<AssessmentComparisonService.CompareResult>> compareByFamily(
-            @RequestAttribute("familyId") Long familyId) {
+            @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }

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

@@ -21,7 +21,7 @@ import java.util.Map;
 
 @Tag(name = "文章内容", description = "文章列表/阅读统计/每日贴士")
 @RestController
-@RequestMapping("/api/articles")
+@RequestMapping({"/api/articles", "/api/article"})
 public class ArticleController {
 
     @Resource

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

@@ -192,7 +192,7 @@ public class FamilyInviteController {
     @Operation(summary = "查询待审批加入请求")
     @PostMapping("/pending-requests")
     public Result<java.util.List<com.etotem.cfc.dto.FamilyJoinRequestDTO>> getPendingRequests(
-        @RequestAttribute("familyId") Long familyId,
+        @RequestAttribute(value = "familyId", required = false) Long familyId,
         @RequestAttribute("userId") Long userId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");

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

@@ -66,7 +66,7 @@ public class InteractionLogController {
 @PostMapping("/list")
 public Result<Page<InteractionLogVO>> listInteractions(@RequestBody Map<String, Object> body,
  @RequestAttribute("userId") Long userId,
- @RequestAttribute("familyId") Long familyId) {
+ @RequestAttribute(value = "familyId", required = false) Long familyId) {
  if (userId == null) {
   return Result.error("请先登录");
  }

+ 3 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/family/RelationshipQualityController.java

@@ -29,7 +29,7 @@ public class RelationshipQualityController {
 
     @Operation(summary = "获取家庭成员关系质量评分")
     @PostMapping("/scores")
-    public Result<List<RelationshipScoreVO>> getScores(@RequestAttribute("familyId") Long familyId) {
+    public Result<List<RelationshipScoreVO>> getScores(@RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
@@ -57,7 +57,7 @@ public class RelationshipQualityController {
     @PostMapping("/health-alerts")
     public Result<List<HealthAlertVO>> getHealthAlerts(@RequestBody Map<String, Object> body,
                                                       @RequestAttribute("userId") Long userId,
-                                                      @RequestAttribute("familyId") Long familyId) {
+                                                      @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (userId == null) {
             return Result.error("请先登录");
         }
@@ -73,7 +73,7 @@ public class RelationshipQualityController {
     @PostMapping("/milestones")
     public Result<List<MilestoneVO>> getMilestones(@RequestBody Map<String, Object> body,
                                                     @RequestAttribute("userId") Long userId,
-                                                    @RequestAttribute("familyId") Long familyId) {
+                                                    @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (userId == null) {
             return Result.error("请先登录");
         }

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

@@ -60,7 +60,7 @@ public class GrowthRecordController {
     public Result<Map<String, Object>> createRecordWithOrder(
             @RequestBody Map<String, Object> body,
             @RequestAttribute("userId") Long userId,
-            @RequestAttribute("familyId") Long familyId,
+            @RequestAttribute(value = "familyId", required = false) Long familyId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");

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

@@ -33,7 +33,7 @@ public class GuideAssessmentController {
     @PostMapping("/family/enable")
     public Result<Boolean> enableFamilyAssessment(
             @RequestAttribute("userId") Long teacherId,
-            @RequestAttribute("familyId") Long familyId,
+            @RequestAttribute(value = "familyId", required = false) Long familyId,
             @RequestBody Map<String, Object> body) {
 
         if (familyId == null) {

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

@@ -110,7 +110,7 @@ public class GuideController {
     @PostMapping("/parent-invite/generate")
     public Result<Map<String, Object>> generateParentInvite(@RequestBody Map<String, Object> params,
         @RequestAttribute("userId") Long userId,
-        @RequestAttribute("familyId") Long familyId) {
+        @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (userId == null) {
             return Result.error("未登录");
         }

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

@@ -39,7 +39,7 @@ public class PlanReviewController {
     public Result<List<TaskPlanInstance>> listFamilyPlans(@RequestBody Map<String, Object> body,
                                                            @RequestAttribute("userId") Long userId,
                                                            @RequestAttribute("role") String role,
-                                                           @RequestAttribute("familyId") Long familyId) {
+                                                           @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (!"teacher".equals(role) && !"admin".equals(role)) {
             return Result.error("无权限");
         }

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

@@ -32,7 +32,7 @@ public class TeacherMessageController {
     @Operation(summary = "家长发送咨询")
     @PostMapping("/send")
     public Result<Long> sendMessage(@RequestAttribute("userId") Long parentId,
-                                     @RequestAttribute("familyId") Long familyId,
+                                     @RequestAttribute(value = "familyId", required = false) Long familyId,
                                      @RequestBody Map<String, Object> body) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");

+ 2 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/mind/MindFortuneController.java

@@ -32,10 +32,10 @@ public class MindFortuneController {
     /**
      * 获取今日家庭运势
      */
-    @PostMapping("/fortune")
+@PostMapping("/fortune")
     public Result<Map<String, Object>> getTodayFortune(@RequestAttribute("userId") Long userId) {
         if (userId == null) return Result.error("请先登录");
-        Long familyId = membershipService.getUserFamilyId(userId);
+        Long familyId = membershipService.getOrCreateUserFamilyId(userId);
         FamilyFortune fortune = fortuneService.getFamilyFortune(familyId, LocalDate.now());
         if (fortune == null) return Result.error("暂无家庭运势数据");
         Map<String, Object> result = batchBuildFortuneResponse(fortune);

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

@@ -84,7 +84,7 @@ public class TraditionalMirrorController {
      * Get family dashboard: all members + family wuxing + pairwise summaries + weekly insight
      */
     @PostMapping("/family-dashboard")
-    public Result<?> familyDashboard(@RequestAttribute("familyId") Long familyId) {
+    public Result<?> familyDashboard(@RequestAttribute(value = "familyId", required = false) Long familyId) {
         try {
             if (familyId == null) {
                 return Result.noFamily("请先创建或加入家庭");

+ 36 - 7
cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductController.java

@@ -104,14 +104,12 @@ public class ProductController {
 
             for (ProductSku sku : skus) {
                 if (sku.getSpecs() == null) continue;
+                if (sku.getSpecs().toString().trim().isEmpty()) continue;
                 try {
-                    com.alibaba.fastjson.JSONArray arr = JSON.parseArray(sku.getSpecs().toString());
-                    if (arr == null) continue;
-                    for (int i = 0; i < arr.size(); i++) {
-                        com.alibaba.fastjson.JSONObject specObj = arr.getJSONObject(i);
-                        if (specObj == null) continue;
-                        String name = specObj.getString("name");
-                        String value = specObj.getString("value");
+                    List<Map<String, String>> specsList = parseSpecs(sku.getSpecs().toString());
+                    for (Map<String, String> specEntry : specsList) {
+                        String name = specEntry.get("name");
+                        String value = specEntry.get("value");
                         if (name == null || value == null) continue;
 
                         if (!nameToGroupId.containsKey(name)) {
@@ -158,4 +156,35 @@ public class ProductController {
         }
         return Result.success(result);
     }
+
+    private List<Map<String, String>> parseSpecs(String specsStr) {
+        String trimmed = specsStr.trim();
+        List<Map<String, String>> result = new ArrayList<>();
+        if (trimmed.startsWith("[")) {
+            com.alibaba.fastjson.JSONArray arr = JSON.parseArray(trimmed);
+            if (arr == null) return result;
+            for (int i = 0; i < arr.size(); i++) {
+                com.alibaba.fastjson.JSONObject specObj = arr.getJSONObject(i);
+                if (specObj == null) continue;
+                String name = specObj.getString("name");
+                String value = specObj.getString("value");
+                if (name != null && value != null) {
+                    Map<String, String> entry = new HashMap<>();
+                    entry.put("name", name);
+                    entry.put("value", value);
+                    result.add(entry);
+                }
+            }
+        } else if (trimmed.startsWith("{")) {
+            com.alibaba.fastjson.JSONObject obj = JSON.parseObject(trimmed);
+            if (obj == null) return result;
+            for (Map.Entry<String, Object> entry : obj.entrySet()) {
+                Map<String, String> specEntry = new HashMap<>();
+                specEntry.put("name", entry.getKey());
+                specEntry.put("value", entry.getValue() != null ? entry.getValue().toString() : "");
+                result.add(specEntry);
+            }
+        }
+        return result;
+    }
 }

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

@@ -27,7 +27,7 @@ public class ProductRecommendationController {
     @Operation(summary = "获取维度页推荐商品")
     @PostMapping("/dimension-products")
     public Result<List<Map<String, Object>>> getDimensionProducts(@RequestBody Map<String, Object> params,
-                                                                  @RequestAttribute("familyId") Long familyId) {
+                                                                  @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }

+ 7 - 7
cfc-backend/src/main/java/com/etotem/cfc/controller/subscription/SubscriptionController.java

@@ -56,7 +56,7 @@ public class SubscriptionController {
      * 获取当前家庭订阅状态
      */
     @PostMapping("/status")
-    public Result<Map<String, Object>> getSubscriptionStatus(@RequestAttribute Long familyId) {
+    public Result<Map<String, Object>> getSubscriptionStatus(@RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
@@ -97,7 +97,7 @@ public class SubscriptionController {
      * 创建订阅订单(JSON body)
      */
     @PostMapping("/create")
-    public Result<Object> createOrder(@RequestAttribute Long familyId,
+    public Result<Object> createOrder(@RequestAttribute(value = "familyId", required = false) Long familyId,
                                       @RequestAttribute("userId") Long userId,
                                       @RequestBody Map<String, Object> body) {
         if (familyId == null) {
@@ -145,7 +145,7 @@ public class SubscriptionController {
      * 订阅(参数式)
      */
     @PostMapping("/subscribe")
-    public Result<Void> subscribe(@RequestAttribute Long familyId,
+    public Result<Void> subscribe(@RequestAttribute(value = "familyId", required = false) Long familyId,
                                    @RequestParam String level,
                                    @RequestParam Integer amount,
                                    @RequestParam String paymentType,
@@ -165,7 +165,7 @@ public class SubscriptionController {
      * 取消订阅
      */
     @PostMapping("/cancel")
-    public Result<Void> cancel(@RequestAttribute Long familyId) {
+    public Result<Void> cancel(@RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
@@ -177,7 +177,7 @@ public class SubscriptionController {
      * 设置自动续费
      */
     @PostMapping("/auto-renew")
-    public Result<Void> setAutoRenew(@RequestAttribute Long familyId,
+    public Result<Void> setAutoRenew(@RequestAttribute(value = "familyId", required = false) Long familyId,
                                       @RequestBody Map<String, Object> body) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
@@ -196,7 +196,7 @@ public class SubscriptionController {
      * 获取我的权益列表
      */
     @PostMapping("/benefits/my")
-    public Result<List<Map<String, Object>>> getMyBenefits(@RequestAttribute Long familyId) {
+    public Result<List<Map<String, Object>>> getMyBenefits(@RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
@@ -227,7 +227,7 @@ public class SubscriptionController {
      * 使用权益
      */
     @PostMapping("/benefit/check")
-    public Result<Map<String, Object>> checkBenefit(@RequestAttribute Long familyId,
+    public Result<Map<String, Object>> checkBenefit(@RequestAttribute(value = "familyId", required = false) Long familyId,
                                                       @RequestBody Map<String, Object> body) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");

+ 3 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/tianpan/TianpanController.java

@@ -30,7 +30,7 @@ public class TianpanController {
      */
     @PostMapping("/dashboard")
     public Result<TianpanDashboardVO> dashboard(HttpServletRequest request, @RequestBody Map<String, Object> params,
-                                                 @RequestAttribute("familyId") Long familyId) {
+                                                 @RequestAttribute(value = "familyId", required = false) Long familyId) {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
@@ -161,7 +161,7 @@ public class TianpanController {
     public Result<DailyFortuneVO> dailyFortune(
             HttpServletRequest request,
             @RequestBody Map<String, Object> params,
-            @RequestAttribute("familyId") Long familyId) {
+            @RequestAttribute(value = "familyId", required = false) Long familyId) {
         
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
@@ -189,7 +189,7 @@ public class TianpanController {
     public Result<RelatedItemsVO> relatedItems(
             HttpServletRequest request,
             @RequestBody Map<String, Object> params,
-            @RequestAttribute("familyId") Long familyId) {
+            @RequestAttribute(value = "familyId", required = false) Long familyId) {
         
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");

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

@@ -224,7 +224,7 @@ public class WishController {
     @PostMapping("/pending")
     public Result<List<WishDTO>> getPendingWishes(
             @RequestAttribute("userId") Long userId,
-            @RequestAttribute("familyId") Long familyId) {
+            @RequestAttribute(value = "familyId", required = false) Long familyId) {
         try {
             if (familyId == null) {
                 return Result.noFamily("请先创建或加入家庭");

+ 15 - 3
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyJoinRequestService.java

@@ -34,6 +34,9 @@ public class FamilyJoinRequestService {
     @Resource
     private UserMapper userMapper;
 
+    @Resource
+    private FamilyInvitationService familyInvitationService;
+
     /**
      * 创建家庭加入请求(待审批)
      */
@@ -140,9 +143,18 @@ public class FamilyJoinRequestService {
         if (user == null) {
             throw new RuntimeException("请求用户不存在");
         }
-        user.setFamilyId(family.getId());
-        user.setUpdatedAt(new Date());
-        userMapper.updateById(user);
+
+        if (user.getFamilyId() != null && user.getFamilyId() != 0L && !user.getFamilyId().equals(family.getId())) {
+            familyInvitationService.mergeIntoFamily(user.getId(), family.getId());
+            log.info("审批通过并合并家庭: userId={}, oldFamily={}, targetFamily={}, requestId={}",
+                    user.getId(), user.getFamilyId(), family.getId(), request.getId());
+        } else {
+            if (user.getFamilyId() == null || user.getFamilyId() == 0L || !user.getFamilyId().equals(family.getId())) {
+                user.setFamilyId(family.getId());
+                user.setUpdatedAt(new Date());
+                userMapper.updateById(user);
+            }
+        }
 
         request.setStatus("approved");
         request.setApproverUserId(adminUserId);

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

@@ -11,11 +11,13 @@ import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
+import java.util.UUID;
 import java.util.stream.Collectors;
 
 @Slf4j
@@ -79,6 +81,32 @@ public class MembershipService implements MembershipServiceInterface {
         return user.getFamilyId();
     }
 
+    @Transactional
+    public Long getOrCreateUserFamilyId(Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            throw new RuntimeException("用户不存在");
+        }
+        if (user.getFamilyId() != null && user.getFamilyId() != 0L) {
+            return user.getFamilyId();
+        }
+        Family family = new Family();
+        String familyName = (user.getNickname() != null && !user.getNickname().isEmpty())
+                ? user.getNickname() + "的家庭" : "我的家庭";
+        family.setName(familyName);
+        family.setInviteCode(UUID.randomUUID().toString().substring(0, 8).toUpperCase());
+        family.setCreatorId(user.getId());
+        family.setCreatedAt(new Date());
+        family.setUpdatedAt(new Date());
+        familyMapper.insert(family);
+        user.setFamilyId(family.getId());
+        user.setIsFamilyAdmin(true);
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+        log.info("自动为用户 {} 创建家庭 {} ({})", userId, family.getId(), familyName);
+        return family.getId();
+    }
+
     /**
      * 获取家庭的会员信息
      */

+ 88 - 31
cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java

@@ -116,7 +116,7 @@ private FamilyInvitationService familyInvitationService;
                 user.setSessionKey(sessionData.get("session_key"));
                 user.setCreatedAt(new Date());
                 user.setUpdatedAt(new Date());
-                userMapper.insert(user);
+                createUserAndFamily(user);
                 try { onboardingService.initForUser(user.getId()); onboardingService.completeTask(user.getId(), "REGISTER"); } catch (Exception e) { /* onboarding init failure should not block login */ }
             } else {
                 // 6. 手机号已存在但openid不同,绑定openid
@@ -136,6 +136,8 @@ private FamilyInvitationService familyInvitationService;
             userMapper.updateById(user);
         }
 
+        ensureUserHasFamily(user);
+
         // 管理员不能登录小程序
         if ("admin".equals(user.getRole())) {
             throw new RuntimeException("该账号不允许登录小程序");
@@ -197,7 +199,7 @@ private FamilyInvitationService familyInvitationService;
     user.setPassword("");
     user.setCreatedAt(new Date());
     user.setUpdatedAt(new Date());
-    userMapper.insert(user);
+    createUserAndFamily(user);
     try { onboardingService.initForUser(user.getId()); onboardingService.completeTask(user.getId(), "REGISTER"); } catch (Exception e) { /* onboarding init failure should not block login */ }
   } else {
     // 老用户,更新信息(如果提供了)
@@ -211,6 +213,8 @@ private FamilyInvitationService familyInvitationService;
     userMapper.updateById(user);
   }
 
+  ensureUserHasFamily(user);
+
   // 生成token
   String token = jwtConfig.generateToken(user.getId(), user.getRole());
 
@@ -267,6 +271,7 @@ private FamilyInvitationService familyInvitationService;
         user.setSessionKey(sessionData.get("session_key"));
         user.setUpdatedAt(new Date());
         userMapper.updateById(user);
+        ensureUserHasFamily(user);
 
         // 4. 生成token
         String token = jwtConfig.generateToken(user.getId(), user.getRole());
@@ -302,6 +307,7 @@ private FamilyInvitationService familyInvitationService;
             return null;
         }
 
+        ensureUserHasFamily(user);
         String token = jwtConfig.generateToken(user.getId(), user.getRole());
 
         LoginResultDTO result = new LoginResultDTO();
@@ -509,7 +515,7 @@ private FamilyInvitationService familyInvitationService;
         user.setPassword("");
         user.setCreatedAt(new Date());
         user.setUpdatedAt(new Date());
-        userMapper.insert(user);
+        createUserAndFamily(user);
 
         // 如果有邀请码,提交加入家庭请求(需管理员审批)
         if (dto.getInviteCode() != null && !dto.getInviteCode().isEmpty()) {
@@ -890,9 +896,11 @@ private FamilyInvitationService familyInvitationService;
             user.setPassword(""); // 初始密码为空,需要后续设置
             user.setCreatedAt(new Date());
             user.setUpdatedAt(new Date());
-            userMapper.insert(user);
+            createUserAndFamily(user);
         }
 
+        ensureUserHasFamily(user);
+
         // 生成token
         String token = jwtConfig.generateToken(user.getId(), user.getRole());
 
@@ -1074,10 +1082,16 @@ FamilyMember child = familyMemberMapper.selectById(memberId);
         }
         // 更新性别
         if (dto.getGender() != null) {
-            if (!"male".equals(dto.getGender()) && !"female".equals(dto.getGender())) {
+            String gender = dto.getGender();
+            if ("男".equals(gender) || "0".equals(gender)) {
+                gender = "male";
+            } else if ("女".equals(gender) || "1".equals(gender)) {
+                gender = "female";
+            }
+            if (!"male".equals(gender) && !"female".equals(gender)) {
                 throw new RuntimeException("性别取值非法,仅支持 male/female");
             }
-            user.setGender(dto.getGender());
+            user.setGender(gender);
         }
         // 更新生日(将字符串转换为Date)
         if (dto.getBirthday() != null && !dto.getBirthday().isEmpty()) {
@@ -1247,6 +1261,58 @@ FamilyMember child = familyMemberMapper.selectById(memberId);
         return true;
     }
 
+    /**
+     * 创建用户并自动创建家庭(注册时用)
+     * @return 创建后的 user(含 familyId 和 isFamilyAdmin)
+     */
+    private User createUserAndFamily(User user) {
+        // 1. 插入 user
+        userMapper.insert(user);
+
+        // 2. 创建家庭
+        Family family = new Family();
+        String familyName = (user.getNickname() != null && !user.getNickname().isEmpty())
+                ? user.getNickname() + "的家庭" : "我的家庭";
+        family.setName(familyName);
+        family.setInviteCode(generateInviteCode());
+        family.setCreatorId(user.getId());
+        family.setCreatedAt(new Date());
+        family.setUpdatedAt(new Date());
+        familyMapper.insert(family);
+
+        // 3. 设置 user 的家庭关联
+        user.setFamilyId(family.getId());
+        user.setIsFamilyAdmin(true);
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+
+        log.info("已为用户 {} 自动创建家庭 {} ({})", user.getId(), family.getId(), familyName);
+        return user;
+    }
+
+    /**
+     * 确保用户有家庭(安全兜底:注册/登录时如果 familyId 为空则自动创建)
+     */
+    private void ensureUserHasFamily(User user) {
+        if (user.getFamilyId() != null && user.getFamilyId() != 0L) {
+            return; // 已有家庭
+        }
+        Family family = new Family();
+        String familyName = (user.getNickname() != null && !user.getNickname().isEmpty())
+                ? user.getNickname() + "的家庭" : "我的家庭";
+        family.setName(familyName);
+        family.setInviteCode(generateInviteCode());
+        family.setCreatorId(user.getId());
+        family.setCreatedAt(new Date());
+        family.setUpdatedAt(new Date());
+        familyMapper.insert(family);
+        user.setFamilyId(family.getId());
+        user.setIsFamilyAdmin(true);
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+        log.info("安全兜底:为用户 {} 自动创建家庭 {}", user.getId(), family.getId());
+    }
+
     /**
      * 为用户创建新家庭
      */
@@ -1289,9 +1355,10 @@ FamilyMember child = familyMemberMapper.selectById(memberId);
             user.setPassword("");
             user.setCreatedAt(new Date());
             user.setUpdatedAt(new Date());
-            userMapper.insert(user);
+            createUserAndFamily(user);
 
             distributionService.registerByInviteCode(inviteCode, user.getId());
+            ensureUserHasFamily(user);
 
             String token = jwtConfig.generateToken(user.getId(), user.getRole());
 
@@ -1328,46 +1395,36 @@ FamilyMember child = familyMemberMapper.selectById(memberId);
                 throw new RuntimeException("该账号不允许登录小程序");
             }
 
-            // 手机号已注册,加入家庭——如有旧家庭则合并
             user = existingUser;
-            if (user.getFamilyId() != null && !user.getFamilyId().equals(family.getId())) {
-                familyInvitationService.mergeIntoFamily(user.getId(), family.getId());
-            } else {
-                user.setFamilyId(family.getId());
-                user.setUpdatedAt(new Date());
-                userMapper.updateById(user);
+            ensureUserHasFamily(user);
+            try {
+                familyJoinRequestService.createJoinRequest(user.getId(), inviteCode);
+                log.info("已为用户 {} 提交加入家庭 {} 的请求", user.getId(), family.getId());
+            } catch (Exception e) {
+                log.warn("提交加入家庭请求失败: {}", e.getMessage());
             }
         } else {
-            // 创建新用户并加入家庭
             isNewUser = true;
             user = new User();
             user.setOpenid("phone:" + phone);
             user.setUnionid("");
-            user.setFamilyId(family.getId());
             user.setRole(role);
             user.setNickname(nickname);
             user.setPhone(phone);
             user.setPassword("");
             user.setCreatedAt(new Date());
             user.setUpdatedAt(new Date());
-            userMapper.insert(user);
-        }
-
-        // 如果家庭绑定了成长规划师,自动将家庭ID添加到成长规划师的 teacherFamilyIds
-        if (family.getTeacherId() != null) {
-            User teacher = userMapper.selectById(family.getTeacherId());
-            if (teacher != null && "teacher".equals(teacher.getRole())) {
-                String currentIds = teacher.getTeacherFamilyIds();
-                String newFamilyId = String.valueOf(family.getId());
-                if (currentIds == null || !currentIds.contains(newFamilyId)) {
-                    String updatedIds = currentIds == null ? newFamilyId : currentIds + "," + newFamilyId;
-                    teacher.setTeacherFamilyIds(updatedIds);
-                    teacher.setUpdatedAt(new Date());
-                    userMapper.updateById(teacher);
-                }
+            createUserAndFamily(user);
+            try {
+                familyJoinRequestService.createJoinRequest(user.getId(), inviteCode);
+                log.info("已为用户 {} 提交加入家庭 {} 的请求", user.getId(), family.getId());
+            } catch (Exception e) {
+                log.warn("提交加入家庭请求失败: {}", e.getMessage());
             }
         }
 
+        ensureUserHasFamily(user);
+
         // 生成token
         String token = jwtConfig.generateToken(user.getId(), user.getRole());
 

+ 75 - 43
cfc-backend/src/main/java/com/etotem/cfc/service/WechatService.java

@@ -246,6 +246,17 @@ public class WechatService implements WechatServiceInterface {
         }
     }
 
+    /**
+     * 清除缓存的 access_token(当微信返回 42001/40001 时调用,强制下次重新获取)
+     * 说明:stable_token 接口在 token 未过期时会返回同一个 token,跨进程/重启继承的
+     * token 可能已临近真实过期时间,本地按获取时刻算的 TTL 会失真,因此必须
+     * 在 API 返回过期/失效错误时主动清除缓存,让 stable_token 重新签发新 token。
+     */
+    public synchronized void invalidateAccessToken() {
+        cachedStableAccessToken = null;
+        stableTokenExpireTime = 0;
+    }
+
     private String doGetStableAccessToken() {
         try {
             String url = "https://api.weixin.qq.com/cgi-bin/stable_token";
@@ -312,22 +323,17 @@ public class WechatService implements WechatServiceInterface {
         }
 
         try {
-            String accessToken = getAccessToken();
-            String url = "https://api.weixin.qq.com/wxa/msg_sec_check?access_token=" + accessToken;
-
-            JSONObject requestBody = new JSONObject();
-            requestBody.put("content", content);
-
-            org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
-            headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
-            org.springframework.http.HttpEntity<String> entity = new org.springframework.http.HttpEntity<>(
-                    requestBody.toJSONString(), headers);
-
-            ResponseEntity<String> response = restTemplate.exchange(url,
-                    org.springframework.http.HttpMethod.POST, entity, String.class);
-            JSONObject json = JSON.parseObject(response.getBody());
+            JSONObject json = callMsgSecCheck(getAccessToken(), content);
             int errcode = json.getInteger("errcode");
 
+            // access_token 过期/失效(42001/40001):清除缓存后用新 token 重试一次
+            if (errcode == 42001 || errcode == 40001) {
+                log.warn("文本内容安全检测 access_token 过期/失效(errcode={}),清除缓存重试", errcode);
+                invalidateAccessToken();
+                json = callMsgSecCheck(getAccessToken(), content);
+                errcode = json.getInteger("errcode");
+            }
+
             if (errcode == 0) {
                 return true;
             } else if (errcode == 87014) {
@@ -344,6 +350,22 @@ public class WechatService implements WechatServiceInterface {
         }
     }
 
+    private JSONObject callMsgSecCheck(String accessToken, String content) {
+        String url = "https://api.weixin.qq.com/wxa/msg_sec_check?access_token=" + accessToken;
+
+        JSONObject requestBody = new JSONObject();
+        requestBody.put("content", content);
+
+        org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
+        headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
+        org.springframework.http.HttpEntity<String> entity = new org.springframework.http.HttpEntity<>(
+                requestBody.toJSONString(), headers);
+
+        ResponseEntity<String> response = restTemplate.exchange(url,
+                org.springframework.http.HttpMethod.POST, entity, String.class);
+        return JSON.parseObject(response.getBody());
+    }
+
     /**
      * 图片内容安全检测(img_sec_check)
      * @param imageBytes 图片二进制数据,格式支持PNG/JPEG/JPG/GIF
@@ -366,37 +388,17 @@ public class WechatService implements WechatServiceInterface {
         }
 
         try {
-            String accessToken = getAccessToken();
-            String url = "https://api.weixin.qq.com/wxa/img_sec_check?access_token=" + accessToken;
-
-            // 使用 HttpURLConnection 发送 multipart/form-data
-            HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
-            conn.setRequestMethod("POST");
-            conn.setConnectTimeout(10000);
-            conn.setReadTimeout(30000);
-
-            String boundary = "----WebKitFormBoundary" + UUID.randomUUID().toString().replace("-", "");
-            conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
-            conn.setDoOutput(true);
-
-            OutputStream os = conn.getOutputStream();
-            // media field
-            os.write(("--" + boundary + "\r\n").getBytes(StandardCharsets.UTF_8));
-            os.write("Content-Disposition: form-data; name=\"media\"; filename=\"check.jpg\"\r\n".getBytes(StandardCharsets.UTF_8));
-            os.write("Content-Type: image/jpeg\r\n\r\n".getBytes(StandardCharsets.UTF_8));
-            os.write(imageBytes);
-            os.write(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
-            os.flush();
-            os.close();
-
-            InputStream is = conn.getInputStream();
-            byte[] responseBytes = StreamUtils.copyToByteArray(is);
-            is.close();
-            String responseBody = new String(responseBytes, StandardCharsets.UTF_8);
-
-            JSONObject json = JSON.parseObject(responseBody);
+            JSONObject json = callImgSecCheck(getAccessToken(), imageBytes);
             int errcode = json.getInteger("errcode");
 
+            // access_token 过期/失效(42001/40001):清除缓存后用新 token 重试一次
+            if (errcode == 42001 || errcode == 40001) {
+                log.warn("图片内容安全检测 access_token 过期/失效(errcode={}),清除缓存重试", errcode);
+                invalidateAccessToken();
+                json = callImgSecCheck(getAccessToken(), imageBytes);
+                errcode = json.getInteger("errcode");
+            }
+
             if (errcode == 0) {
                 return true;
             } else if (errcode == 87014) {
@@ -412,6 +414,36 @@ public class WechatService implements WechatServiceInterface {
         }
     }
 
+    private JSONObject callImgSecCheck(String accessToken, byte[] imageBytes) throws IOException {
+        String url = "https://api.weixin.qq.com/wxa/img_sec_check?access_token=" + accessToken;
+
+        // 使用 HttpURLConnection 发送 multipart/form-data
+        HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
+        conn.setRequestMethod("POST");
+        conn.setConnectTimeout(10000);
+        conn.setReadTimeout(30000);
+
+        String boundary = "----WebKitFormBoundary" + UUID.randomUUID().toString().replace("-", "");
+        conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
+        conn.setDoOutput(true);
+
+        OutputStream os = conn.getOutputStream();
+        // media field
+        os.write(("--" + boundary + "\r\n").getBytes(StandardCharsets.UTF_8));
+        os.write("Content-Disposition: form-data; name=\"media\"; filename=\"check.jpg\"\r\n".getBytes(StandardCharsets.UTF_8));
+        os.write("Content-Type: image/jpeg\r\n\r\n".getBytes(StandardCharsets.UTF_8));
+        os.write(imageBytes);
+        os.write(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
+        os.flush();
+        os.close();
+
+        InputStream is = conn.getInputStream();
+        byte[] responseBytes = StreamUtils.copyToByteArray(is);
+        is.close();
+        String responseBody = new String(responseBytes, StandardCharsets.UTF_8);
+        return JSON.parseObject(responseBody);
+    }
+
     /**
      * 生成小程序二维码(wxacode)
      * @param scene 场景参数(最大32个可见字符)

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-d1d87b22e34d52018f55ce4b84b0f5e0b057504e
+4a851d76a3342a0ffb33c19bcaa409938304509b

+ 6 - 19
cfc-web/public/CHANGELOG-v1.0.md

@@ -6,23 +6,7 @@
 
 ## v1.0.829 (2026-08-06)
 
-### Bug 修复
-- 打卡请求失败时补充错误提示
-- 家庭挑战打卡每日限次 + memberId 归属校验
-
-### 新功能
-- 家庭挑战管理页仅家长可访问
-- 家庭挑战孩子端打卡入口 + cancelled 状态显示修复
-- 家庭挑战到期惰性结算 + 角色校验(家长管理/孩子打卡)
-- 家庭挑战结算发放能量点奖励并标记成员完成
-- 商品详情页按会员身份显示会员价/原价,支付价按身份计算
-- 供应商管理(CRUD + 侧边栏 + 入库单选择供应商)
-
 ### 文档
-- 修复 momus 二轮评审问题(路由命令/打卡状态校验/奖励幂等条件更新)
-- 家庭挑战修复计划按 momus 评审修正(完整方法体/登录角色门控/全员奖励发放/精确paths)
-- 家庭挑战断点修复实现计划 + PROJECT-OVERVIEW 索引更新
-- 家庭挑战 spec 自审修正 - 澄清家长修改进度表述
 - 家庭挑战使用流程梳理 v1.0 - 现状基线 + 断点清单
 
 
@@ -37,18 +21,16 @@
 
 ### Bug 修复
 - 商品海报分享未携带封面图(coverImage 未转完整 URL)
-- 移除无家庭时的创建或加入家庭弹窗(5001 仅拒绝请求)
 
 ### 其他
 - Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
 - 
-- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
-- 
 
 
 ## v1.0.826 (2026-08-06)
 
 ### Bug 修复
+- 移除无家庭时的创建或加入家庭弹窗(5001 仅拒绝请求)
 - 分享海报封面图按原比例绘制(contain 居中),不再拉伸变形
 - 报名弹窗点击家庭成员/添加报名人即关闭(.self 修饰符在 mp-weixin 被丢弃,改用 @click.stop)
 - 活动报名弹窗重复导致点击家庭成员关闭弹窗
@@ -71,6 +53,11 @@
 - 
 - Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
 - 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+### 新功能
+- 供应商管理(CRUD + 侧边栏 + 入库单选择供应商)
 
 ### 文档
 - 新增滔达福项目总体介绍PPT成品(V1/V2)

+ 6 - 19
cfc-web/public/CHANGELOG.md

@@ -10,23 +10,7 @@
 
 ## v1.0.829 (2026-08-06)
 
-### Bug 修复
-- 打卡请求失败时补充错误提示
-- 家庭挑战打卡每日限次 + memberId 归属校验
-
-### 新功能
-- 家庭挑战管理页仅家长可访问
-- 家庭挑战孩子端打卡入口 + cancelled 状态显示修复
-- 家庭挑战到期惰性结算 + 角色校验(家长管理/孩子打卡)
-- 家庭挑战结算发放能量点奖励并标记成员完成
-- 商品详情页按会员身份显示会员价/原价,支付价按身份计算
-- 供应商管理(CRUD + 侧边栏 + 入库单选择供应商)
-
 ### 文档
-- 修复 momus 二轮评审问题(路由命令/打卡状态校验/奖励幂等条件更新)
-- 家庭挑战修复计划按 momus 评审修正(完整方法体/登录角色门控/全员奖励发放/精确paths)
-- 家庭挑战断点修复实现计划 + PROJECT-OVERVIEW 索引更新
-- 家庭挑战 spec 自审修正 - 澄清家长修改进度表述
 - 家庭挑战使用流程梳理 v1.0 - 现状基线 + 断点清单
 
 
@@ -41,18 +25,16 @@
 
 ### Bug 修复
 - 商品海报分享未携带封面图(coverImage 未转完整 URL)
-- 移除无家庭时的创建或加入家庭弹窗(5001 仅拒绝请求)
 
 ### 其他
 - Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
 - 
-- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
-- 
 
 
 ## v1.0.826 (2026-08-06)
 
 ### Bug 修复
+- 移除无家庭时的创建或加入家庭弹窗(5001 仅拒绝请求)
 - 分享海报封面图按原比例绘制(contain 居中),不再拉伸变形
 - 报名弹窗点击家庭成员/添加报名人即关闭(.self 修饰符在 mp-weixin 被丢弃,改用 @click.stop)
 - 活动报名弹窗重复导致点击家庭成员关闭弹窗
@@ -75,6 +57,11 @@
 - 
 - Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
 - 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+### 新功能
+- 供应商管理(CRUD + 侧边栏 + 入库单选择供应商)
 
 ### 文档
 - 新增滔达福项目总体介绍PPT成品(V1/V2)