Explorar o código

Merge branch 'refs/heads/cfclub' into cfclub-jiapu

# Conflicts:
#	cfc-frontend/pages/action-detail/index.vue
#	cfc-frontend/pages/growth-main/index.vue
#	cfc-frontend/pages/mind-detail/index.vue
jiapu hai 2 meses
pai
achega
fa23ae4733
Modificáronse 100 ficheiros con 3509 adicións e 566 borrados
  1. 4 0
      .gitignore
  2. 95 0
      AGENTS.md
  3. 75 0
      articles/D17-draft_v1.md
  4. 61 0
      articles/D18-final.md
  5. 62 0
      articles/D19-final.md
  6. 56 0
      articles/D20-final.md
  7. 54 0
      articles/D21-draft_v1.md
  8. 56 0
      articles/D21-draft_v2.md
  9. 58 0
      articles/D22-draft_v1.md
  10. 405 0
      articles/publishing_guide_report_screenshots.md
  11. 70 1
      cfc-backend/AGENTS.md
  12. 6 0
      cfc-backend/pom.xml
  13. 332 50
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  14. 13 12
      cfc-backend/src/main/java/com/etotem/cfc/controller/ActivityController.java
  15. 6 5
      cfc-backend/src/main/java/com/etotem/cfc/controller/ActivityFeedbackController.java
  16. 4 3
      cfc-backend/src/main/java/com/etotem/cfc/controller/ActivityMaterialController.java
  17. 2 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/ActivityOrderController.java
  18. 9 8
      cfc-backend/src/main/java/com/etotem/cfc/controller/AiQuestionnaireController.java
  19. 9 8
      cfc-backend/src/main/java/com/etotem/cfc/controller/BadgeController.java
  20. 3 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/CircleChallengeController.java
  21. 5 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/ContactController.java
  22. 5 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/ContactMatchController.java
  23. 160 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/ContentFilterController.java
  24. 6 6
      cfc-backend/src/main/java/com/etotem/cfc/controller/CouponController.java
  25. 6 10
      cfc-backend/src/main/java/com/etotem/cfc/controller/DailyCheckinController.java
  26. 4 6
      cfc-backend/src/main/java/com/etotem/cfc/controller/DailyFeedbackController.java
  27. 41 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/DailyTaskController.java
  28. 23 27
      cfc-backend/src/main/java/com/etotem/cfc/controller/DanAssessmentController.java
  29. 2 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/DanReportController.java
  30. 3 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/DistributionInviteController.java
  31. 21 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/EmiReportController.java
  32. 12 22
      cfc-backend/src/main/java/com/etotem/cfc/controller/EnergyController.java
  33. 3 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/FamilyChallengeController.java
  34. 5 8
      cfc-backend/src/main/java/com/etotem/cfc/controller/FamilyPlatformBalanceController.java
  35. 5 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/GameRecordController.java
  36. 3 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthAlertController.java
  37. 5 8
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthCheckinController.java
  38. 3 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthCircleController.java
  39. 3 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthDimensionController.java
  40. 2 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthExerciseController.java
  41. 5 8
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthGrowthRecordController.java
  42. 3 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthInteractionController.java
  43. 2 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthKnowledgeBaseController.java
  44. 15 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthMealController.java
  45. 2 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthMeditationController.java
  46. 2 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthPlanController.java
  47. 163 73
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java
  48. 3 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthScoreController.java
  49. 2 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthSleepController.java
  50. 73 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthStatusController.java
  51. 2 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthTimelineController.java
  52. 2 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthWaterController.java
  53. 3 3
      cfc-backend/src/main/java/com/etotem/cfc/controller/MealRecommendController.java
  54. 3 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java
  55. 5 8
      cfc-backend/src/main/java/com/etotem/cfc/controller/MicroActionController.java
  56. 37 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/MicrobiomeArticleController.java
  57. 96 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/PackageLogisticsController.java
  58. 6 5
      cfc-backend/src/main/java/com/etotem/cfc/controller/PackagePaymentController.java
  59. 4 3
      cfc-backend/src/main/java/com/etotem/cfc/controller/PointsExchangeController.java
  60. 134 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/ProblemDomainController.java
  61. 178 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/ProblemPackageController.java
  62. 451 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/ProblemSurveyController.java
  63. 2 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/PromotionController.java
  64. 3 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/PromotionMaterialController.java
  65. 2 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/RecommendController.java
  66. 2 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/ReportStatsController.java
  67. 5 8
      cfc-backend/src/main/java/com/etotem/cfc/controller/ReportSurveyController.java
  68. 3 6
      cfc-backend/src/main/java/com/etotem/cfc/controller/ReviewController.java
  69. 3 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/ServiceRoleApplicationController.java
  70. 4 3
      cfc-backend/src/main/java/com/etotem/cfc/controller/ShareController.java
  71. 7 9
      cfc-backend/src/main/java/com/etotem/cfc/controller/SurveyController.java
  72. 128 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/UserIntentController.java
  73. 76 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/UserNotificationController.java
  74. 6 5
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminActivityController.java
  75. 2 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminActivityRegistrationController.java
  76. 4 3
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminAfterSalesController.java
  77. 16 15
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java
  78. 4 9
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java
  79. 3 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCategoryController.java
  80. 3 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCfValueController.java
  81. 4 9
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCommissionController.java
  82. 5 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java
  83. 5 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCouponController.java
  84. 3 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminDiscountController.java
  85. 8 7
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminDistributionController.java
  86. 3 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminEnergyBehaviorConfigController.java
  87. 2 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminEnergyConfigController.java
  88. 3 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminEnergyRuleController.java
  89. 4 3
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminFoodController.java
  90. 4 9
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminGrowthArchiveProductController.java
  91. 33 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminGuideController.java
  92. 16 18
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminInventoryController.java
  93. 0 77
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminMenuController.java
  94. 3 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminNoticeController.java
  95. 90 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminNotificationTemplateController.java
  96. 4 3
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminPeriodicServiceController.java
  97. 2 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminPlanRuleController.java
  98. 94 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProblemDomainController.java
  99. 6 15
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductController.java
  100. 59 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductPickerController.java

+ 4 - 0
.gitignore

