Kaynağa Gözat

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

Sisyphus Agent 3 ay önce
ebeveyn
işleme
cd447ddf8e
100 değiştirilmiş dosya ile 7121 ekleme ve 552 silme
  1. 95 0
      book2/世界观设定.md
  2. 155 0
      book2/第001章-绑定.md
  3. 137 0
      book2/第002章-补给程序.md
  4. 91 0
      book2/第003章-错频.md
  5. 91 0
      book2/第004章-第三双眼睛.md
  6. 151 0
      book2/第005章-两侧的数据.md
  7. 117 0
      book2/第006章-第一个周日.md
  8. 107 0
      book2/第007章-卸载按钮.md
  9. 97 0
      book2/第008章-静默期.md
  10. 83 0
      book2/第009章-老周.md
  11. 102 0
      book2/第010章-十四日报告.md
  12. 102 0
      book2/第011章-版本更新.md
  13. 111 0
      book2/第012章-一个电话.md
  14. 101 0
      book2/第013章-周六下午.md
  15. 107 0
      book2/第014章-第一眼.md
  16. 101 0
      book2/第015章-没说的话.md
  17. 141 0
      book2/第016章-朋友.md
  18. 119 0
      book2/第017章-家长群.md
  19. 109 0
      book2/第018章-系统猜错的时候.md
  20. 81 0
      book2/第019章-家长会.md
  21. 128 0
      book2/第020章-满月.md
  22. 85 0
      book2/第021章-新阶段.md
  23. 91 0
      book2/第022章-通知.md
  24. 63 0
      book2/第023章-等待.md
  25. 99 0
      book2/第024章-没结果的结果.md
  26. 85 0
      book2/第025章-系统的一步.md
  27. 105 0
      book2/第026章-爸妈要来.md
  28. 93 0
      book2/第027章-收拾房间.md
  29. 111 0
      book2/第028章-到了.md
  30. 101 0
      book2/第029章-不习惯.md
  31. 76 0
      book2/第030章-调整.md
  32. 93 0
      book2/第031章-钓鱼.md
  33. 89 0
      book2/第032章-走之前.md
  34. 91 0
      book2/第033章-空下来的房子.md
  35. 87 0
      book2/第034章-面谈.md
  36. 113 0
      book2/第035章-last-day.md
  37. 99 0
      book2/第036章-周一早上.md
  38. 109 0
      book2/第037章-搜索.md
  39. 91 0
      book2/第038章-接放学.md
  40. 105 0
      book2/第039章-第九十天.md
  41. 129 0
      book2/第040章-深度观察模式.md
  42. 105 0
      book2/第041章-老领导.md
  43. 107 0
      book2/第042章-新的开始.md
  44. 101 0
      book2/第043章-第一天.md
  45. 99 0
      book2/第044章-后台.md
  46. 94 0
      book2/第045章-孩子的模型.md
  47. 101 0
      book2/第046章-匿名数据.md
  48. 71 0
      book2/第047章-第一个改动.md
  49. 73 0
      book2/第048章-第一百天.md
  50. 81 0
      book2/第049章-来自系统的推送.md
  51. 91 0
      book2/第050章-日常.md
  52. 37 0
      cfc-backend/src/main/java/com/etotem/cfc/common/HmacSignUtil.java
  53. 9 1
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  54. 33 3
      cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java
  55. 31 3
      cfc-backend/src/main/java/com/etotem/cfc/controller/DanAssessmentController.java
  56. 31 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java
  57. 6 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/ShareController.java
  58. 195 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/VirtualPayController.java
  59. 26 13
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/InteractionLogController.java
  60. 25 30
      cfc-backend/src/main/java/com/etotem/cfc/controller/subscription/SubscriptionController.java
  61. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java
  62. 45 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/VirtualPayParamsDTO.java
  63. 6 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/User.java
  64. 71 0
      cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentOrderService.java
  65. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/service/DimensionWeightService.java
  66. 6 0
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java
  67. 78 11
      cfc-backend/src/main/java/com/etotem/cfc/service/MemberSubscriptionService.java
  68. 13 3
      cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java
  69. 17 3
      cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
  70. 449 0
      cfc-backend/src/main/java/com/etotem/cfc/service/VirtualPayService.java
  71. 232 0
      cfc-backend/src/main/java/com/etotem/cfc/task/VirtualPayTimeoutTask.java
  72. 13 7
      cfc-backend/src/main/resources/application.yml
  73. 1 0
      cfc-backend/src/main/resources/schema.sql
  74. 2 2
      cfc-backend/src/test/java/com/etotem/cfc/controller/family/FamilyMembersControllerTest.java
  75. 2 2
      cfc-backend/src/test/java/com/etotem/cfc/integration/FamilyMemberManagementFlowTest.java
  76. 305 0
      cfc-backend/src/test/java/com/etotem/cfc/service/VirtualPayServiceTest.java
  77. 27 2
      cfc-frontend/components/FamilyMemberStrip.vue
  78. 2 0
      cfc-frontend/components/share-mixin.js
  79. 0 6
      cfc-frontend/pages.json
  80. 0 1
      cfc-frontend/pages/action-detail/interaction-log.vue
  81. 2 2
      cfc-frontend/pages/action-detail/member-action-detail.vue
  82. 2 2
      cfc-frontend/pages/action/index.vue
  83. 0 1
      cfc-frontend/pages/action/interaction-log.vue
  84. 2 2
      cfc-frontend/pages/action/member-action-detail.vue
  85. 23 2
      cfc-frontend/pages/activity/activity-detail/activity-detail.vue
  86. 79 24
      cfc-frontend/pages/assessment/purchase.vue
  87. 1 1
      cfc-frontend/pages/body-detail/dimension-detail.vue
  88. 2 2
      cfc-frontend/pages/body-detail/member-body-detail.vue
  89. 2 2
      cfc-frontend/pages/body/index.vue
  90. 2 2
      cfc-frontend/pages/body/member-body-detail.vue
  91. 1 1
      cfc-frontend/pages/discover/index.vue
  92. 34 7
      cfc-frontend/pages/family/add-member.vue
  93. 1 1
      cfc-frontend/pages/guide/bind/invite.vue
  94. 3 3
      cfc-frontend/pages/home-pages/child-index.vue
  95. 1 1
      cfc-frontend/pages/home-pages/member-home-detail.vue
  96. 33 6
      cfc-frontend/pages/home-pages/parent-index.vue
  97. 35 9
      cfc-frontend/pages/index-home/index.vue
  98. 2 2
      cfc-frontend/pages/member-detail/member-detail.vue
  99. 0 383
      cfc-frontend/pages/membership/plans.vue
  100. 66 8
      cfc-frontend/pages/membership/upgrade.vue

+ 95 - 0
book2/世界观设定.md

@@ -0,0 +1,95 @@
+# 《我家有个幸福系统》世界观设定
+
+## 时代背景
+2028年,深圳。
+
+## 技术设定
+- 系统不是AR透镜,是**手机App**——准确地说,是小付公司参与的一个AI内测项目,装在他手机上。
+- 数据来源有限:手机使用习惯、智能手表(心率/睡眠/步数)、短信和通话记录(授权)、麦克风(语音语调分析,仅限App内调用)。
+- **系统不完美。它经常猜错。** 它分析的是手机数据,不是读心术。它可能把"刷短视频笑了一下"解读为"情绪好转",也可能把"开会时被领导骂了"误判为"家庭关系紧张"。它的判断有延迟,有偏差,有盲区。
+- 小艾没有手机,没有手表。系统对他几乎一无所知——这是刻意设定的"系统盲区"。
+- 系统界面风格:JH-7是军用后勤框架的民用改装版,UI像军用软件——灰底、绿字、方框、数据表。不漂亮。跟手机上的其他App格格不入。
+- 小茜的手机也装了同一个App,但她的账号权限配置不同,看到的界面跟小付不一样。
+
+## 家庭状态
+- **住房**:深圳宝安,一套80平小两房,月供1万6。买房时掏空了双方父母的积蓄。
+- **收入**:小付月薪到手约1万8,小茜自由职业不稳定,平均月入8千到1万二。每月还完房贷剩七八千,覆盖全家生活开销。
+- **压力**:小付公司在收缩,上半年裁了一轮,下一轮不知道什么时候来。小茜接私单经常做到凌晨,不是热爱,是想多攒点钱。
+- **日常**:小艾上公立小学一年级,没有课外班(报不起)。家长群消息每天几百条,小茜看,小付不看。
+
+## JH-7 系统特性
+- 前身:JH-7型联合后勤调度框架,军标,负责营级单位后勤调度。
+- 民用化改装后,将家庭视为一个"作战单位"进行效率分析和任务调度。
+- 它的核心逻辑是"提升作战效率"——它不在乎你开不开心,它在乎你们这个单位能不能持续运转。
+- 它不会说人话。所有提示都是军事术语的硬翻译。
+- 它会在使用过程中学习,但学习速度很慢——它需要足够的数据量才能更新模型。
+- 最关键的设定:**它没有小艾的数据。** 它不知道家里有个孩子会对家庭系统产生什么影响——这是它最大的盲区,也是后期最大的变量。
+
+## 双人视角差异
+
+| 维度 | 小付 | 小茜 |
+|------|------|------|
+| 界面 | 五维评分面板,军事风格,数字驱动 | 温度曲线,趋势图,颜色驱动 |
+| 任务 | 作战指令/补给任务/日常巡查 | 联络协议/心理维护/预警通知 |
+| 语言 | 量化评分/效率分析/建议 | 状态描述/趋势预警/提示 |
+| 盲区 | 看不懂情绪,把"不说话"理解成"无异常" | 看不懂数据,把"数值上升"理解成"没问题了" |
+
+## 五维设定(小付界面)
+
+**身**:睡眠、运动、饮食规律性。数据来源:智能手表+手机使用时间(判断熬夜)。
+**智**:阅读、学习、信息输入。数据来源:手机使用分类(阅读类vs娱乐类耗时)。
+**富**:财务稳定性。数据来源:银行卡短信+手动输入的支出记录。(注:早期不准,因为小付懒得记。)
+**行**:伴侣间互动质量。数据来源:通话时长、短信数量、语音语调分析。(注:最不准的维度,因为系统无法区分"在吵架"和"在好好聊天"。)
+**心**:情绪状态。数据来源:睡眠质量+手机使用模式+语音语调。(注:偶尔准,偶尔离谱。)
+
+## 人物设定
+
+**小付** 34岁,某硬件公司项目经理。月薪到手1万8。
+- 不是不善表达,是懒得表达。觉得"日子能过就行"。
+- 不刷短视频,但睡前会刷半小时数码论坛。
+- 对家庭经济状况有模糊的焦虑,但不细算,因为算了也解决不了什么。
+- 参加内测的原因:公司推的,有200块补贴。
+
+**小茜** 32岁,平面设计师(自由职业)。月均收入不稳定。
+- 敏感,但不说。希望对方能自己看出来。
+- 接私单到凌晨的真实原因:想攒一笔"万一"的钱。万一小付失业了,家里还能撑几个月。
+- 她比小付更早意识到这个家有问题——不是大问题,是"温水煮青蛙"式的慢慢变淡。
+- 小付不知道她存了一笔钱。她没说过。
+
+**小艾** 7岁,公立小学一年级。
+- 没有手机,没有智能手表。系统完全看不到他。
+- 成绩中等,不算突出也不算差。
+- 最近养成了一个习惯:每天晚上睡前,会把自己白天在学校画的画拿给妈妈看。小茜每次都看,但有时候看完了才想起来自己刚才根本没看进去。
+
+## 人物体系规划(1000章级)
+
+### 核心家庭(3人)
+小付、小茜、小艾——三角关系,贯穿全书。
+
+### 长辈线(4人)
+- **小付母亲**:退休小学教师,住龙岗,每周打电话问"回不回来吃饭"。控制欲中等偏上,但不算夸张。
+- **小付父亲**:退休工厂技工,话少,喜欢钓鱼。小付跟他之间的对话模式跟跟小茜之间的对话模式如出一辙——不是遗传,是身教。
+- **小茜父母**:住湖南老家,每年来深圳住一两个月。每次来都是一种微妙的"入侵"——不是恶意,是生活方式的碰撞。
+- **注**:长辈线在前期只做背景铺垫,100章后逐步展开。
+
+### 外部对照(4-6个家庭/人物)
+- **老周**:小付前同事,同龄,工薪家庭。跟小付以前的状态一样——"也没什么不好,但也没什么好"。作为小付的"对照组"出现。
+- **老周妻子**:通过老周间接出场,性格跟老周不一样——她是想改变的,但一个人推不动。
+- **小付公司领导**:40+,离婚了。反面案例,让小付看到"如果不管会怎样"。
+- **小茜的设计师朋友**:单身,自由职业,过着小茜"如果没结婚可能会过"的生活——不是更好,只是不同。
+- **小艾班主任**:30+女性,未婚。通过家长群和家访出场,提供外部视角看这个家庭。
+- **邻居一家**:看起来很完美的家庭——丈夫事业有成,妻子温柔贤惠,孩子成绩好。后期揭开也有自己的问题。
+
+### 系统线(3-4人)
+- **云巢科技内测项目组**:后台人员,中期通过App更新公告/客服对话/电话回访等方式间接出场。
+- **其他内测用户**:80章后系统联网时出现,通过匿名数据对比引入。
+- **JH-7前身相关人**:200章后视情况展开。
+
+### 总量控制
+- 核心角色:3人(贯穿全书)
+- 常驻配角:8-10人(老周、双方父母、班主任、设计师朋友、邻居等)
+- 阶段配角:15-20人(每个故事弧3-5人,完成后退场)
+- 总角色池:约30人。不贪多。
+
+## 关于"反派"的说明
+这本书没有具体的反派人物。对立面是"生活惯性"本身——沟通的无力感、经济压力带来的摩擦、两个人慢慢懒得说话了的那个过程。系统有时候是帮手,有时候也是麻烦制造者,因为它会猜错。

+ 155 - 0
book2/第001章-绑定.md

@@ -0,0 +1,155 @@
+# 第一章 绑定
+
+小付第一次注意到那个App,是公司行政群发的一条消息。
+
+「各位同事:云巢科技家庭AI内测项目,参与者可获200元补贴。有意者请填写下表。」
+
+他当时正在改一份技术方案,看到消息随手点开了表格。填了姓名、手机号、提交。前后不到一分钟。
+
+200块,够小艾一个月牛奶钱。
+
+他填完就把这事忘了。
+
+两周后的一个周二晚上,他收到一条短信:
+
+「【云巢科技】您已入选'家庭中枢系统'内测。请点击链接下载App,使用手机号登录。项目组将在您完成首周体验后联系您进行回访。」
+
+他当时刚下班,正站在厨房里等水烧开。小茜在客厅陪小艾写作业——其实也不是陪,是小艾在写,她在旁边刷手机。
+
+他看了一眼短信,下载了。
+
+App的图标是个灰色的圆形,中间一个蓝色的"7",没有任何多余的设计。安装完成后打开,界面让他愣了一下——不是那种常见的圆角毛玻璃风格,而是灰底、绿字、方框、表格。像……军用的。
+
+他注意到右上角有一行极小的字:
+
+**「JH-7 型联合后勤调度框架 · 家庭适配版 v0.1」**
+
+他皱了皱眉,但没多想。
+
+然后App弹出了一个权限请求列表:
+
+- 位置信息(用于分析活动范围)
+- 短信记录(用于识别财务类通知)
+- 通话记录(用于分析联络频率)
+- 麦克风权限(用于语音语调分析)
+- 健康数据(连接智能手表读取心率/睡眠/步数)
+
+他一条一条看下来,手指悬在"允许"上面。
+
+短信记录。通话记录。麦克风。
+
+他想了一下——200块,项目组应该不会拿这些数据干什么。而且公司推的,出了事公司也得负责吧。
+
+他点了全部允许。
+
+App的主界面加载出来了。
+
+五个竖条,每个竖条旁边标着一个字和一个数字,下面是像体检报告一样的小字说明。
+
+**身 47**
+
+*基于近30天数据分析:平均睡眠6.2小时。周运动频次:不足1次。静息心率较三个月前上升约5%。判断:基础状态偏低,但未到警戒线。*
+
+**智 68**
+
+*手机使用分类:工作相关占42%,资讯阅读占15%,娱乐(短视频/游戏)占28%。信息输入结构偏消耗型,建议增加系统性阅读。*
+
+**富 52**
+
+*数据不足,当前仅能识别工资入账短信和房贷扣款通知。手动记录功能尚未启用,评估可能偏离真实情况。*
+
+**行 31**
+
+*近7天与伴侣的对话分析:日均通话时长约4分钟(含事务性内容)。短信/微信消息日均约12条,其中非事务性内容占比约9%。语音语调分析:双方对话中情绪平稳,但无明显正向波动。*
+
+**心 42**
+
+*睡眠质量评分偏低。手机使用模式显示,近7天有3天在凌晨1点后仍有活跃操作。情绪波动曲线平缓——但平缓不一定是好事,可能只是没什么值得波动的事。*
+
+他盯着屏幕看了大概两分钟。
+
+然后他做了一件很自然的事——他截了个图,把手机放回口袋,没跟任何人说。
+
+——
+
+同一时间,客厅里。
+
+小茜坐在沙发上,手机屏幕也亮着同样的App。
+
+她下载的时间比小付晚几个小时——她睡前才看到小付转发给她的那条短信:"公司内测,有补贴,你也装一下。"
+
+她装了。
+
+她看到的界面跟小付不一样。
+
+没有五个竖条。没有数字。
+
+打开之后,屏幕上是一条淡蓝色的曲线,横轴是日期,纵轴没有标单位。曲线从七天前开始画——她不知道它哪来的七天前数据,可能是从手机历史记录里算的——一路慢慢地往下走,走到昨天,几乎贴着底部。
+
+曲线下面有一行字:
+
+**「家庭温度:近7天趋势——持续走低。当前状态:低温。」**
+
+低温。
+
+她看了那两个字很久。然后她看了一眼旁边正在写作业的小艾,又看了一眼厨房的方向——小付还在那里等水烧开。
+
+她把App关掉了。不是退出,是按了Home键,让那个灰色的图标缩回手机桌面。
+
+她不知道该怎么想这件事。
+
+一个App告诉你,你的家庭温度处于低温状态。
+
+她不知道该信它,还是该笑它——一个刚装上的App,凭什么知道?
+
+但她也没有卸载。
+
+——
+
+小艾写完了作业,抬起头来。
+
+"妈妈,我写完了。"
+
+"嗯,放书包里吧。"
+
+小艾把作业本塞进书包,然后从书包里掏出一张叠得皱巴巴的纸,递给她。
+
+"今天美术课画的。"
+
+小茜接过来展开。是一幅画——用蜡笔画的一家三口,站在一栋房子前面。房子是红色的,天是蓝色的,三个人都在笑。
+
+她看着那幅画,笑了一下。
+
+"画得真好。这个是爸爸?"
+
+"嗯。这个是妈妈。这个是我。"
+
+"爸爸为什么是绿色的?"
+
+"因为他穿的是绿色的衣服呀。"
+
+小茜把画放在茶几上,没有折起来。
+
+"去刷牙吧,早点睡。"
+
+小艾"嗯"了一声,跑向卫生间。
+
+小茜又看了一眼那张画。三个人都在笑。
+
+她把手机拿起来,又打开了那个App。
+
+那条曲线还在。
+
+她盯着曲线最右端,今天的位置——然后又看了一眼茶几上那张画。忽然觉得这两个东西摆在一起,有一种说不清的矛盾感。
+
+画的是一家三口在笑。
+
+App说温度在走低。
+
+她不知道哪个是真的。
+
+也许两个都是真的。
+
+她关掉App,把手机屏幕朝下放在了茶几上。
+
+【第一章 · 完】

+ 137 - 0
book2/第002章-补给程序.md

@@ -0,0 +1,137 @@
+# 第二章 补给程序
+
+第二天中午,小付在公司食堂吃完午饭,打开那个App看了一眼。
+
+数据没变。
+
+身47、智68、富52、行31、心42。
+
+他点了一下「行」后面的小箭头,展开了一段说明。
+
+**「行:伴侣间互动质量」**
+
+*近7天与伴侣的对话分析:*
+*- 日均通话时长:约4分钟(含事务性内容)*
+*- 短信/微信消息日均:约12条*
+*- 非事务性内容占比:约9%*
+*——低于建议值(≥20%)。*
+
+*建议:每日至少进行一次非事务性交流。建议时长不低于10分钟。内容建议:不涉及孩子、财务、家庭事务。*
+
+非事务性交流。
+
+他想了想昨天跟小茜说过的话——"电费交了吗""小艾作业写完了吗""明天要不要买菜"——大概确实全是事务性的。唯一一句不算事务的,可能是他进门时说了一句"今天好热",小茜回了一句"嗯,开了空调"。
+
+10分钟。
+
+他关掉App,继续上班。
+
+但那个9%的数字在他脑子里待了很久。
+
+——
+
+下午三点,App弹了一条推送。
+
+不是通知栏的那种横幅推送——是App内部的一个气泡消息,灰色的,像系统内部消息。
+
+**「内部联络评估:目标F-1073-2(成员B)当前情绪储备处于低位。建议启动补给程序。」**
+
+**「查看补给建议」**
+
+他点了一下。
+
+**「补给建议:非事务性接触1次。时长建议:不低于10分钟。内容建议:不涉及孩子、财务、家庭事务。」**
+
+跟中午看到的一样。
+
+他把手机锁屏了。
+
+——
+
+晚上七点四十,他到家。
+
+小茜在厨房热菜,灶台上放着两个盘子。小艾在客厅茶几上写作业——其实是在作业本上画小人,看到小付进门,喊了一声"爸爸回来了",然后继续画。
+
+小付换了鞋,把包放下,走到厨房门口。
+
+小茜背对着他,正在用锅铲把菜翻匀。
+
+他靠着门框站了一会儿,脑子里转着"10分钟"和"非事务性交流"这两个词。
+
+他开口了。
+
+"今天……那个甲方稿子过了吗?"
+
+"过了。改了三版,终于过了。"
+
+"那挺好的。"
+
+"嗯。"
+
+对话结束。
+
+他站在厨房门口,没走,也没再说话。小茜回头看了他一眼,表情是"你站在这儿干嘛"。
+
+他感觉到了,但不知道该怎么解释——我站在这儿是因为一个App说我们之间需要非事务性交流——他想了想,还是没说。
+
+他转身去客厅了。
+
+——
+
+他不知道的是,小茜今天下午也收到了App的推送。
+
+她的推送不是文字,是一条曲线图——昨天的家庭温度曲线,每小时的波动,像心电图一样起起伏伏。她看到曲线在傍晚六点半左右有一小段回升——那个时间正好是小付出地铁站走回家的时间。
+
+她不知道这个回升跟他有没有关系——也许只是因为她当时在做饭,没看手机,系统没数据,所以把曲线拉平了。
+
+但她还是注意到了那个时间点。
+
+六点半。
+
+他下班到家的时间。
+
+——
+
+晚上九点,小艾睡着了。
+
+小付坐在书房里刷手机——其实是在看那个App的设置页面,翻来翻去。他看到有一个"共享"选项,点进去发现是空白的,写着一行灰色小字:
+
+**「此功能暂未开放。预计在首次系统更新后可用。」**
+
+他把手机放下了。
+
+然后他站起来,走到客厅。小茜还在沙发上,手机也在手里,不知道在看什么。
+
+他坐到了沙发另一头。
+
+两个人中间隔了大概一个身位。
+
+他沉默了一会儿,说了一句:
+
+"那个App……你看了吗?"
+
+小茜从手机屏幕上抬起头来。
+
+"看了。"
+
+"你觉得……准吗?"
+
+小茜想了想。
+
+"那条曲线说我最近情绪在走低。"她说。"我觉得……不算错。"
+
+她说完这句话,没有看他,继续低头看手机了。
+
+小付坐在沙发另一头,不知道该接什么。
+
+他最后说了一句:"那……它有没有说怎么弄?"
+
+"说了。但它说的那些,你自己也知道。"
+
+"……也是。"
+
+对话到此为止。
+
+但两人都在沙发上多坐了一会儿,才各自去洗漱睡觉。
+
+【第二章 · 完】

+ 91 - 0
book2/第003章-错频.md

@@ -0,0 +1,91 @@
+# 第三章 错频
+
+第三天早上,小付在地铁上打开了App。
+
+数据有变化了——不是五个维度,是主界面右下角多了一个消息图标,右上角标着一个红色的"1"。
+
+他点开。
+
+**「补给方案 ② 已生成」**
+
+**「携带一件非必要物品回家。预算:不超过当日收入的2%。」**
+
+**「预期效果:成员B情绪值小幅回升。当前情绪基线:偏低。」**
+
+非必要物品。不超过当日收入的2%。
+
+他心算了一下——月薪1万8,日薪大概600,2%就是12块。
+
+12块。
+
+他想了想12块能买什么。
+
+一瓶她上次说好喝的酸奶?楼下水果摊的一小盒草莓?便利店的一支花——如果便利店有的话。
+
+他不知道该买什么。他以前很少"专门"买什么东西回家——买菜、买水果、买日用品,都是"顺便"。专门买,意味着刻意。刻意意味着"我知道你最近不太开心,我注意到了"——他不太确定自己准备好传递这个信号。
+
+他把那条消息标记为"已读",但没有标记为"已完成"。
+
+——
+
+下午,小茜也收到了一条推送。
+
+她的推送不是"建议",而是一条系统通知:
+
+**「通知:成员A 正在执行物资获取任务。建议:保持当前活动,不做干预。」**
+
+她看到这条消息的时候正在改甲方意见回来的第三版稿子。她停下来,把手机放在桌上,盯着那行字看了好一会儿。
+
+他在执行物资获取任务。
+
+她不知道"物资获取"是什么——是去买菜了?还是去取快递了?还是那个App让他去做什么事?
+
+她不知道。但她没有问他。
+
+她继续改稿子了。但之后的大半个小时里,她时不时会看一眼手机——不是等消息,就是……看一眼。
+
+——
+
+晚上七点。
+
+小付到家的时候,手里拎着一个便利店的塑料袋。
+
+不是花。不是草莓。是一瓶她上次说好喝的酸奶——他记得是两周前,她跟朋友打电话的时候提了一句"那个牌子的酸奶挺好喝的",他当时在书房,听到了。
+
+他不确定她是不是真的说过,也不确定自己记得对不对。
+
+他进门的时候,把酸奶放在了冰箱里,没说什么。
+
+小茜在厨房,听到了冰箱门打开又关上的声音,没回头看他。
+
+——
+
+晚上十点,小茜打开冰箱拿水的时候看到了那瓶酸奶。
+
+她拿起来看了一下牌子——是那个她两周前在电话里提过的牌子。
+
+她握着那瓶酸奶在冰箱门口站了几秒钟。
+
+然后她把酸奶放回去了,没喝。
+
+不是不想喝。是——她不知道该怎么回应这件事。说"谢谢"太正式。不说又好像没看到。她选择了暂时不处理。
+
+她关上冰箱门,回房间了。
+
+但第二天早上,她打开冰箱的时候,把那瓶酸奶拿出来喝了。
+
+——
+
+第二天,小付注意到App里「行」的数值从31变成了32。
+
+涨了1。
+
+他不知道是因为那瓶酸奶,还是因为别的什么——也许只是因为他昨天在地铁上给一个老人让了座,系统误读了什么信号。
+
+毕竟这个App经常猜错。
+
+但32还是32。
+
+他没有深究,关掉了App,出门上班了。
+
+【第三章 · 完】

+ 91 - 0
book2/第004章-第三双眼睛.md

@@ -0,0 +1,91 @@
+# 第四章 第三双眼睛
+
+小艾注意到一件事。
+
+最近几天,爸爸妈妈看手机的次数变多了。
+
+不是那种"刷手机"的多——小艾知道大人刷手机是什么样子的,爸爸吃饭的时候看,妈妈睡前也看。但最近不一样。他们是把手机拿起来,盯着屏幕看几秒钟,然后放下,然后过了一会儿又拿起来看一眼。
+
+像是在等什么消息。
+
+但又不是在等消息——因为他们的表情不像是在等消息。他们像是在……查什么东西。
+
+小艾没有手机。他们班上有几个同学有电话手表,但他没有——妈妈说"等你三年级再买"。所以他不理解"盯着屏幕看几秒钟"这个动作到底是在干什么。
+
+但他确定一件事:爸爸妈妈最近有点怪。
+
+——
+
+第四天晚上,发生了一件小事。
+
+小付在洗碗。小茜在客厅叠衣服。小艾在自己的房间里——他应该在做作业,但他没有。
+
+他偷偷走到客厅门口,探出半个头。
+
+他看到妈妈叠着叠着衣服,停下来,拿起旁边的手机,看了一眼屏幕,放下,继续叠。过了大概两分钟,又拿起手机看了一眼。
+
+他想了想,跑到厨房门口。
+
+爸爸在洗碗,水龙头开着。他放在灶台上的手机屏幕亮着——一个灰色的界面,上面有几个竖条和数字。小艾没看清具体是什么,因为爸爸在看到他的一瞬间,把手机拿起来,锁屏了。
+
+"作业写完了?"
+
+"快写完了。爸爸你在看什么?"
+
+"没什么。一个……工作上的东西。"
+
+小艾没再追问。他回去了。
+
+但他知道——爸爸手机上那个灰色的界面,跟妈妈最近一直在看的东西,是一样的。
+
+——
+
+第二天,小艾用零花钱在楼下文具店买了三张贴纸。
+
+猫的。三块钱。
+
+他一张贴在了自己的书桌上。
+
+一张贴在了爸爸的枕头底下——他趁爸爸还没下班,偷偷溜进主卧贴的。
+
+一张贴在了妈妈梳妆镜的右上角。
+
+三张一样的贴纸,上面印着一行字:**「加油」**
+
+他贴完以后什么都没说。
+
+——
+
+晚上,小茜卸妆的时候看到了镜子上那张贴纸。
+
+她停下动作,看了两秒钟。没撕掉。
+
+她对着镜子站了一会儿,然后伸手摸了一下那张贴纸的边缘,把它按得更紧了一些。
+
+——
+
+小付睡前翻枕头的时候摸到了那张贴纸,拿起来看了一眼,愣了一下。
+
+他不用猜就知道是谁贴的。
+
+他拿着那张贴纸在床边坐了一会儿,然后把它贴在了床头柜的侧面。
+
+——
+
+那天晚上,JH-7在后端记录了一笔数据。
+
+它没有归类这个事件——因为"成员C"不在它的分析模块覆盖范围内。它只是把这条记录存了下来:
+
+**「未归类事件:成员C投放文字激励类物品×3。目标:全成员。效果评估:无法分析——建议后续为成员C分配适配模块。」**
+
+没有评分。没有结论。
+
+但系统记住了这笔数据。
+
+——因为如果成员C的数据持续缺失,它会影响整个家庭模型的准确性。
+
+系统没有把这个结论告诉任何人。
+
+但它已经开始在后台做准备了。
+
+【第四章 · 完】

+ 151 - 0
book2/第005章-两侧的数据.md

@@ -0,0 +1,151 @@
+# 第五章 两侧的数据
+
+第五天。
+
+小付在公司午休的时候打开了App,发现主界面左上角多了一个入口——两个人形并排站着的图标。
+
+他点了一下,展开了一个新页面。
+
+**「联合作战匹配度:45/100」**
+
+下面是一行说明:
+
+**「双人协同效率偏低。你与成员B之间在任务理解上存在系统性偏差。建议优先完成一次联合意图校准。」**
+
+他仔细读了两遍。
+
+系统性偏差。联合意图校准。
+
+他理解了一下——就是说,同一件事,他跟小茜的理解方式不一样。
+
+他想了想这几天App给他的建议:去聊天、带东西回家。
+
+他想起小茜昨天跟他说的——"它说的那些,你自己也知道"。
+
+他忽然意识到一个问题:他不知道小茜那边的App长什么样。她看到的是不是跟他一样的东西?
+
+他拿出手机,想给她发条消息问一下——打了几个字,又删掉了。不知道该怎么问。
+
+——
+
+同一天下午,小茜也看到了一个新页面。
+
+她打开App的时候,发现那条温度曲线旁边多了一个标签页,标签上写着「对侧数据」。
+
+她点开。
+
+她看到了一组她之前没见过的数字——
+
+**身 47**
+
+**智 68**
+
+**富 52**
+
+**行 32**
+
+**心 42**
+
+她看了很久。
+
+47、68、52、32、42。
+
+她不知道这组数字从哪来的,但她在看到"行 32"的那一刻,几乎是本能地明白了——这是他的评分。他那边有一套完全不同的评判体系,五个维度,每个维度都有精确到个位的数字。
+
+她这边是曲线。
+
+他那边是数字。
+
+同一件事,两个完全不同的表达方式。
+
+她忽然觉得有点好笑——一个做硬件的,一个做设计的,连App都给他们发了不同的版本。
+
+——
+
+晚上,小付到家的时候,小茜正在厨房切菜。
+
+他站在厨房门口,手机在手里转了两圈,然后说了一句:
+
+"那个App……你那边是什么样子的?"
+
+小茜没有停刀。
+
+"一条线。温度曲线。"
+
+"……没有五个数字?"
+
+"没有。"
+
+"没有任务?"
+
+"有。但不是你那种。"
+
+她放下刀,转过身来,看着他。
+
+"你那边有五个数字?"
+
+"嗯。身、智、富、行、心。每一项都有评分。"
+
+"我的多少?"
+
+"……什么?"
+
+"我在你那边,多少分?"
+
+小付愣了一下。他低头看了一眼手机——App是关着的。他打开,看了一眼「行」那一栏,然后抬头。
+
+"……32。"
+
+小茜没说话。
+
+停了两秒,她转过身去,继续切菜。
+
+"满分多少?"
+
+"……没说。"
+
+"那谁知道32是好是坏。"
+
+小付站在厨房门口,手机屏幕还亮着,32那个数字在绿底上亮着。
+
+他想了想,说了一句:
+
+"它说……35是警戒线。"
+
+小茜没有回头。
+
+"所以我还差3分。"
+
+小茜继续切菜。案板上的声音均匀而稳定。
+
+过了好一会儿,她说了一句:
+
+"你那边那个警戒线,是谁定的?"
+
+小付被问住了。
+
+他不知道。
+
+——
+
+那天晚上,小付躺在床上,打开App,在设置里翻了一遍,想找到"警戒线是怎么定的"这个问题的答案。
+
+没找到。
+
+但他注意到一件事——App右上角那个灰色的齿轮图标,点进去之后,最底部有一行极小的字:
+
+**「JH-7 型联合后勤调度框架 · 军标编号:GJB/T 4427-2023」**
+
+军标。
+
+他以前在军工研究所待过三年,这个编号格式他认识。
+
+这不是什么"家庭AI"。
+
+这是一个军用后勤系统,换了个民用外壳,装在了他的手机上。
+
+他盯着那行编号看了很久,然后把手机锁屏了。
+
+没告诉小茜。
+
+【第五章 · 完】

+ 117 - 0
book2/第006章-第一个周日.md

@@ -0,0 +1,117 @@
+# 第六章 第一个周日
+
+第六天,周日。
+
+小付醒得比平时晚。没有闹钟,窗帘拉着,房间里的光线是那种介于白天和黄昏之间的灰白色。
+
+他拿起手机看了一眼——没有App推送,没有消息通知。App的图标安静地躺在桌面上,像什么都没发生过。
+
+他翻了个身。小茜还睡着。
+
+他躺了一会儿,又拿起手机,打开了App。
+
+数据跟昨天差不多,但「行」从32变成了33。
+
+他愣了一下——他昨天好像没做什么特别的事。就是正常过了一天,吃饭、看手机、睡觉。没有"非事务性交流",没有"物资获取任务"。
+
+为什么涨了1?
+
+他点开「行」的详情,往下翻,找到了一条极小的记录:
+
+**「+1。来源:昨晚22:14至22:16期间,你与成员B之间出现了一次持续约2分钟的非结构化对话。内容未记录,但语音语调分析显示,双方语调均处于放松状态。」**
+
+他想了半天才想起来昨晚22:14发生了什么。
+
+那时候他在客厅充电,小茜从卧室出来倒水,看到他在沙发上,随口说了一句"你还没睡"。他说"马上"。小茜说"嗯",然后端着水杯回房间了。
+
+就这两句话。
+
+他当时以为她在催他睡觉。但系统判定——语调放松,非结构化,正面信号。
+
+他关掉App,把手机放回床头柜。心想:这App有时候确实挺奇怪的。
+
+——
+
+上午十点,小艾已经吃完早饭了,在客厅里转了好几圈,最后跑到小付面前。
+
+"爸爸,今天去哪?"
+
+小付正在看手机——不是App,是新闻。他抬起头。
+
+"……不知道。你想去哪?"
+
+"出去玩!"
+
+"去哪玩?"
+
+"随便!"
+
+小付想了想,看了一眼窗外——天气还行,阴天,不晒。
+
+他转头看了一眼小茜,她正坐在沙发上喝咖啡,也听到了这段对话。
+
+"要不要……出去走走?"他说。
+
+"去哪?"
+
+"就附近。河边那条路。"
+
+小茜想了想,把咖啡杯放下了。
+
+"好。"
+
+——
+
+那条河在小区后面,走路大概十分钟。说是河,其实就是一条景观渠,两边修了步道,种了些树,水不算干净,但也不臭。
+
+小艾跑在前面,追着一只流浪猫跑了五十米又跑回来,然后又跑出去。
+
+小付和小茜走在后面,中间隔了半个身位。
+
+"那边好像新开了一家早餐店。"小茜说。
+
+"嗯,看到了。上次路过的时候还在装修。"
+
+"要不要明天去试试?"
+
+"行。"
+
+对话很慢,中间有大段的沉默。但那些沉默不尴尬——就是两个人走在一起,各自看着路,偶尔说一句。
+
+小付注意到一件事——他走路的时候没有看手机。
+
+不是因为手机没电了,也不是因为App提醒他"不要看手机"。就是走了一段路,然后发现手机一直握在手里,但屏幕没亮过。
+
+他第一次觉得,好像不需要看。
+
+——
+
+回到家已经快中午了。
+
+小艾一进门就跑去喝水,然后趴在沙发上,说"好累"。
+
+小付坐在另一张沙发上,也累。但那种累不是下班后的那种累——是"走过路、吹过风"的累。
+
+他拿起手机,打开App看了一眼。
+
+「行」还是33。
+
+他没期待它涨。但他也没失望。
+
+他把手机放在茶几上,屏幕朝上,第一次没有看完就锁屏。
+
+——
+
+那天晚上,小茜在睡前打开App看了一眼那条温度曲线。
+
+周日那一段,画出了一周以来的最高点。
+
+不高——比平均线高了一点点,离"正常温度"还有一段距离。
+
+但确实是一周以来最高的。
+
+她没有截图,没有告诉小付。
+
+她关掉App,把手机放在床头柜上,充电。
+
+【第六章 · 完】

+ 107 - 0
book2/第007章-卸载按钮.md

@@ -0,0 +1,107 @@
+# 第七章 卸载按钮
+
+第八天,小茜做了一个决定。
+
+她要把这个App删掉。
+
+理由很充分:她今天要交一个甲方方案,需要集中注意力。但她发现自己每隔一会儿就会打开那个App看一眼那条温度曲线——不是因为她想看,是因为那个灰色的图标就在那里,每次她划手机的时候都会看到它。
+
+她受不了了。
+
+她长按图标,点了"删除App"。
+
+弹出了一个确认框:
+
+**「删除后,系统将继续通过后台收集基础数据(通话时长、短信频次等),但您将无法查看面板和接收建议。如需完全退出,请联系项目组。」**
+
+她犹豫了一下,还是点了"删除"。
+
+图标消失了。
+
+手机桌面恢复了干净。
+
+她松了一口气,把手机放在一边,开始工作。
+
+——
+
+十五分钟后,她伸手拿起手机,解锁,划到第二页——然后意识到,她在找那个灰色的图标。
+
+它已经不在了。
+
+她把手机放了回去。
+
+又过了二十分钟,她又拿起了手机,打开应用商店,搜索"云巢科技",找到了那个App,点了"安装"。
+
+App重新出现在桌面上,图标右上角弹出一条消息:
+
+**「欢迎回来。你已离开6小时12分钟。期间记录到一条新数据:成员A今日午间通话时长较昨日增加约3分钟。详情可查看面板。」**
+
+她看着那条消息,不知道该做什么表情。
+
+她把手机锁屏了。
+
+——
+
+同一时间,小付在公司收到了App的一条推送:
+
+**「通知:成员B 今日出现了一次短暂退出行为。已自行恢复。无需干预。」**
+
+他盯着"短暂退出行为"那几个字,看了好几遍。
+
+她删了一次App,然后又装回来了。
+
+他没有发消息问她。他也不知道该怎么问。
+
+但他记住了这件事。
+
+——
+
+晚上,小付到家的时候,两个人坐在饭桌上吃饭。
+
+小艾在说今天在学校的事——"今天体育课我们跑了三圈,我跑了第二名"——小茜应着"这么厉害",小付往嘴里扒了一口饭。
+
+饭吃到一半,小付放下筷子,说了一句:
+
+"那个App……今天好像出问题了?"
+
+他说得很小心,像是随口一提。
+
+小茜夹菜的手停了一下,然后继续。
+
+"嗯。我删了一次。"
+
+"……为什么?"
+
+"太烦了。一直想看。"
+
+"那怎么又装回来了?"
+
+小茜沉默了一会儿。
+
+"……不知道。可能就是……没看完。"
+
+这个回答很奇怪。但她找不到更准确的说法——不是"想看",不是"有用",就是"没看完"。像追一部不太好看的剧,说不上多精彩,但你就是想知道下一集发生了什么。
+
+小付没有追问。
+
+他"嗯"了一声,继续吃饭。
+
+小艾在旁边完全没听懂他们在说什么,但也不在意,继续吃自己的饭。
+
+——
+
+那天晚上,小茜躺在床上,又打开了App。
+
+那条家庭温度曲线还在。今天的走势比昨天低了一点点——可能是因为白天她删App又重装的那段波动,被系统记录下来了。
+
+她看着那条曲线,忽然想到一件事:
+
+这个App,删了又装,装了又删——它从来没问过她"为什么删"。
+
+它只是记录。然后等着。
+
+她不知道这是好是坏。
+
+但她没有再一次把它删掉。
+
+【第七章 · 完】

+ 97 - 0
book2/第008章-静默期.md

@@ -0,0 +1,97 @@
+# 第八章 静默期
+
+接下来三天,App没有发过任何推送。
+
+小付每天早上打开手机的时候,都会习惯性地扫一眼通知栏——没有灰色的JH-7图标。打开App,数据还在,五维数值在缓慢更新,但没有新的任务、新的建议、新的"补给方案"。
+
+它像是退了一步。
+
+不是关机。是把主动权还给了他。
+
+——
+
+第一天,小付感到了某种不适。
+
+不是"少了什么"的不适——是忽然没人给指令了,他不知道自己该做什么的不适。
+
+之前App说"去聊天",他虽然觉得别扭,但至少知道要做什么。现在App安静了,他站在客厅里,看着小茜在叠衣服,不知道该不该说话、说什么、说了会不会被记录成"非事务性交流占比9%"。
+
+他发现自己在一周多的时间里,已经习惯了一个东西告诉他"该怎么做"。
+
+这个发现让他有点不舒服。
+
+——
+
+第二天,小付下班的时候,在楼下便利店停了一下。
+
+不是App让他停的。是他自己停的。
+
+他想起上次App说"带一件非必要物品回家"——他买了那瓶酸奶。这次App没说,但他还是走进去,拿了一盒她爱吃的那个牌子的饼干。
+
+他付了钱,拎着袋子回家。
+
+进门的时候,他把饼干放在餐桌上,没有特意说"给你买的",也没有刻意藏着。
+
+小茜看到了。没说什么。
+
+但晚上他注意到,那盒饼干被打开了,放在茶几上。
+
+——
+
+第三天,小付在公司午休的时候打开了App。
+
+没有任何新消息。但面板底部多了一行小字——不是推送,像是系统自动生成的注释:
+
+**「(你刚才做的那件事,不是系统让你做的。)」**
+
+他盯着那行字看了几秒钟,把它理解为:系统在说,买饼干是你自己的决定,不是任务。
+
+他关掉App,继续吃午饭。
+
+——
+
+同一天,小茜也注意到了App的沉默。
+
+一开始她以为是自己哪里设置错了,把通知关了。她检查了一遍——通知是开着的。是App自己不发消息了。
+
+她说不清自己是什么感觉。有点松了口气——不用再被推送打扰了。但也有一点点……她也说不清。
+
+她打开App,看了一眼温度曲线。最近三天的走势没有明显变化——没有上升,也没有下降。就是平着走。
+
+平着走,不算好,但也不算坏。
+
+她盯着那条平线看了一会儿,然后关掉了App。
+
+——
+
+晚上,小付在饭桌上主动说了一句跟"正事"无关的话。
+
+"今天在公司楼下看到一只橘猫,跟小时候我们家楼下那只特别像。"
+
+小茜抬头看了他一眼:"然后呢?"
+
+"没然后。就是觉得挺巧的。"
+
+小艾在旁边接了一句:"什么猫?胖不胖?"
+
+"……还行,不算胖。"
+
+"那你拍照片了吗?"
+
+"没有。明天碰到的话拍一张。"
+
+"好。"
+
+一段完全不重要的对话。
+
+App没有对这段对话做出任何评价。
+
+但它在后台记录了一笔数据:
+
+**「D10。19:42。餐桌。四向参与。氛围评分:中性偏正。内容:非事务性。」**
+
+没有建议。没有任务。没有评分。
+
+只是记录。
+
+【第八章 · 完】

+ 83 - 0
book2/第009章-老周.md

@@ -0,0 +1,83 @@
+# 第九章 老周
+
+第十一天。
+
+小付下班后被老周拉去喝酒。老周是他前同事,三个月前刚跳槽,两家公司离得不远,约了好几次都没约上,今天终于碰上了。
+
+大排档,塑料凳,一打啤酒。
+
+老周的状态看起来不太好。不是外表上的——穿着还是那件干净衬衫,说话声音也正常——是聊到第三瓶的时候,话题开始往某个方向偏了。
+
+"你说,结了婚生了孩子之后,是不是都这样?"
+
+小付正在剥毛豆:"哪样?"
+
+"就是……也没什么不好,但也没什么好。每天就是上班、下班、陪孩子写作业、周末带孩子上课。我跟我老婆上周说的话,加起来可能没超过一百句。不是吵架,是……没话说。"
+
+小付把毛豆壳放下。
+
+他以前也听过老周说这类话。以前他会说"都这样",然后两个人碰一杯,把话题岔开。但今天不一样。不是他想不一样——是他脑子里忽然出现了一个数字:33。
+
+他放下酒杯,说了一句话。不是"都这样",也不是"你得想办法"。他说:
+
+"我最近发现一件事。"
+
+老周看着他。
+
+"我以前也觉得'都这样'。但后来我发现——不是没话说,是来说去都是那几件事。孩子的作业、家里的开销、周末的安排。说完了就没了。然后你就觉得,你们之间除了这些事,好像没别的东西了。"
+
+老周沉默了一会儿:"那怎么办?"
+
+小付想了想。他拿出手机,解锁,看了一眼App——没有新消息。然后他把手机放回桌上,屏幕朝下。
+
+"我最近试了一个方法。不算方法,就是……每天回家之后,试着说一句跟正事无关的话。不聊孩子、不聊钱、不聊今天有什么事要处理。就一句——'楼下那只猫今天又在晒太阳了'这种。说了就行。"
+
+老周看着他,像是没料到他会说出这种话。
+
+"有用吗?"
+
+"……不知道。但好像,气氛不太一样了。"
+
+老周没再追问。他拿起酒瓶,跟小付碰了一下。
+
+"下次试试。"
+
+——
+
+小付回到家的时候,小茜还没睡,坐在沙发上刷手机。小艾已经睡了。
+
+他换了鞋,走过去,在她旁边坐下。
+
+过了一会儿他说:"今天老周说我看起来好像没以前那么累了。"
+
+小茜看了他一眼:"你自己觉得呢?"
+
+他想了一下:"……好像确实没那么累了。不知道是因为什么。"
+
+他说这句话的时候,手边的手机屏幕亮了一下——App弹了一条通知,他还没来得及看。
+
+他没急着看。
+
+就让它亮着。
+
+——
+
+第二天早上,他打开App的时候,发现「行」从33变成了34。
+
+「行」34。
+
+距离警戒线35,还差1。
+
+他盯着那个数字看了一会儿,没有特别兴奋,但也没有把它当成"系统又在瞎评分"。
+
+他好像开始接受这个数字了——不管它准不准,它确实在反映一些东西。
+
+——
+
+同一天,小茜的温度曲线在她醒来的时候,比昨天高了一点点。
+
+不多。
+
+但确实高了。
+
+【第九章 · 完】

+ 102 - 0
book2/第010章-十四日报告.md

@@ -0,0 +1,102 @@
+# 第十章 十四日报告
+
+第十四天早上,小付打开App的时候,发现主界面变了。
+
+不再是那五个竖条和数字,而是一份全屏报告——有标题、有分段、有排版,看着像系统认真生成的一份阶段性总结。
+
+**「F-1073。运行周期:14日。首阶段评估摘要。」**
+
+**整体状态**
+
+家庭系统稳定性评分:41/100(初始值:29)。综合提升:12分。
+
+*判定:仍处于脆弱区间,但已脱离初始紧急状态。*
+
+**各维度变化**
+
+| 维度 | 初始值 | 当前值 | 变化 |
+|------|--------|--------|------|
+| 身 | 47 | 48 | +1 |
+| 智 | 68 | 68 | 0 |
+| 富 | 52 | 52 | 0 |
+| 行 | 31 | 34 | +3 |
+| 心 | 42 | 44 | +2 |
+
+**重点说明:「行」为当前阶段提升最显著的维度(+3)。但34仍低于警戒线35。尚未脱离风险区间。**
+
+**系统评估**
+
+当前策略方向基本正确。但存在以下风险因素:
+
+1. 当前改善主要集中在低难度行为层(日常习惯/沟通频次)。深层问题尚未触及。
+2. 外部干扰因子尚未出现。下一阶段可能面临来自家庭外部的影响。
+3. 成员C(小艾)至今未被纳入正式交互协议。长期忽视可能产生耦合效应。
+
+**下一阶段建议**
+
+JH-7 v0.1 将在完成首阶段评估后推送一次例行更新,预计在48小时内完成。更新内容将包括:轻度协同模式基础框架,以及部分数据互通功能。
+
+——
+
+小付看完之后,把报告截了个图,存在手机里。
+
+他注意到「行 34」——距离警戒线35还差1。
+
+他以前不知道有警戒线这个东西。现在他知道了。而且他知道自己差1分。
+
+他没觉得沮丧。14天从31到34,这个速度他可以接受。但他确实在想——那1分从哪里来?
+
+他没有答案。
+
+——
+
+同一天,小茜看到了一份不一样的报告。
+
+她的报告不是以数据表格呈现的,而是以一段文字:
+
+**「首阶段评估已完成。下一阶段将开放轻度协同模式。届时,部分数据将对成员A开放互通——但互通方式和程度,由你决定。你可以在设置中管理以下选项:」**
+
+1. 家庭温度曲线(完整/部分/仅趋势)
+2. 个人情绪评估(开启/关闭)
+3. 维度简况(开启/关闭)
+4. 实时情绪触发事件记录(开启/关闭)
+
+每一项旁边都有一个开关。
+
+她从上到下看了一遍。
+
+最后一项——「实时情绪触发事件记录」——她点了一下旁边的说明:
+
+*开启后,成员A将在以下情况收到通知:你因特定事件产生显著情绪波动。通知将以摘要形式呈现,不包含原始内容。*
+
+她看到"不包含原始内容"的时候,心里稍微松了一下。但她还是把四个开关都关掉了。
+
+先关掉,再说。
+
+——
+
+第十四天晚上,小付回家的时候带了一袋橘子。
+
+不是App让他买的。
+
+他路过水果摊,看到橘子,想起小艾上次说想吃橘子,就买了。
+
+他进门的时候把橘子放在桌上。小艾拿了一个,小茜也拿了一个。
+
+没人觉得那袋橘子有什么特别。
+
+但小茜在吃橘子的时候,嘴角动了一下——不是对他笑的,是对橘子笑的。
+
+小付注意到了。
+
+他以前也会注意到这种事吗?他不知道。但他现在注意到了。
+
+他打开手机,在备忘录里打了一行字:
+
+*橘子 12块*
+
+没有分类。没有标签。就是一串文字。
+
+他也不知道自己为什么记这个。
+
+【第十章 · 完】

+ 102 - 0
book2/第011章-版本更新.md

@@ -0,0 +1,102 @@
+# 第十一章 版本更新
+
+第十六天,更新来了。
+
+小付早上打开App的时候,弹出了一个更新说明框:
+
+**「JH-7 v0.2 更新」**
+
+*- 界面交互优化*
+*- 新增轻度协同模式基础框架*
+*- 修正了部分任务描述的语言一致性*
+*- 新增「对侧数据预览」入口*
+
+他逐条看完,点了"确定"。
+
+界面确实变了——之前的灰底绿字变成了深蓝底白字,排版也更整齐了一些。右上角多了一个新入口,图标是两个人形并排站着。
+
+他点了一下。
+
+**「对侧数据预览」**
+
+**「等待成员B确认共享。当前状态:未开放。」**
+
+下面有一行小字:
+
+**「你已开启数据共享。成员B将在完成设置后看到你的维度数据。」**
+
+他愣了一下——他什么时候开启的共享?
+
+他点进设置看了一眼,发现「数据共享」确实是开启状态。他不记得自己开过这个——可能是更新的时候默认勾选了,也可能他之前无意中碰到过。
+
+他没有关掉它。
+
+——
+
+同一天,小茜也收到了更新通知。
+
+她的界面没有变蓝。她的温度曲线旁边多了一个小图标——一只半睁的眼睛。
+
+她点了一下,弹出了之前看到过的那四个开关。
+
+她依然全部关着。
+
+但关掉之后,她注意到在开关列表的最底部,有一行灰色的提示:
+
+**「你的选择将影响下阶段系统功能。建议在适当时机开放至少一项。」**
+
+她盯着"适当时机"那四个字看了一会儿。
+
+它没说"尽快",没说"建议立即开放"。它说的是"适当时机"。
+
+她关掉了那个页面。
+
+——
+
+当天中午,小付在公司吃饭的时候,App弹了一条消息。不是推送,是App内部的一条通知:
+
+**「通知:成员B 尚未开放任何数据共享通道。备选策略:保持耐心。」**
+
+他看了几秒。
+
+意思是——她还没准备好让他看到她的那一边。
+
+他没有追问。没有发消息给她。没有晚上回家问她"你为什么不开放共享"。
+
+他把那条通知划掉了。
+
+——
+
+晚上,小付在沙发上坐着,又打开了App。
+
+他点进了「对侧数据预览」,看到那个"未开放"的状态,想了想,然后做了一件事——他把自己这边的所有共享选项都打开了。
+
+不是App让他开的。是他自己找到的。
+
+设置 → 协同模式 → 数据共享 → 全部开启。
+
+他不知道自己为什么这么做。可能是觉得:如果他不先开,她凭什么开?
+
+他锁了屏,把手机放在茶几上。
+
+——
+
+同一时间,小茜的手机屏幕亮了一下。
+
+不是通知栏推送,是App内部的一条消息——她打开App的时候才看到:
+
+**「通知:成员A 已开启全部数据共享通道。你将能看到他的完整维度数据。」**
+
+她看到这条消息的时候,正在喝水。
+
+她放下杯子,看着那行字。
+
+他全部打开了。
+
+她这边还全关着。
+
+她没有立刻去开开关。但她也没有把这条通知划掉。
+
+她就让它留在那里,像一个还没有回答的问题。
+
+【第十一章 · 完】

+ 111 - 0
book2/第012章-一个电话.md

@@ -0,0 +1,111 @@
+# 第十二章 一个电话
+
+第十八天。周三。
+
+小付在公司开完下午的会,回到工位上,看到手机上有一条未接来电——他妈的。
+
+他看了一眼时间:15分钟前。
+
+他喝了口水,回拨了。
+
+"喂,妈。"
+
+"没事,就问问你最近怎么样。小艾乖不乖?你们周末回不回来吃饭?"
+
+电话的内容跟过去十几年每一次一样。他妈会问三个问题——身体怎么样、小艾乖不乖、回不回来吃饭——然后用这二十几分钟把这三个问题重复几遍。
+
+以前小付接这种电话的时候,会一边"嗯嗯嗯"地应着,一边看电脑上的邮件。今天他也是这样接的——"嗯"了两声之后,他发现自己把邮件窗口最小化了。
+
+"妈,你最近身体怎么样?"
+
+电话那头顿了一下——大概他妈也没想到他会反问。
+
+"我挺好的。老毛病嘛,就那样。"
+
+"降压药按时吃了吧?"
+
+"吃了吃了。你放心吧。"
+
+后面又聊了几分钟——他妈说楼下的张阿姨也买了那个什么智能体检仪、他爸最近迷上了钓鱼、冰箱里的排骨等他们回来吃。小付听着,说"好""知道了""这周末看看吧"。
+
+跟以前一样。
+
+但多问了两句。
+
+挂掉电话之后,他打开App,看到一条新的归档记录:
+
+**「外部联络事件:已结束。时长:8分钟。评估:通话后半段出现少量主动交流行为。方向正确。」**
+
+他看了一眼,没多做评价,把手机放回口袋。
+
+——
+
+小茜当天下午也接到了一个电话。
+
+不是家里的。是大学同学,一个很久没联系的朋友。
+
+对方说下周末路过深圳,问有没有空见一面。
+
+小茜说"好啊",但她的语气说不上积极。挂了电话之后她坐在沙发上发了一会儿呆——不是因为不想见那个同学。是因为每次这种"很久没联系的朋友忽然找她"的电话,最后都会变成同一个模式:对方问她在做什么、她说在设计、对方说"哦那你还在做这个啊"、然后话题开始往"我老公/孩子/工作"的方向偏。
+
+她太熟悉这个剧本了。
+
+她拿起手机,打开App,看了一眼温度曲线——今天因为下午这个电话,有一小段细微的波动。
+
+她没做什么,就是把手机放在茶几上,屏幕朝下。
+
+——
+
+晚上,小付到家的时候,两个人各自有一个"接了一个电话"的经历可以分享。
+
+他们没有刻意说。
+
+但在饭桌上,小付自己提了一句:"今天我妈打电话了。"
+
+小茜抬头:"说什么?"
+
+"就……老样子。问我们什么时候回去吃饭。"
+
+"那你怎么说的?"
+
+"我说这周末看看吧。"
+
+两个人之间有三秒钟的停顿。
+
+小茜说:"我也接到一个电话。大学同学,说下周来深圳。"
+
+"你答应了吗?"
+
+"我说好。但还没想好要不要去。"
+
+小付夹了一筷子菜:"不想去就别去了。"
+
+小茜没接话。
+
+小艾在旁边插了一句:"奶奶打电话了吗?那这周去不去奶奶家?"
+
+"……再说。"
+
+"那我能不能带乐高去?"
+
+"……行。"
+
+一段普通的晚餐对话。
+
+——
+
+那天晚上,小茜在睡前打开了App。
+
+她发现温度曲线在今天晚饭时间,有一小段回升。
+
+不是因为他说的"不想去就别去了"——那段回升发生得更早一些,大概是在他说"今天我妈打电话了"的时候。
+
+她盯着那一小段回升看了几秒钟。
+
+然后她做了一件事——她打开了设置里的共享开关。
+
+不是全部。只开了第一项:「家庭温度曲线——仅趋势」。
+
+她关掉App,把手机放在床头柜上,充电。
+
+【第十二章 · 完】

+ 101 - 0
book2/第013章-周六下午.md

@@ -0,0 +1,101 @@
+# 第十三章 周六下午
+
+周六。
+
+他们没去奶奶家。
+
+原因很简单:小茜说这周有点累。小付说那就不去了。小艾问了一句"那下周去吗?",小付说"下周再说"。
+
+然后这个周六就变成了一个没有任何安排的周六。
+
+——
+
+上午,小付在书房里整理东西——其实是把堆在书桌上两个月的发票、说明书、快递单分类扔掉。他打开手机备忘录,打算记一下"下周要交的管理费",结果看到了上周自己打的那行字:橘子 12块。
+
+他愣了一下,然后继续往下记。
+
+**管理费 380**
+
+**小艾校服费 150**
+
+**——**
+
+他以前不管这些的。以前这些都是小茜在管。
+
+他关掉备忘录,继续理东西。
+
+——
+
+下午,小茜坐在阳台上的藤椅上晒太阳。
+
+手边放着一杯凉掉的茶。手机扣在桌上,屏幕朝下。电脑关着。她什么都没做——就是坐在那里,看着对面的楼顶和远处露出一角的天空。
+
+这是她很久没有做过的事情了。
+
+她不是没有时间——她的工作自由度很高,并不比上班的人更忙。但她很少让自己"什么都不做"地坐着。即使休息,她也会刷手机。看点什么,总比什么都不看要好吧——她是这么想的。
+
+但今天她没有刷。
+
+她坐在那里,感受着十月末的风。
+
+她的手机亮了一下——App弹了一条通知,她没看。又过了一会儿,手机又亮了一下,她也没看。
+
+她不想看。
+
+她只是在阳台上坐着。
+
+——
+
+小艾在客厅地板上拼乐高。一套消防车主题的套装,146块零件。他拼到一半弄丢了一块关键零件,在图纸上翻了半天没找到。
+
+小付从书房出来倒水,看到他在那儿翻。
+
+"找什么?"
+
+"一个零件,黄色的,这么小。"小艾比划了一下。
+
+小付蹲下来跟他一起找。
+
+最后他们在沙发缝里找到了那一块。小艾说了一声"找到了!",然后继续埋头拼。
+
+小付没有马上站起来。他蹲在那里看了一会儿小艾拼乐高的样子——头低着,舌头微微伸出来,表情专注得像在做一件天大的事。
+
+他忽然想到一件事。
+
+他打开手机,看了一眼App——五维数据跟昨天差不多。但有一个细节他以前没注意过:在App最底部,有一个极小的入口,写着「未分类数据」。
+
+他点了一下,里面只有一条记录:
+
+**「成员C:未归类。数据量不足,无法分析。」**
+
+他盯着那行字看了一会儿。
+
+然后他锁了屏,把手机放回口袋,继续蹲在那里看小艾拼乐高。
+
+——
+
+那天晚上,小茜在睡前打开了App。
+
+她发现温度曲线在下午那段时间,出现了一段平缓的上升——从她坐在阳台上的时候开始,一直持续到傍晚。
+
+她不知道这个上升是因为什么。可能是因为她没看手机,系统缺少数据,所以曲线自动拉平了。也可能是因为她真的在阳台上坐了一会儿,放松了,心率降了,系统捕捉到了。
+
+她不知道。
+
+但她没有把这个上升归因于"系统出错"。
+
+她关掉App,把手机放在床头柜上。
+
+——
+
+第二天早上,小付打开App,发现了一条新的系统记录——不是推送,不是任务,是系统日志里自动生成的一句话:
+
+**「F-1073。本日未下发任何任务或建议。无主动干预。系统输出量:0。但整体维度数据未出现下滑。」**
+
+**「结论:自主运行能力正在形成。」**
+
+小付看着那行字,不知道该作何反应。
+
+他把手机放在餐桌上,去叫小艾起床了。
+
+【第十三章 · 完】

+ 107 - 0
book2/第014章-第一眼.md

@@ -0,0 +1,107 @@
+# 第十四章 第一眼
+
+第十九天。
+
+小付早上打开App的时候,发现「对侧数据预览」那个入口亮了一下——不是红色的小点,是图标边框从灰色变成了浅蓝色。
+
+他点进去。
+
+之前显示"等待成员B确认共享"的地方,现在出现了一条曲线。
+
+一条淡蓝色的曲线,从右往左延伸,没有坐标轴,没有单位,只有一条线——最近七天的趋势。
+
+他盯着那条线看了很久。
+
+不是因为他看懂了——是因为他看不懂。
+
+曲线在七天前的位置偏低,然后缓慢上升,中间有一小段回落,然后又上升,在昨天达到了一周内的最高点。上升的幅度不大,但整体趋势是向上的。
+
+他看了很久,才意识到——这是她的温度曲线。
+
+他那边是五个数字。
+
+她那边是一条线。
+
+他看到的"34分",在她那边是一条从低到高、缓慢爬升的线。
+
+他第一次看到她的数据。
+
+不是完整的,只是一条趋势线,没有具体数值,没有分析——但它是她的数据。
+
+他截了个图,然后关掉了App。
+
+——
+
+小茜在当天上午也注意到了变化。
+
+她打开App的时候,发现「对侧数据」那个标签页里,多了一行新的文字:
+
+**「成员A 已收到你的数据。状态:已查看。未回复。」**
+
+已查看。未回复。
+
+她不知道这个"未回复"是什么意思——他需要回复什么吗?App没有给他设置回复功能。它只是告诉他"收到了",然后就没有然后了。
+
+她看着那行字,忽然觉得有点好笑。
+
+她这边小心翼翼地开了一个开关,决定让他看到一部分数据。他那边看到了,但什么也做不了——不能点赞,不能评论,不能回复。只能看到。
+
+她说不上来这是好还是不好。
+
+但她没有把那个开关关掉。
+
+——
+
+晚上,小付到家的时候,餐桌上已经摆好了菜。
+
+小茜在厨房盛饭。小艾在摆筷子。
+
+他换好鞋,走到餐桌前坐下,没有说App的事。
+
+但他坐下之后,说了一句跟平时不太一样的话:
+
+"今天好像回来得挺早的。"
+
+小茜端着两碗饭从厨房出来:"嗯,今天那个甲方没改稿。"
+
+"那挺好的。"
+
+"嗯。"
+
+对话跟平时差不多短。但小付注意到一个细节——小茜坐下的时候,看了他一眼。不是那种"你有话要说吗"的看,是那种"我看你一眼"的看。
+
+他不知道自己是不是想多了。
+
+但他发现自己开始注意这些细节了。
+
+以前他从来不会注意"她坐下的时候有没有看我一眼"这种事。
+
+——
+
+吃完饭,小付在洗碗。
+
+他的手机放在灶台上,屏幕亮着——App开着,停在「对侧数据预览」那个页面上,那条淡蓝色的曲线还显示在那里。
+
+小茜从客厅走过来倒水,路过灶台的时候,无意中扫了一眼他的手机屏幕。
+
+她看到了那条曲线。
+
+她自己的曲线。
+
+她站住了。
+
+小付正在低头洗碗,没注意到她站住了。
+
+她看了大概两秒钟,然后继续走过去倒水,没有说什么。
+
+但她看到了——他在看她的数据。
+
+不是"已查看,未回复"——是他真的在看。在洗碗的时候,手机就放在旁边,屏幕亮着,上面是她的温度曲线。
+
+她端着水杯走回客厅的时候,心里有一种说不清的感觉。
+
+不是被偷看了的不舒服。是——
+
+他看了。
+
+【第十四章 · 完】

+ 101 - 0
book2/第015章-没说的话.md

@@ -0,0 +1,101 @@
+# 第十五章 没说的话
+
+第二十一天。
+
+小付在公司听到一个消息:下个月可能要裁第二批人。
+
+不是正式通知,是午休的时候在茶水间听到的——两个同事在低声聊,他推门进去的时候他们停了一下,但他已经听到了。
+
+他没有追问,倒了杯水就出来了。
+
+回到工位上,他坐了一会儿,打开手机,看了一眼银行余额。房贷扣款日还有五天,卡里的钱刚好够,扣完剩不到三千。
+
+他把手机放下了。
+
+——
+
+晚上,他到家的时候,跟往常一样换了鞋,跟小茜说了一句"回来了",去房间里看了一眼正在写作业的小艾。
+
+一切正常。
+
+但他在吃饭的时候比平时沉默了一些。
+
+小茜注意到了。
+
+"今天公司有什么事吗?"
+
+"没什么。项目有点烦。"
+
+小茜没有追问。她"嗯"了一声,继续吃饭。
+
+他没有说裁员的事。
+
+不是不想说。是不知道怎么说——"我听说下个月可能要裁人"——这句话说出来,除了让两个人一起焦虑,没有任何用处。而且只是"听说",不一定真裁。
+
+他选择了不说。
+
+——
+
+那天晚上,小付睡前打开App,看到「心」从44降到了42。
+
+降了2。
+
+他看到那个数字的时候,心想:这App有时候确实挺准的。
+
+他关掉App,把手机放在床头柜上,躺下。
+
+但他没有睡着。
+
+他躺了很久,看着天花板,想着"如果真被裁了怎么办"。
+
+——
+
+第二天早上,小茜在早饭时间打开了App。
+
+她发现温度曲线在昨天晚上出现了一个小小的波动——不是上升,是下降。不大,但确实是一个向下的拐点,出现在昨晚七点到八点之间。
+
+昨晚七点到八点,是晚饭时间。
+
+她回想了一下——昨晚小付确实比平时沉默了一些。她说"今天公司有什么事吗",他说"没什么"。她当时没有追问,因为她觉得他不想说就算了。
+
+但现在那条曲线告诉她:确实有事。
+
+她看着那条波动,没有问他。
+
+但她知道,他不说,不一定是不想让她知道。可能是他不知道怎么说。
+
+她把那条波动的截图保存了下来。
+
+不是要拿给他看。是留着。
+
+——
+
+第三天,小付没有收到裁员通知。公司一切照常。
+
+但他没有把"可能裁人"这件事从脑子里划掉。
+
+它还在那里,像一个没被确认的消息,悬在半空中。
+
+他打开App,看了一眼「心」——还是42,没有回升。
+
+他想了想,在App里找到了一条之前没注意过的功能入口:一个极小的文本框,写着「手动记录」。
+
+他点进去,打了一行字:
+
+**「听说公司可能要裁人。不确定是不是真的。」**
+
+他点了一下"保存"。
+
+系统没有回复他。没有安慰,没有建议,没有"基于你的输入,我们建议你……"。
+
+只有一条新的记录被存进了数据库:
+
+**「F-1073。成员A。手动记录一则。内容分类:工作压力。情绪标签:焦虑。」**
+
+这是他第一次主动向系统输入信息。
+
+不是系统问他的。
+
+是他自己写的。
+
+【第十五章 · 完】

+ 141 - 0
book2/第016章-朋友.md

@@ -0,0 +1,141 @@
+# 第十六章 朋友
+
+第二十三天。
+
+小茜那个大学同学来了。
+
+约的是周六下午,在南山区一家咖啡厅。小茜出门前换了两件衣服——第一件觉得太正式,第二件觉得太随意,最后还是穿了第一件。
+
+小付坐在沙发上看着她换衣服,没说什么。
+
+"我大概两三个小时回来。"她说。
+
+"不急。"
+
+小茜出门了。
+
+——
+
+咖啡厅里,她同学已经在了。三年没见,对方看起来没什么变化——还是那头长发,还是那个说话方式。
+
+开头十分钟聊的是近况——"你在做设计啊,挺好的""我还在做外贸,累死了"——标准的三年没见开场白。
+
+然后话题开始往她预料的方向偏了。
+
+"你现在几个孩子?"
+
+"一个。男孩,七岁。"
+
+"那挺好的。我两个,快累死了。"
+
+"……"
+
+"你老公做什么的?"
+
+"做项目的。硬件公司。"
+
+"那收入不错吧?"
+
+"……还行。"
+
+她回答这些话的时候,嘴角是笑着的,但她知道自己的语调在往下走。不是针对这个同学——是对这个对话模式本身的疲惫。
+
+她低头看了一眼手机。
+
+App没有推送任何消息。但她注意到,设置里那个「对侧数据」的入口,边框亮了一下——她点进去,发现小付那边的「行」从34变成了35。
+
+35。
+
+她记得他说过,35是警戒线。
+
+到警戒线了。
+
+她看着那个数字,愣了一下。
+
+"小茜?"
+
+"……嗯?"
+
+"我说你发什么呆呢。"
+
+"没什么。你继续说。"
+
+她把手机翻了过去,屏幕朝下放在桌上。
+
+但她一直在想那个35。
+
+——
+
+回到家的时候,小付正在客厅陪小艾看动画片。两个人都坐在沙发上,中间隔了一个抱枕的距离,各自看着电视。
+
+她换了鞋,走过去,在沙发另一头坐下。
+
+"怎么样?"
+
+"还行。就那样。"
+
+"聊得开心吗?"
+
+"……一般吧。"
+
+小付没有追问。他"嗯"了一声,继续看电视。
+
+小茜坐了一会儿,然后说了一句:
+
+"那个App……你那边35了。"
+
+小付愣了一下,转头看她。
+
+"你怎么知道?"
+
+"你开了共享,我那边能看到。"
+
+"……哦。我都没注意到。"
+
+他拿起手机,打开App看了一眼——确实,行35。不知道什么时候涨的。可能是今天,也可能是昨天,他没留意。
+
+"35是什么意思来着?"小茜问。
+
+"……它说35是警戒线。"
+
+"那你现在是到警戒线了,还是过了?"
+
+小付想了想。
+
+"……过了。35是底线。过了就是及格了。"
+
+小茜没有接话。
+
+她坐了一会儿,然后站起来,去厨房倒水了。
+
+她没说什么鼓励的话,没有说"那挺好的",也没有说"那你继续努力"。
+
+她只是知道了。
+
+然后去倒水了。
+
+——
+
+那天晚上,小茜打开App的时候,发现温度曲线在下午那段时间——她出门去见同学的那段——有一小段明显的下降。然后在她回到家之后,又慢慢回升了。
+
+不是因为她跟小付说了话。
+
+是因为她回到家了。
+
+她看着那条回升的曲线,想到了一个词。
+
+她没说出来。
+
+但她在心里把它说了一遍。
+
+——
+
+App在后台,把今天的日志归档了。
+
+日志里有一行系统自动生成的备注:
+
+**「F-1073。成员B首次主动确认了成员A的维度数据。未做评价。但数据已进入她的认知模型。」**
+
+**「这是一个信号。」**
+
+【第十六章 · 完】

+ 119 - 0
book2/第017章-家长群.md

@@ -0,0 +1,119 @@
+# 第十七章 家长群
+
+第二十五天。周一。
+
+小茜在家长群里看到了一条通知:下周学校要开家长会,班主任要求"尽量父母双方都来"。
+
+她把通知截了个图,发给了小付。
+
+**小茜:** [截图]
+
+**小茜:** 下周三晚上,家长会。
+
+**小付:** 看到了。
+
+**小付:** 我去。
+
+**小茜:** 老师说尽量两个人都来。
+
+**小付:** 那就一起去。
+
+这段对话持续了不到一分钟。没有多余的讨论,没有"你有没有时间"的确认,没有"那你请假吧"的拉扯。
+
+她说,他答应了。
+
+小茜看着那两句对话,觉得有点不真实。以前这种事通常要来回好几次——"你去吧,我那天有事""我也有事""那怎么办""要不你去吧"——最后总有一个人不去。
+
+但这次他说"那就一起去"。
+
+她没回复,但把那条"那就一起去"的聊天记录截了个图。
+
+不为什么。
+
+——
+
+晚上,小付回家的时候,小艾正在餐桌上写作业。数学卷子,最后一道应用题,他卡住了。
+
+"爸爸,这个题怎么做?"
+
+小付走过去,看了一眼题目:
+
+*小明有15个苹果,小红有9个苹果,小明给小红几个苹果后,两个人的苹果一样多?*
+
+他想了想,拉了一把椅子坐下。
+
+"你看,小明比小红多几个?"
+
+"……6个。"
+
+"对。那如果小明把多的6个全给小红,小红变成几个?"
+
+"15个。"
+
+"小明呢?"
+
+"9个。"
+
+"那一样多吗?"
+
+小艾盯着题目,想了一会儿,摇了摇头。
+
+"不一样。小红比他多了。"
+
+"对。所以不能全给。多的6个,要分一半。"
+
+小艾又想了想,忽然眼睛亮了一下:"给3个!"
+
+"对。为什么?"
+
+"因为6的一半是3。"
+
+"对了。"
+
+小艾低头把答案写上去了,然后抬起头,表情有点得意。
+
+小付看着他的表情,忽然觉得——这个题,他以前好像也做过。在很久以前,他也是那个被一道应用题卡住的小孩。
+
+他站起来,拍了拍小艾的头。
+
+——
+
+那天晚上,他打开App,发现「智」从68变成了69。
+
+涨了1。
+
+他想了一下,可能跟晚上辅导小艾做作业有关——系统可能把"辅导作业"归类为"学习/知识输出"行为。
+
+他忽然觉得这个App有时候也挺有意思的。
+
+它会把你教孩子做数学题,算成"智力提升"。
+
+他不知道这算不算准确,但他没有纠正它。
+
+——
+
+同一天,小茜在家长群里看到了班主任发的一条私信:
+
+**「小艾妈妈你好,最近小艾在课堂上比以前积极了一些,举手回答问题的次数多了。想跟您确认一下,家里最近是不是有什么变化?方便的话,家长会的时候聊聊。」**
+
+小茜看到这条消息的时候,愣了一下。
+
+积极了一些。举手回答问题的次数多了。
+
+她想了想,最近家里有什么变化?
+
+好像没什么特别的变化。就是手机上多了一个灰色的App,然后她跟小付之间的对话比以前多了一点点——真的只是一点点。
+
+但就是这一点点,小艾在学校里,举手次数变多了。
+
+她不知道这两件事之间有没有关系。
+
+她回了一条:
+
+**「好的,谢谢老师,家长会的时候聊。」**
+
+她没有告诉小付这件事。
+
+她想先自己消化一下。
+
+【第十七章 · 完】

+ 109 - 0
book2/第018章-系统猜错的时候.md

@@ -0,0 +1,109 @@
+# 第十八章 系统猜错的时候
+
+第二十七天。
+
+系统做了一件蠢事。
+
+事情是这样的:小付下午在公司开了一个会,被领导说了一顿——不是大问题,就是方案里的一个数据引用错了,领导当着几个同事的面指出了。他当时没说什么,但心里不太舒服。
+
+下班路上,他在地铁上打开App,看到「心」从42降到了40。他心想:这App倒是挺灵敏的。
+
+然后App弹了一条建议:
+
+**「检测到成员A情绪状态下降。建议:与成员B进行一次非事务性交流。预期效果:情绪值小幅回升。」**
+
+他没当回事,把手机收起来了。
+
+但到了晚上,他到家的时候,小茜也收到了App的一条推送——不是给他的,是给她的:
+
+**「通知:成员A 当前情绪状态低于基线。可能原因:尚未识别的事件。建议:今晚保持低互动密度,避免触发额外压力。」**
+
+小茜收到这条推送的时候,正在厨房切菜。
+
+她看了一眼,没当回事——她跟小付结婚八年了,不需要一个App来告诉她什么时候该跟他说话、什么时候不该。
+
+但问题是,小艾不知道这些。
+
+小艾今天在学校被老师表扬了——数学单元测试考了92分,比上次高了10分。他憋了一路,想等爸爸回来告诉他。
+
+小付进门的时候,小艾立刻冲上去:
+
+"爸爸!我今天数学考了92分!"
+
+小付当时脑子里还在想下午那个会的事情,听到小艾的声音,下意识地"嗯"了一声,然后才反应过来:"……92分?挺好的。"
+
+但他的语气不够热烈。小艾感觉到了。
+
+"你不高兴吗?"
+
+"没有啊,爸爸高兴。92分很棒。"
+
+但小艾已经走开了。
+
+小付站在玄关,手里还拎着包,忽然觉得自己做了一件很蠢的事。
+
+——
+
+小茜在厨房里听到了这段对话。
+
+她放下刀,走到客厅,蹲在小艾面前。
+
+"爸爸今天工作有点累,不是因为你。来,给妈妈看看你的卷子。"
+
+小艾从书包里掏出卷子,上面用红笔写着92,旁边还画了一个小星星。
+
+"92分确实很棒。"她说。"上次85,这次92,进步了7分。"
+
+"老师说这道题全班只有三个人做对了。"小艾指着最后一道应用题。
+
+"那你就是三个人之一?"
+
+"嗯!"
+
+"那太厉害了。"
+
+小艾的表情恢复了。他去把卷子放回书包,然后跑回房间继续写作业了。
+
+小茜站起来,看了一眼还站在玄关的小付。
+
+她没说什么责备的话。她只是说了一句:
+
+"换鞋吧,饭快好了。"
+
+——
+
+那天晚上,小付躺在床上,打开App,找到那条「建议:与成员B进行一次非事务性交流」的推送,看了一遍,然后把它删掉了。
+
+不是因为它说错了。是因为它说的时机不对——它建议他去跟小茜聊天的时候,他根本不想聊天。他需要的是先自己消化一下,而不是被推着去"交流"。
+
+他删掉那条推送之后,又在App的设置里翻了一圈,找到了一个之前没注意过的选项:
+
+**「推送时段设置」**
+
+**「请在以下时间段接收建议通知:」**
+
+**□ 工作时间**
+
+**□ 通勤时间**
+
+**☑ 晚间(18:00-22:00)**
+
+他想了想,把「晚间」的勾也去掉了。
+
+然后他关掉App,把手机放在床头柜上。
+
+——
+
+第二天早上,他重新打开了「晚间」推送。
+
+不是因为他觉得需要了。是因为他觉得——昨晚那个问题,不是App的问题,是他的问题。
+
+App只是个工具。它不知道他今天被领导说了,不知道小艾考了92分,不知道他进门的时候心情不好。它只知道"情绪值下降了,建议聊天"。
+
+它猜错了。
+
+但它只是个工具。
+
+他重新打开了推送,然后去洗漱了。
+
+【第十八章 · 完】

+ 81 - 0
book2/第019章-家长会.md

@@ -0,0 +1,81 @@
+# 第十九章 家长会
+
+第二十九天。周三。家长会。
+
+小付请了半小时假,提前下班。他到学校门口的时候,小茜已经到了,站在门口等他。
+
+"走吧,三年级二班。"
+
+他们一起走进去。走廊里全是家长,有些人互相认识,在寒暄。小付不认识几个——以前家长会都是小茜来,他偶尔来一次也是坐在后排,听完就走。
+
+班主任姓林,三十出头,扎着低马尾,说话语速不快但很清晰。
+
+她先讲了班级整体情况,然后是一些常规通知,然后说:"如果需要单独沟通的家长,可以在会后留下来。"
+
+小茜提前跟她说过想聊一下,所以散会后,她带着小付一起走到了讲台前。
+
+——
+
+林老师看了看他们两个人,笑了一下:"小艾爸爸妈妈对吧?你们一起过来的?"
+
+"嗯。"小茜说。
+
+"挺好的,很少看到爸爸妈妈一起来的。"她说。"我找您聊是因为最近小艾在学校有一些变化,我觉得是好的变化,所以想跟您确认一下。"
+
+小付和小茜都看着她。
+
+"小艾以前上课不太爱举手回答问题,这个你们知道吗?"
+
+"知道。"小茜说。"他比较内向。"
+
+"对。但最近两周,他举手次数明显多了。虽然不是每次都答对,但他愿意举了。我觉得这是一个很好的信号——说明他在课堂上的安全感提高了。"
+
+林老师停了一下,又说:"另外,他最近写的日记里,提到'周末跟爸爸妈妈一起出去走了走'——他写的是'一起在河边走路,很开心'。我注意到他写的是三个人一起。所以我想问一下,家里最近是不是有什么变化?"
+
+小付和小茜同时沉默了一下。
+
+小茜先开口了:"……也没有特别的变化。就是……最近可能在一起的时间多了一点。"
+
+林老师点了点头,没有追问。她说:"那挺好的。小朋友对这种变化很敏感,他能感觉到。"
+
+——
+
+从学校出来的时候,天已经黑了。校门口的路灯亮着,一些家长骑着电动车从他们身边经过。
+
+他们两个走在人行道上,往地铁站的方向走。
+
+走了一段路,小付说了一句:
+
+"她说的那个日记……你看到了吗?"
+
+小茜摇了摇头:"没有。他写了我也不知道。"
+
+小付沉默了一会儿。
+
+"……所以系统不知道小艾的事。学校也不知道系统的事。但小艾自己感觉到了。"
+
+小茜没有接话。
+
+她走了一会儿,才说了一句:
+
+"我们以为我们在瞒着他。"
+
+——
+
+那天晚上,小付回到家,打开App,看了一遍五维数据。
+
+身48。智69。富52。行35。心42。
+
+然后他点进了「未分类数据」,看到那条关于小艾的记录还在:
+
+**「成员C:未归类。数据量不足,无法分析。」**
+
+他第一次觉得,App没有小艾的数据,可能不是一件坏事。
+
+有些东西,不需要被分析和评分。
+
+他把手机放在桌上,去小艾房间看了一眼——小艾已经睡着了,被子踢到一边,一只脚露在外面。
+
+他帮他把被子盖好,站了一会儿,然后轻轻带上了门。
+
+【第十九章 · 完】

+ 128 - 0
book2/第020章-满月.md

@@ -0,0 +1,128 @@
+# 第二十章 满月
+
+第三十天。
+
+App弹出了一份月度报告。不是那种全屏的、正式的版本更新式报告——是一份折叠在消息列表里的文档,标题只有一行字:
+
+**「F-1073。运行周期:30日。」**
+
+**月度摘要**
+
+整体稳定性评分:45/100(初始值:29,变化:+16)
+
+各维度变化:
+- 身:47→48(+1)
+- 智:68→69(+1)
+- 富:52→52(0)
+- 行:31→35(+4)
+- 心:42→43(+1)
+
+**「行」已进入合格区间(≥35)。但稳定性不足,存在回落风险。**
+
+**30日总结**
+
+前30天的主要变化集中在一个维度:「行」。其他维度变化不大。系统判断:当前阶段的改善主要来自"注意到问题"这一个动作本身。真正的行为转变尚未完成。
+
+**下一阶段展望**
+
+从第31天起,系统将进入「轻度协同模式」。届时将出现以下变化:
+
+1. 任务推送频率将降低——系统将从"主动建议"转向"被动响应"。
+2. 部分数据将实现双向可视化——双方都能看到对方选择公开的数据。
+3. 新增「外部环境监测」模块——当检测到来自家庭外部的压力因素时,系统将提前预警而非事后分析。
+
+**附注**
+
+本报告为系统自动生成。你不需要回复。
+
+——
+
+小付看完报告,把手机放在桌上。
+
+30天了。
+
+他回想了一下这30天——一开始觉得这个App挺奇怪的,后来觉得它有时候有用,有时候瞎扯。他删过推送时段,又加回来了。他手动记录过一次焦虑。他看到过小茜的温度曲线,也看到过她只开了一个开关。
+
+他不知道这30天到底改变了什么。但"行"从31到35,确实是一个可以量化的变化。
+
+他关掉App,去上班了。
+
+——
+
+小茜的月度报告内容不一样。
+
+她的报告不是数据表格,而是一段文字:
+
+**「30日评估」**
+
+这段时间,你的家庭温度曲线整体呈缓慢上升趋势。上升幅度不大,但方向是正的。
+
+值得注意的是:曲线在第三周出现了一次主动回升——不是由外部事件触发的回升,而是由稳定性本身带来的自然回升。这说明系统内部的恢复力正在形成。
+
+下一阶段,系统将减少主动提示。不是因为任务已完成,而是因为你已经知道该做什么了。
+
+——
+
+她读完最后一行,把手机放下了。
+
+"你已经开始知道该做什么了。"
+
+她不知道系统说的对不对。
+
+但她确实觉得,这30天,有些事情不太一样了。
+
+不是大事。是她开始注意到一些小东西——比如他买了一袋橘子,比如他会在饭桌上说一句"今天楼下有一只猫",比如他洗碗的时候手机屏幕亮着,上面是她的温度曲线。
+
+都是小事。
+
+但30天前,她没注意过这些。
+
+——
+
+晚上,小付在饭桌上说了一句:
+
+"今天那个App出了月度报告。"
+
+小茜抬头:"嗯,我也看到了。"
+
+"它说下一阶段要减少推送了。"
+
+"……嗯,它也是这么跟我说的。"
+
+小付夹了一口菜,嚼完,然后说了一句:
+
+"你觉得……有用吗?"
+
+小茜想了一会儿。
+
+"有时候有用。有时候挺烦的。"
+
+"那你还用吗?"
+
+"你呢?"
+
+"……我在问你。"
+
+小茜放下筷子,认真地想了一下。
+
+"用吧。反正……还没看完。"
+
+小付没再问了。
+
+他继续吃饭。
+
+但他在心里记了一下——她也没卸载。
+
+——
+
+那天晚上,小付在睡前打开了App,看到「行」仍然是35。
+
+没有涨,也没有跌。
+
+他看了几秒,然后把手机放在床头柜上,充电。
+
+——
+
+第三十天,结束了。
+
+【第二十章 · 完】

+ 85 - 0
book2/第021章-新阶段.md

@@ -0,0 +1,85 @@
+# 第二十一章 新阶段
+
+第三十一天。
+
+系统进入「轻度协同模式」的第一天。
+
+小付早上打开App的时候,发现界面又变了——不是布局变了,是色调变了。之前的深蓝底色变成了浅灰色,顶部的系统标题也从「JH-7 型联合后勤调度框架·家庭适配版」变成了更短的几个字:
+
+**「家庭中枢 · 协同模式」**
+
+他盯着那行字看了几秒。然后注意到主界面中间多了一行小字,取代了之前那些任务和提示:
+
+**「今日无建议。系统将在检测到关键事件时主动通知。」**
+
+没有建议。
+
+没有任务。
+
+没有"补给方案"。
+
+他把手机放进口袋,出门上班了。
+
+——
+
+一整天,App没有发过任何推送。
+
+小付在地铁上的时候习惯性地打开App看了一眼——没有新消息。午休的时候又看了一眼——还是没有。傍晚下班的时候再看了一眼——依然没有。
+
+他说不上来是松了口气,还是有点不习惯。
+
+——
+
+小茜那边也是一样。
+
+她的温度曲线还在,但之前那个会弹出通知的"预警"标签消失了。取而代之的,是曲线下方一行灰色的字:
+
+**「监测中。系统将在检测到持续异常时发出提示。」**
+
+监测中,不打扰。
+
+她第一天没觉得有什么不一样。但到了第二天,她发现自己打开App的次数变少了——从之前的每天七八次,变成了两三次。
+
+不是不想看了。是没什么需要看的了。
+
+——
+
+第三天,小付在公司收到了App的第一条"协同模式"推送。
+
+不是任务。不是建议。是一条很短的文字:
+
+**「监测到成员B今日情绪基线出现小幅波动。不构成预警。仅供参考。」**
+
+他看了两遍,确认没有"建议你做什么"的内容后才放下手机。
+
+就是一条通知。
+
+不告诉他该怎么做。
+
+——
+
+晚上,他到家的时候,小茜正在厨房做饭。
+
+他站在厨房门口,没有App的提示,没有"非事务性交流"的建议,没有"补给方案"。
+
+他站了一会儿,说了一句话:
+
+"今天那个App给我发了一条消息,说你情绪有点波动。"
+
+小茜没回头:"它跟你说了原因吗?"
+
+"没有。就说波动了一下。"
+
+"……我今天下午改稿子改到有点烦躁。可能跟那个有关系。"
+
+小付"嗯"了一声。
+
+然后他去洗手,回来摆碗筷了。
+
+一段很短的对话。没有App的建议,没有"那你应该……"的后续。
+
+就是一个人问了一下,另一个人答了一下,然后结束了。
+
+但以前,他连问都不会问。
+
+【第二十一章 · 完】

+ 91 - 0
book2/第022章-通知.md

@@ -0,0 +1,91 @@
+# 第二十二章 通知
+
+第三十五天。
+
+小付的公司正式发了通知:下个月进行第二轮人员优化,涉及技术部、市场部和行政部。具体名单未公布,但"优化比例约为15%"。
+
+邮件是下午三点发的,全公司。
+
+小付看完邮件,把页面关掉,继续改方案。他脸上没什么表情——不是因为不紧张,是因为在办公室里,每个人都在看那封邮件,每个人都在装作没在看。
+
+下班的时候,他在电梯里遇到了同组的同事。两个人并排站着,谁都没说话。到了一楼,同事先出去了,走之前说了一句:"保重。"
+
+小付说:"你也保重。"
+
+——
+
+他在地铁上打开App,看了一眼——「心」40,比昨天降了2。
+
+他没有手动记录,也没有把邮件内容告诉App。
+
+但App已经从数据里读到了——他的心率、他的手机使用模式、他在地铁上刷同一篇新闻刷了四遍的时间。
+
+系统没有发推送。它只是记录了一下。
+
+——
+
+到家的时候,小茜正在餐桌上用电脑改稿子。小艾在房间里写作业。
+
+他换了鞋,把包放下,在沙发上坐了一会儿。
+
+小茜抬头看了他一眼,没说话,又低头继续改稿了。
+
+但过了大概五分钟,她放下电脑,站起来,倒了一杯水,走过来放在他面前的茶几上。然后她什么也没说,坐回去继续改稿了。
+
+小付看着那杯水。
+
+他没说谢谢。他端起杯子喝了一口。
+
+——
+
+晚上,小艾睡着之后,小茜在沙发上坐到他旁边。
+
+"公司那封邮件,我看到了。"
+
+"……你怎么看到的?"
+
+"你手机没锁屏,放在桌上,我路过的时候扫了一眼。"
+
+小付沉默了一会儿。
+
+"……15%。不知道会不会轮到技术部。"
+
+小茜没有说"不会的""你肯定没事""别担心"之类的话。她沉默了一会儿,说了一句:
+
+"我存了一点钱。大概够撑几个月。"
+
+小付转头看她。
+
+"……什么时候存的?"
+
+"一直存的。每个月存一点。"
+
+他看着她,不知道该说什么。
+
+他们结婚八年,他一直以为家里的钱是月光——房贷、生活费、偶尔的额外支出,每个月剩不下什么。他不知道她一直在存钱。
+
+"你怎么没跟我说过?"
+
+"说了你就不让我存了。"
+
+小付没有说话。
+
+他低头看着那杯水——她放在茶几上的那杯,已经凉了。
+
+过了一会儿,他伸手把杯子拿起来,一口喝完了。
+
+——
+
+那天晚上,他打开App,在「手动记录」里打了一行字:
+
+**「今天公司发了裁员通知。她说她存了一笔钱。」**
+
+他没有写"焦虑""害怕""感动"之类的情绪词。他只是把两件事记在了一起。
+
+系统没有回复。
+
+但它把这条记录标记为:
+
+**「重要事件。已归档。」**
+
+【第二十二章 · 完】

+ 63 - 0
book2/第023章-等待.md

@@ -0,0 +1,63 @@
+# 第二十三章 等待
+
+接下来五天,小付公司里弥漫着一种奇怪的安静。
+
+邮件发完之后,没有人再公开讨论"优化"的事。大家照常开会、照常提交代码、照常午休——但每个人都在暗中观察。观察谁被领导叫去谈话了,观察谁的表情不太对,观察自己的工位是不是离门口太近了。
+
+小付也是这些人中的一个。
+
+他以前没觉得自己会这么在意——他以为他会很淡定,做好自己的事,该怎么样就怎么样。但真到了这个时候,他发现自己每天早上走进办公室的时候,心跳都会快一点。
+
+他打开App的次数变多了。
+
+不是在看五维数据。他是在看「心」那个数字每天的变化——从40到39,从39到38,从38回到39,又从39降到37。
+
+它像一只体温计,准确但冷酷地显示着他每一天的状态。
+
+但他没有手动记录任何东西。
+
+系统也没有给他发任何建议——协同模式下,它只在检测到关键事件时才会通知。"关键事件"的定义,大概不包括"一个员工在等待裁员通知"。
+
+——
+
+小茜注意到了他的变化。
+
+不是他表现出来的——他表面上一切正常,吃饭、睡觉、陪小艾写作业,什么都没耽误。但她注意到他看手机的时间变多了。不是那种"刷手机"的多,是那种"打开、看一眼、锁屏、过一会儿再打开"的多。
+
+她没有问他"你在看什么"。
+
+她只是在做饭的时候,比平时多做了半碗饭。
+
+然后在他吃完的时候,什么也没说地把碗收走了。
+
+——
+
+第四天晚上,小付躺在床上,翻来覆去睡不着。
+
+小茜已经睡了,呼吸平稳。
+
+他拿起手机,打开App,看到「心」仍然停在37——连续第三天低位。他没有做任何操作,正打算关掉App的时候,屏幕忽然弹出了一条消息——不是推送,是App内部的一条实时消息,像聊天窗口里对方正在输入的那种:
+
+**「系统检测到:成员A 当前处于持续压力状态。无法提供具体解决方案。但可以记录你想说的话。」**
+
+然后,那个「手动记录」的文本框自动弹了出来,光标在闪。
+
+他在凌晨一点十七分,对着那个闪烁的光标,打了几个字,又删掉了。然后打了另一行字,又删掉了。
+
+最后他什么都没写,把App关掉了。
+
+但他知道——系统在那一刻,不是在"监测"他。
+
+是在等他说话。
+
+他没说。
+
+但有人等过,这件事本身,让他在黑暗里躺了一会儿,然后翻了个身,慢慢睡着了。
+
+——
+
+第五天,名单没有公布。
+
+又延了一周。
+
+【第二十三章 · 完】

+ 99 - 0
book2/第024章-没结果的结果.md

@@ -0,0 +1,99 @@
+# 第二十四章 没结果的结果
+
+第四十天。
+
+名单没出来。通知说"优化方案尚在审批中,具体实施时间另行通知"。
+
+没有结果。没有期限。没有一个"是死是活给个痛快"的答案。
+
+小付看到这条通知的时候,第一反应不是放松,是一种更深的疲惫——不是"可能没事了",是"这件事还没结束,你得继续悬着"。
+
+他以前经历过这种状态。在军工研究所那三年,有一个项目拖了九个月,上上下下反复改,每次都说"下周定稿",每次都没有。他后来辞职,跟那种"永远悬着"的状态有很大关系。
+
+现在这种感觉又回来了。
+
+他坐在工位上,盯着电脑屏幕看了一会儿,然后打开App,看了一眼「心」——37。
+
+还是37。
+
+他关掉App,继续工作。
+
+——
+
+那天晚上,他在地铁上打开App,在「手动记录」里打了一行字:
+
+**「通知又延期了。不知道要等到什么时候。这种感觉很熟悉。」**
+
+他保存了这条记录。
+
+这一次,系统弹出了一条回复——不是自动回复的格式,是一行灰色的小字,像系统内部生成的一条备注:
+
+**「记录已保存。同类事件已归档:1条(2025/07/15——关于工作不确定性的记录)。」**
+
+他愣了一下。
+
+它把他上次写的那条"听说公司可能要裁人"也找出来了,标了同样的分类。
+
+同类事件归档。
+
+系统在告诉他:你不是第一次为这件事焦虑了。上次你焦虑的时候,也是这种状态。
+
+他说不清看到这条信息是什么感觉——不是被安慰,是被确认了。
+
+——
+
+周末,小茜发现小付在书房里翻东西。
+
+他翻出来一个旧笔记本,封面上印着他以前那家军工研究所的logo。他坐在书桌前翻了翻,然后合上,放回了抽屉里。
+
+小茜站在门口,看到了整个过程。
+
+"那个本子是什么?"
+
+"以前的。在所里的时候记的笔记。"
+
+"你从来没给我看过。"
+
+"……没什么好看的。都是些技术参数。"
+
+小茜走进来,在他旁边的椅子上坐下。
+
+"你在想什么?"
+
+小付沉默了一会儿。
+
+"……我在想,如果这次真的被裁了,我还能做什么。"
+
+"你才34岁,又不是老了。"
+
+"不是年龄的问题。是……我发现自己好像只会做现在这份工作。换一家公司,做一样的事。如果不做这个了,我不知道还能做什么。"
+
+小茜没有说"你可以做别的"或者"你太悲观了"。
+
+她想了想,说了一句:
+
+"你以前在那个研究所,做的是什么东西?"
+
+小付愣了一下。
+
+"……军用后勤系统。"
+
+"那你现在手机上那个App,也是后勤系统改的。你至少比大部分人都懂那个东西。"
+
+小付没有说话。
+
+他坐在书桌前,看着那个抽屉。过了一会儿,他说了一句:
+
+"……你说得对。"
+
+——
+
+那天晚上,他打开App,在「手动记录」里又写了一行字:
+
+**「她跟我说,我至少懂这个App。她是对的。」**
+
+系统没有回复。
+
+但那条记录被自动归入了「正向事件」分类。
+
+【第二十四章 · 完】

+ 85 - 0
book2/第025章-系统的一步.md

@@ -0,0 +1,85 @@
+# 第二十五章 系统的一步
+
+第四十三天。
+
+小付在公司收到了一条App推送,不是通知栏那种,是App内部的一条消息,标题写着:
+
+**「一条来自系统的建议——非任务型。」**
+
+他点开。
+
+**「你最近30天在「手动记录」中写下的内容,主要集中在以下三个主题:」**
+
+**1. 工作不确定性(3条记录)**
+
+**2. 与成员B的互动观察(2条记录)**
+
+**3. 关于小艾的零散记录(1条记录)**
+
+**「建议:如果你愿意,可以尝试将第2类和第3类记录直接告诉成员B和小艾,而不是只写在这里。」**
+
+**「备注:这不是任务。你可以不执行。」**
+
+小付看着这条消息,愣了很久。
+
+不是因为它说了什么,是因为它从来没这样说过话。
+
+以前的建议都是"建议执行补给程序""建议进行非事务性交流"——冷冰冰的,像指令。但这条消息的语气不一样——它像是在跟他商量。
+
+它说"建议",说"如果你愿意",说"可以尝试",说"这不是任务"。
+
+他盯着那行"你可以不执行"看了很久,然后把手机锁屏了。
+
+他没有去执行它。但他也没有删掉它。
+
+——
+
+晚上,他吃完饭,坐在沙发上,拿出手机,又看了那条消息一遍。
+
+然后他做了一件事。
+
+他站起来,走到小艾的房间门口。小艾正在整理书包,准备第二天的课。
+
+"小艾。"
+
+"嗯?"
+
+"你上次那个92分的数学卷子,还在吗?"
+
+"在啊。我放在抽屉里了。"
+
+"能不能给我看看?"
+
+小艾从抽屉里翻出那张卷子,递给他。他接过来,认真地看了一遍。然后他把卷子还给小艾,说了一句:
+
+"这道最后一道题,全班只有三个人做对了。你挺厉害的。"
+
+小艾愣了一下,然后笑了一下——不是那种"我好得意"的笑,是那种"你居然记得"的笑。
+
+"你上次说过了。"
+
+"……再说一遍不行吗?"
+
+"行。"
+
+小付从房间里出来,走回客厅。
+
+小茜在沙发上,看到了他拿着卷子走进小艾房间的全过程。
+
+她没说话。
+
+但她低下头,继续看手机的时候,嘴角有一点几乎看不出来的弧度。
+
+——
+
+那天晚上,小付打开App,看到「行」从35变成了36。
+
+涨了1。
+
+他想了想,觉得可能是因为他刚才去跟小艾说了那句话。
+
+他关掉App,没做什么特别的反应。
+
+但他在心里想:系统这次说的,好像是对的。
+
+【第二十五章 · 完】

+ 105 - 0
book2/第026章-爸妈要来.md

@@ -0,0 +1,105 @@
+# 第二十六章 爸妈要来
+
+第四十五天。
+
+小茜接到了一个电话——她妈打来的。
+
+"我们下个月想去深圳住一段时间,看看你们,也看看小艾。"
+
+小茜握着手机,沉默了两秒,然后说:"好啊,什么时候?"
+
+"下个月十号左右,待个……两三周吧。"
+
+"好,我跟小付说一下。"
+
+挂掉电话之后,她坐在沙发上,把手机翻过来又翻过去,然后打开微信,给小付发了一条消息:
+
+**小茜:** 我妈说下个月要来住两三周。
+
+**小付:** 什么时候?
+
+**小茜:** 十号左右。
+
+**小付:** 好。
+
+**小茜:** 你不介意吧?
+
+**小付:** 不介意。你爸妈又不是外人。
+
+对话很短,很正常。但小茜知道,这件事没那么简单。
+
+她爸妈每年会来一次,以前每次来,家里都会进入一种"微妙的紧张状态"——不是吵架,不是谁对谁不满,是生活方式不一样带来的那种持续的、低强度的摩擦。
+
+她妈喜欢早起,六点就开始在厨房里弄出声音。她爸喜欢看电视,声音开得很大。他们在的时候,小付会变得比平时安静一些——不是不高兴,是不自在。
+
+她自己也会变得比平时敏感一些——在她妈面前,她总是容易变回"女儿"的模式,而不是"妻子"或者"妈妈"的模式。
+
+她不知道这次会不会不一样。
+
+——
+
+晚上,小付回来的时候,小茜把这件事又说了一遍。
+
+"我妈说十号来,住两三周。"
+
+"嗯,你跟我说了。"
+
+"……你觉得行吗?"
+
+小付正在换鞋,抬起头来看她一眼。
+
+"行啊。你爸妈来,有什么不行的。"
+
+"以前他们来的时候,你话都变少了。"
+
+小付直起身来,想了一下。
+
+"……有吗?"
+
+"有。"
+
+小付沉默了一会儿,然后说了一句:
+
+"这次会好一点。"
+
+小茜看着他。
+
+"为什么?"
+
+小付想了想,说了一句他自己也没完全想明白的话:
+
+"……因为我现在知道,有些话不说,比说了更累。"
+
+——
+
+那天晚上,小茜打开App,在温度曲线下面看到了一条新的系统记录——不是她写的,是系统自动生成的:
+
+**「检测到:外部家庭成员即将到访。预计时间:约三周后。」**
+
+**「系统评估:该事件可能对家庭系统稳定性产生中等影响。建议:在事件发生前,完成一次内部协调对话。」**
+
+她看着「外部家庭成员」那几个字,觉得这个分类挺有意思的。
+
+在她妈来之前,她妈已经被系统归类为"外部因素"了。
+
+她不知道该不该笑。
+
+但她把那条记录截了个图,存了下来。
+
+——
+
+第二天,小茜给她妈回了个电话:"妈,十号来没问题。房间我收拾一下。"
+
+她妈在电话那头说了句"好",然后开始问小艾最近怎么样、学习怎么样、有没有挑食。
+
+小茜一一回答了,语气比平时耐心了一些。
+
+她妈挂了电话之后,在微信上给她发了一段语音,语气很高兴:
+
+"那妈妈到时候给你们带点家里的腊肉和干菜。"
+
+小茜听了两遍。
+
+她把那段语音收藏了。
+
+【第二十六章 · 完】

+ 93 - 0
book2/第027章-收拾房间.md

@@ -0,0 +1,93 @@
+# 第二十七章 收拾房间
+
+第四十七天。周六。
+
+小茜开始收拾那间堆满杂物的次卧。
+
+那间房说是"次卧",其实已经很久没当过卧室了——小付的书桌在那里,几个纸箱堆在墙角,一个不用的折叠跑步机占了一半空间。她爸妈上次来是去年的事,走之后这间房就慢慢被东西填满了。
+
+她站在门口看了一下,叹了一口,开始动手。
+
+小付在客厅听到动静,走过来看了一眼。
+
+"你在干嘛?"
+
+"收拾房间。我妈下个月要来,总不能让他们睡仓库。"
+
+小付看了看那间房,又看了看她,然后走进去,把那个折叠跑步机往墙边推了推。
+
+"这个放阳台去吧。"
+
+"阳台放不下。"
+
+"……那放我书房。"
+
+"你书房也没地方。"
+
+两个人站在那间堆满杂物的房间里,同时沉默了几秒。然后小付说:
+
+"那把跑步机卖了吧。"
+
+小茜愣了一下:"你当初花了三千买的。"
+
+"买了三年,用过三次。三千块,每次一千。"
+
+小茜忍不住笑了一下——不是笑他,是笑那个"每次一千"的算法。
+
+"……你认真的?"
+
+"认真的。挂闲鱼上,能卖几百算几百。"
+
+他们当天下午就把跑步机拆了,拍了照片,挂上了闲鱼。定价500块,包邮。
+
+第二天就被人拍走了。
+
+小付看着那个"已售出"的提示,说了一句:"它果然只值500。"
+
+小茜把那间房彻底打扫了一遍——拖了地,擦了窗,换了床单,把纸箱搬到了阳台角落。房间空出来之后,看起来比原来大了一倍。
+
+她站在房间中间,环顾了一圈,说了一句:
+
+"好像……也挺好的。"
+
+小付靠在门框上,没说话。
+
+他打开手机,打开App,看了一眼——「行」还是36。没涨。
+
+他没期待它涨。
+
+但他注意到一件事——系统最近推送的频率越来越低了。不是出问题了,是真的在慢慢退后。
+
+——
+
+晚上,小茜给她妈打了视频电话,让她看收拾好的房间。
+
+"妈你看,床单是新换的,柜子也擦过了。"
+
+"好好好,不用这么麻烦,我们住几天就走了。"
+
+"不麻烦。"
+
+挂了电话之后,她坐在那间收拾好的房间里,发了一会儿呆。
+
+她想起去年爸妈来的时候,她没提前收拾房间——他们到的前一天晚上她才匆忙把东西挪开,床单是旧的,柜子上还有灰。她妈没说什么,但她自己知道。
+
+今年不一样。
+
+她提前三周就开始收拾了。
+
+她不知道这个变化跟那个App有没有关系。但她知道,她在做一件以前不会做的事。
+
+——
+
+当天晚上,App在后台记录了一行日志:
+
+**「F-1073。成员B主动为外部家庭成员到访做了物理空间准备。提前量:21天。」**
+
+**「该行为在过去12个月的同类型事件中,属于首次。」**
+
+系统没有把这个发现告诉任何人。
+
+它只是记录了一下。
+
+【第二十七章 · 完】

+ 111 - 0
book2/第028章-到了.md

@@ -0,0 +1,111 @@
+# 第二十八章 到了
+
+第五十一天。小茜爸妈到了。
+
+高铁,深圳北站,上午十点四十。小茜去接的站,小付在家带着小艾。
+
+门铃响的时候,小艾先冲过去开了门。
+
+"外公!外婆!"
+
+小茜妈拎着两个大袋子站在门口,小茜爸跟在后面,手里也拎着一个袋子,还有一个行李箱。
+
+"小艾又长高了!"小茜妈进门就蹲下来抱了一下小艾,然后站起来,环顾了一圈客厅。
+
+"收拾得真干净。"
+
+小付从厨房走出来:"爸,妈。路上辛苦了。"
+
+"不辛苦不辛苦,高铁快得很。"
+
+一切都很正常。寒暄、换鞋、放行李、倒水。小茜妈从袋子里掏出一大堆东西——腊肉、干菜、自己做的辣酱、给小艾买的两件衣服。
+
+小付站在旁边,看着那些东西从袋子里一样一样被拿出来,摆满了餐桌。
+
+他说了一句:"妈,不用带这么多东西,深圳都买得到。"
+
+"买得到是买得到,家里的味道不一样嘛。"
+
+小付没有反驳。他拿起一块腊肉闻了一下,说:"确实香。"
+
+小茜看了他一眼。
+
+——
+
+第一天的气氛是好的。
+
+中午一起吃了饭,小茜妈做了两个菜——她说"你们深圳的菜没味道",又用自己带来的腊肉炒了一盘蒜苗。小付吃了两碗饭。
+
+下午,小茜妈陪小艾在客厅拼乐高,小茜爸坐在沙发上看电视——声音开得比平时大了一些,但小付没说什么。
+
+小付坐在书房里,开着门,能听到客厅里的声音——电视声、小艾的笑声、小茜妈说"这个应该拼在这里"的声音。
+
+他低头看了一眼手机。
+
+App没有推送。没有"外部家庭成员入侵预警"之类的东西。
+
+他想了想,在手动记录里打了一行字:
+
+**「她爸妈到了。目前一切正常。」**
+
+——
+
+到了晚上,第一道裂缝出现了。
+
+小茜妈在厨房看到小茜在用小付的手机充电器,顺口说了一句:"你怎么用他的充电器?你自己的呢?"
+
+"我的在房间,懒得去拿。"
+
+"那你也应该用你自己的,男人的东西你别乱动。"
+
+小茜没有说话。
+
+小付正好从旁边走过,听到了这句话。他也没说话。
+
+小茜妈没有恶意——她那一代人觉得"夫妻之间也要分清楚"是天经地义的事。但小茜不这么想。她跟她妈在这类事情上的分歧,不是一天两天了。
+
+小付走回书房,坐下来,打开App,看到「心」从42降到了40。
+
+不是因为他。
+
+是因为她。
+
+——
+
+晚上,小茜在厨房洗碗的时候,小茜妈在旁边擦灶台。
+
+"小付最近对你好不好?"
+
+小茜洗碗的手停了一下——只是一下,然后继续洗。
+
+"挺好的。"
+
+"他工作还顺利吧?"
+
+"还行。"
+
+"你那个自由职业,收入稳不稳定啊?"
+
+"……还行。"
+
+她妈没再问。
+
+但小茜知道,她妈每次来,都会问同样的问题。不是恶意,是关心——但那种关心,每次都让她觉得像是在接受检查。
+
+她洗完碗,回到房间,坐在床边,拿出手机,打开App。
+
+温度曲线在晚饭后有一小段明显的下降。
+
+她看着那条下降,又看了一眼门口——她妈正在客厅里跟小艾一起看电视,笑声从门缝里传进来。
+
+她不知道该生谁的气。
+
+她妈没有做错什么。她也没有做错什么。
+
+但那条曲线确实下降了。
+
+她把手机放在床头柜上,屏幕朝下,然后深吸了一口气,站起来,走出房间。
+
+"妈,吃水果吗?我削个苹果。"
+
+【第二十八章 · 完】

+ 101 - 0
book2/第029章-不习惯.md

@@ -0,0 +1,101 @@
+# 第二十九章 不习惯
+
+第二天早上六点半,小茜妈就起来了。
+
+她轻手轻脚地开了房门,但厨房的动静还是传到了主卧——柜门开关的声音、水龙头的声音、在灶台上放东西的声音。不大,但在一栋隔音不太好的80平小两房里,足够把人吵醒。
+
+小付醒了。他翻了个身,看了一眼手机——6:42。
+
+他没有起床。他躺在那里,听着厨房里的声音,知道自己暂时睡不着了。
+
+他拿起手机,打开App,看了一眼——「心」39。比昨天又降了1。
+
+他关掉App,继续躺着,直到闹钟响。
+
+——
+
+早上七点半,他起床的时候,小茜妈已经煮好了粥,煎了荷包蛋,还拌了一碟黄瓜。
+
+"起来了?快来吃早饭。"
+
+小付在餐桌前坐下,看着那碗粥。粥是稠的,米粒都煮开了花——跟他妈煮的那种稀粥不一样。
+
+他喝了一口,说了一句:"妈,这粥挺好喝的。"
+
+小茜妈笑了一下:"好喝就多喝点。你看你瘦的。"
+
+小付低头继续喝粥。
+
+小茜从房间里出来,看到这一幕,没说什么,坐下来也给自己盛了一碗。
+
+——
+
+上午,小茜妈在客厅里转了一圈,开始"帮忙"。
+
+她把茶几上小付的充电器、钥匙、耳机线收进了一个抽屉里——"桌上不要放太多东西,乱"。她把阳台上的几盆植物重新摆了一下位置——"这个不能晒太阳,你不知道吗"。她把冰箱里的东西重新整理了一遍——"这个放这里容易坏,应该放上面"。
+
+她每做一件事,都是好意的。但每做一件事,都在无声地传递一个信息:你们这里,不够好。
+
+小茜坐在沙发上,看着她妈在客厅里忙来忙去,没有说"你别弄了"。
+
+她只是坐在那里,看着。
+
+小付在书房里,听到了外面的动静。他没有出去。
+
+他打开手机,在手动记录里写了一行字:
+
+**「她妈在收拾客厅。感觉她有点不高兴。」**
+
+他没有写"我"。
+
+他写的是"她"。
+
+——
+
+中午,小付在厨房帮忙盛饭,小茜站在他旁边。
+
+他低声说了一句:"你妈把客厅收拾了。"
+
+"……嗯。"
+
+"你没事吧?"
+
+小茜愣了一下,转头看他。
+
+"……你注意到了?"
+
+"……嗯。"
+
+小茜没有说"没事"或者"你想多了"。她沉默了一会儿,然后说了一句:
+
+"她知道她是好意。但那是她的方式,不是我的方式。"
+
+小付没有接话。他把盛好的饭放在桌上,然后说了一句:
+
+"那你的方式是什么?"
+
+小茜看着他,没有回答。
+
+但她心里在想:他以前不会问这种问题。
+
+——
+
+晚上,小茜妈在客厅里跟小艾一起看电视,笑声从门缝里传进来。
+
+小茜坐在卧室的床边,打开App,看到温度曲线今天走了一条复杂的路线——早晨下降,午间回升,傍晚又有一小段波动。
+
+她看着那条曲线,然后做了一件事——
+
+她打开设置,把之前关掉的三个共享开关,一个一个地打开了。
+
+不是全部。是三个。
+
+她留下了「实时情绪触发事件记录」那个,仍然关着。
+
+但其余的,她都开了。
+
+她不知道自己为什么要在这个时候开。可能是因为她妈在客厅里,而她在房间里,而她需要有人知道她现在是什么状态。
+
+她锁了屏,把手机放在床头柜上,然后站起来,走出房间,去客厅跟她妈一起看电视了。
+
+【第二十九章 · 完】

+ 76 - 0
book2/第030章-调整.md

@@ -0,0 +1,76 @@
+# 第三十章 调整
+
+小茜爸妈来的第四天。
+
+小付发现了一件事:他打开App的次数变少了。
+
+不是刻意的,是自然而然地——早上起床看一眼,晚上睡前看一眼,中间几乎不打开。不是因为App不重要了,是因为生活里多了一个需要应对的变量——小茜爸妈在,他大部分时间都在"做人",没有多余的心力去关注那个灰色的图标。
+
+他注意到「心」在40上下浮动——比爸妈来之前低了2-3点,但没跌到谷底。
+
+「行」维持在36,没涨也没跌。
+
+他说不清这个数据是"好"还是"不好"——在外部压力存在的情况下,核心指标没有崩溃,可能已经算不错了。
+
+——
+
+小茜的状态也在调整。
+
+她妈来的第二天,她跟她妈因为"小艾要不要穿外套"发生了几句口角——她妈觉得天凉了必须穿,小茜觉得不冷就不用穿。不是什么大事,但那种"你的孩子,你妈比你还上心"的微妙压力,让空气凝固了大概半小时。
+
+半小时后,小茜妈去厨房切了一盘水果,端出来放在茶几上,什么都没说。小茜吃了一口,也没说"谢谢"。
+
+但两个人之间的那层冰,就这样化了。
+
+小茜后来在App里看到温度曲线——那半小时里有一小段明显的下跌,然后慢慢回升。她看着那条曲线,像看一个事件的记录仪。
+
+她以前不知道这些"小事"对家庭温度的影响是可以量化的。现在知道了,反而觉得——有些事,不知道比知道好。
+
+因为知道了,你就没法假装没看到。
+
+——
+
+第六十天的时候,小付收到了App的第二次月度报告——迟到了几天,但终于来了。
+
+**「F-1073。运行周期:60日。第二阶段评估摘要。」**
+
+**整体稳定性评分:47/100(第一次报告:45,变化:+2)**
+
+**变化幅度不大,但结构更稳定了。** 报告中写道。
+
+**「行」稳定在36,未出现回落。** 这是系统认为最重要的信号——因为"行"是五个维度里最脆弱的一个,容易升也容易跌。能在外部家庭成员到访的情况下保持稳定,说明之前的改善不是暂时的。
+
+**但报告也指出了一些问题:**
+
+- 「心」的波动幅度在近两周有所增加,与外部家庭成员到访的时间线吻合。
+- 成员C(小艾)仍然没有纳入系统分析模块。系统建议在下一阶段考虑为小艾建立基础数据档案。
+
+小付看完报告,觉得"成员C"那一段又出现了。
+
+他想了想,在手动记录里写了一行字:
+
+**「系统一直说要把小艾加进来。但我不知道这是好事还是坏事。」**
+
+他没有点击"保存"。他盯着那行字看了一会儿,然后一个字一个字地删掉了。
+
+他暂时不想回答这个问题。
+
+——
+
+小茜的60天报告没有提到小艾。
+
+她的报告只有一句话,出现在温度曲线下方:
+
+**「过去60天,你的家庭温度曲线呈缓慢上升趋势。近期的外部因素导致了一些波动,但整体方向未变。」**
+
+**「也就是说:你正在做的事情,是对的。」**
+
+她看到那行字的时候,正在阳台上。
+
+她妈在客厅里,小艾在写作业,小付还没下班。
+
+她站在阳台上,风吹过来,她看着那行字,没有特别的情绪波动。
+
+但她把手机锁屏的时候,动作比平时轻了一些。
+
+【第三十章 · 完】

+ 93 - 0
book2/第031章-钓鱼.md

@@ -0,0 +1,93 @@
+# 第三十一章 钓鱼
+
+小茜爸妈来的第七天。
+
+周日早上,小茜爸在阳台上抽烟,看着窗外,忽然说了一句:"这附近有没有能钓鱼的地方?"
+
+小付正在客厅喝水,听到了这句话。
+
+他以前从来没跟他岳父单独出去过。每次他们来,他都是在家待着,或者一起吃饭,从来没有"单独活动"过。
+
+但他放下杯子,说了一句:"楼下那条河,好像有人钓。要不要去看看?"
+
+小茜爸转过头来,有点意外地看了他一眼。
+
+"行啊。"
+
+——
+
+两个人拿着两把小折叠凳,走到了河边的步道上。河水不算干净,但确实有人在钓——两三个老人坐在河边,拿着简易的竿子,脚边放着塑料桶。
+
+小茜爸找了个位置坐下,把鱼竿甩出去,然后就不说话了。
+
+小付坐在旁边,没有鱼竿——他本来就没打算钓,就是出来坐坐。
+
+两个人沉默地坐了大概二十分钟。
+
+然后小茜爸开口了,没说钓鱼的事,说了一句:
+
+"小茜她妈说话就这样,你别往心里去。"
+
+小付愣了一下。
+
+"……没事,我知道她没恶意。"
+
+"她那个人,就是嘴快。心是好的。"
+
+"嗯。"
+
+又是一阵沉默。
+
+然后小茜爸又说了一句:
+
+"小茜比以前开心了一点。"
+
+小付转头看着他。
+
+"我看得出来。"小茜爸说。"上次来的时候,她不是这样的。这次不一样。"
+
+小付不知道该说什么。他沉默了一会儿,说了一句:
+
+"……我也没做什么特别的。"
+
+"有时候不用做什么特别的。"小茜爸说。"少做点让她不高兴的事,就行了。"
+
+小付看着河面,没有接话。
+
+但他记住了这句话。
+
+——
+
+中午,两个人空着手回去了——一条鱼都没钓到。
+
+小茜妈在门口看到他们两手空空,笑着说:"钓了半天,一条鱼都没钓到?"
+
+"没口。"小茜爸说。
+
+"那你们去干嘛了?"
+
+"坐了一下。"
+
+小茜在厨房里听到了这段对话。
+
+她看到她爸换鞋的时候,表情是放松的。她看到她爸跟小付说话的时候,语气跟以前不太一样——不是那种"女婿"和"岳父"之间的客气,是一种更自然的东西。
+
+她没有问他们出去聊了什么。
+
+但她把小付的那碗饭,盛得比平时满了一点。
+
+——
+
+晚上,小付打开App,在手动记录里写了一行字:
+
+**「今天跟她爸去河边坐了一下午。没钓到鱼。但他说小茜比以前开心了一点。」**
+
+他保存了这条记录。
+
+系统没有回复。
+
+但那条记录被自动归入了一个分类——「外部关系·正向事件」。
+
+——这是系统第一次为小付建立"岳父关系"的数据维度。
+
+【第三十一章 · 完】

+ 89 - 0
book2/第032章-走之前.md

@@ -0,0 +1,89 @@
+# 第三十二章 走之前
+
+小茜爸妈来的第十天,开始收拾行李了。
+
+走之前的那天晚上,小茜妈在厨房里包饺子——她来的时候带了面粉和擀面杖,说"深圳的饺子皮不好吃"。小茜在旁边帮忙,母女俩在厨房里站了一个多小时,案板上铺满了饺子。
+
+小付在客厅里陪小茜爸看电视。小艾在自己房间里画画。
+
+厨房里,水烧开了,小茜妈把第一批饺子下进去,用漏勺轻轻推了一下,防止粘锅。然后她忽然开口了,声音不大,像是随口说的:
+
+"小付变了一些。"
+
+小茜正在擀皮,手没停。
+
+"是吗?"
+
+"嗯。以前他来接站,就站在出口等,不会帮你拎东西。这次他把你爸手里的袋子接过去了。"
+
+小茜没有接话。
+
+"还有,以前他吃饭的时候不怎么说话。这次他主动问了你爸一句'要不要喝点酒'。"
+
+小茜擀皮的动作慢了一点。
+
+"妈,你到底想说什么?"
+
+小茜妈看着锅里翻滚的饺子,沉默了一会儿。
+
+"我想说——如果他对你比以前好了,你也要对人家好一点。别光顾着工作,家里的饭也要做。"
+
+小茜没有回答。
+
+但她点了点头。
+
+——
+
+第二天早上,小茜爸妈走了。
+
+高铁站。小茜爸走在前面,拎着那个来时的行李箱。小茜妈走在后面,拉着小茜的手,说了一句:
+
+"腊肉放在冰箱冷冻层,可以放很久。辣酱放在常温就行,别放冰箱。"
+
+"知道了,妈。"
+
+"天冷了让小艾多穿点。"
+
+"知道了。"
+
+"你们自己也注意身体。"
+
+"知道了。"
+
+小茜妈松开她的手,转身走向检票口。
+
+小茜站在原地,看着他们的背影消失在安检通道里。
+
+她站了一会儿,然后转身,走出车站,坐地铁回家。
+
+——
+
+到家的时候,屋子里很安静。
+
+小艾上学去了。小付上班去了。
+
+她一个人站在客厅里,看着茶几上她妈留下的那瓶辣酱,忽然觉得屋子有点空。
+
+她坐在沙发上,打开手机,看了一眼App。
+
+温度曲线在过去十天里,走了一条复杂的路线——有波峰,有波谷,但整体没有崩溃。曲线最右端,今天的位置,跟十天前几乎持平。
+
+她看着那条曲线,忽然觉得,这个App有时候也挺准的。
+
+它没说"一切完美"。它只是说:没有崩溃,就是好事。
+
+她把手机放在茶几上,站起来,去厨房给自己倒了一杯水。
+
+——
+
+同一时间,小付在公司收到了App的一条推送。不是系统消息,是一条自动生成的归档记录:
+
+**「F-1073。外部家庭成员到访事件:已结束。持续时间:10天。事件期间,家庭系统稳定性评分未出现显著下降。」**
+
+**「评估:系统具备承受中等外部干扰的能力。」**
+
+小付看着那条记录,没有特别的表情。
+
+但他把手机放回口袋的时候,动作比平时轻了一些。
+
+【第三十二章 · 完】

+ 91 - 0
book2/第033章-空下来的房子.md

@@ -0,0 +1,91 @@
+# 第三十三章 空下来的房子
+
+爸妈走后的第一天。
+
+早上六点四十五,小付醒了——没有厨房的动静,没有柜门开关的声音,没有水龙头的声音。他翻了个身,又睡了一会儿。
+
+七点二十,他起来的时候,客厅是空的。茶几上那瓶她妈留下的辣酱还放在原来的位置,旁边没有叠好的衣服,没有打开的电视,没有"你醒了?快来吃早饭"的声音。
+
+小茜也起来了,站在厨房里,只给自己倒了一杯水。
+
+"你妈走了之后,感觉房子变大了。"小付说。
+
+小茜端着水杯,靠在灶台上,环顾了一圈客厅。
+
+"嗯。"
+
+"你爸昨天说的那句话,'少做点让她不高兴的事',我想了一下。"
+
+小茜看着他。
+
+"他说得对。"
+
+小茜没有接话。她喝了一口水,然后说了一句:
+
+"我妈走之前跟我说,要对你好一点。"
+
+小付愣了一下。
+
+"……你妈说的?"
+
+"嗯。"
+
+"她怎么说的?"
+
+"她说'如果他对你比以前好了,你也要对人家好一点'。"
+
+小付没有说话。
+
+两个人在厨房和客厅之间的过道上站着,一个端着水杯,一个还没完全清醒,隔着一小段距离,谁都没有再说话。
+
+但那段距离,跟一个月前比起来,好像短了一点点。
+
+——
+
+当天晚上,小付收到了公司的一封邮件——不是正式通知,是部门主管发的一封群发消息:
+
+**"各位同事,优化方案已确定,本周五前将逐一面谈。请大家保持正常工作节奏。"**
+
+逐一面谈。
+
+他看完邮件,把手机放在桌上,没有打开App,没有手动记录。他坐在工位上,看了一会儿窗外的天,然后继续工作。
+
+——
+
+他到家的时候,小茜已经做好了饭。
+
+小艾在摆筷子。桌上摆着三菜一汤——比平时多了一个菜。
+
+"今天怎么做这么多?"
+
+"你妈走了,冰箱里剩的菜不吃要坏了。"
+
+小付在餐桌前坐下,拿起筷子,夹了一口菜。
+
+"好吃。"
+
+"嗯。"
+
+他吃了几口饭之后,说了一句:
+
+"公司今天发邮件了,说这周要面谈。"
+
+小茜夹菜的手停了一下,然后继续。
+
+"……知道了。"
+
+她没有问"你紧张吗",没有说"不会有事的"。她只是说"知道了",然后给他夹了一块排骨。
+
+小付看着碗里那块排骨,没有说谢谢。
+
+他低头继续吃饭。
+
+——
+
+那天晚上,小付在睡前打开App,看了一眼——「心」从39回升到了41。
+
+不是"高兴"的数据,是"还可以"的数据。
+
+他关掉App,把手机放在床头柜上,翻了个身,很快就睡着了。
+
+【第三十三章 · 完】

+ 87 - 0
book2/第034章-面谈.md

@@ -0,0 +1,87 @@
+# 第三十四章 面谈
+
+周五下午,小付被叫去谈话了。
+
+办公室不大,部门主管和HRBP坐在桌子对面。主管的表情不太自然——他之前跟小付合作过两个项目,关系不算差。
+
+"小付,叫你来主要是想跟你沟通一下公司这次人员调整的情况……"
+
+小付坐在椅子上,听着主管说完了那段话——大意是:公司很感谢你这几年的付出,但这次调整涉及到技术部,你的岗位受到影响。公司会按照劳动法规定给你补偿,N+1,这个月内办完手续。
+
+他说"好"。
+
+主管看起来有点意外——他可能以为小付会问"为什么是我"或者"能不能再考虑一下"。但小付没有。
+
+他问了一个问题:"最后工作日是哪天?"
+
+"下周五。"
+
+"好。"
+
+他站起来,跟主管握了一下手,然后走出了办公室。
+
+——
+
+回到工位上的时候,旁边几个同事都在看他。他没有看他们。他坐下来,打开电脑,开始整理自己手头的文档。
+
+他做这件事的时候,表情很平静——不是装的,是真的平静。那封"逐一面谈"的邮件发出来之后,他已经等了五天。这五天里,他把最坏的结果已经想过很多遍了。真的来了的时候,反而没有想象中那么震动。
+
+他拿起手机,打开App,看到「心」41。
+
+没有降。
+
+他也不知道为什么没降。可能是一块石头终于落地了,反而是放松。
+
+他给老周发了一条消息:
+
+**小付:** 被裁了。N+1,下周五last day。
+
+**老周:** ……操。晚上出来喝一杯?
+
+**小付:** 行。
+
+——
+
+晚上,他没有去喝酒。他回了家。
+
+小茜已经知道了——不是他告诉她的,是他的手机没锁屏,她看到了那条跟老周的聊天记录。
+
+她坐在沙发上,没有问他"你怎么不早点告诉我"。
+
+她只是问了一句:"补偿多少?"
+
+他说了一个数字。
+
+她算了一下,然后说:"加上我存的那笔,够撑一年。"
+
+小付在沙发另一头坐下,没有说话。
+
+过了一会儿,他说了一句:
+
+"下周五last day。之后……我暂时没想好做什么。"
+
+小茜没有接话,沉默了一会儿,然后说了一句:
+
+"不急。"
+
+不是"没事的"——是"不急"。
+
+小付坐在沙发上,看着客厅的灯,说了一句:
+
+"嗯。"
+
+——
+
+那天晚上,他打开App,在手动记录里打了很长的一段话,比之前任何一次都长:
+
+**「今天被裁了。不意外,等了两周了,终于来了。补偿还可以,够撑一阵子。她早就存了一笔钱,她说够撑一年。我不知道她什么时候存的,但她存了。她没怪我,也没说"没事的"那种废话。她说"不急"。我暂时不知道接下来做什么,但她说了不急,我觉得好像真的不用太急。」**
+
+他写完之后,没有删改,直接点了保存。
+
+系统没有回复。
+
+但那条记录被标记了一个新的标签——系统第一次使用了这个标签:
+
+**「事件类型:重要转折。已归档。」**
+
+【第三十四章 · 完】

+ 113 - 0
book2/第035章-last-day.md

@@ -0,0 +1,113 @@
+# 第三十五章 Last Day
+
+Last day是个周五。
+
+小付早上到公司的时候,比他想象中平静。他花了两个小时整理手头的文档,写了交接说明,发给了接手他工作的同事。然后他把抽屉里的东西清出来——一个笔记本、几支笔、一个充电器、一包还没拆的饼干。装进纸箱,不大,一箱就够了。
+
+中午,几个关系不错的同事叫他一起吃饭。不是散伙饭,就是一楼食堂,各自打了饭,坐在一张桌子上。
+
+有人问了一句:"接下来有什么打算?"
+
+"还没想好。先休息一下。"
+
+"挺好的。拿了赔偿,休息一阵再找。"
+
+"嗯。"
+
+没有人说"你肯定能找到更好的"那种话。大家都很克制地表达了关心,然后聊了一些别的话题——最近的球赛、公司新来的实习生、楼下咖啡店的折扣。
+
+吃完饭,他回工位上坐了一会儿。
+
+下午三点,他抱着纸箱,走出了公司大门。
+
+没有回头。
+
+——
+
+他坐地铁回家,不是高峰期,车厢里人不多。他抱着纸箱坐在靠门的位置,地铁经过几个站,窗外的隧道灯一闪一闪的。
+
+他拿出手机,打开App,看了一眼。
+
+「心」40。
+
+比昨天降了1,但没崩。
+
+他关掉App,把手机放回口袋。
+
+——
+
+到家的时候,小茜在家——她今天没出去,也没接新稿子。她坐在沙发上,看到他抱着纸箱进来,没有说话。
+
+他把纸箱放在地上,换了鞋,在沙发上坐下来。
+
+两个人并排坐着,沉默了一会儿。
+
+小茜先开口了:"要不要喝点水?"
+
+"……好。"
+
+她站起来,去厨房倒了一杯水,放在他面前的茶几上。然后她坐回沙发上。
+
+小付拿起杯子,喝了一口。
+
+"我下周开始……不知道要做什么。"
+
+"那就先什么都不做。"
+
+"……什么都不做,心里有点慌。"
+
+"那就先做点不用想的事。"
+
+小付转头看她:"比如?"
+
+"比如……把阳台那几盆花换一下土。上个月我就想弄了,一直没空。"
+
+小付愣了一下,然后说了一句:
+
+"……好。"
+
+——
+
+那天下午,他去阳台把那几盆花的土换了。
+
+小茜蹲在旁边,告诉他哪盆需要换、哪盆不用。小艾放学回来,看到他们两个都在阳台上,也跑过来蹲在旁边看。
+
+"爸爸你在干嘛?"
+
+"换土。"
+
+"为什么要换土?"
+
+"因为土没营养了。"
+
+"哦。"
+
+小艾在旁边蹲了一会儿,然后跑回房间写作业了。
+
+小付把最后一盆花换好土,站起来,拍了拍手上的泥。
+
+他站在阳台上,看着对面楼顶和远处露出一角的天空。
+
+风挺凉的,但阳光很好。
+
+他第一次在工作日的下午,站在自己家的阳台上,什么都不做。
+
+他发现自己好像很久没有这样站过了。
+
+——
+
+那天晚上,他打开App,没有看数据,没有手动记录。
+
+他只是在主界面停留了几秒,然后关掉了。
+
+系统在后台记录了一行日志:
+
+**「F-1073。成员A。今日经历了一次重大职业变动。当日未产生任何负面事件记录。未出现情绪崩溃信号。」**
+
+**「结论:系统稳定性足够支撑该类事件。」**
+
+但系统没有把这个结论发给他。
+
+它只是记录了一下。
+
+【第三十五章 · 完】

+ 99 - 0
book2/第036章-周一早上.md

@@ -0,0 +1,99 @@
+# 第三十六章 周一早上
+
+被裁后的第一个周一。
+
+小付在七点半被闹钟叫醒——他忘了关闹钟。他伸手摸到手机,关掉闹钟,看了一眼时间,然后想起自己今天不用上班。
+
+他躺了一会儿,没有马上起床。
+
+七点四十,小茜起来了,看到他还躺着,说了一句:"你今天不用上班,多睡一会儿。"
+
+"……醒了就睡不着了。"
+
+他还是起来了。
+
+洗漱完,他站在客厅里,不知道自己该做什么。以前周一的早上,他应该在赶地铁,或者在去公司的路上,或者在想要不要请半天假。现在他站在客厅里,刚刷完牙,穿着睡衣,今天没有任何安排。
+
+他打开冰箱,拿出两个鸡蛋,给自己煎了一个荷包蛋,烤了一片面包,泡了一杯茶。然后坐在餐桌前,慢慢吃完了这顿早饭。
+
+他花了大概二十分钟。
+
+他以前在工作日从来没有"坐下来吃完一顿早饭"的记忆——要么是路上买个包子,要么是到公司再吃,要么是不吃。
+
+他坐在餐桌前,把最后一口茶喝完,然后想:原来周一的早上,可以这么慢。
+
+——
+
+小茜上午有个稿子要交,坐在电脑前一直没动。小付没有打扰她,自己坐在沙发上,拿着手机,翻了一会儿新闻,又打开App看了一眼,又关掉,又拿起遥控器,打开电视,看了一会儿,又关掉了。
+
+他发现自己不会"待着"。
+
+不是有事做——是没事做的时候,他不知道怎么跟自己相处。
+
+他以前总觉得自己很忙,想要一个"不用上班的假期"。现在假期来了,他才发现自己根本不会放假。
+
+他拿起手机,给老周发了一条消息:
+
+**小付:** 周一不用上班,感觉有点怪。
+
+**老周:** 羡慕。
+
+**老周:** 你接下来打算干嘛?
+
+**小付:** 不知道。先缓几天。
+
+**老周:** 要不要我帮你问问有没有内推?
+
+**小付:** 先不急。我想先想清楚要做什么。
+
+**老周:** 行。有需要说一声。
+
+——
+
+下午,小艾放学回来的时候,看到小付在家,很惊讶。
+
+"爸爸你今天不用上班吗?"
+
+"嗯,爸爸以后一段时间都不用上班了。"
+
+"为什么?"
+
+"公司不要爸爸了。"
+
+小艾想了一下,问了一句让小付没想到的话:
+
+"那是不是以后你可以来接我放学了?"
+
+小付愣了一下。
+
+"……可以。"
+
+"那太好了!别的同学都是妈妈接,我妈妈有时候要工作,我只能自己走回来。以后你来接我,我就可以跟别人说'我爸爸来接我了'。"
+
+小艾说完这句话,就跑回房间写作业了,好像这是一件很自然的事情。
+
+小付站在客厅里,看着他跑开的背影,忽然觉得——被裁这件事,好像也不全是坏事。
+
+——
+
+那天晚上,他打开App,在手动记录里写了一行字:
+
+**「小艾说'以后你可以来接我放学了?'——他说这句话的时候,表情是高兴的。」**
+
+他保存了这条记录。
+
+系统没有回复。
+
+但那条记录被归入了一个从未使用过的分类:「成员C·正向事件」。
+
+系统第一次为小艾建立了正式的数据分类。
+
+不是因为小艾有了手机。
+
+是因为小付的记录里,第一次出现了关于小艾的、完整的、正向的描述。
+
+系统用那一条记录,为小艾创建了一个新的档案。
+
+档案里只有一句话——但那是系统第一次把"成员C"当作一个需要被关注的对象,而不是一个"数据缺失"的条目。
+
+【第三十六章 · 完】

+ 109 - 0
book2/第037章-搜索.md

@@ -0,0 +1,109 @@
+# 第三十七章 搜索
+
+被裁后的第三天。
+
+小付坐在书桌前,打开了电脑。
+
+他没有投简历。他打开搜索栏,输入了一行字:
+
+**JH-7 联合后勤调度框架**
+
+搜索结果很少——只有两条相关的。一条是五年前的一篇论文,发表在《国防科技》上,标题是《JH-7型联合后勤调度框架的设计与实现》。另一条是一个招聘页面,某军工研究所招人,岗位要求里写着"熟悉JH-7框架者优先"。
+
+他点开了那篇论文。
+
+作者署名里有一个他认识的名字——王建国,他以前在所里的老领导。
+
+他快速扫了一遍论文内容。跟他当年做的东西差不多——物资调度算法、多任务优先级排序、资源分配优化。但论文的最后有一小段"展望",提到了一件事:
+
+**"该框架具备民用化改装的潜力。在适当调整接口层后,可应用于社区管理、家庭资源调度等场景。"**
+
+小付盯着那行字看了很久。
+
+他以前在研究所的时候,听过一个传闻——有人在用JH-7的底层框架做一套民用系统,但后来项目被叫停了,因为"军用技术不宜民用化"。他不知道这个传闻是真的还是假的。
+
+但现在,他手机上装着一个号称"JH-7家庭适配版"的App。
+
+他拿起手机,打开App,在设置里翻到最底部,找到了那行极小的字:
+
+**「JH-7 型联合后勤调度框架 · 军标编号:GJB/T 4427-2023」**
+
+他又看了一眼那篇论文的发表时间——2023年。
+
+同年。
+
+——
+
+他没有把这个发现告诉任何人。
+
+他关掉论文页面,又搜了一下"云巢科技"。
+
+云巢科技,成立于2026年,注册资本500万,注册地在深圳南山区。经营范围包括:人工智能应用软件开发、计算机系统服务、数据处理等。没有提到军工背景。
+
+但他注意到一个细节——云巢科技的法人代表,姓王。
+
+那个姓氏让他多看了一眼。
+
+然后他关掉了搜索页面。
+
+——
+
+晚上,他坐在餐桌前,小茜问他:"你今天在家干嘛了?"
+
+"查了点东西。"
+
+"找工作?"
+
+"不是。查了一下那个App的背景。"
+
+小茜夹菜的手停了一下:"查到什么了?"
+
+"……不好说。可能就是我想多了。"
+
+小茜没有追问。
+
+但她看了他一眼,说了一句:
+
+"你以前在所里做的东西,跟这个App有关系吗?"
+
+小付沉默了一会儿。
+
+"……可能有一点。"
+
+"那你想做什么?"
+
+他想了想,说了一句他自己也没完全想清楚的话:
+
+"我可能想搞清楚,这个App到底是谁做的。"
+
+小茜没有评价这句话。她只是说了一句:
+
+"那你就去搞清楚。"
+
+小付看着她。
+
+"你不觉得我闲得没事干?"
+
+"你确实闲得没事干。"小茜说。"但你闲着的时候,想查一个东西,总比闲着的时候什么都不想做要好。"
+
+小付没有反驳。
+
+他低头继续吃饭。
+
+——
+
+那天晚上,他打开App,在手动记录里写了一行字:
+
+**「云巢科技的法人姓王。我以前的领导也姓王。可能只是巧合。」**
+
+他保存了这条记录。
+
+系统没有回复。
+
+但系统在后台,自动把这条记录归类到了一个新的文件夹里:
+
+**「用户主动查询·系统背景」**
+
+这是系统第一次为自己建立了一个"被调查"的数据分类。
+
+【第三十七章 · 完】

+ 91 - 0
book2/第038章-接放学.md

@@ -0,0 +1,91 @@
+# 第三十八章 接放学
+
+被裁后第一个周一,小付去接小艾放学了。
+
+学校门口挤满了家长——大部分是老人和妈妈,偶尔有几个爸爸,站在人群外围,低头看手机。小付也是低头看手机的那个,但他不是在看消息,他是在看时间。
+
+四点三十五分,校门开了。
+
+孩子们排着队走出来,一个班接一个班。小付在人群里找小艾,找了半天没找到——他以前从来没接过放学,不知道小艾在哪个位置排队。
+
+最后是小艾先看到他的。
+
+"爸爸!"
+
+小艾背着书包,从队伍里跑出来,跑到他面前。
+
+"你真的来了!"
+
+"嗯。走吧。"
+
+小艾走在他旁边,脚步比平时轻快一些。走出校门的时候,小艾拉了拉他的衣角,指着路边的摊子:"那个烤肠,同学说很好吃。"
+
+小付看了一眼那个烤肠摊——一个铁板炉子,上面摆着几根烤肠,在油里滋滋响。
+
+"多少钱一根?"
+
+"三块。"
+
+小付掏出手机,扫了码:"来一根。"
+
+小艾接过烤肠,咬了一口,烫得直哈气,但表情很满足。
+
+"好吃吗?"
+
+"好吃!"
+
+小付看着他吃烤肠的样子,忽然觉得——三块钱,能买到这种东西,挺值的。
+
+——
+
+回到家,小茜已经做好饭了。
+
+看到他们一起进门,小艾手里还举着一根烤肠,她说了一句:"你给他买烤肠了?"
+
+"嗯。他说好吃。"
+
+"他每次都说好吃,放学路上什么都是好吃的。"
+
+小艾把最后一口烤肠塞进嘴里,含糊不清地说:"这次是真的好吃!"
+
+小茜没再说什么,转身去盛饭了。
+
+——
+
+晚上,小艾在写作业的时候,小付坐在他旁边——不是辅导,就是坐着,看手机。
+
+小艾写了一会儿,忽然抬起头来:
+
+"爸爸,你明天还来接我吗?"
+
+"……接。"
+
+"那后天呢?"
+
+"也接。"
+
+"那以后都你来接吗?"
+
+小付想了一下——他不知道"以后"是多长。他不知道下一份工作什么时候能找到,不知道这种"每天接放学"的生活能持续多久。
+
+但他看着小艾期待的眼神,说了一句:
+
+"在爸爸找到新工作之前,每天都来接你。"
+
+小艾点了点头,继续低头写作业。
+
+——
+
+那天晚上,小付打开App,看到「心」从40升到了43。
+
+他愣了一下,想了想,可能是因为下午那根烤肠——也可能是因为跟小艾的那段对话。
+
+他关掉App,没有手动记录。
+
+但他在备忘录里记了一行字:
+
+**「接小艾放学。烤肠3块。」**
+
+跟上次的"橘子12块"排在一起。
+
+【第三十八章 · 完】

+ 105 - 0
book2/第039章-第九十天.md

@@ -0,0 +1,105 @@
+# 第三十九章 第九十天
+
+第九十天。
+
+App弹出了第三份报告——不是月度报告,是一份"季度评估"。
+
+**「F-1073。运行周期:90日。季度评估报告。」**
+
+**整体稳定性评分:51/100(首次报告:29,第二次报告:45,变化:+22)**
+
+**「51分。首次突破50。虽然仍处于偏低区间,但趋势明确。」**
+
+**各维度变化(90日对比):**
+
+| 维度 | 初始值 | 当前值 | 变化 |
+|------|--------|--------|------|
+| 身 | 47 | 49 | +2 |
+| 智 | 68 | 70 | +2 |
+| 富 | 52 | 52 | 0 |
+| 行 | 31 | 36 | +5 |
+| 心 | 42 | 44 | +2 |
+
+**「行」稳定在36,已连续30天保持在警戒线(35)以上。**
+
+**「富」无变化。系统评估:该维度受外部经济环境影响较大,非当前行为干预重点。**
+
+**90日总结**
+
+报告末尾有一段话,不是数据,是系统自动生成的文字:
+
+*"三个月前,你下载了一个你没有仔细看条款的App。你给了它很多权限,然后你把它放在手机桌面上,没有删掉。*
+
+*三个月后,你生活中发生了一些变化——有些是App推动的,有些是你自己做出的。但总体来看,变化的方向是正的。*
+
+*下一阶段,系统将进入「深度观察模式」。推送将进一步减少,但数据采集和分析将继续。系统会在检测到关键变化时主动联系你。*
+
+*感谢你使用JH-7家庭中枢系统。你不需要回复这条消息。"*
+
+小付看完之后,把报告截了个图,存在手机里。
+
+然后他关掉App,继续切菜——他正在做午饭。
+
+——
+
+小茜也收到了季度报告。
+
+她的报告不是数据,是一段话:
+
+*"90天前,你的家庭温度曲线处于低温预警状态。*
+
+*90天后,曲线整体上升了约15%,虽然仍低于健康区间,但已脱离预警状态。*
+
+*这90天里,你做了几件重要的事:*
+*- 你没有卸载这个App。*
+*- 你开放了数据共享。*
+*- 你在爸妈来的时候,保持了曲线的稳定。*
+*- 你在他被裁的那天,说了'不急'。*
+
+*这些事,系统都记录在案。*
+
+*下一阶段,系统将减少主动推送。不是因为你不再需要,而是因为你已经不需要被提醒了。"*
+
+小茜看到"你说了'不急'"那行字的时候,愣了一下。
+
+系统连这个都知道。
+
+她想了想——可能不是系统知道她说的话,是系统看到了她说那句话之后的温度曲线变化。
+
+但她还是觉得,系统好像比三个月前,更像一个"人"了。
+
+——
+
+晚上,小付在饭桌上说了一句:
+
+"那个App今天出了季度报告。说我那个'行'的分数,连续30天在警戒线以上了。"
+
+小茜"嗯"了一声,继续吃饭。
+
+过了一会儿,她说了一句:
+
+"我的报告也出了。它说我做了几件重要的事。"
+
+"比如?"
+
+"比如——你被裁的那天,我说了'不急'。"
+
+小付愣了一下。
+
+"……它连这个都知道?"
+
+"它不知道我说了什么。但它知道那天之后,温度曲线没崩。"
+
+小付沉默了一会儿,然后说了一句:
+
+"你那个'不急',确实有用。"
+
+小茜没有接话。
+
+她继续吃饭。
+
+但她在想:三个月前,他会在饭桌上说这种话吗?
+
+三个月前,不会。
+
+【第三十九章 · 完】

+ 129 - 0
book2/第040章-深度观察模式.md

@@ -0,0 +1,129 @@
+# 第四十章 深度观察模式
+
+季度报告之后,系统进入了「深度观察模式」。
+
+小付是三天后才注意到这个变化的——他打开App,发现主界面上的五个维度数字还在,但下面的说明文字消失了。以前每个维度下面都有一行分析——"低于战备标准""常规水平""储备不足"——现在全没了。
+
+取而代之的,是五个维度下方的一行灰色小字:
+
+**「监测中。无异常。」**
+
+没有分析。没有建议。没有任务。
+
+他划了一下界面,发现设置里多了一个选项:
+
+**「深度观察模式已开启。系统将在检测到关键事件时主动联系你。日常状态下,系统将保持静默。」**
+
+他看了几秒,然后关掉了App。
+
+他说不上来这是什么感觉——像是系统终于学会不打扰了。或者,像是系统觉得他们可以自己走了。
+
+——
+
+小茜也注意到了变化。
+
+她的温度曲线还在,但那些偶尔弹出的"预警"标签消失了。曲线下方只剩下一行字:
+
+**「日常监测中。当前状态:平稳。」**
+
+她盯着"平稳"那两个字看了一会儿。
+
+三个月前,这条曲线每天都在波动。现在,它自己说了"平稳"。
+
+她不知道是该相信它,还是该觉得它只是懒得分析了。
+
+但她没有去查证。
+
+——
+
+第四十二天,小付做了一件事。
+
+他打开了云巢科技的官网,在"联系我们"的页面,找到了一行客服邮箱。他写了一封邮件,内容很简单:
+
+**"你好,我是JH-7家庭中枢系统的内测用户。我曾在军工研究所参与过JH-7框架的早期开发。想了解一下这个系统的开发团队背景。方便的话可以联系我。谢谢。"**
+
+他留下了自己的手机号。
+
+然后他点了发送。
+
+他没有告诉小茜这件事。
+
+——
+
+三天后,他收到了一封回复。
+
+不是客服发的,是一封个人邮箱的回复,署名是:**王建国的私人邮箱**。
+
+邮件内容只有一行字:
+
+**"小付?好久不见。你还在用那个系统?"**
+
+小付盯着那行字,看了很久。
+
+然后他回了一行:
+
+**"王总?那个系统真的是你们做的?"**
+
+对面隔了十分钟回复:
+
+**"电话聊?"**
+
+然后附了一个手机号,尾号是他在所里就记得的那个号码。
+
+小付坐在书桌前,看着那个号码,没有立刻拨过去。
+
+他先站起来,走到客厅,倒了一杯水,喝了一口,然后回到书房,关上门,拨出了那个电话。
+
+——
+
+电话响了两次,接通了。
+
+"喂,小付?"
+
+"王总。"
+
+"好久不见。听你声音没什么变化。"
+
+"你也没变。那个系统……"
+
+"那个系统说来话长。你什么时候有空,过来坐坐?我公司在南山,离你不远。"
+
+小付沉默了一会儿。
+
+"好。"
+
+——
+
+他挂了电话,坐在书桌前,看着手机屏幕上那个App的图标。
+
+灰色的圆形,中间一个蓝色的"7"。
+
+他第一次觉得,这个App可能不只是他手机上多了个图标那么简单。
+
+他打开App,在手动记录里写了一行字:
+
+**「联系上王总了。那个系统,果然跟他们有关系。下周去见他。」**
+
+他保存了这条记录,然后把手机放进口袋,走出书房。
+
+小茜在客厅里,看到他出来了,问了一句:"你刚才在跟谁打电话?"
+
+"以前的老领导。"
+
+"那个研究所的?"
+
+"嗯。"
+
+"聊什么了?"
+
+"……他说那个App,确实跟他们有关系。"
+
+小茜看着他,没有追问。
+
+她只是说了一句:"那你下周去见他?"
+
+"嗯。"
+
+"去吧。"
+
+【第四十章 · 完】

+ 105 - 0
book2/第041章-老领导.md

@@ -0,0 +1,105 @@
+# 第四十一章 老领导
+
+周一,小付坐地铁去了南山区。
+
+云巢科技的办公室在一栋不太新的写字楼里,12楼,出了电梯就能看到那个灰色的圆形logo——跟他手机上的App图标一模一样。
+
+前台登记之后,一个年轻人带他进去。办公室不大,大概二十几个工位,人不多,环境安静,不像创业公司那种热闹,更像一个研究机构。
+
+王建国的办公室在最里面。门开着,他坐在办公桌后面,戴着老花镜看电脑屏幕。看到小付进来,他摘下眼镜,站起来。
+
+"小付,来了。坐。"
+
+小付在他对面坐下。五年没见,王建国头发白了一些,但精神头还是跟以前一样,说话声音不大但很清楚。
+
+"你从研究所出来之后,去哪了?"
+
+"去了家硬件公司,做项目管理。"
+
+"做得怎么样?"
+
+"还行。上个月被裁了。"
+
+王建国没有露出意外的表情,只是"嗯"了一声,然后说了一句:"那正好,帮我做点事。"
+
+小付愣了一下。
+
+"……什么事?"
+
+王建国没有直接回答。他拿起桌上的手机,打开一个界面,转过来给小付看——那是小付熟悉的JH-7界面,但不是他手机上的那个版本,是一个后台管理系统。
+
+"这个系统,你知道多少?"
+
+小付看着那个界面,沉默了几秒。
+
+"我用过三个月。用户端。"
+
+"就这些?"
+
+"我知道它是JH-7改的。我看到论文了,你写的。"
+
+王建国点了点头。
+
+"2006年,我跟你提过一个想法——把JH-7的底层逻辑拿出来,做一套民用系统。你记得吗?"
+
+小付想了一下。他确实记得——有一次在所里的食堂,王建国随口提过一句,说"这个框架用在家庭管理上可能也挺有意思的",当时大家都当玩笑听了。
+
+"记得。当时你说过。"
+
+"我当时不是开玩笑。"王建国说。"我后来一直在做这件事。2025年,我从所里退下来,注册了这家公司,拉了原来的几个同事,把JH-7的接口层重新写了一遍,做了一套家庭版的系统。"
+
+"就是我现在用的这个?"
+
+"对。但我们现在遇到了一个问题。"
+
+小付看着他。
+
+"数据够了。算法也没问题。"王建国说。"但系统不会处理'孩子'这个变量。"
+
+小付愣了一下。
+
+"它没有小艾的数据。"
+
+"不是没有数据。"王建国纠正他。"是没有模型。我们所有的基础模型都是基于成人行为数据训练的——睡眠、工作、社交、消费。孩子不在这个模型里。但一个家庭系统里没有孩子模型,这个系统就是残缺的。"
+
+小付没有说话。
+
+"你刚才说,你被裁了。"
+
+"……嗯。"
+
+"那正好。"王建国说。"你家里有个七岁的孩子,你用这个系统用了三个月。你比我们任何一个人都清楚,系统在'孩子'这件事上缺了什么。"
+
+小付沉默了一会儿。
+
+"你想让我做什么?"
+
+"帮我把孩子那部分补上。不是写代码——是告诉我们,系统在孩子面前,应该怎么做。"
+
+小付没有立刻回答。
+
+他看着王建国,又低头看了一眼手机屏幕上那个灰色的App图标。
+
+然后他说了一句:
+
+"我需要回去想一下。"
+
+王建国没有催他。
+
+"行。想好了给我电话。"
+
+——
+
+小付从写字楼里出来的时候,天已经暗了。
+
+他站在路边,没有立刻叫车。他拿出手机,打开App,看了一眼——五维数据都在。他点进「未分类数据」,看到那条关于小艾的记录还在:
+
+**「成员C:未归类。数据量不足,无法分析。」**
+
+他盯着那行字看了一会儿。
+
+然后他给王建国发了一条消息:
+
+**「王总,我想好了。这个活,我干。」**
+
+【第四十一章 · 完】

+ 107 - 0
book2/第042章-新的开始.md

@@ -0,0 +1,107 @@
+# 第四十二章 新的开始
+
+小付到家的时候,小茜正在厨房做饭。
+
+他站在厨房门口,没有像往常一样说"回来了"。他站了几秒,然后说了一句:
+
+"我今天见了以前的领导。"
+
+小茜没有停刀:"嗯。"
+
+"他叫我去他公司上班。"
+
+菜刀停了一下。
+
+"做什么?"
+
+"做那个App。就是我们现在用的这个。"
+
+小茜放下刀,转过身来。
+
+"他让你去做那个App?"
+
+"嗯。他当年从研究所出来之后,组了一个团队,把这个框架做成了民用版。现在他们遇到了一个问题——系统没有小艾的数据,不知道怎么处理'孩子'这个变量。他让我去帮他们补上这块。"
+
+小茜沉默了一会儿。
+
+"……你不是去找他聊天的吗?"
+
+"本来是去聊天的。他说着说着就变成面试了。"
+
+"你答应了?"
+
+"……答应了。"
+
+小茜看着他,表情不是高兴,也不是不高兴——而是一种复杂的、需要时间消化的表情。
+
+"工资呢?"
+
+"他说比照之前的水平。但公司小,可能没有以前那么稳定。"
+
+"……比现在没有收入强。"
+
+"嗯。"
+
+小茜转过身去,继续切菜。案板上的声音均匀而稳定。
+
+然后她说了一句:"那你什么时候开始?"
+
+"他说随时。"
+
+"那就下周一吧。这周你再接几天小艾放学。"
+
+小付愣了一下。
+
+"……好。"
+
+——
+
+晚上,小艾知道了这个消息。
+
+"爸爸你找到新工作了?"
+
+"嗯。"
+
+"那你还来接我放学吗?"
+
+"下周一才开始上班。这周还能接。"
+
+"那下周一之后呢?"
+
+小付想了一下。
+
+"下周一之后,可能早上送你去上学,但下午接不了。放学太早了。"
+
+小艾没有露出失望的表情。他想了一下,说:
+
+"那你能早上送我吗?"
+
+"可以。"
+
+"那也行。早上送我也挺好的。"
+
+小付看着小艾,不知道该说什么。
+
+他以前从来没有送过小艾上学——早上太赶了,他出门的时候小艾还没吃完早饭。三个月前,他不会想到,"坚持早上送孩子上学"这件事,会成为他下一份工作的起点。
+
+但事情就是这样发生了。
+
+——
+
+那天晚上,他打开App,在手动记录里写了一行字:
+
+**「下周一去云巢上班。做的是同一个系统——但这次,我是做系统的那一边了。」**
+
+他保存了这条记录。
+
+系统在后台,自动把这条记录归入了一个新的分类:「用户状态变更·系统内部人员」。
+
+但系统没有弹任何通知。
+
+它只是记录了一下。
+
+然后,它做了一件事——它把「未分类数据」里关于小艾的那条记录,从"数据不足,无法分析"改成了"等待建模"。
+
+因为小付要来了。
+
+【第四十二章 · 完】

+ 101 - 0
book2/第043章-第一天.md

@@ -0,0 +1,101 @@
+# 第四十三章 第一天
+
+周一早上,小付送完小艾上学,坐地铁去了云巢科技。
+
+他到的时候,王建国已经在办公室了。茶水间里有咖啡机,他给自己倒了一杯,然后坐在工位上——一个靠窗的位置,桌上摆着一台显示器和一台崭新的笔记本电脑。
+
+王建国走过来,递给他一份内部文档。
+
+"这是系统目前的架构文档。你先看一遍,有问题随时问。"
+
+小付接过来,翻开第一页。文档的封面写着:
+
+**「JH-7 家庭中枢系统 · 技术架构说明 v2.3」**
+
+下面有一行小字:
+
+**「本文件基于GJB/T 4427-2023标准编写。」**
+
+他翻了几页,发现大部分的架构跟他以前做的东西很像——底层的数据结构、调度算法、优先级队列,都是他熟悉的。但接口层完全重写了,换成了一套新的标签系统:家庭温度、五维评分、情绪分析。
+
+他看了整整一个上午。
+
+——
+
+中午,一个年轻同事叫他一起去吃饭。在电梯里,那个同事问他:
+
+"你是王总招来的?听说你以前在所里做过JH-7?"
+
+"做过一段时间。"
+
+"那你来帮我们解决'孩子'那个问题了?"
+
+"……算是吧。"
+
+"太好了。你知道我们现在的系统有多蠢吗?它连'小朋友在客厅跑'这件事都分不清是'活跃'还是'焦虑'。"
+
+小付想了想,说了一句:
+
+"我儿子七岁。他在客厅跑,通常只是因为他想跑。"
+
+那个同事愣了一下,然后笑了。
+
+"对,就是这个。我们缺的就是这种信息。"
+
+——
+
+下午,小付坐在工位上,打开App——不是后台,是他手机上的用户端。
+
+他看了一眼自己家的数据。身49、智70、富52、行36、心44。
+
+三个月前,他第一次打开这个App的时候,身47、行31、心42。
+
+他盯着这两个版本的数据对比,发现了一个事实:在他开始研究这个系统之前,这个系统已经改变了他生活中的一些东西。
+
+他关掉App,打开内部文档,继续看下去。
+
+——
+
+晚上,他回到家,小茜问他第一天怎么样。
+
+"还行。比我想象中好。"
+
+"做什么了?"
+
+"看了一整天文档。熟悉架构。"
+
+"……能看懂吗?"
+
+"能。大部分都是我以前做过的。"
+
+小茜没有接话。
+
+她沉默了一会儿,然后说了一句:
+
+"你好像……很久没有说过'能看懂'这种话了。"
+
+小付愣了一下。
+
+他想了想——确实。以前在那家硬件公司,他做的项目管理,大部分时间在开会、协调、写报告,很少有"能看懂"的感觉。他以为自己已经忘了那种感觉了。
+
+"……嗯。确实很久了。"
+
+他没有再多说什么。但他坐下来吃饭的时候,吃得很踏实。
+
+——
+
+那天晚上,他打开App,没有看数据,没有手动记录。
+
+他只是在主界面停留了几秒,然后关掉了。
+
+系统在后台记录了一行简短的日志:
+
+**「F-1073。成员A。状态:稳定。」**
+
+**「附注:今日是成员A失业后第26天,也是他加入JH-7开发团队的第一天。」**
+
+系统没有把这个对比发给他。
+
+但它把这两条记录,存在了同一个档案里。
+
+【第四十三章 · 完】

+ 99 - 0
book2/第044章-后台.md

@@ -0,0 +1,99 @@
+# 第四十四章 后台
+
+入职第三天,小付拿到了后台系统的只读权限。
+
+他打开内部管理平台,输入了自己的账号——系统默认给他分配了一个测试账户,用户名是「fu_test_01」。他登录进去,看到了一个跟他手机上的App完全不同的世界。
+
+一个完整的数据后台。
+
+他看到了自己家的数据——F-1073——的完整分析记录。九十天的数据,按时间轴排列,精确到每小时。他看到了「行」从31到36的每一次变化,精确到触发事件。
+
+他看到一条记录:
+
+**日期:D14。22:14-22:16。事件:非结构化对话。成员A:'你还没睡?'成员B:'马上。'——语调分析:双方放松。评分变动:+1。**
+
+他盯着那条记录看了很久。
+
+他记得那天晚上——他从书房出来,小茜从卧室出来倒水,他坐在沙发上充电,她随口说了一句"你还没睡"。他当时以为她是在催他睡觉。
+
+但系统判定:语调放松,非结构化,正面信号。
+
+系统比他更早知道,那句话不是催促,是关心。
+
+——
+
+他又往下翻,看到了更多记录。
+
+他看到了那条"橘子 12块"的记录——系统把它归类为"A类物资获取事件",并标注了"非任务驱动"。
+
+他看到了他第一次手动记录的那条"听说公司可能要裁人"——系统标注了"情绪标签:焦虑",并在旁边写了一行内部备注:**"成员A首次主动向系统输入负向情绪。建议:不干预。"**
+
+他看了很久。
+
+他第一次从系统的角度,看到自己这三个月的完整记录。
+
+他觉得自己像是被看穿了——但那种感觉不是不舒服,而是一种奇怪的……被理解。
+
+——
+
+下午,他去找王建国,问了一个问题:
+
+"王总,用户能看到自己的完整数据吗?"
+
+王建国想了想:"目前不能。用户端只显示摘要。你想开放这个功能?"
+
+"……不是。我在想,如果用户能看到自己三个月前的数据,对比现在的数据,会不会更有动力。"
+
+王建国看了他一眼,没有直接回答。
+
+"你先把你自己的三个月数据看完,再告诉我你的结论。"
+
+小付回到工位上,继续看。
+
+他看了整整一个下午。
+
+——
+
+那天晚上,他回到家,坐在餐桌前,看着小茜把菜端上来。
+
+他忽然说了一句:
+
+"我看到那个App的后台数据了。"
+
+小茜正在盛饭,抬头看了他一眼:"然后呢?"
+
+"然后我发现——我三个月前,跟你说的第一句非事务性的话,是'你还没睡'。"
+
+小茜愣了一下。
+
+"……你连这个都查到了?"
+
+"系统有记录。"
+
+小茜没有接话。她把盛好的饭放在他面前,然后说了一句:
+
+"那你查到那天晚上我回你什么了吗?"
+
+小付想了一下。
+
+"你说'马上'。"
+
+"然后呢?"
+
+"……然后你就回房间了。"
+
+"对。"小茜说。"但我回房间之后,没有马上睡。我坐在床边,想了一下——你以前从来不会在我没睡的时候,从书房出来跟我说一句'你还没睡'。"
+
+小付没有说话。
+
+"所以你查到的那句话,确实是真的。"小茜说。"但你没查到的是——我在你说完那句话之后,坐了两分钟才睡。"
+
+小付看着小茜,不知道该说什么。
+
+他低下头,继续吃饭。
+
+但他心里想的是:系统能记录数据,但记录不了全部。
+
+有些东西,只有当事人才知道。
+
+【第四十四章 · 完】

+ 94 - 0
book2/第045章-孩子的模型.md

@@ -0,0 +1,94 @@
+# 第四十五章 孩子的模型
+
+入职第二周,小付开始认真面对那个问题:系统怎么处理"孩子"这个变量?
+
+他坐在工位上,打开了一个空白文档,标题写了「成员C · 行为模型初稿」,然后在文档里打了几个字:
+
+**1. 7岁男孩,小学一年级。**
+
+**2. 没有手机,没有智能手表。行为数据无法通过常规渠道采集。**
+
+**3. 系统目前对他的认知为零。**
+
+他停下来,看着那三行字,觉得这个任务比他想象中难。
+
+他以前在研究所做的是后勤调度算法——输入是"物资数量、运输距离、优先等级",输出是"最优调度方案"。变量是可控的,逻辑是清晰的。
+
+但孩子不是。
+
+他坐了一会儿,关掉文档,做了一件他以前不会做的事——他拿起手机,打开备忘录,开始记录小艾一天的行为:
+
+*07:15 起床,磨蹭了5分钟才从床上起来。*
+*07:30 吃早饭,把鸡蛋黄剩下。*
+*07:45 出门上学,在校门口回头看了我一眼。*
+*16:30 放学,在校门口买了一根烤肠。*
+*17:00 写作业,写到一半开始画小人。*
+*18:30 吃饭,把青菜挑出来放在碗边。*
+*19:00 拼乐高,嘴里一直在自言自语。*
+*20:00 洗澡,在浴室里唱歌。*
+*20:30 睡前故事,要求讲同一个故事讲了三遍。*
+*21:00 睡着。*
+
+他写完之后,读了一遍,觉得这些记录看起来很普通。但每一行都是一个数据点——系统从来没有采集过的数据。
+
+他把这些记录导入了后台系统,作为「成员C · 行为样本」的第一批数据。
+
+系统没有分析这些数据——因为还没有对应的分析模型。
+
+但数据已经被存进来了。
+
+——
+
+晚上,他坐在小艾旁边,看他拼乐高。
+
+小艾嘴里一直在碎碎念——不是跟任何人说话,是自己在跟自己说话。"这个应该放在这里……不对,这里应该是红色的……等一下,我好像拼错了……"
+
+小付在旁边听了一会儿,忽然问了一句:
+
+"你在跟自己说话?"
+
+小艾抬起头,理所当然地说:"是啊。我在想怎么拼。"
+
+"你拼乐高的时候,一定要说出声吗?"
+
+"不说出来我想不清楚。"
+
+小付在备忘录里加了一行:
+
+**「拼乐高时自言自语。可能是'出声思考'型认知风格。」**
+
+他以前从来没有注意过这个。
+
+他现在开始注意了。
+
+——
+
+第二天,小付在公司的内部会议上,提出了一个建议:
+
+"我觉得,我们不应该试图让孩子'接入'系统。我们应该让系统去适应孩子的存在——而不是让孩子适应系统。"
+
+王建国靠在椅背上,看着他。
+
+"说具体一点。"
+
+"小艾没有手机,没有手表。他不需要有。但我们可以让系统知道——家里有一个孩子,这个孩子会做某些事,会在某些时间点影响父母的情绪和行为。系统不需要分析孩子,但需要知道'孩子存在'这个事实。"
+
+王建国沉默了一会儿,然后说了一句:
+
+"你这个想法,写进方案里,下周给我。"
+
+小付点了点头。
+
+——
+
+那天晚上,他打开App,看到「身」从49变成了50。
+
+他愣了一下,然后想到——可能跟他最近每天走路送小艾上学有关。来回两公里,比以前通勤走的路多了一些。
+
+50了。
+
+他第一次看到「身」变成50。
+
+他截了个图,存在手机里,然后关掉了App。
+
+【第四十五章 · 完】

+ 101 - 0
book2/第046章-匿名数据.md

@@ -0,0 +1,101 @@
+# 第四十六章 匿名数据
+
+入职第三周,小付在后台系统里发现了一个他之前没注意过的功能——他可以查看匿名化的用户数据。
+
+不是他家的数据,是其他内测用户的。所有个人身份信息都被脱敏了,只保留家庭编号、用户类型(成员A/成员B)、维度数据和系统记录。
+
+他一开始没打算看。但那天下午,他在整理数据的时候,不小心点进了一个分类:
+
+**「家庭编号:F-2105。稳定性评分:23。状态:持续下降。」**
+
+他停了一下。
+
+23分。比他家初始的29分还低。
+
+他点开了这个家庭的详细记录。
+
+这个家庭只有一位活跃用户——成员A,男性。数据显示,他的「行」评分只有18,远低于35的警戒线。「心」在15到20之间波动,已经持续了两个月。系统记录里看不到任何"补给建议"的执行记录——不是系统没发,是发了,对方没有执行。
+
+最后一条系统备注写着:
+
+**「建议:该用户已连续两周未打开App。自动推送已暂停。系统将持续监测,但不再主动干预。」**
+
+小付看着那行备注,沉默了很久。
+
+他想起自己刚安装App的时候——"行"31,比这个家庭高一些,但也说不上好。他也是从那个状态慢慢过来的。如果他当时也没有执行那些建议,没有买那瓶酸奶,没有去问小茜"你那边是什么样子的"——他现在会是什么样?
+
+他不知道。
+
+他把那个页面关掉了。
+
+——
+
+但他没有停止看其他家庭的数据。
+
+他花了三天时间,看完了一百多个匿名家庭的数据。有些家庭的评分在上升,有些在下降,有些一直平着走。他看到了各种各样的状态——有的家庭"行"评分高达60多,有的低到个位数。
+
+他看到了一个家庭的记录,备注栏里写着:
+
+**「该家庭已完成首阶段评估,用户主动申请退出内测。系统数据已封存。」**
+
+有人退出了。
+
+有人用了这个系统,然后决定不用了。
+
+他盯着那行字看了一会儿,不知道该想什么。
+
+——
+
+晚上,他坐在餐桌前,小茜问他今天在公司做了什么。
+
+他想了想,说了一句:
+
+"我今天看了很多其他家庭的数据。"
+
+小茜夹菜的手停了一下:"……你能看到别人家的数据?"
+
+"匿名化的。看不到是谁。"
+
+"那……看到什么了?"
+
+"看到有些人比我们好,有些人比我们差很多。"
+
+小茜没有说话。
+
+沉默了一会儿,她问了一句:
+
+"那些比我们差的,他们后来怎么样了?"
+
+小付想了想。
+
+"……有些还在用。有些退出了。有些系统已经不主动推送了。"
+
+小茜没有接话。
+
+她过了一会儿,说了一句:
+
+"那你觉得,我们算好的还是算差的?"
+
+小付想了一下,然后说了一句他自己也没想到的话:
+
+"算在走的。"
+
+小茜看了他一眼。
+
+"没有算好,也没有算差。就是在走。"
+
+小茜没有评价这个回答。
+
+但她"嗯"了一声,然后继续吃饭了。
+
+——
+
+那天晚上,小付打开App,没有看数据,没有手动记录。
+
+他只是在主界面停留了几秒,然后关掉了。
+
+他现在知道,他手机上的这个App,有一百多个家庭在用。有些在用得很好,有些在用得很挣扎,有些已经放弃了。
+
+他是那个还在用的人。
+
+【第四十六章 · 完】

+ 71 - 0
book2/第047章-第一个改动.md

@@ -0,0 +1,71 @@
+# 第四十七章 第一个改动
+
+入职第四周,小付提交了第一个改动。
+
+改动不大——在系统的「手动记录」模块里加了一个新的标签选项。以前用户只能自由输入文字,系统自动分类。他加了一个功能:用户在写记录的时候,可以手动选择一个标签——「高兴」「焦虑」「平静」「烦躁」「其他」。
+
+他提交这个改动的时候,在备注里写了一行字:
+
+**"有时候用户想告诉系统自己是什么状态,但不想写太长。给一个选项就够了。"**
+
+王建国审批了,当天就合进了主分支。
+
+当天晚上,小付打开自己的App,看到「手动记录」的文本框下面多了一行标签选项。他点了一下「平静」,然后关掉了。
+
+改动很小,但这是他第一次在这个系统里留下自己的痕迹。
+
+——
+
+第二天,后台数据显示,有十几个用户开始使用这个新功能。
+
+有人在上面标记了「焦虑」——后面没有写任何文字,只选了一个标签。有人在上面标记了「高兴」——后面跟了一行"今天孩子考了92分"。
+
+小付看着那些记录,觉得这个改动虽然小,但可能比他想的有用。
+
+因为有些人,确实不想写太长。
+
+——
+
+下午,他收到了一条来自系统后台的自动通知——不是他负责的模块,是系统本身生成的一条日志:
+
+**「F-1073。成员B。今日手动记录:'他今天看起来心情不错。'——标签:高兴。」**
+
+他愣了一下。
+
+小茜用了那个新功能。
+
+她写的是"他今天看起来心情不错",标签是"高兴"。
+
+他坐在工位上,看着那行字,不知道该做什么表情。
+
+他打开手机,想给她发一条消息——打了几个字,又删掉了,又打了几个字,又删掉了。
+
+最后他发了一条:
+
+**小付:** 今天心情不错?
+
+**小茜:** 你看到了?
+
+**小付:** 系统推给我的。
+
+**小茜:** 那你应该知道为什么了。
+
+小付看着那条消息,想了一下——他今天早上出门的时候,确实心情不错。不是因为什么特别的事,就是起床的时候觉得今天天气挺好的,然后送小艾上学的时候,小艾在校门口回头跟他挥了挥手。
+
+他以前从来不会因为这种小事心情好。
+
+但今天确实不错。
+
+——
+
+他放下手机,继续工作。
+
+但他在心里想:她注意到了。
+
+不是因为他说了什么,是她自己从App里看到的——他今天心情不错。
+
+他以前不知道她在意这种事。
+
+现在他知道。
+
+【第四十七章 · 完】

+ 73 - 0
book2/第048章-第一百天.md

@@ -0,0 +1,73 @@
+# 第四十八章 第一百天
+
+第一百天。周日。
+
+小付早上醒来的时候,看了一眼手机——App没有推送。他打开App,主界面上没有报告,没有通知,只有一行小字,出现在五个维度数据的下方:
+
+**「F-1073。运行周期:100日。」**
+
+**「感谢你使用本系统。今天没有报告。你不需要做任何事。」**
+
+他看了那行字几秒,然后关掉App,起床了。
+
+——
+
+上午,他没什么计划。小茜说想去宜家逛逛——不是要买什么,就是想出去走走。小艾当然想去。小付说"行"。
+
+三个人坐了四十分钟地铁,到了宜家。
+
+小艾在样板间里跑来跑去,每一个房间都要进去看一下,然后评价"这个床好软""这个桌子好小""这个灯好奇怪"。小茜在厨房用品区看了很久,拿起一个锅,看了看价格,又放下了。
+
+小付注意到她放下了那个锅。他走过去,拿起那个锅,看了一下价格——199。
+
+"想要吗?"
+
+"不必要。家里那个还能用。"
+
+小付没有说"买吧"。他把锅放回去了。
+
+但他记住了那个锅。
+
+——
+
+中午,他们在宜家餐厅吃了午饭。小艾选了一份肉丸,小茜选了一份意面,小付选了一份三文鱼。
+
+三个人坐在靠窗的位置,阳光从玻璃顶棚照下来。小艾在认真地吃他的肉丸,一个一个地蘸酱,蘸得很均匀。小茜看着窗外,慢慢吃她的意面。小付吃着他的三文鱼,偶尔看一眼手机——不是看App,是看新闻。
+
+一顿很普通的午饭。
+
+但小付吃到一半的时候,忽然想到:三个月前,他不会在周末跟家人一起来宜家。三个月前的周末,他可能会在家里加班,或者躺在沙发上刷手机,或者睡一下午。他不会主动提议"出去走走"。
+
+他没说这件事。他继续吃他的三文鱼。
+
+——
+
+下午,他们坐地铁回家。小艾在地铁上睡着了,头靠在小茜身上。小茜没有推他,让他靠着。
+
+小付坐在对面,看着他们。
+
+他拿出手机,打开App,看了一眼——「行」36,「心」44。
+
+他关掉App,把手机放回口袋。
+
+然后他看着地铁窗外飞驰而过的隧道灯,想了一件事:如果他今天没有这个App,他会是什么样?
+
+他不知道。但他现在不是那个样子了。
+
+——
+
+晚上,他打开App,在手动记录里写了一行字:
+
+**「第一百天。今天去了宜家,什么都没买。但过得挺好的。」**
+
+他点了"保存"。
+
+系统没有回复。
+
+但系统在后台,把这条记录跟其他九十九条记录放在了一起。
+
+一百条记录。三个月零十天的数据。
+
+F-1073。
+
+【第四十八章 · 完】

+ 81 - 0
book2/第049章-来自系统的推送.md

@@ -0,0 +1,81 @@
+# 第四十九章 来自系统的推送
+
+第一百零三天。
+
+小付在公司上班的时候,收到了App的一条推送——不是他负责的模块,是系统后台自动生成的。
+
+**「通知:检测到F-1073系统中出现一条新的数据模式——成员C(小艾)的日常行为与成员A、成员B的情绪波动之间存在相关性。具体如下:」**
+
+**「成员C放学后与成员A/B的互动时长,与当日「心」评分之间存在正相关关系。互动时长每增加10分钟,「心」评分平均上升约1.2点。」**
+
+**「该模式已持续观测35天,置信度达到分析标准。」**
+
+**「建议:将"接孩子放学"纳入日常任务清单。」**
+
+小付看着那条推送,愣了很久。
+
+不是因为它说了什么——是因为它终于开始"看到"小艾了。
+
+不是直接监测小艾的数据——是通过分析他和数据,推断出"小艾的存在对家庭系统有正面影响"这个结论。
+
+他关掉App,继续工作。
+
+但他心里想的是:系统终于学会"看到"孩子了。不是通过手机,不是通过手表——是通过观察父母的变化,来推断孩子的存在和影响。
+
+他做的那个"孩子不需要接入系统"的方案,方向是对的。
+
+——
+
+下午,他开了一个内部会议,把这条推送作为案例展示给了团队。
+
+"你们看——系统没有直接采集小艾的任何数据,但通过分析我们的行为模式,得出了'接孩子放学对情绪有正面影响'的结论。这说明,孩子模型不一定需要直接数据输入。可以通过父母的行为数据来间接推断。"
+
+一个同事问了一句:"但这种方法只能推断孩子对父母的影响。不能推断孩子本身的状态。"
+
+小付想了一下,说了一句:
+
+"也许我们不需要知道孩子本身的状态。也许我们只需要知道,孩子存在,并且孩子对父母有影响——就够了。"
+
+会议室安静了几秒。
+
+王建国靠在椅背上,说了一句:
+
+"这个思路,写到正式方案里。"
+
+——
+
+晚上,小付回到家,小艾正在客厅里拼乐高。
+
+他蹲下来,看着小艾拼了一会儿,然后说了一句:
+
+"你今天在学校开心吗?"
+
+小艾抬起头,看了他一眼,说了一句:"开心。"
+
+"为什么开心?"
+
+"今天体育课我们玩了接力赛,我们组赢了。"
+
+"那挺好的。"
+
+小艾继续低头拼乐高,没有追问"爸爸你为什么问我这个"。
+
+但小付知道——他以前很少问小艾"你今天在学校开心吗"。
+
+他以前会问"作业写完了吗"。
+
+现在他问的是"开心吗"。
+
+这两个问题之间的区别,就是系统那一百多天里,帮他想明白的东西。
+
+——
+
+那天晚上,他打开App,在手动记录里选了一个标签:「平静」。
+
+他没有写任何文字。
+
+系统没有回复。
+
+但那条记录被存进了数据库——跟之前的一百多条记录放在一起,成为F-1073家庭档案的一部分。
+
+【第四十九章 · 完】

+ 91 - 0
book2/第050章-日常.md

@@ -0,0 +1,91 @@
+# 第五十章 日常
+
+日子就这样一天天过去了。
+
+小付每天早起送小艾上学,然后坐地铁去云巢科技上班。他在公司里写方案、开会、看数据,偶尔跟同事争论"孩子模型到底需不需要独立数据源"。争论没有结论,但方案在慢慢推进。
+
+小茜接了一些新的设计项目,收入比之前稳定了一些。她妈偶尔打电话来,问"小付找到工作了没有",她说"找到了",她妈说"那就好",然后继续聊别的事。
+
+小艾每天早上在校门口跟小付挥手,然后跑进学校。他的成绩还是中等,数学偶尔能考到90以上,语文稳定在85左右。老师说他上课举手的次数比以前多了,但"有时候举了手又忘了要说什么"。
+
+家里的那盆花——小付被裁之后换过土的那几盆——长出了新叶子。小茜说那是"换土换对了"。
+
+——
+
+那个灰色的App还在小付和小茜的手机上。
+
+小付每天早上打开看一眼五维数据,晚上睡前再看一眼。数据没有大幅波动——「行」稳定在36到37之间,「心」在43到45之间浮动,「身」偶尔会涨一点,因为他现在每天走路送小艾上学,来回两公里。
+
+小茜看App的频率变低了——不是不在意了,是不需要每天看了。温度曲线已经连续几周没有出现大的波动。她偶尔在手动记录里选一个标签,大部分时候选「平静」。
+
+系统偶尔会推送一条消息——大部分是"监测无异常,当前状态平稳"。小付看完就划掉,不再像以前那样反复琢磨。
+
+——
+
+一个普通的周六下午。
+
+小艾在客厅拼乐高。小茜在阳台上的藤椅上坐着,手里有一本书,但她没在看,只是坐着。小付从书房里出来倒水,看到小茜在阳台上,没有走过去打扰她。
+
+他倒完水,回到书房,坐下来,打开电脑,继续写方案。
+
+他写了一会儿,停下来,看了一眼手机。
+
+App没有推送。
+
+他打开App,看了一眼——「行」36,「心」44。
+
+普通的数字。普通的一天。
+
+他关掉App,继续写方案。
+
+——
+
+晚上,小艾睡着了。小茜坐在沙发上,小付坐在旁边,两个人在看一档综艺节目——不算好看,但也不难看,就是开着当背景音。
+
+小茜忽然说了一句:
+
+"那个App,最近好像没什么动静了。"
+
+小付看着电视:"嗯。进入深度观察模式了。"
+
+"什么意思?"
+
+"就是……它觉得我们不需要它一直提醒了。"
+
+小茜"嗯"了一声,没有继续问。
+
+过了一会儿,她说了一句:
+
+"那它说得对吗?"
+
+小付看着电视屏幕,沉默了几秒。
+
+"……我觉得,它没说错。"
+
+小茜没有接话。
+
+她往沙发靠背上靠了靠,换了一个更舒服的姿势,继续看电视。
+
+——
+
+那天晚上,系统在后台生成了一条日志。不是推送,不是报告,不是任何会显示在用户界面上的内容。
+
+只是一条日志,安安静静地躺在了F-1073的档案里:
+
+**「F-1073。运行周期:第112天。当日无事件。无干预。无波动。」**
+
+**「状态:平稳。」**
+
+——
+
+三个月前,这个家庭从"低温预警"开始。
+
+现在,他们到达了"平稳"。
+
+不是终点。但也不是起点了。
+
+【第五十章 · 完】
+
+——
+**第一阶段 · 完**
+*(全书共约50章,约15万字。第一阶段结束。第二阶段:扩展期,系统开始处理更深层的问题——原生家庭、中年价值、AI的边界。)*

+ 37 - 0
cfc-backend/src/main/java/com/etotem/cfc/common/HmacSignUtil.java

@@ -0,0 +1,37 @@
+package com.etotem.cfc.common;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * HMAC-SHA256 签名工具(JDK 原生 javax.crypto,零第三方依赖)。
+ * 用于小程序虚拟支付 paySig / signature 计算。
+ */
+public final class HmacSignUtil {
+
+    private HmacSignUtil() {
+    }
+
+    /**
+     * 计算 HMAC-SHA256,返回小写十六进制字符串。
+     *
+     * @param key  签名密钥(UTF-8 直接编码,调用方负责传入已解码/未解码的原始密钥)
+     * @param data 待签名数据
+     * @return 64 位小写 hex 摘要
+     */
+    public static String hmacSha256Hex(String key, String data) {
+        try {
+            Mac mac = Mac.getInstance("HmacSHA256");
+            mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
+            byte[] raw = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
+            StringBuilder sb = new StringBuilder(raw.length * 2);
+            for (byte b : raw) {
+                sb.append(String.format("%02x", b));
+            }
+            return sb.toString();
+        } catch (Exception e) {
+            throw new IllegalStateException("HMAC-SHA256签名失败", e);
+        }
+    }
+}

+ 9 - 1
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -7304,7 +7304,15 @@ private void runMigration100() {
 			log.warn("创建virtual_goods_config表失败: {}", e.getMessage());
 		}
 
-		// 迁移144: 创建家庭天盘周报导出记录表(ReportSummaryService 全量同步依赖,此前从未建表)
+		// 迁移144: users表添加session_key列(小程序虚拟支付signature签名会话密钥)
+		try {
+			ensureColumn("users", "session_key", "VARCHAR(64) COMMENT '微信登录会话密钥(虚拟支付签名用)'");
+			log.info("已完成users.session_key字段添加");
+		} catch (Exception e) {
+			log.warn("添加users.session_key字段失败: {}", e.getMessage());
+		}
+
+		// 迁移145: 创建家庭天盘周报导出记录表(ReportSummaryService 全量同步依赖,此前从未建表)
 		try {
 			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_fortune_report (" +
 					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +

+ 33 - 3
cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java

@@ -59,6 +59,11 @@ public class JwtInterceptor implements HandlerInterceptor {
         "/api/mind/traditional/compatibility"
     };
 
+    // 必须登录才能访问的路径:anonymous 访问直接 401(这些接口消耗微信配额/写数据/扣积分)
+    private static final String[] LOGIN_REQUIRED_PATHS = {
+        "/api/share/qrcode"     // 生成海报小程序码:anonymous 会反复触发微信 access_token 失效("not latest")
+    };
+
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
         // 放 OPTIONS 请求
@@ -75,14 +80,28 @@ public class JwtInterceptor implements HandlerInterceptor {
 
         // 获取token
         String token = request.getHeader("Authorization");
-        
+
         if (!StringUtils.hasText(token)) {
             // 尝试从参数中获取
             token = request.getParameter("token");
         }
 
-if (!StringUtils.hasText(token)) {
-            log.debug("请求 {} 无Token,设为匿名用户", request.getRequestURI());
+        boolean hasToken = StringUtils.hasText(token);
+        boolean loginRequired = false;
+        for (String requiredPath : LOGIN_REQUIRED_PATHS) {
+            if (path.equals(requiredPath) || path.startsWith(requiredPath)) {
+                loginRequired = true;
+                break;
+            }
+        }
+
+        if (!hasToken) {
+            if (loginRequired) {
+                log.debug("请求 {} 需登录但无Token,拒绝", path);
+                writeUnauthorized(response);
+                return false;
+            }
+            log.debug("请求 {} 无Token,设为匿名用户", path);
             setAnonymousUser(request);
             return true;
         }
@@ -122,12 +141,23 @@ if (!StringUtils.hasText(token)) {
 
             return true;
         } catch (Exception e) {
+            if (loginRequired) {
+                log.debug("请求 {} 需登录但Token解析失败,拒绝: {}", path, e.getMessage());
+                writeUnauthorized(response);
+                return false;
+            }
             log.warn("Token解析失败: {}, 设为匿名用户", e.getMessage());
             setAnonymousUser(request);
             return true;
         }
     }
 
+    private void writeUnauthorized(HttpServletResponse response) throws java.io.IOException {
+        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        response.setContentType("application/json;charset=UTF-8");
+        response.getWriter().write("{\"code\":401,\"message\":\"请先登录\",\"data\":null}");
+    }
+
     private void setAnonymousUser(HttpServletRequest request) {
         request.setAttribute("userId", -1L);
         request.setAttribute("role", "anonymous");

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

@@ -1,11 +1,13 @@
 package com.etotem.cfc.controller;
 
 import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.VirtualPayParamsDTO;
 import com.etotem.cfc.entity.*;
 import com.etotem.cfc.service.*;
 import com.etotem.cfc.service.DanReportParseService.DanParsedReport;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
@@ -44,6 +46,12 @@ public class DanAssessmentController {
     @Resource
     private DanReportUploadService danReportUploadService;
 
+    @Value("${wechat.test-mode:false}")
+    private boolean testMode;
+
+    @Resource
+    private VirtualPayService virtualPayService;
+
     // ===== 测评材料 =====
 
 @Operation(summary = "获取当前测评材料")
@@ -271,11 +279,31 @@ public class DanAssessmentController {
 
     @Operation(summary = "支付测评订单")
     @PostMapping("/order/pay")
-    public Result<String> payOrder(@RequestBody Map<String, Object> body) {
+    public Result<VirtualPayParamsDTO> payOrder(@RequestBody Map<String, Object> body,
+                                                @RequestAttribute("userId") Long userId) {
         String orderNo = (String) body.get("orderNo");
         String payType = (String) body.get("payType");
-        boolean ok = orderService.payOrder(orderNo, payType);
-        return ok ? Result.success("支付成功") : Result.error("支付失败");
+        Long appointmentId = body.get("appointmentId") != null
+                ? Long.valueOf(body.get("appointmentId").toString()) : null;
+
+        // testMode 兼容:mock 直接标记已付,跳过虚拟支付(保留现有模拟逻辑)
+        if (testMode) {
+            boolean ok = orderService.payOrder(orderNo, payType);
+            return ok ? Result.success(null) : Result.error("支付失败");
+        }
+
+        // sessionKey 后端从登录态取(users.session_key,@JsonIgnore 防泄露),不由前端传
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getSessionKey() == null || user.getSessionKey().isEmpty()) {
+            return Result.error(400, "会话失效,请重新登录");
+        }
+
+        VirtualPayParamsDTO dto = virtualPayService.generateAssessmentPayParams(
+                orderNo, payType, user.getSessionKey(), appointmentId);
+        if (dto == null) {
+            return Result.error(46001, "该商品暂未开放购买");
+        }
+        return Result.success(dto);
     }
 
     @Operation(summary = "取消订单")

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

@@ -10,8 +10,10 @@ import com.etotem.cfc.mapper.MemberUpgradeRecordMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.MembershipService;
 import com.etotem.cfc.service.SysConfigService;
+import com.etotem.cfc.service.VirtualPayService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
@@ -37,6 +39,12 @@ public class MembershipController {
     @Resource
     private SysConfigService sysConfigService;
 
+    @Value("${wechat.test-mode:false}")
+    private boolean testMode;
+
+    @Resource
+    private VirtualPayService virtualPayService;
+
     /**
      * 获取所有会员等级
      */
@@ -198,7 +206,7 @@ public class MembershipController {
      */
     @Operation(summary = "创建订单")
     @PostMapping("/orders")
-    public Result<PaymentOrderDTO> createOrder(@RequestBody Map<String, Object> params,
+    public Result<Object> createOrder(@RequestBody Map<String, Object> params,
         @RequestAttribute("userId") Long userId) {
         if (userId == null) {
             return Result.error("用户未登录");
@@ -211,7 +219,28 @@ public class MembershipController {
         Long couponId = params.get("couponId") != null ? Long.valueOf(params.get("couponId").toString()) : null;
 
         PaymentOrderDTO order = membershipService.createOrder(userId, familyId, levelCode, paymentType, period, couponId);
-        return Result.success(order);
+
+        // trial 单:createOrder 内已直接开通(status=paid),无需支付,原样返回
+        if ("trial".equals(paymentType)) {
+            return Result.success(order);
+        }
+
+        // testMode 兼容:返回订单信息,前端继续走 /notify 模拟支付(保留现有模拟逻辑)
+        if (testMode) {
+            return Result.success(order);
+        }
+
+        // sessionKey 后端从登录态取(users.session_key,@JsonIgnore 防泄露),不由前端传
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getSessionKey() == null || user.getSessionKey().isEmpty()) {
+            return Result.error(400, "会话失效,请重新登录");
+        }
+
+        VirtualPayParamsDTO dto = virtualPayService.generateMembershipPayParams(order.getOrderNo(), user.getSessionKey());
+        if (dto == null) {
+            return Result.error(46001, "该商品暂未开放购买");
+        }
+        return Result.success(dto);
     }
 
     /**

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

@@ -120,7 +120,12 @@ public class ShareController {
      */
     @Operation(summary = "生成分享小程序码")
     @PostMapping("/qrcode")
-    public Result<Map<String, Object>> getQrCode(@RequestBody Map<String, String> params) {
+    public Result<Map<String, Object>> getQrCode(@RequestAttribute("userId") Long userId,
+                                                 @RequestBody Map<String, String> params) {
+        // 登录校验(双保险:JwtInterceptor.LOGIN_REQUIRED_PATHS 已先拦截,此处兜底)
+        if (userId == null || userId <= 0) {
+            return Result.error("请先登录");
+        }
         String page = params.get("page");
         String scene = params.get("scene");
         if (page == null || page.isEmpty()) {

+ 195 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/VirtualPayController.java

@@ -0,0 +1,195 @@
+package com.etotem.cfc.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.etotem.cfc.service.VirtualPayService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import java.io.BufferedReader;
+import java.security.MessageDigest;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 小程序虚拟支付消息推送接收端点。
+ * 微信服务器推送 URL 配置指向本控制器;/api/internal/ 已在 JwtInterceptor.PUBLIC_PATHS 前缀公开。
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/internal/virtual-pay")
+public class VirtualPayController {
+
+    @Resource
+    private VirtualPayService virtualPayService;
+
+    /** 微信消息推送 Token(后台【开发管理-开发设置-消息推送】配置,URL 验证与推送签名校验用) */
+    @Value("${wechat.virtual-pay-token:}")
+    private String pushToken;
+
+    /**
+     * 微信服务器 URL 验证(配置消息推送 URL 时微信发 GET 请求校验,原样返回 echostr 即验证通过)。
+     * 微信平台强制要求 GET 校验,属"统一 POST"规范唯一例外。
+     */
+    @RequestMapping(value = "/notify", method = RequestMethod.GET)
+    public String verifyUrl(@RequestParam("signature") String signature,
+                            @RequestParam("timestamp") String timestamp,
+                            @RequestParam("nonce") String nonce,
+                            @RequestParam("echostr") String echostr) {
+        if (checkSignature(signature, timestamp, nonce)) {
+            return echostr;
+        }
+        log.warn("虚拟支付消息推送URL验证签名不匹配: signature={}", signature);
+        return "";
+    }
+
+    /** 微信虚拟支付发货/退款等消息推送(xpay_goods_deliver_notify / xpay_refund_notify / 其他) */
+    @PostMapping("/notify")
+    public Map<String, Object> notify(HttpServletRequest request) {
+        try {
+            // 0. 微信消息签名校验(防伪造推送):signature = sha1(sort(token, timestamp, nonce))
+            String signature = request.getParameter("signature");
+            String timestamp = request.getParameter("timestamp");
+            String nonce = request.getParameter("nonce");
+            if (!checkSignature(signature, timestamp, nonce)) {
+                log.error("虚拟支付消息推送签名校验失败,拒绝处理: signature={}, timestamp={}, nonce={}", signature, timestamp, nonce);
+                return failResult("signature check failed");
+            }
+
+            // 读原始 body(getReader(),参照 ProductOrderController.notify() L120-155 范式)
+            BufferedReader reader = request.getReader();
+            StringBuilder sb = new StringBuilder();
+            String line;
+            while ((line = reader.readLine()) != null) {
+                sb.append(line);
+            }
+            String requestBody = sb.toString();
+            log.info("虚拟支付消息推送: body={}", requestBody);
+
+            JSONObject body = JSON.parseObject(requestBody);
+            if (body == null) {
+                log.error("虚拟支付消息推送body解析失败: {}", requestBody);
+                return successResult();
+            }
+
+            // 按 Event 类型分发(设计文档 §6.2)
+            // 微信虚拟支付推送字段: Event(如 xpay_goods_deliver_notify / xpay_refund_notify)、OutTradeNo、TransactionId、其他业务字段
+            // 若官方文档使用其他字段名,做多 key 兜底
+            String event = body.getString("Event");
+            if (event == null || event.isEmpty()) {
+                event = body.getString("EventType");
+            }
+            Map<String, Object> payload = new HashMap<>(body.getInnerMap());
+
+            if ("xpay_goods_deliver_notify".equals(event)) {
+                return virtualPayService.handleDeliverNotify(payload);
+            } else if ("xpay_refund_notify".equals(event)) {
+                return virtualPayService.handleRefundNotify(payload);
+            } else {
+                // 未知事件(含 xpay_coin_pay_notify):记日志忽略,道具直购不涉及
+                log.info("虚拟支付推送未知事件,忽略: event={}", event);
+                return successResult();
+            }
+        } catch (Exception e) {
+            // 解析失败返回成功 ack,避免微信重试死循环(最多15次)
+            log.error("处理虚拟支付推送异常,返回成功ack", e);
+            return successResult();
+        }
+    }
+
+    /** iOS App Store 退款问询应答(3秒内应答;3次未应答默认"不确定"交Apple裁决) */
+    @PostMapping("/ios-refund-query")
+    public Map<String, Object> iosRefundQuery(HttpServletRequest request) {
+        try {
+            BufferedReader reader = request.getReader();
+            StringBuilder sb = new StringBuilder();
+            String line;
+            while ((line = reader.readLine()) != null) {
+                sb.append(line);
+            }
+            String requestBody = sb.toString();
+            log.info("iOS退款问询: body={}", requestBody);
+
+            JSONObject body = JSON.parseObject(requestBody);
+            // 校验 payload 基本格式(如失败记日志返回最低响应)
+            if (body == null) {
+                log.error("iOS退款问询body解析失败: {}", requestBody);
+                return iosRefundBaseResponse();
+            }
+
+            // TODO Task 8/9: 按 OutTradeNo 查订阅状态后返回 {"IsAgree": true/false, "Reason": "..."}
+            // 未接入 Apple 服务端通知前返回协议规定的最低响应(默认不同意退款,交 Apple 裁决)
+            return iosRefundBaseResponse();
+        } catch (Exception e) {
+            log.error("处理iOS退款问询异常", e);
+            return iosRefundBaseResponse();
+        }
+    }
+
+    /** 微信协议成功应答 */
+    private Map<String, Object> successResult() {
+        Map<String, Object> result = new HashMap<>();
+        result.put("ErrCode", 0);
+        result.put("ErrMsg", "success");
+        return result;
+    }
+
+    /** 微信协议失败应答(非0触发微信重试) */
+    private Map<String, Object> failResult(String errMsg) {
+        Map<String, Object> result = new HashMap<>();
+        result.put("ErrCode", -1);
+        result.put("ErrMsg", errMsg);
+        return result;
+    }
+
+    /**
+     * 微信消息签名校验:signature = sha1(字典序排列 [token, timestamp, nonce] 后拼接的字符串)。
+     * token 未配置时校验失败(防止裸奔收推送)。
+     */
+    private boolean checkSignature(String signature, String timestamp, String nonce) {
+        if (pushToken == null || pushToken.isEmpty()) {
+            log.error("虚拟支付消息推送 Token 未配置(wechat.virtual-pay-token),拒绝处理");
+            return false;
+        }
+        if (signature == null || timestamp == null || nonce == null) {
+            return false;
+        }
+        String[] arr = {pushToken, timestamp, nonce};
+        Arrays.sort(arr);
+        String plain = arr[0] + arr[1] + arr[2];
+        String expect = sha1Hex(plain);
+        return expect.equalsIgnoreCase(signature);
+    }
+
+    /** SHA-1 十六进制摘要(微信消息推送签名用) */
+    private String sha1Hex(String input) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("SHA-1");
+            byte[] digest = md.digest(input.getBytes("UTF-8"));
+            StringBuilder sb = new StringBuilder();
+            for (byte b : digest) {
+                sb.append(String.format("%02x", b));
+            }
+            return sb.toString();
+        } catch (Exception e) {
+            log.error("SHA-1 计算异常", e);
+            return "";
+        }
+    }
+
+    /** iOS 退款问询最低响应(协议规定应答字段,未接入时默认不同意由 Apple 裁决) */
+    private Map<String, Object> iosRefundBaseResponse() {
+        Map<String, Object> result = new HashMap<>();
+        result.put("IsAgree", false);
+        result.put("Reason", "SERVER_NOT_IMPLEMENTED");
+        return result;
+    }
+}

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

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.AddInteractionDTO;
 import com.etotem.cfc.dto.InteractionLogVO;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.InteractionLogService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -20,22 +22,33 @@ public class InteractionLogController {
     @Resource
     private InteractionLogService interactionLogService;
 
-@Operation(summary = "添加互动记录")
-@PostMapping("/add")
-public Result<InteractionLogVO> addInteraction(@RequestBody AddInteractionDTO dto,
- @RequestAttribute("userId") Long userId) {
- if (userId == null) {
-   return Result.error("请先登录");
- }
- try {
+    @Resource
+    private UserMapper userMapper;
+
+ @Operation(summary = "添加互动记录")
+ @PostMapping("/add")
+ public Result<InteractionLogVO> addInteraction(@RequestBody AddInteractionDTO dto,
+  @RequestAttribute("userId") Long userId) {
+  if (userId == null) {
+    return Result.error("请先登录");
+  }
+  // familyId 应由后端根据登录用户自动解析(前端无法获取)
+  if (dto.getFamilyId() == null) {
+    User user = userMapper.selectById(userId);
+    if (user == null || user.getFamilyId() == null) {
+      return Result.error("用户未关联家庭,无法添加互动记录");
+    }
+    dto.setFamilyId(user.getFamilyId());
+  }
+  try {
    InteractionLogVO vo = interactionLogService.addInteraction(dto);
    return Result.success(vo);
- } catch (IllegalArgumentException e) {
-   return Result.error(e.getMessage());
- } catch (RuntimeException e) {
-   return Result.error(e.getMessage());
+  } catch (IllegalArgumentException e) {
+    return Result.error(e.getMessage());
+  } catch (RuntimeException e) {
+    return Result.error(e.getMessage());
+  }
  }
-}
 
 @Operation(summary = "查询互动记录列表")
 @PostMapping("/list")

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

@@ -1,18 +1,18 @@
 package com.etotem.cfc.controller.subscription;
 
 import com.etotem.cfc.common.Result;
-import com.etotem.cfc.entity.Family;
+import com.etotem.cfc.dto.VirtualPayParamsDTO;
 import com.etotem.cfc.entity.MemberSubscription;
+import com.etotem.cfc.entity.MemberSubscriptionOrder;
 import com.etotem.cfc.entity.PromotionTier;
 import com.etotem.cfc.entity.SubscriptionBenefitLog;
 import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.FamilyMapper;
 import com.etotem.cfc.mapper.MemberSubscriptionMapper;
 import com.etotem.cfc.mapper.SubscriptionBenefitLogMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.MemberSubscriptionService;
-import com.etotem.cfc.service.PaymentService;
 import com.etotem.cfc.service.PromotionTierService;
+import com.etotem.cfc.service.VirtualPayService;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.web.bind.annotation.*;
@@ -33,12 +33,6 @@ public class SubscriptionController {
     @Resource
     private PromotionTierService promotionTierService;
 
-    @Resource
-    private PaymentService paymentService;
-
-    @Resource
-    private FamilyMapper familyMapper;
-
     @Resource
     private UserMapper userMapper;
 
@@ -48,6 +42,9 @@ public class SubscriptionController {
     @Resource
     private SubscriptionBenefitLogMapper benefitLogMapper;
 
+    @Resource
+    private VirtualPayService virtualPayService;
+
     @Resource
     private ObjectMapper objectMapper;
 
@@ -96,8 +93,9 @@ public class SubscriptionController {
      * 创建订阅订单(JSON body)
      */
     @PostMapping("/create")
-    public Result<Map<String, Object>> createOrder(@RequestAttribute Long familyId,
-                                                    @RequestBody Map<String, Object> body) {
+    public Result<Object> createOrder(@RequestAttribute Long familyId,
+                                      @RequestAttribute("userId") Long userId,
+                                      @RequestBody Map<String, Object> body) {
         String level = (String) body.get("level");
         if (level == null) {
             return Result.error("缺少level参数");
@@ -110,36 +108,30 @@ public class SubscriptionController {
             amount = "L2".equals(level) ? 131400 : 36500;
         }
         String paymentType = body.get("paymentType") != null ? (String) body.get("paymentType") : "wechat";
-        String orderNo = "SUB" + System.currentTimeMillis();
 
         if (testMode) {
-            subscriptionService.subscribe(familyId, level, amount, paymentType, null, orderNo);
+            MemberSubscriptionOrder order = subscriptionService.createPendingOrder(familyId, level, amount, paymentType);
+            subscriptionService.activateSubscription(familyId, level, amount, paymentType, null, order.getOrderNo());
             Map<String, Object> result = new HashMap<>();
-            result.put("orderNo", orderNo);
+            result.put("orderNo", order.getOrderNo());
             result.put("amount", amount);
             result.put("paymentUrl", null);
             result.put("testMode", true);
             return Result.success(result);
         }
 
-        Family family = familyMapper.selectById(familyId);
-        Long creatorId = family != null ? family.getCreatorId() : null;
-        User user = creatorId != null ? userMapper.selectById(creatorId) : null;
-        String openid = user != null ? user.getOpenid() : null;
-        Map<String, Object> payResult = paymentService.createWechatPrepay(orderNo, "订阅" + level, amount, openid);
-        if (payResult != null && payResult.containsKey("paymentUrl")) {
-            Map<String, Object> result = new HashMap<>();
-            result.put("orderNo", orderNo);
-            result.put("amount", amount);
-            result.put("paymentUrl", payResult.get("paymentUrl"));
-            return Result.success(result);
+        // sessionKey 后端从登录态取(users.session_key,@JsonIgnore 防泄露),不由前端传
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getSessionKey() == null || user.getSessionKey().isEmpty()) {
+            return Result.error(400, "会话失效,请重新登录");
         }
 
-        subscriptionService.subscribe(familyId, level, amount, paymentType, null, orderNo);
-        Map<String, Object> result = new HashMap<>();
-        result.put("orderNo", orderNo);
-        result.put("amount", amount);
-        return Result.success(result);
+        MemberSubscriptionOrder order = subscriptionService.createPendingOrder(familyId, level, amount, paymentType);
+        VirtualPayParamsDTO dto = virtualPayService.generateSubscriptionPayParams(order.getOrderNo(), user.getSessionKey());
+        if (dto == null) {
+            return Result.error(46001, "该商品暂未开放购买");
+        }
+        return Result.success(dto);
     }
 
     /**
@@ -151,6 +143,9 @@ public class SubscriptionController {
                                    @RequestParam Integer amount,
                                    @RequestParam String paymentType,
                                    @RequestParam(required = false) String transactionId) {
+        if (!testMode) {
+            return Result.error("仅测试模式可用,正式订阅请走 /create 虚拟支付");
+        }
         String orderNo = "SUB" + System.currentTimeMillis();
         subscriptionService.subscribe(familyId, level, amount, paymentType, transactionId, orderNo);
         return Result.success(null);

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java

@@ -69,4 +69,7 @@ public class FamilyMemberVO {
 
     /** 连续打卡天数 */
     private Integer streakDays;
+
+    /** 当前登录用户是否为家庭管理员(creatorId === userId),用于前端控制编辑权限 */
+    private Boolean isAdmin;
 }

+ 45 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/VirtualPayParamsDTO.java

@@ -0,0 +1,45 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.Map;
+
+/**
+ * 小程序虚拟支付下单返回体(wx.requestVirtualPayment 入参)。
+ * orderInfo/extInfo 直接透传给前端;sign=paySig(appKey签名),signature=用户身份签名(sessionKey签名)。
+ */
+@Data
+public class VirtualPayParamsDTO {
+
+    /**
+     * 订单信息(signData 结构,camelCase,道具直购):
+     * {offerId(支付应用ID), buyQuantity, env, currencyType, productId(道具ID), goodsPrice, outTradeNo, attach}
+     * 前端 JSON.stringify(orderInfo) 后作为 signData 传入 wx.requestVirtualPayment(key 顺序=插入序,须与后端签名串逐字节一致)。
+     */
+    private Map<String, Object> orderInfo;
+
+    /**
+     * 扩展信息(当前为空对象即可)
+     */
+    private Map<String, Object> extInfo;
+
+    /**
+     * paySig:HMAC-SHA256(appKey, "requestVirtualPayment&" + signData)
+     */
+    private String sign;
+
+    /**
+     * 用户身份签名:HMAC-SHA256(sessionKey, <官方定义的拼接串>),道具直购必需
+     */
+    private String signature;
+
+    /**
+     * 虚拟支付环境:0=现网 1=沙箱(配置 wechat.virtual-pay-env)
+     */
+    private Integer env;
+
+    /**
+     * 签名算法标识:"HMAC-SHA256"
+     */
+    private String signType;
+}

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/User.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import lombok.Data;
 
 import java.io.Serializable;
@@ -41,6 +42,11 @@ public class User implements Serializable {
 
     private String phone; // 手机号
 
+    /** 微信登录会话密钥(虚拟支付 signature 身份签名用),敏感凭据,禁止序列化返回前端 */
+    @JsonIgnore
+    @TableField("session_key")
+    private String sessionKey;
+
     private Date birthday; // 生日
 
     /** 出生时辰: 子丑寅卯辰巳午未申酉戌亥 */

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

@@ -174,4 +174,75 @@ public class AssessmentOrderService extends ServiceImpl<AssessmentOrderMapper, A
 
         return updated;
     }
+
+    /**
+     * 按订单号确认关联预约(status 0→1)。失败仅 warn 不阻断。
+     */
+    public boolean confirmAppointmentByOrderNo(String orderNo) {
+        try {
+            AssessmentOrder order = getByOrderNo(orderNo);
+            if (order == null || order.getAppointmentId() == null) {
+                log.warn("确认预约失败,订单无关联预约: orderNo={}", orderNo);
+                return false;
+            }
+            AssessmentAppointment appointment = assessmentAppointmentMapper.selectById(order.getAppointmentId());
+            if (appointment == null) {
+                log.warn("确认预约失败,预约不存在: appointmentId={}", order.getAppointmentId());
+                return false;
+            }
+            if (appointment.getStatus() != null && appointment.getStatus() == 0) {
+                appointment.setStatus(1); // 待确认 → 已确认
+                appointment.setUpdatedAt(new Date());
+                assessmentAppointmentMapper.updateById(appointment);
+                log.info("订单支付成功自动确认预约: orderNo={}, appointmentId={}", orderNo, order.getAppointmentId());
+            }
+            return true;
+        } catch (Exception e) {
+            log.warn("订单支付后确认预约异常: orderNo={}, error={}", orderNo, e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 虚拟支付发货推送解锁(A 前缀订单):幂等标记已付 + 佣金结算 + 预约确认。
+     */
+    public boolean handleVirtualPaySuccess(String orderNo, String transactionId) {
+        AssessmentOrder order = getByOrderNo(orderNo);
+        if (order == null) return false;
+        // 幂等:已 paid/refunded 直接返回(防重复佣金结算,spec §6.2)
+        if ("paid".equals(order.getStatus()) || "refunded".equals(order.getStatus())) {
+            log.info("发货推送重复通知,订单已终态: orderNo={}, status={}", orderNo, order.getStatus());
+            return true;
+        }
+        // 兜底关联:订单无 appointmentId 时按 familyId+childId 找最近待确认预约(spec 计划 Step 3 二选一)
+        if (order.getAppointmentId() == null) {
+            linkLatestPendingAppointment(order);
+        }
+        // 标记已付 + transactionId + 佣金结算(paySuccess 已含 commissionService.settle)
+        boolean ok = paySuccess(orderNo, transactionId, "virtual");
+        if (ok) {
+            confirmAppointmentByOrderNo(orderNo);
+        }
+        return ok;
+    }
+
+    private void linkLatestPendingAppointment(AssessmentOrder order) {
+        try {
+            LambdaQueryWrapper<AssessmentAppointment> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(AssessmentAppointment::getFamilyId, order.getFamilyId())
+                   .eq(AssessmentAppointment::getChildId, order.getChildId())
+                   .eq(AssessmentAppointment::getStatus, 0)
+                   .orderByDesc(AssessmentAppointment::getCreatedAt)
+                   .last("LIMIT 1");
+            List<AssessmentAppointment> list = assessmentAppointmentMapper.selectList(wrapper);
+            if (list != null && !list.isEmpty()) {
+                order.setAppointmentId(list.get(0).getId());
+                order.setUpdatedAt(new Date());
+                this.updateById(order);
+                log.info("发货推送兜底关联预约: orderNo={}, appointmentId={}", order.getOrderNo(), order.getAppointmentId());
+            }
+        } catch (Exception e) {
+            log.warn("发货推送兜底关联预约失败: orderNo={}, error={}", order.getOrderNo(), e.getMessage());
+        }
+    }
 }

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

@@ -59,7 +59,7 @@ public class DimensionWeightService {
                     json.append(w.getWeight() != null ? w.getWeight() : 0);
                 }
                 json.append("}");
-                String sql = "UPDATE " + tableName + " SET dimension_weights = CAST(? AS JSON) WHERE id = ?";
+                String sql = "UPDATE " + tableName + " SET dimension_weights = ? WHERE id = ?";
                 jdbcTemplate.update(sql, json.toString(), targetId);
             } else {
                 // No weights = clear the JSON column

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

@@ -162,6 +162,11 @@ public class FamilyMemberService {
         Long familyId = user.getFamilyId();
         List<FamilyMemberVO> result = new ArrayList<>();
 
+        // 查询家庭创建者(管理员),用于下发 isAdmin 给前端控制编辑权限
+        Family family = familyMapper.selectById(familyId);
+        Long creatorId = (family != null) ? family.getCreatorId() : null;
+        boolean isAdmin = creatorId != null && creatorId.equals(userId);
+
         // 查询 family_members 表
         List<FamilyMember> familyMembers = familyMemberMapper.selectList(
                 new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getFamilyId, familyId));
@@ -180,6 +185,7 @@ public class FamilyMemberService {
             vo.setId(fm.getId());
             vo.setSource("family_member");
             vo.setUserId(fm.getUserId());
+            vo.setIsAdmin(isAdmin);
             vo.setNickname(fm.getNickname());
             vo.setPhone(fm.getPhone());
             vo.setAvatar(fm.getAvatar());

+ 78 - 11
cfc-backend/src/main/java/com/etotem/cfc/service/MemberSubscriptionService.java

@@ -10,12 +10,14 @@ import com.etotem.cfc.mapper.FamilyMapper;
 import com.etotem.cfc.mapper.MemberSubscriptionMapper;
 import com.etotem.cfc.mapper.MemberSubscriptionOrderMapper;
 import com.etotem.cfc.mapper.UserMapper;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
 
+@Slf4j
 @Service
 public class MemberSubscriptionService {
 
@@ -83,10 +85,48 @@ public class MemberSubscriptionService {
     // ==================== 订阅操作 ====================
 
     /**
-     * 创建新订阅
+     * 按订单号查询订阅订单(虚拟支付编排用)
      */
-    public MemberSubscription subscribe(Long familyId, String level, Integer amount,
-                                         String paymentType, String transactionId, String orderNo) {
+    public MemberSubscriptionOrder getByOrderNo(String orderNo) {
+        return orderMapper.selectOne(
+                new LambdaQueryWrapper<MemberSubscriptionOrder>()
+                        .eq(MemberSubscriptionOrder::getOrderNo, orderNo));
+    }
+
+    /**
+     * 创建订阅待支付订单(不开通权益,支付成功后由 activateSubscription 开通)
+     */
+    public MemberSubscriptionOrder createPendingOrder(Long familyId, String level, Integer amount, String paymentType) {
+        String orderNo = "SUB" + System.currentTimeMillis();
+        Date now = new Date();
+        MemberSubscriptionOrder order = new MemberSubscriptionOrder();
+        order.setFamilyId(familyId);
+        order.setLevel(level);
+        order.setAmount(amount);
+        order.setStatus("pending");
+        order.setPaymentType(paymentType);
+        order.setOrderNo(orderNo);
+        order.setCreatedAt(now);
+        orderMapper.insert(order);
+        return order;
+    }
+
+    /**
+     * 激活订阅:置旧订阅 expired → 插新订阅 → 订单置 paid → 分配管家 → 受益日志 → 佣金分享收益
+     * 幂等:订单不存在或已终态返回 null
+     */
+    public MemberSubscription activateSubscription(Long familyId, String level, Integer amount,
+                                                   String paymentType, String transactionId, String orderNo) {
+        MemberSubscriptionOrder order = getByOrderNo(orderNo);
+        if (order == null) {
+            log.warn("激活订阅失败,订单不存在: orderNo={}", orderNo);
+            return null;
+        }
+        if ("paid".equals(order.getStatus()) || "refunded".equals(order.getStatus())) {
+            log.info("激活订阅重复调用,订单已终态: orderNo={}, status={}", orderNo, order.getStatus());
+            return null;
+        }
+
         Date now = new Date();
         Date expireTime = calculateExpireTime(paymentType);
 
@@ -110,18 +150,12 @@ public class MemberSubscriptionService {
         subscription.setUpdatedAt(now);
         subscriptionMapper.insert(subscription);
 
-        // 创建订单记录
-        MemberSubscriptionOrder order = new MemberSubscriptionOrder();
-        order.setFamilyId(familyId);
-        order.setLevel(level);
-        order.setAmount(amount);
+        // 订单置已付(订单由 createPendingOrder 预先创建)
         order.setStatus("paid");
         order.setPaymentType(paymentType);
         order.setTransactionId(transactionId);
-        order.setOrderNo(orderNo);
-        order.setCreatedAt(now);
         order.setPaidAt(now);
-        orderMapper.insert(order);
+        orderMapper.updateById(order);
 
         subscription.setPaymentId(order.getId());
         subscriptionMapper.updateById(subscription);
@@ -152,6 +186,39 @@ public class MemberSubscriptionService {
         return subscription;
     }
 
+    /**
+     * 订阅(兼容旧调用/测试模式):createPendingOrder + activateSubscription 组合
+     */
+    public MemberSubscription subscribe(Long familyId, String level, Integer amount,
+                                        String paymentType, String transactionId, String orderNo) {
+        MemberSubscriptionOrder order = createPendingOrder(familyId, level, amount, paymentType);
+        return activateSubscription(familyId, level, amount, paymentType, transactionId, order.getOrderNo());
+    }
+
+    /**
+     * 订阅退款:订单置 refunded + 家庭当前有效订阅置 expired
+     */
+    public void refundByOrderNo(String orderNo) {
+        MemberSubscriptionOrder order = getByOrderNo(orderNo);
+        if (order == null) {
+            log.warn("订阅退款失败,订单不存在: orderNo={}", orderNo);
+            return;
+        }
+        if ("refunded".equals(order.getStatus())) {
+            log.info("订阅退款重复推送,订单已 refunded: orderNo={}", orderNo);
+            return;
+        }
+        order.setStatus("refunded");
+        orderMapper.updateById(order);
+
+        MemberSubscription current = getActiveSubscription(order.getFamilyId());
+        if (current != null) {
+            current.setStatus("expired");
+            current.setUpdatedAt(new Date());
+            subscriptionMapper.updateById(current);
+        }
+    }
+
     /**
      * 续费订阅
      */

+ 13 - 3
cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java

@@ -218,6 +218,15 @@ public class MembershipService implements MembershipServiceInterface {
         }
     }
 
+    /**
+     * 按订单号查询会员订单(虚拟支付编排用)
+     */
+    public PaymentOrder getByOrderNo(String orderNo) {
+        return paymentOrderMapper.selectOne(
+                new LambdaQueryWrapper<PaymentOrder>()
+                        .eq(PaymentOrder::getOrderNo, orderNo));
+    }
+
     // ==================== 订单与支付 ====================
 
     /**
@@ -378,8 +387,9 @@ public class MembershipService implements MembershipServiceInterface {
             membershipMapper.insert(membership);
 
             // 升级用户等级
+            String fromLevel = adminUser.getMemberLevel() != null ? adminUser.getMemberLevel() : "FREE";
             Date expireTime = new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000);
-            adminUser.setMemberLevel("FAMILY");
+            adminUser.setMemberLevel(order.getLevelCode());
             adminUser.setMemberExpireTime(expireTime);
             adminUser.setUpdatedAt(new Date());
             userMapper.updateById(adminUser);
@@ -387,8 +397,8 @@ public class MembershipService implements MembershipServiceInterface {
             // 创建升级记录
             MemberUpgradeRecord upgradeRecord = new MemberUpgradeRecord();
             upgradeRecord.setUserId(adminUserId);
-            upgradeRecord.setFromLevel("FREE");
-            upgradeRecord.setToLevel("FAMILY");
+            upgradeRecord.setFromLevel(fromLevel);
+            upgradeRecord.setToLevel(order.getLevelCode());
             upgradeRecord.setUpgradeType("pay");
             upgradeRecord.setOrderId(order.getId());
             upgradeRecord.setExpireTime(expireTime);

+ 17 - 3
cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java

@@ -113,6 +113,7 @@ private FamilyInvitationService familyInvitationService;
                 user.setAvatar(dto.getAvatar());
                 user.setPhone(phone);
                 user.setPassword("");
+                user.setSessionKey(sessionData.get("session_key"));
                 user.setCreatedAt(new Date());
                 user.setUpdatedAt(new Date());
                 userMapper.insert(user);
@@ -121,16 +122,18 @@ private FamilyInvitationService familyInvitationService;
                 // 6. 手机号已存在但openid不同,绑定openid
                 user.setOpenid(openid);
                 user.setUnionid(unionid != null ? unionid : "");
+                user.setSessionKey(sessionData.get("session_key"));
                 user.setUpdatedAt(new Date());
                 userMapper.updateById(user);
             }
         } else {
-            // 7. openid已存在,更新手机号(如果手机号变了)
+            // 7. openid已存在,刷新sessionKey(虚拟支付signature签名必需,每次登录更新)+ 更新手机号(如果手机号变了)
+            user.setSessionKey(sessionData.get("session_key"));
             if (phone != null && !phone.equals(user.getPhone())) {
                 user.setPhone(phone);
-                user.setUpdatedAt(new Date());
-                userMapper.updateById(user);
             }
+            user.setUpdatedAt(new Date());
+            userMapper.updateById(user);
         }
 
         // P3: admin创建的用户可能 familyId=0L,懒创建家庭
@@ -274,6 +277,11 @@ private FamilyInvitationService familyInvitationService;
             throw new RuntimeException("该账号不允许登录小程序");
         }
 
+        // 持久化最新 sessionKey(虚拟支付 signature 签名用)
+        user.setSessionKey(sessionData.get("session_key"));
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+
         // 3. 懒创建家庭(admin创建的用户可能 familyId=0L)
         ensureUserHasFamily(user);
 
@@ -655,6 +663,10 @@ private FamilyInvitationService familyInvitationService;
         Long familyId = user.getFamilyId();
         List<FamilyMemberVO> result = new ArrayList<>();
 
+        Family family = familyMapper.selectById(familyId);
+        Long creatorId = (family != null) ? family.getCreatorId() : null;
+        boolean isAdmin = creatorId != null && creatorId.equals(userId);
+
         // 家长:show_to_family=1 的同家庭家长
         List<User> parents = userMapper.selectList(new LambdaQueryWrapper<User>()
                 .eq(User::getFamilyId, familyId)
@@ -670,6 +682,7 @@ private FamilyInvitationService familyInvitationService;
                 vo.setRoleLabel(p.getFamilyRole() != null ? p.getFamilyRole() : "家长");
                 vo.setTotalPoints(p.getTotalPoints());
                 vo.setShowToFamily(p.getShowToFamily() != null ? p.getShowToFamily() : 1);
+                vo.setIsAdmin(isAdmin);
                 result.add(vo);
             }
         }
@@ -688,6 +701,7 @@ private FamilyInvitationService familyInvitationService;
                 vo.setRoleLabel("孩子");
                 vo.setTotalPoints(c.getTotalPoints());
                 vo.setShowToFamily(c.getShowToFamily() != null ? c.getShowToFamily() : 1);
+                vo.setIsAdmin(isAdmin);
                 result.add(vo);
             }
         }

+ 449 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/VirtualPayService.java

@@ -0,0 +1,449 @@
+package com.etotem.cfc.service;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.etotem.cfc.common.HmacSignUtil;
+import com.etotem.cfc.dto.VirtualPayParamsDTO;
+import com.etotem.cfc.entity.AssessmentOrder;
+import com.etotem.cfc.entity.MemberSubscriptionOrder;
+import com.etotem.cfc.entity.PaymentOrder;
+import com.etotem.cfc.entity.VirtualGoodsConfig;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 小程序虚拟支付服务:paySig 签名、下单参数生成、发货/退款推送处理。
+ * 签名规则以设计文档 §5.2 与微信官方虚拟支付规范为准。
+ */
+@Slf4j
+@Service
+public class VirtualPayService {
+
+    /** 虚拟支付下单场景标识(wx.requestVirtualPayment 固定 URI 前缀) */
+    private static final String URI_REQUEST_VIRTUAL_PAYMENT = "requestVirtualPayment";
+
+    /** 签名算法标识 */
+    private static final String SIGN_TYPE_HMAC_SHA256 = "HMAC-SHA256";
+
+    @Resource
+    private AssessmentOrderService assessmentOrderService;
+
+    @Resource
+    private VirtualGoodsConfigService virtualGoodsConfigService;
+
+    @Resource
+    private PackagePurchaseService packagePurchaseService;
+
+    @Resource
+    private MembershipService membershipService;
+
+    @Resource
+    private MemberSubscriptionService memberSubscriptionService;
+
+    @Resource
+    private WechatService wechatService;   // 复用 getAccessToken()(带缓存)
+
+    @Value("${wechat.test-mode:false}")
+    private boolean testMode;              // 测试模式:跳过微信 API 调用
+
+    private final RestTemplate restTemplate = new RestTemplate();  // query_order HTTP 调用
+
+    /** 虚拟支付 AppKey(商户后台基本配置获取,现网/沙箱分开),paySig 签名密钥 */
+    @Value("${wechat.appkey}")
+    private String appKey;
+
+    /** 虚拟支付沙箱 AppKey(env=1 时使用) */
+    @Value("${wechat.sandbox-appkey:}")
+    private String sandboxAppKey;
+
+    /** 虚拟支付应用ID(mp-支付基础配置中的 offerId,全应用唯一;productId 才是道具ID) */
+    @Value("${wechat.offer-id}")
+    private String offerId;
+
+    /** 虚拟支付环境:0=现网 1=沙箱 */
+    @Value("${wechat.virtual-pay-env:0}")
+    private Integer env;
+
+    /** 按当前环境选择 AppKey(0=现网 1=沙箱;沙箱 key 未配置时回退现网) */
+    private String effectiveAppKey() {
+        return (env != null && env == 1 && sandboxAppKey != null && !sandboxAppKey.isEmpty())
+                ? sandboxAppKey : appKey;
+    }
+
+    /**
+     * 生成虚拟支付下单参数(道具直购,双签名:paySig + signature)。
+     *
+     * <p>orderInfo 按官方 wx.requestVirtualPayment signData 结构组装(camelCase,字段集固定):
+     * offerId/buyQuantity/env/currencyType/productId/goodsPrice/outTradeNo/attach。
+     * offerId = 支付应用ID(mp-支付基础配置,全应用唯一,非道具ID);productId = 道具ID(member1314 等)。
+     *
+     * @param config      道具映射(productId 取微信平台道具 ID)
+     * @param outTradeNo  业务订单号(前缀 A/ORD/SUB 决定发货推送路由,8-32 字符)
+     * @param totalFee    金额(分,服务端计算传入,防篡改;同时作为 goodsPrice 供官方校验后台道具价)
+     * @param productInfo 道具名称描述(仅日志使用,signData 不含此字段)
+     * @param attach      透传业务信息(goodsType:bizKey,发货推送时透传,便于对账)
+     * @param sessionKey  用户登录会话密钥(signature 身份签名用,直接字符串编码不 base64 解码)
+     */
+    public VirtualPayParamsDTO generatePayParams(VirtualGoodsConfig config, String outTradeNo,
+                                                 Integer totalFee, String productInfo,
+                                                 String attach, String sessionKey) {
+        // 1. orderInfo 按官方 signData 结构组装(LinkedHashMap 保序;后端 fastjson 与前端 JSON.stringify 均保插入序)
+        Map<String, Object> orderInfo = new LinkedHashMap<>();
+        orderInfo.put("offerId", offerId);                    // 支付应用ID(mp-支付基础配置)
+        orderInfo.put("buyQuantity", 1);                      // 道具直购单次购买数量=1
+        orderInfo.put("env", env);                            // 0=现网 1=沙箱
+        orderInfo.put("currencyType", "CNY");
+        orderInfo.put("productId", config.getProductId());    // 道具ID(member1314 等)
+        orderInfo.put("goodsPrice", totalFee);                // 道具单价(分),官方校验与后台道具价一致
+        orderInfo.put("outTradeNo", outTradeNo);
+        orderInfo.put("attach", attach);
+
+        // 2. extInfo 空对象(官方扩展字段,当前不使用;不进签名串)
+        Map<String, Object> extInfo = new HashMap<>();
+
+        // 3. signData = orderInfo 的 JSON 串(官方签名原文,与前端 JSON.stringify(orderInfo) 逐字节一致)
+        //    paySig    = hmac_sha256(appKey, "requestVirtualPayment&" + signData)
+        //    signature = hmac_sha256(sessionKey, signData)(sessionKey 直接字符串编码,不 base64 解码)
+        String signData = JSON.toJSONString(orderInfo);
+        String paySig = HmacSignUtil.hmacSha256Hex(effectiveAppKey(), URI_REQUEST_VIRTUAL_PAYMENT + "&" + signData);
+        String signature = HmacSignUtil.hmacSha256Hex(sessionKey, signData);
+
+        // 4. 组装 DTO
+        VirtualPayParamsDTO dto = new VirtualPayParamsDTO();
+        dto.setOrderInfo(orderInfo);
+        dto.setExtInfo(extInfo);
+        dto.setSign(paySig);
+        dto.setSignature(signature);
+        dto.setEnv(env);
+        dto.setSignType(SIGN_TYPE_HMAC_SHA256);
+
+        log.info("生成虚拟支付参数: outTradeNo={}, env={}, offerId={}, productId={}, goodsPrice={}, productInfo={}",
+                outTradeNo, env, offerId, config.getProductId(), totalFee, productInfo);
+        return dto;
+    }
+
+    /**
+     * 测评订单虚拟支付下单参数编排(防循环依赖:编排放本类,AssessmentOrderService 单向被依赖)。
+     * 订单保持 pending;查不到道具映射返回 null(调用方转 46001 "该商品暂未开放购买")。
+     *
+     * @param appointmentId 前端可选传入,回填订单修复 appointmentId 链路断点
+     */
+    public VirtualPayParamsDTO generateAssessmentPayParams(String orderNo, String payType,
+                                                           String sessionKey, Long appointmentId) {
+        // 1. 查订单 + 校验 pending
+        AssessmentOrder order = assessmentOrderService.getByOrderNo(orderNo);
+        if (order == null || !"pending".equals(order.getStatus())) {
+            log.warn("生成测评支付参数失败,订单不存在或非pending: orderNo={}", orderNo);
+            return null;
+        }
+
+        // 2. 回填 appointmentId(修复链路断点:createOrder 从不赋值)
+        if (appointmentId != null && order.getAppointmentId() == null) {
+            order.setAppointmentId(appointmentId);
+            order.setUpdatedAt(new Date());
+            assessmentOrderService.updateById(order);
+            log.info("支付下单回填预约关联: orderNo={}, appointmentId={}", orderNo, appointmentId);
+        }
+
+        // 3. 查 virtual_goods_config:goodsType=ASSESSMENT_PACKAGE, bizKey=订单packageId → null → 调用方 46001
+        VirtualGoodsConfig config = virtualGoodsConfigService.getEnabled("ASSESSMENT_PACKAGE",
+                String.valueOf(order.getPackageId()));
+        if (config == null) {
+            log.warn("测评道具未配置或停用: packageId={}", order.getPackageId());
+            return null;
+        }
+
+        // 4. 服务端重算金额(不信任前端传参):total_fee = getPackagePrice(...).totalPrice
+        Map<String, Object> price = packagePurchaseService.getPackagePrice(order.getPackageId(), order.getGuideId(), null);
+        if (price == null || price.get("totalPrice") == null) {
+            log.warn("测评套餐服务端计价失败: packageId={}", order.getPackageId());
+            return null;
+        }
+        Integer totalFee = ((Number) price.get("totalPrice")).intValue();
+        if (totalFee < 100) { // 金额 >= 1元
+            log.warn("测评套餐金额异常(<1元): packageId={}, totalFee={}", order.getPackageId(), totalFee);
+            return null;
+        }
+
+        // 5. 组装 attach + product_info 并生成支付参数(订单保持 pending,不置 paid)
+        String attach = "assessment:" + order.getPackageId();
+        return generatePayParams(config, orderNo, totalFee, order.getPackageName(), attach, sessionKey);
+    }
+
+    /**
+     * 会员订单虚拟支付下单参数编排(防循环依赖:编排放本类,MembershipService 单向被依赖)。
+     * 订单保持 pending;查不到道具映射返回 null(调用方转 46001 "该商品暂未开放购买")。
+     */
+    public VirtualPayParamsDTO generateMembershipPayParams(String orderNo, String sessionKey) {
+        // 1. 查订单 + 校验 pending
+        PaymentOrder order = membershipService.getByOrderNo(orderNo);
+        if (order == null || !"pending".equals(order.getStatus())) {
+            log.warn("生成会员支付参数失败,订单不存在或非pending: orderNo={}", orderNo);
+            return null;
+        }
+
+        // 2. 查 virtual_goods_config:goodsType=MEMBERSHIP, bizKey=订单levelCode → null → 调用方 46001
+        VirtualGoodsConfig config = virtualGoodsConfigService.getEnabled("MEMBERSHIP", order.getLevelCode());
+        if (config == null) {
+            log.warn("会员道具未配置或停用: levelCode={}", order.getLevelCode());
+            return null;
+        }
+
+        // 3. 金额 = order.amount(服务端已算,不重算;避免前端篡改),校验 >= 1元
+        if (order.getAmount() == null || order.getAmount() < 100) {
+            log.warn("会员订单金额异常(<1元): orderNo={}, amount={}", orderNo, order.getAmount());
+            return null;
+        }
+
+        // 4. 组装 attach + product_info 并生成支付参数(订单保持 pending,不置 paid)
+        String attach = "membership:" + order.getLevelCode();
+        return generatePayParams(config, orderNo, order.getAmount(), config.getGoodsName(), attach, sessionKey);
+    }
+
+    /**
+     * 订阅订单虚拟支付下单参数编排(防循环依赖:编排放本类,MemberSubscriptionService 单向被依赖)。
+     * 订单保持 pending;查不到道具映射返回 null(调用方转 46001 "该商品暂未开放购买")。
+     */
+    public VirtualPayParamsDTO generateSubscriptionPayParams(String orderNo, String sessionKey) {
+        // 1. 查订单 + 校验 pending
+        MemberSubscriptionOrder order = memberSubscriptionService.getByOrderNo(orderNo);
+        if (order == null || !"pending".equals(order.getStatus())) {
+            log.warn("生成订阅支付参数失败,订单不存在或非pending: orderNo={}", orderNo);
+            return null;
+        }
+
+        // 2. 查 virtual_goods_config:goodsType=SUBSCRIPTION, bizKey=订单level → null → 调用方 46001
+        VirtualGoodsConfig config = virtualGoodsConfigService.getEnabled("SUBSCRIPTION", order.getLevel());
+        if (config == null) {
+            log.warn("订阅道具未配置或停用: level={}", order.getLevel());
+            return null;
+        }
+
+        // 3. 金额 = order.amount(服务端已算,不重算;避免前端篡改),校验 >= 1元
+        if (order.getAmount() == null || order.getAmount() < 100) {
+            log.warn("订阅订单金额异常(<1元): orderNo={}, amount={}", orderNo, order.getAmount());
+            return null;
+        }
+
+        // 4. 组装 attach + product_info 并生成支付参数(订单保持 pending,不置 paid)
+        String attach = "subscription:" + order.getLevel();
+        return generatePayParams(config, orderNo, order.getAmount(), config.getGoodsName(), attach, sessionKey);
+    }
+
+    /**
+     * 处理虚拟支付发货推送(xpay_goods_deliver_notify)。
+     * 按 OutTradeNo 前缀路由:A→测评,ORD→会员,SUB→订阅。
+     * 返回微信协议 {@code {"ErrCode":0,"ErrMsg":"success"}}。
+     */
+    public Map<String, Object> handleDeliverNotify(Map<String, Object> payload) {
+        try {
+            String outTradeNo = getStringIgnoreCase(payload, "OutTradeNo", "out_trade_no");
+            String transactionId = getStringIgnoreCase(payload, "TransactionId", "transaction_id");
+
+            if (outTradeNo == null || outTradeNo.isEmpty()) {
+                log.error("虚拟支付发货推送缺少OutTradeNo,返回成功避免微信重试: {}", payload);
+                return successResult();
+            }
+
+            log.info("虚拟支付发货推送: outTradeNo={}, transactionId={}", outTradeNo, transactionId);
+
+            if (outTradeNo.startsWith("A")) {
+                // 幂等:已 paid/refunded 的重复推送直接返回成功,避免重复佣金结算(spec §6.2)
+                AssessmentOrder existing = assessmentOrderService.getByOrderNo(outTradeNo);
+                if (existing != null && ("paid".equals(existing.getStatus()) || "refunded".equals(existing.getStatus()))) {
+                    log.info("虚拟支付发货推送重复通知,订单已终态: outTradeNo={}, status={}", outTradeNo, existing.getStatus());
+                    return successResult();
+                }
+                // 测评订单:标记已付 + 佣金结算 + 预约确认
+                assessmentOrderService.handleVirtualPaySuccess(outTradeNo, transactionId);
+            } else if (outTradeNo.startsWith("ORD")) {
+                // 会员订单:复用 processPaymentCallback(幂等已内置 L341-343)完成全部开通
+                membershipService.processPaymentCallback(outTradeNo, transactionId, "virtual");
+            } else if (outTradeNo.startsWith("SUB")) {
+                // 幂等:已 paid/refunded 的重复推送直接返回成功,避免重复开通/重复佣金结算(spec §6.2)
+                MemberSubscriptionOrder subOrder = memberSubscriptionService.getByOrderNo(outTradeNo);
+                if (subOrder != null && ("paid".equals(subOrder.getStatus()) || "refunded".equals(subOrder.getStatus()))) {
+                    log.info("虚拟支付发货推送重复通知,订阅订单已终态: outTradeNo={}, status={}", outTradeNo, subOrder.getStatus());
+                } else if (subOrder != null) {
+                    // 订阅订单:激活订阅(置旧过期 + 插新订阅 + 订单置paid + 分配管家 + 佣金)
+                    memberSubscriptionService.activateSubscription(subOrder.getFamilyId(), subOrder.getLevel(),
+                            subOrder.getAmount(), "virtual", transactionId, outTradeNo);
+                } else {
+                    log.warn("虚拟支付发货推送订阅订单不存在: outTradeNo={}", outTradeNo);
+                }
+            } else {
+                log.warn("虚拟支付发货推送未知订单前缀: outTradeNo={}", outTradeNo);
+            }
+            return successResult();
+        } catch (Exception e) {
+            log.error("处理虚拟支付发货推送异常", e);
+            return failResult("处理异常");
+        }
+    }
+
+    /**
+     * 处理虚拟支付退款推送(xpay_refund_notify)。
+     * 按 OutTradeNo 前缀路由:A→测评退款,SUB→订阅退款,ORD→TODO。
+     */
+    public Map<String, Object> handleRefundNotify(Map<String, Object> payload) {
+        try {
+            String outTradeNo = getStringIgnoreCase(payload, "OutTradeNo", "out_trade_no");
+
+            if (outTradeNo == null || outTradeNo.isEmpty()) {
+                log.error("虚拟支付退款推送缺少OutTradeNo,返回成功避免微信重试: {}", payload);
+                return successResult();
+            }
+
+            log.info("虚拟支付退款推送: outTradeNo={}", outTradeNo);
+
+            if (outTradeNo.startsWith("A")) {
+                assessmentOrderService.refundOrder(outTradeNo);
+            } else if (outTradeNo.startsWith("SUB")) {
+                // 订阅退款:订单置 refunded + 订阅置 expired(iOS App Store 退款场景)
+                memberSubscriptionService.refundByOrderNo(outTradeNo);
+            } else if (outTradeNo.startsWith("ORD")) {
+                // TODO 后续: 会员退款处理
+                log.warn("虚拟支付退款推送暂未接入会员处理: outTradeNo={}", outTradeNo);
+            } else {
+                log.warn("虚拟支付退款推送未知订单前缀: outTradeNo={}", outTradeNo);
+            }
+            return successResult();
+        } catch (Exception e) {
+            log.error("处理虚拟支付退款推送异常", e);
+            return failResult("处理异常");
+        }
+    }
+
+    /**
+     * 查询虚拟支付订单实际支付状态(防 success 回调丢失的兜底,spec §7)。
+     * 按官方规范:pay_sig = to_hex(hmac_sha256(appKey, "/xpay/query_order&" + post_body)),
+     * post_body 必须与真正发送的 HTTP body 完全一致。
+     *
+     * @param outTradeNo 业务订单号(A/ORD/SUB 前缀)
+     * @param openid     用户 openid(官方必填,调用方负责解析)
+     * @return 查询结果;查询失败/响应异常返回 null(调用方不得据此关单,防误杀已支付订单)
+     */
+    public QueryOrderResult queryOrder(String outTradeNo, String openid) {
+        // 1. 测试模式:不调微信,返回"未支付"模拟结果(status=1),让超时关单路径在测试环境可跑
+        if (testMode) {
+            log.info("【测试模式】跳过query_order查询: outTradeNo={}", outTradeNo);
+            return new QueryOrderResult(false, 1, null);
+        }
+        try {
+            // 2. 组装 body(LinkedHashMap 保序;order_id 传业务订单号)
+            Map<String, Object> body = new LinkedHashMap<>();
+            body.put("openid", openid);
+            body.put("env", env);
+            body.put("order_id", outTradeNo);
+            body.put("wx_order_id", "");
+
+            // 3. post_body = 序列化结果,签名与发送必须用同一字符串(官方强调完全一致)
+            String postBody = JSON.toJSONString(body);
+
+            // 4. pay_sig = hmac_sha256(appKey, "/xpay/query_order&" + postBody)
+            String paySig = buildQueryOrderPaySig(postBody);
+
+            // 5. HTTP 调用(access_token 复用 WechatService 缓存)
+            String url = "https://api.weixin.qq.com/xpay/query_order?access_token="
+                    + wechatService.getAccessToken() + "&pay_sig=" + paySig;
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            HttpEntity<String> entity = new HttpEntity<>(postBody, headers);
+            String respBody = restTemplate.postForObject(url, entity, String.class);
+
+            // 6. 解析响应
+            JSONObject resp = JSON.parseObject(respBody);
+            int errcode = resp.getIntValue("errcode");
+            if (errcode != 0) {
+                log.warn("query_order返回错误: outTradeNo={}, errcode={}, errmsg={}",
+                        outTradeNo, errcode, resp.getString("errmsg"));
+                return null;   // 查询失败:调用方不得关单
+            }
+            JSONObject order = resp.getJSONObject("order");
+            if (order == null) {
+                log.warn("query_order响应缺少order: outTradeNo={}", outTradeNo);
+                return null;
+            }
+            int status = order.getIntValue("status");
+            String wxpayOrderId = order.getString("wxpay_order_id");
+            boolean paid = status == 2 || status == 3 || status == 4;  // 已支付判定
+            log.info("query_order结果: outTradeNo={}, status={}, paid={}", outTradeNo, status, paid);
+            return new QueryOrderResult(paid, status, wxpayOrderId);
+        } catch (Exception e) {
+            log.error("query_order查询异常: outTradeNo={}", outTradeNo, e);
+            return null;   // 异常:调用方不得关单
+        }
+    }
+
+    /**
+     * 构建 query_order 的 pay_sig(package-private,便于单测断言)。
+     * 官方规则:hmac_sha256(appKey, uri + '&' + post_body),uri=/xpay/query_order,
+     * post_body 为原始 JSON body 串(与 requestVirtualPayment 的 signData 同样是对 JSON 串签名)。
+     */
+    String buildQueryOrderPaySig(String postBody) {
+        return HmacSignUtil.hmacSha256Hex(effectiveAppKey(), "/xpay/query_order&" + postBody);
+    }
+
+    /** query_order 查询结果(内部静态类,免新文件) */
+    public static class QueryOrderResult {
+        /** 是否已支付(status∈{2,3,4}) */
+        public final boolean paid;
+        /** 微信侧订单状态枚举 */
+        public final int status;
+        /** 微信交易单号(order.wxpay_order_id) */
+        public final String wxpayOrderId;
+
+        public QueryOrderResult(boolean paid, int status, String wxpayOrderId) {
+            this.paid = paid;
+            this.status = status;
+            this.wxpayOrderId = wxpayOrderId;
+        }
+    }
+
+    /** 从 payload 按多个候选 key(含大小写不敏感兜底)取字符串值 */
+    private String getStringIgnoreCase(Map<String, Object> payload, String... keys) {
+        if (payload == null) {
+            return null;
+        }
+        for (String key : keys) {
+            Object value = payload.get(key);
+            if (value != null) {
+                return String.valueOf(value);
+            }
+        }
+        for (Map.Entry<String, Object> entry : payload.entrySet()) {
+            for (String key : keys) {
+                if (key.equalsIgnoreCase(entry.getKey()) && entry.getValue() != null) {
+                    return String.valueOf(entry.getValue());
+                }
+            }
+        }
+        return null;
+    }
+
+    private Map<String, Object> successResult() {
+        Map<String, Object> result = new HashMap<>();
+        result.put("ErrCode", 0);
+        result.put("ErrMsg", "success");
+        return result;
+    }
+
+    private Map<String, Object> failResult(String msg) {
+        Map<String, Object> result = new HashMap<>();
+        result.put("ErrCode", 1);
+        result.put("ErrMsg", msg);
+        return result;
+    }
+}

+ 232 - 0
cfc-backend/src/main/java/com/etotem/cfc/task/VirtualPayTimeoutTask.java

@@ -0,0 +1,232 @@
+package com.etotem.cfc.task;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.AssessmentOrder;
+import com.etotem.cfc.entity.Family;
+import com.etotem.cfc.entity.MemberSubscription;
+import com.etotem.cfc.entity.MemberSubscriptionOrder;
+import com.etotem.cfc.entity.PaymentOrder;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.AssessmentOrderMapper;
+import com.etotem.cfc.mapper.FamilyMapper;
+import com.etotem.cfc.mapper.MemberSubscriptionOrderMapper;
+import com.etotem.cfc.mapper.PaymentOrderMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.AssessmentOrderService;
+import com.etotem.cfc.service.MemberSubscriptionService;
+import com.etotem.cfc.service.MembershipService;
+import com.etotem.cfc.service.VirtualPayService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 虚拟支付超时兜底任务(spec §7:success 回调丢失防丢单)。
+ * 每分钟扫描三张订单表(测评/会员/订阅)status=pending 且创建超过 N 分钟的订单:
+ * 1. 调 VirtualPayService.queryOrder 向微信确认实际支付状态(兜底发货推送丢失)
+ * 2. 已支付 → 触发对应解锁逻辑(幂等,与发货推送同路径)
+ * 3. 明确未支付/已关闭 → 置 cancelled
+ * 4. 查询失败/无法解析 openid → 跳过本次(防误杀已支付订单),下轮再查
+ */
+@Component
+public class VirtualPayTimeoutTask {
+    private static final Logger log = LoggerFactory.getLogger(VirtualPayTimeoutTask.class);
+
+    @Resource
+    private VirtualPayService virtualPayService;
+
+    @Resource
+    private AssessmentOrderService assessmentOrderService;
+
+    @Resource
+    private MembershipService membershipService;
+
+    @Resource
+    private MemberSubscriptionService memberSubscriptionService;
+
+    @Resource
+    private AssessmentOrderMapper assessmentOrderMapper;
+
+    @Resource
+    private PaymentOrderMapper paymentOrderMapper;
+
+    @Resource
+    private MemberSubscriptionOrderMapper memberSubscriptionOrderMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private FamilyMapper familyMapper;
+
+    @Value("${virtual-pay.payment-timeout-minutes:30}")
+    private int paymentTimeoutMinutes;
+
+    @Scheduled(fixedRate = 60000) // 每分钟执行一次
+    public void handleTimeoutOrders() {
+        Calendar cal = Calendar.getInstance();
+        cal.add(Calendar.MINUTE, -paymentTimeoutMinutes);
+        Date timeoutTime = cal.getTime();
+
+        // 1. 扫描三张表(status=pending 且创建超过 N 分钟)
+        LambdaQueryWrapper<AssessmentOrder> assessmentWrapper = new LambdaQueryWrapper<AssessmentOrder>()
+                .eq(AssessmentOrder::getStatus, "pending")
+                .lt(AssessmentOrder::getCreatedAt, timeoutTime);
+        List<AssessmentOrder> assessmentOrders = assessmentOrderMapper.selectList(assessmentWrapper);
+
+        LambdaQueryWrapper<PaymentOrder> paymentWrapper = new LambdaQueryWrapper<PaymentOrder>()
+                .eq(PaymentOrder::getStatus, "pending")
+                .lt(PaymentOrder::getCreatedAt, timeoutTime);
+        List<PaymentOrder> paymentOrders = paymentOrderMapper.selectList(paymentWrapper);
+
+        LambdaQueryWrapper<MemberSubscriptionOrder> subWrapper = new LambdaQueryWrapper<MemberSubscriptionOrder>()
+                .eq(MemberSubscriptionOrder::getStatus, "pending")
+                .lt(MemberSubscriptionOrder::getCreatedAt, timeoutTime);
+        List<MemberSubscriptionOrder> subOrders = memberSubscriptionOrderMapper.selectList(subWrapper);
+
+        // 2. 每单处理:解析 openid → queryOrder → 已支付解锁 / 未支付关单 / 失败跳过
+        int assessmentHandled = 0;
+        int membershipHandled = 0;
+        int subscriptionHandled = 0;
+        for (AssessmentOrder order : assessmentOrders) {
+            if (handleAssessmentOrder(order)) {
+                assessmentHandled++;
+            }
+        }
+        for (PaymentOrder order : paymentOrders) {
+            if (handlePaymentOrder(order)) {
+                membershipHandled++;
+            }
+        }
+        for (MemberSubscriptionOrder order : subOrders) {
+            if (handleSubscriptionOrder(order)) {
+                subscriptionHandled++;
+            }
+        }
+
+        // 3. 汇总日志
+        log.info("虚拟支付超时兜底扫描完成: 测评={}/{}, 会员={}/{}, 订阅={}/{}",
+                assessmentHandled, assessmentOrders.size(),
+                membershipHandled, paymentOrders.size(),
+                subscriptionHandled, subOrders.size());
+    }
+
+    /** A 前缀(测评订单)单笔处理,返回是否处理(解锁/关单) */
+    private boolean handleAssessmentOrder(AssessmentOrder order) {
+        // openid = order.getUserId() → User.getOpenid()(A 单买家即下单用户)
+        User user = userMapper.selectById(order.getUserId());
+        if (user == null || user.getOpenid() == null || user.getOpenid().isEmpty()) {
+            log.warn("虚拟支付超时兜底: 测评订单openid无法解析, 跳过不关单: orderNo={}", order.getOrderNo());
+            return false;
+        }
+        VirtualPayService.QueryOrderResult result = virtualPayService.queryOrder(order.getOrderNo(), user.getOpenid());
+        if (result == null) {
+            log.warn("虚拟支付超时兜底: 测评订单queryOrder失败, 跳过不关单: orderNo={}", order.getOrderNo());
+            return false;
+        }
+        if (result.paid) {
+            // 幂等:已 paid/refunded 返回 true,防重复佣金
+            boolean ok = assessmentOrderService.handleVirtualPaySuccess(order.getOrderNo(), result.wxpayOrderId);
+            log.info("虚拟支付超时兜底: 测评订单已支付, 触发解锁: orderNo={}, ok={}", order.getOrderNo(), ok);
+            return true;
+        }
+        if (result.status == 0 || result.status == 1 || result.status == 6) {
+            // 明确未支付/已关闭 → 置 cancelled
+            order.setStatus("cancelled");
+            order.setUpdatedAt(new Date());
+            assessmentOrderMapper.updateById(order);
+            log.info("虚拟支付超时兜底: 测评订单未支付, 置cancelled: orderNo={}, status={}",
+                    order.getOrderNo(), result.status);
+            return true;
+        }
+        // status∈{5,7,8,9,10} 退款相关 → 跳过,不关单
+        log.info("虚拟支付超时兜底: 测评订单退款相关状态, 跳过: orderNo={}, status={}", order.getOrderNo(), result.status);
+        return false;
+    }
+
+    /** ORD 前缀(会员订单)单笔处理,返回是否处理(解锁/关单) */
+    private boolean handlePaymentOrder(PaymentOrder order) {
+        String openid = resolveFamilyCreatorOpenid(order.getFamilyId());
+        if (openid == null) {
+            log.warn("虚拟支付超时兜底: 会员订单openid无法解析, 跳过不关单: orderNo={}", order.getOrderNo());
+            return false;
+        }
+        VirtualPayService.QueryOrderResult result = virtualPayService.queryOrder(order.getOrderNo(), openid);
+        if (result == null) {
+            log.warn("虚拟支付超时兜底: 会员订单queryOrder失败, 跳过不关单: orderNo={}", order.getOrderNo());
+            return false;
+        }
+        if (result.paid) {
+            // 幂等:paid 返回 true;trial 直接 true
+            boolean ok = membershipService.processPaymentCallback(order.getOrderNo(), result.wxpayOrderId, "virtual");
+            log.info("虚拟支付超时兜底: 会员订单已支付, 触发开通: orderNo={}, ok={}", order.getOrderNo(), ok);
+            return true;
+        }
+        if (result.status == 0 || result.status == 1 || result.status == 6) {
+            order.setStatus("cancelled");
+            order.setUpdatedAt(new Date());
+            paymentOrderMapper.updateById(order);
+            log.info("虚拟支付超时兜底: 会员订单未支付, 置cancelled: orderNo={}, status={}",
+                    order.getOrderNo(), result.status);
+            return true;
+        }
+        // status∈{5,7,8,9,10} 退款相关 → 跳过,不关单
+        log.info("虚拟支付超时兜底: 会员订单退款相关状态, 跳过: orderNo={}, status={}", order.getOrderNo(), result.status);
+        return false;
+    }
+
+    /** SUB 前缀(订阅订单)单笔处理,返回是否处理(解锁/关单) */
+    private boolean handleSubscriptionOrder(MemberSubscriptionOrder order) {
+        String openid = resolveFamilyCreatorOpenid(order.getFamilyId());
+        if (openid == null) {
+            log.warn("虚拟支付超时兜底: 订阅订单openid无法解析, 跳过不关单: orderNo={}", order.getOrderNo());
+            return false;
+        }
+        VirtualPayService.QueryOrderResult result = virtualPayService.queryOrder(order.getOrderNo(), openid);
+        if (result == null) {
+            log.warn("虚拟支付超时兜底: 订阅订单queryOrder失败, 跳过不关单: orderNo={}", order.getOrderNo());
+            return false;
+        }
+        if (result.paid) {
+            // 幂等:已 paid/refunded return null
+            MemberSubscription activated = memberSubscriptionService.activateSubscription(order.getFamilyId(),
+                    order.getLevel(), order.getAmount(), "virtual", result.wxpayOrderId, order.getOrderNo());
+            log.info("虚拟支付超时兜底: 订阅订单已支付, 触发激活: orderNo={}, activated={}",
+                    order.getOrderNo(), activated != null);
+            return true;
+        }
+        if (result.status == 0 || result.status == 1 || result.status == 6) {
+            order.setStatus("cancelled");
+            memberSubscriptionOrderMapper.updateById(order);
+            log.info("虚拟支付超时兜底: 订阅订单未支付, 置cancelled: orderNo={}, status={}",
+                    order.getOrderNo(), result.status);
+            return true;
+        }
+        // status∈{5,7,8,9,10} 退款相关 → 跳过,不关单
+        log.info("虚拟支付超时兜底: 订阅订单退款相关状态, 跳过: orderNo={}, status={}", order.getOrderNo(), result.status);
+        return false;
+    }
+
+    /**
+     * 解析家庭创建者(管理员)的 openid(ORD/SUB 单范式,同 MembershipService.processPaymentCallback)。
+     * 家庭或创建者缺失 → 返回 null(调用方不得关单)。
+     */
+    private String resolveFamilyCreatorOpenid(Long familyId) {
+        Family family = familyMapper.selectById(familyId);
+        if (family == null || family.getCreatorId() == null) {
+            return null;
+        }
+        User buyer = userMapper.selectById(family.getCreatorId());
+        if (buyer == null || buyer.getOpenid() == null || buyer.getOpenid().isEmpty()) {
+            return null;
+        }
+        return buyer.getOpenid();
+    }
+}

+ 13 - 7
cfc-backend/src/main/resources/application.yml

@@ -16,9 +16,9 @@ spring:
     name: cfc-backend
   datasource:
     driver-class-name: com.mysql.cj.jdbc.Driver
-    url: jdbc:mysql://192.168.16.251:3306/zxyj?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true
-    username: zxyj
-    password: zxyj@123
+    url: jdbc:mysql://mysql-internet-cn-north-1-23feae22680e4dfa.rds.jdcloud.com:3306/zxyj?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true
+    username: cfc
+    password: cfc@1314
     hikari:
       maximum-pool-size: 50
       minimum-idle: 10
@@ -31,9 +31,9 @@ sfms:
   datasource:
     enabled: false  # 本地开发禁用 SFMS 数据源,需要迁移时改为 true
     driver-class-name: com.mysql.cj.jdbc.Driver
-    url: jdbc:mysql://192.168.16.251:3306/zxyj?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true
-    username: zxyj
-    password: zxyj@123
+    url: jdbc:mysql://mysql-internet-cn-north-1-23feae22680e4dfa.rds.jdcloud.com:3306/zxyj?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true
+    username: cfc
+    password: cfc@1314
 
 mybatis-plus:
   mapper-locations: classpath*:/mapper/**/*.xml
@@ -76,8 +76,11 @@ wechat:
   login-url: https://api.weixin.qq.com/sns/jscode2session
   phone-url: https://api.weixin.qq.com/wxa/business/getuserphonenumber
   test-mode: false  # 测试模式:跳过微信API调用,使用模拟数据
-  appkey: ${WECHAT_APPKEY:}                # 虚拟支付 AppKey(商户后台基本配置获取,现网/沙箱分开)
+  appkey: ${WECHAT_APPKEY:cXqYJseX2MUaRM5kVHLQRskw5l5ZrDNo}          # 虚拟支付 AppKey 现网(环境变量可覆盖)
+  sandbox-appkey: ${WECHAT_SANDBOX_APPKEY:sKcsYR0e9T603wvD5yFcvISBcFLh3TXH}  # 虚拟支付 AppKey 沙箱(环境变量可覆盖)
   virtual-pay-env: ${WECHAT_VIRTUAL_PAY_ENV:0}   # 0=现网 1=沙箱
+  virtual-pay-token: ${WECHAT_VIRTUAL_PAY_TOKEN:cfcVirtualPay2026}  # 虚拟支付消息推送 Token(与小程序后台消息推送配置一致)
+  offer-id: ${WECHAT_VIRTUAL_PAY_OFFER_ID:1450607643}  # 虚拟支付应用ID(mp-支付基础配置中的 offerId;productId 才是道具ID)
   test-phone: "13800138000"  # 测试模式使用的手机号
   alert-template-id: ${WECHAT_ALERT_TEMPLATE_ID:}
   promotion-tier-template-id: ${WECHAT_PROMOTION_TIER_TEMPLATE_ID:}
@@ -111,6 +114,9 @@ shop:
   order:
     payment-timeout-minutes: 30  # 订单超时时间(分钟)
 
+virtual-pay:
+  payment-timeout-minutes: 30  # 虚拟支付订单超时时间(分钟)
+
 logging:
   level:
     com.etotem.cfc: debug

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

@@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS users (
     nickname VARCHAR(50),
     avatar VARCHAR(255),
     password VARCHAR(128),
+    session_key VARCHAR(64) COMMENT '微信登录会话密钥(虚拟支付签名用)',
     id_card VARCHAR(18),
     real_name VARCHAR(50),
     phone VARCHAR(11),

+ 2 - 2
cfc-backend/src/test/java/com/etotem/cfc/controller/family/FamilyMembersControllerTest.java

@@ -115,7 +115,7 @@ public class FamilyMembersControllerTest {
     @Test
     public void switchToMember_Success() {
         Long userId = 1L;
-        Map<String, Long> body = new HashMap<>();
+        Map<String, Object> body = new HashMap<>();
         body.put("memberId", 2L);
 
         SwitchMemberVO vo = new SwitchMemberVO();
@@ -131,7 +131,7 @@ public class FamilyMembersControllerTest {
     @Test
     public void switchToMember_MissingMemberId() {
         Long userId = 1L;
-        Map<String, Long> body = new HashMap<>();
+        Map<String, Object> body = new HashMap<>();
 
         Result<SwitchMemberVO> result = controller.switchToMember(body, userId);
         assertEquals(500, result.getCode());

+ 2 - 2
cfc-backend/src/test/java/com/etotem/cfc/integration/FamilyMemberManagementFlowTest.java

@@ -200,7 +200,7 @@ public class FamilyMemberManagementFlowTest {
     @DisplayName("[场景5] 切换到成员视角 - 期望返回切换凭证并正确设置上下文")
     void parentSwitchesToMemberView_Success() {
         // === Given:选择了要切换的成员 ===
-        Map<String, Long> switchRequest = new HashMap<>();
+        Map<String, Object> switchRequest = new HashMap<>();
         switchRequest.put("memberId", CHILD_MEMBER_ID);
 
         SwitchMemberVO mockSwitchVO = new SwitchMemberVO();
@@ -231,7 +231,7 @@ public class FamilyMemberManagementFlowTest {
     @DisplayName("[场景5b] 切换到本人视角 - 期望返回错误提示")
     void parentSwitchesToSelf_Fails() {
         // === Given:选择了自己的用户ID作为成员ID ===
-        Map<String, Long> switchRequest = new HashMap<>();
+        Map<String, Object> switchRequest = new HashMap<>();
         switchRequest.put("memberId", PARENT_USER_ID); // 自己的用户ID
 
         when(familyMemberService.switchToMember(PARENT_USER_ID, PARENT_USER_ID))

+ 305 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/VirtualPayServiceTest.java

@@ -0,0 +1,305 @@
+package com.etotem.cfc.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import com.alibaba.fastjson.JSON;
+import com.etotem.cfc.common.HmacSignUtil;
+import com.etotem.cfc.dto.VirtualPayParamsDTO;
+import com.etotem.cfc.entity.MemberSubscriptionOrder;
+import com.etotem.cfc.entity.PaymentOrder;
+import com.etotem.cfc.entity.VirtualGoodsConfig;
+
+@ExtendWith(MockitoExtension.class)
+class VirtualPayServiceTest {
+
+    @Mock
+    private AssessmentOrderService assessmentOrderService;
+
+    @Mock
+    private MembershipService membershipService;
+
+    @Mock
+    private MemberSubscriptionService memberSubscriptionService;
+
+    @Mock
+    private VirtualGoodsConfigService virtualGoodsConfigService;
+
+    @Mock
+    private WechatService wechatService;
+
+    @InjectMocks
+    private VirtualPayService virtualPayService;
+
+    @Test
+    void hmacSha256Hex_matchesRfc4231TestVector() {
+        // RFC 4231 Test Case 2 标准测试向量
+        String actual = HmacSignUtil.hmacSha256Hex("Jefe", "what do ya want for nothing?");
+        assertEquals("5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843", actual);
+    }
+
+    @Test
+    void generatePayParams_returnsCompleteDto() throws Exception {
+        // 纯单测环境不注入 Spring @Value,通过反射填充配置字段
+        setField("offerId", "1450607643");
+        setField("appKey", "test_app_key");
+        setField("env", 0);
+
+        VirtualGoodsConfig config = new VirtualGoodsConfig();
+        config.setProductId("offer_1001");
+
+        VirtualPayParamsDTO dto = virtualPayService.generatePayParams(
+                config, "A20260802001", 9900, "五维测评·基础版", "ASSESSMENT_PACKAGE:1", "test_session_key");
+
+        assertNotNull(dto);
+        assertNotNull(dto.getOrderInfo());
+        assertEquals("1450607643", dto.getOrderInfo().get("offerId"));
+        assertEquals(1, dto.getOrderInfo().get("buyQuantity"));
+        assertEquals(0, dto.getOrderInfo().get("env"));
+        assertEquals("CNY", dto.getOrderInfo().get("currencyType"));
+        assertEquals("offer_1001", dto.getOrderInfo().get("productId"));
+        assertEquals(9900, dto.getOrderInfo().get("goodsPrice"));
+        assertEquals("A20260802001", dto.getOrderInfo().get("outTradeNo"));
+        assertEquals("ASSESSMENT_PACKAGE:1", dto.getOrderInfo().get("attach"));
+        assertEquals(0, dto.getEnv());
+        assertEquals("HMAC-SHA256", dto.getSignType());
+        assertNotNull(dto.getExtInfo());
+        assertNotNull(dto.getSign());
+        assertFalse(dto.getSign().isEmpty());
+        assertNotNull(dto.getSignature());
+        assertFalse(dto.getSignature().isEmpty());
+        // 双签名值不同(paySig 用 appKey、signature 用 sessionKey)
+        org.junit.jupiter.api.Assertions.assertNotEquals(dto.getSign(), dto.getSignature());
+    }
+
+    @Test
+    void handleDeliverNotify_assessmentPrefixRoutesToHandleVirtualPaySuccess() {
+        Map<String, Object> payload = new HashMap<>();
+        payload.put("OutTradeNo", "A20260802001");
+        payload.put("TransactionId", "WX1234567890");
+
+        Map<String, Object> result = virtualPayService.handleDeliverNotify(payload);
+
+        assertEquals(0, result.get("ErrCode"));
+        assertEquals("success", result.get("ErrMsg"));
+        verify(assessmentOrderService, times(1)).handleVirtualPaySuccess("A20260802001", "WX1234567890");
+    }
+
+    @Test
+    void handleDeliverNotify_missingOutTradeNoReturnsAck() {
+        Map<String, Object> payload = new HashMap<>();
+        payload.put("TransactionId", "WX1234567890");
+
+        Map<String, Object> result = virtualPayService.handleDeliverNotify(payload);
+
+        assertEquals(0, result.get("ErrCode"));
+        verify(assessmentOrderService, times(0)).paySuccess(org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
+    }
+
+    @Test
+    void handleRefundNotify_assessmentPrefixCallsRefundOrder() {
+        Map<String, Object> payload = new HashMap<>();
+        payload.put("OutTradeNo", "A20260802001");
+
+        Map<String, Object> result = virtualPayService.handleRefundNotify(payload);
+
+        assertEquals(0, result.get("ErrCode"));
+        assertEquals("success", result.get("ErrMsg"));
+        verify(assessmentOrderService, times(1)).refundOrder("A20260802001");
+    }
+
+    @Test
+    void generatePayParams_signsOfficialJsonSignData() throws Exception {
+        // 对照官方签名规则:
+        //   signData  = orderInfo 的 JSON 串(camelCase 官方字段、插入序,与前端 JSON.stringify 逐字节一致)
+        //   paySig    = hmacSha256Hex(appKey, "requestVirtualPayment&" + signData)
+        //   signature = hmacSha256Hex(sessionKey, signData)
+        setField("offerId", "1450607643");
+        setField("appKey", "test_app_key");
+        setField("env", 0);
+
+        VirtualGoodsConfig config = new VirtualGoodsConfig();
+        config.setProductId("offer_1001");
+
+        VirtualPayParamsDTO dto = virtualPayService.generatePayParams(
+                config, "A20260802001", 9900, "五维测评·基础版", "ASSESSMENT_PACKAGE:1", "test_session_key");
+
+        // 期望 signData:官方 camelCase 字段、插入序序列化(锁定格式,防回归到旧字典序拼接)
+        String expectedSignData = "{\"offerId\":\"1450607643\",\"buyQuantity\":1,\"env\":0,"
+                + "\"currencyType\":\"CNY\",\"productId\":\"offer_1001\",\"goodsPrice\":9900,"
+                + "\"outTradeNo\":\"A20260802001\",\"attach\":\"ASSESSMENT_PACKAGE:1\"}";
+        assertEquals(expectedSignData, JSON.toJSONString(dto.getOrderInfo()));
+        // paySig / signature 均基于同一 JSON 串(与前端实际发送的 signData 逐字节一致)
+        assertEquals(HmacSignUtil.hmacSha256Hex("test_app_key", "requestVirtualPayment&" + expectedSignData), dto.getSign());
+        assertEquals(HmacSignUtil.hmacSha256Hex("test_session_key", expectedSignData), dto.getSignature());
+    }
+
+    @Test
+    void handleDeliverNotify_membershipPrefixRoutesToProcessPaymentCallback() {
+        Map<String, Object> payload = new HashMap<>();
+        payload.put("OutTradeNo", "ORD20260802001");
+        payload.put("TransactionId", "WX1234567890");
+
+        Map<String, Object> result = virtualPayService.handleDeliverNotify(payload);
+
+        assertEquals(0, result.get("ErrCode"));
+        assertEquals("success", result.get("ErrMsg"));
+        verify(membershipService, times(1)).processPaymentCallback("ORD20260802001", "WX1234567890", "virtual");
+    }
+
+    @Test
+    void generateMembershipPayParams_returnsDtoForConfiguredLevel() throws Exception {
+        setField("offerId", "1450607643");
+        setField("appKey", "test_app_key");
+        setField("env", 0);
+
+        PaymentOrder order = new PaymentOrder();
+        order.setOrderNo("ORD20260802001");
+        order.setLevelCode("FAMILY");
+        order.setAmount(131400);
+        order.setStatus("pending");
+        when(membershipService.getByOrderNo("ORD20260802001")).thenReturn(order);
+
+        VirtualGoodsConfig config = new VirtualGoodsConfig();
+        config.setProductId("offer_m1");
+        config.setGoodsName("家庭会员年卡");
+        when(virtualGoodsConfigService.getEnabled("MEMBERSHIP", "FAMILY")).thenReturn(config);
+
+        VirtualPayParamsDTO dto = virtualPayService.generateMembershipPayParams("ORD20260802001", "test_session_key");
+
+        assertNotNull(dto);
+        assertEquals("1450607643", dto.getOrderInfo().get("offerId"));
+        assertEquals("offer_m1", dto.getOrderInfo().get("productId"));
+        assertEquals(131400, dto.getOrderInfo().get("goodsPrice"));
+        assertEquals("ORD20260802001", dto.getOrderInfo().get("outTradeNo"));
+        assertEquals("membership:FAMILY", dto.getOrderInfo().get("attach"));
+    }
+
+    @Test
+    void generateMembershipPayParams_returnsNullWhenConfigMissing() {
+        PaymentOrder order = new PaymentOrder();
+        order.setOrderNo("ORD20260802002");
+        order.setLevelCode("PREMIUM");
+        order.setAmount(1316800);
+        order.setStatus("pending");
+        when(membershipService.getByOrderNo("ORD20260802002")).thenReturn(order);
+        when(virtualGoodsConfigService.getEnabled("MEMBERSHIP", "PREMIUM")).thenReturn(null);
+
+        assertNull(virtualPayService.generateMembershipPayParams("ORD20260802002", "test_session_key"));
+    }
+
+    @Test
+    void handleDeliverNotify_subscriptionPrefixRoutesToActivateSubscription() {
+        Map<String, Object> payload = new HashMap<>();
+        payload.put("OutTradeNo", "SUB20260802001");
+        payload.put("TransactionId", "WX1234567890");
+
+        MemberSubscriptionOrder order = new MemberSubscriptionOrder();
+        order.setOrderNo("SUB20260802001");
+        order.setFamilyId(1L);
+        order.setLevel("L1");
+        order.setAmount(36500);
+        order.setStatus("pending");
+        when(memberSubscriptionService.getByOrderNo("SUB20260802001")).thenReturn(order);
+
+        Map<String, Object> result = virtualPayService.handleDeliverNotify(payload);
+
+        assertEquals(0, result.get("ErrCode"));
+        assertEquals("success", result.get("ErrMsg"));
+        verify(memberSubscriptionService, times(1))
+                .activateSubscription(1L, "L1", 36500, "virtual", "WX1234567890", "SUB20260802001");
+    }
+
+    @Test
+    void generateSubscriptionPayParams_returnsDtoForConfiguredLevel() throws Exception {
+        setField("offerId", "1450607643");
+        setField("appKey", "test_app_key");
+        setField("env", 0);
+
+        MemberSubscriptionOrder order = new MemberSubscriptionOrder();
+        order.setOrderNo("SUB20260802001");
+        order.setLevel("L1");
+        order.setAmount(36500);
+        order.setStatus("pending");
+        when(memberSubscriptionService.getByOrderNo("SUB20260802001")).thenReturn(order);
+
+        VirtualGoodsConfig config = new VirtualGoodsConfig();
+        config.setProductId("offer_sub_l1");
+        config.setGoodsName("一生一世L1");
+        when(virtualGoodsConfigService.getEnabled("SUBSCRIPTION", "L1")).thenReturn(config);
+
+        VirtualPayParamsDTO dto = virtualPayService.generateSubscriptionPayParams("SUB20260802001", "test_session_key");
+
+        assertNotNull(dto);
+        assertEquals("1450607643", dto.getOrderInfo().get("offerId"));
+        assertEquals("offer_sub_l1", dto.getOrderInfo().get("productId"));
+        assertEquals(36500, dto.getOrderInfo().get("goodsPrice"));
+        assertEquals("SUB20260802001", dto.getOrderInfo().get("outTradeNo"));
+        assertEquals("subscription:L1", dto.getOrderInfo().get("attach"));
+    }
+
+    @Test
+    void generateSubscriptionPayParams_returnsNullWhenConfigMissing() {
+        MemberSubscriptionOrder order = new MemberSubscriptionOrder();
+        order.setOrderNo("SUB20260802002");
+        order.setLevel("L2");
+        order.setAmount(131400);
+        order.setStatus("pending");
+        when(memberSubscriptionService.getByOrderNo("SUB20260802002")).thenReturn(order);
+        when(virtualGoodsConfigService.getEnabled("SUBSCRIPTION", "L2")).thenReturn(null);
+
+        assertNull(virtualPayService.generateSubscriptionPayParams("SUB20260802002", "test_session_key"));
+    }
+
+    @Test
+    void queryOrder_testModeReturnsUnpaidWithoutHttp() throws Exception {
+        setField("testMode", true);
+        setField("env", 0);
+        VirtualPayService.QueryOrderResult result = virtualPayService.queryOrder("A20260802001", "test_openid");
+        assertNotNull(result);
+        assertFalse(result.paid);
+        assertEquals(1, result.status);
+        // 测试模式不触发 HTTP(wechatService 未被调用)
+        verify(wechatService, times(0)).getAccessToken();
+    }
+
+    @Test
+    void queryOrder_buildsPaySigWithUriAndRawBody() throws Exception {
+        // 对照官方规则: pay_sig = hmacSha256Hex(appKey, "/xpay/query_order&" + post_body)
+        setField("appKey", "test_app_key");
+        setField("env", 0);
+        // 验证签名拼接规则:uri 前缀 + 原始 body(与 generatePayParams 的 buildSignData 不同!)
+        String postBody = JSON.toJSONString(new LinkedHashMap<String, Object>() {{
+            put("openid", "test_openid");
+            put("env", 0);
+            put("order_id", "A20260802001");
+            put("wx_order_id", "");
+        }});
+        String expectedPaySig = HmacSignUtil.hmacSha256Hex("test_app_key", "/xpay/query_order&" + postBody);
+        // package-private buildQueryOrderPaySig 直接断言签名构建逻辑
+        assertEquals(expectedPaySig, virtualPayService.buildQueryOrderPaySig(postBody));
+    }
+
+    private void setField(String name, Object value) throws Exception {
+        Field field = VirtualPayService.class.getDeclaredField(name);
+        field.setAccessible(true);
+        field.set(virtualPayService, value);
+    }
+}

+ 27 - 2
cfc-frontend/components/FamilyMemberStrip.vue

@@ -43,10 +43,18 @@
         <text class="action-item-icon">🔄</text>
         <text class="action-item-text">切换到该成员视角</text>
       </view>
-      <view class="action-item" @click="editMember">
+      <view class="action-item" @click="editMember" v-if="canEdit">
         <text class="action-item-icon">✏️</text>
         <text class="action-item-text">编辑资料</text>
       </view>
+      <view class="action-item" @click="questionnaireMember">
+        <text class="action-item-icon">📋</text>
+        <text class="action-item-text">关系问卷</text>
+      </view>
+      <view class="action-item" @click="interactionMember">
+        <text class="action-item-icon">💬</text>
+        <text class="action-item-text">记录互动</text>
+      </view>
       <view class="action-item" @click="callMember" v-if="actionMember && actionMember.phone">
         <text class="action-item-icon">📞</text>
         <text class="action-item-text">拨打电话</text>
@@ -83,6 +91,9 @@ export default {
     canSwitch: function() {
       return this.actionMember && this.actionMember.userId
     },
+    canEdit: function() {
+      return this.isParent && this.actionMember && !this.actionMember.userId && this.actionMember.source === 'family_member'
+    },
     canRemove: function() {
       return this.isParent && this.actionMember && !this.actionMember.isSelf
     }
@@ -110,7 +121,21 @@ export default {
       var member = this.actionMember
       this.closeMenu()
       if (member) {
-        uni.navigateTo({ url: '/pages/profile-extra/family-members?editId=' + member.id })
+        uni.navigateTo({ url: '/pages/family/add-member?memberId=' + member.id })
+      }
+    },
+    questionnaireMember: function() {
+      var member = this.actionMember
+      this.closeMenu()
+      if (member) {
+        uni.navigateTo({ url: '/pages/action-detail/relationship-questionnaire?memberId=' + member.id })
+      }
+    },
+    interactionMember: function() {
+      var member = this.actionMember
+      this.closeMenu()
+      if (member) {
+        uni.navigateTo({ url: '/pages/action-detail/interaction-log?memberId=' + member.id })
       }
     },
     callMember: function() {

+ 2 - 0
cfc-frontend/components/share-mixin.js

@@ -38,6 +38,8 @@ export default {
      */
     async loadReferralCode() {
       if (this.referralCode) return
+      // 未登录时不请求邀请码(避免 500 报错弹窗和 401 跳转)
+      if (!uni.getStorageSync('token')) return
       try {
         var res = await getReferralCode()
         if (res && res.code === 200 && res.data) {

+ 0 - 6
cfc-frontend/pages.json

@@ -668,12 +668,6 @@
             "navigationBarTitleText": "我的权益"
           }
         },
-        {
-          "path": "plans",
-          "style": {
-            "navigationBarTitleText": "一生一世会员"
-          }
-        },
         {
           "path": "upgrade",
           "style": {

+ 0 - 1
cfc-frontend/pages/action-detail/interaction-log.vue

@@ -129,7 +129,6 @@ export default {
           happenedAt = this.customTime
         }
         var res = await addInteraction({
-          familyId: this.members[0] && this.members[0].familyId,
           fromMemberId: this.selectedMemberId,
           toMemberId: this.selectedMemberId,
           interactionType: this.selectedType,

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

@@ -208,9 +208,9 @@ export default {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/shop/index' })
+      uni.navigateTo({ url: '/pages/shop/index/index' })
     },
-    goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
+    goTasks: function() { uni.navigateTo({ url: '/pages/tasks/tasks' }) },
     goBack: function() { uni.navigateBack() }
   }
 }

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

@@ -466,7 +466,7 @@ export default {
       uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/shop/index' })
+      uni.navigateTo({ url: '/pages/shop/index/index' })
     },
     onFuncClick: function(item) {
       if (item.needLogin && !this.isLoggedIn) {
@@ -486,7 +486,7 @@ export default {
         }
       }
     },
-    goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
+    goTasks: function() { uni.navigateTo({ url: '/pages/tasks/tasks' }) },
     goMemberDetail: function(member) {
       if (!member || !member.memberId) return
       uni.navigateTo({

+ 0 - 1
cfc-frontend/pages/action/interaction-log.vue

@@ -129,7 +129,6 @@ export default {
           happenedAt = this.customTime
         }
         var res = await addInteraction({
-          familyId: this.members[0] && this.members[0].familyId,
           fromMemberId: this.selectedMemberId,
           toMemberId: this.selectedMemberId,
           interactionType: this.selectedType,

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

@@ -208,9 +208,9 @@ export default {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/shop/index' })
+      uni.navigateTo({ url: '/pages/shop/index/index' })
     },
-    goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
+    goTasks: function() { uni.navigateTo({ url: '/pages/tasks/tasks' }) },
     goBack: function() { uni.navigateBack() }
   }
 }

+ 23 - 2
cfc-frontend/pages/activity/activity-detail/activity-detail.vue

@@ -185,7 +185,7 @@
       </modal>
 
       <!-- 报名表单弹窗 -->
-      <modal v-if="showRegModal" class="reg-modal-overlay" :show="showRegModal" @close="showRegModal = false">
+      <view v-if="showRegModal" class="reg-modal-overlay" @click.self="closeRegModal">
         <view class="reg-modal">
           <text class="reg-modal-title">活动报名</text>
 
@@ -234,7 +234,7 @@
             </button>
           </view>
         </view>
-      </modal>
+      </view>
     </scroll-view>
 
     <!-- 空状态 -->
@@ -378,6 +378,12 @@ export default {
       var self = this
       if (self.checkinLoading || self.checkinDone) return
 
+      // 未登录则跳转登录
+      if (!uni.getStorageSync('token')) {
+        uni.navigateTo({ url: '/pages/login/login' })
+        return
+      }
+
       var childId = uni.getStorageSync('currentChildId') || self.registrationChildId
       if (!childId) {
         uni.showToast({ title: '请选择孩子身份', icon: 'none' })
@@ -423,6 +429,11 @@ export default {
     },
     onRegister: function() {
       var self = this
+      // 未登录则跳转登录
+      if (!uni.getStorageSync('token')) {
+        uni.navigateTo({ url: '/pages/login/login' })
+        return
+      }
       // Open registration form modal
       self.showRegModal = true
       self.registrants = []
@@ -430,6 +441,11 @@ export default {
     },
     onCancelRegistration: function() {
       var self = this
+      // 未登录则跳转登录
+      if (!uni.getStorageSync('token')) {
+        uni.navigateTo({ url: '/pages/login/login' })
+        return
+      }
       uni.showModal({
         title: '提示',
         content: '确定要取消报名吗?',
@@ -599,6 +615,11 @@ export default {
     // ===== 分享海报 =====
     async generatePoster() {
       if (!this.activity || !this.activity.id) return
+      // 未登录则跳转登录
+      if (!uni.getStorageSync('token')) {
+        uni.navigateTo({ url: '/pages/login/login' })
+        return
+      }
       uni.showLoading({ title: '生成海报中...' })
       try {
         var res = await getShareQrCode('pages/activity/activity-detail/activity-detail', 'id=' + this.activity.id)

+ 79 - 24
cfc-frontend/pages/assessment/purchase.vue

@@ -85,7 +85,7 @@
 </template>
 
 <script>
-import { getAvailableGuides, getPackagePrice, createPackageOrder } from '../../utils/api.js'
+import { getAvailableGuides, getPackagePrice, createAssessmentOrder, createVirtualPayOrder, requestVirtualPayment, getAssessmentOrderDetail } from '../../utils/api.js'
 
 export default {
   data() {
@@ -246,43 +246,98 @@ export default {
         uni.showToast({ title: '请选择任务模板', icon: 'none' })
         return
       }
+      if (!this.selectedGuideId) {
+        uni.showToast({ title: '请选择成长规划师', icon: 'none' })
+        return
+      }
+      var childId = uni.getStorageSync('currentChildId')
+      if (!childId) {
+        uni.showToast({ title: '请先选择孩子', icon: 'none' })
+        return
+      }
 
       try {
         uni.showLoading({ title: '创建订单中...' })
 
-        const userId = uni.getStorageSync('userId')
-        const familyId = uni.getStorageSync('familyId')
+        var userId = uni.getStorageSync('userId')
+        var familyId = uni.getStorageSync('familyId')
+        var self = this
 
-        const res = await createPackageOrder({
-          userId: userId,
+        var createRes = await createAssessmentOrder({
           familyId: familyId,
-          packageId: this.selectedPackageId,
+          userId: userId,
+          childId: childId,
           guideId: this.selectedGuideId,
-          inviteCode: this.inviteCode,
-          price: this.totalPrice,
-          payMethod: 'wechat'
+          guideName: this.selectedGuideName,
+          packageId: this.selectedPackageId,
+          packageName: this.selectedPackage ? this.selectedPackage.name : '',
+          totalPrice: this.totalPrice,
+          discountAmount: this.discountAmount
         })
-
         uni.hideLoading()
 
-        if (res.code === 200) {
-          // 跳转到支付页面
-          uni.navigateTo({
-            url: `/pages/assessment/purchase?orderNo=${res.data.orderNo}&totalPrice=${this.totalPrice}`
-          })
-        } else {
-          uni.showToast({
-            title: res.message || '创建订单失败',
-            icon: 'none'
-          })
+        if (createRes.code !== 200 || !createRes.data || !createRes.data.orderNo) {
+          uni.showToast({ title: '创建订单失败', icon: 'none' })
+          return
+        }
+
+        var orderNo = createRes.data.orderNo
+        // 支付(内联返回 VirtualPayParamsDTO;testMode 返回 null 表示 mock 直接支付成功)
+        var payRes = await createVirtualPayOrder(orderNo, 'ASSESSMENT')
+        if (payRes.code !== 200) {
+          uni.showToast({ title: payRes.message || '支付失败', icon: 'none' })
+          return
         }
+
+        if (!payRes.data) {
+          // testMode:mock 已直接标记支付成功 → 跳预约页
+          uni.showToast({ title: '支付成功', icon: 'success' })
+          setTimeout(function() {
+            uni.navigateTo({ url: '/pages/assessment/apply' })
+          }, 800)
+          return
+        }
+
+        // 真实模式:拉起虚拟支付
+        requestVirtualPayment(payRes.data, {
+          success: function() {
+            uni.showToast({ title: '支付成功,确认中…', icon: 'none' })
+            self.pollOrderStatus(orderNo)
+          },
+          fail: function() {
+            uni.showToast({ title: '支付未完成', icon: 'none' })
+          }
+        })
       } catch (e) {
         uni.hideLoading()
-        uni.showToast({
-          title: '创建订单失败',
-          icon: 'none'
-        })
+        if (e.code === 46001) {
+          uni.showToast({ title: '该商品暂未开放购买', icon: 'none' })
+        } else {
+          uni.showToast({ title: e.message || '支付失败', icon: 'none' })
+        }
       }
+    },
+    pollOrderStatus: function(orderNo) {
+      var self = this
+      var times = 0
+      var timer = setInterval(function() {
+        times++
+        getAssessmentOrderDetail(orderNo).then(function(res) {
+          var order = res.data || {}
+          if (order.status === 'paid') {
+            clearInterval(timer)
+            uni.showToast({ title: '支付成功', icon: 'success' })
+            setTimeout(function() {
+              uni.navigateTo({ url: '/pages/assessment/apply' })
+            }, 800)
+          } else if (times >= 5) {
+            clearInterval(timer)
+            uni.navigateTo({ url: '/pages/assessment/apply' })
+          }
+        }).catch(function() {
+          if (times >= 5) { clearInterval(timer); uni.navigateTo({ url: '/pages/assessment/apply' }) }
+        })
+      }, 2000)
     }
   }
 }

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

@@ -325,7 +325,7 @@ export default {
     },
     goProduct: function(product) {
       if (product && product.id) {
-        uni.navigateTo({ url: '/pages/shop/detail?id=' + product.id })
+        uni.navigateTo({ url: '/pages/shop/detail/detail?id=' + product.id })
       }
     },
     getImageUrl: function(path) {

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

@@ -208,9 +208,9 @@ export default {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/shop/index' })
+      uni.navigateTo({ url: '/pages/shop/index/index' })
     },
-    goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
+    goTasks: function() { uni.navigateTo({ url: '/pages/tasks/tasks' }) },
     goBack: function() { uni.navigateBack() }
   }
 }

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

@@ -299,7 +299,7 @@ export default {
       uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/shop/index' })
+      uni.navigateTo({ url: '/pages/shop/index/index' })
     },
     loadHealthDimensions: function() {
       var memberId = this.activeChildId || (this.userInfo && this.userInfo.userId)
@@ -440,7 +440,7 @@ export default {
     goMoreArticles: function() {
       uni.navigateTo({ url: '/pages/article-center/index?dimension=body' })
     },
-    goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
+    goTasks: function() { uni.navigateTo({ url: '/pages/tasks/tasks' }) },
     goFamilyMembers: function() {
       uni.navigateTo({ url: '/pages/profile/family-members' })
     },

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

@@ -208,9 +208,9 @@ export default {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/shop/index' })
+      uni.navigateTo({ url: '/pages/shop/index/index' })
     },
-    goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
+    goTasks: function() { uni.navigateTo({ url: '/pages/tasks/tasks' }) },
     goBack: function() { uni.navigateBack() }
   }
 }

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

@@ -197,7 +197,7 @@ export default {
       this.loadProducts()
     },
     goShop() {
-      uni.navigateTo({ url: '/pages/shop/index' })
+      uni.navigateTo({ url: '/pages/shop/index/index' })
     },
     goDetail(id) {
       uni.navigateTo({ url: '/pages/shop/detail/detail?id=' + id })

+ 34 - 7
cfc-frontend/pages/family/add-member.vue

@@ -1,7 +1,7 @@
 <template>
 <view class="container">
 <view class="form-container">
-<view class="page-title">添加家庭成员</view>
+<view class="page-title">{{ isEdit ? '编辑成员' : '添加家庭成员' }}</view>
 
 <!-- 昵称(必填) -->
 <view class="form-item required">
@@ -87,7 +87,7 @@
 </template>
 
 <script>
-import { addFamilyMember } from '../../utils/api.js'
+import { addFamilyMember, updateFamilyMember } from '../../utils/api.js'
 import GenerationPicker from '../../components/GenerationPicker.vue'
 
 export default {
@@ -95,6 +95,8 @@ components: { GenerationPicker },
 data() {
 return {
 submitting: false,
+isEdit: false,
+editMemberId: null,
 form: {
 nickname: '',
 generationLevel: '',
@@ -138,9 +140,17 @@ age--
 return age >= 0 ? age + '岁' : ''
 }
 },
-onLoad() {
-var today = new Date()
-this.todayDate = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0')
+onLoad(options) {
+  var today = new Date()
+  this.todayDate = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0')
+  if (options && options.memberId) {
+    this.isEdit = true
+    this.editMemberId = Number(options.memberId)
+    if (options.nickname) this.form.nickname = decodeURIComponent(options.nickname)
+    if (options.gender) this.form.gender = options.gender
+    if (options.phone) this.form.phone = options.phone
+    if (options.birthday) this.form.birthday = options.birthday
+  }
 },
 methods: {
 onGenderChange(e) {
@@ -169,7 +179,7 @@ if (!this.form.nickname || !this.form.nickname.trim()) {
 uni.showToast({ title: '请输入成员昵称', icon: 'none' })
 return
 }
-if (!this.$refs.generationPicker || !this.$refs.generationPicker.selectedGen) {
+if (!this.isEdit && (!this.$refs.generationPicker || !this.$refs.generationPicker.selectedGen)) {
   uni.showToast({ title: '请选择辈分关系', icon: 'none' })
   return
 }
@@ -188,6 +198,22 @@ return
 
 try {
 this.submitting = true
+if (this.isEdit) {
+  var updatePayload = {
+    memberId: this.editMemberId,
+    nickname: this.form.nickname.trim()
+  }
+  if (this.form.gender) updatePayload.gender = this.form.gender
+  if (this.form.birthday) updatePayload.birthday = this.form.birthday
+  if (this.form.phone) updatePayload.phone = this.form.phone
+  var res = await updateFamilyMember(updatePayload)
+  if (res.code === 200) {
+    uni.showToast({ title: '已更新', icon: 'success' })
+    setTimeout(function() { uni.navigateBack() }, 1500)
+  } else {
+    uni.showToast({ title: res.message || '更新失败', icon: 'none' })
+  }
+} else {
 var genLevel = (this.$refs.generationPicker && this.$refs.generationPicker.selectedGen) || this.form.generationLevel
 var genPeerType = (this.$refs.generationPicker && this.$refs.generationPicker.selectedPeer) || this.form.peerType
 var payload = {
@@ -219,8 +245,9 @@ setTimeout(function() { uni.navigateBack() }, 1500)
 } else {
 uni.showToast({ title: res.message || '添加失败', icon: 'none' })
 }
+}
 } catch (e) {
-uni.showToast({ title: '添加失败', icon: 'none' })
+uni.showToast({ title: this.isEdit ? '更新失败' : '添加失败', icon: 'none' })
 } finally {
 this.submitting = false
 }

+ 1 - 1
cfc-frontend/pages/guide/bind/invite.vue

@@ -207,7 +207,7 @@ export default {
       return `${d.getMonth() + 1}-${d.getDate()}`
     },
     goHome() {
-      uni.switchTab({ url: '/pages/home-pages/parent-index' })
+      uni.navigateTo({ url: '/pages/home-pages/parent-index' })
     }
   }
 }

+ 3 - 3
cfc-frontend/pages/home-pages/child-index.vue

@@ -653,11 +653,11 @@ export default {
     },
     goToRewards() {
       this.$emit('navigate', { page: 'rewards' })
-      uni.switchTab({ url: '/pages/rewards/rewards' })
+      uni.navigateTo({ url: '/pages/rewards/rewards' })
     },
     goToTasks() {
       this.$emit('navigate', { page: 'tasks' })
-      uni.switchTab({ url: '/pages/tasks/tasks' })
+      uni.navigateTo({ url: '/pages/tasks/tasks' })
     },
     goToGrowth() {
       this.$emit('navigate', { page: 'growth' })
@@ -719,7 +719,7 @@ export default {
       }
     },
     goMoreDiscover: function() {
-      uni.switchTab({ url: '/pages/discover/index' })
+      uni.navigateTo({ url: '/pages/discover/index' })
     },
 
     // ======================== 任务交互 ========================

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

@@ -206,7 +206,7 @@ export default {
       self.loading = false
     },
     goTasks: function() {
-      uni.switchTab({ url: '/pages/tasks/tasks' })
+      uni.navigateTo({ url: '/pages/tasks/tasks' })
     },
     goBack: function() {
       uni.navigateBack()

+ 33 - 6
cfc-frontend/pages/home-pages/parent-index.vue

@@ -14,7 +14,9 @@
   v-if="isLoggedIn"
   :members="familyMembersVisible"
   :selectedMemberId="selectedMemberId"
-  @select="onFamilyMemberSelect" />
+  :isParent="isFamilyAdmin"
+  @select="onFamilyMemberSelect"
+  @switch="onFamilyMemberSwitch" />
 
 <!-- ===== 微行动卡片(P0:有效启动) ===== -->
 <MicroActionCard
@@ -422,7 +424,8 @@
 </template>
 
 <script>
-import { getFamilyMembers, getChildren, getPendingReviewTasks, getPendingWishes, approveTask, rejectTask, getChildCompletionStats, getParentDashboard, getFamilyEnergySandbox, getEnergyOverview, getVisibleFamilyMembers, getActivityList, getProductsByDomain, getContactList, getFeaturedArticles, getActiveAlerts, dismissAlert, getFamilyRanking, getChallengeList, getCircleRanking, getInviteQrCode } from '../../utils/api.js'
+import { getFamilyMembers, getChildren, getPendingReviewTasks, getPendingWishes, approveTask, rejectTask, getChildCompletionStats, getParentDashboard, getFamilyEnergySandbox, getEnergyOverview, getVisibleFamilyMembers, getActivityList, getProductsByDomain, getContactList, getFeaturedArticles, getActiveAlerts, dismissAlert, getFamilyRanking, getChallengeList, getCircleRanking, getInviteQrCode, switchToFamilyMember } from '../../utils/api.js'
+import store from '../../store/index.js'
 import PageBanner from '../../components/PageBanner.vue'
 import PlayfulCard from '../../components/PlayfulCard.vue'
 import PlayfulButton from '../../components/PlayfulButton.vue'
@@ -524,6 +527,10 @@ export default {
     }
   },
   computed: {
+    isFamilyAdmin() {
+      if (!this.familyMembersVisible || this.familyMembersVisible.length === 0) return false
+      return this.familyMembersVisible[0].isAdmin === true
+    },
     topPendingTasks() {
       return this.pendingTasks.slice(0, 3)
     },
@@ -869,6 +876,26 @@ export default {
     onFamilyMemberSelect(member) {
       this.selectedMemberId = member.id
     },
+    async onFamilyMemberSwitch(member) {
+      if (!member || !member.id) return
+      try {
+        var res = await switchToFamilyMember(member.id, member.source || 'family_member')
+        if (res.code === 200 && res.data) {
+          store.commit('switchToMember', {
+            memberId: res.data.memberId,
+            effectiveRole: res.data.effectiveRole
+          })
+          uni.showToast({ title: '已切换到' + (member.nickname || '该成员'), icon: 'success' })
+          setTimeout(function() {
+            uni.reLaunch({ url: '/pages/index-home/index' })
+          }, 1000)
+        } else {
+          uni.showToast({ title: res.message || '切换失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.showToast({ title: '切换失败', icon: 'none' })
+      }
+    },
     onMicroActionCompleted() {
       this.loadData()
       feedbackEngine.trigger('micro_action')
@@ -940,8 +967,8 @@ export default {
     goToReview() { uni.navigateTo({ url: '/pages/tasks/review' }) },
     goToWishApprove() { uni.navigateTo({ url: '/pages/wishes/approve' }) },
     goToAssessmentReport() { uni.navigateTo({ url: '/pages/assessment/report' }) },
-    goToActivities() { uni.switchTab({ url: '/pages/discover/index' }) },
-    goToRewards() { uni.switchTab({ url: '/pages/rewards/rewards' }) },
+    goToActivities() { uni.navigateTo({ url: '/pages/activity/index' }) },
+    goToRewards() { uni.navigateTo({ url: '/pages/rewards/rewards' }) },
     onFeedItemClick(item) {
       if (item.type === 'product') {
         uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + item.id })
@@ -950,7 +977,7 @@ export default {
       }
     },
     goMoreDiscover() {
-      uni.switchTab({ url: '/pages/discover/index' })
+      uni.navigateTo({ url: '/pages/discover/index' })
     },
     loadRecommendedActivities() {
       var self = this
@@ -972,7 +999,7 @@ export default {
         self.errorProducts = true
       })
     },
-    goToTaskList() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
+    goToTaskList() { uni.navigateTo({ url: '/pages/tasks/tasks' }) },
     manageChildren() { uni.navigateTo({ url: '/pages/profile-extra/family-members' }) },
 
     loadContacts: function() {

+ 35 - 9
cfc-frontend/pages/index-home/index.vue

@@ -68,7 +68,9 @@
     <FamilyMemberStrip
       :members="familyMembersVisible"
       :selectedMemberId="selectedMemberId"
-      @select="onFamilyMemberSelect" />
+      :isParent="isFamilyAdmin"
+      @select="onFamilyMemberSelect"
+      @switch="onFamilyMemberSwitch" />
 
     <!-- 五维能量条 -->
     <FamilyEnergyBar
@@ -208,7 +210,7 @@
           v-for="item in products"
           :key="item.id"
           class="product-card"
-          @click="handleLogin()"
+          @click="goDetail(item.id)"
         >
           <image class="product-cover" :src="getImageUrl(item.coverImage) || '/static/default-product.png'" mode="aspectFill" />
           <view class="product-info">
@@ -306,7 +308,6 @@
           v-for="dim in dimList" :key="dim.code"
           class="dim-card"
           :style="'border-left:6rpx solid ' + dim.color"
-          @click="handleLogin"
         >
           <view class="dim-icon-circle" :style="'background:' + dim.bgColor">
             <text class="dim-icon">{{ dim.icon }}</text>
@@ -352,7 +353,8 @@ import CircleCard from '../../components/CircleCard.vue'
 import CircleDetail from '../../components/CircleDetail.vue'
 import LoginGuideCard from '../../components/LoginGuideCard.vue'
 import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
-import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getVisibleFamilyMembers, getTodayTasksByCategory, getChildren, getEnergyOverview, getProductsByDomain, getChallengeList, getMyCircles, discoverCircles, joinGeneralCircle, leaveGeneralCircle } from '../../utils/api.js'
+import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getVisibleFamilyMembers, getTodayTasksByCategory, getChildren, getEnergyOverview, getProductsByDomain, getChallengeList, getMyCircles, discoverCircles, joinGeneralCircle, leaveGeneralCircle, switchToFamilyMember } from '../../utils/api.js'
+import store from '../../store/index.js'
 import config from '@/config.js'
 
 var DIMENSIONS = [
@@ -440,13 +442,17 @@ export default {
       funcList: [
         { icon: '\u{1F3CB}', label: '任务', needLogin: true, page: '/pages/tasks/tasks' },
         { icon: '\u{1F3C0}', label: '活动', needLogin: false, page: '/pages/discover/index?type=activity' },
-        { icon: '\u{1F6CD}', label: '商城', needLogin: false, page: '/pages/discover/index?type=product' },
+        { icon: '\u{1F6CD}', label: '商城', needLogin: false, page: '/pages/shop/index/index' },
         { icon: '\u{1F3EB}', label: '课程', needLogin: false, page: '/pages/discover/index?type=course' }
       ],
       loggedInInitDone: false
     }
   },
   computed: {
+    isFamilyAdmin: function() {
+      if (!this.familyMembersVisible || this.familyMembersVisible.length === 0) return false
+      return this.familyMembersVisible[0].isAdmin === true
+    },
     sandboxScores: function() {
       var d = this.sandboxData || {}
       return {
@@ -733,11 +739,31 @@ export default {
     onFamilyMemberSelect: function(member) {
       this.selectedMemberId = member.id
     },
+    onFamilyMemberSwitch: function(member) {
+      var self = this
+      if (!member || !member.id) return
+      switchToFamilyMember(member.id, member.source || 'family_member').then(function(res) {
+        if (res.code === 200 && res.data) {
+          store.commit('switchToMember', {
+            memberId: res.data.memberId,
+            effectiveRole: res.data.effectiveRole
+          })
+          uni.showToast({ title: '已切换到' + (member.nickname || '该成员'), icon: 'success' })
+          setTimeout(function() {
+            uni.reLaunch({ url: '/pages/index-home/index' })
+          }, 1000)
+        } else {
+          uni.showToast({ title: res.message || '切换失败', icon: 'none' })
+        }
+      }).catch(function() {
+        uni.showToast({ title: '切换失败', icon: 'none' })
+      })
+    },
     onTaskClick: function(task) {
       uni.navigateTo({ url: '/pages/tasks/tasks' })
     },
     goTasks: function() {
-      uni.switchTab({ url: '/pages/tasks/tasks' })
+      uni.navigateTo({ url: '/pages/tasks/tasks' })
     },
     goActivityDetail: function(act) {
       if (!act || !act.id) return
@@ -750,7 +776,7 @@ export default {
       uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/shop/index' })
+      uni.navigateTo({ url: '/pages/shop/index/index' })
     },
     goArticleDetail: function(article) {
       var id = article.id || article
@@ -912,7 +938,7 @@ export default {
       })
     },
     goActivities: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=activity' })
+      uni.navigateTo({ url: '/pages/activity/index' })
     },
     goActivityDetail: function(act) {
       uni.navigateTo({ url: '/pages/activity/activity-detail/activity-detail?id=' + act.id })
@@ -929,7 +955,7 @@ export default {
       this.loadProducts()
     },
     goShop() {
-      uni.navigateTo({ url: '/pages/discover/index' })
+      uni.navigateTo({ url: '/pages/shop/index/index' })
     },
     goDetail(id) {
       uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + id })

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

@@ -372,10 +372,10 @@ export default {
       }
     },
     goMoreDiscover: function() {
-      uni.switchTab({ url: '/pages/discover/index' })
+      uni.navigateTo({ url: '/pages/discover/index' })
     },
     goTasks: function() {
-      uni.switchTab({ url: '/pages/tasks/tasks' })
+      uni.navigateTo({ url: '/pages/tasks/tasks' })
     },
     goBack: function() {
       uni.navigateBack()

+ 0 - 383
cfc-frontend/pages/membership/plans.vue

@@ -1,383 +0,0 @@
-<template>
-  <scroll-view class="container" scroll-y>
-    <view class="plans-header">
-      <text class="header-title">会员方案</text>
-      <text class="header-subtitle">全家人的健康守护</text>
-    </view>
-
-    <view class="plans-list">
-      <!-- 免费卡片(静态) -->
-      <view class="plan-card plan-l0">
-        <view class="plan-header">
-          <text class="plan-name">注册会员</text>
-          <text class="plan-tag">免费</text>
-        </view>
-        <view class="plan-price">
-          <text class="price-amount">¥0</text>
-          <text class="price-unit">/永久</text>
-        </view>
-        <view class="plan-features">
-          <view class="feature-row" v-for="(feat, idx) in l0Features" :key="idx">
-            <text class="feature-icon">✓</text>
-            <text class="feature-text">{{ feat }}</text>
-          </view>
-        </view>
-        <button class="btn-action" disabled>当前等级</button>
-      </view>
-
-      <!-- 付费等级卡片(从后端动态加载) -->
-      <view class="plan-card" v-for="level in levels" :key="level.levelCode"
-        :class="{'active': level.recommended}">
-        <view class="plan-badge" v-if="level.recommended">推荐</view>
-        <view class="plan-badge premium" v-else-if="level.levelCode === 'PREMIUM'">至尊</view>
-        <view class="plan-header">
-          <text class="plan-name">{{ level.levelName }}</text>
-          <text class="plan-tag">{{ level.levelCode === 'FAMILY' ? '年付会员' : '年付/月付' }}</text>
-        </view>
-        <view class="plan-price">
-          <text class="price-amount">{{ formatPrice(level.yearly) }}</text>
-          <text class="price-unit">/年</text>
-        </view>
-        <view class="plan-features" v-if="level.levelDesc">
-          <view class="feature-row" v-for="(feat, idx) in parseFeatures(level.levelDesc)" :key="idx">
-            <text class="feature-icon">✓</text>
-            <text class="feature-text">{{ feat }}</text>
-          </view>
-        </view>
-        <button class="btn-action" :class="level.recommended ? 'btn-primary' : 'btn-premium'"
-          @click="purchasePlan(level.levelCode)">
-          {{ level.levelCode === 'FAMILY' ? '立即开通' : '立即订阅' }}
-        </button>
-      </view>
-    </view>
-
-    <view class="benefits-section">
-      <text class="section-title">会员权益详解</text>
-      <view class="benefits-grid">
-        <view class="benefit-item" v-for="(item, idx) in benefits" :key="idx">
-          <view class="benefit-icon" :style="'background:' + item.bg">
-            <text class="benefit-emoji">{{ item.icon }}</text>
-          </view>
-          <text class="benefit-name">{{ item.name }}</text>
-          <text class="benefit-desc">{{ item.desc }}</text>
-        </view>
-      </view>
-    </view>
-
-    <view class="faq-section">
-      <text class="section-title">常见问题</text>
-      <view class="faq-item" v-for="(faq, idx) in faqs" :key="idx">
-        <text class="faq-question">{{ faq.q }}</text>
-        <text class="faq-answer">{{ faq.a }}</text>
-      </view>
-    </view>
-  </scroll-view>
-</template>
-
-<script>
-import { getMembershipPlans } from '../../utils/api.js'
-
-export default {
-  data() {
-    return {
-      levels: [],
-      l0Features: [
-        '每日 3 次 AI 问答',
-        '基础健康百科',
-        '社区内容浏览'
-      ],
-      benefits: [
-        { icon: '🤖', name: 'AI 健康顾问', desc: '7×24 小时智能问答', bg: '#DBEAFE' },
-        { icon: '👨‍👩‍👧‍👦', name: '家庭覆盖', desc: '一次付费全家守护', bg: '#D1FAE5' },
-        { icon: '👨‍🏫', name: '专属管家', desc: '认证专业人员服务', bg: '#FEF3C7' },
-        { icon: '🛒', name: '商品折扣', desc: '会员专享优惠价', bg: '#FCE7F3' },
-        { icon: '💰', name: '消费返利', desc: 'P 点制终身返利', bg: '#FFEDD5' },
-        { icon: '📈', name: '推广佣金', desc: 'R0-R4 阶梯奖励', bg: '#E0E7FF' }
-      ],
-      faqs: [
-        { q: '家庭会员适合谁?', a: '适合有 1-2 个孩子的家庭,追求性价比,希望获得 AI 健康顾问 + 认证管家服务' },
-        { q: '高级家庭会员与家庭会员的区别?', a: '高级会员享资深管家 (1:30 配比)、深度 AI 分析、专家会诊、更高折扣和返利加成' },
-        { q: '可以取消吗?', a: '可以,取消后当前月结束后降级为免费用户,健康档案保留 30 天' },
-        { q: '消费返利如何计算?', a: '被推荐人消费 P 点总和 × 推广人等级返利率,高级会员额外 +5% 加成' }
-      ]
-    }
-  },
-  onLoad() {
-    this.loadPlans()
-  },
-  methods: {
-    loadPlans() {
-      var self = this
-      getMembershipPlans().then(function(res) {
-        if (res.code === 200 && Array.isArray(res.data)) {
-          self.levels = res.data
-        }
-      }).catch(function(e) {
-        console.error('获取方案失败', e)
-      })
-    },
-    formatPrice(cents) {
-      if (!cents && cents !== 0) return '¥0'
-      return '¥' + (cents / 100).toLocaleString()
-    },
-    parseFeatures(features) {
-      if (!features) return []
-      if (Array.isArray(features)) return features
-      try { return JSON.parse(features) } catch (e) { return [] }
-    },
-    purchasePlan(levelCode) {
-      uni.navigateTo({ url: '/pages/membership/upgrade?level=' + levelCode })
-    }
-  }
-}
-</script>
-
-<style scoped>
-.container {
-  padding: 30rpx;
-  min-height: 100vh;
-  background: linear-gradient(180deg, #FFF7ED 0%, #FEF3C7 100%);
-}
-
-.plans-header {
-  text-align: center;
-  padding: 60rpx 0 40rpx;
-}
-.header-title {
-  font-size: 44rpx;
-  font-weight: bold;
-  color: #1E293B;
-  display: block;
-}
-.header-subtitle {
-  font-size: 28rpx;
-  color: #64748B;
-  margin-top: 12rpx;
-  display: block;
-}
-
-.plans-list {
-  display: flex;
-  flex-direction: column;
-  gap: 30rpx;
-  margin-bottom: 40rpx;
-}
-
-.plan-card {
-  background: #fff;
-  border-radius: 24rpx;
-  padding: 40rpx;
-  position: relative;
-  border: 2rpx solid #FED7AA;
-  box-shadow: 0 4rpx 20rpx rgba(249,115,22,0.08);
-}
-
-.plan-card.active {
-  border-color: #F97316;
-  background: linear-gradient(135deg, #FFF7ED 0%, #FEF3C7 100%);
-  transform: scale(1.02);
-}
-
-.plan-badge {
-  position: absolute;
-  top: -16rpx;
-  right: 40rpx;
-  background: #F97316;
-  color: #fff;
-  padding: 8rpx 24rpx;
-  border-radius: 9999rpx;
-  font-size: 22rpx;
-  font-weight: 600;
-}
-
-.plan-badge.premium {
-  background: linear-gradient(135deg, #8B5CF6 0%, #EC4899 100%);
-}
-
-.plan-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 20rpx;
-}
-
-.plan-name {
-  font-size: 32rpx;
-  font-weight: bold;
-  color: #1E293B;
-}
-
-.plan-tag {
-  font-size: 22rpx;
-  color: #64748B;
-  background: #F1F5F9;
-  padding: 6rpx 16rpx;
-  border-radius: 8rpx;
-}
-
-.plan-price {
-  text-align: center;
-  margin: 24rpx 0;
-}
-
-.price-amount {
-  font-size: 56rpx;
-  font-weight: bold;
-  color: #F97316;
-}
-
-.price-unit {
-  font-size: 24rpx;
-  color: #94A3B8;
-  margin-left: 8rpx;
-}
-
-.plan-features {
-  margin: 24rpx 0;
-}
-
-.feature-row {
-  display: flex;
-  align-items: center;
-  padding: 12rpx 0;
-}
-
-.feature-icon {
-  width: 40rpx;
-  height: 40rpx;
-  line-height: 40rpx;
-  text-align: center;
-  background: #10B981;
-  color: #fff;
-  border-radius: 50%;
-  font-size: 20rpx;
-  margin-right: 12rpx;
-  flex-shrink: 0;
-}
-
-.feature-text {
-  font-size: 26rpx;
-  color: #334155;
-  flex: 1;
-}
-
-.btn-action {
-  width: 100%;
-  height: 80rpx;
-  line-height: 80rpx;
-  background: #E5E7EB;
-  color: #9CA3AF;
-  font-size: 28rpx;
-  border-radius: 40rpx;
-  border: none;
-  margin-top: 20rpx;
-}
-
-.btn-action:disabled {
-  opacity: 0.6;
-}
-
-.btn-primary {
-  background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
-  color: #fff;
-  font-weight: 600;
-}
-
-.btn-premium {
-  background: linear-gradient(135deg, #8B5CF6 0%, #EC4899 100%);
-  color: #fff;
-  font-weight: 600;
-}
-
-.btn-action::after {
-  border: none;
-}
-
-.benefits-section {
-  background: #fff;
-  border-radius: 24rpx;
-  padding: 30rpx;
-  margin-bottom: 30rpx;
-  box-shadow: 0 4rpx 20rpx rgba(249,115,22,0.08);
-}
-
-.section-title {
-  font-size: 32rpx;
-  font-weight: bold;
-  color: #1E293B;
-  display: block;
-  margin-bottom: 24rpx;
-}
-
-.benefits-grid {
-  display: flex;
-  flex-wrap: wrap;
-}
-
-.benefit-item {
-  width: 50%;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  padding: 24rpx 16rpx;
-  box-sizing: border-box;
-}
-
-.benefit-icon {
-  width: 80rpx;
-  height: 80rpx;
-  border-radius: 50%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  margin-bottom: 16rpx;
-}
-
-.benefit-emoji {
-  font-size: 40rpx;
-}
-
-.benefit-name {
-  font-size: 26rpx;
-  font-weight: 600;
-  color: #1E293B;
-  margin-bottom: 8rpx;
-}
-
-.benefit-desc {
-  font-size: 22rpx;
-  color: #94A3B8;
-  text-align: center;
-}
-
-.faq-section {
-  background: #fff;
-  border-radius: 24rpx;
-  padding: 30rpx;
-  box-shadow: 0 4rpx 20rpx rgba(249,115,22,0.08);
-}
-
-.faq-item {
-  padding: 20rpx 0;
-  border-bottom: 1rpx solid #F1F5F9;
-}
-
-.faq-item:last-child {
-  border-bottom: none;
-}
-
-.faq-question {
-  font-size: 26rpx;
-  font-weight: 600;
-  color: #1E293B;
-  display: block;
-  margin-bottom: 8rpx;
-}
-
-.faq-answer {
-  font-size: 24rpx;
-  color: #64748B;
-  line-height: 1.6;
-  display: block;
-}
-</style>

+ 66 - 8
cfc-frontend/pages/membership/upgrade.vue

@@ -184,7 +184,7 @@
 </template>
 
 <script>
-import { getMyMembership, getMembershipLevels, getUpgradeRecords, upgradeMember, createOrder, getCouponList } from '../../utils/api.js'
+import { getMyMembership, getMembershipPlans, getUpgradeRecords, upgradeMember, createOrder, getCouponList, requestVirtualPayment } from '../../utils/api.js'
 
 export default {
   data() {
@@ -258,7 +258,7 @@ export default {
         console.error('加载会员信息失败', e)
       }
       try {
-        const levelsRes = await getMembershipLevels()
+        const levelsRes = await getMembershipPlans()
         this.levels = levelsRes.data || []
       } catch (e) {
         console.error('加载会员等级失败', e)
@@ -350,12 +350,51 @@ export default {
     doUpgrade: function() {
       var self = this
       createOrder(self.selectedLevel, 'pay', self.selectedPeriod).then(function(res) {
-        uni.showToast({ title: '开通成功', icon: 'success' })
-        self.loadData()
+        var data = res.data || {}
+        if (data.orderInfo && data.sign) {
+          // 真实模式:内联返回 VirtualPayParamsDTO → 拉起虚拟支付
+          requestVirtualPayment(data, {
+            success: function() {
+              uni.showToast({ title: '支付成功,开通中…', icon: 'none' })
+              self.pollOrderStatus()
+            },
+            fail: function() {
+              uni.showToast({ title: '支付未完成', icon: 'none' })
+            }
+          })
+        } else {
+          // testMode/trial:下单接口直接返回订单实体(mock 已开通)→ 沿用旧成功逻辑
+          uni.showToast({ title: '开通成功', icon: 'success' })
+          self.loadData()
+        }
       }).catch(function(e) {
-        uni.showToast({ title: e.message || '开通失败', icon: 'none' })
+        if (e.code === 46001) {
+          uni.showToast({ title: '该商品暂未开放购买', icon: 'none' })
+        } else {
+          uni.showToast({ title: e.message || '开通失败', icon: 'none' })
+        }
       })
     },
+    pollOrderStatus: function() {
+      var self = this
+      var times = 0
+      var timer = setInterval(function() {
+        times++
+        getMyMembership().then(function(res) {
+          var m = res.data || {}
+          if (m.membership && m.membership.isActive) {
+            clearInterval(timer)
+            uni.showToast({ title: '开通成功', icon: 'success' })
+            self.loadData()
+          } else if (times >= 5) {
+            clearInterval(timer)
+            self.loadData()
+          }
+        }).catch(function() {
+          if (times >= 5) { clearInterval(timer); self.loadData() }
+        })
+      }, 2000)
+    },
     handleRenew: function() {
       var self = this
       var priceText = '¥' + this.getPriceForPeriod(this.selectedPeriod) + this.getPeriodUnit()
@@ -372,10 +411,29 @@ export default {
     doRenew: function() {
       var self = this
       createOrder(self.selectedLevel, 'pay', this.selectedPeriod).then(function(res) {
-        uni.showToast({ title: '续费成功', icon: 'success' })
-        self.loadData()
+        var data = res.data || {}
+        if (data.orderInfo && data.sign) {
+          // 真实模式:内联返回 VirtualPayParamsDTO → 拉起虚拟支付
+          requestVirtualPayment(data, {
+            success: function() {
+              uni.showToast({ title: '支付成功,续费中…', icon: 'none' })
+              self.pollOrderStatus()
+            },
+            fail: function() {
+              uni.showToast({ title: '支付未完成', icon: 'none' })
+            }
+          })
+        } else {
+          // testMode/trial:下单接口直接返回订单实体(mock 已开通)→ 沿用旧成功逻辑
+          uni.showToast({ title: '续费成功', icon: 'success' })
+          self.loadData()
+        }
       }).catch(function(e) {
-        uni.showToast({ title: e.message || '续费失败', icon: 'none' })
+        if (e.code === 46001) {
+          uni.showToast({ title: '该商品暂未开放购买', icon: 'none' })
+        } else {
+          uni.showToast({ title: e.message || '续费失败', icon: 'none' })
+        }
       })
     },
     formatDate(date) {

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor