Bläddra i källkod

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

asus 1 månad sedan
förälder
incheckning
1b95f0a068
100 ändrade filer med 1613 tillägg och 98 borttagningar
  1. 7 0
      cfc-backend/pom.xml
  2. 209 0
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  3. 35 0
      cfc-backend/src/main/java/com/etotem/cfc/config/SortInterceptor.java
  4. 8 2
      cfc-backend/src/main/java/com/etotem/cfc/config/WebConfig.java
  5. 19 0
      cfc-backend/src/main/java/com/etotem/cfc/config/WebSortConfig.java
  6. 71 7
      cfc-backend/src/main/java/com/etotem/cfc/controller/EnergyController.java
  7. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthExerciseController.java
  8. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthMealController.java
  9. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthMeditationController.java
  10. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthSleepController.java
  11. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthWaterController.java
  12. 0 3
      cfc-backend/src/main/java/com/etotem/cfc/controller/SurveyController.java
  13. 42 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/UnlockController.java
  14. 169 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/UserController.java
  15. 4 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java
  16. 15 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java
  17. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCouponController.java
  18. 21 11
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminFoodController.java
  19. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminNoticeController.java
  20. 0 49
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminSeasonalController.java
  21. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminSubscriptionController.java
  22. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminSupplierController.java
  23. 114 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminUnlockGateController.java
  24. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminVendorController.java
  25. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/DimensionAdminController.java
  26. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/ReportParserAdminController.java
  27. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/SupplyManageController.java
  28. 8 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentReportController.java
  29. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentResultController.java
  30. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/butler/ButlerController.java
  31. 4 3
      cfc-backend/src/main/java/com/etotem/cfc/controller/diet/DietMealConfigController.java
  32. 15 6
      cfc-backend/src/main/java/com/etotem/cfc/controller/diet/DietRecordController.java
  33. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyInviteController.java
  34. 14 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyMembersController.java
  35. 17 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.java
  36. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideOrderController.java
  37. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideTeamController.java
  38. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/PlanReviewController.java
  39. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/notice/NoticeController.java
  40. 81 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/nutrition/NutritionProductController.java
  41. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/stats/StatsController.java
  42. 28 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/NutritionRecommendDTO.java
  43. 23 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/UnlockGateDTO.java
  44. 31 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/UnlockStatusDTO.java
  45. 37 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyGateProgress.java
  46. 23 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/FileRecord.java
  47. 27 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/NutritionBrand.java
  48. 25 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/NutritionCategory.java
  49. 36 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/NutritionIngredient.java
  50. 42 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/NutritionPlatformListing.java
  51. 46 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/NutritionProduct.java
  52. 30 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/NutritionProductReview.java
  53. 60 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/UnlockGate.java
  54. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyGateProgressMapper.java
  55. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/FileRecordMapper.java
  56. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/NutritionBrandMapper.java
  57. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/NutritionCategoryMapper.java
  58. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/NutritionIngredientMapper.java
  59. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/NutritionPlatformListingMapper.java
  60. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/NutritionProductMapper.java
  61. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/NutritionProductReviewMapper.java
  62. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/UnlockGateMapper.java
  63. 6 0
      cfc-backend/src/main/java/com/etotem/cfc/service/AIService.java
  64. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ActivityAdminService.java
  65. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ActivityFeedbackService.java
  66. 4 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ActivityRegistrationService.java
  67. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ActivityService.java
  68. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/service/AfterSalesService.java
  69. 4 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ArticleCommentService.java
  70. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java
  71. 4 0
      cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentAppointmentService.java
  72. 12 0
      cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentOrderService.java
  73. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentPermissionService.java
  74. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentQuotaService.java
  75. 12 0
      cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentService.java
  76. 6 0
      cfc-backend/src/main/java/com/etotem/cfc/service/BeijingNutritionService.java
  77. 4 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ButlerService.java
  78. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ButlerSessionService.java
  79. 4 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ChatMirrorService.java
  80. 4 0
      cfc-backend/src/main/java/com/etotem/cfc/service/CircleChallengeService.java
  81. 12 0
      cfc-backend/src/main/java/com/etotem/cfc/service/CircleMatchService.java
  82. 6 0
      cfc-backend/src/main/java/com/etotem/cfc/service/CircleService.java
  83. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/service/CognitiveService.java
  84. 5 0
      cfc-backend/src/main/java/com/etotem/cfc/service/CommissionDistService.java
  85. 4 0
      cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java
  86. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/service/CompatibilityService.java
  87. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ContactService.java
  88. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ContentRecommendService.java
  89. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/service/DanKnowledgeBaseService.java
  90. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/service/DanReportUploadService.java
  91. 45 16
      cfc-backend/src/main/java/com/etotem/cfc/service/DietRecordService.java
  92. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/service/DiscountService.java
  93. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/service/DistributionService.java
  94. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/service/EcomSupplierService.java
  95. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/service/EmotionAlertService.java
  96. 6 0
      cfc-backend/src/main/java/com/etotem/cfc/service/EmotionCheckinService.java
  97. 45 0
      cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java
  98. 8 0
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyChallengeService.java
  99. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyCommissionService.java
  100. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyEarningsService.java

+ 7 - 0
cfc-backend/pom.xml

@@ -141,6 +141,13 @@
             <version>1.8.0</version>
         </dependency>
 
+        <!-- AWS S3 SDK (JD Cloud OSS S3兼容) -->
+        <dependency>
+            <groupId>com.amazonaws</groupId>
+            <artifactId>aws-java-sdk-s3</artifactId>
+            <version>1.11.136</version>
+        </dependency>
+
         <!-- Test -->
         <dependency>
             <groupId>org.springframework.boot</groupId>

+ 209 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -8611,5 +8611,214 @@ private void runMigration100() {
 		} catch (Exception e) {
 			log.warn("迁移213: 微行动维度映射更新失败: " + e.getMessage());
 		}
+
+		// 迁移214: 创建 nutrition_category 表(营养产品分类)
+		try {
+			jdbcTemplate.execute(
+				"CREATE TABLE IF NOT EXISTS nutrition_category (" +
+				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"name VARCHAR(50) NOT NULL COMMENT '分类名', " +
+				"parent_id BIGINT DEFAULT NULL COMMENT '父分类ID', " +
+				"sort_order INT DEFAULT 0 COMMENT '排序', " +
+				"create_time DATETIME DEFAULT CURRENT_TIMESTAMP" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营养产品分类'"
+			);
+			log.info("已创建nutrition_category表");
+		} catch (Exception e) {
+			log.warn("创建nutrition_category表失败(可能已存在): " + e.getMessage());
+		}
+
+		// 迁移215: 创建 nutrition_brand 表(品牌)
+		try {
+			jdbcTemplate.execute(
+				"CREATE TABLE IF NOT EXISTS nutrition_brand (" +
+				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"name VARCHAR(100) NOT NULL COMMENT '品牌名', " +
+				"logo_url VARCHAR(500) DEFAULT NULL COMMENT '品牌Logo', " +
+				"description TEXT COMMENT '品牌简介', " +
+				"country VARCHAR(50) DEFAULT NULL COMMENT '产地国', " +
+				"create_time DATETIME DEFAULT CURRENT_TIMESTAMP" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营养产品品牌'"
+			);
+			log.info("已创建nutrition_brand表");
+		} catch (Exception e) {
+			log.warn("创建nutrition_brand表失败(可能已存在): " + e.getMessage());
+		}
+
+		// 迁移216: 创建 nutrition_product 表(营养产品主表)
+		try {
+			jdbcTemplate.execute(
+				"CREATE TABLE IF NOT EXISTS nutrition_product (" +
+				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"name VARCHAR(200) NOT NULL COMMENT '产品名称', " +
+				"brand_id BIGINT DEFAULT NULL COMMENT '品牌ID', " +
+				"category_id BIGINT DEFAULT NULL COMMENT '分类ID', " +
+				"sub_category VARCHAR(50) DEFAULT NULL COMMENT '子分类', " +
+				"form VARCHAR(30) DEFAULT NULL COMMENT '剂型', " +
+				"net_content VARCHAR(50) DEFAULT NULL COMMENT '净含量', " +
+				"shelf_life VARCHAR(50) DEFAULT NULL COMMENT '保质期', " +
+				"suitable_for VARCHAR(200) DEFAULT NULL COMMENT '适用人群', " +
+				"image_url VARCHAR(500) DEFAULT NULL COMMENT '产品图', " +
+				"description TEXT COMMENT '产品描述', " +
+				"status TINYINT DEFAULT 1 COMMENT '状态 0下架/1上架', " +
+				"create_time DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+				"update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+				"INDEX idx_brand (brand_id), " +
+				"INDEX idx_category (category_id)" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营养产品'"
+			);
+			log.info("已创建nutrition_product表");
+		} catch (Exception e) {
+			log.warn("创建nutrition_product表失败(可能已存在): " + e.getMessage());
+		}
+
+		// 迁移217: 创建 nutrition_ingredient 表(成分明细)
+		try {
+			jdbcTemplate.execute(
+				"CREATE TABLE IF NOT EXISTS nutrition_ingredient (" +
+				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"product_id BIGINT NOT NULL COMMENT '产品ID', " +
+				"ingredient_type VARCHAR(20) COMMENT '成分类型', " +
+				"name VARCHAR(100) NOT NULL COMMENT '成分名', " +
+				"strain VARCHAR(100) DEFAULT NULL COMMENT '菌株号', " +
+				"amount_per_serving DECIMAL(10,2) DEFAULT NULL COMMENT '每份含量', " +
+				"unit VARCHAR(20) DEFAULT NULL COMMENT '单位', " +
+				"daily_value VARCHAR(50) DEFAULT NULL COMMENT '每日参考值%', " +
+				"purpose VARCHAR(200) DEFAULT NULL COMMENT '功效说明', " +
+				"create_time DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+				"INDEX idx_product (product_id)" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营养产品成分明细'"
+			);
+			log.info("已创建nutrition_ingredient表");
+		} catch (Exception e) {
+			log.warn("创建nutrition_ingredient表失败(可能已存在): " + e.getMessage());
+		}
+
+		// 迁移218: 创建 nutrition_platform_listing 表(平台在售信息)
+		try {
+			jdbcTemplate.execute(
+				"CREATE TABLE IF NOT EXISTS nutrition_platform_listing (" +
+				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"product_id BIGINT NOT NULL COMMENT '产品ID', " +
+				"platform VARCHAR(30) COMMENT '平台', " +
+				"platform_url VARCHAR(500) DEFAULT NULL COMMENT '商品链接', " +
+				"current_price DECIMAL(10,2) DEFAULT NULL COMMENT '当前价格', " +
+				"original_price DECIMAL(10,2) DEFAULT NULL COMMENT '原价/划线价', " +
+				"price_unit VARCHAR(30) DEFAULT NULL COMMENT '价格单位', " +
+				"price_per_unit DECIMAL(10,2) DEFAULT NULL COMMENT '每单位价格', " +
+				"sales_count INT DEFAULT NULL COMMENT '销量', " +
+				"stock_status VARCHAR(20) DEFAULT NULL COMMENT '库存状态', " +
+				"last_checked DATETIME DEFAULT NULL COMMENT '最后爬取时间', " +
+				"create_time DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+				"update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+				"INDEX idx_product (product_id), " +
+				"INDEX idx_platform (platform)" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营养产品平台在售信息'"
+			);
+			log.info("已创建nutrition_platform_listing表");
+		} catch (Exception e) {
+			log.warn("创建nutrition_platform_listing表失败(可能已存在): " + e.getMessage());
+		}
+
+		// 迁移219: 创建 nutrition_product_review 表(评价)
+		try {
+			jdbcTemplate.execute(
+				"CREATE TABLE IF NOT EXISTS nutrition_product_review (" +
+				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"platform_listing_id BIGINT NOT NULL COMMENT '平台在售ID', " +
+				"source VARCHAR(30) DEFAULT NULL COMMENT '来源平台', " +
+				"rating DECIMAL(2,1) DEFAULT NULL COMMENT '评分', " +
+				"review_count INT DEFAULT NULL COMMENT '评价数', " +
+				"good_rate VARCHAR(10) DEFAULT NULL COMMENT '好评率', " +
+				"last_updated DATETIME DEFAULT NULL COMMENT '最后更新时间', " +
+				"INDEX idx_listing (platform_listing_id)" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营养产品评价'"
+			);
+			log.info("已创建nutrition_product_review表");
+		} catch (Exception e) {
+			log.warn("创建nutrition_product_review表失败(可能已存在): " + e.getMessage());
+		}
+
+		// 迁移220: 创建 file_record 表(文件去重+维护)
+		try {
+			jdbcTemplate.execute(
+				"CREATE TABLE IF NOT EXISTS file_record (" +
+				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"hash VARCHAR(64) NOT NULL COMMENT 'SHA-256 文件哈希', " +
+				"url VARCHAR(500) NOT NULL COMMENT '文件访问URL', " +
+				"file_size BIGINT NOT NULL COMMENT '文件大小(字节)', " +
+				"content_type VARCHAR(100) DEFAULT NULL COMMENT 'MIME类型', " +
+				"original_filename VARCHAR(255) DEFAULT NULL COMMENT '原始文件名', " +
+				"category VARCHAR(50) DEFAULT 'general' COMMENT '分类', " +
+				"description VARCHAR(500) DEFAULT NULL COMMENT '描述', " +
+				"created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
+				"updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+				"UNIQUE KEY uk_hash (hash), " +
+				"INDEX idx_category (category), " +
+				"INDEX idx_created_at (created_at)" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件记录表(去重&维护)'"
+			);
+			log.info("已创建file_record表");
+		} catch (Exception e) {
+			log.warn("创建file_record表失败(可能已存在): " + e.getMessage());
+		}
+
+		// 迁移221: nutrition_product 添加 local_product_id 字段(关联本平台商品)
+		ensureColumn("nutrition_product", "local_product_id", "BIGINT DEFAULT NULL COMMENT '本平台商品ID,关联products.id'");
+
+		// 迁移222: 创建 unlock_gates 表(通关关卡定义)
+		try {
+			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS unlock_gates (" +
+				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"gate_type VARCHAR(32) NOT NULL COMMENT 'SELF_CHECK/INVITE_FAMILY/CREATE_FAMILY_MEMBER/PURCHASE/MEMBERSHIP/REPORT_UPLOAD/MICRO_ACTION/STREAK_DAYS', " +
+				"name VARCHAR(50) NOT NULL COMMENT '关卡名称', " +
+				"description VARCHAR(200) COMMENT '关卡说明', " +
+				"icon VARCHAR(10) COMMENT '图标emoji', " +
+				"sort_order INT DEFAULT 0 COMMENT '排序号', " +
+				"enabled TINYINT DEFAULT 1 COMMENT '启用:1 禁用:0', " +
+				"is_required TINYINT DEFAULT 1 COMMENT '主线:1 增值:0', " +
+				"is_soft TINYINT DEFAULT 0 COMMENT '硬门禁:0 软引导:1', " +
+				"unlock_target VARCHAR(32) COMMENT 'PERSONAL_SANDBOX/FAMILY_SANDBOX/CHILD_MICRO_ACTION/...', " +
+				"condition_params VARCHAR(500) COMMENT '条件参数JSON', " +
+				"reward_points INT DEFAULT 0 COMMENT '通关奖励积分', " +
+				"reward_energy INT DEFAULT 0 COMMENT '通关奖励能量', " +
+				"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+				"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+				"INDEX idx_sort (enabled, is_required, is_soft, sort_order)" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通关解锁关卡定义'");
+			log.info("已创建unlock_gates表");
+		} catch (Exception e) {
+			log.warn("创建unlock_gates表失败(可能已存在): " + e.getMessage());
+		}
+
+		// 迁移223: 创建 family_gate_progress 表(家庭通关进度)
+		try {
+			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_gate_progress (" +
+				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"family_id BIGINT NOT NULL COMMENT '家庭ID', " +
+				"gate_id BIGINT NOT NULL COMMENT '关卡ID', " +
+				"status VARCHAR(16) DEFAULT 'LOCKED' COMMENT 'LOCKED/UNLOCKED', " +
+				"unlocked_by BIGINT COMMENT '解锁人user_id', " +
+				"unlocked_at DATETIME COMMENT '解锁时间', " +
+				"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+				"UNIQUE KEY uk_family_gate (family_id, gate_id)" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭关卡通关进度'");
+			log.info("已创建family_gate_progress表");
+
+			// 默认关卡种子数据(幂等:INSERT IGNORE)
+			jdbcTemplate.execute("INSERT IGNORE INTO unlock_gates (gate_type, name, description, icon, sort_order, enabled, is_required, is_soft, unlock_target, condition_params, reward_points, reward_energy) VALUES " +
+				"('SELF_CHECK', '五维自检', '完成15题五维自检,看见自己的能量状态', '📝', 1, 1, 1, 0, 'PERSONAL_SANDBOX', '{\"minCount\":1}', 60, 10), " +
+				"('INVITE_FAMILY', '邀请家人', '邀请家人加入,共同创建家庭数据', '👨‍👩‍👧‍👦', 2, 1, 1, 0, 'FAMILY_SANDBOX', '{}', 80, 15), " +
+				"('CREATE_FAMILY_MEMBER', '创建家人', '为孩子创建账号,开启家庭互动', '👶', 3, 1, 1, 0, 'CHILD_MICRO_ACTION', '{}', 50, 10), " +
+				"('MICRO_ACTION', '首次微行动', '完成一次微行动,养成每日小习惯', '🎯', 4, 1, 1, 1, 'MICRO_ACTION_DAILY', '{\"count\":1}', 50, 5), " +
+				"('STREAK_DAYS', '连续打卡3天', '连续打卡3天,获得里程碑奖励', '🔥', 5, 1, 1, 1, 'BADGE_MILESTONE', '{\"days\":3}', 30, 5), " +
+				"('SELF_CHECK', '复测对比', '完成第二次自检,看见前后变化', '📊', 6, 1, 1, 1, 'TREND_COMPARE', '{\"minCount\":2}', 60, 10), " +
+				"('PURCHASE', '首次购物', '在商城完成首次购物,解锁深度分析', '🛒', 7, 0, 0, 0, 'PREMIUM_MODULES', '{}', 150, 30), " +
+				"('MEMBERSHIP', '加入会员', '加入家庭会员,享受专属权益', '👑', 8, 0, 0, 0, 'PREMIUM_MODULES', '{}', 200, 50), " +
+				"('REPORT_UPLOAD', '健康报告上传', '上传健康报告,获取AI解读与洞察', '📋', 9, 0, 0, 0, 'HEALTH_INSIGHTS', '{\"minCount\":1}', 100, 20)");
+			log.info("已插入默认关卡种子数据");
+		} catch (Exception e) {
+			log.warn("创建family_gate_progress表或插入种子数据失败: " + e.getMessage());
+		}
 	}
 }

+ 35 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/SortInterceptor.java

@@ -0,0 +1,35 @@
+package com.etotem.cfc.config;
+
+import com.etotem.cfc.util.SortContext;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+import java.util.Map;
+
+@Component
+public class SortInterceptor implements HandlerInterceptor {
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        String sortJson = request.getParameter("sort");
+        if (sortJson != null && !sortJson.isEmpty()) {
+            try {
+                @SuppressWarnings("unchecked")
+                List<Map<String, String>> sortSpecs = new com.fasterxml.jackson.databind.ObjectMapper()
+                        .readValue(sortJson, List.class);
+                SortContext.set(sortSpecs);
+            } catch (Exception e) {
+                // ignore invalid sort param
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
+        SortContext.clear();
+    }
+}

+ 8 - 2
cfc-backend/src/main/java/com/etotem/cfc/config/WebConfig.java

@@ -38,6 +38,9 @@ public class WebConfig implements WebMvcConfigurer {
     @Value("${upload.base-dir:/data/cfc-uploads}")
     private String uploadBaseDir;
 
+    @Value("${storage.type:local}")
+    private String storageType;
+
     @Override
     public void addCorsMappings(CorsRegistry registry) {
         registry.addMapping("/**")
@@ -158,7 +161,10 @@ public class WebConfig implements WebMvcConfigurer {
 
     @Override
     public void addResourceHandlers(ResourceHandlerRegistry registry) {
-        registry.addResourceHandler("/uploads/**")
-                .addResourceLocations("file:" + uploadBaseDir + "/");
+        // 仅在本地存储模式下注册静态资源映射;jdcloud 模式文件走京东云公网URL
+        if ("local".equals(storageType)) {
+            registry.addResourceHandler("/uploads/**")
+                    .addResourceLocations("file:" + uploadBaseDir + "/");
+        }
     }
 }

+ 19 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/WebSortConfig.java

@@ -0,0 +1,19 @@
+package com.etotem.cfc.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebSortConfig implements WebMvcConfigurer {
+    @Autowired
+    private SortInterceptor sortInterceptor;
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(sortInterceptor)
+                .addPathPatterns("/api/admin/**")
+                .addPathPatterns("/api/health/**");
+    }
+}

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

@@ -4,11 +4,14 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.EnergySandboxDTO;
 import com.etotem.cfc.dto.MemberEnergyDTO;
+import com.etotem.cfc.dto.UnlockGateDTO;
 import com.etotem.cfc.entity.EnergyLog;
+import com.etotem.cfc.entity.UnlockGate;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.EnergyService;
 import com.etotem.cfc.service.FiveDimensionScoreService;
+import com.etotem.cfc.service.UnlockGateService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.*;
@@ -30,14 +33,19 @@ public class EnergyController {
     @Resource
     private UserMapper userMapper;
 
+    @Resource
+    private UnlockGateService unlockGateService;
+
     /**
-     * 获取家庭能量沙盘数据
-     * 返回家庭五维聚合值 + 每个家庭成员的个人五维数据
+     * 获取能量沙盘数据(个人/全家两层,受通关门禁控制)
+     * view=personal: 当前登录用户个人五维(需通过 SELF_CHECK 关卡)
+     * view=family:   家庭所有成员聚合五维(需通过 INVITE_FAMILY 关卡)
      */
-    @Operation(summary = "获取家庭能量沙盘")
+    @Operation(summary = "获取能量沙盘(支持个人/全家,含门禁)")
     @PostMapping("/sandbox")
-    public Result<EnergySandboxDTO> getFamilyEnergySandbox(
-            @RequestAttribute("userId") Long userId) {
+    public Result<Map<String, Object>> getFamilyEnergySandbox(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody(required = false) Map<String, Object> body) {
 
         User user = userMapper.selectById(userId);
         if (user == null) {
@@ -47,8 +55,64 @@ public class EnergyController {
             return Result.noFamily("请先创建或加入家庭");
         }
 
-        EnergySandboxDTO dto = energyService.calculateFamilyEnergy(user.getFamilyId());
-        return Result.success(dto);
+        String view = "personal";
+        if (body != null && body.get("view") != null) {
+            view = String.valueOf(body.get("view"));
+        }
+
+        Map<String, Object> result = new HashMap<>();
+
+        if ("family".equals(view)) {
+            // 全家沙盘:需通过 INVITE_FAMILY 关卡
+            UnlockGate inviteGate = unlockGateService.getGateByType("INVITE_FAMILY");
+            boolean unlocked = inviteGate == null
+                    || unlockGateService.isGateUnlocked(user.getFamilyId(), inviteGate);
+            if (!unlocked && inviteGate != null) {
+                result.put("locked", true);
+                result.put("view", "family");
+                result.put("gate", gateToMap(inviteGate));
+                return Result.success(result);
+            }
+            EnergySandboxDTO dto = energyService.calculateFamilyEnergy(user.getFamilyId());
+            result.put("locked", false);
+            result.put("view", "family");
+            result.put("familyData", dto);
+            return Result.success(result);
+        }
+
+        // 个人沙盘(默认):需通过 SELF_CHECK 关卡
+        boolean personalPassed = unlockGateService.isPersonalSelfCheckPassed(userId);
+        if (!personalPassed) {
+            UnlockGate gate = unlockGateService.getGateByType("SELF_CHECK");
+            result.put("locked", true);
+            result.put("view", "personal");
+            result.put("gate", gate != null ? gateToMap(gate) : null);
+            return Result.success(result);
+        }
+
+        EnergySandboxDTO dto = energyService.calculatePersonalEnergy(userId);
+        UnlockGate inviteGate = unlockGateService.getGateByType("INVITE_FAMILY");
+        boolean familyUnlocked = inviteGate == null
+                || unlockGateService.isGateUnlocked(user.getFamilyId(), inviteGate);
+        result.put("locked", false);
+        result.put("view", "personal");
+        result.put("personalData", dto);
+        result.put("familyUnlocked", familyUnlocked);
+        if (!familyUnlocked && inviteGate != null) {
+            result.put("familyGate", gateToMap(inviteGate));
+        }
+        return Result.success(result);
+    }
+
+    private Map<String, Object> gateToMap(UnlockGate gate) {
+        if (gate == null) return null;
+        Map<String, Object> m = new HashMap<>();
+        m.put("id", gate.getId());
+        m.put("gateType", gate.getGateType());
+        m.put("name", gate.getName());
+        m.put("description", gate.getDescription());
+        m.put("icon", gate.getIcon());
+        return m;
     }
 
     /**

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

@@ -12,6 +12,7 @@ import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @Tag(name = "运动打卡")
 @RestController

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

@@ -12,6 +12,7 @@ import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @Tag(name = "饮食打卡")
 @RestController

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

@@ -12,6 +12,7 @@ import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @Tag(name = "冥想打卡")
 @RestController

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

@@ -12,6 +12,7 @@ import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @Tag(name = "睡眠打卡")
 @RestController

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

@@ -12,6 +12,7 @@ import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @Tag(name = "喝水打卡")
 @RestController

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

@@ -29,7 +29,6 @@ public class SurveyController {
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
         Long memberId = params.get("memberId") != null
                 ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
-        if (memberId == null) return Result.error("缺少成员ID");
         return Result.success(surveyService.getSurveyStatus(userId, memberId));
     }
 
@@ -47,7 +46,6 @@ public class SurveyController {
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
         Long memberId = params.get("memberId") != null
                 ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
-        if (memberId == null) return Result.error("缺少成员ID");
         Long templateId = Long.valueOf(params.get("templateId").toString());
         String answersJson = (String) params.get("answersJson");
         Date ps = params.get("periodStart") != null ? new Date(Long.valueOf(params.get("periodStart").toString())) : null;
@@ -63,7 +61,6 @@ public class SurveyController {
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
         Long memberId = params.get("memberId") != null
                 ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
-        if (memberId == null) return Result.error("缺少成员ID");
         return Result.success(surveyService.getSurveyHistory(userId, memberId));
     }
 }

+ 42 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/UnlockController.java

@@ -0,0 +1,42 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.UnlockStatusDTO;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.UnlockGateService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+@Tag(name = "通关解锁", description = "关卡状态查询")
+@RestController
+@RequestMapping("/api/unlock")
+public class UnlockController {
+
+    @Resource
+    private UnlockGateService unlockGateService;
+
+    @Resource
+    private UserMapper userMapper;
+
+    /**
+     * 查询用户关卡全景
+     */
+    @Operation(summary = "查询用户关卡全景")
+    @PostMapping("/status")
+    public Result<UnlockStatusDTO> getStatus(@RequestAttribute("userId") Long userId) {
+        if (userId == null) {
+            return Result.error("请先登录");
+        }
+        User user = userMapper.selectById(userId);
+        Long familyId = user != null ? user.getFamilyId() : null;
+        UnlockStatusDTO dto = unlockGateService.getUserUnlockStatus(userId, familyId);
+        return Result.success(dto);
+    }
+}

+ 169 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/UserController.java

@@ -11,9 +11,14 @@ import com.etotem.cfc.entity.PaymentOrder;
 import com.etotem.cfc.entity.Task;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.AssessmentOrderMapper;
+import com.etotem.cfc.mapper.ChildBadgeMapper;
+import com.etotem.cfc.mapper.FamilyChallengeProgressMapper;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.FamilyMapper;
+import com.etotem.cfc.mapper.HealthReportMapper;
+import com.etotem.cfc.mapper.GrowthRecordMapper;
 import com.etotem.cfc.mapper.PaymentOrderMapper;
+import com.etotem.cfc.mapper.PointsExchangeRecordMapper;
 import com.etotem.cfc.mapper.TaskMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.UserService;
@@ -169,6 +174,170 @@ public class UserController {
         return Result.success(result);
     }
 
+    @Resource
+    private com.etotem.cfc.mapper.GrowthRecordMapper growthRecordMapper;
+
+    @Resource
+    private com.etotem.cfc.mapper.HealthReportMapper healthReportMapper;
+
+    @Resource
+    private com.etotem.cfc.mapper.PointsExchangeRecordMapper pointsExchangeRecordMapper;
+
+    @Resource
+    private com.etotem.cfc.mapper.ChildBadgeMapper childBadgeMapper;
+
+    @Resource
+    private com.etotem.cfc.mapper.FamilyChallengeProgressMapper familyChallengeProgressMapper;
+
+    @PostMapping("/profile-stats")
+    @Operation(summary = "获取个人成就统计")
+    public Result<Map<String, Object>> getProfileStats(HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        if (userId == null) {
+            return Result.error("用户未登录");
+        }
+
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+
+        Long familyId = user.getFamilyId();
+        Map<String, Object> stats = new HashMap<>();
+
+        // 1. 总积分
+        Integer totalPoints = 0;
+        try {
+            Object points = userService.getPointsBalance(userId);
+            if (points != null) {
+                if (points instanceof Map) {
+                    Object v = ((Map<?, ?>) points).get("totalPoints");
+                    if (v != null) totalPoints = ((Number) v).intValue();
+                } else if (points instanceof Number) {
+                    totalPoints = ((Number) points).intValue();
+                }
+            }
+        } catch (Exception e) {
+            log.warn("获取积分失败: userId={}", userId, e);
+        }
+        stats.put("totalPoints", totalPoints);
+
+        // 2. 连续打卡天数(取所有孩子中最高值)
+        Integer maxStreakDays = 0;
+        if (familyId != null) {
+            List<FamilyMember> members = familyMemberMapper.selectList(
+                new LambdaQueryWrapper<FamilyMember>()
+                    .eq(FamilyMember::getFamilyId, familyId)
+            );
+            for (FamilyMember m : members) {
+                if (m.getStreakDays() != null && m.getStreakDays() > maxStreakDays) {
+                    maxStreakDays = m.getStreakDays();
+                }
+            }
+        }
+        stats.put("streakDays", maxStreakDays);
+
+        // 3. 完成任务数(当前家庭)
+        Long completedTasks = 0L;
+        if (familyId != null) {
+            completedTasks = taskMapper.selectCount(
+                new LambdaQueryWrapper<Task>()
+                    .eq(Task::getFamilyId, familyId)
+                    .eq(Task::getStatus, "completed")
+            );
+        }
+        stats.put("completedTasks", completedTasks);
+
+        // 4. 心愿兑换次数(按家庭成员统计)
+        Long rewardsEarned = 0L;
+        if (familyId != null) {
+            List<FamilyMember> exchangeMembers = familyMemberMapper.selectList(
+                new LambdaQueryWrapper<FamilyMember>()
+                    .eq(FamilyMember::getFamilyId, familyId)
+            );
+            for (FamilyMember m : exchangeMembers) {
+                Long cnt = pointsExchangeRecordMapper.selectCount(
+                    new LambdaQueryWrapper<com.etotem.cfc.entity.PointsExchangeRecord>()
+                        .eq(com.etotem.cfc.entity.PointsExchangeRecord::getFamilyMemberId, m.getId())
+                );
+                rewardsEarned += cnt != null ? cnt.longValue() : 0L;
+            }
+        }
+        stats.put("rewardsEarned", rewardsEarned);
+
+        // 5. 收集勋章数(所有家庭成员)
+        Integer badgesCollected = 0;
+        if (familyId != null) {
+            List<FamilyMember> members = familyMemberMapper.selectList(
+                new LambdaQueryWrapper<FamilyMember>()
+                    .eq(FamilyMember::getFamilyId, familyId)
+            );
+            for (FamilyMember m : members) {
+                try {
+                    Long cnt = childBadgeMapper.selectCount(
+                        new LambdaQueryWrapper<com.etotem.cfc.entity.ChildBadge>()
+                            .eq(com.etotem.cfc.entity.ChildBadge::getChildId, m.getId())
+                    );
+                    badgesCollected += cnt != null ? cnt.intValue() : 0;
+                } catch (Exception e) {
+                    log.warn("获取勋章数失败: memberId={}", m.getId(), e);
+                }
+            }
+        }
+        stats.put("badgesCollected", badgesCollected);
+
+        // 6. 成长记录数(按当前用户为parentId统计)
+        Long growthRecords = growthRecordMapper.selectCount(
+            new LambdaQueryWrapper<com.etotem.cfc.entity.GrowthRecord>()
+                .eq(com.etotem.cfc.entity.GrowthRecord::getParentId, userId)
+        );
+        stats.put("growthRecords", growthRecords);
+
+        // 7. 参加挑战数(按家庭成员childId统计)
+        Integer challengesJoined = 0;
+        if (familyId != null) {
+            List<FamilyMember> challengeMembers = familyMemberMapper.selectList(
+                new LambdaQueryWrapper<FamilyMember>()
+                    .eq(FamilyMember::getFamilyId, familyId)
+            );
+            for (FamilyMember m : challengeMembers) {
+                Long cnt = familyChallengeProgressMapper.selectCount(
+                    new LambdaQueryWrapper<com.etotem.cfc.entity.FamilyChallengeProgress>()
+                        .eq(com.etotem.cfc.entity.FamilyChallengeProgress::getChildId, m.getId())
+                );
+                challengesJoined += cnt != null ? cnt.intValue() : 0;
+            }
+        }
+        stats.put("challengesJoined", challengesJoined);
+
+        // 8. 上传健康报告数
+        Long healthReports = 0L;
+        if (familyId != null) {
+            healthReports = healthReportMapper.selectCount(
+                new LambdaQueryWrapper<com.etotem.cfc.entity.HealthReport>()
+                    .eq(com.etotem.cfc.entity.HealthReport::getFamilyId, familyId)
+            );
+        }
+        stats.put("healthReports", healthReports);
+
+        // 9. 家庭成员数
+        Integer familyMemberCount = 0;
+        if (familyId != null) {
+            Long cnt = familyMemberMapper.selectCount(
+                new LambdaQueryWrapper<FamilyMember>()
+                    .eq(FamilyMember::getFamilyId, familyId)
+            );
+            familyMemberCount = cnt != null ? cnt.intValue() : 0;
+        }
+        stats.put("familyMemberCount", familyMemberCount);
+
+        // 10. 累计文章阅读数
+        Integer articlesRead = 0;
+        stats.put("articlesRead", articlesRead);
+
+        return Result.success(stats);
+    }
+
     @PostMapping("/action-stats")
     @Operation(summary = "获取用户行动统计")
     public Result<Map<String, Object>> getActionStats(HttpServletRequest request) {

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java

@@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.HashMap;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @RestController
 @RequestMapping("/api/admin/butler")
@@ -55,6 +56,7 @@ public class AdminButlerController {
         }
         wrapper.orderByDesc(ButlerProfile::getCreatedAt);
 
+        SortUtil.applySort(wrapper);
         Page<ButlerProfile> result = butlerProfileMapper.selectPage(pageParam, wrapper);
 
         Map<String, Object> data = new HashMap<>();
@@ -132,6 +134,7 @@ public class AdminButlerController {
         }
         wrapper.orderByDesc(ButlerServiceRecord::getCreatedAt);
 
+        SortUtil.applySort(wrapper);
         Page<ButlerServiceRecord> result = butlerServiceRecordMapper.selectPage(pageParam, wrapper);
 
         Map<String, Object> data = new HashMap<>();
@@ -161,6 +164,7 @@ public class AdminButlerController {
         }
         wrapper.orderByDesc(ButlerCommissionSettlement::getCreatedAt);
 
+        SortUtil.applySort(wrapper);
         Page<ButlerCommissionSettlement> result = butlerCommissionSettlementMapper.selectPage(pageParam, wrapper);
 
         Map<String, Object> data = new HashMap<>();

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java

@@ -23,6 +23,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.ArrayList;
 import java.util.Collections;
+import com.etotem.cfc.util.SortUtil;
 
 @RestController
 @RequestMapping("/api/admin")
@@ -96,6 +97,7 @@ public class AdminController {
         if (params.containsKey("phone") && params.get("phone") != null && !params.get("phone").toString().isEmpty()) {
             wrapper.like(User::getPhone, String.valueOf(params.get("phone")));
         }
+        SortUtil.applySort(wrapper);
         Page<User> result = userMapper.selectPage(pageParam, wrapper);
         return Result.success(result);
     }
@@ -182,6 +184,7 @@ public class AdminController {
             }
         }
         wrapper.orderByDesc(User::getUpdatedAt).last("LIMIT 50");
+        SortUtil.applySort(wrapper);
         List<User> users = userMapper.selectList(wrapper);
         // 清除密码
         for (User u : users) {
@@ -270,6 +273,7 @@ public class AdminController {
             wrapper.eq(Task::getFamilyId, familyId);
         }
         wrapper.orderByDesc(Task::getCreatedAt);
+        SortUtil.applySort(wrapper);
         Page<Task> result = taskMapper.selectPage(pageParam, wrapper);
         return Result.success(result);
     }
@@ -305,6 +309,7 @@ public class AdminController {
             wrapper.eq(Reward::getFamilyId, familyId);
         }
         wrapper.orderByDesc(Reward::getCreatedAt);
+        SortUtil.applySort(wrapper);
         Page<Reward> result = rewardMapper.selectPage(pageParam, wrapper);
         return Result.success(result);
     }
@@ -456,6 +461,7 @@ public class AdminController {
             wrapper.eq(PointsLog::getType, type);
         }
         wrapper.orderByDesc(PointsLog::getCreatedAt);
+        SortUtil.applySort(wrapper);
         Page<PointsLog> result = pointsLogMapper.selectPage(pageParam, wrapper);
         return Result.success(result);
     }
@@ -488,6 +494,7 @@ public class AdminController {
             wrapper.like(Family::getName, name);
         }
         wrapper.orderByDesc(Family::getCreatedAt);
+        SortUtil.applySort(wrapper);
         Page<Family> result = familyMapper.selectPage(pageParam, wrapper);
         return Result.success(result);
     }
@@ -515,11 +522,13 @@ public class AdminController {
         // 查询家长
         LambdaQueryWrapper<User> parentWrapper = new LambdaQueryWrapper<>();
         parentWrapper.eq(User::getFamilyId, id).eq(User::getRole, "parent");
+        SortUtil.applySort(parentWrapper);
         java.util.List<User> parents = userMapper.selectList(parentWrapper);
 
         // 查询孩子
         LambdaQueryWrapper<FamilyMember> childWrapper = new LambdaQueryWrapper<>();
         childWrapper.eq(FamilyMember::getFamilyId, id);
+        SortUtil.applySort(childWrapper);
         java.util.List<FamilyMember> children = familyMemberMapper.selectList(childWrapper);
 
         Map<String, Object> result = new HashMap<>();
@@ -640,6 +649,7 @@ public class AdminController {
             wrapper.eq(TaskTemplate::getIsActive, isActive);
         }
         wrapper.orderByDesc(TaskTemplate::getCreatedAt);
+        SortUtil.applySort(wrapper);
         Page<TaskTemplate> result = taskTemplateMapper.selectPage(pageParam, wrapper);
         return Result.success(result);
     }
@@ -692,6 +702,7 @@ public class AdminController {
             .isNotNull(User::getRealName)
             .orderByDesc(User::getUpdatedAt);
 
+        SortUtil.applySort(wrapper);
         Page<User> result = userMapper.selectPage(pageParam, wrapper);
         return Result.success(result);
     }
@@ -798,6 +809,7 @@ public class AdminController {
             .isNotNull(User::getRealName)
             .orderByDesc(User::getUpdatedAt);
 
+        SortUtil.applySort(wrapper);
         Page<User> result = userMapper.selectPage(pageParam, wrapper);
         return Result.success(result);
     }
@@ -1114,6 +1126,7 @@ public class AdminController {
         wrapper.orderByDesc(MemberUpgradeRecord::getCreatedAt);
 
         Page<MemberUpgradeRecord> pageParam = new Page<>(page, size);
+        SortUtil.applySort(wrapper);
         Page<MemberUpgradeRecord> result = memberUpgradeRecordMapper.selectPage(pageParam, wrapper);
         return Result.success(result);
     }
@@ -1327,6 +1340,7 @@ public class AdminController {
             String kw = params.get("keyword").toString();
             LambdaQueryWrapper<User> userWrapper = new LambdaQueryWrapper<>();
             userWrapper.like(User::getNickname, kw).select(User::getId);
+            SortUtil.applySort(userWrapper);
             java.util.List<User> matchedUsers = userMapper.selectList(userWrapper);
             java.util.List<Long> matchedIds = new java.util.ArrayList<>();
             for (User u : matchedUsers) { matchedIds.add(u.getId()); }
@@ -1338,6 +1352,7 @@ public class AdminController {
         }
         wrapper.orderByDesc(AssessmentAppointment::getCreatedAt);
 
+        SortUtil.applySort(wrapper);
         Page<AssessmentAppointment> result = assessmentAppointmentMapper.selectPage(pageParam, wrapper);
 
         java.util.List<Map<String, Object>> list = new java.util.ArrayList<>();

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

@@ -13,6 +13,7 @@ import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @RestController
 @RequestMapping("/api/admin/coupon")

+ 21 - 11
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminFoodController.java

@@ -1,5 +1,6 @@
 package com.etotem.cfc.controller.admin;
 
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.Food;
 import com.etotem.cfc.service.FoodService;
@@ -8,10 +9,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.stream.Collectors;
 
 @Tag(name = "\u7ba1\u7406\u7aef-\u98df\u6750\u7ba1\u7406")
@@ -24,12 +22,19 @@ public class AdminFoodController {
 
     @Operation(summary = "\u98df\u6750\u5217\u8868")
     @PostMapping("/list")
-    public Result<List<Map<String, Object>>> list(@RequestBody Map<String, Object> params) {
+    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params) {
+        long page = params.get("page") != null ? ((Number) params.get("page")).longValue() : 1;
+        long size = params.get("size") != null ? ((Number) params.get("size")).longValue() : 20;
         String category = (String) params.get("category");
-        List<Food> foods = foodService.listAll(category, "active");
-        List<Map<String, Object>> result = new ArrayList<>();
-        for (Food food : foods) {
-            Map<String, Object> item = new java.util.HashMap<>();
+        String keyword = (String) params.get("keyword");
+        @SuppressWarnings("unchecked")
+        List<Map<String, String>> sortSpecs = params.get("sort") != null
+                ? (List<Map<String, String>>) params.get("sort")
+                : null;
+        Page<Food> pageResult = foodService.listPage(page, size, category, keyword, "active", sortSpecs);
+        List<Map<String, Object>> records = new ArrayList<>();
+        for (Food food : pageResult.getRecords()) {
+            Map<String, Object> item = new HashMap<>();
             item.put("id", food.getId());
             item.put("name", food.getName());
             item.put("category", food.getCategory());
@@ -52,9 +57,14 @@ public class AdminFoodController {
             item.put("createdAt", food.getCreatedAt());
             item.put("updatedAt", food.getUpdatedAt());
             item.put("months", foodService.getMonths(food.getId()));
-            result.add(item);
+            records.add(item);
         }
-        return Result.success(result);
+        Map<String, Object> data = new HashMap<>();
+        data.put("records", records);
+        data.put("total", pageResult.getTotal());
+        data.put("page", pageResult.getCurrent());
+        data.put("size", pageResult.getSize());
+        return Result.success(data);
     }
 
     @Operation(summary = "\u65b0\u589e\u98df\u6750")

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminNoticeController.java

@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
+import com.etotem.cfc.util.SortUtil;
 
 @Tag(name = "公告管理", description = "平台公告的增删改查")
 @RestController
@@ -35,6 +36,7 @@ public class AdminNoticeController {
         q.orderByDesc(Notice::getCreatedAt);
         q.last("LIMIT " + ((page - 1) * size) + "," + size);
 
+        SortUtil.applySort(q);
         List<Notice> list = noticeMapper.selectList(q);
         return Result.success(list);
     }

+ 0 - 49
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminSeasonalController.java

@@ -1,49 +0,0 @@
-package com.etotem.cfc.controller.admin;
-
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.entity.SeasonalFood;
-import com.etotem.cfc.service.SeasonalFoodService;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import org.springframework.web.bind.annotation.*;
-
-import javax.annotation.Resource;
-import java.time.LocalDate;
-import java.util.List;
-import java.util.Map;
-
-@Tag(name = "\u7ba1\u7406\u7aef-\u5e94\u5b63\u98df\u6750\u914d\u7f6e")
-@RestController
-@RequestMapping("/api/admin/seasonal")
-public class AdminSeasonalController {
-
-    @Resource
-    private SeasonalFoodService seasonalFoodService;
-
-    @Operation(summary = "\u5e94\u5b63\u5217\u8868")
-    @PostMapping("/list")
-    public Result<List<SeasonalFood>> list(@RequestBody Map<String, Object> params) {
-        Integer month = params.get("month") != null
-                ? Integer.valueOf(params.get("month").toString()) : null;
-        if (month != null) {
-            return Result.success(seasonalFoodService.listByMonth(month));
-        }
-        return Result.success(seasonalFoodService.listByMonth(LocalDate.now().getMonthValue()));
-    }
-
-    @Operation(summary = "\u65b0\u589e\u5e94\u5b63\u6620\u5c04")
-    @PostMapping("/create")
-    public Result<String> create(@RequestBody SeasonalFood seasonalFood) {
-        seasonalFoodService.create(seasonalFood);
-        return Result.success("ok");
-    }
-
-    @Operation(summary = "\u5220\u9664\u5e94\u5b63\u6620\u5c04")
-    @PostMapping("/delete")
-    public Result<String> delete(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
-        if (id == null) return Result.error("id\u4e0d\u80fd\u4e3a\u7a7a");
-        seasonalFoodService.delete(id);
-        return Result.success("ok");
-    }
-}

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminSubscriptionController.java

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import java.util.*;
+import com.etotem.cfc.util.SortUtil;
 
 @RestController
 @RequestMapping("/api/admin/subscription")
@@ -39,6 +40,7 @@ public class AdminSubscriptionController {
         }
         wrapper.orderByDesc(MemberSubscription::getCreatedAt);
 
+        SortUtil.applySort(wrapper);
         List<MemberSubscription> subs = subscriptionMapper.selectList(wrapper);
         List<Map<String, Object>> list = new ArrayList<>();
         for (MemberSubscription sub : subs) {

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminSupplierController.java

@@ -13,6 +13,7 @@ import javax.annotation.Resource;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @Tag(name = "管理端-电商供应商管理")
 @RestController
@@ -35,6 +36,7 @@ public class AdminSupplierController {
             wrapper.like(EcomSupplier::getName, keyword);
         }
         wrapper.orderByDesc(EcomSupplier::getCreatedAt);
+        SortUtil.applySort(wrapper);
         Page<EcomSupplier> result = ecomSupplierMapper.selectPage(pageParam, wrapper);
 
         Map<String, Object> data = new HashMap<>();

+ 114 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminUnlockGateController.java

@@ -0,0 +1,114 @@
+package com.etotem.cfc.controller.admin;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.FamilyGateProgress;
+import com.etotem.cfc.entity.UnlockGate;
+import com.etotem.cfc.mapper.FamilyGateProgressMapper;
+import com.etotem.cfc.mapper.UnlockGateMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 管理端:通关解锁关卡配置
+ */
+@RestController
+@RequestMapping("/api/admin/unlock-gates")
+public class AdminUnlockGateController {
+
+    @Resource
+    private UnlockGateMapper unlockGateMapper;
+
+    @Resource
+    private FamilyGateProgressMapper familyGateProgressMapper;
+
+    @Operation(summary = "关卡列表")
+    @PostMapping("/list")
+    public Result<List<UnlockGate>> list() {
+        List<UnlockGate> gates = unlockGateMapper.selectList(
+                new LambdaQueryWrapper<UnlockGate>().orderByAsc(UnlockGate::getSortOrder));
+        return Result.success(gates);
+    }
+
+    @Operation(summary = "新增/编辑关卡")
+    @PostMapping("/save")
+    public Result<Long> save(@RequestBody UnlockGate gate) {
+        if (gate.getId() != null) {
+            gate.setUpdatedAt(new Date());
+            unlockGateMapper.updateById(gate);
+            return Result.success(gate.getId());
+        }
+        gate.setCreatedAt(new Date());
+        gate.setUpdatedAt(new Date());
+        unlockGateMapper.insert(gate);
+        return Result.success(gate.getId());
+    }
+
+    @Operation(summary = "删除关卡")
+    @Transactional
+    @PostMapping("/delete")
+    public Result<Boolean> delete(@RequestBody Map<String, Long> body) {
+        Long id = body.get("id");
+        if (id == null) return Result.error("id不能为空");
+        // 级联清理 family_gate_progress
+        familyGateProgressMapper.delete(
+                new LambdaQueryWrapper<FamilyGateProgress>().eq(FamilyGateProgress::getGateId, id));
+        unlockGateMapper.deleteById(id);
+        return Result.success(true);
+    }
+
+    @Operation(summary = "批量排序")
+    @PostMapping("/sort")
+    public Result<Boolean> sort(@RequestBody List<Map<String, Object>> sortList) {
+        for (Map<String, Object> item : sortList) {
+            Long id = Long.valueOf(item.get("id").toString());
+            Integer sortOrder = Integer.valueOf(item.get("sortOrder").toString());
+            UnlockGate gate = unlockGateMapper.selectById(id);
+            if (gate != null) {
+                gate.setSortOrder(sortOrder);
+                gate.setUpdatedAt(new Date());
+                unlockGateMapper.updateById(gate);
+            }
+        }
+        return Result.success(true);
+    }
+
+    @Operation(summary = "启停关卡")
+    @PostMapping("/toggle")
+    public Result<Boolean> toggle(@RequestBody Map<String, Object> body) {
+        Long id = Long.valueOf(body.get("id").toString());
+        Integer enabled = Integer.valueOf(body.get("enabled").toString());
+        UnlockGate gate = unlockGateMapper.selectById(id);
+        if (gate == null) return Result.error("关卡不存在");
+        gate.setEnabled(enabled);
+        gate.setUpdatedAt(new Date());
+        unlockGateMapper.updateById(gate);
+        return Result.success(true);
+    }
+
+    @Operation(summary = "重置某家庭全部关卡进度")
+    @PostMapping("/reset-family")
+    @Transactional
+    public Result<Boolean> resetFamily(@RequestBody Map<String, Object> body) {
+        Long familyId = Long.valueOf(body.get("familyId").toString());
+        // 删除该家庭所有进度记录,使其重新开始
+        familyGateProgressMapper.delete(
+                new LambdaQueryWrapper<FamilyGateProgress>().eq(FamilyGateProgress::getFamilyId, familyId));
+        return Result.success(true);
+    }
+
+    @Operation(summary = "查看某家庭关卡进度")
+    @PostMapping("/family-progress")
+    public Result<List<FamilyGateProgress>> getFamilyProgress(@RequestBody Map<String, Object> body) {
+        Long familyId = Long.valueOf(body.get("familyId").toString());
+        List<FamilyGateProgress> list = familyGateProgressMapper.selectList(
+                new LambdaQueryWrapper<FamilyGateProgress>().eq(FamilyGateProgress::getFamilyId, familyId));
+        return Result.success(list);
+    }
+}

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminVendorController.java

@@ -11,6 +11,7 @@ import javax.annotation.Resource;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @RestController
 @RequestMapping("/api/admin/vendor")
@@ -42,6 +43,7 @@ public class AdminVendorController {
         }
 
         wrapper.orderByDesc(User::getUpdatedAt);
+        SortUtil.applySort(wrapper);
         Page<User> result = userMapper.selectPage(pageParam, wrapper);
 
         Map<String, Object> data = new HashMap<>();

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/DimensionAdminController.java

@@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
+import com.etotem.cfc.util.SortUtil;
 
 @Tag(name = "管理端-健康维度配置")
 @RestController
@@ -65,6 +66,7 @@ public class DimensionAdminController {
         Page<HealthDataSourceRecord> pageParam = new Page<>(page, size);
         LambdaQueryWrapper<HealthDataSourceRecord> wrapper = new LambdaQueryWrapper<HealthDataSourceRecord>()
             .orderByDesc(HealthDataSourceRecord::getCreatedAt);
+        SortUtil.applySort(wrapper);
         Page<HealthDataSourceRecord> result = dataSourceRecordMapper.selectPage(pageParam, wrapper);
         return Result.success(result);
     }

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

@@ -17,6 +17,7 @@ import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
 import java.util.*;
+import com.etotem.cfc.util.SortUtil;
 
 @RestController
 @RequestMapping("/api/admin/report-parser")

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/SupplyManageController.java

@@ -12,6 +12,7 @@ import javax.annotation.Resource;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 /**
  * 供应商管理控制器 - 供应商管理员角色专用
@@ -54,6 +55,7 @@ public class SupplyManageController {
         }
 
         wrapper.orderByDesc(User::getUpdatedAt);
+        SortUtil.applySort(wrapper);
         Page<User> result = userMapper.selectPage(pageParam, wrapper);
 
         Map<String, Object> data = new HashMap<>();

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

@@ -10,6 +10,7 @@ import com.etotem.cfc.service.AssessmentPlanGenerator;
 import com.etotem.cfc.service.FileStorageService;
 import com.etotem.cfc.service.GrowthGuidanceService;
 import com.etotem.cfc.service.ReportParseService;
+import com.etotem.cfc.service.StorageService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.slf4j.Logger;
@@ -42,6 +43,9 @@ public class AssessmentReportController {
     @Resource
     private ReportParseService reportParseService;
 
+    @Resource
+    private StorageService storageService;
+
     @Resource
     private AssessmentPlanGenerator planGenerator;
 
@@ -78,9 +82,12 @@ public class AssessmentReportController {
         // 异步解析上传的报告文件
         final String parsePath = filePath;
         final Long parseResultId = result.getId();
+        final MultipartFile uploadFile = file;
         CompletableFuture.runAsync(() -> {
             try {
-                reportParseService.parseReport(parsePath);
+                // 下载到临时目录后解析(兼容本地/京东云两种存储模式)
+                String localPath = storageService.getFilePath(parsePath);
+                reportParseService.parseReport(localPath);
                 log.info("异步解析报告完成: resultId={}", parseResultId);
             } catch (Exception e) {
                 log.error("异步解析报告失败: resultId={}", parseResultId, e);

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentResultController.java

@@ -15,6 +15,7 @@ import javax.annotation.Resource;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @Tag(name = "测评结果", description = "DAN认知测评结果查询")
 @RestController
@@ -80,6 +81,7 @@ public class AssessmentResultController {
                 .eq(DanAssessmentResult::getStatus, "completed")
                 .orderByDesc(DanAssessmentResult::getAssessmentDate)
                 .last("LIMIT " + size);
+        SortUtil.applySort(wrapper);
         List<DanAssessmentResult> results = danAssessmentResultMapper.selectList(wrapper);
 
         List<AssessmentHistoryItemDTO> list = new ArrayList<>();

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

@@ -14,6 +14,7 @@ import javax.servlet.http.HttpServletRequest;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @RestController("butlerController")
 @RequestMapping("/api/butler")
@@ -71,6 +72,7 @@ public class ButlerController {
                 .eq(User::getReferrerId, userId)
                 .orderByDesc(User::getUpdatedAt);
 
+        SortUtil.applySort(wrapper);
         Page<User> result = userMapper.selectPage(pageParam, wrapper);
 
         Map<String, Object> data = new HashMap<>();

+ 4 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/diet/DietMealConfigController.java

@@ -16,9 +16,10 @@ public class DietMealConfigController {
 
     @PostMapping("/config")
     public Result<Map> getConfig(
-            @RequestAttribute("familyId") Long familyId,
-            @RequestParam String dateType,
-            @RequestParam String mealType) {
+            @RequestAttribute(value = "familyId", required = false) Long familyId,
+            @RequestBody Map<String, Object> request) {
+        String dateType = request.get("date_type") != null ? request.get("date_type").toString() : null;
+        String mealType = request.get("meal_type") != null ? request.get("meal_type").toString() : null;
         return Result.success(dietMealConfigService.getConfig(familyId, dateType, mealType));
     }
 

+ 15 - 6
cfc-backend/src/main/java/com/etotem/cfc/controller/diet/DietRecordController.java

@@ -28,15 +28,24 @@ public class DietRecordController {
 
     @PostMapping("/daily")
     public Result<Map> getDailySummary(
-            @RequestAttribute("familyMemberId") Long familyMemberId,
-            @RequestParam String date) {
-        return Result.success(dietRecordService.getDailySummary(familyMemberId, date));
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute(value = "familyMemberId", required = false) Long familyMemberId,
+            @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+        Long memberId = familyMemberId != null ? familyMemberId : currentMemberId;
+        String date = params.get("date") != null ? params.get("date").toString() : java.time.LocalDate.now().toString();
+        return Result.success(dietRecordService.getDailySummary(userId, memberId, date));
     }
 
     @PostMapping("/weekly")
     public Result<Map> getWeeklyTrend(
-            @RequestAttribute("familyMemberId") Long familyMemberId,
-            @RequestParam String startDate) {
-        return Result.success(dietRecordService.getWeeklyTrend(familyMemberId, startDate));
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute(value = "familyMemberId", required = false) Long familyMemberId,
+            @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+        Long memberId = familyMemberId != null ? familyMemberId : currentMemberId;
+        String startDate = params.get("startDate") != null ? params.get("startDate").toString() : null;
+        if (startDate == null) return Result.error("startDate不能为空");
+        return Result.success(dietRecordService.getWeeklyTrend(userId, memberId, startDate));
     }
 }

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

@@ -14,6 +14,7 @@ import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.FamilyInvitationService;
 import com.etotem.cfc.service.FamilyJoinRequestService;
 import com.etotem.cfc.service.QrCodeService;
+import com.etotem.cfc.service.UnlockGateService;
 import com.etotem.cfc.service.WechatService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -55,6 +56,9 @@ public class FamilyInviteController {
     @Resource
     private WechatService wechatService;
 
+    @Resource
+    private UnlockGateService unlockGateService;
+
     /**
      * 生成家庭邀请二维码
      * 返回: base64 QR 码图片 + 邀请码 + 家庭名称
@@ -314,6 +318,11 @@ public class FamilyInviteController {
 
         try {
             familyInvitationService.acceptInvitation(userId, dto.getToken(), dto.getFamilyRole(), dto.getMergeFamily());
+            // 触发通关解锁刷新:邀请家人成功
+            User user = userMapper.selectById(userId);
+            if (user != null && user.getFamilyId() != null) {
+                unlockGateService.refreshFamilyGates(user.getFamilyId());
+            }
             return Result.success("加入家庭成功");
         } catch (Exception e) {
             return Result.error(e.getMessage());

+ 14 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyMembersController.java

@@ -5,7 +5,10 @@ import com.etotem.cfc.dto.AddFamilyMemberDTO;
 import com.etotem.cfc.dto.FamilyMemberVO;
 import com.etotem.cfc.dto.SwitchMemberVO;
 import com.etotem.cfc.entity.FamilyMemberLog;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.FamilyMemberService;
+import com.etotem.cfc.service.UnlockGateService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.*;
@@ -22,6 +25,12 @@ public class FamilyMembersController {
     @Resource
     private FamilyMemberService familyMemberService;
 
+    @Resource
+    private UnlockGateService unlockGateService;
+
+    @Resource
+    private UserMapper userMapper;
+
 @Operation(summary = "添加家庭成员")
 @PostMapping("/add")
 public Result<FamilyMemberVO> addMember(@RequestBody AddFamilyMemberDTO dto,
@@ -31,6 +40,11 @@ public Result<FamilyMemberVO> addMember(@RequestBody AddFamilyMemberDTO dto,
  }
  try {
    FamilyMemberVO vo = familyMemberService.addMember(userId, dto);
+   // 触发通关解锁刷新:创建家人成功
+   User user = userMapper.selectById(userId);
+   if (user != null && user.getFamilyId() != null) {
+       unlockGateService.refreshFamilyGates(user.getFamilyId());
+   }
    return Result.success(vo);
  } catch (IllegalArgumentException e) {
    return Result.error(e.getMessage());

+ 17 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.java

@@ -4,7 +4,10 @@ import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.SelfCheckQuestionVO;
 import com.etotem.cfc.dto.SelfCheckResultVO;
 import com.etotem.cfc.dto.SubmitSelfCheckDTO;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.FiveDimensionSelfCheckService;
+import com.etotem.cfc.service.UnlockGateService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -28,6 +31,12 @@ public class FiveDimensionSelfCheckController {
     @Resource
     private FiveDimensionSelfCheckService selfCheckService;
 
+    @Resource
+    private UnlockGateService unlockGateService;
+
+    @Resource
+    private UserMapper userMapper;
+
     @Operation(summary = "获取自检问卷题目(15题静态题库)")
     @PostMapping("/questions")
     public Result<List<SelfCheckQuestionVO>> getQuestions(@RequestAttribute("userId") Long userId) {
@@ -46,6 +55,14 @@ public class FiveDimensionSelfCheckController {
         }
         try {
             SelfCheckResultVO vo = selfCheckService.submitSelfCheck(dto, userId);
+            // 触发通关解锁刷新:自检通过 → 解锁个人沙盘
+            User user = userMapper.selectById(userId);
+            if (user != null) {
+                unlockGateService.refreshPersonalGates(userId, user.getFamilyId());
+                if (user.getFamilyId() != null) {
+                    unlockGateService.refreshFamilyGates(user.getFamilyId());
+                }
+            }
             return Result.success(vo);
         } catch (IllegalArgumentException e) {
             return Result.error(e.getMessage());

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

@@ -13,6 +13,7 @@ import javax.annotation.Resource;
 import java.util.HashMap;
 import java.util.Objects;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @Tag(name = "成长规划师订单", description = "成长规划师订单与佣金接口")
 @RestController
@@ -38,6 +39,7 @@ public class GuideOrderController {
         wrapper.orderByDesc(PackageOrder::getCreatedAt);
 
         Page<PackageOrder> pagination = new Page<>(page, size);
+        SortUtil.applySort(wrapper);
         Page<PackageOrder> result = packageOrderMapper.selectPage(pagination, wrapper);
 
         Map<String, Object> response = new HashMap<>();
@@ -55,6 +57,7 @@ public class GuideOrderController {
         LambdaQueryWrapper<PackageOrder> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(PackageOrder::getGuideId, guideId);
 
+        SortUtil.applySort(wrapper);
         Long totalOrders = packageOrderMapper.selectCount(wrapper);
         Long paidOrders = packageOrderMapper.selectCount(new LambdaQueryWrapper<PackageOrder>()
                 .eq(PackageOrder::getGuideId, guideId)

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

@@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import java.util.*;
+import com.etotem.cfc.util.SortUtil;
 
 @Tag(name = "成长规划师团队", description = "成长规划师团队管理接口")
 @RestController
@@ -110,6 +111,7 @@ public class GuideTeamController {
             LambdaQueryWrapper<GuideHierarchy> teamCheck = new LambdaQueryWrapper<>();
             teamCheck.eq(GuideHierarchy::getChildId, userId)
                      .eq(GuideHierarchy::getStatus, "active");
+            SortUtil.applySort(teamCheck);
             GuideHierarchy existingTeam = guideHierarchyService.getOne(teamCheck);
 
             result.put("inTeam", existingTeam != null);
@@ -169,6 +171,7 @@ public class GuideTeamController {
         LambdaQueryWrapper<GuideHierarchy> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(GuideHierarchy::getChildId, userId)
                .eq(GuideHierarchy::getStatus, "active");
+        SortUtil.applySort(wrapper);
         GuideHierarchy hierarchy = guideHierarchyService.getOne(wrapper);
 
         if (hierarchy == null) {

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

@@ -17,6 +17,7 @@ import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @Tag(name = "指导师-方案审核", description = "指导师查看/审核/激活测评方案")
 @RestController

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/notice/NoticeController.java

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import java.util.List;
+import com.etotem.cfc.util.SortUtil;
 
 @Tag(name = "公告", description = "前端公告列表")
 @RestController

+ 81 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/nutrition/NutritionProductController.java

@@ -0,0 +1,81 @@
+package com.etotem.cfc.controller.nutrition;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.NutritionRecommendDTO;
+import com.etotem.cfc.entity.NutritionCategory;
+import com.etotem.cfc.entity.NutritionPlatformListing;
+import com.etotem.cfc.entity.NutritionProduct;
+import com.etotem.cfc.service.NutritionProductService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 营养产品(益生菌/益生元/营养素)推荐接口
+ * 提供分类/品牌/产品搜索/详情/推荐/比价
+ */
+@Tag(name = "营养产品推荐")
+@Slf4j
+@RestController
+@RequestMapping("/api/nutrition/product")
+public class NutritionProductController {
+
+    @Resource
+    private NutritionProductService nutritionProductService;
+
+    @Operation(summary = "分类列表")
+    @PostMapping("/category/list")
+    public Result<List<NutritionCategory>> listCategories() {
+        return Result.success(nutritionProductService.listCategories());
+    }
+
+    @Operation(summary = "品牌列表(含产品数)")
+    @PostMapping("/brand/list")
+    public Result<List<Map<String, Object>>> listBrands() {
+        return Result.success(nutritionProductService.listBrands());
+    }
+
+    @Operation(summary = "产品搜索")
+    @PostMapping("/search")
+    public Result<List<NutritionProduct>> search(@RequestBody NutritionRecommendDTO.SearchQuery query) {
+        return Result.success(nutritionProductService.search(query));
+    }
+
+    @Operation(summary = "产品详情")
+    @PostMapping("/detail")
+    public Result<Map<String, Object>> detail(@RequestBody Map<String, Long> params) {
+        Long productId = params.get("productId");
+        if (productId == null) {
+            return Result.error("productId 不能为空");
+        }
+        Map<String, Object> detail = nutritionProductService.detail(productId);
+        if (detail == null) {
+            return Result.error("产品不存在");
+        }
+        return Result.success(detail);
+    }
+
+    @Operation(summary = "产品推荐")
+    @PostMapping("/recommend")
+    public Result<List<Map<String, Object>>> recommend(@RequestBody NutritionRecommendDTO.RecommendQuery query) {
+        return Result.success(nutritionProductService.recommend(query));
+    }
+
+    @Operation(summary = "平台比价")
+    @PostMapping("/price-compare")
+    public Result<List<NutritionPlatformListing>> priceCompare(@RequestBody Map<String, Long> params) {
+        Long productId = params.get("productId");
+        if (productId == null) {
+            return Result.error("productId 不能为空");
+        }
+        return Result.success(nutritionProductService.priceCompare(productId));
+    }
+}

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/stats/StatsController.java

@@ -35,6 +35,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import com.etotem.cfc.util.SortUtil;
 
 @Tag(name = "任务统计", description = "任务完成率分析")
 @RestController
@@ -94,6 +95,7 @@ public class StatsController {
                 .eq("status", "pending")), taskExecutor);
         CompletableFuture<List<Task>> recentTasksFuture = CompletableFuture.supplyAsync(() -> {
             QueryWrapper<Task> q = new QueryWrapper<Task>().orderByDesc("created_at").last("LIMIT 5");
+            SortUtil.applySort(q);
             return taskMapper.selectList(q);
         }, taskExecutor);
 

+ 28 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/NutritionRecommendDTO.java

@@ -0,0 +1,28 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+public class NutritionRecommendDTO {
+
+    @Data
+    public static class SearchQuery {
+        private String name;
+        private Long brandId;
+        private Long categoryId;
+    }
+
+    @Data
+    public static class RecommendQuery {
+        /** 适用人群 */
+        private String suitableFor;
+        /** 目标成分关键词列表 */
+        private List<String> targetIngredients;
+        /** 预算上限 */
+        private BigDecimal maxPrice;
+        /** 返回数量 */
+        private Integer limit;
+    }
+}

+ 23 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/UnlockGateDTO.java

@@ -0,0 +1,23 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class UnlockGateDTO implements Serializable {
+
+    private Long id;
+    private String gateType;
+    private String name;
+    private String description;
+    private String icon;
+    private Integer sortOrder;
+    private Integer enabled;
+    private Integer isRequired;
+    private Integer isSoft;
+    private String unlockTarget;
+    private String status;          // LOCKED/UNLOCKED/CURRENT
+    private Integer rewardPoints;
+    private Integer rewardEnergy;
+}

+ 31 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/UnlockStatusDTO.java

@@ -0,0 +1,31 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 用户关卡全景(/api/unlock/status 返回结构)
+ */
+@Data
+public class UnlockStatusDTO implements Serializable {
+
+    /** 当前主线硬门禁关卡(无则 null) */
+    private UnlockGateDTO currentLockGate;
+
+    /** 个人沙盘是否锁定 */
+    private boolean personalSandboxLocked;
+
+    /** 全家沙盘是否锁定 */
+    private boolean familySandboxLocked;
+
+    /** 孩子微行动是否锁定 */
+    private boolean childMicroActionLocked;
+
+    /** 是否已加入家庭 */
+    private boolean hasFamily;
+
+    /** 全量关卡 */
+    private List<UnlockGateDTO> gates;
+}

+ 37 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyGateProgress.java

@@ -0,0 +1,37 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 家庭关卡通关进度
+ */
+@Data
+@TableName("family_gate_progress")
+public class FamilyGateProgress implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 家庭ID */
+    private Long familyId;
+
+    /** 关卡ID */
+    private Long gateId;
+
+    /** 状态: LOCKED/UNLOCKED */
+    private String status;
+
+    /** 解锁人user_id */
+    private Long unlockedBy;
+
+    /** 解锁时间 */
+    private Date unlockedAt;
+
+    private Date createdAt;
+}

+ 23 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/FileRecord.java

@@ -0,0 +1,23 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("file_record")
+public class FileRecord implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String hash;
+    private String url;
+    private Long fileSize;
+    private String contentType;
+    private String originalFilename;
+    private String category;
+    private Date createdAt;
+    private Date updatedAt;
+}

+ 27 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/NutritionBrand.java

@@ -0,0 +1,27 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("nutrition_brand")
+public class NutritionBrand implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String name;
+
+    private String logoUrl;
+
+    private String description;
+
+    private String country;
+
+    private Date createTime;
+}

+ 25 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/NutritionCategory.java

@@ -0,0 +1,25 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("nutrition_category")
+public class NutritionCategory implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String name;
+
+    private Long parentId;
+
+    private Integer sortOrder;
+
+    private Date createTime;
+}

+ 36 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/NutritionIngredient.java

@@ -0,0 +1,36 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("nutrition_ingredient")
+public class NutritionIngredient implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long productId;
+
+    private String ingredientType;
+
+    private String name;
+
+    private String strain;
+
+    private BigDecimal amountPerServing;
+
+    private String unit;
+
+    private String dailyValue;
+
+    private String purpose;
+
+    private Date createTime;
+}

+ 42 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/NutritionPlatformListing.java

@@ -0,0 +1,42 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("nutrition_platform_listing")
+public class NutritionPlatformListing implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long productId;
+
+    private String platform;
+
+    private String platformUrl;
+
+    private BigDecimal currentPrice;
+
+    private BigDecimal originalPrice;
+
+    private String priceUnit;
+
+    private BigDecimal pricePerUnit;
+
+    private Integer salesCount;
+
+    private String stockStatus;
+
+    private Date lastChecked;
+
+    private Date createTime;
+
+    private Date updateTime;
+}

+ 46 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/NutritionProduct.java

@@ -0,0 +1,46 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("nutrition_product")
+public class NutritionProduct implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String name;
+
+    private Long brandId;
+
+    private Long categoryId;
+
+    private String subCategory;
+
+    private String form;
+
+    private String netContent;
+
+    private String shelfLife;
+
+    private String suitableFor;
+
+    private String imageUrl;
+
+    private String description;
+
+    private Integer status;
+
+    /** 本平台商品ID(关联 products.id),有则直接跳转购买 */
+    private Long localProductId;
+
+    private Date createTime;
+
+    private Date updateTime;
+}

+ 30 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/NutritionProductReview.java

@@ -0,0 +1,30 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("nutrition_product_review")
+public class NutritionProductReview implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long platformListingId;
+
+    private String source;
+
+    private BigDecimal rating;
+
+    private Integer reviewCount;
+
+    private String goodRate;
+
+    private Date lastUpdated;
+}

+ 60 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/UnlockGate.java

@@ -0,0 +1,60 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 通关解锁关卡定义
+ */
+@Data
+@TableName("unlock_gates")
+public class UnlockGate implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 关卡类型: SELF_CHECK/INVITE_FAMILY/CREATE_FAMILY_MEMBER/PURCHASE/MEMBERSHIP/REPORT_UPLOAD/MICRO_ACTION/STREAK_DAYS */
+    private String gateType;
+
+    /** 关卡名称 */
+    private String name;
+
+    /** 关卡说明 */
+    private String description;
+
+    /** 图标emoji */
+    private String icon;
+
+    /** 排序号 */
+    private Integer sortOrder;
+
+    /** 启用:1 禁用:0 */
+    private Integer enabled;
+
+    /** 归属: 1主线 0增值 */
+    private Integer isRequired;
+
+    /** 形态: 0硬门禁 1软引导 */
+    private Integer isSoft;
+
+    /** 解锁目标模块 */
+    private String unlockTarget;
+
+    /** 条件参数JSON */
+    private String conditionParams;
+
+    /** 通关奖励积分 */
+    private Integer rewardPoints;
+
+    /** 通关奖励能量 */
+    private Integer rewardEnergy;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyGateProgressMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.FamilyGateProgress;
+
+public interface FamilyGateProgressMapper extends BaseMapper<FamilyGateProgress> {
+}

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

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

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/NutritionBrandMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.NutritionBrand;
+
+public interface NutritionBrandMapper extends BaseMapper<NutritionBrand> {
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/NutritionCategoryMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.NutritionCategory;
+
+public interface NutritionCategoryMapper extends BaseMapper<NutritionCategory> {
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/NutritionIngredientMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.NutritionIngredient;
+
+public interface NutritionIngredientMapper extends BaseMapper<NutritionIngredient> {
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/NutritionPlatformListingMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.NutritionPlatformListing;
+
+public interface NutritionPlatformListingMapper extends BaseMapper<NutritionPlatformListing> {
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/NutritionProductMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.NutritionProduct;
+
+public interface NutritionProductMapper extends BaseMapper<NutritionProduct> {
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/NutritionProductReviewMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.NutritionProductReview;
+
+public interface NutritionProductReviewMapper extends BaseMapper<NutritionProductReview> {
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/UnlockGateMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.UnlockGate;
+
+public interface UnlockGateMapper extends BaseMapper<UnlockGate> {
+}

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AIService.java

@@ -19,6 +19,7 @@ import org.springframework.scheduling.annotation.Async;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import java.util.*;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.SortUtil;
 
 /**
  * Dify AI API 封装
@@ -389,6 +390,7 @@ public class AIService {
         summaryQ.eq(AiConversationSummary::getConversationId, conversationId);
         summaryQ.orderByDesc(AiConversationSummary::getCreatedAt);
         summaryQ.last("LIMIT 1");
+        SortUtil.applySort(summaryQ);
         AiConversationSummary currentSummary = aiConversationSummaryMapper.selectOne(summaryQ);
         if (currentSummary != null) {
             ctx.put("conversation_summary", currentSummary.getSummaryText());
@@ -399,6 +401,7 @@ public class AIService {
         recentQ.eq(AiConversationSummary::getUserId, userId);
         recentQ.orderByDesc(AiConversationSummary::getCreatedAt);
         recentQ.last("LIMIT 5");
+        SortUtil.applySort(recentQ);
         List<AiConversationSummary> recentList = aiConversationSummaryMapper.selectList(recentQ);
         List<String> summaries = recentList.stream()
                 .map(AiConversationSummary::getSummaryText)
@@ -411,6 +414,7 @@ public class AIService {
         // 用户关键事实
         LambdaQueryWrapper<AiUserFact> factQ = new LambdaQueryWrapper<>();
         factQ.eq(AiUserFact::getUserId, userId);
+        SortUtil.applySort(factQ);
         List<AiUserFact> facts = aiUserFactMapper.selectList(factQ);
         if (!facts.isEmpty()) {
             Map<String, String> factMap = new LinkedHashMap<>();
@@ -480,6 +484,7 @@ public class AIService {
         recentQ.eq(AiConversationSummary::getUserId, userId);
         recentQ.orderByDesc(AiConversationSummary::getCreatedAt);
         recentQ.last("LIMIT 3");
+        SortUtil.applySort(recentQ);
         List<AiConversationSummary> recentList = aiConversationSummaryMapper.selectList(recentQ);
         List<String> summaries = recentList.stream()
                 .map(AiConversationSummary::getSummaryText)
@@ -492,6 +497,7 @@ public class AIService {
         // 用户事实
         LambdaQueryWrapper<AiUserFact> factQ = new LambdaQueryWrapper<>();
         factQ.eq(AiUserFact::getUserId, userId);
+        SortUtil.applySort(factQ);
         List<AiUserFact> facts = aiUserFactMapper.selectList(factQ);
         if (!facts.isEmpty()) {
             StringBuilder factSb = new StringBuilder();

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityAdminService.java

@@ -12,6 +12,7 @@ import javax.annotation.Resource;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 /**
  * 后台活动管理业务逻辑
@@ -47,6 +48,7 @@ public class ActivityAdminService {
         }
         wrapper.orderByDesc(Activity::getUpdatedAt);
 
+        SortUtil.applySort(wrapper);
         Page<Activity> pageResult = activityMapper.selectPage(new Page<>(page, size), wrapper);
         Map<String, Object> data = new HashMap<>();
         data.put("records", pageResult.getRecords());

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

@@ -9,6 +9,7 @@ import org.springframework.stereotype.Service;
 import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
+import com.etotem.cfc.util.SortUtil;
 
 @Service
 public class ActivityFeedbackService {
@@ -33,6 +34,7 @@ public class ActivityFeedbackService {
         QueryWrapper<ActivityFeedback> wrapper = new QueryWrapper<>();
         wrapper.eq("activity_id", activityId)
                .orderByDesc("created_at");
+        SortUtil.applySort(wrapper);
         List<ActivityFeedback> list = activityFeedbackMapper.selectList(wrapper);
         return Result.success(list);
     }
@@ -44,6 +46,7 @@ public class ActivityFeedbackService {
             wrapper.eq("child_id", memberId);
         }
         wrapper.orderByDesc("created_at").last("LIMIT 1");
+        SortUtil.applySort(wrapper);
         ActivityFeedback feedback = activityFeedbackMapper.selectOne(wrapper);
         return Result.success(feedback);
     }

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityRegistrationService.java

@@ -20,6 +20,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.SortUtil;
 
 @Slf4j
 @Service
@@ -130,6 +131,7 @@ public class ActivityRegistrationService {
                 .or().isNull(ActivityRegistration::getChildId));
         }
         wrapper.orderByDesc(ActivityRegistration::getRegisteredAt);
+        SortUtil.applySort(wrapper);
         Page<ActivityRegistration> pageResult = registrationMapper.selectPage(new Page<>(page, size), wrapper);
         List<Map<String, Object>> records = pageResult.getRecords().stream().map(reg -> {
             Map<String, Object> item = new HashMap<>();
@@ -155,6 +157,7 @@ public class ActivityRegistrationService {
         LambdaQueryWrapper<ActivityRegistration> wrapper = new LambdaQueryWrapper<ActivityRegistration>()
             .eq(ActivityRegistration::getActivityId, activityId).orderByDesc(ActivityRegistration::getRegisteredAt);
         if (status != null && !status.isEmpty()) wrapper.eq(ActivityRegistration::getStatus, status);
+        SortUtil.applySort(wrapper);
         List<ActivityRegistration> list = registrationMapper.selectList(wrapper);
         List<Map<String, Object>> result = list.stream().map(reg -> {
             Map<String, Object> item = new HashMap<>();
@@ -249,6 +252,7 @@ public class ActivityRegistrationService {
             .eq(ActivityRegistration::getUserId, userId);
         if (status != null) wrapper.eq(ActivityRegistration::getStatus, status);
         wrapper.last("LIMIT 1");
+        SortUtil.applySort(wrapper);
         return registrationMapper.selectOne(wrapper);
     }
 }

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityService.java

@@ -30,6 +30,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.SortUtil;
 
 @Slf4j
 @Service
@@ -80,6 +81,7 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
                     .or().eq(Activity::getVisibility, "all")
                     .or().isNull(Activity::getVisibility));
         }
+        SortUtil.applySort(query);
         Page<Activity> pageResult = this.page(new Page<>(page, size), query);
         boolean isGuest = (userId == null || userId <= 0);
         Long effectiveUserId = isGuest ? null : userId;

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AfterSalesService.java

@@ -13,6 +13,7 @@ import org.springframework.stereotype.Service;
 import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
+import com.etotem.cfc.util.SortUtil;
 
 @Service
 public class AfterSalesService {
@@ -62,6 +63,7 @@ public class AfterSalesService {
         if (status != null && !status.isEmpty()) {
             wrapper.eq(AfterSalesRequest::getStatus, status);
         }
+        SortUtil.applySort(wrapper);
         List<AfterSalesRequest> list = afterSalesRequestMapper.selectList(wrapper);
         return Result.success(list);
     }

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ArticleCommentService.java

@@ -14,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional;
 import javax.annotation.Resource;
 import java.util.*;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.SortUtil;
 
 @Service
 public class ArticleCommentService {
@@ -151,6 +152,7 @@ public class ArticleCommentService {
             wrapper.eq(ArticleComment::getArticleId, articleId);
         }
         wrapper.orderByDesc(ArticleComment::getCreatedAt);
+        SortUtil.applySort(wrapper);
         return articleCommentMapper.selectList(wrapper);
     }
 
@@ -173,6 +175,7 @@ public class ArticleCommentService {
         }
 
         wrapper.orderByDesc(ArticleComment::getCreatedAt);
+        SortUtil.applySort(wrapper);
         List<ArticleComment> comments = articleCommentMapper.selectList(wrapper);
 
         Map<Long, User> userCache = new HashMap<>();
@@ -221,6 +224,7 @@ public class ArticleCommentService {
         }
         wrapper.orderByDesc(ArticleComment::getCreatedAt);
 
+        SortUtil.applySort(wrapper);
         Page<ArticleComment> p = articleCommentMapper.selectPage(new Page<>(page, size), wrapper);
         Page<Map<String, Object>> result = new Page<>(p.getCurrent(), p.getSize(), p.getTotal());
 

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java

@@ -28,6 +28,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import javax.annotation.Resource;
 import java.util.*;
+import com.etotem.cfc.util.SortUtil;
 
 @Slf4j
 @Service
@@ -169,6 +170,7 @@ public class ArticleService {
                 .eq(Article::getId, articleId);
         articleMapper.update(null, wrapper);
 
+
         // 2. Save reading record
         ArticleReadingRecord record = new ArticleReadingRecord();
         record.setUserId(userId);

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentAppointmentService.java

@@ -12,6 +12,7 @@ import java.util.Date;
 import java.util.List;
 
 import lombok.extern.slf4j.Slf4j;
+import com.etotem.cfc.util.SortUtil;
 
 @Slf4j
 @Service
@@ -94,6 +95,7 @@ public class AssessmentAppointmentService extends ServiceImpl<AssessmentAppointm
         LambdaQueryWrapper<AssessmentAppointment> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AssessmentAppointment::getUserId, userId);
         wrapper.orderByDesc(AssessmentAppointment::getCreatedAt);
+        SortUtil.applySort(wrapper);
         return assessmentAppointmentMapper.selectList(wrapper);
     }
 
@@ -101,6 +103,7 @@ public class AssessmentAppointmentService extends ServiceImpl<AssessmentAppointm
         LambdaQueryWrapper<AssessmentAppointment> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AssessmentAppointment::getGuideId, guideId);
         wrapper.orderByDesc(AssessmentAppointment::getCreatedAt);
+        SortUtil.applySort(wrapper);
         return assessmentAppointmentMapper.selectList(wrapper);
     }
 
@@ -108,6 +111,7 @@ public class AssessmentAppointmentService extends ServiceImpl<AssessmentAppointm
         LambdaQueryWrapper<AssessmentAppointment> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AssessmentAppointment::getAssessorId, assessorId);
         wrapper.orderByDesc(AssessmentAppointment::getCreatedAt);
+        SortUtil.applySort(wrapper);
         return assessmentAppointmentMapper.selectList(wrapper);
     }
 

+ 12 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentOrderService.java

@@ -18,6 +18,7 @@ import java.util.Date;
 import java.util.List;
 
 import lombok.extern.slf4j.Slf4j;
+import com.etotem.cfc.util.SortUtil;
 
 @Slf4j
 @Service
@@ -62,6 +63,7 @@ public class AssessmentOrderService extends ServiceImpl<AssessmentOrderMapper, A
     public boolean paySuccess(String orderNo, String transactionId, String payType) {
         LambdaQueryWrapper<AssessmentOrder> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AssessmentOrder::getOrderNo, orderNo);
+        SortUtil.applySort(wrapper);
         AssessmentOrder order = this.getOne(wrapper);
         if (order == null) return false;
         order.setStatus("paid");
@@ -80,6 +82,7 @@ public class AssessmentOrderService extends ServiceImpl<AssessmentOrderMapper, A
     public boolean cancelOrder(String orderNo) {
         LambdaQueryWrapper<AssessmentOrder> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AssessmentOrder::getOrderNo, orderNo);
+        SortUtil.applySort(wrapper);
         AssessmentOrder order = this.getOne(wrapper);
         if (order == null) return false;
         if (!"pending".equals(order.getStatus())) return false;
@@ -91,6 +94,7 @@ public class AssessmentOrderService extends ServiceImpl<AssessmentOrderMapper, A
     public boolean refundOrder(String orderNo) {
         LambdaQueryWrapper<AssessmentOrder> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AssessmentOrder::getOrderNo, orderNo);
+        SortUtil.applySort(wrapper);
         AssessmentOrder order = this.getOne(wrapper);
         if (order == null) return false;
         if (!"paid".equals(order.getStatus())) return false;
@@ -102,6 +106,7 @@ public class AssessmentOrderService extends ServiceImpl<AssessmentOrderMapper, A
     public AssessmentOrder getByOrderNo(String orderNo) {
         LambdaQueryWrapper<AssessmentOrder> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AssessmentOrder::getOrderNo, orderNo);
+        SortUtil.applySort(wrapper);
         return this.getOne(wrapper);
     }
 
@@ -109,18 +114,21 @@ public class AssessmentOrderService extends ServiceImpl<AssessmentOrderMapper, A
         LambdaQueryWrapper<AssessmentOrder> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AssessmentOrder::getFamilyId, familyId);
         wrapper.orderByDesc(AssessmentOrder::getCreatedAt);
+        SortUtil.applySort(wrapper);
         return this.list(wrapper);
     }
 
     public AssessmentOrder getByAppointmentId(Long appointmentId) {
         LambdaQueryWrapper<AssessmentOrder> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AssessmentOrder::getAppointmentId, appointmentId);
+        SortUtil.applySort(wrapper);
         return this.getOne(wrapper);
     }
 
     public boolean markResultsReady(String orderNo) {
         LambdaQueryWrapper<AssessmentOrder> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AssessmentOrder::getOrderNo, orderNo);
+        SortUtil.applySort(wrapper);
         AssessmentOrder order = this.getOne(wrapper);
         if (order == null || !"paid".equals(order.getStatus())) return false;
         order.setStatus("results_ready");
@@ -132,6 +140,7 @@ public class AssessmentOrderService extends ServiceImpl<AssessmentOrderMapper, A
         LambdaQueryWrapper<AssessmentOrder> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AssessmentOrder::getUserId, userId);
         wrapper.orderByDesc(AssessmentOrder::getCreatedAt);
+        SortUtil.applySort(wrapper);
         return this.list(wrapper);
     }
 
@@ -143,12 +152,14 @@ public class AssessmentOrderService extends ServiceImpl<AssessmentOrderMapper, A
         wrapper.eq(AssessmentOrder::getFamilyId, familyId);
         wrapper.in(AssessmentOrder::getStatus, "paid", "results_ready");
         wrapper.orderByDesc(AssessmentOrder::getCreatedAt);
+        SortUtil.applySort(wrapper);
         return this.list(wrapper);
     }
 
     public boolean payOrder(String orderNo, String payType) {
         LambdaQueryWrapper<AssessmentOrder> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AssessmentOrder::getOrderNo, orderNo);
+        SortUtil.applySort(wrapper);
         AssessmentOrder order = this.getOne(wrapper);
         if (order == null || !"pending".equals(order.getStatus())) return false;
         order.setStatus("paid");
@@ -234,6 +245,7 @@ public class AssessmentOrderService extends ServiceImpl<AssessmentOrderMapper, A
                    .eq(AssessmentAppointment::getStatus, 0)
                    .orderByDesc(AssessmentAppointment::getCreatedAt)
                    .last("LIMIT 1");
+            SortUtil.applySort(wrapper);
             List<AssessmentAppointment> list = assessmentAppointmentMapper.selectList(wrapper);
             if (list != null && !list.isEmpty()) {
                 order.setAppointmentId(list.get(0).getId());

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

@@ -13,6 +13,7 @@ import org.springframework.stereotype.Service;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.SortUtil;
 
 /**
  * 测评报告权限服务
@@ -65,6 +66,7 @@ public class AssessmentPermissionService {
                .eq(DanAssessmentResult::getStatus, "completed")
                .orderByDesc(DanAssessmentResult::getAssessmentDate);
 
+        SortUtil.applySort(wrapper);
         return danAssessmentResultMapper.selectList(wrapper);
     }
 
@@ -77,6 +79,7 @@ public class AssessmentPermissionService {
                .eq(DanAssessmentResult::getStatus, "completed")
                .orderByDesc(DanAssessmentResult::getAssessmentDate);
 
+        SortUtil.applySort(wrapper);
         return danAssessmentResultMapper.selectList(wrapper);
     }
 

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentQuotaService.java

@@ -14,6 +14,7 @@ import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.List;
 import java.util.Random;
+import com.etotem.cfc.util.SortUtil;
 
 @Service
 public class AssessmentQuotaService {
@@ -79,6 +80,7 @@ public class AssessmentQuotaService {
                 .gt(AssessmentQuota::getRemainingSessions, 0)
                 .gt(AssessmentQuota::getValidityEnd, new Date())
                 .orderByDesc(AssessmentQuota::getCreatedAt);
+        SortUtil.applySort(wrapper);
         return mapper.selectList(wrapper);
     }
 

+ 12 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentService.java

@@ -31,6 +31,7 @@ import java.util.Date;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @Slf4j
 @Service
@@ -63,12 +64,14 @@ public class AssessmentService extends ServiceImpl<AssessmentMaterialMapper, Ass
     public AssessmentMaterial getActiveMaterial() {
         LambdaQueryWrapper<AssessmentMaterial> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AssessmentMaterial::getStatus, "active");
+        SortUtil.applySort(wrapper);
         return this.getOne(wrapper);
     }
 
     public List<AssessmentMaterial> getMaterialList() {
         LambdaQueryWrapper<AssessmentMaterial> wrapper = new LambdaQueryWrapper<>();
         wrapper.orderByDesc(AssessmentMaterial::getCreatedAt);
+        SortUtil.applySort(wrapper);
         return this.list(wrapper);
     }
 
@@ -87,6 +90,7 @@ public class AssessmentService extends ServiceImpl<AssessmentMaterialMapper, Ass
     public AssessmentPointsConfig getPointsConfig() {
         LambdaQueryWrapper<AssessmentPointsConfig> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AssessmentPointsConfig::getIsActive, 1);
+        SortUtil.applySort(wrapper);
         return pointsConfigMapper.selectOne(wrapper);
     }
 
@@ -110,6 +114,7 @@ public class AssessmentService extends ServiceImpl<AssessmentMaterialMapper, Ass
     public FamilyAssessmentConfig getFamilyConfig(Long familyId) {
         LambdaQueryWrapper<FamilyAssessmentConfig> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(FamilyAssessmentConfig::getFamilyId, familyId);
+        SortUtil.applySort(wrapper);
         return familyConfigMapper.selectOne(wrapper);
     }
 
@@ -155,6 +160,7 @@ public class AssessmentService extends ServiceImpl<AssessmentMaterialMapper, Ass
     public List<FamilyAssessmentConfig> getEnabledConfigs() {
         LambdaQueryWrapper<FamilyAssessmentConfig> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(FamilyAssessmentConfig::getIsEnabled, 1);
+        SortUtil.applySort(wrapper);
         return familyConfigMapper.selectList(wrapper);
     }
 
@@ -164,6 +170,7 @@ public class AssessmentService extends ServiceImpl<AssessmentMaterialMapper, Ass
         }
         LambdaQueryWrapper<FamilyAssessmentConfig> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(FamilyAssessmentConfig::getCreatedBy, teacherId);
+        SortUtil.applySort(wrapper);
         return familyConfigMapper.selectList(wrapper);
     }
 
@@ -192,6 +199,7 @@ public class AssessmentService extends ServiceImpl<AssessmentMaterialMapper, Ass
         LambdaQueryWrapper<AssessmentRecord> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AssessmentRecord::getFamilyId, familyId);
         wrapper.orderByDesc(AssessmentRecord::getCreatedAt);
+        SortUtil.applySort(wrapper);
         return recordMapper.selectList(wrapper);
     }
 
@@ -201,6 +209,7 @@ public class AssessmentService extends ServiceImpl<AssessmentMaterialMapper, Ass
         wrapper.eq(AssessmentRecord::getStatus, "pending");
         wrapper.orderByDesc(AssessmentRecord::getCreatedAt);
         wrapper.last("LIMIT 1");
+        SortUtil.applySort(wrapper);
         return recordMapper.selectOne(wrapper);
     }
 
@@ -270,6 +279,7 @@ public class AssessmentService extends ServiceImpl<AssessmentMaterialMapper, Ass
         LambdaQueryWrapper<DanAssessmentResult> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(DanAssessmentResult::getTeacherId, guideId);
         wrapper.orderByDesc(DanAssessmentResult::getCreatedAt);
+        SortUtil.applySort(wrapper);
         return danAssessmentResultMapper.selectList(wrapper);
     }
 
@@ -277,6 +287,7 @@ public class AssessmentService extends ServiceImpl<AssessmentMaterialMapper, Ass
         LambdaQueryWrapper<DanAssessmentResult> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(DanAssessmentResult::getAppointmentId, appointmentId);
         wrapper.last("LIMIT 1");
+        SortUtil.applySort(wrapper);
         return danAssessmentResultMapper.selectOne(wrapper);
     }
 
@@ -293,6 +304,7 @@ public class AssessmentService extends ServiceImpl<AssessmentMaterialMapper, Ass
         wrapper.eq(DanAssessmentResult::getStatus, "completed");
         wrapper.orderByDesc(DanAssessmentResult::getAssessmentDate);
         wrapper.last("LIMIT 1");
+        SortUtil.applySort(wrapper);
         return danAssessmentResultMapper.selectOne(wrapper);
     }
 }

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/BeijingNutritionService.java

@@ -23,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional;
 import javax.annotation.Resource;
 import java.util.*;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.SortUtil;
 
 /**
  * 北京菌群报告 — 精准营养推荐服务
@@ -564,6 +565,7 @@ public class BeijingNutritionService {
         LambdaQueryWrapper<MealConfig> configWrapper = new LambdaQueryWrapper<>();
         configWrapper.eq(MealConfig::getFamilyId, familyId);
         configWrapper.eq(MealConfig::getConfigDateType, isWeekend(date) ? "weekend" : "weekday");
+        SortUtil.applySort(configWrapper);
         List<MealConfig> mealConfigs = mealConfigMapper.selectList(configWrapper);
         
         // 2. 收集参与成员
@@ -588,6 +590,7 @@ public class BeijingNutritionService {
         if (participantIds.isEmpty()) {
             LambdaQueryWrapper<FamilyMember> memberWrapper = new LambdaQueryWrapper<>();
             memberWrapper.eq(FamilyMember::getFamilyId, familyId);
+            SortUtil.applySort(memberWrapper);
             List<FamilyMember> allMembers = familyMemberMapper.selectList(memberWrapper);
             for (FamilyMember m : allMembers) {
                 participantIds.add(m.getId());
@@ -608,6 +611,7 @@ public class BeijingNutritionService {
             // 查调研表
             LambdaQueryWrapper<com.etotem.cfc.entity.DietPreferences> prefWrapper = new LambdaQueryWrapper<>();
             prefWrapper.eq(com.etotem.cfc.entity.DietPreferences::getFamilyMemberId, memberId);
+            SortUtil.applySort(prefWrapper);
             com.etotem.cfc.entity.DietPreferences prefs = dietPreferencesMapper.selectOne(prefWrapper);
             
             if (prefs != null) {
@@ -635,11 +639,13 @@ public class BeijingNutritionService {
                          .eq(HealthReport::getReportType, "gut_flora")
                          .orderByDesc(HealthReport::getReportDate)
                          .last("LIMIT 1");
+            SortUtil.applySort(reportWrapper);
             HealthReport report = healthReportMapper.selectOne(reportWrapper);
             
             if (report != null) {
                 LambdaQueryWrapper<HealthGutFlora> floraWrapper = new LambdaQueryWrapper<>();
                 floraWrapper.eq(HealthGutFlora::getReportId, report.getId());
+                SortUtil.applySort(floraWrapper);
                 List<HealthGutFlora> floraList = healthGutFloraMapper.selectList(floraWrapper);
                 
                 Map<String, String> bacteriaStatus = new HashMap<>();

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

@@ -17,6 +17,7 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @Service
 public class ButlerService {
@@ -180,6 +181,7 @@ public class ButlerService {
                 .isNotNull(User::getButlerStatus)
                 .eq(User::getButlerStatus, "approved")
                 .orderByDesc(User::getUpdatedAt);
+        SortUtil.applySort(wrapper);
         java.util.List<User> butlers = userMapper.selectList(wrapper);
         return Result.success(butlers);
     }
@@ -207,6 +209,7 @@ public class ButlerService {
         }
 
         wrapper.orderByDesc(User::getUpdatedAt);
+        SortUtil.applySort(wrapper);
         Page<User> result = userMapper.selectPage(pageParam, wrapper);
 
         Map<String, Object> data = new HashMap<>();
@@ -296,6 +299,7 @@ public class ButlerService {
              .orderByAsc(ButlerProfile::getCreatedAt)  // round-robin by created_at
              .last("LIMIT 1");
 
+        SortUtil.applySort(query);
         ButlerProfile butler = butlerProfileMapper.selectOne(query);
 
         if (butler == null) {

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ButlerSessionService.java

@@ -9,6 +9,7 @@ import org.springframework.stereotype.Service;
 import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
+import com.etotem.cfc.util.SortUtil;
 
 @Service
 public class ButlerSessionService {
@@ -88,6 +89,7 @@ public class ButlerSessionService {
         if (status != null && !status.isEmpty()) {
             wrapper.eq(ButlerSession::getStatus, status);
         }
+        SortUtil.applySort(wrapper);
         return butlerSessionMapper.selectPage(pageParam, wrapper);
     }
 

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ChatMirrorService.java

@@ -13,6 +13,7 @@ import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @Slf4j
 @Service
@@ -37,6 +38,7 @@ public class ChatMirrorService {
         LambdaQueryWrapper<ChatConversation> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.eq(ChatConversation::getDifyConversationId, difyConversationId);
         
+        SortUtil.applySort(queryWrapper);
         ChatConversation existing = chatConversationMapper.selectOne(queryWrapper);
         
         if (existing != null) {
@@ -101,6 +103,7 @@ public class ChatMirrorService {
         queryWrapper.eq(ChatConversation::getUserId, userId)
                    .orderByDesc(ChatConversation::getLastMessageAt);
         
+        SortUtil.applySort(queryWrapper);
         return chatConversationMapper.selectList(queryWrapper);
     }
 
@@ -114,6 +117,7 @@ public class ChatMirrorService {
         queryWrapper.eq(ChatMessage::getConversationId, conversationId)
                    .orderByAsc(ChatMessage::getCreatedAt);
         
+        SortUtil.applySort(queryWrapper);
         return chatMessageMapper.selectList(queryWrapper);
     }
 }

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CircleChallengeService.java

@@ -9,6 +9,7 @@ import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import java.util.*;
+import com.etotem.cfc.util.SortUtil;
 
 @Slf4j
 @Service
@@ -22,9 +23,11 @@ public class CircleChallengeService {
         LambdaQueryWrapper<CircleChallenge> cw = new LambdaQueryWrapper<CircleChallenge>()
                 .eq(CircleChallenge::getCircleId, circleId)
                 .eq(CircleChallenge::getStatus, "active");
+        SortUtil.applySort(cw);
         List<CircleChallenge> active = circleChallengeMapper.selectList(cw);
         if (active.isEmpty()) {
             autoCreateChallenges(circleId);
+            SortUtil.applySort(cw);
             active = circleChallengeMapper.selectList(cw);
         }
         return active;
@@ -74,6 +77,7 @@ public class CircleChallengeService {
                 .eq(CircleChallenge::getCircleId, circleId)
                 .eq(CircleChallenge::getStatus, "completed")
                 .orderByDesc(CircleChallenge::getEndDate);
+        SortUtil.applySort(cw);
         return circleChallengeMapper.selectList(cw);
     }
 }

+ 12 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CircleMatchService.java

@@ -25,6 +25,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.SortUtil;
 
 /**
  * 圈子匹配引擎
@@ -128,6 +129,7 @@ public class CircleMatchService {
             // 查找孩子参与的活动
             LambdaQueryWrapper<ActivityRegistration> regWrapper = new LambdaQueryWrapper<ActivityRegistration>()
                     .eq(ActivityRegistration::getChildId, memberId);
+            SortUtil.applySort(regWrapper);
             List<ActivityRegistration> myRegs = activityRegistrationMapper.selectList(regWrapper);
 
             for (ActivityRegistration reg : myRegs) {
@@ -137,6 +139,7 @@ public class CircleMatchService {
                 LambdaQueryWrapper<ActivityRegistration> sameActWrapper = new LambdaQueryWrapper<ActivityRegistration>()
                         .eq(ActivityRegistration::getActivityId, reg.getActivityId())
                         .ne(ActivityRegistration::getChildId, memberId);
+                SortUtil.applySort(sameActWrapper);
                 List<ActivityRegistration> sameAct = activityRegistrationMapper.selectList(sameActWrapper);
 
                 if (!sameAct.isEmpty()) {
@@ -170,6 +173,7 @@ public class CircleMatchService {
                     .eq(DanAssessmentResult::getStatus, "completed")
                     .ne(DanAssessmentResult::getChildId, memberId)
                     .orderByDesc(DanAssessmentResult::getAssessmentDate);
+            SortUtil.applySort(qw);
             List<DanAssessmentResult> others = danAssessmentResultMapper.selectList(qw);
 
             // 按memberId去重取最新
@@ -210,6 +214,7 @@ public class CircleMatchService {
                     .eq(DanAssessmentResult::getStatus, "completed")
                     .ne(DanAssessmentResult::getChildId, memberId)
                     .orderByDesc(DanAssessmentResult::getAssessmentDate);
+            SortUtil.applySort(qw);
             List<DanAssessmentResult> others = danAssessmentResultMapper.selectList(qw);
 
             Map<Long, DanAssessmentResult> latestByChild = new HashMap<>();
@@ -248,6 +253,7 @@ public class CircleMatchService {
                     .eq(HealthReport::getStatus, "active")
                     .orderByDesc(HealthReport::getReportDate)
                     .last("LIMIT 1");
+            SortUtil.applySort(myHw);
             HealthReport myReport = healthReportMapper.selectOne(myHw);
             if (myReport == null || myReport.getOverallScore() == null) return result;
 
@@ -256,6 +262,7 @@ public class CircleMatchService {
                     .eq(HealthReport::getStatus, "active")
                     .ne(HealthReport::getUserId, memberId)
                     .orderByDesc(HealthReport::getReportDate);
+            SortUtil.applySort(othersHw);
             List<HealthReport> others = healthReportMapper.selectList(othersHw);
 
             Map<Long, HealthReport> latestByUser = new HashMap<>();
@@ -361,6 +368,7 @@ public class CircleMatchService {
             // 查找孩子所在的家庭
             LambdaQueryWrapper<FamilyMember> fmw = new LambdaQueryWrapper<FamilyMember>()
                     .eq(FamilyMember::getId, memberId);
+            SortUtil.applySort(fmw);
             FamilyMember member = familyMemberMapper.selectOne(fmw);
             if (member == null || member.getFamilyId() == null) return result;
 
@@ -374,11 +382,13 @@ public class CircleMatchService {
             LambdaQueryWrapper<Family> familyQw = new LambdaQueryWrapper<Family>()
                     .eq(Family::getTeacherId, teacherId)
                     .ne(Family::getId, member.getFamilyId());
+            SortUtil.applySort(familyQw);
             List<Family> sameTeacherFamilies = familyMapper.selectList(familyQw);
 
             for (Family f : sameTeacherFamilies) {
                 LambdaQueryWrapper<FamilyMember> childQw = new LambdaQueryWrapper<FamilyMember>()
                         .eq(FamilyMember::getFamilyId, f.getId());
+                SortUtil.applySort(childQw);
                 List<FamilyMember> siblings = familyMemberMapper.selectList(childQw);
                 if (!siblings.isEmpty()) {
                     Map<String, Object> circle = getOrCreateRecommendation(
@@ -410,6 +420,7 @@ public class CircleMatchService {
             LambdaQueryWrapper<SocialCircle> qw = new LambdaQueryWrapper<SocialCircle>()
                     .eq(SocialCircle::getSourceId, sourceId)
                     .eq(SocialCircle::getType, type);
+            SortUtil.applySort(qw);
             SocialCircle existing = circleService.getOne(qw);
 
             if (existing != null) {
@@ -449,6 +460,7 @@ public class CircleMatchService {
                 .eq(DanAssessmentResult::getStatus, "completed")
                 .orderByDesc(DanAssessmentResult::getAssessmentDate)
                 .last("LIMIT 1");
+        SortUtil.applySort(qw);
         return danAssessmentResultMapper.selectOne(qw);
     }
 

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CircleService.java

@@ -17,6 +17,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.SortUtil;
 
 @Service
 public class CircleService extends ServiceImpl<SocialCircleMapper, SocialCircle> {
@@ -36,6 +37,7 @@ public class CircleService extends ServiceImpl<SocialCircleMapper, SocialCircle>
         LambdaQueryWrapper<SocialCircleMember> mw = new LambdaQueryWrapper<SocialCircleMember>()
                 .eq(SocialCircleMember::getMemberId, memberId)
                 .eq(SocialCircleMember::getMemberType, memberType);
+        SortUtil.applySort(mw);
         List<SocialCircleMember> myMemberships = socialCircleMemberMapper.selectList(mw);
 
         if (myMemberships.isEmpty()) {
@@ -49,6 +51,7 @@ public class CircleService extends ServiceImpl<SocialCircleMapper, SocialCircle>
         // 查询圈子详情
         LambdaQueryWrapper<SocialCircle> cw = new LambdaQueryWrapper<SocialCircle>()
                 .in(SocialCircle::getId, circleIds);
+        SortUtil.applySort(cw);
         List<SocialCircle> circles = this.list(cw);
 
         // 组装结果
@@ -79,6 +82,7 @@ public class CircleService extends ServiceImpl<SocialCircleMapper, SocialCircle>
         LambdaQueryWrapper<SocialCircleMember> mw = new LambdaQueryWrapper<SocialCircleMember>()
                 .eq(SocialCircleMember::getMemberId, memberId)
                 .eq(SocialCircleMember::getMemberType, "child");
+        SortUtil.applySort(mw);
         List<SocialCircleMember> existing = socialCircleMemberMapper.selectList(mw);
         List<Long> joinedCircleIds = existing.stream()
                 .map(SocialCircleMember::getCircleId)
@@ -107,6 +111,7 @@ public class CircleService extends ServiceImpl<SocialCircleMapper, SocialCircle>
         LambdaQueryWrapper<SocialCircleMember> check = new LambdaQueryWrapper<SocialCircleMember>()
                 .eq(SocialCircleMember::getCircleId, circleId)
                 .eq(SocialCircleMember::getMemberId, memberId);
+        SortUtil.applySort(check);
         SocialCircleMember existing = socialCircleMemberMapper.selectOne(check);
         if (existing != null) {
             return true; // 已加入,视为成功
@@ -140,6 +145,7 @@ public class CircleService extends ServiceImpl<SocialCircleMapper, SocialCircle>
                 .eq(SocialCircleMember::getCircleId, circleId)
                 .eq(SocialCircleMember::getMemberId, memberId)
                 .eq(SocialCircleMember::getMemberType, memberType);
+        SortUtil.applySort(wrapper);
         int deleted = socialCircleMemberMapper.delete(wrapper);
         if (deleted > 0) {
             // 更新圈子成员数

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CognitiveService.java

@@ -12,6 +12,7 @@ import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
 import java.util.*;
+import com.etotem.cfc.util.SortUtil;
 
 /**
  * 认知维度服务 — 六维认知体系(感知、专注、记忆、逻辑、空间、加工速度)
@@ -41,6 +42,7 @@ public class CognitiveService {
                 .eq(DanAssessmentResult::getStatus, "completed")
                 .orderByDesc(DanAssessmentResult::getAssessmentDate)
                 .last("LIMIT 1");
+        SortUtil.applySort(wrapper);
         return danAssessmentResultMapper.selectOne(wrapper);
     }
 

+ 5 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CommissionDistService.java

@@ -11,6 +11,7 @@ import org.springframework.transaction.annotation.Transactional;
 import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
+import com.etotem.cfc.util.SortUtil;
 
 /**
  * P点分润服务
@@ -79,6 +80,7 @@ public class CommissionDistService {
                 .eq(ReferralTree::getChildId, buyerId)
                 .in(ReferralTree::getLevel, 1, 2)
                 .orderByAsc(ReferralTree::getLevel);
+        SortUtil.applySort(wrapper);
         List<ReferralTree> referrals = referralTreeMapper.selectList(wrapper);
 
         if (referrals == null || referrals.isEmpty()) {
@@ -173,6 +175,7 @@ public class CommissionDistService {
                 .or()
                 .eq(CommissionDistLog::getLevel2UserId, userId)
                 .orderByDesc(CommissionDistLog::getCreatedAt);
+        SortUtil.applySort(wrapper);
         return commissionDistLogMapper.selectList(wrapper);
     }
 
@@ -184,6 +187,7 @@ public class CommissionDistService {
                 .eq(CommissionDistLog::getStatus, "completed")
                 .and(w -> w.eq(CommissionDistLog::getLevel1UserId, userId)
                         .or().eq(CommissionDistLog::getLevel2UserId, userId));
+        SortUtil.applySort(wrapper);
         List<CommissionDistLog> list = commissionDistLogMapper.selectList(wrapper);
         return list.stream()
                 .mapToInt(r -> {
@@ -211,6 +215,7 @@ public class CommissionDistService {
             wrapper.and(w -> w.eq(CommissionDistLog::getLevel1UserId, levelUserId)
                     .or().eq(CommissionDistLog::getLevel2UserId, levelUserId));
         }
+        SortUtil.applySort(wrapper);
         return commissionDistLogMapper.selectPage(pageParam, wrapper);
     }
 }

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java

@@ -29,6 +29,7 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Random;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.SortUtil;
 
 @Service
 public class CommissionService {
@@ -428,6 +429,7 @@ public class CommissionService {
             wrapper.eq(CommissionRecord::getLevel, 2);
         }
 
+        SortUtil.applySort(wrapper);
         List<CommissionRecord> allRecords = commissionRecordMapper.selectList(wrapper);
 
         Map<Long, Map<String, Object>> memberMap = new LinkedHashMap<>();
@@ -462,6 +464,7 @@ public class CommissionService {
             } else if ("L2".equals(level)) {
                 orderWrapper.eq(CommissionRecord::getLevel, 2);
             }
+            SortUtil.applySort(orderWrapper);
             Long orderCount = commissionRecordMapper.selectCount(orderWrapper);
             memberInfo.put("orderCount", orderCount != null ? orderCount.intValue() : 0);
 
@@ -680,6 +683,7 @@ public class CommissionService {
 
         LambdaQueryWrapper<CommissionRecord> purchaseWrapper = new LambdaQueryWrapper<CommissionRecord>()
                 .eq(CommissionRecord::getReferrerId, userId);
+        SortUtil.applySort(purchaseWrapper);
         List<CommissionRecord> allRecords = commissionRecordMapper.selectList(purchaseWrapper);
 
         long distinctBuyers = allRecords.stream()

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CompatibilityService.java

@@ -15,6 +15,7 @@ import org.springframework.stereotype.Service;
 import javax.annotation.Resource;
 import java.util.*;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.SortUtil;
 
 @Service
 public class CompatibilityService {
@@ -97,6 +98,7 @@ public class CompatibilityService {
         // Get all family members
         LambdaQueryWrapper<FamilyMember> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(FamilyMember::getFamilyId, familyId);
+        SortUtil.applySort(wrapper);
         List<FamilyMember> members = familyMemberMapper.selectList(wrapper);
         if (members.isEmpty()) return dashboard;
 
@@ -145,12 +147,14 @@ public class CompatibilityService {
         // Look up both orderings
         qw.eq(ZodiacPairing::getZodiacCode1, zodiac1);
         qw.eq(ZodiacPairing::getZodiacCode2, zodiac2);
+        SortUtil.applySort(qw);
         ZodiacPairing pairing = zodiacPairingMapper.selectOne(qw);
         if (pairing == null) {
             // Try reverse order
             LambdaQueryWrapper<ZodiacPairing> reverseQw = new LambdaQueryWrapper<>();
             reverseQw.eq(ZodiacPairing::getZodiacCode1, zodiac2);
             reverseQw.eq(ZodiacPairing::getZodiacCode2, zodiac1);
+            SortUtil.applySort(reverseQw);
             pairing = zodiacPairingMapper.selectOne(reverseQw);
         }
 
@@ -240,11 +244,13 @@ public class CompatibilityService {
         LambdaQueryWrapper<BloodTypePairing> qw = new LambdaQueryWrapper<>();
         qw.eq(BloodTypePairing::getBloodType1, bt1);
         qw.eq(BloodTypePairing::getBloodType2, bt2);
+        SortUtil.applySort(qw);
         BloodTypePairing pairing = bloodTypePairingMapper.selectOne(qw);
         if (pairing == null) {
             LambdaQueryWrapper<BloodTypePairing> reverseQw = new LambdaQueryWrapper<>();
             reverseQw.eq(BloodTypePairing::getBloodType1, bt2);
             reverseQw.eq(BloodTypePairing::getBloodType2, bt1);
+            SortUtil.applySort(reverseQw);
             pairing = bloodTypePairingMapper.selectOne(reverseQw);
         }
 
@@ -413,11 +419,14 @@ public class CompatibilityService {
             LambdaQueryWrapper<ZodiacPairing> qw = new LambdaQueryWrapper<>();
             qw.eq(ZodiacPairing::getZodiacCode1, m1.getZodiac());
             qw.eq(ZodiacPairing::getZodiacCode2, m2.getZodiac());
+            SortUtil.applySort(qw);
             ZodiacPairing p = zodiacPairingMapper.selectOne(qw);
             if (p == null) {
                 LambdaQueryWrapper<ZodiacPairing> rqw = new LambdaQueryWrapper<>();
                 rqw.eq(ZodiacPairing::getZodiacCode1, m2.getZodiac());
                 rqw.eq(ZodiacPairing::getZodiacCode2, m1.getZodiac());
+                SortUtil.applySort(qw);
+                SortUtil.applySort(rqw);
                 p = zodiacPairingMapper.selectOne(rqw);
             }
             if (p != null) {

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

@@ -14,6 +14,7 @@ import java.util.Calendar;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @Service
 public class ContactService {
@@ -25,6 +26,7 @@ public class ContactService {
         LambdaQueryWrapper<Contact> query = new LambdaQueryWrapper<Contact>()
             .eq(Contact::getUserId, userId)
             .orderByDesc(Contact::getIntimacyLevel);
+        SortUtil.applySort(query);
         List<Contact> list = contactMapper.selectList(query);
         return Result.success(toDTOList(list));
     }
@@ -80,6 +82,7 @@ public class ContactService {
         LambdaQueryWrapper<Contact> query = new LambdaQueryWrapper<Contact>()
             .eq(Contact::getUserId, userId)
             .eq(Contact::getPhone, phone);
+        SortUtil.applySort(query);
         Contact existing = contactMapper.selectOne(query);
         if (existing != null) {
             return Result.error("该联系人已在您的关系列表中");

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ContentRecommendService.java

@@ -12,6 +12,7 @@ import org.springframework.stereotype.Service;
 import javax.annotation.Resource;
 import java.util.*;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.SortUtil;
 
 @Slf4j
 @Service
@@ -125,6 +126,7 @@ public class ContentRecommendService {
                     .or().like(Article::getRelatedDimensions, dimensionCode.trim()));
         }
 
+        SortUtil.applySort(wrapper);
         List<Article> allArticles = articleMapper.selectList(wrapper);
         if (allArticles.isEmpty()) return new ArrayList<>();
 
@@ -217,6 +219,7 @@ public class ContentRecommendService {
                     .or().like(Article::getRelatedDimensions, dimensionCode.trim()));
         }
 
+        SortUtil.applySort(articleWrapper);
         List<Article> articles = articleMapper.selectList(articleWrapper);
         for (Article a : articles) {
             if (excludeIds.contains(a.getId())) continue;
@@ -264,6 +267,7 @@ private List<RecommendItemVO> recommendProducts(String dimensionCode, Long userI
                 .or().eq(Product::getDomain, dimensionCode.trim()));
         }
 
+        SortUtil.applySort(wrapper);
         List<Product> allProducts = productMapper.selectList(wrapper);
         if (allProducts.isEmpty()) return new ArrayList<>();
 
@@ -289,6 +293,7 @@ private List<RecommendItemVO> recommendProducts(String dimensionCode, Long userI
             } else {
                 orderWrapper.eq(ProductOrder::getBuyerId, userId);
             }
+            SortUtil.applySort(orderWrapper);
             userOrders = productOrderMapper.selectList(orderWrapper);
         }
 
@@ -366,6 +371,7 @@ private List<RecommendItemVO> recommendProducts(String dimensionCode, Long userI
                 .or().eq(Activity::getDimensionCode, dimensionCode.trim()));
         }
 
+        SortUtil.applySort(wrapper);
         List<Activity> allActivities = activityMapper.selectList(wrapper);
         if (allActivities.isEmpty()) return new ArrayList<>();
 
@@ -486,6 +492,7 @@ private List<RecommendItemVO> recommendProducts(String dimensionCode, Long userI
         } else {
             return ids;
         }
+        SortUtil.applySort(wrapper);
         List<ProductOrder> orders = productOrderMapper.selectList(wrapper);
         for (ProductOrder o : orders) {
             ids.add(o.getProductId());

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

@@ -19,6 +19,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.SortUtil;
 
 @Service
 public class DanKnowledgeBaseService extends ServiceImpl<DanKnowledgeBaseMapper, DanKnowledgeBase> {
@@ -47,6 +48,8 @@ public class DanKnowledgeBaseService extends ServiceImpl<DanKnowledgeBaseMapper,
         }
         wrapper.orderByAsc(DanKnowledgeBase::getSort);
         this.page(p, wrapper);
+
+        SortUtil.applySort(wrapper);
         Map<String, Object> data = new HashMap<>();
         data.put("records", p.getRecords());
         data.put("total", p.getTotal());

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DanReportUploadService.java

@@ -24,6 +24,7 @@ import java.util.Date;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @Service
 public class DanReportUploadService {
@@ -270,6 +271,7 @@ if (!DanReportUpload.DIM_MIND.equals(dimension) && !DanReportUpload.DIM_WISDOM.e
         if (dimension != null && !dimension.isEmpty()) {
             q.eq(DanReportUpload::getDimension, dimension);
         }
+        SortUtil.applySort(q);
         List<DanReportUpload> list = uploadMapper.selectList(q);
         java.util.List<DanReportUploadVO> result = new java.util.ArrayList<>();
         for (DanReportUpload u : list) {

+ 45 - 16
cfc-backend/src/main/java/com/etotem/cfc/service/DietRecordService.java

@@ -4,8 +4,10 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.dto.RecognizeFoodResult;
 import com.etotem.cfc.entity.DietRecord;
 import com.etotem.cfc.entity.DietRecordItem;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.mapper.DietRecordItemMapper;
 import com.etotem.cfc.mapper.DietRecordMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
@@ -16,6 +18,7 @@ import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
+import com.etotem.cfc.util.SortUtil;
 
 @Slf4j
 @Service
@@ -27,6 +30,9 @@ public class DietRecordService {
     @Resource
     private DietRecordItemMapper dietRecordItemMapper;
 
+    @Resource
+    private FamilyMemberMapper familyMemberMapper;
+
     @Resource
     private AiGateway aiGateway;
 
@@ -84,40 +90,63 @@ public class DietRecordService {
         return result;
     }
 
-    public Map<String, Object> getDailySummary(Long memberId, String dateStr) {
+    public Map<String, Object> getDailySummary(Long userId, Long memberId, String dateStr) {
+        memberId = resolveMemberId(userId, memberId);
         LocalDate date = LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
-        
-        LambdaQueryWrapper<DietRecord> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(DietRecord::getMemberId, memberId)
-               .eq(DietRecord::getRecordDate, date);
-        List<DietRecord> records = dietRecordMapper.selectList(wrapper);
-        
+
         Map<String, Object> summary = new HashMap<>();
         summary.put("date", dateStr);
-        summary.put("records", records);
+        summary.put("records", java.util.Collections.emptyList());
         summary.put("totalCalories", 0);
         summary.put("totalProtein", 0);
         summary.put("totalCarbs", 0);
-        
+        if (memberId == null) return summary;
+
+        LambdaQueryWrapper<DietRecord> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(DietRecord::getMemberId, memberId)
+               .eq(DietRecord::getRecordDate, date);
+        SortUtil.applySort(wrapper);
+        List<DietRecord> records = dietRecordMapper.selectList(wrapper);
+        summary.put("records", records);
         return summary;
     }
 
-    public Map<String, Object> getWeeklyTrend(Long memberId, String startDateStr) {
+    public Map<String, Object> getWeeklyTrend(Long userId, Long memberId, String startDateStr) {
+        memberId = resolveMemberId(userId, memberId);
         LocalDate startDate = LocalDate.parse(startDateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
         LocalDate endDate = startDate.plusDays(6);
-        
+
+        Map<String, Object> trend = new HashMap<>();
+        trend.put("start_date", startDateStr);
+        trend.put("end_date", endDate.toString());
+        trend.put("records", java.util.Collections.emptyList());
+        trend.put("days", 0);
+        if (memberId == null) return trend;
+
         LambdaQueryWrapper<DietRecord> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(DietRecord::getMemberId, memberId)
                .ge(DietRecord::getRecordDate, startDate)
                .le(DietRecord::getRecordDate, endDate);
+        SortUtil.applySort(wrapper);
         List<DietRecord> records = dietRecordMapper.selectList(wrapper);
-        
-        Map<String, Object> trend = new HashMap<>();
-        trend.put("start_date", startDateStr);
-        trend.put("end_date", endDate.toString());
         trend.put("records", records);
         trend.put("days", records.size());
-        
         return trend;
     }
+
+    private Long resolveMemberId(Long userId, Long memberId) {
+        if (memberId != null) return memberId;
+        if (userId == null) return null;
+        try {
+            FamilyMember self = familyMemberMapper.selectOne(
+                new LambdaQueryWrapper<FamilyMember>()
+                    .eq(FamilyMember::getUserId, userId)
+                    .last("LIMIT 1")
+            );
+            return self != null ? self.getId() : null;
+        } catch (Exception e) {
+            log.warn("根据userId反查family_members失败: userId={}, error={}", userId, e.getMessage());
+            return null;
+        }
+    }
 }

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

@@ -10,6 +10,7 @@ import javax.annotation.Resource;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
+import com.etotem.cfc.util.SortUtil;
 
 @Service
 public class DiscountService {

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

@@ -12,6 +12,7 @@ import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
 import java.util.*;
+import com.etotem.cfc.util.SortUtil;
 
 @Service
 public class DistributionService {

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/EcomSupplierService.java

@@ -12,6 +12,7 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @Service
 public class EcomSupplierService {
@@ -26,6 +27,7 @@ public class EcomSupplierService {
         if (keyword != null && !keyword.trim().isEmpty()) {
             wrapper.like(EcomSupplier::getName, keyword.trim());
         }
+        SortUtil.applySort(wrapper);
         Page<EcomSupplier> result = supplierMapper.selectPage(pageParam, wrapper);
         Map<String, Object> data = new HashMap<>();
         data.put("records", result.getRecords());

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/EmotionAlertService.java

@@ -16,6 +16,7 @@ import org.springframework.stereotype.Service;
 import javax.annotation.Resource;
 import java.util.*;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.SortUtil;
 
 /**
  * 情绪风险告警服务 —— 在每次情绪打卡完成后自动检查风险模式:
@@ -145,6 +146,7 @@ public class EmotionAlertService {
         LambdaQueryWrapper<EmotionAlert> wrapper = new LambdaQueryWrapper<EmotionAlert>()
                 .eq(resolved != null, EmotionAlert::getResolved, resolved)
                 .orderByDesc(EmotionAlert::getCreatedAt);
+        SortUtil.applySort(wrapper);
         List<EmotionAlert> all = emotionAlertMapper.selectList(wrapper);
         int total = all.size();
         int fromIndex = (page - 1) * size;

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

@@ -19,6 +19,7 @@ import org.springframework.transaction.annotation.Transactional;
 import javax.annotation.Resource;
 import java.util.*;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.SortUtil;
 
 @Slf4j
 @Service
@@ -88,6 +89,7 @@ public class EmotionCheckinService {
         LambdaQueryWrapper<EmotionCheckin> wrapper = new LambdaQueryWrapper<EmotionCheckin>()
                 .eq(EmotionCheckin::getChildId, childId)
                 .orderByDesc(EmotionCheckin::getCheckinDate);
+        SortUtil.applySort(wrapper);
         List<EmotionCheckin> list = emotionCheckinMapper.selectList(wrapper);
 
         FamilyMember child = familyMemberMapper.selectById(childId);
@@ -110,6 +112,7 @@ public class EmotionCheckinService {
                 .eq(EmotionCheckin::getChildId, childId)
                 .orderByDesc(EmotionCheckin::getCreatedAt)
                 .last("LIMIT 1");
+        SortUtil.applySort(wrapper);
         EmotionCheckin checkin = emotionCheckinMapper.selectOne(wrapper);
         if (checkin == null) return null;
 
@@ -123,6 +126,7 @@ public class EmotionCheckinService {
         LambdaQueryWrapper<EmotionCheckin> wrapper = new LambdaQueryWrapper<EmotionCheckin>()
                 .eq(EmotionCheckin::getChildId, childId)
                 .orderByDesc(EmotionCheckin::getCheckinDate);
+        SortUtil.applySort(wrapper);
         List<EmotionCheckin> all = emotionCheckinMapper.selectList(wrapper);
 
         Calendar cal = Calendar.getInstance();
@@ -200,6 +204,7 @@ public class EmotionCheckinService {
         LambdaQueryWrapper<EmotionCheckin> wrapper = new LambdaQueryWrapper<EmotionCheckin>()
                 .eq(EmotionCheckin::getChildId, childId)
                 .orderByDesc(EmotionCheckin::getCheckinDate);
+        SortUtil.applySort(wrapper);
         List<EmotionCheckin> all = emotionCheckinMapper.selectList(wrapper);
 
         Map<String, Integer> breakdown = new HashMap<>();
@@ -229,6 +234,7 @@ public class EmotionCheckinService {
         LambdaQueryWrapper<EmotionCheckin> wrapper = new LambdaQueryWrapper<EmotionCheckin>()
                 .eq(EmotionCheckin::getChildId, childId)
                 .orderByDesc(EmotionCheckin::getCheckinDate);
+        SortUtil.applySort(wrapper);
         List<EmotionCheckin> all = emotionCheckinMapper.selectList(wrapper);
 
         Calendar cal = Calendar.getInstance();

+ 45 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java

@@ -17,6 +17,7 @@ import javax.annotation.Resource;
 
 import java.util.*;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.SortUtil;
 
 /**
  * 家庭能量沙盘 — 核心计算服务
@@ -154,6 +155,39 @@ public class EnergyService {
         return dto;
     }
 
+    /**
+     * 计算当前用户个人能量沙盘(只含用户本人,不聚合家庭)
+     */
+    public EnergySandboxDTO calculatePersonalEnergy(Long userId) {
+        if (userId == null) {
+            return buildEmptyDTO();
+        }
+        try {
+            User user = userMapper.selectById(userId);
+            if (user == null) {
+                return buildEmptyDTO();
+            }
+            EnergySandboxDTO dto = new EnergySandboxDTO();
+            List<MemberEnergyDTO> members = new ArrayList<>();
+            MemberEnergyDTO me = calcParentEnergy(user);
+            members.add(me);
+            dto.setMembers(members);
+            dto.setBodyScore(safeScore(me.getBodyScore()));
+            dto.setMindScore(safeScore(me.getMindScore()));
+            dto.setWisdomScore(safeScore(me.getWisdomScore()));
+            dto.setActionScore(safeScore(me.getActionScore()));
+            dto.setWealthScore(safeScore(me.getWealthScore()));
+            int overall = (safeScore(me.getBodyScore()) + safeScore(me.getMindScore())
+                    + safeScore(me.getWisdomScore()) + safeScore(me.getActionScore())
+                    + safeScore(me.getWealthScore())) / 5;
+            dto.setOverallScore(overall);
+            return dto;
+        } catch (Exception e) {
+            log.error("计算个人能量异常, userId={}: {}", userId, e.getMessage(), e);
+            return buildEmptyDTO();
+        }
+    }
+
     private void persistMemberScores(Long memberId, String memberType, Long familyId,
                                      MemberEnergyDTO member, Date assessedAt) {
         recordDimension(memberId, memberType, familyId, "body", member.getBodyScore(), assessedAt);
@@ -234,6 +268,7 @@ public class EnergyService {
                         .last("LIMIT 1");
                 HealthReport report = null;
                 try {
+                    SortUtil.applySort(hrWrapper);
                     report = healthReportMapper.selectOne(hrWrapper);
                 } catch (Exception e) {
                     log.warn("健康报告查询异常, userIds={}: {}", childUserIds, e.getMessage());
@@ -310,6 +345,7 @@ public class EnergyService {
                 .eq(Task::getExecutorType, "parent")
                 .gt(Task::getCreatedAt, cal.getTime())
                 .ne(Task::getIsTemplate, 1);
+        SortUtil.applySort(wrapper);
         List<Task> tasks = taskMapper.selectList(wrapper);
         if (tasks.isEmpty()) return 0;
         long completed = tasks.stream().filter(t -> "completed".equals(t.getStatus())).count();
@@ -536,6 +572,7 @@ public class EnergyService {
                 .eq(DanAssessmentResult::getStatus, "completed")
                 .orderByDesc(DanAssessmentResult::getAssessmentDate)
                 .last("LIMIT 1");
+        SortUtil.applySort(qw);
         return danAssessmentResultMapper.selectOne(qw);
     }
 
@@ -569,6 +606,7 @@ public class EnergyService {
         // Game performance (±5%)
         LambdaQueryWrapper<GameRecord> gw = new LambdaQueryWrapper<GameRecord>()
                 .eq(GameRecord::getChildId, memberId);
+        SortUtil.applySort(gw);
         List<GameRecord> games = gameRecordMapper.selectList(gw);
         if (!games.isEmpty()) {
             double avgScore = games.stream()
@@ -592,6 +630,7 @@ public class EnergyService {
         // 1. 游戏得分 (0-100, 平均分)
         LambdaQueryWrapper<GameRecord> gw = new LambdaQueryWrapper<GameRecord>()
                 .eq(GameRecord::getChildId, memberId);
+        SortUtil.applySort(gw);
         List<GameRecord> games = gameRecordMapper.selectList(gw);
         if (!games.isEmpty()) {
             double avgScore = games.stream()
@@ -641,6 +680,7 @@ public class EnergyService {
         LambdaQueryWrapper<StreakMilestone> mw = new LambdaQueryWrapper<StreakMilestone>()
                 .orderByDesc(StreakMilestone::getDays)
                 .last("LIMIT 1");
+        SortUtil.applySort(mw);
         List<StreakMilestone> milestones = streakMilestoneMapper.selectList(mw);
         int maxMilestone = 30;
         if (!milestones.isEmpty() && milestones.get(0).getDays() != null) {
@@ -676,6 +716,7 @@ public class EnergyService {
                 .eq(DanAssessmentResult::getStatus, "completed")
                 .orderByDesc(DanAssessmentResult::getAssessmentDate)
                 .last("LIMIT 3");
+        SortUtil.applySort(qw);
         List<DanAssessmentResult> results = danAssessmentResultMapper.selectList(qw);
         if (results.isEmpty()) return 0;
 
@@ -715,6 +756,7 @@ public class EnergyService {
                 .eq(PointsLog::getChildId, memberId)
                 .gt(PointsLog::getAmount, 0)
                 .gt(PointsLog::getCreatedAt, oneYearAgo);
+        SortUtil.applySort(earnedWrapper);
         List<PointsLog> earnedLogs = pointsLogMapper.selectList(earnedWrapper);
         int earned = earnedLogs.stream()
                 .mapToInt(pl -> pl.getAmount() != null ? pl.getAmount() : 0)
@@ -725,6 +767,7 @@ public class EnergyService {
                 .ne(Task::getIsTemplate, 1)
                 .ne(Task::getStatus, "cancelled")
                 .gt(Task::getCreatedAt, oneYearAgo);
+        SortUtil.applySort(taskWrapper);
         List<Task> tasks = taskMapper.selectList(taskWrapper);
         int available = tasks.stream()
                 .mapToInt(t -> t.getPoints() != null ? t.getPoints() : 0)
@@ -1071,6 +1114,7 @@ public class EnergyService {
                 .ne(Task::getStatus, "cancelled")
                 .gt(Task::getCreatedAt, since);
 
+        SortUtil.applySort(wrapper);
         List<Task> tasks = taskMapper.selectList(wrapper);
 
         long total = tasks.size();
@@ -1455,6 +1499,7 @@ public class EnergyService {
             if (dim != null) wrapper.eq(EnergyLog::getDimensionId, dim.getId());
         }
         wrapper.orderByDesc(EnergyLog::getCreatedAt);
+        SortUtil.applySort(wrapper);
         return energyLogMapper.selectPage(pageParam, wrapper);
     }
 

+ 8 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyChallengeService.java

@@ -18,6 +18,7 @@ import org.springframework.transaction.annotation.Transactional;
 import javax.annotation.Resource;
 import java.text.SimpleDateFormat;
 import java.util.*;
+import com.etotem.cfc.util.SortUtil;
 
 @Slf4j
 @Service
@@ -51,6 +52,7 @@ public class FamilyChallengeService {
                 .eq(FamilyChallenge::getFamilyId, familyId)
                 .in(FamilyChallenge::getStatus, "active", "pending")
                 .orderByDesc(FamilyChallenge::getCreatedAt);
+        SortUtil.applySort(wrapper);
         List<FamilyChallenge> active = familyChallengeMapper.selectList(wrapper);
 
         for (FamilyChallenge c : active) {
@@ -61,6 +63,7 @@ public class FamilyChallengeService {
             }
         }
 
+        SortUtil.applySort(wrapper);
         active = familyChallengeMapper.selectList(wrapper);
 
         fillProgress(active);
@@ -124,6 +127,7 @@ public class FamilyChallengeService {
             // 系统创建:写 participant 行(全员 agreed,与存量迁移口径一致)
             LambdaQueryWrapper<FamilyMember> mw = new LambdaQueryWrapper<FamilyMember>()
                     .eq(FamilyMember::getFamilyId, familyId);
+            SortUtil.applySort(mw);
             List<FamilyMember> members = familyMemberMapper.selectList(mw);
             for (FamilyMember m : members) {
                 FamilyChallengeParticipant participant = new FamilyChallengeParticipant();
@@ -289,6 +293,7 @@ public class FamilyChallengeService {
         LambdaQueryWrapper<FamilyChallengeProgress> wrapper = new LambdaQueryWrapper<FamilyChallengeProgress>()
                 .eq(FamilyChallengeProgress::getChallengeId, challengeId)
                 .eq(FamilyChallengeProgress::getChildId, childId);
+        SortUtil.applySort(wrapper);
         FamilyChallengeProgress progress = familyChallengeProgressMapper.selectOne(wrapper);
 
         // 5. 每日限次:同一 challengeId+childId 当天只能打卡一次(按 updatedAt 判断)
@@ -338,6 +343,7 @@ public class FamilyChallengeService {
         boolean isCompleted = false;
         LambdaQueryWrapper<FamilyChallengeProgress> pw = new LambdaQueryWrapper<FamilyChallengeProgress>()
                 .eq(FamilyChallengeProgress::getChallengeId, challenge.getId());
+        SortUtil.applySort(pw);
         List<FamilyChallengeProgress> progresses = familyChallengeProgressMapper.selectList(pw);
         int totalProgress = 0;
         for (FamilyChallengeProgress p : progresses) {
@@ -418,6 +424,7 @@ public class FamilyChallengeService {
                 .eq(FamilyChallenge::getFamilyId, familyId)
                 .eq(FamilyChallenge::getStatus, "completed")
                 .orderByDesc(FamilyChallenge::getEndDate);
+        SortUtil.applySort(wrapper);
         List<FamilyChallenge> history = familyChallengeMapper.selectList(wrapper);
         fillProgress(history);
         return history;
@@ -435,6 +442,7 @@ public class FamilyChallengeService {
 
             LambdaQueryWrapper<FamilyChallengeProgress> pw = new LambdaQueryWrapper<FamilyChallengeProgress>()
                     .eq(FamilyChallengeProgress::getChallengeId, c.getId());
+            SortUtil.applySort(pw);
             List<FamilyChallengeProgress> progresses = familyChallengeProgressMapper.selectList(pw);
 
             int total = 0;

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyCommissionService.java

@@ -13,6 +13,7 @@ import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.util.Date;
 import java.util.List;
+import com.etotem.cfc.util.SortUtil;
 
 @Service
 public class FamilyCommissionService {
@@ -76,6 +77,7 @@ public class FamilyCommissionService {
         LambdaQueryWrapper<FamilyCommission> wrapper = new LambdaQueryWrapper<FamilyCommission>()
                 .eq(FamilyCommission::getPromoterFamilyId, familyId)
                 .eq(FamilyCommission::getStatus, "paid");
+        SortUtil.applySort(wrapper);
         List<FamilyCommission> list = familyCommissionMapper.selectList(wrapper);
         BigDecimal total = BigDecimal.ZERO;
         for (FamilyCommission c : list) {

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyEarningsService.java

@@ -12,6 +12,7 @@ import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.SortUtil;
 
 @Service
 public class FamilyEarningsService {
@@ -278,6 +279,7 @@ public class FamilyEarningsService {
         }
         
         wrapper.orderByDesc(FamilyEarningsRecord::getCreatedAt);
+        SortUtil.applySort(wrapper);
         return familyEarningsRecordMapper.selectList(wrapper);
     }
     

Vissa filer visades inte eftersom för många filer har ändrats