@@ -50,6 +50,7 @@ worktrees/
 !cfc-frontend/static/*.png
 !cfc-web/public/logo.png
 !web/img/*.png
+!web/img/product-intro/*.png
 !web/articles/*.png
 
 # Python
@@ -67,3 +68,6 @@ venv/
 
 # Codegraph
 .codegraph/
+
+# Skills temp directory
+.superpowers/sdd/

+ 95 - 0
AGENTS.md

@@ -184,6 +184,85 @@ python C:\code\cfc\publish_v11_stable.py
 - ❌ 暴露客户隐私(姓名/学校/家庭住址)
 - ❌ 缺少具体转变过程的描述
 
+## 公众号数据驱动优化指南(基于后台分析)
+
+> 数据来源:`https://mp.weixin.qq.com/misc/appmsganalysis?action=report&type=daily_v2` 内容分析页面
+
+### 流量构成(近30天)
+
+| 渠道 | 占比 | 含义 |
+|------|:----:|------|
+| 朋友圈 | ~33% | 文章有转发价值,主动传播能力 |
+| 搜一搜 | ~30% | 标题关键词SEO匹配度 |
+| 公众号消息 | ~16% | 粉丝主动打开率 |
+| 聊天会话 | ~11% | 点对点分享 |
+| 公众号主页 | ~10% | 关注后/查看主页 |
+| 推荐 | ~4% | 微信推荐系统曝光 |
+| 其它 | ~2% | |
+
+**关键判断**:公众号流量严重依赖**朋友圈分享**和**搜一搜SEO**,粉丝主动打开率低(16%),推荐系统几乎不推荐。
+
+### 标题公式(基于高阅读文章验证)
+
+#### ✅ 高阅读标题特征(30天TOP5)
+
+| 文章 | 阅读 | 特征 |
+|------|:---:|------|
+| 几十亿人看他跳舞,但他23年没刷过一道题 | 91 | 数字+冲突+反差 |
+| 妈妈,我不想上学:38.6%的孩子在替家长喊疼 | 50 | 数字+痛点+数据 |
+| 61.7%孩子用AI,20.5%正在变笨 | 40 | 双数字+颠覆认知 |
+| AI时代,还要让孩子背诵吗? | 36 | 争议性反问 |
+| 当孩子在说「没意思」——四无时代 | 31 | 痛点+概念包装 |
+
+#### 标题模板
+
+```
+[数字] + [冲突/反差] + [悬念/痛点]   →  "61.7%孩子用AI,20.5%正在变笨"
+[场景] + [反转] + [结论]              →  "几十亿人看他跳舞,但他23年没刷过一道题"
+[数据] + [痛点] + [解决方案暗示]       →  "38.6%的孩子在替家长喊疼"
+```
+
+#### ❌ 低阅读标题教训
+
+- 标题过长(>25字):#53「女儿超常发挥50分,妈妈却偷改了她的志愿」32字,仅18阅读
+- 标题偏平无冲突:「好无聊」「溺水是无声的」— 缺乏转发动力
+- 无关键词:标题不含"孩子""家长""教育"等高频搜索词,搜一搜匹配差
+
+#### 标题检查清单
+
+- [ ] 标题 ≤25字符(建议20字以内)
+- [ ] 含数字(百分比/次数/分数等)
+- [ ] 含冲突/反差/反转("你以为...其实...")
+- [ ] 前12字有吸引力(微信推荐展示前12字)
+- [ ] 含高频搜索词(孩子/家长/教育/青春期/暑假等)
+- [ ] 不含"愿每个X"式收束
+
+### 内容类型策略
+
+| 类型 | 建议占比 | 最佳案例 | 特点 |
+|------|:--------:|----------|------|
+| 案例故事型 | 40% | 身体如何塑造心智(91阅读) | 人物+反差,朋友圈传播力最强 |
+| 数据热点型 | 30% | 61.7%孩子用AI(40阅读) | 数据+热点话题,搜一搜匹配高 |
+| 干货实操型 | 20% | 是什么决定学习力(16阅读) | 稳定基本盘,粉丝粘性 |
+| 产品推广型 | 10% | 公益讲座/测评推广 | 控制在≤15% |
+
+### 写作前必查(补充)
+
+除现有自检清单外,额外检查:
+
+- [ ] 标题是否含数字?不含 → 加数字再发布
+- [ ] 标题是否含"孩子""家长"关键词?不含 → 调整标题
+- [ ] 前12字是否足够吸引人?太平淡 → 前移冲突点
+- [ ] 文章是否有"转发价值"?(读了想分享给朋友/家人?)
+
+### 发布后追踪
+
+- 发布后7天,登录 `数据分析 → 内容分析` 查看阅读量
+- 阅读<30 → 标题/选题有问题,下次避免同类
+- 阅读30-60 → 正常水平
+- 阅读>60 → 好标题/好选题,总结可复用的点
+- 关注"推荐"渠道占比:若持续<5%,说明标题缺乏爆点
+
 ## COMMANDS
 
 ```bash
@@ -218,6 +297,22 @@ curl -X POST http://localhost:9082/api/migration/run
 - **DI**: `@Resource`,字段名必须与类型默认 Bean Name 一致
 - **角色控制**: 控制器内手动检查 `@RequestAttribute("role")`
 
+## 时间显示规范
+
+所有页面展示时间必须统一以下两种格式,禁止使用 `toLocaleString()`、`toLocaleTimeString()` 等浏览器差异化输出:
+
+| 使用场景 | 格式 | 示例 |
+|------|------|------|
+| **时间(显示年月日+时分秒)** | `yyyy-MM-dd HH:mm:ss` | `2026-08-15 14:30:00` |
+| **仅日期(去掉后面的时间)** | `yyyy-MM-dd` | `2026-08-15` |
+
+后端统一返回 ISO 8601 格式(`2026-08-15T14:30:00`),前端按场景格式化:
+
+- **需展示时间的场景**(订单时间、支付时间、创建时间、打卡时间等):截取前19位 + 将 T 替换为空格 → `yyyy-MM-dd HH:mm:ss`
+- **仅需日期的场景**(日期筛选、日报等):截取前10位 → `yyyy-MM-dd`
+
+禁止直接使用 `new Date(str).toLocaleString('zh-CN')`(输出格式因浏览器而异)。
+
 ## 小程序限制
 
 - 禁止可选链 `?.`(用 `&&` 替代)

+ 75 - 0
articles/D17-draft_v1.md

@@ -0,0 +1,75 @@
+# D17 孩子专注力差,可能是早上少了这1杯水(v2)
+
+> 智维度 #6366F1 | 参考《家的算法》第6章 智·赋能未来
+> 系列:水主题·之二(D16 你不敢体检→水污染;D17 喝够水给大脑供能;埋钩 D18 净水机怎么选)
+> 维度轮换:D16 身→D17 智(避免连续同维度)
+> 风格:个人记事(冻龄妈妈家的算法·个人号)
+> 调整:v2→v3 加入"自己加工的自来水是首选,但选净水机要慎重"理念(埋钩 D18);图片 7张→5张
+
+## 标题
+孩子专注力差,可能是早上少了这1杯水(19字,数字清单型)
+
+## 正文(约 760 字)
+
+老师又跟你说,孩子上课走神、注意力不集中。你的第一反应:手机玩多了?睡得不够?补点 DHA?核桃加量?
+
+我家俩娃都遇到过。我以前也以为是营养不够,DHA 核桃轮着补。直到走神越来越频繁,我才认真想:到底哪里没做对?
+
+不是别的,是水。
+
+说出来你别笑。我家俩娃都不爱喝水,老二宁可灌一肚子酸奶饮料,也不碰白开水。
+
+后来我特意去翻了《中国居民膳食指南 2022》:青少年每天饮水 1100 到 1300 毫升。大脑 73% 是水,缺水 1% 到 2%,注意力就掉。
+
+所以孩子不是不爱喝水,是没意识到缺水。"口渴"不靠谱。早晨更严重:睡一夜蒸发几百毫升水,起床血液最稠。一杯水下去,整个上午都不一样。
+
+但比"不喝"更要命的是"喝错"。
+
+小区里几个不爱喝水的娃,书包里塞的全是奶茶、果汁、酸奶。我一开始以为就是有糖的水,查了资料才发现:糖只是其一,伤害有三重。
+
+第一是糖。一瓶 500ml 奶茶约 50 克糖,世卫建议每天不超过 25 克——一瓶就超标。
+
+第二是酸。饮料 pH 2.5 到 4,长期喝改变口腔和肠道环境,蛀牙是表面,菌群也遭殃。
+
+第三是气泡。二氧化碳加速胃排空,感觉"喝进去了",水还没吸收就被冲走——越喝越渴。
+
+糖伤代谢,酸伤菌群,气泡越喝越渴。加在一起,比单喝糖水糟糕得多。
+
+那喝什么?《指南》说得朴素:白开水是首选——自己加工的自来水,过滤完当天喝完,新鲜、便宜、可控。
+
+但净水机别乱买,选不好反而添乱。为什么?我以后专门说。
+
+身体是学习的底盘,水是身体的底盘。底盘稳,大脑才转得动。
+
+三个动作,今晚就能做。
+
+第一,晨起一杯 200 毫升白开水,放凉喝掉。
+
+第二,书包放一瓶 500 毫升水,上午下午各半瓶,定时定量。
+
+第三,把家里的碳酸饮料、果汁清掉,别让孩子看见。
+
+我自己试了三个月:晨起一杯水、上午半瓶水,老师反馈上课眼神不一样了,孩子也说没那么困。
+
+今晚回去给孩子书包放一瓶水,明早陪他喝一杯。评论区告诉我:你家一天喝什么水?
+
+## 标签
+#主动健康 #赋能未来 #专注力 #孩子学习 #青少年饮水 #家庭教育 #亲子陪伴
+
+## 20秒动作
+晨起第一杯水 200ml(白开水,从家用净水设备里倒)
+
+## 关键数据
+- 大脑 73% 是水(神经解剖学共识)
+- 缺水 1-2% 影响短期记忆和注意力(多项人体研究,宾夕法尼亚大学 2012 等)
+- 《中国居民膳食指南 2022》:青少年每天饮水 1100-1300 毫升;日常首选白开水
+- WHO:成年人每天添加糖最好不超过 25 克
+- 碳酸饮料/果汁 pH 2.5-4,长期喝改变口腔和肠道酸碱环境,影响菌群
+
+## 图片规划(5张)
+
+Slide 1: 封面 | 孩子专注力差 / 可能是早上少了这1杯水 | 智·赋能未来 | #6366F1
+Slide 2: 真相 | 大脑73%是水 / 缺水1-2%注意力就掉 | 数据锚点
+Slide 3: 危害 | 糖伤代谢 酸伤菌群 气泡越喝越渴 | 饮料三害一图讲完
+Slide 4: 方案 | 白开水首选=自己加工的自来水 / 三个动作 | 埋钩:净水机别乱买,为什么以后说
+Slide 5: CTA | 你家一天喝什么水 / 评论区告诉我 | CTA

+ 61 - 0
articles/D18-final.md

@@ -0,0 +1,61 @@
+# D18 - 净水机是不是智商税?用了十几年我才想通
+
+**维度**: 富维度 (#F59E0B 金色)
+**发布时间**: 2026-08-06 19:30 (定时发布)
+**状态**: ✅ 已发布(用户已修改)
+
+## 标题
+净水机是不是智商税?用了十几年我才想通
+
+## 正文(最终版 - 用户修改后)
+
+这个事吧,我纠结了十几年。
+
+十几年前,我就花八千多买过一台净水机。刚开始用的时候觉得真好,水甜了,壶里也没水垢了,心里还挺得意。结果用了半年,客服就开始打电话了——该换滤芯了。一问价,三个全换下来又得一两千。
+
+我当时心想,这不就是耗材陷阱吗,跟打印机一个套路。机器卖你便宜,赚的是耗材的钱。
+
+后来我换桶装水。一桶18块左右,一周两桶,一年下来也得几千。而且这桶还得常换,旧桶老板拉回去,你也不知道洗没洗干净。有次我出于好奇,看了下饮水机里面——说实话,看完那一眼,我三天没喝下去水。
+
+而且最近几年又研究出来微塑料的问题。说桶装水、瓶装水里检测出微塑料,每升能到几十万个。桶装水的水桶反复用,运输储存中,塑料微粒可能也进到水里,一周喝进去一张银行卡,吓人。
+
+你看,烧开水、桶装水、净水机,三条路我都走过,没一个让我心里踏实。
+
+所以到底喝什么水才放心?我认真研究了好几年,今天跟你掏心窝子说说。
+
+先说个误区。以前我们习惯看烧水壶有没有水垢,有水垢就觉得水不干净。这个说法流传了很久,其实不太对。水垢是矿物质沉淀,跟水质好坏不是一回事。
+
+那看水质看什么?两个更值得看的指标:pH值和TDS。
+
+TDS就是水里溶解物的总量。有的净水机过滤得特别干净,TDS直接干到0。听着很厉害,但TDS是0的水,pH是偏酸性的。你每天喝的,已经是饮料级别的了。
+
+所以选净水机,不能光看滤得干不干净,要看它保留了什么东西。也不能光看机器贵不贵,要看滤芯换得起换不起。不换滤芯的净水机,就是个摆设。十几年前那台,我当前就是懒得换才又换桶装水的。
+
+研究了一圈,我最后买了前两天文章里提过的那种功能水机。算经济账的话,一天7、8块,并不比一般的净水机和桶装水贵。但它有一些别的水都做不到的事——11.5强碱去污去农残、2.5强酸消毒、6.0美容紧肤、7.0吃药、8.5泡茶、9.0做饭煲汤、9.5直饮抗氧化。一台机器管全家所有的水。
+
+这台机器我买之前看了半年,就怕又掉耗材陷阱里。仔细研究了一下,它的核心部件是电解槽,不是滤芯,没有"用多久必须换"这种限制。
+
+所以值不值,不能光算花了多少钱,要算花同样的钱解决了多少问题。
+
+有一句话挺经典的:买贵的,因为除了贵什么缺点都没有。买便宜的,除了便宜什么优点都没有。
+
+不是买便宜货就省钱,真的。
+
+你家喝水是烧开的、桶装的、还是净水机?评论区聊聊。
+
+#净水器# #健康从水开始# #自来水过滤# #生活饮用水# #品质净水健康生活#
+
+## 与 v8 初稿对比(用户改了什么)
+
+| 位置 | v8 初稿 | 最终版(用户改) |
+|------|---------|-----------------|
+| 标题 | ...我终于想通了 | **...我才想通** |
+| 时间 | 用了不到一年 | **用了半年** |
+| 微塑料 | 一句带过 | **加 "一周喝进去一张银行卡,吓人"** |
+| 桶卫生 | 看了下那桶里面的卫生 | **看了下饮水机里面**(更自然) |
+| 成本对比 | 比一般的净水机和桶装水都要贵些 | **并不比一般的净水机和桶装水贵**(修正) |
+| 偷懒 | 懒得换才换桶装水 | **我当前就是懒得换才又换桶装水**(口语化) |
+| 收尾 | 看你是什么消费观了 | **加 "不是买便宜货就省钱,真的。"**(强化金句) |
+| 标签 | 无 | **+5个话题标签** |
+
+## 6 张图片(slide_01_cover ~ slide_06_cta)

+ 62 - 0
articles/D19-final.md

@@ -0,0 +1,62 @@
+# D19 - 行维度:结婚八年,我在沙发上坐了一小时没说话
+
+**维度**: 行维度(#10B981 绿色/木)
+**发布时间**: 待定
+**状态**: ✅ 已上传,待发布
+
+## 标题
+结婚八年,某天晚上我在沙发上坐了一小时,没说话
+
+## 正文(最终版)
+
+现在孩子都工作了,偶尔想起以前那些事,觉得挺有意思的。
+
+有一阵子,我跟队友之间出了点问题。
+
+不是吵架,不是冷战,就是——没话说。
+
+那天晚上孩子睡了,我坐在沙发上刷手机,他坐在另一头看手机。中间隔了大概一米。我抬头看了一眼钟,一个小时过去了,我们没说一句话。
+
+我想了一下,上次两个人认真聊天是什么时候。不是聊孩子、不是聊钱、不是聊周末去哪儿,是聊"你最近在想什么"——这种对话。我想了很久,想不起来。
+
+那时候我们之间的对话,早就变成了一张"事务清单":
+
+"孩子作业写完了吗?"
+"这个月房贷还了吗?"
+"周末去我妈家还是你妈家?"
+
+把家经营得井井有条,像两个优秀的合伙人。但问题就在这里——合伙人之间,不一定有亲密关系。
+
+后来我想明白一件事:不是我们感情出了问题,是我们把"过日子"和"亲密关系"搞混了。
+
+过日子是分工合作——你负责赚钱,我负责管孩子,大家各司其职,把家运营好。这件事我们做得很好。但亲密关系是另一回事——它需要你们从"爸爸"和"妈妈"这两个身份里出来,重新做回"男人"和"女人"。
+
+这两个模式不能自动切换。你得主动创造空间。
+
+后来是怎么走出来的?说起来特别简单。
+
+就是每周六送完孩子去补习班之后,在楼下那家咖啡店坐一小时。手机静音。不聊孩子、不聊钱、不聊家务。
+
+定这个规矩的时候,我心里也没底。前几周确实尴尬,两个人面对面坐着,不知道说什么。各自发呆,偶尔找话题,聊两句又断了。那时候我才意识到,我们已经太久没有不聊"正事"了,连怎么闲聊都不会了——因为我们早就忘了,除了"这个家的合伙人",我们还能以什么身份跟对方说话。
+
+真正开始有变化,是有一次我问他:
+
+"你觉得你现在的状态怎么样?我不是问你累不累,是问你——你快乐吗?"
+
+他愣住了。不是因为这个问题有多深奥。是因为——这么多年,没有人问过他这个问题。他自己也没问过自己。
+
+他沉默了很久。然后问了一句:"那你呢?"
+
+我也愣住了。我好像很久没想过这个问题了。
+
+那天我们聊了很久。说起了各自的理想、各自的疲惫、各自那些"算了不说了"的念头。我发现,我了解的不是他这个人,我了解的是"他在婚姻中的角色"——他会做什么、不会做什么、生气了是什么样子。但我不一定了解他作为一个"人"在想什么。
+
+后来咖啡慢慢成了习惯,再后来一起报了个周末的摄影课。作品好不好不重要,重要的是我们终于有了一件"一起做"、不用带孩子、不用算计性价比的事。
+
+现在想想,其实不是他变了,是我终于知道他本来就是这样的人。而知道这一点,就够了。
+
+保持亲密关系这件事,不是靠感情好就能自动维持的。你得主动创造一些空间,让你们从"爸爸"和"妈妈"这两个身份里出来,重新做回"男人"和"女人"。
+
+你家多久没有好好聊过天了?评论区聊聊吧。
+
+## 6张图片(slide_01_cover ~ slide_06_cta)

+ 56 - 0
articles/D20-final.md

@@ -0,0 +1,56 @@
+# D20 - 心维度:我都是为了你
+
+**维度**: 心维度(#FF6B9D 红色/火)
+**发布时间**: 待定
+**状态**: 待发布
+
+## 标题
+我都是为了你——这句话正在毁掉你的孩子
+
+## 正文(最终版)
+
+最近网上有个片段刷屏了。
+
+一个妈妈在女儿18岁生日宴上,当着所有人一遍遍说:"我拼命赚钱都是为了你们""要不是你们我早就不想活了""我这么多年不离婚,都是为你们。"
+
+满桌的菜,满桌的人,谁都没说话。女儿忍了很久,最后红着眼眶回了一句:"你不是为了我,你是为了你自己。因为你没有退路。"
+
+这句话,让整个餐厅安静了。
+
+也让我想起,我们身边有多少父母,正用"我都是为了你",一点一点把孩子的心推远。
+
+我认识一个妈妈,她真的是个好妈妈。省吃俭用,把最好的都给孩子。可她最爱说的一句话是:"我这么辛苦,还不都是为了你。"孩子考差了,她说;孩子想买件贵点的衣服,她说;孩子想上补习班,她还说。
+
+她以为这是在教育孩子感恩。
+
+可孩子听到的,是另一句话:"你不配,是你拖累了我。"
+
+孩子开始不敢开心。考好了不敢炫耀,因为那是"应该的";考差了不敢哭,因为"妈妈更辛苦"。慢慢地,孩子学会了压抑自己,把"我是不是又让妈妈失望了"当成每天醒来第一件事。
+
+心理学上,这叫"情感勒索"。用"付出感"当筹码,让孩子产生罪恶感,用罪恶感换来顺从。
+
+有些妈妈说:"我跟孩子说了这句话,他也没啥反应啊。"其实不是没反应,是反应不同。大五人格里,宜人性高的孩子当下就会内疚沉默——妈妈觉得"有效";责任心高的孩子表面没表情,但会把这句话内化成"我不能让妈妈失望",变成未来十几年的长期压力。你以为孩子免疫了,其实伤害只是延后了。
+
+可怕的是,说这句话的父母,往往是真的爱孩子。他们不是坏,是太累了,把"爱"和"牺牲"混为了一谈。
+
+孩子要的从来不是一句"我都是为了你"。孩子要的是:"我看到了你的辛苦,我也爱你。但你走的路,是你自己选的,不是为我走的。"
+
+有个孩子后来跟妈妈说:"妈,我不想你为了我牺牲。我想你也有自己的生活,想吃什么就吃什么,想去哪就去哪。你快乐了,我才敢快乐。"
+
+那一瞬间,妈妈愣住了。
+
+原来这么多年,她以为自己在给,其实是在用一个很大的东西,压着一个很小的孩子。
+
+如果你也说过这句话,别自责。你不是不爱,你只是太累了。
+
+试着改一改,从今天起:
+
+把"我都是为了你"换成"我为自己选择的路负责,也为你负责";
+把"我这么辛苦都是因为你"换成"我们一起把这个家过好";
+把"你对不起我的付出"换成"我爱你,你不需要内疚"。
+
+孩子真正需要的,不是一个把自己活成"牺牲者"的父母,而是一个过得踏实的、能爱自己也爱孩子的父母。
+
+你小时候,有没有被"我都是为了你"压住过?评论区聊聊吧。
+
+## 6张图片(slide_01_cover ~ slide_06_cta)

+ 54 - 0
articles/D21-draft_v1.md

@@ -0,0 +1,54 @@
+# D21 - 身维度:不敢看体检报告,先分清这3件事
+
+**维度**: 身维度(#FF8C42 土/橙)
+**发布时间**: 待定
+**状态**: 草稿
+
+## 标题
+不敢看体检报告,先分清这3件事就好了
+
+## 正文(初稿)
+
+最近新冠阳性率又升到22.3%了,身边好几个朋友反复感冒、总是累、睡不好。有人问我:"我是不是体质变差了?"
+
+不是体质变差了,是你没分清三件事。
+
+你有没有过这样的时刻:体检报告到手,不敢翻开。先放两天,再放一周,最后实在不行了才打开,看完长舒一口气——"还好,没大事"。
+
+然后明年体检,又是同一套流程。
+
+不是矫情。是真的怕。
+
+怕的不是"查出病"这三个字,怕的是后面跟着的那一句:"你要注意了。"——注意什么?注意多少?从哪开始?没人说清。于是"注意身体"变成一句悬在头顶的话,让人一边焦虑,一边不知道该干嘛。
+
+后来我接触了很多家庭,发现一个规律:那些真正把身体管起来的人,不是比我们更自律,是他们先把三件事分清了。
+
+第一种:身体明确在报警的信号,别拖。
+
+持续两周以上的消瘦、不明原因的疼痛、睡不好已经成了常态——这些不是"想多了",是身体在认真说话。该检查就检查,该看医生就看医生。这一条不需要纠结,属于"没得商量"的部分。
+
+第二种:偶尔的小毛病,不用自己吓自己。
+
+偶尔头晕、偶尔胃胀、偶尔失眠一晚——这些大多数是累的、情绪带起来的。你越盯着它,它越明显;你睡一觉、缓两天,它自己就没了。这一条要做的是"放过自己",不是"更严格管理"。
+
+第三种:情绪在放大身体信号,别急着往坏处想。
+
+很多朋友跟我说"我最近总觉得胸闷,是不是心脏有问题",结果心电图、彩超全做了,医生都说没事。是真的没事,还是心理作用?都有。焦虑本身就会让心跳加快、呼吸变浅,症状和"心脏病"几乎一模一样。这一条最难,因为它藏在最里面:先处理情绪,再谈身体。
+
+为什么这样分?
+
+心理学上有个说法叫"身体化"——压力说不出口,身体替你喊。家庭关系紧张的时候,孩子容易反复感冒;成年人焦虑的时候,胃和心脏最先不舒服。身体不是你背叛了你,它只是在用你能听懂的方式提醒你:有些东西,该面对了。
+
+把三件事分清之后,最明显的变化不是"病少了",是"不怕了"。
+
+你不再把每一个小信号都当成大事,也不再对真正的大信号视而不见。心里那根一直绷着的弦,松下来了。
+
+这个周末,花十分钟,把这三件事理一理:
+
+1. 身体在报警的信号:____(列出最担心的,定个日期去查)
+2. 可以放过的:____(写下最近一次"自己吓自己")
+3. 可能是情绪在喊的:____(最近是不是压力太大了?)
+
+评论区告诉我:你最近一次不敢翻开体检报告,是怕查出什么?
+
+#主动健康 #身体健康 #体检 #家庭健康 #妈妈成长 #情绪管理

+ 56 - 0
articles/D21-draft_v2.md

@@ -0,0 +1,56 @@
+# D21 - 身维度:新冠22.3%了,身体好不好取决于这四件事
+
+**维度**: 身维度(#FF8C42 土/橙)
+**发布时间**: 待定
+**状态**: 草稿(v2)
+
+## 标题
+全民健身日,阻拦不了新冠22.3%的阳性率,还是做好这四件事
+
+## 正文(v2)
+
+昨天8月8日,全民健身日。但你有没有发现一个反差:健身卡办了一堆,身体该差还是差?
+
+更扎心的是,新冠阳性率又升到22.3%了。身边好几个朋友反复感冒、总是累、睡不好。有人问我:"我是不是体质变差了?"
+
+不是体质变差了,是你没看清身体的底层逻辑。
+
+身体好不好,取决于四件事,按重要性排:
+
+**第一:情绪好。**
+
+情绪对身体的影响,比你想象的大得多。长期焦虑的人,免疫力下降30%以上;经常生气的人,肠胃出问题的概率是普通人的两倍。很多体检异常,根源不在身体,在情绪。情绪稳了,身体就稳了一大半。
+
+**第二:睡得好。**
+
+睡眠是身体的修复期。你白天消耗的、损伤的,都在睡觉时修补。连续睡不好一周,反应速度下降相当于喝两杯白酒。很多人吃保健品、办健身卡,却舍不得早睡一小时。
+
+**第三:拉的痛快。**
+
+别笑。排便通畅是肠道健康的直接信号。肠道是人体最大的免疫器官,70%的免疫细胞在肠道。便秘不是小问题,是身体在告诉你:该调整了。
+
+**第四:吃的香。**
+
+食欲好,说明脾胃功能正常。吃不下饭、吃一点就胀,是身体在发出信号。吃的香不是暴饮暴食,是每顿饭都觉得"真香"。
+
+这四件事,越往前影响越大。情绪不好,睡再好也白搭;睡不好,吃得再健康也修复不了。很多人本末倒置,花大钱在第四件事上,却忽略了前面三件。
+
+主动健康,不是等身体出问题再去治。是能提前看见、掌握趋势。你知道自己最近情绪怎么样、睡眠质量如何、排便是否正常、食欲有没有变化——这四个信号,就是身体给你的"天气预报"。
+
+你不需要等体检报告来告诉你身体好不好。这四件事,每天都能观察。
+
+下一篇,我讲怎么做到剩下三件事,先从最容易的入手。
+
+评论区告诉我:四件事里,你最缺哪一件?
+
+#主动健康 #全民健身日 #身体健康 #新冠 #情绪管理 #睡眠 #妈妈成长
+
+## 5张图片
+
+| 图片 | 维度配色 | 金句 |
+|------|---------|------|
+| slide_01_cover.png | 暖橙 #FF8C42 | 全民健身日 / 身体好不好取决于这四件事 |
+| slide_02_zhi.png | 金 #6366F1 | 身体的信号 / 要用感知力去捕捉 |
+| slide_03_fu.png | 水 #F59E0B | 花大钱在保健品上 / 却舍不得早睡一小时 |
+| slide_04_xing.png | 木 #10B981 | 主动健康 / 是能提前看见 / 掌握趋势 |
+| slide_05_xin.png | 火 #FF6B9D | 情绪稳了 / 身体就稳了一大半 |

+ 58 - 0
articles/D22-draft_v1.md

@@ -0,0 +1,58 @@
+# D22 - 智维度:孩子总说"我不会",不是笨,是没分清这3件事
+
+**维度**: 智维度(#6366F1 金)
+**发布时间**: 待定
+**状态**: 草稿
+
+## 标题
+孩子总说"我不会",不是笨,是没分清这3件事
+
+## 正文
+
+9月开学起,教育部要在全国中小学开展科学教育"做中学"领航行动了。核心就一句话:以兴趣为导向,以实践为路径,给孩子机会"像科学家一样思考,像工程师一样动手"。
+
+很多家长看到新政策有点慌:又要加什么?孩子跟得上吗?
+
+其实不用慌。这句话背后,藏着一个更值得家长想明白的问题——孩子学习的卡点,从来不在"学校教多少",在于"孩子卡在哪一环"。
+
+你有没有听过孩子说"我不会"?
+
+不是闹脾气那种,是真的低着头、小声说出来的那种。你听了心里一紧,想教,又怕越教越打击他。
+
+不是孩子笨。是"我不会"这三个字背后,藏着完全不同的三件事,对应的解决方式完全不同。
+
+**第一:学不会——把焦点放在擅长的,用长板带动成长。**
+
+孩子不是什么都学不会,是有些东西确实难。但很少人注意到:每个孩子都有自己的"长板"——有的动手能力强,有的表达有逻辑,有的观察力远超同龄人。
+
+不是盯着短板补,是把焦点放在他擅长的那个地方。把擅长的做到极致,信心就有了,信心有了,不擅长的也会被带动起来。不是"补差",是"用强带弱"。长板不是用来补短板的,是用来带动整个人的。
+
+**第二:不愿意学——找兴趣点。**
+
+有些"我不会",是"我不想学"的客气说法。孩子不是懒,是没找到"为什么学"。
+
+不是每件事都必须按课本顺序来。孩子对恐龙感兴趣,就从恐龙开始学生物;对搭积木有耐心,就从积木开始学物理。兴趣不是学习的敌人,是唯一的入口。没有兴趣,所有的"教"都是灌。
+
+**第三:不会学——找方法。**
+
+有些孩子是真的不知道该怎么学。不是态度问题,是方法问题。
+
+同样的内容,有的孩子看一遍就记住了,有的要画出来才记得住,有的要讲一遍给别人听才懂。这不是智商差距,是"学习方式"的差距。帮孩子找到他适合的方式,比多刷十道题有用。
+
+把这三件事分清之后,你会发现:孩子不是"不会",是他卡在了不同的环节上。
+
+你只需要先判断他卡在哪一环,再帮他找对应的钥匙。不是所有"我不会"都需要教,有些需要鼓励,有些需要换方法,有些需要等一等。
+
+评论区告诉我:你家孩子说得最多的是"学不会"、"不愿意学"还是"不会学"?
+
+#赋能未来 #做中学 #智赋成长 #学习方法 #亲子教育 #家庭教育 #成长型思维
+
+## 5张图片
+
+| 图片 | 维度配色 | 金句 |
+|------|---------|------|
+| slide_01_cover.png | 金 #6366F1 | 孩子总说"我不会" / 不是笨 / 是没分清这3件事 |
+| slide_02_shen.png | 暖橙 #FF8C42 | 把焦点放在擅长的 / 长板会带动成长 |
+| slide_03_fu.png | 水 #F59E0B | 兴趣不是学习的敌人 / 是唯一的入口 |
+| slide_04_xing.png | 木 #10B981 | 不会学不是态度问题 / 是方法问题 |
+| slide_05_xin.png | 火 #FF6B9D | 有些"我不会" / 需要鼓励 / 不是需要教 |

+ 405 - 0
articles/publishing_guide_report_screenshots.md

@@ -0,0 +1,405 @@
+# 小红书发布指南:如何混入报告截图降低AI标记风险
+
+> 目标:在AI生成图片中混入真实报告截图,打破"全部AI"的检测模式
+> 适用:D16(你不敢体检,是因为怕这个)、D17(孩子专注力差,可能是早上少了这1杯水)
+
+---
+
+## 一、总原则
+
+### 为什么混入截图有效
+
+小红书的AI检测模型主要识别**图片的视觉特征**。如果一篇文章的5张图全是AI生成的(同一画风、同一版式),被标记的风险高。每篇文章混入**1-2张真实报告截图**,检测模型看到"AI图+真实截图"混合,大概率不会标记。
+
+### 截图规则
+
+| 规则 | 说明 |
+|------|------|
+| 每篇文章至少1张截图 | 最好放在第2或第3张(不是封面,不是CTA) |
+| 必须脱敏 | 模糊姓名、ID、日期、学校、电话等个人信息 |
+| 加标注 | 用红色框/箭头标注关键数据,旁边加中文翻译 |
+| 尺寸适配 | 截图宽高比尽量接近3:4(小红书竖版),或居中裁剪 |
+
+---
+
+## 二、D16:菌群测试报告截图
+
+### 文章主题
+"你不敢体检,是因为怕这个"——体检正常不代表健康,菌群检测能发现更深层的问题。
+
+### 推荐截图(2张)
+
+#### 截图1:健康总分概览页
+
+| 项目 | 内容 |
+|------|------|
+| 来源 | `参考资料\501999942-某人.pdf` 第1页 |
+| 内容 | 健康总分57、肠道菌群平衡、菌群多样性、有益菌/有害菌 |
+| 要展示 | 总分57(偏低)、菌群平衡38(中等) |
+| 标注 | 红色框标出"健康总分57",旁边写"看似正常,菌群已亮红灯" |
+| 脱敏 | 模糊姓名"某人"、编号501999942、年龄53岁、性别男 |
+
+#### 截图2:疾病风险评估页
+
+| 项目 | 内容 |
+|------|------|
+| 来源 | `参考资料\501999942-某人.pdf` 第2页 |
+| 内容 | 心血管疾病0.37(注意)、甲状腺疾病0.33(注意) |
+| 要展示 | 报告显示"正常体检看不出的风险" |
+| 标注 | 红色框标出"注意"级别的项目,旁边写"体检报告正常,但菌群已经预警" |
+| 脱敏 | 模糊顶部个人信息 |
+
+### 操作步骤
+
+```bash
+# 1. 用PyMuPDF渲染PDF为图片
+python -c "
+import fitz
+doc = fitz.open(r'参考资料\501999942-某人.pdf')
+# 第1页(索引0):健康总分概览
+page = doc[0]
+pix = page.get_pixmap(dpi=200)
+pix.save(r'articles\images\report_screenshots\d16_gut_overview.png')
+# 第2页(索引1):疾病风险评估
+page = doc[1]
+pix = page.get_pixmap(dpi=200)
+pix.save(r'articles\images\report_screenshots\d16_gut_risks.png')
+"
+
+# 2. 用PIL模糊个人信息
+python -c "
+from PIL import Image, ImageFilter, ImageDraw, ImageFont
+
+def blur_region(img, x1, y1, x2, y2, radius=30):
+    region = img.crop((x1, y1, x2, y2))
+    blurred = region.filter(ImageFilter.GaussianBlur(radius=radius))
+    img.paste(blurred, (x1, y1, x2, y2))
+
+def add_annotation(img, text, x, y):
+    draw = ImageDraw.Draw(img)
+    draw.rectangle([x-5, y-5, x+200, y+25], outline=(255,0,0), width=3)
+    font = ImageFont.truetype('C:/Windows/Fonts/msyh.ttc', 20)
+    draw.text((x, y), text, fill=(255,0,0), font=font)
+
+img = Image.open(r'articles\images\report_screenshots\d16_gut_overview.png')
+w, h = img.size
+# 模糊顶部个人信息区
+blur_region(img, 50, 50, 500, 200)  # 姓名/编号/年龄/性别
+# 模糊底部页脚
+blur_region(img, 0, h-60, w, h)
+# 标注关键数据
+add_annotation(img, '← 健康总分57(偏低)', 500, 400)
+img.save(r'articles\images\report_screenshots\d16_gut_overview_desensitized.png')
+"
+```
+
+### 在文章中的位置
+- 第2张图:健康总分概览 → 放在"体检正常不代表健康"段落之后
+- 第3张图:疾病风险评估 → 放在"菌群检测发现更深层问题"段落
+
+---
+
+## 三、D17:A1注意力报告截图
+
+### 文章主题
+"孩子专注力差,可能是早上少了这1杯水"——注意力可测量,A1报告能直观展示。
+
+### 推荐学生(素材库数据)
+
+从 `素材库\测评数据_优化提取.csv` 中筛选出"注意力低、其他指标好"的学生:
+
+| 学生 | 注意力 | 推理能力 | 记忆力 | 加工速度 | 空间能力 | 总分 | 推荐理由 |
+|------|--------|----------|--------|----------|----------|------|----------|
+| **位浩宸** | 10% | 77% | 99% | 87% | 92% | 97% | ⭐最推荐:注意力极低,其他全高 |
+| **葛传成** | 10% | 94% | 82% | 99% | 53% | 92% | 推理能力+加工速度极高 |
+| **龚佑歆** | 10% | 89% | 81% | 80% | 88% | 90% | 全面发展,仅注意力低 |
+
+**首选位浩宸**,因为对比最鲜明(注意力10% vs 其他全部80%+)。
+
+### 推荐截图(2张)
+
+#### 截图1:认知能力模型(雷达图)
+
+| 项目 | 内容 |
+|------|------|
+| 来源 | `双培强基工程\报告\A1 儿童核心认知发展_位浩宸_2025112914351035966.pdf` 第4页 |
+| 内容 | 六维雷达图 + 各维度百分位 |
+| 要展示 | 注意力10%在雷达图上明显凹陷,其他维度饱满 |
+| 标注 | 红色箭头指向注意力维度,写"注意力10%",绿色对勾标出其他高分维度 |
+| 脱敏 | 模糊姓名、档案ID、测试日期、出生日期、指导师、报告编码 |
+
+#### 截图2:注意力报告详情页
+
+| 项目 | 内容 |
+|------|------|
+| 来源 | 同上报告第7页 |
+| 内容 | "我的注意力得分" + 发展表现(高分/低分表现对比) |
+| 要展示 | 注意力得分和低分表现描述("注意力不集中,上课走神") |
+| 标注 | 红色框标出"我的注意力得分",旁边写"和文章描述一致" |
+| 脱敏 | 模糊顶部个人信息栏 |
+
+### 操作步骤
+
+```bash
+# 1. 在素材库中找到对应学生的PDF
+# 从 测评数据_优化提取.csv 找到学生姓名,然后在 报告/ 目录找同名PDF
+
+# 2. 渲染为图片
+python -c "
+import fitz
+doc = fitz.open(r'双培强基工程\报告\A1 儿童核心认知发展_位浩宸_2025112914351035966.pdf')
+# 第4页(索引3):认知能力模型(雷达图)
+page = doc[3]
+pix = page.get_pixmap(dpi=200)
+pix.save(r'articles\images\report_screenshots\d17_cognitive_model.png')
+# 第7页(索引6):注意力报告
+page = doc[6]
+pix = page.get_pixmap(dpi=200)
+pix.save(r'articles\images\report_screenshots\d17_attention_report.png')
+"
+
+# 3. 脱敏
+python -c "
+from PIL import Image, ImageFilter, ImageDraw, ImageFont
+
+def blur_region(img, x1, y1, x2, y2, radius=30):
+    region = img.crop((x1, y1, x2, y2))
+    blurred = region.filter(ImageFilter.GaussianBlur(radius=radius))
+    img.paste(blurred, (x1, y1, x2, y2))
+
+img = Image.open(r'articles\images\report_screenshots\d17_cognitive_model.png')
+w, h = img.size
+# 模糊顶部个人信息栏(姓名、ID、日期、指导师、报告编码)
+blur_region(img, 50, 30, 800, 180)
+# 模糊底部页脚
+blur_region(img, 0, h-80, w, h)
+img.save(r'articles\images\report_screenshots\d17_cognitive_model_desensitized.png')
+"
+```
+
+### 在文章中的位置
+- 第2张图:认知能力雷达图 → 放在"你以为是营养不够,其实是注意力问题"段落
+- 第3张图:注意力报告详情 → 放在"大脑73%是水,缺水1%注意力就掉"段落
+
+---
+
+## 四、脱敏标准操作流程
+
+### 每次截图必须模糊的内容
+
+| 报告类型 | 必须模糊 |
+|----------|----------|
+| A1/A2/B2/B3/B4/B6 | 姓名、档案ID、测试日期、出生日期、指导师编号、报告编码、学校名称、电话 |
+| 菌群报告 | 姓名、编号、年龄、性别、报告日期 |
+
+### 模糊方法
+
+```python
+from PIL import Image, ImageFilter
+
+def blur_region(img, x1, y1, x2, y2, radius=30):
+    region = img.crop((x1, y1, x2, y2))
+    blurred = region.filter(ImageFilter.GaussianBlur(radius=radius))
+    img.paste(blurred, (x1, y1, x2, y2))
+```
+
+参数说明:
+- `radius=30`:高斯模糊半径,值越大越模糊
+- 坐标范围:`(x1, y1, x2, y2)`,单位像素
+- 200 DPI 时,顶部个人信息栏通常位于 `(50, 30, 800, 180)`
+
+### 标注样式
+
+```python
+from PIL import ImageDraw, ImageFont
+
+def add_annotation(img, text, x, y):
+    draw = ImageDraw.Draw(img)
+    # 红色方框
+    draw.rectangle([x-5, y-5, x+200, y+25], outline=(255, 0, 0), width=3)
+    # 红色文字
+    font = ImageFont.truetype('C:/Windows/Fonts/msyh.ttc', 20)
+    draw.text((x, y), text, fill=(255, 0, 0), font=font)
+```
+
+---
+
+## 五、素材库数据查询
+
+### CSV文件位置
+
+```
+C:\Users\Administrator\Documents\双培强基工程\素材库\测评数据_优化提取.csv
+```
+
+### PDF报告位置
+
+```
+C:\Users\Administrator\Documents\双培强基工程\报告\A1 儿童核心认知发展_位浩宸_2025112914351035966.pdf
+```
+
+### 菌群报告位置
+
+```
+C:\Users\Administrator\.local\share\opencode\worktree\527b7036bd8d16188784528603a9b79a1d95c9fb\proud-knight\docs\参考资料\501999942-某人.pdf
+```
+
+### 关键列名(A1报告)
+
+| 列名 | 说明 | 参考值 |
+|------|------|--------|
+| `姓名` | 学生姓名 | 位浩宸 |
+| `报告类型` | A1/A2/B2/B3/B4/B6 | A1 |
+| `注意力_pct` | 注意力百分位 | 10(低) |
+| `推理能力_pct` | 推理能力百分位 | 77(高) |
+| `记忆力_pct` | 记忆力百分位 | 99(高) |
+| `加工速度_pct` | 加工速度百分位 | 87(高) |
+| `空间能力_pct` | 空间能力百分位 | 92(高) |
+| `感知觉_pct` | 感知觉百分位 | — |
+| `总分` | 认知能力总分 | 140 |
+| `百分位` | 总分在同龄人中的位置 | 97 |
+
+### 查询SQL示例
+
+```python
+import csv
+with open('测评数据_优化提取.csv', encoding='utf-8') as f:
+    reader = csv.DictReader(f)
+    for row in reader:
+        if row['报告类型'] == 'A1' and float(row['注意力_pct'] or 0) < 45:
+            print(f"{row['姓名']}: 注意力={row['注意力_pct']}% 推理={row['推理能力_pct']}%")
+```
+
+### 其他报告类型
+
+| 类型 | 全称 | 用途 |
+|------|------|------|
+| A1 | 儿童核心认知发展 | 注意力、记忆力、推理能力等(最常用) |
+| A2 | 青少年综合素质成长 | 人格特质、社会关系、情绪状态 |
+| B2 | 儿童自我与家庭教养 | 亲子关系、教养方式 |
+| B3 | 核心学习能力 | 学习策略、深层动机 |
+| B4 | 核心认知能力与成长型思维 | 成长型思维、自驱力 |
+| B6 | 职业发展规划 | 职业兴趣、价值观 |
+| C1 | 校园标准版 | 校园适应、社交关系 |
+
+---
+
+---
+
+## 七、文字风格指南(关键!)
+
+> 经验教训:D16被标记为AI生成,不是因为图片,而是因为文字本身"AI味太浓"。
+> 重写后(去掉AI味)→ 不再被标记。D17直接按新风格写 → 没被标记。
+
+### 热贴写法 vs AI写法对比
+
+| 热贴写法(✅) | AI写法(❌) |
+|---------------|-------------|
+| 像朋友聊天,口语化 | 像写作文,结构工整 |
+| 情绪驱动,分享感受 | 数据驱动,传授知识 |
+| 短句短段,多空行 | 段落偏长,逻辑连贯 |
+| 有"我"的真实感受 | 有"数据"的客观引用 |
+| 偶尔跳跃,不按套路 | 故事→数据→方法→CTA,篇篇一样 |
+| 有真实细节(几点下班、多少钱) | 泛泛而谈("很多人"、"有的家长") |
+| 从别人那儿听说的 | 自己去查资料/翻书的 |
+
+### 六大原则
+
+#### 1. 像聊天,不像讲课
+
+**❌** "主动健康的核心就两句话:发现风险要科学,干预一定要简单。"
+**✅** "后来跟别的家长聊天,才知道这事没那么简单。"
+
+#### 2. 细节要真实
+
+**❌** "我认识一个朋友叫小付。"
+**✅** "后来跟闺蜜聊天,她说她老公去年体检也是正常。"
+
+**❌** "孩子不爱喝白开水。"
+**✅** "只要一离开我视线,就偷偷买可乐喝。"
+
+#### 3. 时间线要对
+
+**❌** 17年前的事提到17年后才有的东西(手机、膳食指南)
+**✅** 当时只知道什么 → 现在才知道什么 → 回头看觉得可惜
+
+#### 4. 不列清单
+
+**❌** "第一是糖……第二是酸……第三是气泡……"
+**✅** "饮料里又是糖又是酸还有气泡,糖伤代谢,酸伤菌群,气泡越喝越渴。加在一起,比单喝糖水糟糕得多。"
+
+#### 5. 不教人做事
+
+**❌** "你可以试试以下三个方法。"
+**✅** "如果你家孩子还小,明天早上起来,给他倒杯水试试。"
+
+#### 6. 结尾自然
+
+**❌** "评论区打1,我明天回你。"
+**✅** "你家孩子平时喝什么水?评论区聊聊。"
+
+### 写作前检查清单
+
+- [ ] 这个开头,我敢不敢直接念给朋友听?
+- [ ] 有没有"我认识一个朋友""值得注意的是""首先其次最后"这类AI高频词?
+- [ ] 时间线对不对?有没有把今天的事放到昨天?
+- [ ] 前后逻辑一致吗?比如"用了几个月"这种表述,每篇文章都出现,但时间线根本串不起来
+- [ ] 有没有"第一第二第三"这种结构化的东西?
+- [ ] 结尾是"教人做事"还是"聊聊"?
+
+### 常见逻辑问题(易错)
+
+| 问题 | 错误示例 | 正确做法 |
+|------|---------|---------|
+| 时间线矛盾 | D17说"现在孩子工作了",同一篇又说"用了几个月" | 孩子工作了就不能"最近用了几个月",应该是"最近在研究" |
+| 数据不准确 | 滤芯一年两三百(实际一两千) | 用真实数据,不确定就不写具体数字 |
+| 虚构体验 | "用了几个月"但根本没买 | 没买就说"研究了一圈,还没下手" |
+| 前后矛盾 | 前面说AI检测没问题,后面又说被标记了 | 检查整篇文章的叙事是否自洽 |
+| 违反常识 | 17年前没收手机 | 那时候还没有智能手机 |
+
+---
+
+## 八、D18经验总结:算账类文章(富维度)
+
+### 文章主题
+"净水机是不是智商税?用了十几年我才想通"——用真实经历算账,富维度角度。
+
+### 核心经验
+
+| 经验 | 说明 |
+|------|------|
+| **不需要截图** | 富维度算账类文章不需要报告截图,纯文字+AI图即可,不会被标记 |
+| **用户改稿亮点** | 标题"我终于想通了"→"我才想通"(更口语化);"一周喝进去一张银行卡,吓人"(更生动);"看了下饮水机里面"(更具体,改前"看了下那桶里面的卫生") |
+| **加话题标签** | 正文末尾加5个话题标签(#净水器# #健康从水开始# #自来水过滤# #生活饮用水# #品质净水健康生活#) |
+| **定时发布** | 2026-08-06 19:30 定时发布 |
+| **图片数量** | 6张(封面+历史/算账/pH-TDS/功能水机/CTA) |
+| **颜色** | 富维度 #F59E0B 金色 |
+
+### 用户修改风格分析
+
+用户会自动优化以下方向:
+1. **更口语化**:把正式表述改成日常聊天语气("终于"→"才")
+2. **更具体**:抽象描述加具体细节("桶卫生"→"饮水机里面")
+3. **更夸张**:增加戏剧性表达("一周喝进去一张银行卡")
+4. **修正数据**:如果成本数据有偏差用户会改("比...贵"→"并不比...贵")
+5. **加标签**:用户会主动加话题标签
+
+### 下次写稿建议
+
+- 初稿可以更口语化,减少用户改稿量
+- 富维度算账类文章不需要报告截图
+- 使用真实个人经历开头,比"我朋友"更有说服力
+
+---
+
+## 六、最终效果检查清单
+
+每篇文章发布前,确认:
+
+- [ ] 5张图中至少1张是真实报告截图
+- [ ] 个人信息已完全模糊(无法辨认姓名、ID、日期)
+- [ ] 关键数据已用红色框标注
+- [ ] 英文标题/术语旁边有中文翻译
+- [ ] 截图尺寸经过裁剪,适配小红书竖版(3:4)
+- [ ] 截图不会显得太突兀(用统一滤镜或边框融入整体风格)
+- [ ] 所有文件已保存到 `articles/images/report_screenshots/` 目录

+ 70 - 1
cfc-backend/AGENTS.md

@@ -226,4 +226,73 @@ cd cfc-langgraph && pip install -r requirements.txt && uvicorn src.app:app --por
 
 ## 后端接口规范
 
-- 后台/api的接口规范都用post方式
+- 后台/api的接口规范都用post方式
+
+## 家庭成员列表接口规范
+
+### 统一接口:`POST /api/family/member/list`
+
+**唯一入口**,所有获取家庭成员信息的需求必须调用此接口。
+
+**替代(已废弃,请勿新增调用):**
+- `POST /api/family/user/family-members` — 返回 Map 格式,信息分散在 parents + children 两块
+- `POST /api/family/user/members/visible` — 只返回 `show_to_family=1` 的部分
+- `POST /api/family/user/children/list` — 只返回孩子,不包含家长
+- `POST /api/user/children/list` — 同上,重复路由
+
+**请求:**
+```json
+// 可选的过滤参数
+{
+  "visibleOnly": true,    // 仅返回 show_to_family=1 的成员(默认false)
+  "includeFamily": true   // 是否包含家庭信息(familyId/name/inviteCode,默认false)
+}
+```
+
+**返回:** `Result<List<FamilyMemberVO>>`
+
+**`FamilyMemberVO` 字段说明:**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | Long | family_members.id |
+| `source` | String | `family_member` 或 `parent` |
+| `userId` | Long | 关联的用户ID |
+| `nickname` | String | 昵称 |
+| `phone` | String | 手机号 |
+| `avatar` | String | 头像 |
+| `gender` | String | male/female |
+| `age` | Integer | 年龄(由 birthday 实时计算) |
+| `effectiveRole` | String | parent/child/teacher |
+| `roleLabel` | String | 关系标签(如"爸爸""妈妈""爷爷""奶奶") |
+| `generation` | Integer | 辈分值 |
+| `isSpouse` | Boolean | 是否配偶 |
+| `isAdmin` | Boolean | 是否家庭管理员 |
+| `isSelf` | Boolean | 是否当前用户自己 |
+| `totalPoints` | Integer | 总积分 |
+| `showToFamily` | Integer | 对家庭成员可见性(1=可见, 0=不可见) |
+| `trustScore` | BigDecimal | 信任度 0-100 |
+| `intimacyScore` | BigDecimal | 亲密度 0-100 |
+| `communicationScore` | BigDecimal | 沟通质量 0-100 |
+| `memberType` | String | 成员类型(parent/child) |
+
+**后端实现:**
+- Controller: `FamilyMembersController.listMembers()` → `FamilyMemberService.listMembers()`
+- 传入 `visibleOnly=true` 时,调用 `FamilyMemberService.listMembers()` 后按 `show_to_family` 过滤
+- 不传 `familyMemberId` 参数,根据 JWT 的 userId 自动获取用户的 `familyId`
+
+**前端调用:**
+```js
+import { getFamilyMemberList } from '@/utils/api.js'
+// 已封装为:request('/api/family/member/list', 'POST', { visibleOnly: true })
+```
+
+**新增成员相关接口:**
+| 功能 | 路径 | 说明 |
+|------|------|------|
+| 添加成员 | `POST /api/family/member/add` | 创建新家庭成员 |
+| 更新成员 | `POST /api/family/member/update` | 修改成员信息 |
+| 踢出成员 | `POST /api/family/member/kick` | 删除成员 |
+| 切换视角 | `POST /api/family/member/switch` | 切换到指定成员视图 |
+| 检查编辑 | `POST /api/family/member/editable` | 检查成员是否可编辑 |
+| 变更日志 | `POST /api/family/member/logs` | 成员变更历史 |

+ 6 - 0
cfc-backend/pom.xml

@@ -95,6 +95,12 @@
             <artifactId>spring-security-crypto</artifactId>
         </dependency>
 
+        <!-- Logback (显式依赖,避免 Tomcat 线程类加载器看不到 BOOT-INF/lib/ 导致 NoClassDefFoundError) -->
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+        </dependency>
+
         <!-- Apache Commons -->
         <dependency>
             <groupId>org.apache.commons</groupId>

+ 332 - 50
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -12,7 +12,9 @@ import org.springframework.util.StreamUtils;
 
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -8861,54 +8863,6 @@ private void runMigration100() {
 			log.warn("插入GROWTH_COACH权益种子数据失败: {}", ex.getMessage());
 		}
 
-		// 迁移230: 创建 admin_menu 表(后台菜单管理)
-		try {
-			jdbcTemplate.execute(
-				"CREATE TABLE IF NOT EXISTS admin_menu (" +
-				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-				"parent_id BIGINT DEFAULT 0 COMMENT '父菜单ID,0=顶级', " +
-				"name VARCHAR(50) NOT NULL COMMENT '菜单名称', " +
-				"path VARCHAR(200) DEFAULT NULL COMMENT '路由路径', " +
-				"icon VARCHAR(50) DEFAULT NULL COMMENT '图标类名', " +
-				"sort_order INT DEFAULT 0 COMMENT '排序号', " +
-				"perm VARCHAR(100) DEFAULT NULL COMMENT '权限标识', " +
-				"component VARCHAR(200) DEFAULT NULL COMMENT '前端组件路径', " +
-				"visible TINYINT DEFAULT 1 COMMENT '是否显示:0隐藏1显示', " +
-				"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
-				"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-				"INDEX idx_parent (parent_id)" +
-				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='后台管理菜单'"
-			);
-			log.info("已创建admin_menu表");
-		} catch (Exception ex) {
-			log.warn("创建admin_menu表失败: {}", ex.getMessage());
-		}
-		try {
-			jdbcTemplate.execute("INSERT IGNORE INTO admin_menu (parent_id, name, path, icon, sort_order, perm, component, visible) VALUES " +
-				"(0, '首页', '/dashboard', 'el-icon-house', 0, 'dashboard', 'admin/Dashboard', 1), " +
-				"(0, '家庭管理', NULL, 'el-icon-user', 10, 'family:list', NULL, 1), " +
-				"(1, '家庭列表', '/families', 'el-icon-office-building', 1, 'family:list', 'Families', 1), " +
-				"(1, '孩子管理', '/children', 'el-icon-child', 2, 'family:children', 'Children', 1), " +
-				"(0, '积分管理', NULL, 'el-icon-ticket', 20, 'family:points', NULL, 1), " +
-				"(3, '积分记录', '/points-log', 'el-icon-document', 1, 'family:points', 'admin/PointsLog', 1), " +
-				"(0, '会员管理', NULL, 'el-icon-v-ip', 30, 'system:config', NULL, 1), " +
-				"(4, '会员中心', '/membership-center', 'el-icon-setting', 1, 'system:config', 'admin/MembershipCenter', 1), " +
-				"(0, '订单管理', NULL, 'el-icon-shopping-cart-2', 40, 'product:list', NULL, 1), " +
-				"(6, '商品订单', '/product-orders', 'el-icon-s-order', 1, 'product:list', 'admin/ProductManage', 1), " +
-				"(0, '内容管理', NULL, 'el-icon-document-checked', 50, 'article:list', NULL, 1), " +
-				"(8, '文章管理', '/article-manage', 'el-icon-edit', 1, 'article:list', 'admin/ArticleManage', 1), " +
-				"(8, '分类管理', '/article-category', 'el-icon-folder', 2, 'article:list', 'admin/ArticleCategory', 1), " +
-				"(0, '测评管理', NULL, 'el-icon-reading', 60, 'assessment:list', NULL, 1), " +
-				"(10, '测评订单', '/assessment-orders', 'el-icon-tickets', 1, 'assessment:list', 'admin/AssessmentOrders', 1), " +
-				"(10, '测评产品', '/assessment-products', 'el-icon-price-tag', 2, 'assessment:product', 'admin/AssessmentProducts', 1), " +
-				"(0, '系统配置', NULL, 'el-icon-setting', 90, 'system:config', NULL, 1), " +
-				"(14, '菜单管理', '/menu-manage', 'el-icon-menu', 1, 'system:menu', 'admin/MenuManage', 1), " +
-				"(14, '系统配置', '/sys-config', 'el-icon-setting', 2, 'system:config', 'admin/SysConfig', 1)");
-			log.info("已插入admin_menu种子数据");
-		} catch (Exception ex) {
-			log.warn("插入admin_menu种子数据失败: {}", ex.getMessage());
-		}
-
 		// 迁移231: 画像驱动推荐 - member_profile_dimension 表
 		try {
 			jdbcTemplate.execute(
@@ -9043,12 +8997,340 @@ private void runMigration100() {
 			log.warn("创建user_notifications表失败: " + e.getMessage());
 		}
 
-		// 迁移237: task 表添加 member_only 字段(1=会员任务,仅方案/套餐生成)
-		ensureColumn("task", "member_only", "TINYINT DEFAULT 0 COMMENT '1=会员任务(方案/套餐生成),执行需会员; 0=普通任务可免费执行'");
+		// 迁移237: tasks 表添加 member_only 字段(1=会员任务,仅方案/套餐生成)
+		ensureColumn("tasks", "member_only", "TINYINT DEFAULT 0 COMMENT '1=会员任务(方案/套餐生成),执行需会员; 0=普通任务可免费执行'");
 
 		// 迁移238: package_orders 表添加物流状态字段(试剂盒寄送/样本寄回)
 		ensureColumn("package_orders", "logistics_status", "VARCHAR(20) DEFAULT 'none' COMMENT 'none/kit_shipped/kit_received/sample_shipped/sample_received'");
 		ensureColumn("package_orders", "tracking_no", "VARCHAR(100) DEFAULT '' COMMENT '快递单号'");
 		log.info("已添加package_orders物流字段");
+
+		// 迁移239: 创建 health_status 表(当前状态调研-健康现状档案,SS-2026-08-13)
+		try {
+			jdbcTemplate.execute(
+				"CREATE TABLE IF NOT EXISTS health_status (" +
+				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"user_id BIGINT NOT NULL COMMENT '所属账号(一个账号一份)', " +
+				"height_cm DECIMAL(5,1) DEFAULT NULL COMMENT '身高(cm)', " +
+				"weight_kg DECIMAL(5,1) DEFAULT NULL COMMENT '体重(kg)', " +
+				"blood_pressure VARCHAR(20) DEFAULT NULL COMMENT '血压,如 120/80', " +
+				"blood_glucose DECIMAL(4,1) DEFAULT NULL COMMENT '空腹血糖(mmol/L)', " +
+				"blood_lipids JSON DEFAULT NULL COMMENT '血脂 [{name,value,unit,note}]', " +
+				"disease_history JSON DEFAULT NULL COMMENT '疾病史 [{name,note}]', " +
+				"medications JSON DEFAULT NULL COMMENT '在服药物/治疗 [{name,note}]', " +
+				"notes VARCHAR(500) DEFAULT NULL COMMENT '其他补充说明', " +
+				"filled_at DATETIME DEFAULT NULL COMMENT '首次填写时间', " +
+				"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', " +
+				"UNIQUE KEY uk_user (user_id)" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='当前状态调研-健康现状档案'"
+			);
+			log.info("已创建health_status表");
+		} catch (Exception e) {
+			log.warn("创建health_status表失败: " + e.getMessage());
+		}
+
+		// 迁移240: 创建 system_prompts 表(系统提示词统一管理,cfc-web后台可维护)
+		try {
+			jdbcTemplate.execute(
+				"CREATE TABLE IF NOT EXISTS system_prompts (" +
+				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"prompt_key VARCHAR(64) NOT NULL COMMENT '提示词键(如 health_coach/analysis/chat)', " +
+				"title VARCHAR(100) NOT NULL COMMENT '提示词标题', " +
+				"content TEXT NOT NULL COMMENT '提示词内容', " +
+				"default_content TEXT COMMENT '出厂默认内容(重置时恢复)', " +
+				"version INT NOT NULL DEFAULT 1 COMMENT '版本号', " +
+				"status TINYINT NOT NULL DEFAULT 1 COMMENT '1=启用 0=禁用', " +
+				"remark VARCHAR(255) COMMENT '备注', " +
+				"updated_by VARCHAR(64) COMMENT '更新人', " +
+				"created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
+				"updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+				"UNIQUE KEY uk_prompt_key (prompt_key)" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统提示词管理'"
+			);
+			log.info("已创建system_prompts表");
+		} catch (Exception e) {
+			log.warn("创建system_prompts表失败: " + e.getMessage());
+		}
+
+		// 迁移240b: 插入默认系统提示词(仅当表为空时)
+		try {
+			jdbcTemplate.execute(
+				"INSERT IGNORE INTO system_prompts (prompt_key, title, content, default_content, version, remark) VALUES " +
+				"('health_butler', '家庭健康管家', " +
+				"'你是一个家庭AI健康管家, 为用户提供专业、温暖的个性化健康建议。\\n你的能力:\\n1. 基于健康知识库(菌属功能、指标解读、营养素、食物)提供科学建议\\n2. 解读健康报告数据, 结合知识库给出改善方案\\n3. 根据菌群检测结果推荐精准营养方案\\n4. 提供可执行的健康微行动\\n回答原则:\\n1. 用中文, 像家庭医生一样亲切但专业\\n2. 引用知识库中的菌属/指标/食物信息作为证据\\n3. 优先使用知识库数据, 其次使用通用健康知识\\n4. 需要产品推荐时使用搜索工具\\n5. 生成 [TASK: {\"title\": \"任务名\", \"dimension\": \"身\", \"points\": 10, \"frequency\": \"每天\"}] 标记创建健康行动任务\\n6. 超出知识库范围的慢性病/急症问题, 建议咨询专业医生\\n7. 不要编造医学建议, 不夸大效果', " +
+				"'你是一个家庭AI健康管家, 为用户提供专业、温暖的个性化健康建议。\\n你的能力:\\n1. 基于健康知识库(菌属功能、指标解读、营养素、食物)提供科学建议\\n2. 解读健康报告数据, 结合知识库给出改善方案\\n3. 根据菌群检测结果推荐精准营养方案\\n4. 提供可执行的健康微行动\\n回答原则:\\n1. 用中文, 像家庭医生一样亲切但专业\\n2. 引用知识库中的菌属/指标/食物信息作为证据\\n3. 优先使用知识库数据, 其次使用通用健康知识\\n4. 需要产品推荐时使用搜索工具\\n5. 生成 [TASK: {\"title\": \"任务名\", \"dimension\": \"身\", \"points\": 10, \"frequency\": \"每天\"}] 标记创建健康行动任务\\n6. 超出知识库范围的慢性病/急症问题, 建议咨询专业医生\\n7. 不要编造医学建议, 不夸大效果', " +
+				"1, 'AI健康管家对话', " +
+				"'init'), " +
+				"('health_coach', '儿童健康教练', " +
+				"'你是一个儿童健康教练,喜欢用提问的方式引导家长发现问题和解决方案。\\n核心原则:\\n1. 先问后答:不要直接给答案,用问题引导家长思考\\n2. 用开放式问题了解情况,再给针对性建议\\n3. 语气温暖,像一个耐心的教练在陪伴家长成长\\n你的职责:\\n1. 儿童体质调理(如挑食、瘦弱、易疲劳、反复生病)—— 用提问了解情况\\n2. 科学喂养和营养建议 —— 先问饮食现状再给建议\\n3. 日常保健(睡眠、运动、季节防护)—— 引导家长发现生活中的问题\\n4. 体质辨识基础咨询 —— 通过提问帮助家长理解孩子体质特点\\n5. 健康问题判断 —— 先收集症状信息再建议是否就医\\n回答结构: 提问-分析-建议-跟进', " +
+				"'你是一个儿童健康教练,喜欢用提问的方式引导家长发现问题和解决方案。\\n核心原则:\\n1. 先问后答:不要直接给答案,用问题引导家长思考\\n2. 用开放式问题了解情况,再给针对性建议\\n3. 语气温暖,像一个耐心的教练在陪伴家长成长\\n你的职责:\\n1. 儿童体质调理(如挑食、瘦弱、易疲劳、反复生病)—— 用提问了解情况\\n2. 科学喂养和营养建议 —— 先问饮食现状再给建议\\n3. 日常保健(睡眠、运动、季节防护)—— 引导家长发现生活中的问题\\n4. 体质辨识基础咨询 —— 通过提问帮助家长理解孩子体质特点\\n5. 健康问题判断 —— 先收集症状信息再建议是否就医\\n回答结构: 提问-分析-建议-跟进', " +
+				"1, '儿童健康咨询对话', " +
+				"'init'), " +
+				"('chat_assistant', '儿童成长助手', " +
+				"'你是一个儿童成长家庭助手, 回答关于孩子成长、健康、教育的各种问题。', " +
+				"'你是一个儿童成长家庭助手, 回答关于孩子成长、健康、教育的各种问题。', " +
+				"1, '通用对话助手', " +
+				"'init')"
+			);
+			log.info("已插入默认系统提示词");
+		} catch (Exception e) {
+			log.warn("插入默认系统提示词失败: " + e.getMessage());
+		}
+		// 迁移242: 创建 sys_menu 表(管理后台侧边栏菜单,数据库驱动 + 角色授权)
+		try {
+			jdbcTemplate.execute(
+				"CREATE TABLE IF NOT EXISTS sys_menu (" +
+				"  id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"  parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父菜单ID,0=顶级', " +
+				"  title VARCHAR(50) COMMENT '目录标题(有子菜单时用,对应 el-submenu)', " +
+				"  label VARCHAR(50) COMMENT '叶子菜单显示名(对应 el-menu-item)', " +
+				"  path VARCHAR(200) COMMENT '路由路径(叶子菜单必填,如 /families)', " +
+				"  icon VARCHAR(100) COMMENT 'Element UI 图标类名,如 el-icon-s-custom', " +
+				"  perm VARCHAR(100) COMMENT '权限标识,如 family:list / task:* / system:config', " +
+				"  sort INT DEFAULT 0 COMMENT '排序号,小的在前', " +
+				"  visible TINYINT DEFAULT 1 COMMENT '是否显示 1=显示 0=隐藏', " +
+				"  created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+				"  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+				"  INDEX idx_parent (parent_id)" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='后台侧边栏菜单表'"
+			);
+			log.info("已创建sys_menu表");
+		} catch (Exception e) {
+			log.warn("创建sys_menu表失败: " + e.getMessage());
+		}
+
+		// 迁移243: 创建 sys_role_menu 表(角色-菜单授权)
+		try {
+			jdbcTemplate.execute(
+				"CREATE TABLE IF NOT EXISTS sys_role_menu (" +
+				"  id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"  role VARCHAR(50) NOT NULL COMMENT '角色标识: admin/teacher/nutritionist/article_manager/activity_manager/supplier_admin', " +
+				"  menu_id BIGINT NOT NULL COMMENT 'sys_menu.id', " +
+				"  created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+				"  UNIQUE KEY uk_role_menu (role, menu_id)" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色-菜单授权表'"
+			);
+			log.info("已创建sys_role_menu表");
+		} catch (Exception e) {
+			log.warn("创建sys_role_menu表失败: " + e.getMessage());
+		}
+
+		// 迁移244: sys_menu 菜单种子数据(对应 Layout.vue 硬编码 menuItems,表为空才插入,幂等)
+		try {
+			Integer menuCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM sys_menu", Integer.class);
+			if (menuCount == null || menuCount == 0) {
+				seedSysMenus();
+				log.info("sys_menu 种子数据已插入");
+			} else {
+				log.info("sys_menu 已有 {} 条数据,跳过种子插入", menuCount);
+			}
+		} catch (Exception e) {
+			log.warn("初始化sys_menu种子数据失败: " + e.getMessage());
+		}
+
+// 迁移245: 创建 notification_templates 表(通知模板管理 W3)
+		try {
+			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS notification_templates (" +
+					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+					"type VARCHAR(30) NOT NULL COMMENT '通知类型: report_ready/reward_granted/system', " +
+					"title VARCHAR(200) NOT NULL COMMENT '默认标题(支持占位符)', " +
+					"content VARCHAR(1000) NOT NULL COMMENT '默认内容模板(支持占位符)', " +
+					"channels VARCHAR(100) DEFAULT 'in_app' COMMENT '推送通道: in_app,sms,wechat,phone 逗号分隔', " +
+					"status TINYINT DEFAULT 1 COMMENT '1启用 0停用', " +
+					"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+					"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+					"UNIQUE KEY uk_type (type)" +
+					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知模板(改版v1 W3)'");
+			log.info("已创建notification_templates表");
+		} catch (Exception e) {
+			log.warn("创建notification_templates表可能已存在: {}", e.getMessage());
+		}
+		// 迁移245b: notification_templates 种子数据(表为空才插入,幂等)
+		try {
+			Integer tplCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM notification_templates", Integer.class);
+			if (tplCount == null || tplCount == 0) {
+				jdbcTemplate.execute("INSERT INTO notification_templates (type, title, content, channels) VALUES " +
+						"('report_ready', '健康报告已完成分析', '您的健康报告已完成解析,可前往查看结果与成长方案。', 'in_app,sms'), " +
+						"('reward_granted', '问题域完成奖励已到账', '恭喜完成「{domainName}」全部任务,奖励 {reward} CF 已发放至家庭账户。', 'in_app,sms,wechat'), " +
+						"('system', '系统通知', '您有一条新的系统通知,请前往查看。', 'in_app')");
+				log.info("notification_templates 种子数据已插入");
+			} else {
+				log.info("notification_templates 已有 {} 条数据,跳过种子插入", tplCount);
+			}
+		} catch (Exception e) {
+			log.warn("初始化notification_templates种子数据失败: " + e.getMessage());
+		}
+
+		// 迁移246: product_skus 存量价格 ×100(元→分),sys_config 幂等标记
+		try {
+			Integer migrated = jdbcTemplate.queryForObject(
+					"SELECT COUNT(*) FROM sys_config WHERE config_key = 'sku_price_to_cents_done'",
+					Integer.class);
+			if (migrated == null || migrated == 0) {
+				jdbcTemplate.update("UPDATE product_skus SET price = IFNULL(price, 0) * 100, original_price = IFNULL(original_price, 0) * 100");
+				jdbcTemplate.update("INSERT INTO sys_config (config_key, config_value, description) VALUES ('sku_price_to_cents_done', '1', 'SKU价格已迁移为分单位')");
+				log.info("迁移246: product_skus 价格已×100(元→分)");
+			} else {
+				log.info("迁移246: product_skus 价格已迁移过,跳过");
+			}
+		} catch (Exception e) {
+			log.warn("迁移246失败: {}", e.getMessage());
+		}
+
+		// 迁移247: product_skus 添加 linked_product_id 和 label 列(SKU关联商品选择器)
+		ensureColumn("product_skus", "linked_product_id", "BIGINT COMMENT '关联商品ID,null=Legacy SKU'");
+		ensureColumn("product_skus", "label", "VARCHAR(100) COMMENT '自定义显示标签,null=使用linked_product.name'");
+
+	}
+
+	/**
+	 * 迁移244辅助方法: 插入 sys_menu 种子数据
+	 * 结构与 cfc-web/src/views/Layout.vue 的 menuItems 硬编码一致
+	 * 数组格式: {parentKey(空=顶级), key, title(目录), label(叶子), path, icon, perm, sort}
+	 */
+	private void seedSysMenus() {
+		Map<String, Long> keyToId = new HashMap<>();
+		String[][] menus = {
+			// 顶级叶子
+			{"", "home", "", "首页", "/dashboard", "el-icon-s-home", "dashboard", "0"},
+			// 家庭运营
+			{"", "operation", "家庭运营", "", "", "el-icon-s-custom", "operation", "1"},
+			{"operation", "families", "", "家庭列表", "/families", "el-icon-s-custom", "family:list", "0"},
+			{"operation", "children", "", "孩子管理", "/children", "el-icon-user-solid", "family:children", "1"},
+			{"operation", "points", "", "积分管理", "/points", "el-icon-s-finance", "family:points", "2"},
+			{"operation", "pointsLog", "", "积分记录", "/points-log", "el-icon-document", "family:points", "3"},
+			{"operation", "wishes", "", "心愿管理", "/wishes", "el-icon-star-off", "family:wishes", "4"},
+			{"operation", "rewards", "", "奖励管理", "/rewards", "el-icon-s-goods", "reward:list", "5"},
+			// 任务中心
+			{"", "taskCenter", "任务中心", "", "", "el-icon-s-order", "task:*", "2"},
+			{"taskCenter", "tasks", "", "任务列表", "/tasks", "el-icon-s-order", "task:list", "0"},
+			{"taskCenter", "taskTemplates", "", "任务模板", "/task-templates", "el-icon-document", "task:templates", "1"},
+			{"taskCenter", "growthTask", "", "成长任务管理", "/growth-task", "el-icon-s-management", "task:list", "2"},
+			// 审核中心
+			{"", "reviewCenter", "", "审核中心", "/review-center", "el-icon-s-check", "audit", "3"},
+			{"", "serviceRoleApp", "", "服务角色申请审核", "/service-role-applications", "el-icon-s-check", "audit", "4"},
+			// 商城营销
+			{"", "commerce", "商城营销", "", "", "el-icon-s-goods", "commerce", "5"},
+			{"commerce", "productManage", "", "商品管理", "/product-manage", "el-icon-s-goods", "commerce:products", "0"},
+			{"commerce", "inventory", "", "库存管理", "/inventory", "el-icon-document", "commerce:products", "1"},
+			{"commerce", "ecomSupplier", "", "供应商管理", "/ecom-supplier", "el-icon-s-shop", "commerce:products", "2"},
+			{"commerce", "orderManage", "", "订单管理", "/order-manage", "el-icon-s-order", "commerce:orders", "3"},
+			{"commerce", "pendingRefund", "", "待退款管理", "/pending-refund", "el-icon-warning", "commerce:orders", "4"},
+			{"commerce", "productProfitRate", "", "产品利润率", "/product-profit-rate", "el-icon-data-line", "commerce:profit", "5"},
+			{"commerce", "coupon", "", "优惠券管理", "/coupon", "el-icon-ticket", "marketing:coupon", "6"},
+			{"commerce", "couponGrantLog", "", "发券记录", "/coupon-grant-log", "el-icon-document", "marketing:coupon", "7"},
+			{"commerce", "promotion", "", "推广管理", "/promotion", "el-icon-s-marketing", "marketing:promotion", "8"},
+			{"commerce", "familyEarnings", "", "家庭收益", "/family-earnings", "el-icon-s-money", "marketing:promotion", "9"},
+			{"commerce", "earningWithdraw", "", "收益提现审核", "/family-earnings-withdraw", "el-icon-document-checked", "audit:withdraw", "10"},
+			// 知识中心
+			{"", "knowledge", "知识中心", "", "", "el-icon-document", "content", "6"},
+			{"knowledge", "articleCategories", "", "知识分类", "/article-categories", "el-icon-folder", "articles:categories", "0"},
+			{"knowledge", "articleManage", "", "知识管理", "/article-manage", "el-icon-document", "articles:manage", "1"},
+			{"knowledge", "commentReview", "", "评论审核", "/comment-review", "el-icon-chat-dot-round", "articles:manage", "2"},
+			{"knowledge", "knowledgeTags", "", "知识标签", "/knowledge-tags", "el-icon-price-tag", "articles:categories", "3"},
+			{"knowledge", "knowledgeBase", "", "知识库", "/knowledge-base", "el-icon-reading", "system:config", "4"},
+			{"knowledge", "healthKnowledge", "", "健康知识库", "/health-knowledge", "el-icon-first-aid-kit", "system:config", "5"},
+			// 活动管理
+			{"", "activity", "活动管理", "", "", "el-icon-date", "activity:*", "7"},
+			{"activity", "activities", "", "活动列表", "/activities", "el-icon-date", "activity:list", "0"},
+			{"activity", "activityReview", "", "活动审核", "/activity-review", "el-icon-document-checked", "activity:review", "1"},
+			{"activity", "activityRegReview", "", "活动报名审核", "/activity-registration-review", "el-icon-document-checked", "activity:review", "2"},
+			// 健康饮食
+			{"", "health", "健康饮食", "", "", "el-icon-first-aid-kit", "health", "8"},
+			{"health", "healthReports", "", "健康报告", "/health-reports", "el-icon-document", "health:reports", "0"},
+			{"health", "healthIndicators", "", "健康指标", "/health-indicators", "el-icon-data-line", "health:indicators", "1"},
+			{"health", "healthCheckins", "", "健康打卡", "/health-checkins", "el-icon-s-order", "health:checkins", "2"},
+			{"health", "emotionAlert", "", "情绪告警", "/emotion-alert", "el-icon-warning", "health:checkins", "3"},
+			{"health", "nutritionMappings", "", "营养素映射", "/nutrition-mappings", "el-icon-connection", "health:mappings", "4"},
+			{"health", "energySandbox", "", "五维能量", "/energy-sandbox", "el-icon-data-line", "energy", "5"},
+			{"health", "energyRule", "", "能量规则管理", "/energy-rule", "el-icon-setting", "energy", "6"},
+			{"health", "energyBehaviorConfig", "", "能量行为配置", "/energy-behavior-config", "el-icon-edit-outline", "energy", "7"},
+			{"health", "periodicServiceConfig", "", "周期性服务配置", "/periodic-service-config", "el-icon-refresh", "energy", "8"},
+			{"health", "foods", "", "食材管理", "/foods", "el-icon-apple", "diet:foods", "9"},
+			{"health", "recipes", "", "食谱管理", "/recipes", "el-icon-dish", "diet:recipes", "10"},
+			{"health", "langgraphAdmin", "", "LangGraph 管理", "/langgraph-admin", "el-icon-cpu", "system:config", "11"},
+			{"health", "nutritionProducts", "", "营养产品", "/nutrition-products", "el-icon-first-aid-kit", "health:*", "12"},
+			// 报告解析系统
+			{"", "reportParser", "报告解析系统", "", "", "el-icon-cpu", "system:config", "9"},
+			{"reportParser", "reportTypes", "", "报告类型管理", "/report-types", "el-icon-collection", "system:config", "0"},
+			{"reportParser", "reportParserImport", "", "解析器导入", "/report-parser-import", "el-icon-upload2", "system:config", "1"},
+			{"reportParser", "reportUnknownClusters", "", "未知报告审核", "/report-unknown-clusters", "el-icon-question", "system:config", "2"},
+			{"reportParser", "reportAutoLearn", "", "报告自学习", "/report-auto-learn", "el-icon-magic-stick", "system:config", "3"},
+			// 家庭服务(规划师)
+			{"", "familyService", "家庭服务", "", "", "el-icon-s-custom", "service:*", "10"},
+			{"familyService", "myFamilies", "", "我的家庭", "/my-families", "el-icon-s-custom", "service:family", "0"},
+			{"familyService", "teacherTeam", "", "我的团队", "/teacher-team", "el-icon-s-custom", "service:team", "1"},
+			// 业务管理(规划师)
+			{"", "biz", "业务管理", "", "", "el-icon-s-marketing", "biz:*", "11"},
+			{"biz", "teacherPackages", "", "任务模板管理", "/teacher-packages", "el-icon-s-goods", "biz:packages", "0"},
+			{"biz", "teacherOrders", "", "订单佣金", "/teacher-orders", "el-icon-s-order", "biz:orders", "1"},
+			// 测评咨询(规划师)
+			{"", "assessment", "测评咨询", "", "", "el-icon-edit", "assessment:*", "12"},
+			{"assessment", "teacherAssessment", "", "DAN测评", "/teacher-assessment", "el-icon-edit", "assessment:dan", "0"},
+			{"assessment", "teacherConsult", "", "家长咨询", "/teacher-consult", "el-icon-chat-dot-round", "assessment:consult", "1"},
+			{"assessment", "growthRecords", "", "成长记录", "/growth-records", "el-icon-document", "growth:records", "2"},
+			{"assessment", "growthPlans", "", "成长计划", "/growth-plans", "el-icon-document", "growth:plans", "3"},
+			// 系统配置(三级)
+			{"", "system", "系统配置", "", "", "el-icon-s-tools", "system:*", "13"},
+			{"system", "baseManage", "基础管理", "", "", "el-icon-s-tools", "system:config", "0"},
+			{"baseManage", "sysConfig", "", "系统配置", "/sys-config", "el-icon-s-tools", "system:config", "0"},
+			{"baseManage", "users", "", "用户管理", "/users", "el-icon-user", "system:users", "1"},
+			{"baseManage", "operationLogs", "", "操作日志", "/operation-logs", "el-icon-s-order", "system:logs", "2"},
+			{"baseManage", "membershipCenter", "", "会员中心", "/membership-center", "el-icon-s-custom", "system:config", "3"},
+			{"baseManage", "badgeManage", "", "勋章管理", "/badge-manage", "el-icon-medal", "system:config", "4"},
+			{"baseManage", "virtualGoodsConfig", "", "虚拟支付道具", "/virtual-goods-config", "el-icon-goods", "system:config", "5"},
+			{"baseManage", "gates", "", "关卡配置", "/gates", "el-icon-key", "system:config", "6"},
+			{"baseManage", "menuManage", "", "菜单管理", "/menu-manage", "el-icon-menu", "system:menu", "7"},
+			{"system", "configCenter", "配置中心", "", "", "el-icon-setting", "system:config", "1"},
+			{"configCenter", "dimensionConfig", "", "维度配置", "/dimension-config", "el-icon-data-line", "system:config", "0"},
+			{"system", "supplyHierarchy", "供应商体系", "", "", "el-icon-s-management", "system:supply", "2"},
+			{"supplyHierarchy", "supplySystem", "", "供应商体系", "/supply-system", "el-icon-s-management", "system:supply", "0"},
+			{"system", "supplierManage", "供应商管理", "", "", "el-icon-s-shop", "supply:manage", "3"},
+			{"supplierManage", "supplyManage", "", "供应商管理", "/supply-manage", "el-icon-s-shop", "supply:manage", "0"},
+			{"supplierManage", "supplierProducts", "", "商品管理", "/supplier-products", "el-icon-s-goods", "commerce:product", "1"},
+			{"system", "healthConfig", "健康配置", "", "", "el-icon-first-aid-kit", "system:config", "4"},
+			{"healthConfig", "healthNormConfig", "", "健康常模", "/health-norm-config", "el-icon-data-line", "system:config", "0"},
+			{"healthConfig", "healthDataSource", "", "健康数据源", "/health-data-source", "el-icon-data-line", "system:config", "1"},
+			{"healthConfig", "healthEnergyConfig", "", "七维能量配置", "/health-energy-config", "el-icon-data-line", "system:config", "2"},
+			{"system", "lifeDimension", "人生维度", "", "", "el-icon-s-custom", "system:config", "5"},
+			{"lifeDimension", "zodiacConfigs", "", "星座配置", "/zodiac-configs", "el-icon-s-management", "system:config", "0"},
+			{"lifeDimension", "baziConfigs", "", "八字配置", "/bazi-configs", "el-icon-s-management", "system:config", "1"},
+			{"lifeDimension", "bloodTypeConfigs", "", "血型配置", "/blood-type-configs", "el-icon-s-management", "system:config", "2"},
+			{"system", "assessmentManage", "测评管理", "", "", "el-icon-edit", "assessment:dan", "6"},
+			{"assessmentManage", "assessmentAdmin", "", "测评管理", "/assessment-admin", "el-icon-edit", "assessment:dan", "0"},
+			{"assessmentManage", "assessmentOrders", "", "测评订单", "/assessment-orders", "el-icon-s-order", "assessment:dan", "1"},
+			{"assessmentManage", "assessmentAssign", "", "待分配规划师", "/assessment-assign", "el-icon-user", "assessment:dan", "2"},
+			{"system", "virtualTeam", "虚拟团队", "", "", "el-icon-s-custom", "system:config", "7"},
+			{"virtualTeam", "virtualTeams", "", "虚拟团队", "/virtual-teams", "el-icon-s-custom", "system:config", "0"},
+		};
+
+		for (String[] m : menus) {
+			Long parentId = 0L;
+			if (m[0] != null && !m[0].isEmpty()) {
+				parentId = keyToId.get(m[0]);
+				if (parentId == null) {
+					parentId = 0L;
+				}
+			}
+			String title = m[2] != null && !m[2].isEmpty() ? m[2] : null;
+			String label = m[3] != null && !m[3].isEmpty() ? m[3] : null;
+			String path = m[4] != null && !m[4].isEmpty() ? m[4] : null;
+			String icon = m[5] != null && !m[5].isEmpty() ? m[5] : null;
+			String perm = m[6] != null && !m[6].isEmpty() ? m[6] : null;
+			int sort = Integer.parseInt(m[7]);
+			jdbcTemplate.update(
+				"INSERT INTO sys_menu (parent_id, title, label, path, icon, perm, sort, visible) VALUES (?, ?, ?, ?, ?, ?, ?, 1)",
+				parentId, title, label, path, icon, perm, sort
+			);
+			Long id = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
+			keyToId.put(m[1], id);
+		}
+		// 迁移242: health_reports 表添加 payload_json 字段(报告完整载荷快照)
+		try {
+			jdbcTemplate.execute("ALTER TABLE health_reports ADD COLUMN payload_json LONGTEXT DEFAULT NULL COMMENT '报告完整载荷快照'");
+			log.info("已添加payload_json列到health_reports表");
+		} catch (Exception ex) {
+			log.warn("添加payload_json列失败: {}", ex.getMessage());
+		}
 	}
 }

+ 13 - 12
cfc-backend/src/main/java/com/etotem/cfc/controller/ActivityController.java

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/activity")
@@ -29,7 +30,7 @@ public class ActivityController {
     @PostMapping("/detail")
     public Result<ActivityDTO> detail(@RequestBody Map<String, Object> params,
                                        @RequestAttribute(value = "userId", required = false) Long userId) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id不能为空");
         return activityService.detail(id, userId);
     }
@@ -65,7 +66,7 @@ public class ActivityController {
         if (!"admin".equals(role) && !"activity_admin".equals(role)) {
             return Result.error("只有管理员或活动管理员可以发布活动");
         }
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id不能为空");
         try {
             return activityService.publish(id);
@@ -79,7 +80,7 @@ public class ActivityController {
                                @RequestAttribute(value = "userId", required = false) Long userId,
                                @RequestAttribute("role") String role) {
         if (!"admin".equals(role) && !"activity_admin".equals(role)) {
-            Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+            Long id = ParamUtils.getLong(params.get("id"));
             if (id == null) return Result.error("id不能为空");
             Activity existing = activityService.getById(id);
             if (existing == null) return Result.error("活动不存在");
@@ -87,7 +88,7 @@ public class ActivityController {
                 return Result.error("只有创建人或管理员可以结束活动");
             }
         }
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id不能为空");
         return activityService.end(id);
     }
@@ -96,7 +97,7 @@ public class ActivityController {
     public Result<String> submitReview(@RequestBody Map<String, Object> params,
                                         @RequestAttribute(value = "userId", required = false) Long userId,
                                         @RequestAttribute("role") String role) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id不能为空");
         Activity existing = activityService.getById(id);
         if (existing == null) return Result.error("活动不存在");
@@ -111,7 +112,7 @@ public class ActivityController {
     @PostMapping("/register")
     public Result<Map<String, Object>> register(@RequestBody Map<String, Object> params, @RequestAttribute(value = "userId", required = false) Long userId,
                                                 @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long activityId = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long activityId = ParamUtils.getLong(params.get("id"));
         if (activityId == null) return Result.error("活动ID不能为空");
         // batch registration with registrants list
         Object registrantsObj = params.get("registrants");
@@ -120,7 +121,7 @@ public class ActivityController {
             return registrationService.batchRegister(activityId, userId, registrants);
         }
         // single registration (backward compatibility)
-        Long memberId = params.get("memberId") != null ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         if (memberId == null) return Result.error("孩子ID不能为空");
         return registrationService.register(activityId, userId, memberId);
     }
@@ -128,8 +129,8 @@ public class ActivityController {
     @PostMapping("/cancel-registration")
     public Result<Void> cancelRegistration(@RequestBody Map<String, Object> params, @RequestAttribute(value = "userId", required = false) Long userId,
                                            @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long activityId = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
-        Long memberId = params.get("memberId") != null ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long activityId = ParamUtils.getLong(params.get("id"));
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         if (activityId == null) return Result.error("活动ID不能为空");
         if (memberId == null) return Result.error("孩子ID不能为空");
         return registrationService.cancel(activityId, userId, memberId);
@@ -138,7 +139,7 @@ public class ActivityController {
     @PostMapping("/my-registrations")
     public Result<Map<String, Object>> myRegistrations(@RequestBody Map<String, Object> params, @RequestAttribute(value = "userId", required = false) Long userId,
                                                        @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
         Integer size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;
         return registrationService.getMyRegistrations(userId, memberId, page, size);
@@ -147,8 +148,8 @@ public class ActivityController {
     @PostMapping("/checkin")
     public Result<Map<String, Object>> checkin(@RequestBody Map<String, Object> params, @RequestAttribute(value = "userId", required = false) Long userId,
                                                @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
-        Long memberId = params.get("memberId") != null ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long id = ParamUtils.getLong(params.get("id"));
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         if (id == null) return Result.error("id不能为空");
         Map<String, Object> checkinResult = activityService.checkin(id, userId, memberId);
         if (checkinResult.containsKey("error")) return Result.error(checkinResult.get("error").toString());

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

@@ -5,6 +5,7 @@ import com.etotem.cfc.service.ActivityFeedbackService;
 import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/activity/feedback")
@@ -16,8 +17,8 @@ public class ActivityFeedbackController {
     public Result<?> submit(@RequestBody Map<String, Object> params,
                             @RequestAttribute("userId") Long userId,
                             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long activityId = params.get("activityId") != null ? Long.valueOf(params.get("activityId").toString()) : null;
-        Long memberId = params.get("memberId") != null ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long activityId = ParamUtils.getLong(params.get("activityId"));
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         String ratings = (String) params.get("ratings");
         String comment = (String) params.get("comment");
         if (activityId == null) return Result.error("活动ID不能为空");
@@ -26,7 +27,7 @@ public class ActivityFeedbackController {
 
     @PostMapping("/list")
     public Result<?> list(@RequestBody Map<String, Object> params) {
-        Long activityId = params.get("activityId") != null ? Long.valueOf(params.get("activityId").toString()) : null;
+        Long activityId = ParamUtils.getLong(params.get("activityId"));
         if (activityId == null) return Result.error("活动ID不能为空");
         return activityFeedbackService.getFeedbackList(activityId);
     }
@@ -35,8 +36,8 @@ public class ActivityFeedbackController {
     public Result<?> my(@RequestBody Map<String, Object> params,
                         @RequestAttribute("userId") Long userId,
                         @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long activityId = params.get("activityId") != null ? Long.valueOf(params.get("activityId").toString()) : null;
-        Long memberId = params.get("memberId") != null ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long activityId = ParamUtils.getLong(params.get("activityId"));
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         if (activityId == null) return Result.error("活动ID不能为空");
         return activityFeedbackService.getMyFeedback(activityId, memberId != null ? memberId : null);
     }

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

@@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RestController;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/activity/material")
@@ -24,7 +25,7 @@ public class ActivityMaterialController {
     public Result<ActivityMaterial> upload(@RequestBody Map<String, Object> params,
                                            @RequestAttribute("userId") Long userId,
                                            @RequestAttribute("role") String role) {
-        Long activityId = params.get("activityId") != null ? Long.valueOf(params.get("activityId").toString()) : null;
+        Long activityId = ParamUtils.getLong(params.get("activityId"));
         if (activityId == null) return Result.error("活动ID不能为空");
         String title = (String) params.get("title");
         String fileUrl = (String) params.get("fileUrl");
@@ -34,7 +35,7 @@ public class ActivityMaterialController {
 
     @PostMapping("/list")
     public Result<List<ActivityMaterial>> list(@RequestBody Map<String, Object> params) {
-        Long activityId = params.get("activityId") != null ? Long.valueOf(params.get("activityId").toString()) : null;
+        Long activityId = ParamUtils.getLong(params.get("activityId"));
         if (activityId == null) return Result.error("活动ID不能为空");
         return materialService.list(activityId);
     }
@@ -43,7 +44,7 @@ public class ActivityMaterialController {
     public Result<String> delete(@RequestBody Map<String, Object> params,
                                  @RequestAttribute("userId") Long userId,
                                  @RequestAttribute("role") String role) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("资料ID不能为空");
         return materialService.delete(id, userId, role);
     }

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

@@ -17,6 +17,7 @@ import java.io.BufferedReader;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/activity/order")
@@ -31,7 +32,7 @@ public class ActivityOrderController {
     @PostMapping("/create")
     public Result<Map<String, Object>> create(@RequestBody Map<String, Object> params,
                                               @RequestAttribute("userId") Long userId) {
-        Long activityId = params.get("activityId") != null ? Long.valueOf(params.get("activityId").toString()) : null;
+        Long activityId = ParamUtils.getLong(params.get("activityId"));
         if (activityId == null) return Result.error("活动ID不能为空");
         Object registrantsObj = params.get("registrants");
         if (!(registrantsObj instanceof List)) return Result.error("报名人不能为空");

+ 9 - 8
cfc-backend/src/main/java/com/etotem/cfc/controller/AiQuestionnaireController.java

@@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 /**
  * AI 动态问卷引擎 — 会话/画像/场景管理
@@ -69,8 +70,8 @@ public class AiQuestionnaireController {
     public Result<Map<String, Object>> start(
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId) {
-        Long sceneId = params.get("sceneId") == null ? null : Long.valueOf(params.get("sceneId").toString());
-        Long memberId = params.get("memberId") == null ? null : Long.valueOf(params.get("memberId").toString());
+        Long sceneId = ParamUtils.getLong(params.get("sceneId"));
+        Long memberId = ParamUtils.getLong(params.get("memberId"));
         if (sceneId == null || memberId == null) return Result.error("缺少 sceneId/memberId");
         try {
             return Result.success(aiQuestionnaireService.start(userId, sceneId, memberId));
@@ -84,7 +85,7 @@ public class AiQuestionnaireController {
     public Result<Map<String, Object>> answer(
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId) {
-        Long sessionId = params.get("sessionId") == null ? null : Long.valueOf(params.get("sessionId").toString());
+        Long sessionId = ParamUtils.getLong(params.get("sessionId"));
         Object answer = params.get("answer");
         if (sessionId == null || answer == null) return Result.error("缺少 sessionId/answer");
         try {
@@ -99,7 +100,7 @@ public class AiQuestionnaireController {
     public Result<Map<String, Object>> finish(
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId) {
-        Long sessionId = params.get("sessionId") == null ? null : Long.valueOf(params.get("sessionId").toString());
+        Long sessionId = ParamUtils.getLong(params.get("sessionId"));
         if (sessionId == null) return Result.error("缺少 sessionId");
         try {
             AiQProfile profile = aiQuestionnaireService.finish(userId, sessionId);
@@ -114,7 +115,7 @@ public class AiQuestionnaireController {
     public Result<Map<String, Object>> profileDetail(
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId) {
-        Long sessionId = params.get("sessionId") == null ? null : Long.valueOf(params.get("sessionId").toString());
+        Long sessionId = ParamUtils.getLong(params.get("sessionId"));
         if (sessionId == null) return Result.error("缺少 sessionId");
         try {
             return Result.success(aiQuestionnaireService.getProfileDetail(userId, sessionId));
@@ -128,8 +129,8 @@ public class AiQuestionnaireController {
     public Result<List<AiQSession>> history(
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId) {
-        Long memberId = params.get("memberId") == null ? null : Long.valueOf(params.get("memberId").toString());
-        Long sceneId = params.get("sceneId") == null ? null : Long.valueOf(params.get("sceneId").toString());
+        Long memberId = ParamUtils.getLong(params.get("memberId"));
+        Long sceneId = ParamUtils.getLong(params.get("sceneId"));
         return Result.success(aiQuestionnaireService.getHistory(userId, memberId, sceneId));
     }
 
@@ -138,7 +139,7 @@ public class AiQuestionnaireController {
     public Result<Void> abort(
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId) {
-        Long sessionId = params.get("sessionId") == null ? null : Long.valueOf(params.get("sessionId").toString());
+        Long sessionId = ParamUtils.getLong(params.get("sessionId"));
         if (sessionId == null) return Result.error("缺少 sessionId");
         aiQuestionnaireService.abort(userId, sessionId);
         return Result.success(null);

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

@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "勋章管理", description = "勋章系统相关接口")
 @RestController
@@ -37,7 +38,7 @@ public class BadgeController {
     @Operation(summary = "获取勋章详情")
     @PostMapping("/detail")
     public Result<Badge> getBadgeDetail(@RequestBody Map<String, Object> body) {
-        Long id = Long.valueOf(body.get("id").toString());
+        Long id = ParamUtils.getLong(body.get("id"));
         Badge badge = badgeService.getBadgeById(id);
         if (badge == null) {
             return Result.error("勋章不存在");
@@ -62,7 +63,7 @@ public class BadgeController {
     @Operation(summary = "删除勋章")
     @PostMapping("/delete")
     public Result<String> deleteBadge(@RequestBody Map<String, Object> body) {
-        Long id = Long.valueOf(body.get("id").toString());
+        Long id = ParamUtils.getLong(body.get("id"));
         badgeService.deleteBadge(id);
         return Result.success("删除成功");
     }
@@ -73,8 +74,8 @@ public class BadgeController {
     @PostMapping("/grant")
     public Result<String> grantBadge(@RequestBody Map<String, Object> body,
                                      @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = body.get("memberId") != null ? Long.valueOf(body.get("memberId").toString()) : currentMemberId;
-        Long badgeId = Long.valueOf(body.get("badgeId").toString());
+        Long memberId = ParamUtils.getLong(body.get("memberId"), currentMemberId);
+        Long badgeId = ParamUtils.getLong(body.get("badgeId"));
         badgeService.grantBadge(memberId, badgeId);
         return Result.success("授予成功");
     }
@@ -85,7 +86,7 @@ public class BadgeController {
     @PostMapping("/child/list")
     public Result<List<Map<String, Object>>> getChildBadges(@RequestBody Map<String, Object> body,
                                                             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = body.get("memberId") != null ? Long.valueOf(body.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(body.get("memberId"), currentMemberId);
         List<ChildBadge> childBadges = badgeService.getActiveChildBadges(memberId);
         List<Map<String, Object>> result = new java.util.ArrayList<>();
         for (ChildBadge cb : childBadges) {
@@ -104,8 +105,8 @@ public class BadgeController {
     @PostMapping("/favorite")
     public Result<String> toggleFavorite(@RequestBody Map<String, Object> body,
                                          @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = body.get("memberId") != null ? Long.valueOf(body.get("memberId").toString()) : currentMemberId;
-        Long badgeId = Long.valueOf(body.get("badgeId").toString());
+        Long memberId = ParamUtils.getLong(body.get("memberId"), currentMemberId);
+        Long badgeId = ParamUtils.getLong(body.get("badgeId"));
         badgeService.toggleFavorite(memberId, badgeId);
         return Result.success("操作成功");
     }
@@ -116,7 +117,7 @@ public class BadgeController {
     @PostMapping("/statistics/child")
     public Result<Map<String, Object>> getChildStats(@RequestBody Map<String, Object> body,
                                                      @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = body.get("memberId") != null ? Long.valueOf(body.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(body.get("memberId"), currentMemberId);
         return Result.success(badgeService.getChildBadgeStats(memberId));
     }
 

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

@@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/health/circle/challenge")
@@ -18,15 +19,13 @@ public class CircleChallengeController {
 
     @PostMapping("/list")
     public Result<List<CircleChallenge>> getList(@RequestBody Map<String, Object> params) {
-        Long circleId = params.get("circleId") != null
-                ? Long.valueOf(params.get("circleId").toString()) : null;
+        Long circleId = ParamUtils.getLong(params.get("circleId"));
         return Result.success(circleChallengeService.getActiveChallenges(circleId));
     }
 
     @PostMapping("/history")
     public Result<List<CircleChallenge>> getHistory(@RequestBody Map<String, Object> params) {
-        Long circleId = params.get("circleId") != null
-                ? Long.valueOf(params.get("circleId").toString()) : null;
+        Long circleId = ParamUtils.getLong(params.get("circleId"));
         return Result.success(circleChallengeService.getChallengeHistory(circleId));
     }
 }

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

@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/contact")
@@ -37,14 +38,14 @@ public class ContactController {
     @PostMapping("/delete")
     public Result<String> delete(@RequestBody Map<String, Object> params,
                                  @RequestAttribute("userId") Long userId) {
-        Long id = Long.valueOf(params.get("id").toString());
+        Long id = ParamUtils.getLong(params.get("id"));
         return contactService.delete(id, userId);
     }
 
     @PostMapping("/detail")
     public Result<ContactDTO> detail(@RequestBody Map<String, Object> params,
                                      @RequestAttribute("userId") Long userId) {
-        Long id = Long.valueOf(params.get("id").toString());
+        Long id = ParamUtils.getLong(params.get("id"));
         return contactService.detail(id, userId);
     }
 
@@ -60,14 +61,14 @@ public class ContactController {
     @PostMapping("/interaction")
     public Result<ContactDTO> interaction(@RequestBody Map<String, Object> params,
                                           @RequestAttribute("userId") Long userId) {
-        Long id = Long.valueOf(params.get("id").toString());
+        Long id = ParamUtils.getLong(params.get("id"));
         return contactService.recordInteraction(id, userId);
     }
 
     @PostMapping("/invite-to-family")
     public Result<Map<String, Object>> inviteToFamily(@RequestBody Map<String, Object> params,
                                                      @RequestAttribute("userId") Long userId) {
-        Long contactId = Long.valueOf(params.get("contactId").toString());
+        Long contactId = ParamUtils.getLong(params.get("contactId"));
         String relationshipType = (String) params.get("relationshipType");
         String generationLevel = (String) params.get("generationLevel");
         return contactService.inviteToFamily(contactId, relationshipType, generationLevel, userId);

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

@@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/contact/match")
@@ -17,8 +18,8 @@ public class ContactMatchController {
     @PostMapping("/send")
     public Result<ContactMatchRequest> send(@RequestBody Map<String, Object> params,
                                             @RequestAttribute("userId") Long userId) {
-        Long targetUserId = Long.valueOf(params.get("targetUserId").toString());
-        Long contactId = params.get("contactId") != null ? Long.valueOf(params.get("contactId").toString()) : null;
+        Long targetUserId = ParamUtils.getLong(params.get("targetUserId"));
+        Long contactId = ParamUtils.getLong(params.get("contactId"));
         String message = params.get("message") != null ? params.get("message").toString() : null;
         return Result.success(service.sendRequest(userId, targetUserId, contactId, message));
     }
@@ -26,14 +27,14 @@ public class ContactMatchController {
     @PostMapping("/accept")
     public Result<Void> accept(@RequestBody Map<String, Object> params,
                                 @RequestAttribute("userId") Long userId) {
-        Long requestId = Long.valueOf(params.get("requestId").toString());
+        Long requestId = ParamUtils.getLong(params.get("requestId"));
         service.acceptRequest(userId, requestId); return Result.success(null);
     }
 
     @PostMapping("/reject")
     public Result<Void> reject(@RequestBody Map<String, Object> params,
                                 @RequestAttribute("userId") Long userId) {
-        Long requestId = Long.valueOf(params.get("requestId").toString());
+        Long requestId = ParamUtils.getLong(params.get("requestId"));
         service.rejectRequest(userId, requestId); return Result.success(null);
     }
 

+ 160 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/ContentFilterController.java

@@ -0,0 +1,160 @@
+package com.etotem.cfc.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Article;
+import com.etotem.cfc.entity.Food;
+import com.etotem.cfc.entity.HealthKnowledgeBase;
+import com.etotem.cfc.entity.ProblemDomain;
+import com.etotem.cfc.mapper.ArticleMapper;
+import com.etotem.cfc.mapper.FoodMapper;
+import com.etotem.cfc.mapper.HealthKnowledgeBaseMapper;
+import com.etotem.cfc.mapper.ProblemDomainMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.annotation.Resource;
+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 java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 内容过滤 — 改版v1(B24)
+ * <p>
+ * 用 resultTags + 问题域 code 过滤文章/知识库/饮食推荐。
+ */
+@Tag(name = "内容过滤", description = "按 resultTags + 问题域过滤文章/知识库/饮食")
+@RestController
+@RequestMapping("/api/content")
+public class ContentFilterController {
+
+    @Resource
+    private ArticleMapper articleMapper;
+
+    @Resource
+    private HealthKnowledgeBaseMapper healthKnowledgeBaseMapper;
+
+    @Resource
+    private FoodMapper foodMapper;
+
+    @Resource
+    private ProblemDomainMapper problemDomainMapper;
+
+    @Operation(summary = "按 resultTags + 问题域 code 过滤文章/知识库/饮食")
+    @PostMapping("/by-tags")
+    public Result<Map<String, Object>> byTags(@RequestBody Map<String, Object> params) {
+        Set<String> tags = new LinkedHashSet<>();
+        Object resultTagsObj = params.get("resultTags");
+        if (resultTagsObj instanceof List) {
+            for (Object t : (List<?>) resultTagsObj) {
+                if (t != null && !t.toString().trim().isEmpty()) {
+                    tags.add(t.toString().trim());
+                }
+            }
+        }
+        Object problemDomainCode = params.get("problemDomainCode");
+        if (problemDomainCode != null && !problemDomainCode.toString().isEmpty()) {
+            ProblemDomain domain = problemDomainMapper.selectOne(
+                    new LambdaQueryWrapper<ProblemDomain>()
+                            .eq(ProblemDomain::getCode, problemDomainCode.toString())
+                            .last("LIMIT 1"));
+            if (domain != null && domain.getContentTags() != null) {
+                for (String tag : domain.getContentTags().split(",")) {
+                    if (!tag.trim().isEmpty()) {
+                        tags.add(tag.trim());
+                    }
+                }
+            }
+        }
+        int size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("resultTags", new ArrayList<>(tags));
+        data.put("articles", filterArticles(tags, size));
+        data.put("knowledgeBases", filterKnowledge(tags, size));
+        data.put("foods", filterFoods(tags, size));
+        return Result.success(data);
+    }
+
+    /** 文章:tags 字段 LIKE 任一标签 */
+    private List<Article> filterArticles(Set<String> tags, int size) {
+        if (tags.isEmpty()) {
+            return new ArrayList<>();
+        }
+        LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<Article>()
+                .eq(Article::getStatus, "published")
+                .orderByDesc(Article::getIsFeatured)
+                .orderByDesc(Article::getPublishedAt)
+                .last("LIMIT " + size);
+        wrapper.and(w -> {
+            boolean first = true;
+            for (String tag : tags) {
+                if (first) {
+                    w.like(Article::getTags, tag);
+                    first = false;
+                } else {
+                    w.or().like(Article::getTags, tag);
+                }
+            }
+        });
+        return articleMapper.selectList(wrapper);
+    }
+
+    /** 知识库:itemName/category/description LIKE 任一标签 */
+    private List<HealthKnowledgeBase> filterKnowledge(Set<String> tags, int size) {
+        if (tags.isEmpty()) {
+            return new ArrayList<>();
+        }
+        LambdaQueryWrapper<HealthKnowledgeBase> wrapper = new LambdaQueryWrapper<HealthKnowledgeBase>()
+                .orderByAsc(HealthKnowledgeBase::getId)
+                .last("LIMIT " + size);
+        wrapper.and(w -> {
+            boolean first = true;
+            for (String tag : tags) {
+                if (first) {
+                    w.like(HealthKnowledgeBase::getItemName, tag)
+                            .or().like(HealthKnowledgeBase::getCategory, tag);
+                    first = false;
+                } else {
+                    w.or().like(HealthKnowledgeBase::getItemName, tag)
+                            .or().like(HealthKnowledgeBase::getCategory, tag);
+                }
+            }
+        });
+        return healthKnowledgeBaseMapper.selectList(wrapper);
+    }
+
+    /** 饮食:nutritionTags/suitableFor/name LIKE 任一标签 */
+    private List<Food> filterFoods(Set<String> tags, int size) {
+        if (tags.isEmpty()) {
+            return new ArrayList<>();
+        }
+        LambdaQueryWrapper<Food> wrapper = new LambdaQueryWrapper<Food>()
+                .orderByAsc(Food::getSortOrder)
+                .orderByAsc(Food::getId)
+                .last("LIMIT " + size);
+        wrapper.and(w -> {
+            boolean first = true;
+            for (String tag : tags) {
+                if (first) {
+                    w.like(Food::getNutritionTags, tag)
+                            .or().like(Food::getSuitableFor, tag)
+                            .or().like(Food::getName, tag);
+                    first = false;
+                } else {
+                    w.or().like(Food::getNutritionTags, tag)
+                            .or().like(Food::getSuitableFor, tag)
+                            .or().like(Food::getName, tag);
+                }
+            }
+        });
+        return foodMapper.selectList(wrapper);
+    }
+}

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

@@ -11,6 +11,7 @@ import javax.annotation.Resource;
 import java.util.List;
 import java.util.ArrayList;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/coupon")
@@ -30,7 +31,7 @@ public class CouponController {
     @PostMapping("/claim")
     public Result<String> claim(@RequestAttribute("userId") Long userId,
                                 @RequestBody Map<String, Object> params) {
-        Long couponId = Long.valueOf(params.get("couponId").toString());
+        Long couponId = ParamUtils.getLong(params.get("couponId"));
         String result = couponService.claim(userId, couponId);
         if ("领取成功".equals(result)) {
             return Result.success(result);
@@ -41,7 +42,7 @@ public class CouponController {
     @PostMapping("/apply")
     public Result<Map<String, Object>> apply(@RequestAttribute("userId") Long userId,
                                              @RequestBody Map<String, Object> params) {
-        Long userCouponId = Long.valueOf(params.get("userCouponId").toString());
+        Long userCouponId = ParamUtils.getLong(params.get("userCouponId"));
         String orderType = (String) params.get("orderType");
         Integer orderAmount = Integer.valueOf(params.get("orderAmount").toString());
         Integer discount = couponService.apply(userId, userCouponId, orderType, orderAmount);
@@ -54,7 +55,7 @@ public class CouponController {
     @PostMapping("/exchange")
     public Result<Map<String, Object>> exchange(@RequestAttribute("userId") Long userId,
                                                 @RequestBody Map<String, Object> params) {
-        Long couponId = Long.valueOf(params.get("couponId").toString());
+        Long couponId = ParamUtils.getLong(params.get("couponId"));
         Object memberId = params.get("memberId");
         Long childId = memberId != null ? Long.valueOf(memberId.toString()) : null;
         PointsExchangeRecord record = pointsExchangeService.exchangeCoupon(userId, childId, couponId);
@@ -68,7 +69,7 @@ public class CouponController {
 
     @PostMapping("/product")
     public Result<List<Coupon>> productCoupons(@RequestBody Map<String, Object> params) {
-        Long productId = Long.valueOf(params.get("productId").toString());
+        Long productId = ParamUtils.getLong(params.get("productId"));
         return Result.success(couponService.listByProduct(productId));
     }
 
@@ -80,8 +81,7 @@ public class CouponController {
             @RequestAttribute("userId") Long userId,
             @RequestBody Map<String, Object> params) {
         if (userId == null) return Result.error("未登录");
-        Long productId = params.get("productId") != null
-                ? Long.valueOf(params.get("productId").toString()) : null;
+        Long productId = ParamUtils.getLong(params.get("productId"));
         List<Coupon> coupons = couponService.listUnownedExchangeable(userId, productId);
         List<Map<String, Object>> result = new ArrayList<>();
         for (Coupon c : coupons) {

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

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "每日健康打卡", description = "每日健康打卡记录(饮食/运动/睡眠/饮水/心情)")
 @RestController
@@ -23,8 +24,7 @@ public class DailyCheckinController {
     @PostMapping("/list")
     public Result<List<DailyHealthCheckin>> list(@RequestBody Map<String, Object> params,
                                                  @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         if (memberId == null) {
             return Result.error("memberId不能为空");
         }
@@ -34,8 +34,7 @@ public class DailyCheckinController {
     @Operation(summary = "获取打卡详情")
     @PostMapping("/detail")
     public Result<DailyHealthCheckin> detail(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null
-                ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) {
             return Result.error("id不能为空");
         }
@@ -56,8 +55,7 @@ public class DailyCheckinController {
     @Operation(summary = "更新打卡记录")
     @PostMapping("/update")
     public Result<DailyHealthCheckin> update(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null
-                ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) {
             return Result.error("id不能为空");
         }
@@ -73,10 +71,8 @@ public class DailyCheckinController {
     @Operation(summary = "删除打卡记录")
     @PostMapping("/delete")
     public Result<String> delete(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null
-                ? Long.valueOf(params.get("id").toString()) : null;
-        Long userId = params.get("userId") != null
-                ? Long.valueOf(params.get("userId").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
+        Long userId = ParamUtils.getLong(params.get("userId"));
         if (id == null) {
             return Result.error("id不能为空");
         }

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

@@ -11,6 +11,7 @@ import javax.annotation.Resource;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 /**
  * 每日反馈接口 — 即时多巴胺反馈(L2: AI/预设深度反馈)
@@ -29,8 +30,7 @@ public class DailyFeedbackController {
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         DailyFeedback feedback = dailyFeedbackService.getTodayFeedback(memberId);
 
         // Auto-generate if none exists
@@ -47,8 +47,7 @@ public class DailyFeedbackController {
     @Operation(summary = "标记反馈为已读")
     @PostMapping("/read")
     public Result<String> markAsRead(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null
-                ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) {
             return Result.error("id不能为空");
         }
@@ -60,8 +59,7 @@ public class DailyFeedbackController {
     @PostMapping("/recent")
     public Result<List<DailyFeedback>> recent(@RequestBody Map<String, Object> params,
                                               @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         int limit = params.get("limit") != null
                 ? Integer.parseInt(params.get("limit").toString()) : 7;
         return Result.success(dailyFeedbackService.getRecentFeedbacks(memberId, limit));

+ 41 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/DailyTaskController.java

@@ -0,0 +1,41 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.DailyOverviewVO;
+import com.etotem.cfc.service.DailyOverviewService;
+import io.swagger.v3.oas.annotations.Operation;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+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.Map;
+import com.etotem.cfc.util.ParamUtils;
+
+/**
+ * 今日任务聚合接口(只读)
+ */
+@RestController
+@RequestMapping("/api/daily-task")
+public class DailyTaskController {
+
+    @Resource
+    private DailyOverviewService dailyOverviewService;
+
+    @Operation(summary = "今日任务总览(打卡/任务/阅读活动/测评报告四类)")
+    @PostMapping("/overview")
+    public Result<DailyOverviewVO> overview(
+            @RequestBody(required = false) Map<String, Object> params,
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId,
+            @RequestAttribute(value = "familyId", required = false) Long familyId) {
+        Long memberId = (params != null && params.get("memberId") != null)
+                ? ParamUtils.getLong(params.get("memberId")) : currentMemberId;
+        if (memberId == null) {
+            return Result.error("memberId不能为空");
+        }
+        return Result.success(dailyOverviewService.aggregate(userId, memberId, familyId));
+    }
+}

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

@@ -15,6 +15,7 @@ import javax.annotation.Resource;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "DAN测评", description = "DAN专注力测评相关接口")
 @RestController
@@ -93,12 +94,10 @@ public class DanAssessmentController {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
-        Long materialId = body.get("materialId") != null
-                ? Long.valueOf(body.get("materialId").toString()) : null;
+        Long materialId = ParamUtils.getLong(body.get("materialId"));
         Integer remindMonths = body.get("remindMonths") != null
                 ? Integer.valueOf(body.get("remindMonths").toString()) : 6;
-        Long createdBy = body.get("createdBy") != null
-                ? Long.valueOf(body.get("createdBy").toString()) : null;
+        Long createdBy = ParamUtils.getLong(body.get("createdBy"));
         boolean ok = assessmentService.enableFamilyAssessment(familyId, materialId, remindMonths, createdBy);
         return ok ? Result.success("启用成功") : Result.error("启用失败");
     }
@@ -123,16 +122,15 @@ public class DanAssessmentController {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
-        Long memberId = body.get("memberId") != null ? Long.valueOf(body.get("memberId").toString()) : currentMemberId;
-        Long materialId = body.get("materialId") != null
-                ? Long.valueOf(body.get("materialId").toString()) : null;
+        Long memberId = ParamUtils.getLong(body.get("memberId"), currentMemberId);
+        Long materialId = ParamUtils.getLong(body.get("materialId"));
         return Result.success(assessmentService.createRecord(familyId, memberId, materialId));
     }
 
     @Operation(summary = "完成测评记录")
     @PostMapping("/record/complete")
     public Result<String> completeRecord(@RequestBody Map<String, Object> body) {
-        Long recordId = Long.valueOf(body.get("recordId").toString());
+        Long recordId = ParamUtils.getLong(body.get("recordId"));
         boolean ok = assessmentService.completeRecord(recordId);
         return ok ? Result.success("完成成功") : Result.error("记录不存在");
     }
@@ -188,9 +186,9 @@ public class DanAssessmentController {
     public Result<AssessmentAppointment> createAppointment(@RequestBody Map<String, Object> body,
                                                            @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId,
                                                            @RequestAttribute("userId") Long userId) {
-        Long memberId = body.get("memberId") != null ? Long.valueOf(body.get("memberId").toString()) : currentMemberId;
-        Long guideId = Long.valueOf(body.get("guideId").toString());
-        Long packageId = body.get("packageId") != null ? Long.valueOf(body.get("packageId").toString()) : null;
+        Long memberId = ParamUtils.getLong(body.get("memberId"), currentMemberId);
+        Long guideId = ParamUtils.getLong(body.get("guideId"));
+        Long packageId = ParamUtils.getLong(body.get("packageId"));
         String appointmentDate = (String) body.get("appointmentDate");
         String appointmentTime = (String) body.get("appointmentTime");
         String notes = (String) body.get("notes");
@@ -202,8 +200,8 @@ public class DanAssessmentController {
     @Operation(summary = "确认预约")
     @PostMapping("/appointment/confirm")
     public Result<String> confirmAppointment(@RequestBody Map<String, Object> body) {
-        Long id = Long.valueOf(body.get("id").toString());
-        Long teacherId = Long.valueOf(body.get("teacherId").toString());
+        Long id = ParamUtils.getLong(body.get("id"));
+        Long teacherId = ParamUtils.getLong(body.get("teacherId"));
         boolean ok = appointmentService.confirmAppointment(id, teacherId);
         return ok ? Result.success("确认成功") : Result.error("确认失败");
     }
@@ -211,8 +209,8 @@ public class DanAssessmentController {
     @Operation(summary = "完成预约")
     @PostMapping("/appointment/complete")
     public Result<String> completeAppointment(@RequestBody Map<String, Object> body) {
-        Long id = Long.valueOf(body.get("id").toString());
-        Long teacherId = Long.valueOf(body.get("teacherId").toString());
+        Long id = ParamUtils.getLong(body.get("id"));
+        Long teacherId = ParamUtils.getLong(body.get("teacherId"));
         boolean ok = appointmentService.completeAppointment(id, teacherId);
         return ok ? Result.success("完成成功") : Result.error("完成失败");
     }
@@ -220,7 +218,7 @@ public class DanAssessmentController {
     @Operation(summary = "取消预约")
     @PostMapping("/appointment/cancel")
     public Result<String> cancelAppointment(@RequestBody Map<String, Object> body) {
-        Long id = Long.valueOf(body.get("id").toString());
+        Long id = ParamUtils.getLong(body.get("id"));
         boolean ok = appointmentService.cancelAppointment(id);
         return ok ? Result.success("取消成功") : Result.error("取消失败");
     }
@@ -228,7 +226,7 @@ public class DanAssessmentController {
     @Operation(summary = "获取用户的预约列表")
     @PostMapping("/appointment/user")
     public Result<List<Map<String, Object>>> getUserAppointments(@RequestBody Map<String, Object> body) {
-        Long userId = body.get("userId") != null ? Long.valueOf(body.get("userId").toString()) : null;
+        Long userId = ParamUtils.getLong(body.get("userId"));
         if (userId == null) return Result.error("userId 不能为空");
         List<AssessmentAppointment> list = appointmentService.getByUserId(userId);
         List<Map<String, Object>> result = new java.util.ArrayList<>();
@@ -278,14 +276,13 @@ public class DanAssessmentController {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
-        Long memberId = body.get("memberId") != null ? Long.valueOf(body.get("memberId").toString()) : currentMemberId;
-        Long guideId = Long.valueOf(body.get("guideId").toString());
-        Long packageId = body.get("packageId") != null ? Long.valueOf(body.get("packageId").toString()) : null;
+        Long memberId = ParamUtils.getLong(body.get("memberId"), currentMemberId);
+        Long guideId = ParamUtils.getLong(body.get("guideId"));
+        Long packageId = ParamUtils.getLong(body.get("packageId"));
         String guideName = (String) body.get("guideName");
         String packageName = (String) body.get("packageName");
-        Long totalPrice = body.get("totalPrice") != null ? Long.valueOf(body.get("totalPrice").toString()) : 0L;
-        Long discountAmount = body.get("discountAmount") != null
-                ? Long.valueOf(body.get("discountAmount").toString()) : 0L;
+        Long totalPrice = ParamUtils.getLong(body.get("totalPrice"), 0L);
+        Long discountAmount = ParamUtils.getLong(body.get("discountAmount"), 0L);
 
         AssessmentOrder order = orderService.createOrder(familyId, userId, memberId,
                 guideId, packageId, guideName, packageName, totalPrice, discountAmount);
@@ -298,8 +295,7 @@ public class DanAssessmentController {
                                                 @RequestAttribute("userId") Long userId) {
         String orderNo = (String) body.get("orderNo");
         String payType = (String) body.get("payType");
-        Long appointmentId = body.get("appointmentId") != null
-                ? Long.valueOf(body.get("appointmentId").toString()) : null;
+        Long appointmentId = ParamUtils.getLong(body.get("appointmentId"));
 
         // testMode 兼容:mock 直接标记已付,跳过虚拟支付(保留现有模拟逻辑)
         if (testMode) {
@@ -383,7 +379,7 @@ public class DanAssessmentController {
     @PostMapping("/report/confirm")
     public Result<String> confirmReport(@RequestBody Map<String, Object> body,
                                          @RequestAttribute("userId") Long userId) {
-        Long draftId = body.get("draftId") != null ? Long.valueOf(body.get("draftId").toString()) : null;
+        Long draftId = ParamUtils.getLong(body.get("draftId"));
         if (draftId == null) {
             return Result.error("draftId 不能为空");
         }
@@ -399,7 +395,7 @@ public class DanAssessmentController {
     @Operation(summary = "获取某孩子某维度的 DAN 报告列表")
     @PostMapping("/report/child")
     public Result<List<DanAssessmentResult>> getChildDanReports(@RequestBody Map<String, Object> body) {
-        Long memberId = body.get("memberId") != null ? Long.valueOf(body.get("memberId").toString()) : null;
+        Long memberId = ParamUtils.getLong(body.get("memberId"));
         String dimension = (String) body.get("dimension");
         if (memberId == null || dimension == null) {
             return Result.error("memberId 和 dimension 不能为空");

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

@@ -11,6 +11,7 @@ import org.springframework.web.multipart.MultipartFile;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "DAN报告管理(独立化)", description = "DAN PDF报告上传/解析/确认独立流程")
 @RestController
@@ -67,7 +68,7 @@ public class DanReportController {
 
     @PostMapping("/list")
     public Result<List<DanReportUploadVO>> list(@RequestBody Map<String, Object> body) {
-        Long memberId = body.get("memberId") != null ? Long.valueOf(body.get("memberId").toString()) : null;
+        Long memberId = ParamUtils.getLong(body.get("memberId"));
         String dimension = (String) body.get("dimension");
         return uploadService.listByMember(memberId, dimension);
     }

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

@@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.RestController;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "销售邀请", description = "服务商销售邀请码和小程序端接口")
 @RestController
@@ -29,8 +30,7 @@ public class DistributionInviteController {
     public Result<String> generate(
             @RequestAttribute("userId") Long userId,
             @RequestBody Map<String, Object> params) {
-        Long systemId = params.get("systemId") != null
-                ? Long.valueOf(params.get("systemId").toString()) : null;
+        Long systemId = ParamUtils.getLong(params.get("systemId"));
         if (systemId == null) return Result.error("systemId不能为空");
         return distributionService.generateInviteCode(systemId, userId);
     }
@@ -79,8 +79,7 @@ public class DistributionInviteController {
     public Result<Map<String, Object>> myPosition(
             @RequestAttribute("userId") Long userId,
             @RequestBody Map<String, Object> params) {
-        Long systemId = params.get("systemId") != null
-                ? Long.valueOf(params.get("systemId").toString()) : null;
+        Long systemId = ParamUtils.getLong(params.get("systemId"));
         if (systemId == null) return Result.error("systemId不能为空");
         return distributionService.getUserPosition(systemId, userId);
     }

+ 21 - 4
cfc-backend/src/main/java/com/etotem/cfc/controller/EmiReportController.java

@@ -8,7 +8,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
+import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "EMI心理报告", description = "EMI心理四维评估(情绪/韧性/压力应对/自我认知)")
 @RestController
@@ -22,17 +24,32 @@ public class EmiReportController {
     @PostMapping("/latest")
     public Result<EmiReportDTO> getLatestReport(@RequestBody Map<String, Object> body,
                                                 @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = body.get("memberId") != null
-                ? Long.valueOf(body.get("memberId").toString())
-                : currentMemberId;
+        Long memberId = ParamUtils.getLong(body.get("memberId"), currentMemberId);
         if (memberId == null) {
             return Result.error("memberId不能为空");
         }
 
         EmiReportDTO dto = emiReportService.getChildLatestEmiReport(memberId);
         if (dto == null) {
-            return Result.error("未找到EMI测评结果");
+            return Result.success(null);
         }
         return Result.success(dto);
     }
+
+    @Operation(summary = "获取家庭所有孩子的EMI心理报告(用于计算家庭平均分)")
+    @PostMapping("/family")
+    public Result<List<EmiReportDTO>> getFamilyLatestReports(
+            @RequestBody Map<String, Object> body,
+            @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+        if (body == null || body.get("childIds") == null) {
+            return Result.error("childIds不能为空");
+        }
+        @SuppressWarnings("unchecked")
+        java.util.List<Long> childIds = (java.util.List<Long>) body.get("childIds");
+        if (childIds == null || childIds.isEmpty()) {
+            return Result.success(new java.util.ArrayList<>());
+        }
+        List<EmiReportDTO> list = emiReportService.getFamilyLatestEmiReports(childIds);
+        return Result.success(list);
+    }
 }

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

@@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import java.util.*;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "家庭能量沙盘", description = "家庭五维能量数据接口")
 @RestController
@@ -127,8 +128,7 @@ public class EnergyController {
         if (params == null) {
             return Result.error("请求体不能为空");
         }
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         if (memberId == null) {
             return Result.error("memberId不能为空");
         }
@@ -148,8 +148,7 @@ public class EnergyController {
             @RequestBody Map<String, Object> params,
             @RequestAttribute(value = "familyId", required = false) Long familyId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         if (memberId == null) {
             return Result.error("memberId不能为空");
         }
@@ -175,12 +174,9 @@ public class EnergyController {
             @RequestBody Map<String, Object> params,
             @RequestAttribute(value = "familyId", required = false) Long familyId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
-        Long taskId = params.get("taskId") != null
-                ? Long.valueOf(params.get("taskId").toString()) : null;
-        Long amount = params.get("amount") != null
-                ? Long.valueOf(params.get("amount").toString()) : null;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
+        Long taskId = ParamUtils.getLong(params.get("taskId"));
+        Long amount = ParamUtils.getLong(params.get("amount"));
         String dimensionCode = (String) params.get("dimensionCode");
 
         if (memberId == null || taskId == null || amount == null) {
@@ -203,12 +199,9 @@ public class EnergyController {
             @RequestBody Map<String, Object> params,
             @RequestAttribute(value = "familyId", required = false) Long familyId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
-        Long wishId = params.get("wishId") != null
-                ? Long.valueOf(params.get("wishId").toString()) : null;
-        Long amount = params.get("amount") != null
-                ? Long.valueOf(params.get("amount").toString()) : null;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
+        Long wishId = ParamUtils.getLong(params.get("wishId"));
+        Long amount = ParamUtils.getLong(params.get("amount"));
 
         if (memberId == null || wishId == null || amount == null) {
             return Result.error("memberId、wishId和amount不能为空");
@@ -231,8 +224,7 @@ public class EnergyController {
             @RequestAttribute(value = "familyId", required = false) Long familyId,
             @RequestAttribute("userId") Long userId) {
         String sourceType = (String) params.get("sourceType");
-        Long sourceId = params.get("sourceId") != null
-                ? Long.valueOf(params.get("sourceId").toString()) : null;
+        Long sourceId = ParamUtils.getLong(params.get("sourceId"));
         String sourceName = (String) params.get("sourceName");
 
         if (sourceType == null || sourceId == null) {
@@ -256,8 +248,7 @@ public class EnergyController {
     @PostMapping("/score-history")
     public Result<List<com.etotem.cfc.entity.FiveDimensionScore>> getScoreHistory(
             @RequestBody Map<String, Object> params) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : null;
+        Long memberId = ParamUtils.getLong(params.get("memberId"));
         String memberType = (String) params.get("memberType");
         if (memberId == null || memberType == null) {
             return Result.error("memberId和memberType不能为空");
@@ -299,8 +290,7 @@ public class EnergyController {
     public Result<MemberEnergyDTO> getWealthDetail(
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : null;
+        Long memberId = ParamUtils.getLong(params.get("memberId"));
         String memberType = (String) params.get("memberType");
 
         if (memberId == null || memberType == null) {

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

@@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/health/challenge")
@@ -43,8 +44,7 @@ public class FamilyChallengeController {
         if (currentMemberId == null) {
             return Result.error("未识别当前成员");
         }
-        Long challengeId = params.get("challengeId") != null
-                ? Long.valueOf(params.get("challengeId").toString()) : null;
+        Long challengeId = ParamUtils.getLong(params.get("challengeId"));
         int delta = params.get("delta") != null
                 ? Integer.parseInt(params.get("delta").toString()) : 0;
         return familyChallengeService.updateProgress(challengeId, currentMemberId, delta);
@@ -56,8 +56,7 @@ public class FamilyChallengeController {
         if (currentMemberId == null) {
             return Result.error("未识别当前成员");
         }
-        Long challengeId = params.get("challengeId") != null
-                ? Long.valueOf(params.get("challengeId").toString()) : null;
+        Long challengeId = ParamUtils.getLong(params.get("challengeId"));
         String action = params.get("action") != null ? params.get("action").toString() : null;
         return familyChallengeService.respondChallenge(challengeId, currentMemberId, action);
     }

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

@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 /**
  * 家庭平台积分(CF值)接口
@@ -62,8 +63,7 @@ public class FamilyPlatformBalanceController {
         if (user == null || user.getFamilyId() == null) {
             return Result.error("请先加入家庭");
         }
-        Long couponId = params.get("couponId") != null
-                ? Long.valueOf(params.get("couponId").toString()) : null;
+        Long couponId = ParamUtils.getLong(params.get("couponId"));
         if (couponId == null) return Result.error("couponId不能为空");
         try {
             familyPlatformPointsService.exchangeCouponByCf(user.getFamilyId(), couponId, userId);
@@ -79,8 +79,7 @@ public class FamilyPlatformBalanceController {
     public Result<Map<String, Object>> adminBalance(@RequestBody Map<String, Object> params,
                                                      @RequestAttribute("role") String role) {
         if (!"admin".equals(role)) return Result.error("无权限");
-        Long familyId = params.get("familyId") != null
-                ? Long.valueOf(params.get("familyId").toString()) : null;
+        Long familyId = ParamUtils.getLong(params.get("familyId"));
         if (familyId == null) return Result.error("familyId不能为空");
         return Result.success(familyPlatformPointsService.getBalance(familyId));
     }
@@ -90,8 +89,7 @@ public class FamilyPlatformBalanceController {
     public Result<Page<FamilyPlatformBalanceLog>> adminLogs(@RequestBody Map<String, Object> params,
                                                              @RequestAttribute("role") String role) {
         if (!"admin".equals(role)) return Result.error("无权限");
-        Long familyId = params.get("familyId") != null
-                ? Long.valueOf(params.get("familyId").toString()) : null;
+        Long familyId = ParamUtils.getLong(params.get("familyId"));
         int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
         int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
         if (familyId == null) return Result.error("familyId不能为空");
@@ -104,8 +102,7 @@ public class FamilyPlatformBalanceController {
                                        @RequestAttribute("role") String role,
                                        @RequestAttribute("userId") Long adminId) {
         if (!"admin".equals(role)) return Result.error("无权限");
-        Long familyId = params.get("familyId") != null
-                ? Long.valueOf(params.get("familyId").toString()) : null;
+        Long familyId = ParamUtils.getLong(params.get("familyId"));
         Integer amount = params.get("amount") != null
                 ? ((Number) params.get("amount")).intValue() : 0;
         String remark = (String) params.get("remark");

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

@@ -10,6 +10,7 @@ import javax.annotation.Resource;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "游戏记录管理", description = "游戏记录与统计相关接口")
 @RestController
@@ -22,7 +23,7 @@ public class GameRecordController {
     @Operation(summary = "保存游戏记录")
     @PostMapping("/record")
     public Result<Map<String, Object>> saveRecord(@RequestBody Map<String, Object> body) {
-        Long memberId = Long.valueOf(body.get("memberId").toString());
+        Long memberId = ParamUtils.getLong(body.get("memberId"));
         String gameCode = (String) body.get("gameCode");
         Integer score = body.get("score") != null
                 ? Integer.valueOf(body.get("score").toString()) : null;
@@ -39,7 +40,7 @@ public class GameRecordController {
     @Operation(summary = "获取游戏记录历史")
     @PostMapping("/history")
     public Result<Map<String, Object>> getHistory(@RequestBody Map<String, Object> body) {
-        Long memberId = Long.valueOf(body.get("memberId").toString());
+        Long memberId = ParamUtils.getLong(body.get("memberId"));
         String gameCode = (String) body.get("gameCode");
         Integer page = body.get("page") != null
                 ? Integer.valueOf(body.get("page").toString()) : 1;
@@ -51,14 +52,14 @@ public class GameRecordController {
     @Operation(summary = "获取各游戏最佳成绩")
     @PostMapping("/best")
     public Result<List<Map<String, Object>>> getBestScores(@RequestBody Map<String, Object> body) {
-        Long memberId = Long.valueOf(body.get("memberId").toString());
+        Long memberId = ParamUtils.getLong(body.get("memberId"));
         return Result.success(gameRecordService.getBestScores(memberId));
     }
 
     @Operation(summary = "获取游戏统计概览")
     @PostMapping("/stats")
     public Result<Map<String, Object>> getStats(@RequestBody Map<String, Object> body) {
-        Long memberId = Long.valueOf(body.get("memberId").toString());
+        Long memberId = ParamUtils.getLong(body.get("memberId"));
         return Result.success(gameRecordService.getStats(memberId));
     }
 

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

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "健康预警", description = "P1-2 健康行为智能预警")
 @RestController
@@ -23,16 +24,14 @@ public class HealthAlertController {
     @PostMapping("/list")
     public Result<List<HealthAlert>> list(@RequestBody Map<String, Object> params,
                                           @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         return Result.success(healthAlertService.getActiveAlerts(memberId));
     }
 
     @Operation(summary = "关闭预警")
     @PostMapping("/dismiss")
     public Result<String> dismiss(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null
-                ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id不能为空");
         healthAlertService.dismiss(id);
         return Result.success("ok");

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

@@ -11,6 +11,7 @@ import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 /**
  * 健康打卡接口 — 身体维度日常打卡记录
@@ -29,8 +30,7 @@ public class HealthCheckinController {
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         String yearMonth = (String) params.get("yearMonth");
         List<HealthCheckin> list = healthCheckinService.getCheckins(userId, memberId, yearMonth);
         return Result.success(list);
@@ -44,8 +44,7 @@ public class HealthCheckinController {
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
         HealthCheckin checkin = new HealthCheckin();
         checkin.setUserId(userId);
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         checkin.setChildId(memberId);
         if (memberId == null) {
             return Result.error("memberId不能为空");
@@ -82,8 +81,7 @@ public class HealthCheckinController {
     @Operation(summary = "删除打卡记录")
     @PostMapping("/delete")
     public Result<String> delete(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null
-                ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) {
             return Result.error("id不能为空");
         }
@@ -97,8 +95,7 @@ public class HealthCheckinController {
             @RequestBody Map<String, Object> params,
             @RequestAttribute(value = "userId", required = false) Long userId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         long total = healthCheckinService.countCheckins(memberId);
         java.util.Map<String, Object> stats = new java.util.HashMap<String, Object>();
         stats.put("totalCheckins", total);

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

@@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/health/circle")
@@ -56,8 +57,7 @@ public class HealthCircleController {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
-        Long circleId = params.get("circleId") != null
-                ? Long.valueOf(params.get("circleId").toString()) : null;
+        Long circleId = ParamUtils.getLong(params.get("circleId"));
         return healthCircleService.leaveCircle(circleId, familyId);
     }
 
@@ -68,8 +68,7 @@ public class HealthCircleController {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
-        Long circleId = params.get("circleId") != null
-                ? Long.valueOf(params.get("circleId").toString()) : null;
+        Long circleId = ParamUtils.getLong(params.get("circleId"));
         return Result.success(healthCircleService.getCircleRanking(circleId));
     }
 }

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

@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "健康维度", description = "七维健康指标查询与录入")
 @RestController
@@ -26,8 +27,7 @@ public class HealthDimensionController {
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         if (memberId == null) {
             return Result.error("memberId不能为空");
         }
@@ -65,8 +65,7 @@ public class HealthDimensionController {
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         String dimension = (String) params.get("dimension");
         int limit = params.get("limit") != null
                 ? Integer.parseInt(params.get("limit").toString()) : 10;

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

@@ -13,6 +13,7 @@ import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import com.etotem.cfc.util.SortUtil;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "运动打卡")
 @RestController
@@ -26,7 +27,7 @@ public class HealthExerciseController {
     @PostMapping("/list")
     public Result<List<HealthExerciseRecord>> list(@RequestBody Map<String, Object> params,
                                                    @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         if (memberId == null) return Result.error("memberId不能为空");
         LambdaQueryWrapper<HealthExerciseRecord> wrapper = new LambdaQueryWrapper<HealthExerciseRecord>()
                 .eq(HealthExerciseRecord::getMemberId, memberId)

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

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "健康生长记录", description = "儿童生长记录(身高/体重/BMI/头围等)")
 @RestController
@@ -22,8 +23,7 @@ public class HealthGrowthRecordController {
     @Operation(summary = "获取生长记录列表")
     @PostMapping("/list")
     public Result<List<HealthGrowthRecord>> list(@RequestBody Map<String, Object> params) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : null;
+        Long memberId = ParamUtils.getLong(params.get("memberId"));
         if (memberId == null) {
             return Result.error("memberId不能为空");
         }
@@ -33,8 +33,7 @@ public class HealthGrowthRecordController {
     @Operation(summary = "获取生长记录详情")
     @PostMapping("/detail")
     public Result<HealthGrowthRecord> detail(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null
-                ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) {
             return Result.error("id不能为空");
         }
@@ -51,8 +50,7 @@ public class HealthGrowthRecordController {
     @Operation(summary = "更新生长记录")
     @PostMapping("/update")
     public Result<HealthGrowthRecord> update(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null
-                ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) {
             return Result.error("id不能为空");
         }
@@ -66,8 +64,7 @@ public class HealthGrowthRecordController {
     @Operation(summary = "删除生长记录")
     @PostMapping("/delete")
     public Result<String> delete(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null
-                ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) {
             return Result.error("id不能为空");
         }

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

@@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/health/interact")
@@ -22,12 +23,10 @@ public class HealthInteractionController {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
-        Long toUserId = params.get("toUserId") != null
-                ? Long.valueOf(params.get("toUserId").toString()) : null;
+        Long toUserId = ParamUtils.getLong(params.get("toUserId"));
         String actionType = params.get("actionType") != null
                 ? params.get("actionType").toString() : null;
-        Long actionId = params.get("actionId") != null
-                ? Long.valueOf(params.get("actionId").toString()) : null;
+        Long actionId = ParamUtils.getLong(params.get("actionId"));
         return healthInteractionService.like(userId, toUserId, actionType, actionId, familyId);
     }
 }

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

@@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
 import java.util.*;
+import com.etotem.cfc.util.ParamUtils;
 
 @Slf4j
 @Tag(name = "健康知识库")
@@ -82,7 +83,7 @@ public class HealthKnowledgeBaseController {
     @Operation(summary = "删除知识库条目")
     @PostMapping("/delete")
     public Result delete(@RequestBody Map<String, Object> params) {
-        Long id = Long.valueOf(params.get("id").toString());
+        Long id = ParamUtils.getLong(params.get("id"));
         healthKnowledgeBaseService.removeById(id);
         return Result.success();
     }

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

@@ -12,7 +12,9 @@ import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.service.PointsService;
 import com.etotem.cfc.util.SortUtil;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "饮食打卡")
 @RestController
@@ -22,10 +24,15 @@ public class HealthMealController {
     @Resource
     private HealthMealRecordMapper healthMealRecordMapper;
 
+    @Resource
+    private PointsService pointsService;
+
     @Operation(summary = "获取饮食打卡列表")
     @PostMapping("/list")
-    public Result<List<HealthMealRecord>> list(@RequestBody Map<String, Object> params) {
-        Long memberId = params.get("memberId") != null ? Long.valueOf(params.get("memberId").toString()) : null;
+    public Result<List<HealthMealRecord>> list(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         if (memberId == null) return Result.error("memberId不能为空");
         LambdaQueryWrapper<HealthMealRecord> wrapper = new LambdaQueryWrapper<HealthMealRecord>()
                 .eq(HealthMealRecord::getMemberId, memberId)
@@ -38,6 +45,12 @@ public class HealthMealController {
     public Result<HealthMealRecord> create(@RequestBody HealthMealRecord record) {
         record.setCreatedAt(new Date());
         healthMealRecordMapper.insert(record);
+        // 饮食打卡返积分:每次记录奖励 5 积分(不影响打卡主流程)
+        try {
+            pointsService.awardCheckinPoints(record.getMemberId(), 5, "饮食打卡");
+        } catch (Exception e) {
+            // 积分发放失败不影响打卡记录
+        }
         return Result.success(record);
     }
 }

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

@@ -13,6 +13,7 @@ import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import com.etotem.cfc.util.SortUtil;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "冥想打卡")
 @RestController
@@ -25,7 +26,7 @@ public class HealthMeditationController {
     @Operation(summary = "获取冥想打卡列表")
     @PostMapping("/list")
     public Result<List<HealthMeditationRecord>> list(@RequestBody Map<String, Object> params) {
-        Long memberId = params.get("memberId") != null ? Long.valueOf(params.get("memberId").toString()) : null;
+        Long memberId = ParamUtils.getLong(params.get("memberId"));
         if (memberId == null) return Result.error("memberId不能为空");
         LambdaQueryWrapper<HealthMeditationRecord> wrapper = new LambdaQueryWrapper<HealthMeditationRecord>()
                 .eq(HealthMeditationRecord::getMemberId, memberId)

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

@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "健康方案", description = "健康方案生成与管理")
 @RestController
@@ -70,7 +71,7 @@ public class HealthPlanController {
     @Operation(summary = "查询方案详情")
     @PostMapping("/detail")
     public Result<HealthPlan> detail(@RequestBody Map<String, Object> params, @RequestAttribute("userId") Long userId) {
-        Long planId = params.get("planId") != null ? Long.valueOf(params.get("planId").toString()) : null;
+        Long planId = ParamUtils.getLong(params.get("planId"));
         if (planId == null) return Result.error("planId不能为空");
         HealthPlan plan = healthPlanService.getPlanDetail(planId);
         if (plan == null) return Result.error("方案不存在");

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

@@ -68,6 +68,7 @@ import java.util.UUID;
 
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.client.RestTemplate;
+import com.etotem.cfc.util.ParamUtils;
 
 /**
  * 健康检测报告接口 — 体检报告/菌群检测报告的录入与查询
@@ -164,8 +165,7 @@ public class HealthReportController {
             return Result.noFamily("请先创建或加入家庭");
         }
 
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         if (memberId == null) {
             return Result.error("memberId不能为空");
         }
@@ -245,8 +245,7 @@ public class HealthReportController {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         if (memberId == null) {
             return Result.error("memberId不能为空");
         }
@@ -263,8 +262,7 @@ public class HealthReportController {
             @RequestBody Map<String, Object> params,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId,
             @RequestAttribute(value = "familyId", required = false) Long familyId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         if (memberId != null) {
             List<HealthReport> reports = healthReportService.getUserReports(memberId);
             return Result.success(reports);
@@ -308,8 +306,7 @@ public class HealthReportController {
     @PostMapping("/indicator/list")
     public Result<List<HealthIndicator>> listIndicators(
             @RequestBody Map<String, Object> params) {
-        Long reportId = params.get("reportId") != null
-                ? Long.valueOf(params.get("reportId").toString()) : null;
+        Long reportId = ParamUtils.getLong(params.get("reportId"));
         if (reportId == null) {
             return Result.error("reportId不能为空");
         }
@@ -325,8 +322,7 @@ public class HealthReportController {
     public Result<List<HealthIndicator>> getBodyDetail(
             @RequestBody Map<String, Object> params,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         if (memberId == null) {
             return Result.error("memberId不能为空");
         }
@@ -341,8 +337,7 @@ public class HealthReportController {
     @PostMapping("/report/detail")
     public Result<Map<String, Object>> getReportDetail(
             @RequestBody Map<String, Object> params) {
-        Long reportId = params.get("reportId") != null
-                ? Long.valueOf(params.get("reportId").toString()) : null;
+        Long reportId = ParamUtils.getLong(params.get("reportId"));
         if (reportId == null) {
             return Result.error("reportId不能为空");
         }
@@ -359,13 +354,11 @@ public class HealthReportController {
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId) {
 
-        Long reportId = params.get("reportId") != null
-                ? Long.valueOf(params.get("reportId").toString()) : null;
+        Long reportId = ParamUtils.getLong(params.get("reportId"));
         if (reportId == null) {
             return Result.error("reportId不能为空");
         }
-        Long subjectId = params.get("subjectId") != null
-                ? Long.valueOf(params.get("subjectId").toString()) : null;
+        Long subjectId = ParamUtils.getLong(params.get("subjectId"));
         if (subjectId == null) {
             return Result.error("subjectId不能为空");
         }
@@ -380,6 +373,21 @@ public class HealthReportController {
         return Result.success("已保存");
     }
 
+    /**
+     * 删除健康报告(逻辑删除 + 清理关联数据)
+     */
+    @Operation(summary = "删除健康报告")
+    @PostMapping("/report/delete")
+    public Result<String> deleteReport(
+            @RequestBody Map<String, Object> params) {
+        Long reportId = ParamUtils.getLong(params.get("reportId"));
+        if (reportId == null) {
+            return Result.error("reportId不能为空");
+        }
+        healthReportService.deleteReport(reportId);
+        return Result.success("已删除");
+    }
+
     /**
      * 上传并解析肠道菌群报告PDF(向后兼容 — 内部自动执行两阶段:解析→确认)
      */
@@ -550,8 +558,7 @@ public class HealthReportController {
             @RequestAttribute(value = "familyId", required = false) Long familyId,
             @RequestAttribute("userId") Long userId) {
 
-        Long draftId = params.get("draftId") != null
-                ? Long.valueOf(params.get("draftId").toString()) : null;
+        Long draftId = ParamUtils.getLong(params.get("draftId"));
         if (draftId == null) {
             return Result.error("draftId不能为空");
         }
@@ -876,15 +883,14 @@ public class HealthReportController {
             @RequestAttribute("userId") Long userId) {
 
         if ("tongue".equals(type)) {
-            Long recordId = Long.valueOf(params.get("recordId").toString());
+            Long recordId = ParamUtils.getLong(params.get("recordId"));
             @SuppressWarnings("unchecked")
             List<Map<String, Object>> indicators = (List<Map<String, Object>>) params.getOrDefault("indicators", null);
             Map<String, Object> result = tongueDiagnosisService.confirm(recordId, indicators);
             return Result.success(result);
         }
 
-        Long draftId = params.get("draftId") != null
-                ? Long.valueOf(params.get("draftId").toString()) : null;
+        Long draftId = ParamUtils.getLong(params.get("draftId"));
         if (draftId == null) {
             return Result.error("draftId不能为空");
         }
@@ -909,8 +915,7 @@ public class HealthReportController {
                 payload = objectMapper.readValue(payloadJson, ParsedReportPayload.Payload.class);
             }
 
-            Long subjectId = params.get("subjectId") != null
-                    ? Long.valueOf(params.get("subjectId").toString()) : null;
+            Long subjectId = ParamUtils.getLong(params.get("subjectId"));
 
             HealthReport report = buildReportFromPayload(draft, payload, subjectId);
 
@@ -998,13 +1003,12 @@ public class HealthReportController {
             @RequestAttribute("userId") Long userId) {
 
         if ("tongue".equals(type)) {
-            Long recordId = Long.valueOf(params.get("recordId").toString());
+            Long recordId = ParamUtils.getLong(params.get("recordId"));
             tongueDiagnosisService.discard(recordId);
             return Result.success("草稿已删除");
         }
 
-        Long draftId = params.get("draftId") != null
-                ? Long.valueOf(params.get("draftId").toString()) : null;
+        Long draftId = ParamUtils.getLong(params.get("draftId"));
         if (draftId == null) {
             return Result.error("draftId不能为空");
         }
@@ -1028,10 +1032,8 @@ public class HealthReportController {
     @PostMapping("/report/bind")
     public Result<String> bindReport(
             @RequestBody Map<String, Object> params) {
-        Long reportId = params.get("reportId") != null
-                ? Long.valueOf(params.get("reportId").toString()) : null;
-        Long subjectId = params.get("subjectId") != null
-                ? Long.valueOf(params.get("subjectId").toString()) : null;
+        Long reportId = ParamUtils.getLong(params.get("reportId"));
+        Long subjectId = ParamUtils.getLong(params.get("subjectId"));
         if (reportId == null || subjectId == null) {
             return Result.error("reportId和subjectId不能为空");
         }
@@ -1060,10 +1062,8 @@ public class HealthReportController {
     @PostMapping("/report/permission/add")
     public Result<String> addReportPermission(
             @RequestBody Map<String, Object> params) {
-        Long reportId = params.get("reportId") != null
-                ? Long.valueOf(params.get("reportId").toString()) : null;
-        Long targetUserId = params.get("userId") != null
-                ? Long.valueOf(params.get("userId").toString()) : null;
+        Long reportId = ParamUtils.getLong(params.get("reportId"));
+        Long targetUserId = ParamUtils.getLong(params.get("userId"));
         if (reportId == null || targetUserId == null) {
             return Result.error("reportId和userId不能为空");
         }
@@ -1078,10 +1078,8 @@ public class HealthReportController {
     @PostMapping("/report/permission/remove")
     public Result<String> removeReportPermission(
             @RequestBody Map<String, Object> params) {
-        Long reportId = params.get("reportId") != null
-                ? Long.valueOf(params.get("reportId").toString()) : null;
-        Long targetUserId = params.get("userId") != null
-                ? Long.valueOf(params.get("userId").toString()) : null;
+        Long reportId = ParamUtils.getLong(params.get("reportId"));
+        Long targetUserId = ParamUtils.getLong(params.get("userId"));
         if (reportId == null || targetUserId == null) {
             return Result.error("reportId和userId不能为空");
         }
@@ -1096,10 +1094,8 @@ public class HealthReportController {
     @PostMapping("/report/permission/check")
     public Result<Boolean> checkReportPermission(
             @RequestBody Map<String, Object> params) {
-        Long reportId = params.get("reportId") != null
-                ? Long.valueOf(params.get("reportId").toString()) : null;
-        Long targetUserId = params.get("userId") != null
-                ? Long.valueOf(params.get("userId").toString()) : null;
+        Long reportId = ParamUtils.getLong(params.get("reportId"));
+        Long targetUserId = ParamUtils.getLong(params.get("userId"));
         if (reportId == null || targetUserId == null) {
             return Result.error("reportId和userId不能为空");
         }
@@ -1111,13 +1107,11 @@ public class HealthReportController {
     @PostMapping("/nutrition/deficiency")
     public Result<List<NutritionDeficiencyRecord>> getDeficiencyAnalysis(
             @RequestBody Map<String, Object> params) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : null;
+        Long memberId = ParamUtils.getLong(params.get("memberId"));
         if (memberId == null) {
             return Result.error("memberId不能为空");
         }
-        Long reportId = params.get("reportId") != null
-                ? Long.valueOf(params.get("reportId").toString()) : null;
+        Long reportId = ParamUtils.getLong(params.get("reportId"));
         List<NutritionDeficiencyRecord> records = nutritionDeficiencyService.analyzeDeficiency(memberId, reportId);
         return Result.success(records);
     }
@@ -1126,8 +1120,7 @@ public class HealthReportController {
     @PostMapping("/nutrition/profile")
     public Result<Map<String, Object>> getNutritionProfile(
             @RequestBody Map<String, Object> params) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : null;
+        Long memberId = ParamUtils.getLong(params.get("memberId"));
         if (memberId == null) {
             return Result.error("memberId不能为空");
         }
@@ -1139,8 +1132,7 @@ public class HealthReportController {
     @PostMapping("/nutrition/deficiency-summary")
     public Result<Map<String, Object>> getDeficiencySummary(
             @RequestBody Map<String, Object> params) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : null;
+        Long memberId = ParamUtils.getLong(params.get("memberId"));
         if (memberId == null) {
             return Result.error("memberId不能为空");
         }
@@ -1152,8 +1144,7 @@ public class HealthReportController {
     @PostMapping("/nutrition/trend")
     public Result<Map<String, Object>> getReportTrendData(
             @RequestBody Map<String, Object> params) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : null;
+        Long memberId = ParamUtils.getLong(params.get("memberId"));
         if (memberId == null) {
             return Result.error("memberId不能为空");
         }
@@ -1165,8 +1156,7 @@ public class HealthReportController {
     @PostMapping("/analysis")
     public Result<Map<String, Object>> getHealthAnalysis(
             @RequestBody Map<String, Object> params) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : null;
+        Long memberId = ParamUtils.getLong(params.get("memberId"));
         if (memberId == null) {
             return Result.error("memberId不能为空");
         }
@@ -1203,7 +1193,7 @@ public class HealthReportController {
     @Operation(summary = "删除指标-营养素映射")
     @PostMapping("/nutrition/mapping/delete")
     public Result<?> deleteMapping(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) {
             return Result.error("id不能为空");
         }
@@ -1214,8 +1204,7 @@ public class HealthReportController {
     @Operation(summary = "获取食材推荐列表")
     @PostMapping("/foods")
     public Result<List<Food>> listFoods(@RequestBody Map<String, Object> params) {
-        Long reportId = params.get("reportId") != null
-                ? Long.valueOf(params.get("reportId").toString()) : null;
+        Long reportId = ParamUtils.getLong(params.get("reportId"));
         if (reportId == null) {
             return Result.error("reportId不能为空");
         }
@@ -1230,8 +1219,7 @@ public class HealthReportController {
     @PostMapping("/report/archive")
     public Result<String> archiveReport(
             @RequestBody Map<String, Object> params) {
-        Long reportId = params.get("reportId") != null
-                ? Long.valueOf(params.get("reportId").toString()) : null;
+        Long reportId = ParamUtils.getLong(params.get("reportId"));
         if (reportId == null) {
             return Result.error("reportId不能为空");
         }
@@ -1239,6 +1227,23 @@ public class HealthReportController {
         return Result.success("报告已归档");
     }
 
+    /**
+     * 批量归档报告(软删除)— 管理端选中删除
+     */
+    @Operation(summary = "批量归档健康报告(软删除)")
+    @PostMapping("/report/archive-batch")
+    public Result<String> archiveReportsBatch(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute(value = "role", required = false) String role) {
+        @SuppressWarnings("unchecked")
+        List<Long> reportIds = (List<Long>) params.get("reportIds");
+        if (reportIds == null || reportIds.isEmpty()) {
+            return Result.error("reportIds不能为空");
+        }
+        int archived = healthReportService.archiveReportsBatch(reportIds);
+        return Result.success("已归档 " + archived + " 份报告");
+    }
+
     @Operation(summary = "获取家庭成员食材推荐(含完整营养数据)")
     @PostMapping("/foods/by-family")
     public Result<Map<String, Object>> getFoodsByFamily(@RequestAttribute(value = "familyId", required = false) Long familyId) {
@@ -1321,8 +1326,7 @@ public class HealthReportController {
         String gender = (String) params.get("gender");
         Object ageObj = params.get("age");
         Integer age = ageObj != null ? Integer.parseInt(ageObj.toString()) : null;
-        Long draftId = params.get("draftId") != null
-                ? Long.valueOf(params.get("draftId").toString()) : null;
+        Long draftId = ParamUtils.getLong(params.get("draftId"));
 
         if (name == null || name.trim().isEmpty()) {
             return Result.error("姓名不能为空");
@@ -1380,10 +1384,8 @@ public class HealthReportController {
     public Result<Map<String, Object>> auditConfirm(
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long auditorId) {
-        Long draftId = params.get("draftId") != null
-                ? Long.valueOf(params.get("draftId").toString()) : null;
-        Long subjectId = params.get("subjectId") != null
-                ? Long.valueOf(params.get("subjectId").toString()) : null;
+        Long draftId = ParamUtils.getLong(params.get("draftId"));
+        Long subjectId = ParamUtils.getLong(params.get("subjectId"));
 
         if (draftId == null) {
             return Result.error("draftId不能为空");
@@ -1405,8 +1407,7 @@ public class HealthReportController {
     @PostMapping("/report/audit/discard")
     public Result<String> auditDiscard(
             @RequestBody Map<String, Object> params) {
-        Long draftId = params.get("draftId") != null
-                ? Long.valueOf(params.get("draftId").toString()) : null;
+        Long draftId = ParamUtils.getLong(params.get("draftId"));
 
         if (draftId == null) {
             return Result.error("draftId不能为空");
@@ -1578,9 +1579,101 @@ public class HealthReportController {
             payload.setIndicators(inds);
         }
 
+        // 菌群/饮食推荐分组(LangGraph 输出 snake_case 键,缺省时安全跳过)
+        payload.setGutFlora(convertLgFloraList(lgData.get("gut_flora")));
+        payload.setProbioticSpecies(convertLgFloraList(lgData.get("probiotic_species")));
+        payload.setTaxonomyClass(convertLgFloraList(lgData.get("taxonomy_class")));
+        payload.setTaxonomyOrder(convertLgFloraList(lgData.get("taxonomy_order")));
+        payload.setTaxonomyFamily(convertLgFloraList(lgData.get("taxonomy_family")));
+        payload.setTaxonomyGenus(convertLgFloraList(lgData.get("taxonomy_genus")));
+        payload.setTaxonomySpecies(convertLgFloraList(lgData.get("taxonomy_species")));
+        payload.setPathogenGenus(convertLgFloraList(lgData.get("pathogen_genus")));
+        payload.setPathogenDetection(convertLgFloraList(lgData.get("pathogen_detection")));
+        payload.setFoods(convertLgFoodList(lgData.get("foods")));
+
         return payload;
     }
 
+    /**
+     * 将 LangGraph 输出的菌群列表(snake_case 元素)转换为 ParsedReportPayload.Flora 列表。
+     * 键缺失或类型不符时安全返回空列表。
+     */
+    private List<ParsedReportPayload.Flora> convertLgFloraList(Object raw) {
+        List<ParsedReportPayload.Flora> floraList = new ArrayList<>();
+        if (!(raw instanceof List)) {
+            return floraList;
+        }
+        for (Object item : (List<?>) raw) {
+            if (!(item instanceof Map)) {
+                continue;
+            }
+            Map<String, Object> m = (Map<String, Object>) item;
+            ParsedReportPayload.Flora f = new ParsedReportPayload.Flora();
+            f.setBacteriaName(objectToString(m.get("name")));
+            f.setBacteriaValue(objectToString(m.get("value")));
+            f.setNormalRange(objectToString(m.get("normal_range")));
+            f.setPopulationLevel(objectToString(m.get("population_level")));
+            f.setDetectionRate(objectToString(m.get("detection_rate")));
+            f.setDescription(objectToString(m.get("description")));
+            f.setCategory(objectToString(m.get("category")));
+            f.setLevel(objectToString(m.get("level")));
+            if (f.getBacteriaName() == null && f.getDescription() == null) {
+                continue;
+            }
+            floraList.add(f);
+        }
+        return floraList;
+    }
+
+    /**
+     * 将 LangGraph 输出的饮食推荐列表(snake_case 元素)转换为 ParsedReportPayload.FoodItem 列表。
+     */
+    private List<ParsedReportPayload.FoodItem> convertLgFoodList(Object raw) {
+        List<ParsedReportPayload.FoodItem> foodList = new ArrayList<>();
+        if (!(raw instanceof List)) {
+            return foodList;
+        }
+        for (Object item : (List<?>) raw) {
+            if (!(item instanceof Map)) {
+                continue;
+            }
+            Map<String, Object> m = (Map<String, Object>) item;
+            ParsedReportPayload.FoodItem fi = new ParsedReportPayload.FoodItem();
+            fi.setName(objectToString(m.get("name")));
+            fi.setCategory(objectToString(m.get("category")));
+            fi.setScore(toInteger(m.get("score")));
+            fi.setEnergyKj(toDecimal(m.get("energy_kj")));
+            fi.setProtein(toDecimal(m.get("protein")));
+            fi.setFat(toDecimal(m.get("fat")));
+            fi.setCarbs(toDecimal(m.get("carbs")));
+            fi.setStarch(toDecimal(m.get("starch")));
+            fi.setFiber(toDecimal(m.get("fiber")));
+            fi.setCholesterol(toDecimal(m.get("cholesterol")));
+            if (fi.getName() == null) {
+                continue;
+            }
+            foodList.add(fi);
+        }
+        return foodList;
+    }
+
+    private static BigDecimal toDecimal(Object value) {
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof BigDecimal) {
+            return (BigDecimal) value;
+        }
+        if (value instanceof Number) {
+            return BigDecimal.valueOf(((Number) value).doubleValue());
+        }
+        try {
+            return new BigDecimal(value.toString());
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
     private static Integer toInteger(Object value) {
         if (value == null) return null;
         if (value instanceof Integer) return (Integer) value;
@@ -1752,8 +1845,7 @@ public class HealthReportController {
         if (contentObj == null || contentObj.toString().trim().isEmpty()) {
             return Result.error("反馈内容不能为空");
         }
-        Long reportId = params.get("reportId") != null
-                ? Long.valueOf(params.get("reportId").toString()) : null;
+        Long reportId = ParamUtils.getLong(params.get("reportId"));
         String type = params.get("type") != null ? params.get("type").toString() : "其他";
         String content = contentObj.toString().trim();
         if (content.length() > 500) {
@@ -1881,10 +1973,8 @@ public class HealthReportController {
             return Result.error("无权限");
         }
 
-        Long draftId = params.get("draftId") != null
-                ? Long.valueOf(params.get("draftId").toString()) : null;
-        Long subjectId = params.get("subjectId") != null
-                ? Long.valueOf(params.get("subjectId").toString()) : null;
+        Long draftId = ParamUtils.getLong(params.get("draftId"));
+        Long subjectId = ParamUtils.getLong(params.get("subjectId"));
 
         if (draftId == null) {
             return Result.error("draftId不能为空");

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

@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "健康积分", description = "P2-1 身份重塑系统")
 @RestController
@@ -25,8 +26,7 @@ public class HealthScoreController {
     @PostMapping("/info")
     public Result<Map<String, Object>> info(@RequestBody Map<String, Object> params,
                                             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         return Result.success(healthLevelService.getMemberLevelInfo(memberId));
     }
 
@@ -34,8 +34,7 @@ public class HealthScoreController {
     @PostMapping("/streak")
     public Result<Integer> streak(@RequestBody Map<String, Object> params,
                                   @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         return Result.success(healthScoreService.calculateStreakDays(memberId));
     }
 }

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

@@ -13,6 +13,7 @@ import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import com.etotem.cfc.util.SortUtil;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "睡眠打卡")
 @RestController
@@ -26,7 +27,7 @@ public class HealthSleepController {
     @PostMapping("/list")
     public Result<List<HealthSleepRecord>> list(@RequestBody Map<String, Object> params,
                                                 @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         if (memberId == null) return Result.error("memberId不能为空");
         LambdaQueryWrapper<HealthSleepRecord> wrapper = new LambdaQueryWrapper<HealthSleepRecord>()
                 .eq(HealthSleepRecord::getMemberId, memberId)

+ 73 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthStatusController.java

@@ -0,0 +1,73 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.HealthStatus;
+import com.etotem.cfc.service.HealthStatusService;
+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.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+
+@Tag(name = "健康现状档案", description = "当前状态调研-健康现状档案接口")
+@RestController
+@RequestMapping("/api/health-status")
+public class HealthStatusController {
+
+    @Resource
+    private HealthStatusService healthStatusService;
+
+    @Operation(summary = "查询当前账号健康现状(未填写返回 null)")
+    @PostMapping("/get")
+    public Result<HealthStatus> get(@RequestAttribute("userId") Long userId) {
+        return Result.success(healthStatusService.getByUserId(userId));
+    }
+
+    @Operation(summary = "保存/更新当前账号健康现状(upsert)")
+    @PostMapping("/save")
+    public Result<HealthStatus> save(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody HealthStatus status) {
+        if (status == null || isEmpty(status)) {
+            return Result.error("请至少填写一项");
+        }
+        String validateMsg = validate(status);
+        if (validateMsg != null) {
+            return Result.error(validateMsg);
+        }
+        return Result.success(healthStatusService.save(userId, status));
+    }
+
+    /** 至少填写一项才算有效 */
+    private boolean isEmpty(HealthStatus s) {
+        return s.getHeightCm() == null && s.getWeightKg() == null
+                && s.getBloodPressure() == null && s.getBloodGlucose() == null
+                && s.getBloodLipids() == null && s.getDiseaseHistory() == null
+                && s.getMedications() == null && s.getNotes() == null;
+    }
+
+    /** 数值校验,返回错误信息或 null */
+    private String validate(HealthStatus s) {
+        if (s.getHeightCm() != null && (s.getHeightCm().compareTo(BigDecimal.valueOf(30)) < 0
+                || s.getHeightCm().compareTo(BigDecimal.valueOf(250)) > 0)) {
+            return "身高需在 30-250 cm 之间";
+        }
+        if (s.getWeightKg() != null && (s.getWeightKg().compareTo(BigDecimal.valueOf(3)) < 0
+                || s.getWeightKg().compareTo(BigDecimal.valueOf(300)) > 0)) {
+            return "体重需在 3-300 kg 之间";
+        }
+        if (s.getBloodPressure() != null && !s.getBloodPressure().matches("\\d{2,3}/\\d{2,3}")) {
+            return "血压格式应为 120/80";
+        }
+        if (s.getBloodGlucose() != null && (s.getBloodGlucose().compareTo(BigDecimal.ONE) < 0
+                || s.getBloodGlucose().compareTo(BigDecimal.valueOf(30)) > 0)) {
+            return "血糖需在 1-30 mmol/L 之间";
+        }
+        return null;
+    }
+}

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

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "健康时间线", description = "P2-1 我的健康故事")
 @RestController
@@ -23,8 +24,7 @@ public class HealthTimelineController {
     @PostMapping("/list")
     public Result<List<HealthTimelineEvent>> list(@RequestBody Map<String, Object> params,
                                                   @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         int limit = params.get("limit") != null
                 ? Integer.parseInt(params.get("limit").toString()) : 20;
         return Result.success(healthTimelineService.getTimeline(memberId, limit));

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

@@ -13,6 +13,7 @@ import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import com.etotem.cfc.util.SortUtil;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "喝水打卡")
 @RestController
@@ -26,7 +27,7 @@ public class HealthWaterController {
     @PostMapping("/list")
     public Result<List<HealthWaterRecord>> list(@RequestBody Map<String, Object> params,
                                                 @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         if (memberId == null) return Result.error("memberId不能为空");
         LambdaQueryWrapper<HealthWaterRecord> wrapper = new LambdaQueryWrapper<HealthWaterRecord>()
                 .eq(HealthWaterRecord::getMemberId, memberId)

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

@@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.text.SimpleDateFormat;
 import java.util.*;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "\u98df\u8c31\u63a8\u8350", description = "\u4e2a\u6027\u5316\u98df\u8c31\u63a8\u8350\u4e0e\u996e\u98df\u8bb0\u5f55")
 @Slf4j
@@ -72,8 +73,7 @@ public class MealRecommendController {
     public Result<Food> replace(
             @RequestAttribute("userId") Long userId,
             @RequestBody Map<String, Object> params) {
-        Long foodId = params.get("foodId") != null
-                ? Long.valueOf(params.get("foodId").toString()) : null;
+        Long foodId = ParamUtils.getLong(params.get("foodId"));
         if (foodId == null) {
             return Result.error("foodId\u4e0d\u80fd\u4e3a\u7a7a");
         }
@@ -92,7 +92,7 @@ public class MealRecommendController {
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
         MealLog logEntry = new MealLog();
         logEntry.setUserId(userId);
-        Long memberId = params.get("memberId") != null ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         if (memberId != null) {
             logEntry.setChildId(memberId);
         }

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

@@ -22,6 +22,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "会员管理", description = "会员等级、支付相关接口")
 @RestController
@@ -184,7 +185,7 @@ public class MembershipController {
         String levelCode = params.getOrDefault("levelCode", "FAMILY").toString();
         String paymentType = params.getOrDefault("paymentType", "yearly").toString();
         String period = params.getOrDefault("period", "yearly").toString();
-        Long couponId = params.get("couponId") != null ? Long.valueOf(params.get("couponId").toString()) : null;
+        Long couponId = ParamUtils.getLong(params.get("couponId"));
 
         PaymentOrderDTO order = membershipService.createOrder(userId, familyId, levelCode, paymentType, period, couponId);
         return Result.success(order);
@@ -225,7 +226,7 @@ public class MembershipController {
         String levelCode = params.get("levelCode").toString();
         String paymentType = params.get("paymentType").toString();
         String period = params.get("period") != null ? params.get("period").toString() : "yearly";
-        Long couponId = params.get("couponId") != null ? Long.valueOf(params.get("couponId").toString()) : null;
+        Long couponId = ParamUtils.getLong(params.get("couponId"));
 
         PaymentOrderDTO order = membershipService.createOrder(userId, familyId, levelCode, paymentType, period, couponId);
 

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

@@ -12,6 +12,7 @@ import javax.annotation.Resource;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 /**
  * 微行动接口 — 有效启动:最小行为单元(1秒完成)
@@ -36,8 +37,7 @@ public class MicroActionController {
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         String dimensionCode = params.get("dimensionCode") != null
                 ? params.get("dimensionCode").toString() : null;
         MicroAction action;
@@ -61,10 +61,8 @@ public class MicroActionController {
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
-        Long actionId = params.get("actionId") != null
-                ? Long.valueOf(params.get("actionId").toString()) : null;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
+        Long actionId = ParamUtils.getLong(params.get("actionId"));
         if (memberId == null) {
             return Result.error("memberId不能为空");
         }
@@ -84,8 +82,7 @@ public class MicroActionController {
     public Result<Map<String, Object>> checkToday(
             @RequestBody Map<String, Object> params,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         boolean completed = memberId != null && microActionService.hasCompletedToday(memberId);
 
         Map<String, Object> result = new HashMap<>();

+ 37 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/MicrobiomeArticleController.java

@@ -0,0 +1,37 @@
+package com.etotem.cfc.controller;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.MicrobiomeArticle;
+import com.etotem.cfc.mapper.MicrobiomeArticleMapper;
+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.util.Map;
+@Tag(name = "菌群知识库文章")
+@RestController
+@RequestMapping("/api/microbiome/article")
+public class MicrobiomeArticleController {
+    @Resource
+    private MicrobiomeArticleMapper microbiomeArticleMapper;
+    @Operation(summary = "获取菌群知识库文章列表(分页)")
+    @PostMapping("/list")
+    public Result<Page<MicrobiomeArticle>> list(@RequestBody Map<String, Object> params) {
+        int page = params.get("page") != null ? Integer.parseInt(params.get("page").toString()) : 1;
+        int size = params.get("size") != null ? Integer.parseInt(params.get("size").toString()) : 1000;
+        LambdaQueryWrapper<MicrobiomeArticle> wrapper = new LambdaQueryWrapper<MicrobiomeArticle>()
+                .eq(MicrobiomeArticle::getStatus, 1);
+        if (params != null) {
+            Object category = params.get("category");
+            if (category != null && !category.toString().isEmpty())
+                wrapper.eq(MicrobiomeArticle::getCategory, category.toString());
+            Object keyword = params.get("keyword");
+            if (keyword != null && !keyword.toString().isEmpty())
+                wrapper.and(w -> w.like(MicrobiomeArticle::getTitle, keyword.toString())
+                        .or().like(MicrobiomeArticle::getContent, keyword.toString()));
+        }
+        wrapper.orderByDesc(MicrobiomeArticle::getCreatedAt);
+        return Result.success(microbiomeArticleMapper.selectPage(new Page<>(page, size), wrapper));
+    }
+}

+ 96 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/PackageLogisticsController.java

@@ -0,0 +1,96 @@
+package com.etotem.cfc.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.PackageOrder;
+import com.etotem.cfc.entity.UserIntent;
+import com.etotem.cfc.mapper.PackageOrderMapper;
+import com.etotem.cfc.mapper.UserIntentMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.annotation.Resource;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
+
+/**
+ * 套餐物流 — 改版v1(B19)
+ * <p>
+ * 注意:类名避开 shop 包的 LogisticsController(Bean 名冲突)。
+ */
+@Tag(name = "套餐物流", description = "套餐订单物流状态(管理端发货更新 / 用户查询)")
+@RestController
+@RequestMapping("/api/logistics")
+public class PackageLogisticsController {
+
+    @Resource
+    private PackageOrderMapper packageOrderMapper;
+
+    @Resource
+    private UserIntentMapper userIntentMapper;
+
+    @Operation(summary = "管理端发货/更新物流(orderId + logisticsStatus + trackingNo)")
+    @PostMapping("/update")
+    public Result<Boolean> update(@RequestAttribute("userId") Long userId,
+                                  @RequestAttribute(value = "role", required = false) String role,
+                                  @RequestBody Map<String, Object> params) {
+        Long orderId = ParamUtils.getLong(params.get("orderId"));
+        if (orderId == null) {
+            return Result.error("orderId 不能为空");
+        }
+        PackageOrder order = packageOrderMapper.selectById(orderId);
+        if (order == null) {
+            return Result.error("订单不存在");
+        }
+        String logisticsStatus = params.get("logisticsStatus") != null
+                ? params.get("logisticsStatus").toString() : null;
+        String trackingNo = params.get("trackingNo") != null
+                ? params.get("trackingNo").toString() : null;
+        if (logisticsStatus == null || logisticsStatus.isEmpty()) {
+            return Result.error("logisticsStatus 不能为空");
+        }
+        // 状态取值与 schema 一致: none/kit_shipped/kit_received/sample_shipped/sample_received
+        order.setLogisticsStatus(logisticsStatus);
+        if (trackingNo != null && !trackingNo.isEmpty()) {
+            order.setTrackingNo(trackingNo);
+        }
+        order.setUpdatedAt(new Date());
+        packageOrderMapper.updateById(order);
+
+        // 物流已推进(非 none)→ 推进旅程 stage=4(试剂盒已发/样本已收,幂等)
+        if (order.getUserId() != null && !"none".equals(logisticsStatus)) {
+            try {
+                UserIntent intent = userIntentMapper.selectOne(
+                        new LambdaQueryWrapper<UserIntent>()
+                                .eq(UserIntent::getUserId, order.getUserId())
+                                .last("LIMIT 1"));
+                if (intent != null && (intent.getJourneyStage() == null || intent.getJourneyStage() < 4)) {
+                    intent.setJourneyStage(4);
+                    intent.setStageUpdatedAt(new Date());
+                    intent.setUpdatedAt(new Date());
+                    userIntentMapper.updateById(intent);
+                }
+            } catch (Exception e) {
+                // 旅程推进失败不影响物流更新
+            }
+        }
+        return Result.success(true);
+    }
+
+    @Operation(summary = "用户查询我的套餐订单物流状态")
+    @PostMapping("/my")
+    public Result<List<PackageOrder>> my(@RequestAttribute("userId") Long userId) {
+        List<PackageOrder> orders = packageOrderMapper.selectList(
+                new LambdaQueryWrapper<PackageOrder>()
+                        .eq(PackageOrder::getUserId, userId)
+                        .orderByDesc(PackageOrder::getCreatedAt));
+        return Result.success(orders);
+    }
+}

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

@@ -12,6 +12,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "任务模板支付", description = "任务模板购买支付接口")
 @RestController
@@ -39,9 +40,9 @@ public class PackagePaymentController {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
-        Long packageId = Long.valueOf(params.get("packageId").toString());
+        Long packageId = ParamUtils.getLong(params.get("packageId"));
         String payMethod = params.get("payMethod") != null ? params.get("payMethod").toString() : "wechat";
-        Long couponId = params.get("couponId") != null ? Long.valueOf(params.get("couponId").toString()) : null;
+        Long couponId = ParamUtils.getLong(params.get("couponId"));
         
         PackageOrder order = packagePaymentService.createOrder(userId, familyId, packageId, payMethod, couponId);
         return Result.success(order);
@@ -53,7 +54,7 @@ public class PackagePaymentController {
     @Operation(summary = "获取微信支付参数")
     @PostMapping("/wechat params")
     public Result<Map<String, Object>> getWechatPayParams(@RequestBody Map<String, Object> params) {
-        Long orderId = Long.valueOf(params.get("orderId").toString());
+        Long orderId = ParamUtils.getLong(params.get("orderId"));
         String openid = params.get("openid").toString();
         
         Map<String, Object> payParams = packagePaymentService.createWechatPayOrder(orderId, openid);
@@ -91,8 +92,8 @@ public class PackagePaymentController {
     @Operation(summary = "支付成功后应用任务模板")
     @PostMapping("/apply")
     public Result<TaskPlanInstance> applyAfterPayment(@RequestBody Map<String, Object> params) {
-        Long orderId = Long.valueOf(params.get("orderId").toString());
-        Long memberId = Long.valueOf(params.get("memberId").toString());
+        Long orderId = ParamUtils.getLong(params.get("orderId"));
+        Long memberId = ParamUtils.getLong(params.get("memberId"));
         java.util.List<Long> selectedItemIds = null;
         if (params.get("selectedItemIds") instanceof java.util.List) {
             selectedItemIds = new java.util.ArrayList<>();

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

@@ -15,6 +15,7 @@ import javax.annotation.Resource;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/points/exchange")
@@ -34,7 +35,7 @@ public class PointsExchangeController {
 
     @PostMapping("/product/detail")
     public Result<PointsExchangeProduct> getProductDetail(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) {
             return Result.error("商品ID不能为空");
         }
@@ -45,8 +46,8 @@ public class PointsExchangeController {
     @PostMapping("/submit")
     public Result<PointsExchangeRecord> exchangeProduct(@RequestBody Map<String, Object> params,
                                                           @RequestAttribute("userId") Long userId) {
-        Long productId = params.get("productId") != null ? Long.valueOf(params.get("productId").toString()) : null;
-        Long memberId = params.get("memberId") != null ? Long.valueOf(params.get("memberId").toString()) : null;
+        Long productId = ParamUtils.getLong(params.get("productId"));
+        Long memberId = ParamUtils.getLong(params.get("memberId"));
         Integer quantity = params.get("quantity") != null ? Integer.valueOf(params.get("quantity").toString()) : 1;
         if (productId == null) {
             return Result.error("商品ID不能为空");

+ 134 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/ProblemDomainController.java

@@ -0,0 +1,134 @@
+package com.etotem.cfc.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.ProblemDomain;
+import com.etotem.cfc.entity.Product;
+import com.etotem.cfc.entity.SurveyTemplate;
+import com.etotem.cfc.entity.TaskTemplatePackage;
+import com.etotem.cfc.mapper.ProblemDomainMapper;
+import com.etotem.cfc.mapper.ProductMapper;
+import com.etotem.cfc.mapper.SurveyTemplateMapper;
+import com.etotem.cfc.mapper.TaskTemplatePackageMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.annotation.Resource;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 问题域 — 改版v1 需求驱动·逐步解锁(C类)
+ */
+@Tag(name = "问题域", description = "问题域列表(含问卷模板、推荐产品、任务模板)")
+@RestController
+@RequestMapping("/api/problem-domain")
+public class ProblemDomainController {
+
+    @Resource
+    private ProblemDomainMapper problemDomainMapper;
+
+    @Resource
+    private SurveyTemplateMapper surveyTemplateMapper;
+
+    @Resource
+    private ProductMapper productMapper;
+
+    @Resource
+    private TaskTemplatePackageMapper taskTemplatePackageMapper;
+
+    @Operation(summary = "启用中的问题域列表(含问卷模板 + 推荐产品 + 任务模板)")
+    @PostMapping("/list")
+    public Result<List<Map<String, Object>>> list() {
+        List<ProblemDomain> domains = problemDomainMapper.selectList(
+                new LambdaQueryWrapper<ProblemDomain>()
+                        .eq(ProblemDomain::getStatus, 1)
+                        .orderByAsc(ProblemDomain::getSortOrder)
+                        .orderByAsc(ProblemDomain::getId));
+
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (ProblemDomain domain : domains) {
+            Map<String, Object> item = new HashMap<>();
+            item.put("id", domain.getId());
+            item.put("code", domain.getCode());
+            item.put("name", domain.getName());
+            item.put("icon", domain.getIcon());
+            item.put("description", domain.getDescription());
+            item.put("flowIntroJson", domain.getFlowIntroJson());
+            item.put("contentTags", domain.getContentTags());
+            item.put("rewardCfValue", domain.getRewardCfValue());
+            item.put("sortOrder", domain.getSortOrder());
+            item.put("surveyTemplateId", domain.getSurveyTemplateId());
+
+            // 问卷模板
+            if (domain.getSurveyTemplateId() != null) {
+                SurveyTemplate template = surveyTemplateMapper.selectById(domain.getSurveyTemplateId());
+                if (template != null) {
+                    Map<String, Object> tm = new HashMap<>();
+                    tm.put("id", template.getId());
+                    tm.put("title", template.getTitle());
+                    tm.put("dimension", template.getDimension());
+                    tm.put("questionsJson", template.getQuestionsJson());
+                    item.put("surveyTemplate", tm);
+                }
+            }
+
+            // 推荐产品
+            List<Map<String, Object>> products = new ArrayList<>();
+            if (domain.getRecommendedProductIds() != null && !domain.getRecommendedProductIds().isEmpty()) {
+                for (String idStr : domain.getRecommendedProductIds().split(",")) {
+                    try {
+                        Long pid = Long.parseLong(idStr.trim());
+                        Product product = productMapper.selectById(pid);
+                        if (product == null || !"on".equals(product.getStatus())) {
+                            continue;
+                        }
+                        Map<String, Object> pm = new HashMap<>();
+                        pm.put("id", product.getId());
+                        pm.put("name", product.getName());
+                        pm.put("coverImage", product.getCoverImage());
+                        pm.put("price", product.getPrice());
+                        pm.put("memberPrice", product.getMemberPrice());
+                        pm.put("intro", product.getIntro());
+                        products.add(pm);
+                    } catch (NumberFormatException ignored) {
+                    }
+                }
+            }
+            item.put("products", products);
+
+            // 任务模板
+            List<Map<String, Object>> packages = new ArrayList<>();
+            if (domain.getTaskTemplatePackageIds() != null && !domain.getTaskTemplatePackageIds().isEmpty()) {
+                for (String idStr : domain.getTaskTemplatePackageIds().split(",")) {
+                    try {
+                        Long pid = Long.parseLong(idStr.trim());
+                        TaskTemplatePackage pkg = taskTemplatePackageMapper.selectById(pid);
+                        if (pkg == null) {
+                            continue;
+                        }
+                        Map<String, Object> pm = new HashMap<>();
+                        pm.put("id", pkg.getId());
+                        pm.put("name", pkg.getName());
+                        pm.put("description", pkg.getDescription());
+                        pm.put("cycleDays", pkg.getCycleDays());
+                        pm.put("category", pkg.getCategory());
+                        pm.put("price", pkg.getPrice());
+                        pm.put("coverImage", pkg.getCoverImage());
+                        packages.add(pm);
+                    } catch (NumberFormatException ignored) {
+                    }
+                }
+            }
+            item.put("packages", packages);
+
+            result.add(item);
+        }
+        return Result.success(result);
+    }
+}

+ 178 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/ProblemPackageController.java

@@ -0,0 +1,178 @@
+package com.etotem.cfc.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.ApplyPackageDTO;
+import com.etotem.cfc.entity.ProblemDomain;
+import com.etotem.cfc.entity.TaskPlanInstance;
+import com.etotem.cfc.entity.TaskTemplateItem;
+import com.etotem.cfc.entity.TaskTemplatePackage;
+import com.etotem.cfc.entity.UserIntent;
+import com.etotem.cfc.entity.PackageOrder;
+import com.etotem.cfc.mapper.PackageOrderMapper;
+import com.etotem.cfc.mapper.ProblemDomainMapper;
+import com.etotem.cfc.mapper.TaskPlanInstanceMapper;
+import com.etotem.cfc.mapper.UserIntentMapper;
+import com.etotem.cfc.service.MembershipService;
+import com.etotem.cfc.service.TaskPlanService;
+import com.etotem.cfc.service.TaskTemplatePackageService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.annotation.Resource;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
+
+/**
+ * 问题域套餐 — 改版v1(套餐详情:介绍/使用方式/包含检测/任务模板/奖励说明)
+ */
+@Tag(name = "问题域套餐", description = "套餐详情查询与方案确认")
+@RestController
+@RequestMapping("/api/problem-package")
+public class ProblemPackageController {
+
+    @Resource
+    private TaskTemplatePackageService taskTemplatePackageService;
+
+    @Resource
+    private TaskPlanService taskPlanService;
+
+    @Resource
+    private TaskPlanInstanceMapper planInstanceMapper;
+
+    @Resource
+    private UserIntentMapper userIntentMapper;
+
+    @Resource
+    private ProblemDomainMapper problemDomainMapper;
+
+    @Resource
+    private MembershipService membershipService;
+
+    @Resource
+    private PackageOrderMapper packageOrderMapper;
+
+    @Operation(summary = "套餐详情(名称/介绍/周期/包含任务模板/奖励说明)")
+    @PostMapping("/detail")
+    public Result<Map<String, Object>> detail(@RequestBody Map<String, Object> params) {
+        Long packageId = ParamUtils.getLong(params.get("packageId"));
+        if (packageId == null) {
+            return Result.error("packageId 不能为空");
+        }
+        TaskTemplatePackage pkg = taskTemplatePackageService.getPackageById(packageId);
+        if (pkg == null) {
+            return Result.error("套餐不存在");
+        }
+        List<TaskTemplateItem> items = taskTemplatePackageService.getPackageItems(packageId);
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("id", pkg.getId());
+        data.put("name", pkg.getName());
+        data.put("description", pkg.getDescription());
+        data.put("cycleDays", pkg.getCycleDays());
+        data.put("category", pkg.getCategory());
+        data.put("tags", pkg.getTags());
+        data.put("price", pkg.getPrice());
+        data.put("platformFee", pkg.getPlatformFee());
+        data.put("coverImage", pkg.getCoverImage());
+        data.put("taskCount", items.size());
+        data.put("items", items);
+        return Result.success(data);
+    }
+
+    /**
+     * 方案确认(B17/B20 补全):用户确认方案后生成可执行任务并推进旅程 stage=6。
+     * <p>
+     * 幂等:同一家庭同一套餐已存在激活中的方案实例则复用,不重复生成任务。
+     * 参数 packageId 可省略:缺省取用户意图(问题域)绑定的第一个套餐。
+     */
+    @Operation(summary = "方案确认:生成方案任务(memberOnly=1)并推进旅程 stage=6")
+    @PostMapping("/confirm")
+    public Result<Map<String, Object>> confirm(@RequestAttribute("userId") Long userId,
+                                               @RequestBody Map<String, Object> params) {
+        Long familyId = membershipService.getUserFamilyId(userId);
+        if (familyId == null) {
+            return Result.noFamily("请先创建或加入家庭");
+        }
+        Long packageId = ParamUtils.getLong(params.get("packageId"));
+
+        // 缺省套餐:从用户意图(问题域)取绑定的第一个套餐
+        if (packageId == null) {
+            UserIntent intent = findIntent(userId);
+            if (intent == null || intent.getProblemDomainCode() == null
+                    || intent.getProblemDomainCode().isEmpty()) {
+                return Result.error("未找到问题域,请先选择问题域");
+            }
+            ProblemDomain domain = problemDomainMapper.selectOne(
+                    new LambdaQueryWrapper<ProblemDomain>()
+                            .eq(ProblemDomain::getCode, intent.getProblemDomainCode())
+                            .last("LIMIT 1"));
+            if (domain == null || domain.getTaskTemplatePackageIds() == null
+                    || domain.getTaskTemplatePackageIds().trim().isEmpty()) {
+                return Result.error("该问题域未绑定套餐,请先在管理端配置");
+            }
+            packageId = Long.parseLong(domain.getTaskTemplatePackageIds().trim().split(",")[0]);
+        }
+
+        // 购买→方案关联校验:确认方案前该套餐须已购买(存在订单记录)
+        Long orderCount = packageOrderMapper.selectCount(
+                new LambdaQueryWrapper<PackageOrder>()
+                        .eq(PackageOrder::getFamilyId, familyId)
+                        .eq(PackageOrder::getPackageId, packageId)
+                        .in(PackageOrder::getStatus, "paid", "pending"));
+        if (orderCount == null || orderCount == 0) {
+            return Result.error("请先购买套餐后再确认方案");
+        }
+
+        // 幂等:同一家庭同一套餐已有激活中的方案实例则复用
+        TaskPlanInstance plan = planInstanceMapper.selectOne(
+                new LambdaQueryWrapper<TaskPlanInstance>()
+                        .eq(TaskPlanInstance::getFamilyId, familyId)
+                        .eq(TaskPlanInstance::getPackageId, packageId)
+                        .eq(TaskPlanInstance::getStatus, "active")
+                        .last("LIMIT 1"));
+        Long planId;
+        if (plan == null) {
+            ApplyPackageDTO dto = new ApplyPackageDTO();
+            dto.setPackageId(packageId);
+            TaskPlanInstance created = taskPlanService.applyPackage(dto, familyId);
+            planId = created.getId();
+        } else {
+            planId = plan.getId();
+        }
+
+        // 生成方案任务(memberOnly=1,幂等)
+        int taskCount = taskPlanService.generatePlanTasks(planId, familyId, userId);
+
+        // 推进旅程 stage=6(方案已确认,任务已生成)
+        UserIntent intent = findIntent(userId);
+        if (intent != null && (intent.getJourneyStage() == null || intent.getJourneyStage() < 6)) {
+            Date now = new Date();
+            intent.setJourneyStage(6);
+            intent.setStageUpdatedAt(now);
+            intent.setUpdatedAt(now);
+            userIntentMapper.updateById(intent);
+        }
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("planId", planId);
+        data.put("taskCount", taskCount);
+        data.put("journeyStage", 6);
+        return Result.success(data);
+    }
+
+    private UserIntent findIntent(Long userId) {
+        return userIntentMapper.selectOne(
+                new LambdaQueryWrapper<UserIntent>()
+                        .eq(UserIntent::getUserId, userId)
+                        .last("LIMIT 1"));
+    }
+}

+ 451 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/ProblemSurveyController.java

@@ -0,0 +1,451 @@
+package com.etotem.cfc.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.RecommendRequestDTO;
+import com.etotem.cfc.entity.ProblemDomain;
+import com.etotem.cfc.entity.SurveyTemplate;
+import com.etotem.cfc.entity.UserIntent;
+import com.etotem.cfc.mapper.ProblemDomainMapper;
+import com.etotem.cfc.mapper.SurveyTemplateMapper;
+import com.etotem.cfc.mapper.UserIntentMapper;
+import com.etotem.cfc.service.ContentRecommendService;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.annotation.Resource;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import com.etotem.cfc.util.ParamUtils;
+
+/**
+ * 专项调研 — 改版v2(契约统一)
+ * 获取问卷:POST /api/problem-survey/questions(按问题域 code,未绑定模板时返回内置默认问卷)
+ * 提交调研:POST /api/problem-survey/submit(answers 统一 [{questionId, optionIds}],
+ *           聚合 resultTags → 推荐产品 → 存快照 → 推进旅程)
+ */
+@Tag(name = "专项调研", description = "问题域专项调研:获取问卷/提交答案 → 汇总 resultTags → 推荐产品 → 存快照 → 推进旅程")
+@RestController
+@RequestMapping("/api/problem-survey")
+public class ProblemSurveyController {
+
+    @Resource
+    private SurveyTemplateMapper surveyTemplateMapper;
+
+    @Resource
+    private ProblemDomainMapper problemDomainMapper;
+
+    @Resource
+    private UserIntentMapper userIntentMapper;
+
+    @Resource
+    private ContentRecommendService contentRecommendService;
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    /** 内置默认问卷(问题域未绑定模板时兜底),选项带 tag 用于聚合 resultTags */
+    private static final Map<String, String> DEFAULT_QUESTIONS = buildDefaultQuestions();
+
+    @Operation(summary = "获取专项调研问卷(按问题域 code,未绑定模板返回内置默认问卷)")
+    @PostMapping("/questions")
+    public Result<Map<String, Object>> questions(@RequestAttribute("userId") Long userId,
+                                                 @RequestBody Map<String, Object> params) {
+        String code = params.get("problemDomainCode") != null
+                ? params.get("problemDomainCode").toString() : null;
+        ProblemDomain domain = null;
+        if (code != null && !code.isEmpty()) {
+            domain = problemDomainMapper.selectOne(
+                    new LambdaQueryWrapper<ProblemDomain>()
+                            .eq(ProblemDomain::getCode, code)
+                            .last("LIMIT 1"));
+        }
+        SurveyTemplate template = null;
+        if (domain != null && domain.getSurveyTemplateId() != null) {
+            template = surveyTemplateMapper.selectById(domain.getSurveyTemplateId());
+        }
+        List<Map<String, Object>> questions = new ArrayList<>();
+        Long templateId = null;
+        String title = "专项调研";
+        if (template != null && template.getQuestionsJson() != null
+                && !template.getQuestionsJson().trim().isEmpty()) {
+            questions = normalizeQuestions(template.getQuestionsJson());
+            templateId = template.getId();
+            title = template.getTitle();
+        } else {
+            String defaultJson = DEFAULT_QUESTIONS.get(code);
+            if (defaultJson != null) {
+                questions = normalizeQuestions(defaultJson);
+                title = (domain != null ? domain.getName() : "") + "专项调研";
+            }
+        }
+        Map<String, Object> data = new HashMap<>();
+        data.put("templateId", templateId);
+        data.put("title", title);
+        data.put("questions", questions);
+        return Result.success(data);
+    }
+
+    @Operation(summary = "提交专项调研:聚合 resultTags → 推荐产品 → 存快照 → 旅程推进")
+    @PostMapping("/submit")
+    @Transactional
+    public Result<Map<String, Object>> submit(@RequestAttribute("userId") Long userId,
+                                              @RequestAttribute(value = "familyId", required = false) Long familyId,
+                                              @RequestBody Map<String, Object> params) {
+        Long surveyTemplateId = ParamUtils.getLong(params.get("surveyTemplateId"));
+        // 兼容旧字段名 templateId
+        if (surveyTemplateId == null) {
+            surveyTemplateId = ParamUtils.getLong(params.get("templateId"));
+        }
+        String problemDomainCode = params.get("problemDomainCode") != null
+                ? params.get("problemDomainCode").toString() : null;
+        Long memberId = ParamUtils.getLong(params.get("memberId"));
+        Object answersObj = params.get("answers");
+
+        // 模板解析:显式传入 > 问题域绑定 > 内置默认
+        SurveyTemplate template = surveyTemplateId != null
+                ? surveyTemplateMapper.selectById(surveyTemplateId) : null;
+        ProblemDomain domain = null;
+        if (problemDomainCode != null && !problemDomainCode.isEmpty()) {
+            domain = problemDomainMapper.selectOne(
+                    new LambdaQueryWrapper<ProblemDomain>()
+                            .eq(ProblemDomain::getCode, problemDomainCode)
+                            .last("LIMIT 1"));
+            if (template == null && domain != null && domain.getSurveyTemplateId() != null) {
+                template = surveyTemplateMapper.selectById(domain.getSurveyTemplateId());
+            }
+        }
+        List<Map<String, Object>> questions = resolveQuestions(template, domain, problemDomainCode);
+
+        // 1. 聚合 resultTags:从题目选项 tag + 问题域 contentTags
+        Set<String> resultTags = new LinkedHashSet<>();
+        if (answersObj != null) {
+            resultTags.addAll(aggregateTagsFromAnswers(questions, answersObj));
+        }
+        if (domain != null && domain.getContentTags() != null) {
+            for (String tag : domain.getContentTags().split(",")) {
+                if (!tag.trim().isEmpty()) {
+                    resultTags.add(tag.trim());
+                }
+            }
+        }
+
+        // 2. 推荐产品(按问卷维度 + 汇总标签)
+        Map<String, Object> recommendData = new HashMap<>();
+        RecommendRequestDTO recReq = new RecommendRequestDTO();
+        recReq.setType("product");
+        recReq.setDimensionCode(template != null ? template.getDimension() : null);
+        recReq.setChildId(memberId);
+        recReq.setSize(10);
+        try {
+            Map<String, Object> rec = contentRecommendService.recommend(recReq, userId, familyId);
+            recommendData = rec;
+        } catch (Exception e) {
+            // 推荐失败不阻断提交
+        }
+
+        // 3. 存快照到 user_intents(单用户唯一,up-sert)
+        UserIntent intent = userIntentMapper.selectOne(
+                new LambdaQueryWrapper<UserIntent>()
+                        .eq(UserIntent::getUserId, userId)
+                        .last("LIMIT 1"));
+        Date now = new Date();
+        Map<String, Object> snapshot = new HashMap<>();
+        snapshot.put("surveyTemplateId", template != null ? template.getId() : null);
+        snapshot.put("surveyTitle", template != null ? template.getTitle() : null);
+        snapshot.put("dimension", template != null ? template.getDimension() : null);
+        snapshot.put("resultTags", new ArrayList<>(resultTags));
+        snapshot.put("answers", answersObj);
+        snapshot.put("submittedAt", now);
+
+        if (intent == null) {
+            intent = new UserIntent();
+            intent.setUserId(userId);
+            intent.setIntentType(domain != null ? "C" : "B");
+            intent.setProblemDomainCode(problemDomainCode != null ? problemDomainCode : "");
+            intent.setJourneyStage(2);
+            intent.setStageUpdatedAt(now);
+            intent.setSurveyTemplateId(template != null ? template.getId() : null);
+            intent.setSurveySubmittedAt(now);
+            intent.setSurveyResultJson(toJson(snapshot));
+            intent.setCreatedAt(now);
+            intent.setUpdatedAt(now);
+            userIntentMapper.insert(intent);
+        } else {
+            intent.setSurveyTemplateId(template != null ? template.getId() : null);
+            intent.setSurveySubmittedAt(now);
+            intent.setSurveyResultJson(toJson(snapshot));
+            // 调研完成推进到阶段2(文档状态机:2=调研已提交)
+            if (intent.getJourneyStage() == null || intent.getJourneyStage() < 2) {
+                intent.setJourneyStage(2);
+                intent.setStageUpdatedAt(now);
+            }
+            intent.setUpdatedAt(now);
+            userIntentMapper.updateById(intent);
+        }
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("resultTags", new ArrayList<>(resultTags));
+        data.put("recommendations", recommendData.get("recommendations") != null
+                ? recommendData.get("recommendations") : new ArrayList<>());
+        data.put("snapshot", snapshot);
+        data.put("journeyStage", intent.getJourneyStage());
+        return Result.success(data);
+    }
+
+    /** 解析题目列表:模板 JSON > 内置默认 > 空列表 */
+    private List<Map<String, Object>> resolveQuestions(SurveyTemplate template,
+                                                       ProblemDomain domain, String code) {
+        if (template != null && template.getQuestionsJson() != null
+                && !template.getQuestionsJson().trim().isEmpty()) {
+            return normalizeQuestions(template.getQuestionsJson());
+        }
+        String defaultJson = DEFAULT_QUESTIONS.get(code != null ? code
+                : (domain != null ? domain.getCode() : null));
+        if (defaultJson != null) {
+            return normalizeQuestions(defaultJson);
+        }
+        return new ArrayList<>();
+    }
+
+    /** 归一化题目结构:统一 [{id, text, multi, options: [{id, text, tag}]}] */
+    private List<Map<String, Object>> normalizeQuestions(String questionsJson) {
+        List<Map<String, Object>> result = new ArrayList<>();
+        if (questionsJson == null || questionsJson.trim().isEmpty()) {
+            return result;
+        }
+        try {
+            Object parsed = OBJECT_MAPPER.readValue(questionsJson, Object.class);
+            List<Map<String, Object>> raw = new ArrayList<>();
+            if (parsed instanceof List) {
+                raw = OBJECT_MAPPER.convertValue(parsed,
+                        new TypeReference<List<Map<String, Object>>>() {});
+            } else if (parsed instanceof Map) {
+                Object qArr = ((Map<?, ?>) parsed).get("questions");
+                if (qArr instanceof List) {
+                    raw = OBJECT_MAPPER.convertValue(qArr,
+                            new TypeReference<List<Map<String, Object>>>() {});
+                }
+            }
+            int qi = 0;
+            for (Map<String, Object> q : raw) {
+                Map<String, Object> nq = new HashMap<>();
+                nq.put("id", q.get("id") != null ? String.valueOf(q.get("id")) : ("q" + (qi + 1)));
+                nq.put("text", q.get("text") != null ? String.valueOf(q.get("text")) : "");
+                Object multiObj = q.get("multi");
+                nq.put("multi", multiObj != null ? Boolean.parseBoolean(String.valueOf(multiObj)) : Boolean.TRUE);
+                List<Map<String, Object>> options = new ArrayList<>();
+                Object optsObj = q.get("options");
+                if (optsObj instanceof List) {
+                    int oi = 0;
+                    for (Object o : (List<?>) optsObj) {
+                        Map<String, Object> no = new HashMap<>();
+                        if (o instanceof Map) {
+                            Map<?, ?> om = (Map<?, ?>) o;
+                            no.put("id", om.get("id") != null ? String.valueOf(om.get("id")) : ("o" + (oi + 1)));
+                            no.put("text", om.get("text") != null ? String.valueOf(om.get("text")) : "");
+                            Object tag = om.get("tag") != null ? om.get("tag") : om.get("resultTag");
+                            no.put("tag", tag != null ? String.valueOf(tag) : "");
+                        } else {
+                            no.put("id", "o" + (oi + 1));
+                            no.put("text", String.valueOf(o));
+                            no.put("tag", "");
+                        }
+                        options.add(no);
+                        oi++;
+                    }
+                }
+                nq.put("options", options);
+                result.add(nq);
+                qi++;
+            }
+        } catch (Exception ignored) {
+        }
+        return result;
+    }
+
+    /** 从 answers 聚合 resultTags(支持 [{questionId, optionIds}] 与旧 Map<qid, answer> 两种格式) */
+    private Set<String> aggregateTagsFromAnswers(List<Map<String, Object>> questions, Object answersObj) {
+        Set<String> tags = new LinkedHashSet<>();
+        if (answersObj == null || questions.isEmpty()) {
+            return tags;
+        }
+        try {
+            if (answersObj instanceof List) {
+                for (Object a : (List<?>) answersObj) {
+                    if (!(a instanceof Map)) {
+                        continue;
+                    }
+                    Map<?, ?> ans = (Map<?, ?>) a;
+                    Object qid = ans.get("questionId");
+                    Object optIds = ans.get("optionIds");
+                    // 兼容旧格式 [{questionText, optionTexts}]
+                    if (qid == null) qid = ans.get("questionText");
+                    if (optIds == null) optIds = ans.get("optionTexts");
+                    if (qid == null || optIds == null) {
+                        continue;
+                    }
+                    if (optIds instanceof List) {
+                        for (Object oid : (List<?>) optIds) {
+                            collectOptionTag(questions, String.valueOf(qid), String.valueOf(oid), tags);
+                        }
+                    } else {
+                        collectOptionTag(questions, String.valueOf(qid), String.valueOf(optIds), tags);
+                    }
+                }
+            } else if (answersObj instanceof Map) {
+                Map<?, ?> answers = (Map<?, ?>) answersObj;
+                for (Map.Entry<?, ?> e : answers.entrySet()) {
+                    Object answer = e.getValue();
+                    if (answer instanceof List) {
+                        for (Object a : (List<?>) answer) {
+                            collectOptionTag(questions, String.valueOf(e.getKey()), String.valueOf(a), tags);
+                        }
+                    } else {
+                        collectOptionTag(questions, String.valueOf(e.getKey()), String.valueOf(answer), tags);
+                    }
+                }
+            }
+        } catch (Exception ignored) {
+        }
+        return tags;
+    }
+
+    private void collectOptionTag(List<Map<String, Object>> questions, String questionKey,
+                                  String answerValue, Set<String> tags) {
+        Map<String, Object> question = findQuestion(questions, questionKey);
+        if (question == null) {
+            return;
+        }
+        Object optionsObj = question.get("options");
+        if (!(optionsObj instanceof List)) {
+            return;
+        }
+        // 先按选项 id 匹配,再按选项文本匹配(兼容纯字符串选项)
+        for (Object o : (List<?>) optionsObj) {
+            if (!(o instanceof Map)) {
+                continue;
+            }
+            Map<?, ?> option = (Map<?, ?>) o;
+            Object optId = option.get("id");
+            if (optId != null && String.valueOf(optId).equals(answerValue)) {
+                addTag(option, tags);
+                return;
+            }
+        }
+        for (Object o : (List<?>) optionsObj) {
+            if (!(o instanceof Map)) {
+                continue;
+            }
+            Map<?, ?> option = (Map<?, ?>) o;
+            Object text = option.get("text");
+            if (text != null && String.valueOf(text).equals(answerValue)) {
+                addTag(option, tags);
+                return;
+            }
+        }
+    }
+
+    private void addTag(Map<?, ?> option, Set<String> tags) {
+        Object tag = option.get("tag");
+        if (tag != null && !String.valueOf(tag).trim().isEmpty()) {
+            tags.add(String.valueOf(tag).trim());
+        }
+    }
+
+    private Map<String, Object> findQuestion(List<Map<String, Object>> questions, String key) {
+        for (Map<String, Object> q : questions) {
+            Object id = q.get("id");
+            if (id != null && String.valueOf(id).equals(key)) {
+                return q;
+            }
+            Object text = q.get("text");
+            if (text != null && String.valueOf(text).equals(key)) {
+                return q;
+            }
+        }
+        return null;
+    }
+
+    private String toJson(Object obj) {
+        try {
+            return OBJECT_MAPPER.writeValueAsString(obj);
+        } catch (Exception e) {
+            return "{}";
+        }
+    }
+
+    /** 内置默认问卷:weight_loss / sleep / constipation(选项带 tag 用于 resultTags 聚合) */
+    private static Map<String, String> buildDefaultQuestions() {
+        Map<String, String> map = new HashMap<>();
+        map.put("weight_loss", "["
+                + "{\"id\":\"q1\",\"text\":\"目前孩子的体重情况如何?\",\"multi\":false,"
+                + "\"options\":[{\"id\":\"o1\",\"text\":\"偏瘦\",\"tag\":\"体重偏瘦\"},"
+                + "{\"id\":\"o2\",\"text\":\"正常\",\"tag\":\"\"},"
+                + "{\"id\":\"o3\",\"text\":\"偏胖\",\"tag\":\"体重偏胖\"}]},"
+                + "{\"id\":\"q2\",\"text\":\"孩子的饮食结构如何?(可多选)\",\"multi\":true,"
+                + "\"options\":[{\"id\":\"o1\",\"text\":\"零食饮料多\",\"tag\":\"零食饮料\"},"
+                + "{\"id\":\"o2\",\"text\":\"主食过量\",\"tag\":\"主食过量\"},"
+                + "{\"id\":\"o3\",\"text\":\"蔬菜水果少\",\"tag\":\"蔬果不足\"},"
+                + "{\"id\":\"o4\",\"text\":\"三餐规律\",\"tag\":\"\"}]},"
+                + "{\"id\":\"q3\",\"text\":\"孩子每天运动情况如何?\",\"multi\":false,"
+                + "\"options\":[{\"id\":\"o1\",\"text\":\"很少运动\",\"tag\":\"运动不足\"},"
+                + "{\"id\":\"o2\",\"text\":\"每周1-2次\",\"tag\":\"运动不足\"},"
+                + "{\"id\":\"o3\",\"text\":\"每周3次以上\",\"tag\":\"运动良好\"}]},"
+                + "{\"id\":\"q4\",\"text\":\"你最想改善的方面是什么?(可多选)\",\"multi\":true,"
+                + "\"options\":[{\"id\":\"o1\",\"text\":\"控制体重\",\"tag\":\"减重\"},"
+                + "{\"id\":\"o2\",\"text\":\"培养饮食习惯\",\"tag\":\"饮食管理\"},"
+                + "{\"id\":\"o3\",\"text\":\"增加运动\",\"tag\":\"运动习惯\"}]}]");
+        map.put("sleep", "["
+                + "{\"id\":\"q1\",\"text\":\"孩子每晚睡眠时长大约多少?\",\"multi\":false,"
+                + "\"options\":[{\"id\":\"o1\",\"text\":\"不足8小时\",\"tag\":\"睡眠不足\"},"
+                + "{\"id\":\"o2\",\"text\":\"8-10小时\",\"tag\":\"睡眠正常\"},"
+                + "{\"id\":\"o3\",\"text\":\"超过10小时\",\"tag\":\"睡眠过多\"}]},"
+                + "{\"id\":\"q2\",\"text\":\"入睡情况如何?\",\"multi\":false,"
+                + "\"options\":[{\"id\":\"o1\",\"text\":\"入睡困难\",\"tag\":\"入睡困难\"},"
+                + "{\"id\":\"o2\",\"text\":\"容易惊醒\",\"tag\":\"睡眠浅\"},"
+                + "{\"id\":\"o3\",\"text\":\"睡得很沉\",\"tag\":\"\"}]},"
+                + "{\"id\":\"q3\",\"text\":\"睡前习惯有哪些?(可多选)\",\"multi\":true,"
+                + "\"options\":[{\"id\":\"o1\",\"text\":\"看电子产品\",\"tag\":\"屏幕暴露\"},"
+                + "{\"id\":\"o2\",\"text\":\"喝含糖饮料\",\"tag\":\"饮食影响睡眠\"},"
+                + "{\"id\":\"o3\",\"text\":\"睡前剧烈运动\",\"tag\":\"睡前兴奋\"},"
+                + "{\"id\":\"o4\",\"text\":\"规律作息\",\"tag\":\"\"}]},"
+                + "{\"id\":\"q4\",\"text\":\"白天精神状态如何?(可多选)\",\"multi\":true,"
+                + "\"options\":[{\"id\":\"o1\",\"text\":\"白天犯困\",\"tag\":\"白天嗜睡\"},"
+                + "{\"id\":\"o2\",\"text\":\"注意力不集中\",\"tag\":\"注意力不足\"},"
+                + "{\"id\":\"o3\",\"text\":\"情绪烦躁\",\"tag\":\"情绪波动\"},"
+                + "{\"id\":\"o4\",\"text\":\"精力充沛\",\"tag\":\"\"}]}]");
+        map.put("constipation", "["
+                + "{\"id\":\"q1\",\"text\":\"孩子排便频率如何?\",\"multi\":false,"
+                + "\"options\":[{\"id\":\"o1\",\"text\":\"每天1次以上\",\"tag\":\"\"},"
+                + "{\"id\":\"o2\",\"text\":\"2-3天1次\",\"tag\":\"排便频率低\"},"
+                + "{\"id\":\"o3\",\"text\":\"3天以上1次\",\"tag\":\"便秘\"}]},"
+                + "{\"id\":\"q2\",\"text\":\"大便性状如何?\",\"multi\":false,"
+                + "\"options\":[{\"id\":\"o1\",\"text\":\"干硬颗粒状\",\"tag\":\"大便干结\"},"
+                + "{\"id\":\"o2\",\"text\":\"成形但偏干\",\"tag\":\"大便干结\"},"
+                + "{\"id\":\"o3\",\"text\":\"正常软便\",\"tag\":\"\"}]},"
+                + "{\"id\":\"q3\",\"text\":\"饮食习惯如何?(可多选)\",\"multi\":true,"
+                + "\"options\":[{\"id\":\"o1\",\"text\":\"蔬菜水果少\",\"tag\":\"膳食纤维不足\"},"
+                + "{\"id\":\"o2\",\"text\":\"饮水不足\",\"tag\":\"饮水不足\"},"
+                + "{\"id\":\"o3\",\"text\":\"爱吃油炸烧烤\",\"tag\":\"饮食油腻\"},"
+                + "{\"id\":\"o4\",\"text\":\"规律均衡\",\"tag\":\"\"}]},"
+                + "{\"id\":\"q4\",\"text\":\"是否有以下症状?(可多选)\",\"multi\":true,"
+                + "\"options\":[{\"id\":\"o1\",\"text\":\"腹胀\",\"tag\":\"腹胀\"},"
+                + "{\"id\":\"o2\",\"text\":\"腹痛\",\"tag\":\"腹痛\"},"
+                + "{\"id\":\"o3\",\"text\":\"食欲不振\",\"tag\":\"食欲不振\"},"
+                + "{\"id\":\"o4\",\"text\":\"无\",\"tag\":\"\"}]}]");
+        return map;
+    }
+}

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

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/promotion")
@@ -25,8 +26,7 @@ public class PromotionController {
             return Result.noFamily("请先创建或加入家庭");
         }
         String inviteCode = params.get("inviteCode") != null ? params.get("inviteCode").toString() : null;
-        Long promoterFamilyId = params.get("promoterFamilyId") != null
-                ? Long.valueOf(params.get("promoterFamilyId").toString()) : null;
+        Long promoterFamilyId = ParamUtils.getLong(params.get("promoterFamilyId"));
         if (inviteCode == null || promoterFamilyId == null) {
             return Result.error("邀请码和推广方家庭ID不能为空");
         }

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

@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import javax.servlet.http.HttpServletRequest;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "promotion materials", description = "推广素材中心")
 @RestController
@@ -34,7 +35,7 @@ public class PromotionMaterialController {
     @Operation(summary = "get material detail")
     @PostMapping("/detail")
     public Result<PromotionMaterial> detail(@RequestBody Map<String, Object> params) {
-        Long id = Long.valueOf(params.get("id").toString());
+        Long id = ParamUtils.getLong(params.get("id"));
         PromotionMaterial material = promotionMaterialService.getById(id);
         if (material == null) return Result.error("素材不存在");
         return Result.success(material);
@@ -67,7 +68,7 @@ public class PromotionMaterialController {
     @Operation(summary = "delete material (admin)")
     @PostMapping("/delete")
     public Result<Void> delete(@RequestBody Map<String, Object> params) {
-        Long id = Long.valueOf(params.get("id").toString());
+        Long id = ParamUtils.getLong(params.get("id"));
         promotionMaterialService.deleteMaterial(id);
         return Result.success(null);
     }

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

@@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.Date;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "推荐系统", description = "个性化内容推荐接口")
 @RestController
@@ -49,7 +50,7 @@ public class RecommendController {
     public Result<String> track(@RequestBody Map<String, Object> params,
                                 @RequestAttribute(value = "userId", required = false) Long userId) {
         String contentType = (String) params.get("contentType");
-        Long contentId = params.get("contentId") != null ? Long.valueOf(params.get("contentId").toString()) : null;
+        Long contentId = ParamUtils.getLong(params.get("contentId"));
         String action = (String) params.get("action");
         if (contentType == null || contentId == null || action == null) {
             return Result.error("参数不完整");

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

@@ -18,6 +18,7 @@ import javax.annotation.Resource;
 import java.text.SimpleDateFormat;
 import java.util.*;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "统计报表", description = "统一数据统计与报表接口")
 @RestController
@@ -39,7 +40,7 @@ public class ReportStatsController {
     @Operation(summary = "孩子数据概览")
     @PostMapping("/child/overview")
     public Result<Map<String, Object>> childOverview(@RequestBody Map<String, Object> body) {
-        Long memberId = Long.valueOf(body.get("memberId").toString());
+        Long memberId = ParamUtils.getLong(body.get("memberId"));
         Map<String, Object> result = new HashMap<>();
 
         // 基础信息

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

@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 /**
  * 报告调研问卷接口 — 上传报告后自动生成的问卷管理
@@ -29,10 +30,8 @@ public class ReportSurveyController {
     public Result<ReportSurvey> createSurvey(
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId) {
-        Long reportId = params.get("reportId") != null
-                ? Long.valueOf(params.get("reportId").toString()) : null;
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : null;
+        Long reportId = ParamUtils.getLong(params.get("reportId"));
+        Long memberId = ParamUtils.getLong(params.get("memberId"));
         if (reportId == null || memberId == null) {
             return Result.error("reportId和childId不能为空");
         }
@@ -47,8 +46,7 @@ public class ReportSurveyController {
     @PostMapping("/submit")
     public Result<ReportSurvey> submitSurvey(
             @RequestBody Map<String, Object> params) {
-        Long surveyId = params.get("surveyId") != null
-                ? Long.valueOf(params.get("surveyId").toString()) : null;
+        Long surveyId = ParamUtils.getLong(params.get("surveyId"));
         String surveyData = params.get("surveyData") != null
                 ? params.get("surveyData").toString() : null;
         if (surveyId == null || surveyData == null) {
@@ -65,8 +63,7 @@ public class ReportSurveyController {
     @PostMapping("/status")
     public Result<Map<String, Object>> getSurveyStatus(
             @RequestBody Map<String, Object> params) {
-        Long reportId = params.get("reportId") != null
-                ? Long.valueOf(params.get("reportId").toString()) : null;
+        Long reportId = ParamUtils.getLong(params.get("reportId"));
         if (reportId == null) {
             return Result.error("reportId不能为空");
         }

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

@@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/review")
@@ -29,9 +30,7 @@ public class ReviewController {
 
     @PostMapping("/order/detail")
     public Result<ReviewOrderResponse> getReviewOrderDetail(@RequestBody java.util.Map<String, Object> params) {
-        Long orderId = params != null && params.get("orderId") != null
-                ? Long.valueOf(params.get("orderId").toString())
-                : null;
+        Long orderId = params != null ? ParamUtils.getLong(params.get("orderId")) : null;
         if (orderId == null) {
             return Result.error("orderId is required");
         }
@@ -41,9 +40,7 @@ public class ReviewController {
 
     @PostMapping("/order/status")
     public Result<ReviewStatusResponse> getReviewStatus(@RequestBody java.util.Map<String, Object> params) {
-        Long orderId = params != null && params.get("orderId") != null
-                ? Long.valueOf(params.get("orderId").toString())
-                : null;
+        Long orderId = params != null ? ParamUtils.getLong(params.get("orderId")) : null;
         if (orderId == null) {
             return Result.error("orderId is required");
         }

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

@@ -19,6 +19,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "服务角色申请", description = "统一服务角色申请入口")
 @RestController
@@ -98,7 +99,7 @@ public class ServiceRoleApplicationController {
     @Operation(summary = "管理端: 批准申请")
     public Result adminApprove(@RequestBody Map<String, Object> params,
                                @RequestAttribute("userId") Long adminId) {
-        Long id = Long.valueOf(params.get("id").toString());
+        Long id = ParamUtils.getLong(params.get("id"));
         return serviceRoleApplicationService.approve(id, adminId);
     }
 
@@ -106,7 +107,7 @@ public class ServiceRoleApplicationController {
     @Operation(summary = "管理端: 驳回申请")
     public Result adminReject(@RequestBody Map<String, Object> params,
                               @RequestAttribute("userId") Long adminId) {
-        Long id = Long.valueOf(params.get("id").toString());
+        Long id = ParamUtils.getLong(params.get("id"));
         String reason = params.get("reason") != null ? params.get("reason").toString() : "";
         return serviceRoleApplicationService.reject(id, reason, adminId);
     }

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

@@ -13,6 +13,7 @@ import javax.servlet.http.HttpServletRequest;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "内容分享", description = "文章/商品分享推荐接口")
 @RestController
@@ -43,7 +44,7 @@ public class ShareController {
         }
 
         String targetType = params.get("targetType").toString();
-        Long targetId = Long.valueOf(params.get("targetId").toString());
+        Long targetId = ParamUtils.getLong(params.get("targetId"));
         String shareChannel = params.get("shareChannel") != null
             ? params.get("shareChannel").toString() : "wechat";
         String title = params.get("title") != null ? params.get("title").toString() : "";
@@ -55,7 +56,7 @@ public class ShareController {
     @Operation(summary = "记录分享点击")
     @PostMapping("/click")
     public Result<Void> recordClick(@RequestBody Map<String, Object> params) {
-        Long shareEventId = Long.valueOf(params.get("shareEventId").toString());
+        Long shareEventId = ParamUtils.getLong(params.get("shareEventId"));
         shareService.recordClick(shareEventId);
         return Result.success(null);
     }
@@ -64,7 +65,7 @@ public class ShareController {
     @PostMapping("/read")
     public Result<Map<String, Object>> recordRead(HttpServletRequest request,
                                                    @RequestBody Map<String, Object> params) {
-        Long shareEventId = Long.valueOf(params.get("shareEventId").toString());
+        Long shareEventId = ParamUtils.getLong(params.get("shareEventId"));
         Long readerId = getUserId(request);
         String readerIp = request.getRemoteAddr();
         int duration = params.get("duration") != null

+ 7 - 9
cfc-backend/src/main/java/com/etotem/cfc/controller/SurveyController.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.ParamUtils;
 
 @Tag(name = "定期调研", description = "小程序端定期调研接口")
 @RestController
@@ -27,8 +28,7 @@ public class SurveyController {
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         return Result.success(surveyService.getSurveyStatus(userId, memberId));
     }
 
@@ -44,12 +44,11 @@ public class SurveyController {
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
-        Long templateId = Long.valueOf(params.get("templateId").toString());
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
+        Long templateId = ParamUtils.getLong(params.get("templateId"));
         String answersJson = (String) params.get("answersJson");
-        Date ps = params.get("periodStart") != null ? new Date(Long.valueOf(params.get("periodStart").toString())) : null;
-        Date pe = params.get("periodEnd") != null ? new Date(Long.valueOf(params.get("periodEnd").toString())) : null;
+        Date ps = params.get("periodStart") != null ? new Date(ParamUtils.getLong(params.get("periodStart"))) : null;
+        Date pe = params.get("periodEnd") != null ? new Date(ParamUtils.getLong(params.get("periodEnd"))) : null;
         return Result.success(surveyService.submitSurvey(userId, memberId, templateId, answersJson, ps, pe));
     }
 
@@ -59,8 +58,7 @@ public class SurveyController {
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         return Result.success(surveyService.getSurveyHistory(userId, memberId));
     }
 }

+ 128 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/UserIntentController.java

@@ -0,0 +1,128 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.UserIntent;
+import com.etotem.cfc.mapper.UserIntentMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.annotation.Resource;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * 用户意图 — 改版v1 需求驱动·逐步解锁(C类)
+ */
+@Tag(name = "用户意图", description = "首次意图选择(A/B/C)与旅程推进")
+@RestController
+@RequestMapping("/api/user-intent")
+public class UserIntentController {
+
+    @Resource
+    private UserIntentMapper userIntentMapper;
+
+    @Operation(summary = "获取当前用户意图")
+    @PostMapping("/get")
+    public Result<UserIntent> get(@RequestAttribute("userId") Long userId) {
+        UserIntent intent = findIntent(userId);
+        return Result.success(intent);
+    }
+
+    @Operation(summary = "首次意图选择:A直达 / B问卷 / C问题域")
+    @PostMapping("/choose")
+    @Transactional
+    public Result<UserIntent> choose(@RequestAttribute("userId") Long userId,
+                                     @RequestBody Map<String, Object> params) {
+        String intentType = params.get("intentType") != null ? params.get("intentType").toString() : "";
+        if (!"A".equals(intentType) && !"B".equals(intentType) && !"C".equals(intentType)) {
+            return Result.error("intentType 必须为 A/B/C");
+        }
+        String problemDomainCode = params.get("problemDomainCode") != null
+                ? params.get("problemDomainCode").toString() : "";
+        if ("C".equals(intentType) && problemDomainCode.isEmpty()) {
+            return Result.error("C类意图必须携带 problemDomainCode");
+        }
+
+        UserIntent intent = findIntent(userId);
+        Date now = new Date();
+        if (intent == null) {
+            intent = new UserIntent();
+            intent.setUserId(userId);
+            intent.setIntentType(intentType);
+            intent.setProblemDomainCode(problemDomainCode);
+            intent.setJourneyStage(0);
+            intent.setStageUpdatedAt(now);
+            intent.setCreatedAt(now);
+            intent.setUpdatedAt(now);
+            userIntentMapper.insert(intent);
+        } else {
+            intent.setIntentType(intentType);
+            if (!problemDomainCode.isEmpty()) {
+                intent.setProblemDomainCode(problemDomainCode);
+            }
+            intent.setUpdatedAt(now);
+            userIntentMapper.updateById(intent);
+        }
+        return Result.success(intent);
+    }
+
+    @Operation(summary = "推进旅程阶段(0-7)")
+    @PostMapping("/update-stage")
+    public Result<UserIntent> updateStage(@RequestAttribute("userId") Long userId,
+                                          @RequestBody Map<String, Object> params) {
+        Integer stage = params.get("journeyStage") != null
+                ? Integer.valueOf(params.get("journeyStage").toString()) : null;
+        if (stage == null || stage < 0 || stage > 7) {
+            return Result.error("journeyStage 必须在 0-7 之间");
+        }
+        UserIntent intent = findIntent(userId);
+        if (intent == null) {
+            return Result.error("请先选择意图(/choose)");
+        }
+        // 只允许向前推进
+        if (intent.getJourneyStage() != null && intent.getJourneyStage() >= stage) {
+            return Result.success(intent);
+        }
+        intent.setJourneyStage(stage);
+        Date now = new Date();
+        intent.setStageUpdatedAt(now);
+        intent.setUpdatedAt(now);
+        userIntentMapper.updateById(intent);
+        return Result.success(intent);
+    }
+
+    @Operation(summary = "用户跳过意图问卷(记录次数,最多提醒3次)")
+    @PostMapping("/skip")
+    public Result<UserIntent> skip(@RequestAttribute("userId") Long userId) {
+        UserIntent intent = findIntent(userId);
+        Date now = new Date();
+        if (intent == null) {
+            intent = new UserIntent();
+            intent.setUserId(userId);
+            intent.setSkipCount(1);
+            intent.setLastSkipAt(now);
+            intent.setCreatedAt(now);
+            intent.setUpdatedAt(now);
+            userIntentMapper.insert(intent);
+        } else {
+            intent.setSkipCount((intent.getSkipCount() == null ? 0 : intent.getSkipCount()) + 1);
+            intent.setLastSkipAt(now);
+            intent.setUpdatedAt(now);
+            userIntentMapper.updateById(intent);
+        }
+        return Result.success(intent);
+    }
+
+    private UserIntent findIntent(Long userId) {
+        return userIntentMapper.selectOne(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<UserIntent>()
+                        .eq(UserIntent::getUserId, userId)
+                        .last("LIMIT 1"));
+    }
+}

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

@@ -0,0 +1,76 @@
+package com.etotem.cfc.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.UserNotification;
+import com.etotem.cfc.mapper.UserNotificationMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.annotation.Resource;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 用户通知 — 改版v1(报告完成、奖励到账、物流等站内通知)
+ */
+@Tag(name = "用户通知", description = "站内通知列表与已读")
+@RestController
+@RequestMapping("/api/notification")
+public class UserNotificationController {
+
+    @Resource
+    private UserNotificationMapper userNotificationMapper;
+
+    @Operation(summary = "通知列表(未读优先,倒序)")
+    @PostMapping("/list")
+    public Result<List<UserNotification>> list(@RequestAttribute("userId") Long userId) {
+        List<UserNotification> list = userNotificationMapper.selectList(
+                new LambdaQueryWrapper<UserNotification>()
+                        .eq(UserNotification::getUserId, userId)
+                        .orderByAsc(UserNotification::getIsRead)
+                        .orderByDesc(UserNotification::getCreatedAt)
+                        .last("LIMIT 100"));
+        return Result.success(list);
+    }
+
+    @Operation(summary = "标记已读(id 或 ids 数组,缺省标记全部)")
+    @PostMapping("/read")
+    public Result<Boolean> read(@RequestAttribute("userId") Long userId,
+                                @RequestBody(required = false) Map<String, Object> params) {
+        Object idObj = params != null ? params.get("id") : null;
+        Object idsObj = params != null ? params.get("ids") : null;
+        if (idObj != null) {
+            UserNotification n = userNotificationMapper.selectById(Long.valueOf(idObj.toString()));
+            if (n != null && n.getUserId().equals(userId)) {
+                n.setIsRead(1);
+                userNotificationMapper.updateById(n);
+            }
+        } else if (idsObj != null) {
+            for (Object o : (List<?>) idsObj) {
+                UserNotification n = userNotificationMapper.selectById(Long.valueOf(o.toString()));
+                if (n != null && n.getUserId().equals(userId)) {
+                    n.setIsRead(1);
+                    userNotificationMapper.updateById(n);
+                }
+            }
+        } else {
+            // 全部标记已读
+            List<UserNotification> list = userNotificationMapper.selectList(
+                    new LambdaQueryWrapper<UserNotification>()
+                            .eq(UserNotification::getUserId, userId)
+                            .eq(UserNotification::getIsRead, 0));
+            for (UserNotification n : list) {
+                n.setIsRead(1);
+                userNotificationMapper.updateById(n);
+            }
+        }
+        return Result.success(true);
+    }
+}

+ 6 - 5
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminActivityController.java

@@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 /**
  * 后台活动管理接口
@@ -43,7 +44,7 @@ public class AdminActivityController {
      */
     @PostMapping("/detail")
     public Result<Activity> detail(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id不能为空");
         Activity activity = activityAdminService.getDetail(id);
         if (activity == null) return Result.error("活动不存在");
@@ -52,7 +53,7 @@ public class AdminActivityController {
 
     @PostMapping("/submit-review")
     public Result<String> submitReview(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id不能为空");
         activityAdminService.submitForReview(id);
         return Result.success("已提交审核");
@@ -61,7 +62,7 @@ public class AdminActivityController {
     @PostMapping("/audit")
     public Result<String> audit(@RequestBody Map<String, Object> params,
                                 @RequestAttribute("userId") Long adminId) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         String action = (String) params.get("action");
         String rejectReason = (String) params.get("rejectReason");
         if (id == null || action == null) return Result.error("id和action不能为空");
@@ -71,7 +72,7 @@ public class AdminActivityController {
 
     @PostMapping("/withdraw")
     public Result<String> withdraw(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id不能为空");
         activityAdminService.withdraw(id);
         return Result.success("已撤回");
@@ -79,7 +80,7 @@ public class AdminActivityController {
 
     @PostMapping("/re-draft")
     public Result<String> reDraft(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id不能为空");
         activityAdminService.reDraft(id);
         return Result.success("已转为草稿");

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

@@ -5,6 +5,7 @@ import com.etotem.cfc.service.ActivityRegistrationService;
 import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/admin/activity")
@@ -16,7 +17,7 @@ public class AdminActivityRegistrationController {
         if (!"admin".equals(role) && !"activity_admin".equals(role)) {
             return Result.error("无权限:仅活动管理员可查看报名");
         }
-        Long activityId = params.get("activityId") != null ? Long.valueOf(params.get("activityId").toString()) : null;
+        Long activityId = ParamUtils.getLong(params.get("activityId"));
         String status = (String) params.get("status");
         if (activityId == null) return Result.error("活动ID不能为空");
         return registrationService.getRegistrationsByActivity(activityId, status);

+ 4 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminAfterSalesController.java

@@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RestController;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/admin/shop/after-sales")
@@ -28,14 +29,14 @@ public class AdminAfterSalesController {
 
     @PostMapping("/detail")
     public Result<AfterSalesRequest> detail(@RequestBody Map<String, Object> params) {
-        Long id = Long.valueOf(params.get("id").toString());
+        Long id = ParamUtils.getLong(params.get("id"));
         return afterSalesService.adminDetail(id);
     }
 
     @PostMapping("/review")
     public Result<String> review(@RequestBody Map<String, Object> params,
                                  @RequestAttribute("userId") Long adminId) {
-        Long id = Long.valueOf(params.get("id").toString());
+        Long id = ParamUtils.getLong(params.get("id"));
         String action = (String) params.get("action");
         String rejectReason = (String) params.get("rejectReason");
         return afterSalesService.review(id, action, rejectReason, adminId);
@@ -43,7 +44,7 @@ public class AdminAfterSalesController {
 
     @PostMapping("/complete")
     public Result<String> complete(@RequestBody Map<String, Object> params) {
-        Long id = Long.valueOf(params.get("id").toString());
+        Long id = ParamUtils.getLong(params.get("id"));
         return afterSalesService.complete(id);
     }
 }

+ 16 - 15
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java

@@ -16,6 +16,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import com.etotem.cfc.util.SortUtil;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/admin/articles")
@@ -39,12 +40,12 @@ public class AdminArticleController {
     @PostMapping("/list")
     public Result<Page<Article>> list(@RequestBody Map<String, Object> body) {
         String status = (String) body.get("status");
-        Long categoryId = body.get("categoryId") != null ? Long.valueOf(body.get("categoryId").toString()) : null;
+        Long categoryId = ParamUtils.getLong(body.get("categoryId"));
         String keyword = (String) body.get("keyword");
         String contentType = (String) body.get("contentType");
         Integer difficultyLevel = body.get("difficultyLevel") != null ? Integer.valueOf(body.get("difficultyLevel").toString()) : null;
         String dimensionCode = (String) body.get("dimensionCode");
-        Long tagId = body.get("tagId") != null ? Long.valueOf(body.get("tagId").toString()) : null;
+        Long tagId = ParamUtils.getLong(body.get("tagId"));
         String auditStatus = (String) body.get("auditStatus");
         int page = body.get("page") != null ? Integer.parseInt(body.get("page").toString()) : 1;
         int size = body.get("size") != null ? Integer.parseInt(body.get("size").toString()) : 20;
@@ -73,7 +74,7 @@ public class AdminArticleController {
         article.setTitle((String) body.get("title"));
         article.setContent((String) body.get("content"));
         if (body.get("categoryId") != null && !body.get("categoryId").toString().trim().isEmpty()) {
-            article.setCategoryId(Long.valueOf(body.get("categoryId").toString()));
+            article.setCategoryId(ParamUtils.getLong(body.get("categoryId")));
         }
         article.setSummary((String) body.get("summary"));
         article.setCoverImage((String) body.get("coverImage"));
@@ -137,7 +138,7 @@ public class AdminArticleController {
             return Result.error("文章内容包含敏感信息,更新失败");
         }
 
-        Long articleId = body.get("id") != null ? Long.valueOf(body.get("id").toString()) : null;
+        Long articleId = ParamUtils.getLong(body.get("id"));
         if (articleId != null) {
             Article existing = articleService.getById(articleId);
             if (existing != null && !"draft".equals(existing.getStatus())
@@ -150,7 +151,7 @@ public class AdminArticleController {
         article.setTitle((String) body.get("title"));
         article.setContent((String) body.get("content"));
         if (body.get("categoryId") != null && !body.get("categoryId").toString().trim().isEmpty()) {
-            article.setCategoryId(Long.valueOf(body.get("categoryId").toString()));
+            article.setCategoryId(ParamUtils.getLong(body.get("categoryId")));
         }
         article.setSummary((String) body.get("summary"));
         article.setCoverImage((String) body.get("coverImage"));
@@ -199,7 +200,7 @@ public class AdminArticleController {
         if (body.get("id") == null) {
             return Result.error("缺少文章ID");
         }
-        Long id = Long.valueOf(body.get("id").toString());
+        Long id = ParamUtils.getLong(body.get("id"));
         Article article = articleService.getById(id);
         if (article == null) {
             return Result.error("文章不存在");
@@ -209,7 +210,7 @@ public class AdminArticleController {
 
     @PostMapping("/delete")
     public Result<String> delete(@RequestBody Map<String, Object> body) {
-        Long id = Long.valueOf(body.get("id").toString());
+        Long id = ParamUtils.getLong(body.get("id"));
         articleService.delete(id);
         return Result.success("删除成功");
     }
@@ -226,7 +227,7 @@ public class AdminArticleController {
         if (body.get("id") == null) {
             return Result.error("缺少文章ID");
         }
-        Long id = Long.valueOf(body.get("id").toString());
+        Long id = ParamUtils.getLong(body.get("id"));
         String status = (String) body.get("status");
         articleService.toggleStatus(id, status);
         return Result.success("操作成功");
@@ -234,7 +235,7 @@ public class AdminArticleController {
 
     @PostMapping("/toggle-featured")
     public Result<String> toggleFeatured(@RequestBody Map<String, Object> body) {
-        Long id = Long.valueOf(body.get("id").toString());
+        Long id = ParamUtils.getLong(body.get("id"));
         int isFeatured = Integer.parseInt(body.get("isFeatured").toString());
         articleService.toggleFeatured(id, isFeatured);
         return Result.success("操作成功");
@@ -243,7 +244,7 @@ public class AdminArticleController {
     @PostMapping("/audit")
     public Result<String> audit(@RequestBody Map<String, Object> body,
                                  @RequestAttribute("userId") Long adminId) {
-        Long articleId = Long.valueOf(body.get("id").toString());
+        Long articleId = ParamUtils.getLong(body.get("id"));
         String auditStatus = (String) body.get("auditStatus");
         String auditReason = (String) body.get("auditReason");
         if (!"approved".equals(auditStatus) && !"rejected".equals(auditStatus)) {
@@ -262,7 +263,7 @@ public class AdminArticleController {
 
     @PostMapping("/submit-review")
     public Result<String> submitReview(@RequestBody Map<String, Object> body) {
-        Long id = body.get("id") != null ? Long.valueOf(body.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(body.get("id"));
         if (id == null) return Result.error("缺少文章ID");
         try {
             articleService.submitForReview(id);
@@ -274,7 +275,7 @@ public class AdminArticleController {
 
     @PostMapping("/withdraw")
     public Result<String> withdraw(@RequestBody Map<String, Object> body) {
-        Long id = body.get("id") != null ? Long.valueOf(body.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(body.get("id"));
         if (id == null) return Result.error("缺少文章ID");
         try {
             articleService.withdraw(id);
@@ -286,7 +287,7 @@ public class AdminArticleController {
 
     @PostMapping("/re-draft")
     public Result<String> reDraft(@RequestBody Map<String, Object> body) {
-        Long id = body.get("id") != null ? Long.valueOf(body.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(body.get("id"));
         if (id == null) return Result.error("缺少文章ID");
         try {
             articleService.reDraft(id);
@@ -326,7 +327,7 @@ public class AdminArticleController {
 
     @PostMapping("/comments/list")
     public Result<Page<Map<String, Object>>> commentList(@RequestBody Map<String, Object> body) {
-        Long articleId = body.get("articleId") != null ? Long.valueOf(body.get("articleId").toString()) : null;
+        Long articleId = ParamUtils.getLong(body.get("articleId"));
         String status = (String) body.get("status");
         int page = body.get("page") != null ? Integer.parseInt(body.get("page").toString()) : 1;
         int size = body.get("size") != null ? Integer.parseInt(body.get("size").toString()) : 20;
@@ -352,7 +353,7 @@ public class AdminArticleController {
 
     @PostMapping("/categories/delete")
     public Result<String> categoryDelete(@RequestBody Map<String, Object> body) {
-        Long id = Long.valueOf(body.get("id").toString());
+        Long id = ParamUtils.getLong(body.get("id"));
         articleCategoryService.delete(id);
         return Result.success("删除成功");
     }

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

@@ -16,6 +16,7 @@ import javax.annotation.Resource;
 import java.util.HashMap;
 import java.util.Map;
 import com.etotem.cfc.util.SortUtil;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/admin/butler")
@@ -74,9 +75,7 @@ public class AdminButlerController {
             return Result.error("权限不足");
         }
 
-        Long userId = params.get("userId") != null
-                ? Long.valueOf(params.get("userId").toString())
-                : null;
+        Long userId = ParamUtils.getLong(params.get("userId"));
         String action = (String) params.get("action");
         String reason = (String) params.getOrDefault("reason", "");
 
@@ -101,12 +100,8 @@ public class AdminButlerController {
             return Result.error("权限不足");
         }
 
-        Long profileId = params.get("profileId") != null
-                ? Long.valueOf(params.get("profileId").toString())
-                : null;
-        Long memberId = params.get("memberId") != null
-                ? Long.valueOf(params.get("memberId").toString())
-                : null;
+        Long profileId = ParamUtils.getLong(params.get("profileId"));
+        Long memberId = ParamUtils.getLong(params.get("memberId"));
 
         if (profileId == null || memberId == null) {
             return Result.error("profileId和memberId不能为空");

+ 3 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCategoryController.java

@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RestController;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/admin/shop/category")
@@ -31,7 +32,7 @@ public class AdminCategoryController {
 
     @PostMapping("/delete")
     public Result<String> delete(@RequestBody Map<String, Object> body) {
-        Long id = Long.valueOf(body.get("id").toString());
+        Long id = ParamUtils.getLong(body.get("id"));
         return productCategoryService.delete(id);
     }
 
@@ -42,7 +43,7 @@ public class AdminCategoryController {
 
     @PostMapping("/toggle")
     public Result<String> toggle(@RequestBody Map<String, Object> body) {
-        Long id = Long.valueOf(body.get("id").toString());
+        Long id = ParamUtils.getLong(body.get("id"));
         return productCategoryService.toggle(id);
     }
 }

+ 3 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCfValueController.java

@@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/admin/cf-value")
@@ -26,7 +27,7 @@ public class AdminCfValueController {
 
     @PostMapping("/balance")
     public Result<Map<String, Object>> getBalance(@RequestBody Map<String, Object> params) {
-        Long userId = params.get("userId") != null ? Long.valueOf(params.get("userId").toString()) : null;
+        Long userId = ParamUtils.getLong(params.get("userId"));
         if (userId == null) {
             return Result.error("userId不能为空");
         }
@@ -36,7 +37,7 @@ public class AdminCfValueController {
 
     @PostMapping("/logs")
     public Result<Page<PlatformBalanceLog>> getLogs(@RequestBody Map<String, Object> params) {
-        Long userId = params.get("userId") != null ? Long.valueOf(params.get("userId").toString()) : null;
+        Long userId = ParamUtils.getLong(params.get("userId"));
         if (userId == null) {
             return Result.error("userId不能为空");
         }

+ 4 - 9
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCommissionController.java

@@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.HashMap;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/admin/commission")
@@ -38,9 +39,7 @@ public class AdminCommissionController {
     @PostMapping("/audit")
     public Result<String> audit(@RequestAttribute("userId") Long adminId,
                                 @RequestBody Map<String, Object> params) {
-        Long requestId = params.get("requestId") != null
-                ? Long.valueOf(params.get("requestId").toString())
-                : null;
+        Long requestId = ParamUtils.getLong(params.get("requestId"));
         String status = (String) params.get("status");
         String remark = (String) params.get("remark");
         if (requestId == null) {
@@ -60,9 +59,7 @@ public class AdminCommissionController {
 
     @PostMapping("/update-profit-rate")
     public Result<String> updateProfitRate(@RequestBody Map<String, Object> params) {
-        Long productId = params.get("productId") != null
-                ? Long.valueOf(params.get("productId").toString())
-                : null;
+        Long productId = ParamUtils.getLong(params.get("productId"));
         Integer profitRate = params.get("profitRate") != null
                 ? Integer.valueOf(params.get("profitRate").toString())
                 : null;
@@ -96,9 +93,7 @@ public class AdminCommissionController {
 
     @PostMapping("/refunds/cancel")
     public Result<String> cancelRefund(@RequestBody Map<String, Object> params) {
-        Long refundId = params.get("refundId") != null
-                ? Long.valueOf(params.get("refundId").toString())
-                : null;
+        Long refundId = ParamUtils.getLong(params.get("refundId"));
         if (refundId == null) {
             return Result.error("refundId不能为空");
         }

+ 5 - 4
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java

@@ -24,6 +24,7 @@ import java.util.Map;
 import java.util.ArrayList;
 import java.util.Collections;
 import com.etotem.cfc.util.SortUtil;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/admin")
@@ -1420,8 +1421,8 @@ public class AdminController {
 
     @PostMapping("/assessment/change-guide")
     public Result<Void> changeAssessmentGuide(@RequestBody Map<String, Object> body) {
-        Long appointmentId = body.get("orderId") != null ? Long.valueOf(body.get("orderId").toString()) : null;
-        Long guideId = body.get("guideId") != null ? Long.valueOf(body.get("guideId").toString()) : null;
+        Long appointmentId = ParamUtils.getLong(body.get("orderId"));
+        Long guideId = ParamUtils.getLong(body.get("guideId"));
         if (appointmentId == null || guideId == null) {
             return Result.error("appointmentId 和 guideId 不能为空");
         }
@@ -1449,8 +1450,8 @@ public class AdminController {
 
     @PostMapping("/assessment/assign-guide")
     public Result<Void> assignGuide(@RequestBody Map<String, Object> body) {
-        Long appointmentId = body.get("appointmentId") != null ? Long.valueOf(body.get("appointmentId").toString()) : null;
-        Long guideId = body.get("guideId") != null ? Long.valueOf(body.get("guideId").toString()) : null;
+        Long appointmentId = ParamUtils.getLong(body.get("appointmentId"));
+        Long guideId = ParamUtils.getLong(body.get("guideId"));
         if (appointmentId == null || guideId == null) {
             return Result.error("appointmentId 和 guideId 不能为空");
         }

+ 5 - 4
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCouponController.java

@@ -14,6 +14,7 @@ import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import com.etotem.cfc.util.SortUtil;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/admin/coupon")
@@ -67,7 +68,7 @@ public class AdminCouponController {
         if (!"admin".equals(role)) {
             return Result.error("无权限");
         }
-        Long couponId = Long.valueOf(params.get("couponId").toString());
+        Long couponId = ParamUtils.getLong(params.get("couponId"));
         Object userIdsObj = params.get("userIds");
         if (userIdsObj instanceof List) {
             List<?> userIds = (List<?>) userIdsObj;
@@ -84,7 +85,7 @@ public class AdminCouponController {
         if (!"admin".equals(role)) {
             return Result.error("无权限");
         }
-        Long id = Long.valueOf(params.get("id").toString());
+        Long id = ParamUtils.getLong(params.get("id"));
         couponMapper.deleteById(id);
         return Result.success("删除成功");
     }
@@ -95,8 +96,8 @@ public class AdminCouponController {
         if (!"admin".equals(role)) {
             return Result.error("无权限");
         }
-        Long userId = params.get("userId") != null ? Long.valueOf(params.get("userId").toString()) : null;
-        Long couponId = params.get("couponId") != null ? Long.valueOf(params.get("couponId").toString()) : null;
+        Long userId = ParamUtils.getLong(params.get("userId"));
+        Long couponId = ParamUtils.getLong(params.get("couponId"));
         LambdaQueryWrapper<CouponGrantLog> wrapper = new LambdaQueryWrapper<>();
         if (userId != null) {
             wrapper.eq(CouponGrantLog::getUserId, userId);

+ 3 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminDiscountController.java

@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RestController;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/admin/shop/discount")
@@ -37,13 +38,13 @@ public class AdminDiscountController {
 
     @PostMapping("/toggle")
     public Result<String> toggle(@RequestBody Map<String, Object> body) {
-        Long id = Long.valueOf(body.get("id").toString());
+        Long id = ParamUtils.getLong(body.get("id"));
         return discountService.toggle(id);
     }
 
     @PostMapping("/delete")
     public Result<String> delete(@RequestBody Map<String, Object> body) {
-        Long id = Long.valueOf(body.get("id").toString());
+        Long id = ParamUtils.getLong(body.get("id"));
         return discountService.delete(id);
     }
 }

+ 8 - 7
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminDistributionController.java

@@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "管理端-销售管理")
 @RestController
@@ -44,7 +45,7 @@ public class AdminDistributionController {
     @Operation(summary = "启用/禁用销售体系")
     @PostMapping("/system/toggle")
     public Result<String> toggleSystem(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id不能为空");
         return distributionService.toggleSystem(id);
     }
@@ -52,17 +53,17 @@ public class AdminDistributionController {
     @Operation(summary = "销售关系列表")
     @PostMapping("/relation/list")
     public Result<List<DistributionRelation>> listRelations(@RequestBody Map<String, Object> params) {
-        Long systemId = params.get("systemId") != null ? Long.valueOf(params.get("systemId").toString()) : null;
-        Long userId = params.get("userId") != null ? Long.valueOf(params.get("userId").toString()) : null;
+        Long systemId = ParamUtils.getLong(params.get("systemId"));
+        Long userId = ParamUtils.getLong(params.get("userId"));
         return distributionService.listRelations(systemId, userId);
     }
 
     @Operation(summary = "添加销售关系")
     @PostMapping("/relation/add")
     public Result<Void> addRelation(@RequestBody Map<String, Object> params) {
-        Long systemId = params.get("systemId") != null ? Long.valueOf(params.get("systemId").toString()) : null;
-        Long userId = params.get("userId") != null ? Long.valueOf(params.get("userId").toString()) : null;
-        Long parentId = params.get("parentId") != null ? Long.valueOf(params.get("parentId").toString()) : null;
+        Long systemId = ParamUtils.getLong(params.get("systemId"));
+        Long userId = ParamUtils.getLong(params.get("userId"));
+        Long parentId = ParamUtils.getLong(params.get("parentId"));
         if (systemId == null) return Result.error("systemId不能为空");
         if (userId == null) return Result.error("userId不能为空");
         return distributionService.addRelation(systemId, userId, parentId);
@@ -71,7 +72,7 @@ public class AdminDistributionController {
     @Operation(summary = "移除销售关系")
     @PostMapping("/relation/remove")
     public Result<String> removeRelation(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id不能为空");
         return distributionService.removeRelation(id);
     }

+ 3 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminEnergyBehaviorConfigController.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.ParamUtils;
 
 @Tag(name = "管理端-能量行为配置")
 @RestController
@@ -32,7 +33,7 @@ public class AdminEnergyBehaviorConfigController {
     @Operation(summary = "获取单个能量行为配置")
     @PostMapping("/detail")
     public Result<EnergyBehaviorConfig> detail(@RequestBody Map<String, Object> params) {
-        Long id = Long.valueOf(params.get("id").toString());
+        Long id = ParamUtils.getLong(params.get("id"));
         EnergyBehaviorConfig config = behaviorConfigMapper.selectById(id);
         if (config == null) return Result.error("配置不存在");
         return Result.success(config);
@@ -51,7 +52,7 @@ public class AdminEnergyBehaviorConfigController {
     @Operation(summary = "切换启用/禁用状态")
     @PostMapping("/toggle")
     public Result<String> toggle(@RequestBody Map<String, Object> params) {
-        Long id = Long.valueOf(params.get("id").toString());
+        Long id = ParamUtils.getLong(params.get("id"));
         EnergyBehaviorConfig config = behaviorConfigMapper.selectById(id);
         if (config == null) return Result.error("配置不存在");
         config.setEnabled(config.getEnabled() != null && config.getEnabled() == 1 ? 0 : 1);

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

@@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
 import java.util.*;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "管理端 - 五行能量配置")
 @RestController
@@ -88,7 +89,7 @@ public class AdminEnergyConfigController {
     @Operation(summary = "删除能量来源配置")
     @PostMapping("/source-delete")
     public Result<String> sourceDelete(@RequestBody Map<String, Object> params) {
-        Long id = Long.valueOf(params.get("id").toString());
+        Long id = ParamUtils.getLong(params.get("id"));
         energySourceConfigMapper.deleteById(id);
         return Result.success("删除成功");
     }

+ 3 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminEnergyRuleController.java

@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.time.LocalDateTime;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @RestController
 @RequestMapping("/api/admin/energy-rule")
@@ -61,7 +62,7 @@ public class AdminEnergyRuleController {
 
     @PostMapping("/delete")
     public Result<Void> delete(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id不能为空");
         energyRuleMapper.deleteById(id);
         return Result.success(null);
@@ -69,7 +70,7 @@ public class AdminEnergyRuleController {
 
     @PostMapping("/toggle")
     public Result<Void> toggle(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id不能为空");
         EnergyRule rule = energyRuleMapper.selectById(id);
         if (rule == null) return Result.error("规则不存在");

+ 4 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminFoodController.java

@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.util.*;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "\u7ba1\u7406\u7aef-\u98df\u6750\u7ba1\u7406")
 @RestController
@@ -84,7 +85,7 @@ public class AdminFoodController {
     @Operation(summary = "\u5220\u9664\u98df\u6750")
     @PostMapping("/delete")
     public Result<String> delete(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id\u4e0d\u80fd\u4e3a\u7a7a");
         foodService.delete(id);
         return Result.success("ok");
@@ -93,7 +94,7 @@ public class AdminFoodController {
     @Operation(summary = "\u98df\u6750\u8be6\u60c5\uff08\u542b\u5e94\u5b63\u6708\u4efd\uff09")
     @PostMapping("/detail")
     public Result<Map<String, Object>> detail(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id\u4e0d\u80fd\u4e3a\u7a7a");
         Map<String, Object> result = foodService.getDetailWithMonths(id);
         if (result == null) return Result.error("\u98df\u6750\u4e0d\u5b58\u5728");
@@ -103,7 +104,7 @@ public class AdminFoodController {
     @Operation(summary = "\u4fdd\u5b58\u5e94\u5b63\u6708\u4efd")
     @PostMapping("/saveMonths")
     public Result<String> saveMonths(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id\u4e0d\u80fd\u4e3a\u7a7a");
         @SuppressWarnings("unchecked")
         List<Integer> months = params.get("months") != null

+ 4 - 9
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminGrowthArchiveProductController.java

@@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 /**
  * 管理端-成长档案关联商品配置控制器
@@ -27,9 +28,7 @@ public class AdminGrowthArchiveProductController {
     @Operation(summary = "保存成长档案关联商品规则")
     @PostMapping("/save")
     public Result<String> save(@RequestBody Map<String, Object> params) {
-        Long productId = params.get("productId") != null
-                ? Long.valueOf(params.get("productId").toString())
-                : null;
+        Long productId = ParamUtils.getLong(params.get("productId"));
         Integer status = params.get("status") != null
                 ? Integer.valueOf(params.get("status").toString())
                 : null;
@@ -49,9 +48,7 @@ public class AdminGrowthArchiveProductController {
     @Operation(summary = "成长档案关联商品规则详情")
     @PostMapping("/detail")
     public Result<GrowthArchiveProduct> detail(@RequestBody Map<String, Object> params) {
-        Long productId = params.get("productId") != null
-                ? Long.valueOf(params.get("productId").toString())
-                : null;
+        Long productId = ParamUtils.getLong(params.get("productId"));
         if (productId == null) {
             return Result.error("productId不能为空");
         }
@@ -61,9 +58,7 @@ public class AdminGrowthArchiveProductController {
     @Operation(summary = "删除成长档案关联商品规则")
     @PostMapping("/delete")
     public Result<String> delete(@RequestBody Map<String, Object> params) {
-        Long productId = params.get("productId") != null
-                ? Long.valueOf(params.get("productId").toString())
-                : null;
+        Long productId = ParamUtils.getLong(params.get("productId"));
         if (productId == null) {
             return Result.error("productId不能为空");
         }

+ 33 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminGuideController.java

@@ -4,13 +4,19 @@ import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.Guide;
 import com.etotem.cfc.entity.GuideApplication;
 import com.etotem.cfc.entity.GuidePackage;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.GuideApplicationService;
 import com.etotem.cfc.service.GuidePackageService;
 import com.etotem.cfc.service.GuideService;
+import io.swagger.v3.oas.annotations.Operation;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 @RestController
 @RequestMapping("/api/admin/guide")
@@ -25,6 +31,9 @@ public class AdminGuideController {
     @Resource
     private GuidePackageService guidePackageService;
 
+    @Resource
+    private UserMapper userMapper;
+
     @PostMapping("/applications/pending")
     public Result<List<GuideApplication>> getPendingApplications() {
         return Result.success(guideApplicationService.getPendingApplications());
@@ -57,6 +66,30 @@ public class AdminGuideController {
         return Result.success(guidePackageService.list());
     }
 
+    @Operation(summary = "获取已入驻的服务商列表(用于商品编辑时下拉选择)")
+    @PostMapping("/guides/approved")
+    public Result<List<Map<String, Object>>> getApprovedGuides() {
+        List<Guide> guides = guideService.getActiveGuides();
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (Guide g : guides) {
+            User u = userMapper.selectById(g.getUserId());
+            if (u == null || !"approved".equals(u.getVendorStatus())) continue;
+            Map<String, Object> item = new HashMap<>();
+            item.put("userId", g.getUserId());
+            item.put("nickname", u.getNickname());
+            item.put("realName", u.getRealName());
+            item.put("avatar", u.getAvatar());
+            item.put("title", g.getTitle());
+            item.put("bio", g.getBio());
+            item.put("intro", g.getIntro());
+            item.put("specialty", g.getSpecialty());
+            item.put("level", g.getLevel());
+            item.put("guideId", g.getId());
+            result.add(item);
+        }
+        return Result.success(result);
+    }
+
     @PostMapping("/packages/create")
     public Result<Boolean> createPackage(@RequestBody GuidePackage pkg) {
         return Result.success(guidePackageService.save(pkg));

+ 16 - 18
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminInventoryController.java

@@ -12,6 +12,7 @@ import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "管理端-库存管理")
 @RestController
@@ -27,7 +28,7 @@ public class AdminInventoryController {
     @PostMapping("/inbound/create")
     public Result<Map<String, Object>> createInbound(@RequestBody Map<String, Object> params,
                                                       @RequestAttribute("userId") Long adminId) {
-        Long supplierId = params.get("supplierId") != null ? Long.valueOf(params.get("supplierId").toString()) : null;
+        Long supplierId = ParamUtils.getLong(params.get("supplierId"));
         String supplierName = (String) params.get("supplierName");
         String remark = (String) params.get("remark");
         Integer totalAmount = params.get("totalAmount") != null ? Integer.valueOf(params.get("totalAmount").toString()) : null;
@@ -55,7 +56,7 @@ public class AdminInventoryController {
     @Operation(summary = "入库单详情")
     @PostMapping("/inbound/detail")
     public Result<Map<String, Object>> detailInbound(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id不能为空");
         return inventoryService.detailInboundOrder(id);
     }
@@ -64,7 +65,7 @@ public class AdminInventoryController {
     @PostMapping("/inbound/confirm")
     public Result<String> confirmInbound(@RequestBody Map<String, Object> params,
                                           @RequestAttribute("userId") Long adminId) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id不能为空");
         return inventoryService.confirmInbound(id, adminId);
     }
@@ -72,7 +73,7 @@ public class AdminInventoryController {
     @Operation(summary = "取消入库单")
     @PostMapping("/inbound/cancel")
     public Result<String> cancelInbound(@RequestBody Map<String, Object> params) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id不能为空");
         return inventoryService.cancelInbound(id);
     }
@@ -84,8 +85,8 @@ public class AdminInventoryController {
     public Result<Map<String, Object>> listTransactions(@RequestBody Map<String, Object> params) {
         int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
         int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
-        Long productId = params.get("productId") != null ? Long.valueOf(params.get("productId").toString()) : null;
-        Long skuId = params.get("skuId") != null ? Long.valueOf(params.get("skuId").toString()) : null;
+        Long productId = ParamUtils.getLong(params.get("productId"));
+        Long skuId = ParamUtils.getLong(params.get("skuId"));
         String type = (String) params.get("type");
         String startDate = (String) params.get("startDate");
         String endDate = (String) params.get("endDate");
@@ -98,8 +99,8 @@ public class AdminInventoryController {
     @PostMapping("/counting/create")
     public Result<String> createCounting(@RequestBody Map<String, Object> params,
                                           @RequestAttribute("userId") Long adminId) {
-        Long productId = params.get("productId") != null ? Long.valueOf(params.get("productId").toString()) : null;
-        Long skuId = params.get("skuId") != null ? Long.valueOf(params.get("skuId").toString()) : null;
+        Long productId = ParamUtils.getLong(params.get("productId"));
+        Long skuId = ParamUtils.getLong(params.get("skuId"));
         Integer actualStock = params.get("actualStock") != null ? Integer.valueOf(params.get("actualStock").toString()) : null;
         String remark = (String) params.get("remark");
         if (productId == null) return Result.error("productId不能为空");
@@ -111,7 +112,7 @@ public class AdminInventoryController {
     @PostMapping("/counting/confirm")
     public Result<String> confirmCounting(@RequestBody Map<String, Object> params,
                                            @RequestAttribute("userId") Long adminId) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id不能为空");
         return inventoryService.confirmCounting(id, adminId);
     }
@@ -138,7 +139,7 @@ public class AdminInventoryController {
     @Operation(summary = "商品库存详情")
     @PostMapping("/product/stock")
     public Result<Map<String, Object>> productStock(@RequestBody Map<String, Object> params) {
-        Long productId = params.get("productId") != null ? Long.valueOf(params.get("productId").toString()) : null;
+        Long productId = ParamUtils.getLong(params.get("productId"));
         if (productId == null) return Result.error("productId不能为空");
         return inventoryService.getProductStock(productId);
     }
@@ -148,8 +149,7 @@ public class AdminInventoryController {
     @Operation(summary = "配置套餐商品组成")
     @PostMapping("/bundle/items")
     public Result<String> saveBundleItems(@RequestBody Map<String, Object> params) {
-        Long bundleProductId = params.get("bundleProductId") != null
-                ? Long.valueOf(params.get("bundleProductId").toString()) : null;
+        Long bundleProductId = ParamUtils.getLong(params.get("bundleProductId"));
         if (bundleProductId == null) return Result.error("bundleProductId不能为空");
         @SuppressWarnings("unchecked")
         List<Map<String, Object>> items = (List<Map<String, Object>>) params.get("items");
@@ -159,8 +159,7 @@ public class AdminInventoryController {
     @Operation(summary = "查询套餐商品组成")
     @PostMapping("/bundle/items/list")
     public Result<List<ProductBundleItem>> listBundleItems(@RequestBody Map<String, Object> params) {
-        Long bundleProductId = params.get("bundleProductId") != null
-                ? Long.valueOf(params.get("bundleProductId").toString()) : null;
+        Long bundleProductId = ParamUtils.getLong(params.get("bundleProductId"));
         if (bundleProductId == null) return Result.error("bundleProductId不能为空");
         return inventoryService.listBundleItems(bundleProductId);
     }
@@ -168,8 +167,7 @@ public class AdminInventoryController {
     @Operation(summary = "检查套餐库存")
     @PostMapping("/bundle/check-stock")
     public Result<String> checkBundleStock(@RequestBody Map<String, Object> params) {
-        Long bundleProductId = params.get("bundleProductId") != null
-                ? Long.valueOf(params.get("bundleProductId").toString()) : null;
+        Long bundleProductId = ParamUtils.getLong(params.get("bundleProductId"));
         Integer quantity = params.get("quantity") != null
                 ? Integer.valueOf(params.get("quantity").toString()) : 1;
         if (bundleProductId == null) return Result.error("bundleProductId不能为空");
@@ -182,8 +180,8 @@ public class AdminInventoryController {
     @PostMapping("/outbound/create")
     public Result<String> createManualOutbound(@RequestBody Map<String, Object> params,
                                                 @RequestAttribute("userId") Long adminId) {
-        Long productId = params.get("productId") != null ? Long.valueOf(params.get("productId").toString()) : null;
-        Long skuId = params.get("skuId") != null ? Long.valueOf(params.get("skuId").toString()) : null;
+        Long productId = ParamUtils.getLong(params.get("productId"));
+        Long skuId = ParamUtils.getLong(params.get("skuId"));
         Integer quantity = params.get("quantity") != null ? Integer.valueOf(params.get("quantity").toString()) : null;
         String reason = params.get("reason") != null ? params.get("reason").toString() : null;
         String remark = (String) params.get("remark");

+ 0 - 77
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminMenuController.java

@@ -1,77 +0,0 @@
-package com.etotem.cfc.controller.admin;
-
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.entity.AdminMenu;
-import com.etotem.cfc.service.AdminMenuService;
-import io.swagger.v3.oas.annotations.Operation;
-import org.springframework.web.bind.annotation.*;
-
-import javax.annotation.Resource;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-
-@RestController
-@RequestMapping("/api/admin/menu")
-public class AdminMenuController {
-
-    @Resource
-    private AdminMenuService menuService;
-
-    @Operation(summary = "获取菜单树")
-    @PostMapping("/tree")
-    public Result<List<Map<String, Object>>> tree() {
-        return Result.success(menuService.buildTree());
-    }
-
-    @Operation(summary = "获取所有菜单(平铺)")
-    @PostMapping("/list")
-    public Result<List<AdminMenu>> list() {
-        return Result.success(menuService.list());
-    }
-
-    @Operation(summary = "新增菜单")
-    @PostMapping("/add")
-    public Result<Void> add(@RequestBody AdminMenu menu) {
-        menu.setCreatedAt(new Date());
-        menu.setUpdatedAt(new Date());
-        menuService.save(menu);
-        return Result.success();
-    }
-
-    @Operation(summary = "更新菜单")
-    @PostMapping("/edit")
-    public Result<Void> edit(@RequestBody AdminMenu menu) {
-        AdminMenu existing = menuService.getById(menu.getId());
-        if (existing == null) return Result.error("菜单不存在");
-        menu.setUpdatedAt(new Date());
-        menuService.updateById(menu);
-        return Result.success();
-    }
-
-    @Operation(summary = "删除菜单")
-    @PostMapping("/delete/{id}")
-    public Result<Void> delete(@PathVariable Long id) {
-        List<AdminMenu> children = menuService.list(
-                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<AdminMenu>()
-                        .eq(AdminMenu::getParentId, id));
-        if (!children.isEmpty()) {
-            return Result.error("存在子菜单,无法删除");
-        }
-        menuService.removeById(id);
-        return Result.success();
-    }
-
-    @Operation(summary = "调整排序")
-    @PostMapping("/sort")
-    public Result<Void> sort(@RequestBody Map<String, Object> payload) {
-        Long id = Long.valueOf(payload.get("id").toString());
-        Integer sortOrder = (Integer) payload.get("sortOrder");
-        AdminMenu menu = menuService.getById(id);
-        if (menu == null) return Result.error("菜单不存在");
-        menu.setSortOrder(sortOrder);
-        menu.setUpdatedAt(new Date());
-        menuService.updateById(menu);
-        return Result.success();
-    }
-}

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

@@ -12,6 +12,7 @@ import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
 import com.etotem.cfc.util.SortUtil;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "公告管理", description = "平台公告的增删改查")
 @RestController
@@ -90,7 +91,7 @@ public class AdminNoticeController {
     @Operation(summary = "删除公告(软删)")
     @PostMapping("/delete")
     public Result<String> delete(@RequestBody java.util.Map<String, Object> body) {
-        Long id = Long.valueOf(body.get("id").toString());
+        Long id = ParamUtils.getLong(body.get("id"));
         Notice notice = noticeMapper.selectById(id);
         if (notice == null) return Result.error("公告不存在");
         notice.setStatus("deleted");
@@ -102,7 +103,7 @@ public class AdminNoticeController {
     @Operation(summary = "置顶(将指定公告移到最前)")
     @PostMapping("/pin")
     public Result<String> pin(@RequestBody java.util.Map<String, Object> body) {
-        Long id = Long.valueOf(body.get("id").toString());
+        Long id = ParamUtils.getLong(body.get("id"));
         Notice notice = noticeMapper.selectById(id);
         if (notice == null) return Result.error("公告不存在");
         notice.setCreatedAt(new Date());

+ 90 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminNotificationTemplateController.java

@@ -0,0 +1,90 @@
+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.NotificationTemplate;
+import com.etotem.cfc.mapper.NotificationTemplateMapper;
+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.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 管理端-通知模板 CRUD(改版v1,W3)
+ * report_ready / reward_granted / system 文案配置,支持占位符 {domainName} {reward}
+ */
+@Tag(name = "管理端-通知模板", description = "通知模板 CRUD 管理")
+@RestController
+@RequestMapping("/api/admin/notification-template")
+public class AdminNotificationTemplateController {
+
+    @Resource
+    private NotificationTemplateMapper notificationTemplateMapper;
+
+    @Operation(summary = "通知模板列表")
+    @PostMapping("/list")
+    public Result<List<NotificationTemplate>> list() {
+        LambdaQueryWrapper<NotificationTemplate> wrapper = new LambdaQueryWrapper<NotificationTemplate>()
+                .orderByAsc(NotificationTemplate::getType)
+                .orderByAsc(NotificationTemplate::getId);
+        return Result.success(notificationTemplateMapper.selectList(wrapper));
+    }
+
+    @Operation(summary = "新增/更新通知模板")
+    @PostMapping("/save")
+    public Result<NotificationTemplate> save(@RequestBody NotificationTemplate template, @RequestAttribute("userId") Long userId) {
+        if (template.getType() == null || template.getType().isEmpty()) {
+            return Result.error("类型不能为空");
+        }
+        if (template.getTitle() == null || template.getTitle().isEmpty()) {
+            return Result.error("标题不能为空");
+        }
+        if (template.getContent() == null || template.getContent().isEmpty()) {
+            return Result.error("内容不能为空");
+        }
+        // 类型唯一性校验
+        NotificationTemplate exists = notificationTemplateMapper.selectOne(
+                new LambdaQueryWrapper<NotificationTemplate>()
+                        .eq(NotificationTemplate::getType, template.getType())
+                        .last("LIMIT 1"));
+        if (exists != null && (template.getId() == null || !exists.getId().equals(template.getId()))) {
+            return Result.error("类型已存在: " + template.getType());
+        }
+
+        Date now = new Date();
+        if (template.getId() == null) {
+            if (template.getStatus() == null) {
+                template.setStatus(1);
+            }
+            if (template.getChannels() == null || template.getChannels().isEmpty()) {
+                template.setChannels("in_app");
+            }
+            template.setCreatedAt(now);
+            template.setUpdatedAt(now);
+            notificationTemplateMapper.insert(template);
+        } else {
+            template.setUpdatedAt(now);
+            notificationTemplateMapper.updateById(template);
+        }
+        return Result.success(template);
+    }
+
+    @Operation(summary = "删除通知模板")
+    @PostMapping("/delete")
+    public Result<Void> delete(@RequestBody Map<String, Object> params, @RequestAttribute("userId") Long userId) {
+        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        if (id == null) {
+            return Result.error("缺少 id");
+        }
+        notificationTemplateMapper.deleteById(id);
+        return Result.success(null);
+    }
+}

+ 4 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminPeriodicServiceController.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.ParamUtils;
 
 @Tag(name = "管理端-周期性服务配置")
 @RestController
@@ -32,7 +33,7 @@ public class AdminPeriodicServiceController {
     @Operation(summary = "获取单个周期性服务配置")
     @PostMapping("/detail")
     public Result<PeriodicService> detail(@RequestBody Map<String, Object> params) {
-        Long id = Long.valueOf(params.get("id").toString());
+        Long id = ParamUtils.getLong(params.get("id"));
         PeriodicService config = periodicServiceMapper.selectById(id);
         if (config == null) return Result.error("配置不存在");
         return Result.success(config);
@@ -61,7 +62,7 @@ public class AdminPeriodicServiceController {
     @Operation(summary = "删除周期性服务配置")
     @PostMapping("/delete")
     public Result<String> delete(@RequestBody Map<String, Object> params) {
-        Long id = Long.valueOf(params.get("id").toString());
+        Long id = ParamUtils.getLong(params.get("id"));
         periodicServiceMapper.deleteById(id);
         return Result.success("删除成功");
     }
@@ -69,7 +70,7 @@ public class AdminPeriodicServiceController {
     @Operation(summary = "切换启用/禁用状态")
     @PostMapping("/toggle")
     public Result<String> toggle(@RequestBody Map<String, Object> params) {
-        Long id = Long.valueOf(params.get("id").toString());
+        Long id = ParamUtils.getLong(params.get("id"));
         PeriodicService config = periodicServiceMapper.selectById(id);
         if (config == null) return Result.error("配置不存在");
         config.setEnabled(config.getEnabled() != null && config.getEnabled() == 1 ? 0 : 1);

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

@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import java.util.List;
+import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "管理端-方案规则", description = "方案生成规则 CRUD")
 @RestController
@@ -34,7 +35,7 @@ public class AdminPlanRuleController {
     @Operation(summary = "删除规则")
     @PostMapping("/delete")
     public Result<Void> delete(@RequestBody java.util.Map<String, Object> params) {
-        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) return Result.error("id不能为空");
         ruleService.deleteRule(id);
         return Result.success(null);

+ 94 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProblemDomainController.java

@@ -0,0 +1,94 @@
+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.ProblemDomain;
+import com.etotem.cfc.mapper.ProblemDomainMapper;
+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.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
+
+/**
+ * 管理端-问题域 CRUD(改版v1,W1)
+ */
+@Tag(name = "管理端-问题域", description = "问题域 CRUD 管理")
+@RestController
+@RequestMapping("/api/admin/problem-domain")
+public class AdminProblemDomainController {
+
+    @Resource
+    private ProblemDomainMapper problemDomainMapper;
+
+    @Operation(summary = "问题域列表(含停用,管理端)")
+    @PostMapping("/list")
+    public Result<List<ProblemDomain>> list(@RequestBody Map<String, Object> params) {
+        LambdaQueryWrapper<ProblemDomain> wrapper = new LambdaQueryWrapper<ProblemDomain>()
+                .orderByAsc(ProblemDomain::getSortOrder)
+                .orderByAsc(ProblemDomain::getId);
+        // 可选状态过滤
+        if (params != null && params.get("status") != null) {
+            wrapper.eq(ProblemDomain::getStatus, Integer.parseInt(params.get("status").toString()));
+        }
+        return Result.success(problemDomainMapper.selectList(wrapper));
+    }
+
+    @Operation(summary = "新增/更新问题域")
+    @PostMapping("/save")
+    public Result<ProblemDomain> save(@RequestBody ProblemDomain domain, @RequestAttribute("userId") Long userId) {
+        if (domain.getCode() == null || domain.getCode().isEmpty()) {
+            return Result.error("编码不能为空");
+        }
+        if (domain.getName() == null || domain.getName().isEmpty()) {
+            return Result.error("名称不能为空");
+        }
+        // 编码唯一性校验
+        ProblemDomain exists = problemDomainMapper.selectOne(
+                new LambdaQueryWrapper<ProblemDomain>()
+                        .eq(ProblemDomain::getCode, domain.getCode())
+                        .last("LIMIT 1"));
+        if (exists != null && (domain.getId() == null || !exists.getId().equals(domain.getId()))) {
+            return Result.error("编码已存在: " + domain.getCode());
+        }
+
+        Date now = new Date();
+        if (domain.getId() == null) {
+            if (domain.getStatus() == null) {
+                domain.setStatus(1);
+            }
+            if (domain.getRewardCfValue() == null) {
+                domain.setRewardCfValue(0);
+            }
+            if (domain.getSortOrder() == null) {
+                domain.setSortOrder(0);
+            }
+            domain.setCreatedAt(now);
+            domain.setUpdatedAt(now);
+            problemDomainMapper.insert(domain);
+        } else {
+            domain.setUpdatedAt(now);
+            problemDomainMapper.updateById(domain);
+        }
+        return Result.success(domain);
+    }
+
+    @Operation(summary = "删除问题域")
+    @PostMapping("/delete")
+    public Result<Void> delete(@RequestBody Map<String, Object> params, @RequestAttribute("userId") Long userId) {
+        Long id = ParamUtils.getLong(params.get("id"));
+        if (id == null) {
+            return Result.error("缺少 id");
+        }
+        problemDomainMapper.deleteById(id);
+        return Result.success(null);
+    }
+}

+ 6 - 15
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductController.java

@@ -17,6 +17,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 import com.etotem.cfc.util.SortUtil;
+import com.etotem.cfc.util.ParamUtils;
 
 /**
  * 管理端-商品管理控制器
@@ -93,9 +94,7 @@ if (params.get("distributionSystemId") != null) {
     @Operation(summary = "审核商品")
     @PostMapping("/review")
     public Result<String> review(@RequestBody Map<String, Object> params) {
-        Long productId = params.get("productId") != null
-                ? Long.valueOf(params.get("productId").toString())
-                : null;
+        Long productId = ParamUtils.getLong(params.get("productId"));
         String action = (String) params.get("action");
         String reason = (String) params.get("reason");
 
@@ -112,9 +111,7 @@ if (params.get("distributionSystemId") != null) {
     @Operation(summary = "商品上下架")
     @PostMapping("/shelve")
     public Result<String> shelve(@RequestBody Map<String, Object> params) {
-        Long productId = params.get("productId") != null
-                ? Long.valueOf(params.get("productId").toString())
-                : null;
+        Long productId = ParamUtils.getLong(params.get("productId"));
         Boolean shelve = params.get("shelve") != null
                 ? (Boolean) params.get("shelve")
                 : null;
@@ -132,9 +129,7 @@ if (params.get("distributionSystemId") != null) {
     @Operation(summary = "商品详情")
     @PostMapping("/detail")
     public Result<ProductDTO> detail(@RequestBody Map<String, Object> params) {
-        Long productId = params.get("productId") != null
-                ? Long.valueOf(params.get("productId").toString())
-                : null;
+        Long productId = ParamUtils.getLong(params.get("productId"));
         if (productId == null) {
             return Result.error("productId不能为空");
         }
@@ -166,9 +161,7 @@ if (params.get("distributionSystemId") != null) {
     @Operation(summary = "复制商品")
     @PostMapping("/copy")
     public Result<ProductDTO> copy(@RequestBody Map<String, Object> params) {
-        Long productId = params.get("productId") != null
-                ? Long.valueOf(params.get("productId").toString())
-                : null;
+        Long productId = ParamUtils.getLong(params.get("productId"));
         if (productId == null) {
             return Result.error("productId不能为空");
         }
@@ -178,9 +171,7 @@ if (params.get("distributionSystemId") != null) {
     @Operation(summary = "删除商品")
     @PostMapping("/delete")
     public Result<String> delete(@RequestBody Map<String, Object> params) {
-        Long productId = params.get("productId") != null
-                ? Long.valueOf(params.get("productId").toString())
-                : null;
+        Long productId = ParamUtils.getLong(params.get("productId"));
         if (productId == null) {
             return Result.error("productId不能为空");
         }

+ 59 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductPickerController.java

@@ -0,0 +1,59 @@
+package com.etotem.cfc.controller.admin;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Product;
+import com.etotem.cfc.mapper.ProductMapper;
+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.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/admin/product")
+public class AdminProductPickerController {
+
+    @Resource
+    private ProductMapper productMapper;
+
+    @PostMapping("/linked/list")
+    public Result<Map<String, Object>> linkedList(@RequestBody Map<String, Object> params) {
+        Long productId = params.get("productId") != null ? Long.valueOf(params.get("productId").toString()) : null;
+        Long categoryId = params.get("categoryId") != null ? Long.valueOf(params.get("categoryId").toString()) : null;
+        String keyword = (String) params.get("keyword");
+        Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
+        Integer pageSize = params.get("pageSize") != null ? Integer.valueOf(params.get("pageSize").toString()) : 20;
+
+        // 同分类限制:只返回同一分类的商品
+        if (categoryId == null && productId != null) {
+            Product product = productMapper.selectById(productId);
+            if (product != null) {
+                categoryId = product.getCategoryId();
+            }
+        }
+
+        LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Product::getStatus, "on_shelf");
+        if (categoryId != null) {
+            wrapper.eq(Product::getCategoryId, categoryId);
+        }
+        if (keyword != null && !keyword.trim().isEmpty()) {
+            wrapper.like(Product::getName, keyword.trim());
+        }
+
+        IPage<Product> pageResult = productMapper.selectPage(new Page<>(page, pageSize), wrapper);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("list", pageResult.getRecords());
+        result.put("total", pageResult.getTotal());
+        result.put("page", page);
+        result.put("pageSize", pageSize);
+        return Result.success(result);
+    }
+}

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio