Sfoglia il codice sorgente

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

asus 1 mese fa
parent
commit
fdb9e4c9f7
51 ha cambiato i file con 1634 aggiunte e 789 eliminazioni
  1. 54 0
      articles/D15-draft_v1.md
  2. 51 0
      articles/D16-draft_v1.md
  3. 43 0
      articles/D16-draft_v2.md
  4. 1 1
      book/主动健康-书籍策划方案.md
  5. 2 2
      book/内容素材整合与章节映射.md
  6. 15 15
      book/成书稿.txt
  7. 2 2
      book/第一章-我们到底在为什么焦虑.md
  8. 4 4
      book/第三章-身-内容草稿.md
  9. 3 3
      book/第五章-富-内容草稿.md
  10. 2 2
      book/第六章-行-内容草稿.md
  11. 4 0
      cfc-backend/config/application.yml
  12. 5 1
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  13. 3 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java
  14. 17 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyChallenge.java
  15. 106 20
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyChallengeService.java
  16. 6 0
      cfc-backend/src/main/java/com/etotem/cfc/service/WechatService.java
  17. 2 0
      cfc-backend/src/main/resources/schema.sql
  18. 228 17
      cfc-frontend/components/FamilyChallengeCard.vue
  19. 1 12
      cfc-frontend/pages.json
  20. 1 1
      cfc-frontend/pages/action/index.vue
  21. 52 4
      cfc-frontend/pages/activity/activity-detail/activity-detail.vue
  22. 9 1
      cfc-frontend/pages/activity/create.vue
  23. 24 2
      cfc-frontend/pages/article-center/article-detail.vue
  24. 1 1
      cfc-frontend/pages/body/index.vue
  25. 1 1
      cfc-frontend/pages/discover/index.vue
  26. 3 1
      cfc-frontend/pages/home-pages/parent-index.vue
  27. 2 2
      cfc-frontend/pages/mind-extra/articles.vue
  28. 1 1
      cfc-frontend/pages/mind/index.vue
  29. 0 619
      cfc-frontend/pages/points/mall.vue
  30. 0 1
      cfc-frontend/pages/shop/cart/cart.vue
  31. 3 3
      cfc-frontend/pages/shop/index/index.vue
  32. 2 10
      cfc-frontend/pages/wealth/index.vue
  33. 1 1
      cfc-frontend/pages/wisdom/index.vue
  34. 14 0
      cfc-frontend/patch-node-compat.cjs
  35. 1 1
      cfc-web/.last_build_commit
  36. 2 2
      cfc-web/package-lock.json
  37. 1 1
      cfc-web/package.json
  38. 84 0
      cfc-web/public/CHANGELOG-v1.0.md
  39. 85 1
      cfc-web/public/CHANGELOG.md
  40. 3 0
      cfc-web/src/views/admin/Activities.vue
  41. 23 5
      cfc-web/src/views/admin/ActivityEdit.vue
  42. 9 5
      cfc-web/src/views/admin/ArticleEdit.vue
  43. 2 2
      cfc-web/src/views/admin/ArticleManage.vue
  44. 13 6
      cfc-web/src/views/admin/ProductEdit.vue
  45. 36 2
      cfc-web/src/views/admin/components/ProductSkuEditor.vue
  46. 1 1
      运营文案/小红书内容/D14-对外人客气是因为不在乎.md
  47. 56 0
      运营文案/小红书内容/D15-老人带娃翻脸是爱的语言不同.md
  48. 56 0
      运营文案/小红书内容/D16-体检正常不等于健康.md
  49. 29 9
      运营文案/小红书内容/README.md
  50. 570 25
      运营文案/小红书内容/小红书发布指南.md
  51. 0 0
      运营文案/小红书内容/第三篇-月入2万的家庭比想象中穷得更稳.md

+ 54 - 0
articles/D15-draft_v1.md

@@ -0,0 +1,54 @@
+# D15 老人带娃翻脸,是爱的语言不同
+
+> 心维度 #FF6B9D | 参考《家的算法》第7章 心·大爱相随
+> 系列:老人带娃三部曲·之一
+
+## 标题
+老人带娃翻脸,是爱的语言不同(14字)
+
+## 正文(695字)
+
+老人带娃起冲突,你以为是方法不对——不是,是爱的语言不同。
+
+你妈给孩子夹菜、添饭、追着喂,你烦了:妈,别惯着他,让他自己吃。你妈委屈了:我辛辛苦苦给你带孩子,你还嫌我。
+
+你觉得自己没错——科学育儿,培养独立性。她也觉得自己没错——怕孩子饿着,这是爱。
+
+你们都没错。但你们在用不同的语言说爱。
+
+我认识一个女孩叫小茜。每次回家,桌上永远有一盘她爱吃的鱼。父母觉得,给她吃好喝好就是最好的爱。但小茜想要的不是鱼,是有人问她一句:你最近在想什么?
+
+两种爱都没错,但语言不通。老人觉得"我付出了这么多你怎么还不满意",年轻人觉得"你们根本不理解我"。
+
+这不是谁对谁错的问题,是两个人的"爱的语言"不匹配。
+
+怎么解决?三步走。
+
+第一步,看见她的语言。老人给你带孩子,她用的方式是她从小被爱的方式——吃饱穿暖就是爱。不是她固执,她的词典里就这些词。
+
+第二步,翻译你的话。不要说"妈别追着喂了",试试:妈,我知道你怕他饿着。以后让他自己吃,吃不完我再喂。翻译过来就是:我看到你的爱了,咱换个方式一起爱他。
+
+第三步,找共同语言。不是谁听谁的,是找一条两个人都能走的路。比如:白天你按你的方式带,晚饭后我来管。
+
+我认识一个年轻妈妈,用这个方法试了两周,跟我说:以前跟婆婆说话,开口就是"妈你别",现在变成了"妈,谢谢你"。不是婆婆变了,是她换了翻译器。
+
+今晚回去试试。对帮你带娃的老人说一句:妈,你辛苦了。评论区打1,我明天回你。关注我,下周讲:为什么老人一插手你就急。
+
+## 标签
+#亲子关系 #家庭教育 #婆媳关系 #老人带娃 #情绪管理 #主动健康 #家庭幸福
+
+## 20秒动作
+对带娃的老人说一句"妈,你辛苦了"
+
+## 关键数据
+爱的语言不匹配→误解循环(参考书稿第7章·大爱三境界)
+
+## 图片规划(7张)
+
+Slide 1: 标题 | 老人带娃翻脸 / 是爱的语言不同 | 心·大爱相随 | #FF6B9D
+Slide 2: 场景 | 老人追着喂饭 / 年轻妈妈皱眉 | 冲突画面
+Slide 3: 故事 | 小茜父母夹鱼 / 她想要的是倾听 | 故事画面
+Slide 4: 反常识 | 不是方法不对 / 是爱的语言不同 | 核心洞察
+Slide 5: 方法1 | 看见他的语言 / 翻译你的语言 | 三步走
+Slide 6: 方法2 | 找到共同语言 / 不是谁说服谁 | 方法
+Slide 7: CTA | 说一句"妈你辛苦了" / 评论区打1 | CTA

+ 51 - 0
articles/D16-draft_v1.md

@@ -0,0 +1,51 @@
+# D16 体检正常≠健康,你信了多久
+
+> 身维度 #FF8C42 | 参考书稿第3章 身·主动健康
+
+## 标题
+体检正常≠健康,你信了多久(13字)
+
+## 正文(698字)
+
+体检报告每一项都正常,你就觉得自己健康了?
+
+不是。体检是查病的,不是查健康的。正常只代表你没查出来病,不代表你身体真的好。
+
+我认识一个朋友叫小付。三十八岁,体检报告有三项指标接近临界值。医生跟他说"要注意",他点了点头,出了诊室就忘了。不是不想记,是每天的生活根本没有给他注意的空间——早上七点起床,送孩子,上班,加班,晚上十点到家,躺下刷十分钟手机,睡着。第二天再来一遍。
+
+他觉得自己"还行"。直到有一天,他半夜抱着发烧的孩子在客厅来回走,走了两个小时。看着鱼缸里慢悠悠游的鱼,他忽然想:为什么总是我们家?为什么每次都是孩子先倒下?
+
+医生说了一句话让他记了很久:你孩子的指标都在正常范围内。但正常,不等于健康。
+
+这话听着像绕口令,但仔细一想,真不是。他翻出自己的体检报告——每一项指标都在正常范围,但有三项已经逼近红线。他的睡眠质量很差,精力大不如前,腰酸背痛是常态。报告上没有一项说他生病,但他的身体每天都在告诉他:我不太对劲。
+
+这就是问题:我们等身体出问题了才去修,而不是在没出问题之前就去养。
+
+三个层次,你到哪一层了?
+
+第一层,不做有害的事。不熬夜、不暴饮暴食、不把自己熬到扛不住才休息。
+
+第二层,做对的事。每天喝够水、每周运动两次、睡前不看手机。
+
+第三层,建立系统。不是靠意志力,是让好习惯自动运行——比如睡前把手机放到客厅充电,早上起来先喝一杯水。
+
+今晚回去试一件事:睡前把手机放到客厅,不要带进卧室。评论区打1,我明天回你。关注我,下周讲:你喝的每一口水,可能比外卖还脏。
+
+## 标签
+#主动健康 #体检 #睡眠 #健康管理 #家庭健康 #养生 #身体
+
+## 20秒动作
+睡前把手机放到客厅充电
+
+## 关键数据
+超过六成家庭过去一年中至少有人经历过三次以上急性疾病(浠艾福家庭健康报告 2026H1)
+
+## 图片规划(7张)
+
+Slide 1: 标题 | 体检正常≠健康 / 你信了多久 | 身·主动健康 | #FF8C42
+Slide 2: 场景 | 小付体检报告三项标红 / 医生说要注意转头就忘 | 故事
+Slide 3: 故事 | 半夜抱着发烧的孩子 / 在客厅走了一圈又一圈 | 场景
+Slide 4: 数据 | 超过六成家庭 / 一年三次以上急性病 | 数据
+Slide 5: 反常识 | 正常不等于健康 / 体检是查病不是查健康 | 洞察
+Slide 6: 方法 | 三个层次 / 不做有害的事→做对的事→建立系统 | 方法
+Slide 7: CTA | 今晚把手机放客厅 / 评论区打1 | CTA

+ 43 - 0
articles/D16-draft_v2.md

@@ -0,0 +1,43 @@
+# D16 体检正常≠健康,你信了多久(v2)
+
+> 身维度 #FF8C42 | 参考书稿第3章 身·主动健康
+
+## 标题
+体检正常≠健康,你信了多久(13字)
+
+## 正文(695字)
+
+体检报告每一项都正常,你就觉得自己健康了?
+
+不是。体检是查病的,不是查健康的。但问题不是体检没用,是你只靠体检这一种方式。
+
+主动健康的核心就两句话:发现风险要科学,干预一定要简单。
+
+先说发现风险。大多数人怎么判断自己健康?凭感觉,或者等体检报告。但感觉不靠谱——你觉得累,以为是没睡好,其实是身体在发出信号。体检也不够——一年一次,中间十一个月是盲区,而且很多指标逼近临界值时,报告上写的是"正常",但你的身体已经在走下坡了。
+
+我认识一个朋友叫小付。三十八岁,体检报告有三项指标接近临界值。医生跟他说要注意,他点了点头,出了诊室就忘了。不是不想记,是每天的生活根本没给他注意的空间。
+
+他觉得自己还行。直到有一天,他半夜抱着发烧的孩子在客厅走了两个小时,看着鱼缸里慢悠悠游的鱼,他才意识到:身体早就告诉他了,他没听。
+
+这就是问题:我们等身体出问题了才去修,不是早在出问题之前就去养。
+
+再说干预。很多人以为主动健康就是"坚持早睡、坚持运动、坚持健康饮食"——三个"坚持"一出来,就注定坚持不了。因为靠意志力的事,都撑不过两周。
+
+主动健康的干预,不是让你坚持,是让你改环境。不是靠意志力早睡,是把手机放到客厅充电。不是靠意志力多喝水,是床头放一杯水早上起来先喝。不是靠意志力运动,是穿上鞋就算出门。
+
+把动作拆到20秒内能完成,大脑才不会启动回避。
+
+回到小付。他后来没做什么了不起的事。就改了一件事:睡前把手机放到客厅。两周后,他入睡时间提前了。他说:"不是我能坚持,是我根本没机会拿到手机。"
+
+这就是主动健康:发现风险靠科学,不是靠感觉。干预靠简单,不是靠坚持。
+
+今晚回去试一件事:睡前把手机放到客厅,不要带进卧室。评论区打1,我明天回你。关注我,下周讲:你喝的每一口水,可能比外卖还脏。
+
+## 标签
+#主动健康 #体检 #睡眠 #健康管理 #家庭健康 #养生 #身体
+
+## 20秒动作
+睡前把手机放到客厅充电
+
+## 关键数据
+无——本文核心是理念,不是数据

+ 1 - 1
book/主动健康-书籍策划方案.md

@@ -400,7 +400,7 @@
 |:--------:|:--------|:-----|
 | 权威学术文献 | 作者 + 期刊/书名 + 年份,可在公开数据库检索 | Kluger & DeNisi, Psychological Bulletin, 1996 |
 | 行业调查报告 | 机构 + 报告名 + 年份 | 智课教育×新浪教育,《中国家长教育焦虑指数调查报告》,2018 |
-| 内部调研 | 统一冠名"浠艾福家庭健康报告 2026H1"+ 样本量,明示"内部调研",不作对外宣传承诺 | 浠艾福家庭健康报告 2026H1,n=326(内部调研) |
+| 内部调研 | 统一冠名"作者团队家庭健康服务实践观察(2025-2026)"+ 样本量,明示为团队实践观察,不作对外宣传承诺 | 作者团队家庭健康服务实践观察(2025-2026,n=326) |
 | 畅销书方法 | 标注"(畅销书方法,非严格实验研究)"或"(畅销书)",与学术文献区分 | Achor S, The Happiness Advantage, 2010 |
 
 ### 本次新增引用清单(2026-08)

+ 2 - 2
book/内容素材整合与章节映射.md

@@ -116,7 +116,7 @@
 
 **3. 内容案例规范**
 - 化名:小茜、小付、小艾、小然(复用同一化名形成系列)
-- 数据来源标注:"浠艾福家庭健康报告 2026H1,n=246"
+- 数据来源标注:"作者团队家庭健康服务实践观察(2025-2026,n=xxx)"(去品牌名,保留样本量)
 - 脱敏:不出现真实姓名、学校、住址
 
 ---
@@ -147,7 +147,7 @@
 2. **小红书 = 案例库**:故事、场景、行动指令 → 用于第3-13章的场景代入和行动卡
 3. **方法论 = 框架库**:三定律、六阶矩阵、3/7/14天节奏 → 用于第8-13章的核心方法论
 4. **所有素材必须二次加工**:不直接搬运原文,改写为书的口吻(温暖+有洞察力)
-5. **数据引用标注统一**:"浠艾福家庭健康报告"+"样本量"
+5. **数据引用标注统一**:"作者团队家庭健康服务实践观察(2025-2026)"+"样本量"(不用品牌名,正文以"在我们服务过的家庭中"等中性表述引出)
 
 ---
 

+ 15 - 15
book/成书稿.txt

@@ -223,7 +223,7 @@
 
 但更深一层,小付不敢承认的是:他自己也做不到。他今年三十八岁,体检报告上已经出现了三个标红的指标。他知道应该运动,知道应该少熬夜,知道应该少吃外卖——知道归知道,做不做得到是另一回事。
 
-健康焦虑有一个很特别的地方:它不紧急,但很重要。不紧急的意思是,你今天不运动,不会马上出事。重要的意思是,等它真的出事了,一切都来不及了。这种"今天没事但明天可能出事"的悬空感,是所有健康焦虑的底色。这不是个别家庭的感受。在我们对家庭的调研中,反复出现的健康问题相当普遍——几乎每个家庭都能举出一两个反复发作的小毛病(数据来源:浠艾福家庭健康报告 2026H1,n=326)
+健康焦虑有一个很特别的地方:它不紧急,但很重要。不紧急的意思是,你今天不运动,不会马上出事。重要的意思是,等它真的出事了,一切都来不及了。这种"今天没事但明天可能出事"的悬空感,是所有健康焦虑的底色。这不是个别家庭的感受。在我们过去一年服务过的数百个家庭中,反复出现的健康问题相当普遍——几乎每个家庭都能举出一两个反复发作的小毛病。
 
 ## 第二块石头:成长
 
@@ -400,7 +400,7 @@
 - 下一章,我们开始画这张地图
 
 > **本章数据来源说明:**
-> - 家庭健康问题普遍性调研:浠艾福家庭健康报告 2026H1,n=326(内部调研
+> - 家庭健康问题普遍性观察:作者团队家庭健康服务实践观察(2025-2026,n=326
 > - 教育焦虑数据(68%家长焦虑、仅6%不焦虑):智课教育×新浪教育,《中国家长教育焦虑指数调查报告》,2018
 > - 辅导班参与率(93%大陆父母报过辅导班,15国第一):HSBC,《全球教育报告(The Value of Education)》,2017
 > - 家庭金融脆弱性(应急储蓄不足3个月生活支出):姚健、臧旭恒、周博文,金融研究,2024(基于CHFS数据)
@@ -744,7 +744,7 @@
 
 他们家的情况,不是个例。
 
-在浠艾福的家庭健康调研中,有一个数据让小付后来看到的时候心里一紧:**超过六成的家庭在过去一年中,至少有一个人经历过三次以上的急性疾病**。(数据来源:浠艾福家庭健康报告 2026H1)但更让人在意的不是这个数字本身,而是这些家庭的回应方式——绝大多数人的第一反应是"扛一扛就好了",第二反应是"去医院看看"。很少有人会停下来问一句:为什么?
+在我们服务过的家庭中,有一个数据让小付后来看到的时候心里一紧:**超过六成的家庭在过去一年中,至少有一个人经历过三次以上的急性疾病**。但更让人在意的不是这个数字本身,而是这些家庭的回应方式——绝大多数人的第一反应是"扛一扛就好了",第二反应是"去医院看看"。很少有人会停下来问一句:为什么?
 
 为什么总是我们家的孩子?为什么别人家的孩子没事?
 
@@ -899,7 +899,7 @@
 
 这件事给他们的触动,比什么体检报告都大。
 
-在浠艾福的家庭健康实践中,有一个被很多家庭验证过的规律:**肠道健康改善的第一信号不是免疫力,而是排便。** 当一个人的排便规律、性状恢复正常时,他的肠道菌群大概率在往好的方向走。这个信号比任何体检指标都来得更早、更直观。因为它是你每天都能"看见"的东西,不需要去医院,不需要等报告。
+在我们的家庭健康实践中,有一个被很多家庭验证过的规律:**肠道健康改善的第一信号不是免疫力,而是排便。** 当一个人的排便规律、性状恢复正常时,他的肠道菌群大概率在往好的方向走。这个信号比任何体检指标都来得更早、更直观。因为它是你每天都能"看见"的东西,不需要去医院,不需要等报告。
 
 ---
 
@@ -1164,7 +1164,7 @@
 - 健康不是省出来的,是"看见"和"选择"出来的
 
 > **本章数据来源说明:**
-> - "六成家庭反复疾病"数据:浠艾福家庭健康报告 2026H1,n=326(内部调研
+> - "六成家庭反复疾病"数据:作者团队家庭健康服务实践观察(2025-2026,n=326
 > - "健康是身体、心理和社会适应的完好状态":WHO Constitution, 1948
 > - 身体活动建议(成人每周150-300分钟中等强度或75-150分钟高强度有氧运动):WHO, Guidelines on Physical Activity and Sedentary Behaviour, 2020
 > - 氢水睡眠改善研究:Todorovic N 等,Medicina, 2025(HYDRAPPET 随机双盲对照试验,受试为肥胖成年人,n=36,8周)
@@ -1174,7 +1174,7 @@
 > - 肠道菌群-肠脑轴通路:Cryan JF 等,Physiological Reviews, 2019;益生菌改善肠易激综合征患者情绪:Pinto-Sanchez MI 等,Gastroenterology, 2017
 > - 人体微生物与人体细胞数量比例:Sender R 等,PLoS Biology, 2016
 > - 决策疲劳:Vohs KD 等,Journal of Personality and Social Psychology, 2008
-> - 饭后散步效果数据:浠艾福家庭健康报告 2026H1,n=237(内部调研
+> - 饭后散步效果数据:作者团队家庭健康服务实践观察(2025-2026,n=237
 > - 案例均为真实故事脱敏改写,人物使用化名,情节做了文学化处理
 
 ---
@@ -1876,7 +1876,7 @@
 
 但正是这个"没做什么",就是别人最需要的。
 
-在浠艾福的家庭调研中,我们发现一个有意思的数据:**"被倾听的需求"在家庭关系满意度指标中排名前三,而"有人愿意认真听我说话"这个指标的得分,在参与调研家庭中平均只有4.9分(满分10分)**。(数据来源:浠艾福家庭健康报告 2026H1)
+在我们服务过的家庭中,我们发现一个有意思的数据:**"被倾听的需求"在家庭关系满意度指标中排名前三,而"有人愿意认真听我说话"这个指标的得分,在参与调研家庭中平均只有4.9分(满分10分)**。
 
 一个人最大的需求,有时候不是被解决问题,而是被看见、被听见。
 
@@ -2108,8 +2108,8 @@
 - 看见真相的那一刻,改变就已经开始了
 
 > **本章数据来源说明:**
-> - "被倾听需求"调研数据:浠艾福家庭健康报告 2026H1
-> - "记账家庭儿童财商+19分":浠艾福家庭健康报告 2026H1,n=246
+> - "被倾听需求"观察数据:作者团队家庭健康服务实践观察(2025-2026)
+> - "记账家庭儿童财商+19分":作者团队家庭健康服务实践观察(2025-2026,n=246)
 > - 亲社会支出与幸福感(覆盖136个国家):Aknin LB 等,Journal of Personality and Social Psychology, 2013
 > - 善意行为提升幸福感(27项实验元分析,N=4045):Curry OS 等,Journal of Experimental Social Psychology, 2018
 > - 积极幻想降低行动动机:Oettingen G 等,Journal of Personality and Social Psychology, 2002
@@ -2217,7 +2217,7 @@
 
 这个场景你太熟悉了。我们管这叫"关心",孩子管这叫"检查"。
 
-有数据说,78%的孩子认为"爸妈不关心我的感受"。但同一个调研里,100%的家长都认为自己"非常关心孩子"(浠艾福家庭健康报告 2026H1)。这不是个别家庭的错位。2016年,《青少年心理学杂志》发表了一项覆盖85项研究的元分析,结论是:父母对自己教养行为的评价,与孩子实际感知到的教养行为之间,一致性低得惊人——父母总是倾向于高估自己"关心"的分量,孩子则总是倾向于低估父母的"关心"(Korelitz & Garber, 2016)。于是"我明明这么关心你"和"你根本不懂我",才会在同一屋檐下同时成立。而在我们自己的跟踪数据里,这种错位并非无解——200多个家庭在经历几个简单的亲子互动调整之后,孩子对家庭归属感的平均评分在三个月内从2.0分(满分10)上升到了7.3分(浠艾福家庭健康报告 2026H1)
+有数据说,78%的孩子认为"爸妈不关心我的感受"。但同一个调研里,100%的家长都认为自己"非常关心孩子"。这不是个别家庭的错位。2016年,《青少年心理学杂志》发表了一项覆盖85项研究的元分析,结论是:父母对自己教养行为的评价,与孩子实际感知到的教养行为之间,一致性低得惊人——父母总是倾向于高估自己"关心"的分量,孩子则总是倾向于低估父母的"关心"(Korelitz & Garber, 2016)。于是"我明明这么关心你"和"你根本不懂我",才会在同一屋檐下同时成立。而在我们自己的跟踪数据里,这种错位并非无解——200多个家庭在经历几个简单的亲子互动调整之后,孩子对家庭归属感的平均评分在三个月内从2.0分(满分10)上升到了7.3分。
 
 问题出在哪?**出在我们把"控制"当成了"爱"的等价物。**
 
@@ -2879,9 +2879,9 @@
 - 24730法则:24小时联系、7天见面、30天找TA办件小事——新关系是这样长出来的
 
 > **本章数据来源说明:**
-> - "78%的孩子认为父母不关心自己的感受、100%的家长认为自己非常关心孩子"调研数据:浠艾福家庭健康报告 2026H1(内部调研
+> - "78%的孩子认为父母不关心自己的感受、100%的家长认为自己非常关心孩子"观察数据:作者团队家庭健康服务实践观察(2025-2026
 > - 父母与孩子对教养行为感知的错位:Korelitz K E & Garber J, "Congruence of Parents' and Children's Perceptions of Parenting: A Meta-Analysis", Journal of Youth and Adolescence, 2016(85项研究元分析)
-> - "家庭归属感评分2.0→7.3分(200+家庭跟踪)":浠艾福家庭健康报告 2026H1(内部调研
+> - "家庭归属感评分2.0→7.3分(200+家庭跟踪)":作者团队家庭健康服务实践观察(2025-2026
 > - "富兰克林效应"(帮过你的人反而更喜欢你):源自本杰明·富兰克林自传中的借书轶事
 > - 情绪脑(边缘系统)接管、杏仁核"警报系统":Goleman D, Emotional Intelligence, 1995
 > - 健康关系"5:1"积极/消极互动比例:Gottman J M & Levenson R W, "The Timing of Divorce", Journal of Marriage and Family, 2000
@@ -5059,7 +5059,7 @@ _________________________________
 
 # 参考文献总表
 
-> 正文中的每一个核心论点,均可追溯至下表所列文献或调研报告。学术文献按作者、来源、年份标注,可在公开数据库检索核对;内部调研数据仅用于本书论述,不作对外宣传承诺。
+> 正文中的每一个核心论点,均可追溯至下表所列文献或实践观察。学术文献按作者、来源、年份标注,可在公开数据库检索核对;作者团队的实践观察数据仅用于本书论述,不作对外宣传承诺。
 
 ## 一、权威学术文献(按章节首次出现排序)
 
@@ -5112,9 +5112,9 @@ _________________________________
 - HSBC,《全球教育报告(The Value of Education)》,2017
 - 姚健、臧旭恒、周博文,金融研究,2024(基于CHFS数据)
 
-## 三、内部调研报告
+## 三、作者团队实践观察
 
-- 浠艾福家庭健康报告 2026H1(n=326;另有n=246、n=237专项样本)
+- 作者团队家庭健康服务实践观察(2025-2026,n=326;另有n=246、n=237专项样本)
 - 双培强基工程DAN素质能力评估系统(A2六维认知测评)
 - 双培强基工程评估报告 2025-2026
 - 双培强基工程家庭关系评估报告 2025-2026

+ 2 - 2
book/第一章-我们到底在为什么焦虑.md

@@ -24,7 +24,7 @@
 
 但更深一层,小付不敢承认的是:他自己也做不到。他今年三十八岁,体检报告上已经出现了三个标红的指标。他知道应该运动,知道应该少熬夜,知道应该少吃外卖——知道归知道,做不做得到是另一回事。
 
-健康焦虑有一个很特别的地方:它不紧急,但很重要。不紧急的意思是,你今天不运动,不会马上出事。重要的意思是,等它真的出事了,一切都来不及了。这种"今天没事但明天可能出事"的悬空感,是所有健康焦虑的底色。这不是个别家庭的感受。在我们对家庭的调研中,反复出现的健康问题相当普遍——几乎每个家庭都能举出一两个反复发作的小毛病(数据来源:浠艾福家庭健康报告 2026H1,n=326)
+健康焦虑有一个很特别的地方:它不紧急,但很重要。不紧急的意思是,你今天不运动,不会马上出事。重要的意思是,等它真的出事了,一切都来不及了。这种"今天没事但明天可能出事"的悬空感,是所有健康焦虑的底色。这不是个别家庭的感受。在我们过去一年服务过的数百个家庭中,反复出现的健康问题相当普遍——几乎每个家庭都能举出一两个反复发作的小毛病。
 
 ## 第二块石头:成长
 
@@ -201,7 +201,7 @@
 - 下一章,我们开始画这张地图
 
 > **本章数据来源说明:**
-> - 家庭健康问题普遍性调研:浠艾福家庭健康报告 2026H1,n=326(内部调研
+> - 家庭健康问题普遍性观察:作者团队家庭健康服务实践观察(2025-2026,n=326
 > - 教育焦虑数据(68%家长焦虑、仅6%不焦虑):智课教育×新浪教育,《中国家长教育焦虑指数调查报告》,2018
 > - 辅导班参与率(93%大陆父母报过辅导班,15国第一):HSBC,《全球教育报告(The Value of Education)》,2017
 > - 家庭金融脆弱性(应急储蓄不足3个月生活支出):姚健、臧旭恒、周博文,金融研究,2024(基于CHFS数据)

+ 4 - 4
book/第三章-身-内容草稿.md

@@ -53,7 +53,7 @@
 
 他们家的情况,不是个例。
 
-在浠艾福的家庭健康调研中,有一个数据让小付后来看到的时候心里一紧:**超过六成的家庭在过去一年中,至少有一个人经历过三次以上的急性疾病**。(数据来源:浠艾福家庭健康报告 2026H1)但更让人在意的不是这个数字本身,而是这些家庭的回应方式——绝大多数人的第一反应是"扛一扛就好了",第二反应是"去医院看看"。很少有人会停下来问一句:为什么?
+在我们服务过的家庭中,有一个数据让小付后来看到的时候心里一紧:**超过六成的家庭在过去一年中,至少有一个人经历过三次以上的急性疾病**。但更让人在意的不是这个数字本身,而是这些家庭的回应方式——绝大多数人的第一反应是"扛一扛就好了",第二反应是"去医院看看"。很少有人会停下来问一句:为什么?
 
 为什么总是我们家的孩子?为什么别人家的孩子没事?
 
@@ -201,7 +201,7 @@
 
 这件事给他们的触动,比什么体检报告都大。
 
-在浠艾福的家庭健康实践中,有一个被很多家庭验证过的规律:**肠道健康改善的第一信号不是免疫力,而是排便。** 当一个人的排便规律、性状恢复正常时,他的肠道菌群大概率在往好的方向走。这个信号比任何体检指标都来得更早、更直观。因为它是你每天都能"看见"的东西,不需要去医院,不需要等报告。
+在我们的家庭健康实践中,有一个被很多家庭验证过的规律:**肠道健康改善的第一信号不是免疫力,而是排便。** 当一个人的排便规律、性状恢复正常时,他的肠道菌群大概率在往好的方向走。这个信号比任何体检指标都来得更早、更直观。因为它是你每天都能"看见"的东西,不需要去医院,不需要等报告。
 
 ---
 
@@ -464,7 +464,7 @@
 - 健康不是省出来的,是"看见"和"选择"出来的
 
 > **本章数据来源说明:**
-> - "六成家庭反复疾病"数据:浠艾福家庭健康报告 2026H1,n=326(内部调研
+> - "六成家庭反复疾病"数据:作者团队家庭健康服务实践观察(2025-2026,n=326
 > - 氢水睡眠改善研究:Todorovic N 等,Medicina, 2025(HYDRAPPET 随机双盲对照试验,受试为肥胖成年人,n=36,8周)
 > - 氢水情绪与自主神经研究:Mizuno K 等,Medical Gas Research, 2017(随机双盲交叉试验,n=26,4周)
 > - "人体约七成免疫细胞位于肠道相关淋巴组织":West CE 等,Journal of Allergy and Clinical Immunology, 2015
@@ -472,7 +472,7 @@
 > - 肠道菌群-肠脑轴通路:Cryan JF 等,Physiological Reviews, 2019;益生菌改善肠易激综合征患者情绪:Pinto-Sanchez MI 等,Gastroenterology, 2017
 > - 人体微生物与人体细胞数量比例:Sender R 等,PLoS Biology, 2016
 > - 决策疲劳:Vohs KD 等,Journal of Personality and Social Psychology, 2008
-> - 饭后散步效果数据:浠艾福家庭健康报告 2026H1,n=237(内部调研
+> - 饭后散步效果数据:作者团队家庭健康服务实践观察(2025-2026,n=237
 > - 案例均为真实故事脱敏改写,人物使用化名,情节做了文学化处理
 
 ---

+ 3 - 3
book/第五章-富-内容草稿.md

@@ -153,7 +153,7 @@
 
 但正是这个"没做什么",就是别人最需要的。
 
-在浠艾福的家庭调研中,我们发现一个有意思的数据:**"被倾听的需求"在家庭关系满意度指标中排名前三,而"有人愿意认真听我说话"这个指标的得分,在参与调研家庭中平均只有4.9分(满分10分)**。(数据来源:浠艾福家庭健康报告 2026H1)
+在我们服务过的家庭中,我们发现一个有意思的数据:**"被倾听的需求"在家庭关系满意度指标中排名前三,而"有人愿意认真听我说话"这个指标的得分,在参与调研家庭中平均只有4.9分(满分10分)**。
 
 一个人最大的需求,有时候不是被解决问题,而是被看见、被听见。
 
@@ -379,8 +379,8 @@
 - 看见真相的那一刻,改变就已经开始了
 
 > **本章数据来源说明:**
-> - "被倾听需求"调研数据:浠艾福家庭健康报告 2026H1
-> - "记账家庭儿童财商+19分":浠艾福家庭健康报告 2026H1,n=246
+> - "被倾听需求"观察数据:作者团队家庭健康服务实践观察(2025-2026)
+> - "记账家庭儿童财商+19分":作者团队家庭健康服务实践观察(2025-2026,n=246)
 > - 亲社会支出与幸福感(覆盖136个国家):Aknin LB 等,Journal of Personality and Social Psychology, 2013
 > - 善意行为提升幸福感(27项实验元分析,N=4045):Curry OS 等,Journal of Experimental Social Psychology, 2018
 > - 积极幻想降低行动动机:Oettingen G 等,Journal of Personality and Social Psychology, 2002

+ 2 - 2
book/第六章-行-内容草稿.md

@@ -735,8 +735,8 @@
 - 24730法则:24小时联系、7天见面、30天找TA办件小事——新关系是这样长出来的
 
 > **本章数据来源说明:**
-> - "78%的孩子认为父母不关心自己的感受、100%的家长认为自己非常关心孩子"调研数据:浠艾福家庭健康报告 2026H1(内部调研
-> - "家庭归属感评分2.0→7.3分(200+家庭跟踪)":浠艾福家庭健康报告 2026H1(内部调研
+> - "78%的孩子认为父母不关心自己的感受、100%的家长认为自己非常关心孩子"观察数据:作者团队家庭健康服务实践观察(2025-2026
+> - "家庭归属感评分2.0→7.3分(200+家庭跟踪)":作者团队家庭健康服务实践观察(2025-2026
 > - "富兰克林效应"(帮过你的人反而更喜欢你):源自本杰明·富兰克林自传中的借书轶事
 > - 情绪脑(边缘系统)接管、杏仁核"警报系统":Goleman D, Emotional Intelligence, 1995
 > - 健康关系"5:1"积极/消极互动比例:Gottman J M & Levenson R W, "The Timing of Divorce", Journal of Marriage and Family, 2000

+ 4 - 0
cfc-backend/config/application.yml

@@ -12,6 +12,10 @@ server:
   port: 9082
 
 spring:
+  servlet:
+    multipart:
+      max-file-size: 50MB
+      max-request-size: 50MB
   datasource:
     driver-class-name: com.mysql.cj.jdbc.Driver
     url: jdbc:mysql://127.0.0.1:3306/zxyj?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true

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

@@ -2921,9 +2921,13 @@ log.info("已添加template_id列到tasks表");
             jdbcTemplate.execute("ALTER TABLE activities ADD COLUMN audited_at DATETIME DEFAULT NULL COMMENT '审核时间'");
             log.info("已添加audited_at列到activities表");
         } catch (Exception e) {
-            // 列已存在,忽略错误
+
         }
 
+        // 家庭挑战:目标模式与目标值(aggregate=集体总合目标 / all_members=全员个人目标)
+        ensureColumn("family_challenge", "target_mode", "VARCHAR(20) DEFAULT 'aggregate' COMMENT '目标模式: aggregate=集体总合/all_members=全员个人目标'");
+        ensureColumn("family_challenge", "target_value", "INT DEFAULT 0 COMMENT '目标值(aggregate=家庭合计值, all_members=每人目标值)'");
+
         log.info("数据库迁移完成");
 
         // ==================== 健康维度 Phase 1: health_dimension_score / health_data_source_record / health_norm_reference ====================

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

@@ -135,8 +135,9 @@ public class AdminArticleController {
         Long articleId = body.get("id") != null ? Long.valueOf(body.get("id").toString()) : null;
         if (articleId != null) {
             Article existing = articleService.getById(articleId);
-            if (existing != null && !"draft".equals(existing.getStatus())) {
-                return Result.error("非草稿状态不可编辑");
+            if (existing != null && !"draft".equals(existing.getStatus())
+                    && !"published".equals(existing.getStatus())) {
+                return Result.error("仅草稿或已上架状态可编辑");
             }
         }
         Article article = new Article();

+ 17 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyChallenge.java

@@ -1,12 +1,15 @@
 package com.etotem.cfc.entity;
 
 import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 
 import java.io.Serializable;
 import java.util.Date;
+import java.util.List;
+import java.util.Map;
 
 @Data
 @TableName("family_challenge")
@@ -29,6 +32,12 @@ public class FamilyChallenge implements Serializable {
 
     private String rewardBadge;
 
+    /** 目标模式: aggregate=集体总合目标 / all_members=全员个人目标 */
+    private String targetMode;
+
+    /** 目标值(aggregate=家庭合计值, all_members=每人目标值) */
+    private Integer targetValue;
+
     private Date startDate;
 
     private Date endDate;
@@ -36,4 +45,12 @@ public class FamilyChallenge implements Serializable {
     private String status;
 
     private Date createdAt;
+
+    /** 非表字段:家庭总进度(aggregate 模式使用) */
+    @TableField(exist = false)
+    private Integer totalProgress;
+
+    /** 非表字段:成员进度列表 [{childId, nickname, progressValue, completed}] */
+    @TableField(exist = false)
+    private List<Map<String, Object>> memberProgress;
 }

+ 106 - 20
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyChallengeService.java

@@ -4,8 +4,10 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.FamilyChallenge;
 import com.etotem.cfc.entity.FamilyChallengeProgress;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.mapper.FamilyChallengeMapper;
 import com.etotem.cfc.mapper.FamilyChallengeProgressMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -24,6 +26,9 @@ public class FamilyChallengeService {
     @Resource
     private FamilyChallengeProgressMapper familyChallengeProgressMapper;
 
+    @Resource
+    private FamilyMemberMapper familyMemberMapper;
+
     @Resource
     private HealthScoreService healthScoreService;
 
@@ -40,6 +45,7 @@ public class FamilyChallengeService {
             active = familyChallengeMapper.selectList(wrapper);
         }
 
+        fillProgress(active);
         return active;
     }
 
@@ -57,9 +63,9 @@ public class FamilyChallengeService {
         Date endDate = cal.getTime();
 
         String[][] templates = {
-            {"full_checkin", "全员打卡挑战", "全家连续打卡3天,一起养成好习惯!", "3", "50", ""},
-            {"sports", "运动PK挑战", "全周家庭总运动时长超过300分钟!", "7", "100", ""},
-            {"health_week", "健康周挑战", "全员每日完成微行动,连续7天健康生活!", "7", "50", "健康之家"}
+            {"full_checkin", "全员打卡挑战", "全家连续打卡3天,一起养成好习惯!", "3", "50", "", "all_members", "3"},
+            {"sports", "运动PK挑战", "全周家庭总运动时长超过300分钟!", "7", "100", "", "aggregate", "300"},
+            {"health_week", "健康周挑战", "全员每日完成微行动,连续7天健康生活!", "7", "50", "健康之家", "all_members", "7"}
         };
 
         for (String[] t : templates) {
@@ -77,6 +83,8 @@ public class FamilyChallengeService {
             challenge.setDurationDays(Integer.parseInt(t[3]));
             challenge.setRewardPoints(Integer.parseInt(t[4]));
             challenge.setRewardBadge(t[5].isEmpty() ? null : t[5]);
+            challenge.setTargetMode(t[6]);
+            challenge.setTargetValue(Integer.parseInt(t[7]));
             challenge.setStartDate(startDate);
             challenge.setEndDate(endDate);
             challenge.setStatus("active");
@@ -137,22 +145,34 @@ public class FamilyChallengeService {
             totalProgress += (p.getProgressValue() != null ? p.getProgressValue() : 0);
         }
 
-        if ("full_checkin".equals(challenge.getChallengeType())) {
-            isCompleted = progresses.size() > 0;
-            for (FamilyChallengeProgress p : progresses) {
-                if (p.getProgressValue() == null || p.getProgressValue() < 3) {
-                    isCompleted = false;
-                    break;
+        // 目标模式:新挑战取 target_mode;旧挑战(target_mode为NULL)按 challengeType 兼容推断
+        normalizeTarget(challenge);
+        String targetMode = challenge.getTargetMode();
+        int targetValue = challenge.getTargetValue() != null ? challenge.getTargetValue() : 0;
+
+        if ("aggregate".equals(targetMode)) {
+            // 集体总合目标:全家人合计达到 target_value
+            isCompleted = totalProgress >= targetValue;
+        } else {
+            // 全员个人目标:家庭内每个成员都达到 target_value
+            LambdaQueryWrapper<FamilyMember> mw = new LambdaQueryWrapper<FamilyMember>()
+                    .eq(FamilyMember::getFamilyId, challenge.getFamilyId());
+            List<FamilyMember> members = familyMemberMapper.selectList(mw);
+            if (members.isEmpty()) {
+                isCompleted = false;
+            } else {
+                Map<Long, Integer> progressMap = new HashMap<>();
+                for (FamilyChallengeProgress p : progresses) {
+                    progressMap.put(p.getChildId(),
+                            p.getProgressValue() != null ? p.getProgressValue() : 0);
                 }
-            }
-        } else if ("sports".equals(challenge.getChallengeType())) {
-            isCompleted = totalProgress >= 300;
-        } else if ("health_week".equals(challenge.getChallengeType())) {
-            isCompleted = progresses.size() > 0;
-            for (FamilyChallengeProgress p : progresses) {
-                if (p.getProgressValue() == null || p.getProgressValue() < 7) {
-                    isCompleted = false;
-                    break;
+                isCompleted = true;
+                for (FamilyMember m : members) {
+                    Integer v = progressMap.get(m.getId());
+                    if (v == null || v < targetValue) {
+                        isCompleted = false;
+                        break;
+                    }
                 }
             }
         }
@@ -160,7 +180,8 @@ public class FamilyChallengeService {
         if (isCompleted || challenge.getEndDate() == null || new Date().after(challenge.getEndDate())) {
             challenge.setStatus("completed");
             familyChallengeMapper.updateById(challenge);
-            log.info("挑战已结算: id={}, type={}, completed={}", challenge.getId(), challenge.getChallengeType(), isCompleted);
+            log.info("挑战已结算: id={}, type={}, targetMode={}, targetValue={}, completed={}",
+                    challenge.getId(), challenge.getChallengeType(), targetMode, targetValue, isCompleted);
         }
     }
 
@@ -170,6 +191,71 @@ public class FamilyChallengeService {
                 .eq(FamilyChallenge::getFamilyId, familyId)
                 .eq(FamilyChallenge::getStatus, "completed")
                 .orderByDesc(FamilyChallenge::getEndDate);
-        return familyChallengeMapper.selectList(wrapper);
+        List<FamilyChallenge> history = familyChallengeMapper.selectList(wrapper);
+        fillProgress(history);
+        return history;
+    }
+
+    /**
+     * 为挑战填充进度数据(totalProgress + memberProgress),
+     * 并对旧挑战(target_mode为NULL)向后规范化 target_mode/target_value。
+     */
+    private void fillProgress(List<FamilyChallenge> challenges) {
+        if (challenges == null || challenges.isEmpty()) return;
+
+        // 缓存各家庭全部成员,避免逐挑战重复查询
+        Map<Long, List<FamilyMember>> familyMembersCache = new HashMap<>();
+
+        for (FamilyChallenge c : challenges) {
+            normalizeTarget(c);
+
+            LambdaQueryWrapper<FamilyChallengeProgress> pw = new LambdaQueryWrapper<FamilyChallengeProgress>()
+                    .eq(FamilyChallengeProgress::getChallengeId, c.getId());
+            List<FamilyChallengeProgress> progresses = familyChallengeProgressMapper.selectList(pw);
+
+            int total = 0;
+            Map<Long, Integer> progressMap = new HashMap<>();
+            for (FamilyChallengeProgress p : progresses) {
+                int v = p.getProgressValue() != null ? p.getProgressValue() : 0;
+                total += v;
+                progressMap.put(p.getChildId(), v);
+            }
+            c.setTotalProgress(total);
+
+            List<FamilyMember> members = familyMembersCache.get(c.getFamilyId());
+            if (members == null) {
+                LambdaQueryWrapper<FamilyMember> mw = new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getFamilyId, c.getFamilyId());
+                members = familyMemberMapper.selectList(mw);
+                familyMembersCache.put(c.getFamilyId(), members);
+            }
+
+            List<Map<String, Object>> memberProgress = new ArrayList<>();
+            if (members != null) {
+                for (FamilyMember m : members) {
+                    Integer v = progressMap.get(m.getId());
+                    int value = v != null ? v : 0;
+                    Map<String, Object> mp = new HashMap<>();
+                    mp.put("childId", m.getId());
+                    mp.put("nickname", m.getNickname() != null ? m.getNickname() : "成员" + m.getId());
+                    mp.put("progressValue", value);
+                    mp.put("completed", value >= c.getTargetValue());
+                    memberProgress.add(mp);
+                }
+            }
+            c.setMemberProgress(memberProgress);
+        }
+    }
+
+    /** 旧挑战 target_mode 为 NULL 时,按 challengeType 推断补全 */
+    private void normalizeTarget(FamilyChallenge c) {
+        if (c.getTargetMode() != null) return;
+        if ("sports".equals(c.getChallengeType())) {
+            c.setTargetMode("aggregate");
+            c.setTargetValue(300);
+        } else {
+            c.setTargetMode("all_members");
+            c.setTargetValue("full_checkin".equals(c.getChallengeType()) ? 3 : 7);
+        }
     }
 }

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

@@ -320,6 +320,12 @@ public class WechatService implements WechatServiceInterface {
             return true;
         }
 
+        // 微信 img_sec_check 限制 1MB,超过则跳过检测(fail-open)
+        if (imageBytes.length > 1024 * 1024) {
+            log.info("图片大小 {} 超过微信安全检测上限 1MB,跳过检测", imageBytes.length);
+            return true;
+        }
+
         try {
             String accessToken = getAccessToken();
             String url = "https://api.weixin.qq.com/wxa/img_sec_check?access_token=" + accessToken;

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

@@ -3140,6 +3140,8 @@ CREATE TABLE IF NOT EXISTS family_challenge (
     duration_days INT DEFAULT 7 COMMENT '持续天数',
     reward_points INT DEFAULT 0 COMMENT '奖励积分',
     reward_badge VARCHAR(100) COMMENT '奖励徽章',
+    target_mode VARCHAR(20) DEFAULT 'aggregate' COMMENT '目标模式: aggregate=集体总合/all_members=全员个人目标',
+    target_value INT DEFAULT 0 COMMENT '目标值(aggregate=家庭合计值, all_members=每人目标值)',
     start_date DATE COMMENT '开始日期',
     end_date DATE COMMENT '结束日期',
     status VARCHAR(20) DEFAULT 'active' COMMENT 'active/completed/cancelled',

+ 228 - 17
cfc-frontend/components/FamilyChallengeCard.vue

@@ -2,11 +2,42 @@
   <view class="challenge-card" v-if="challenge">
     <view class="card-header">
       <text class="card-title">{{ challenge.title }}</text>
-      <text class="card-status" :class="'status-' + (challenge.status || 'active')">
+      <text class="card-status" :class="statusClass">
         {{ challenge.status === 'active' ? '进行中' : '已完成' }}
       </text>
     </view>
     <text class="card-desc">{{ challenge.description || '' }}</text>
+
+    <!-- 目标展示(两种模式) -->
+    <view class="target-section" v-if="challenge.targetMode">
+      <view class="target-header">
+        <text class="target-label">{{ targetModeLabel }}</text>
+        <text class="target-value">{{ formatTargetValue }}</text>
+      </view>
+
+      <!-- aggregate 模式:家庭总进度条 -->
+      <view class="card-progress" v-if="challenge.targetMode === 'aggregate'">
+        <view class="progress-bar-bg">
+          <view class="progress-bar-fill" :style="{ width: totalPercent + '%' }"></view>
+        </view>
+        <text class="progress-text">{{ challenge.totalProgress || 0 }}/{{ challenge.targetValue }}</text>
+      </view>
+
+      <!-- all_members 模式:成员进度列表 -->
+      <view class="member-list" v-if="challenge.targetMode === 'all_members' && challenge.memberProgress && challenge.memberProgress.length > 0">
+        <view class="member-row" v-for="mp in challenge.memberProgress" :key="mp.childId">
+          <text class="member-name">{{ mp.nickname }}</text>
+          <view class="member-bar-wrap">
+            <view class="member-bar-bg">
+              <view class="member-bar-fill" :style="{ width: memberPercent(mp.progressValue) + '%' }"></view>
+            </view>
+          </view>
+          <text class="member-val">{{ mp.progressValue }}/{{ challenge.targetValue }}</text>
+          <text class="member-done" v-if="mp.completed">✓</text>
+        </view>
+      </view>
+    </view>
+
     <view class="card-detail-row" v-if="challenge.durationDays">
       <text class="detail-label">⏱ 期限</text>
       <text class="detail-val">{{ challenge.durationDays }}天</text>
@@ -20,25 +51,107 @@
       <text class="detail-val reward">+{{ challenge.rewardPoints }}分</text>
       <text class="detail-val badge" v-if="challenge.rewardBadge"> 🎖️ {{ challenge.rewardBadge }}</text>
     </view>
-    <view class="card-progress" v-if="progressPercent !== undefined">
-      <view class="progress-bar-bg">
-        <view class="progress-bar-fill" :style="{ width: Math.min(progressPercent, 100) + '%' }"></view>
-      </view>
-      <text class="progress-text">{{ progressPercent }}%</text>
+
+    <!-- 打卡上报入口(active 挑战 + 有 childId) -->
+    <view class="checkin-section" v-if="challenge.status === 'active'">
+      <template v-if="!showInput">
+        <view class="checkin-btn" @click="openInput">
+          <text class="checkin-btn-text">上报进度</text>
+        </view>
+      </template>
+      <template v-else>
+        <view class="checkin-form">
+          <input class="checkin-input" type="number" v-model="deltaValue" placeholder="输入本次数量" />
+          <view class="checkin-submit" @click="submitProgress">
+            <text class="checkin-btn-text">确认</text>
+          </view>
+          <view class="checkin-cancel" @click="closeInput">
+            <text class="checkin-cancel-text">取消</text>
+          </view>
+        </view>
+      </template>
     </view>
   </view>
 </template>
 
 <script>
+import { updateChallengeProgress } from '@/utils/api.js'
+
 export default {
   props: {
     challenge: { type: Object, default: null },
-    progressPercent: { type: Number, default: undefined }
+    childId: { type: Number, default: null }
+  },
+  data: function() {
+    return {
+      showInput: false,
+      deltaValue: ''
+    }
+  },
+  computed: {
+    statusClass: function() {
+      return 'status-' + (this.challenge && this.challenge.status ? this.challenge.status : 'active')
+    },
+    targetModeLabel: function() {
+      if (!this.challenge) return ''
+      if (this.challenge.targetMode === 'aggregate') return '🎯 全家目标'
+      return '🎯 每人目标'
+    },
+    formatTargetValue: function() {
+      if (!this.challenge) return ''
+      return this.challenge.targetValue
+    },
+    totalPercent: function() {
+      var total = this.challenge && this.challenge.totalProgress ? this.challenge.totalProgress : 0
+      var target = this.challenge && this.challenge.targetValue ? this.challenge.targetValue : 1
+      if (target <= 0) return 0
+      var p = Math.round(total / target * 100)
+      return Math.min(p, 100)
+    }
   },
   methods: {
     formatDate: function(dateStr) {
       if (!dateStr) return ''
       return String(dateStr).slice(0, 10)
+    },
+    memberPercent: function(value) {
+      var target = this.challenge && this.challenge.targetValue ? this.challenge.targetValue : 1
+      if (target <= 0) return 0
+      var p = Math.round((value || 0) / target * 100)
+      return Math.min(p, 100)
+    },
+    openInput: function() {
+      this.deltaValue = ''
+      this.showInput = true
+    },
+    closeInput: function() {
+      this.showInput = false
+      this.deltaValue = ''
+    },
+    submitProgress: function() {
+      var delta = parseInt(this.deltaValue, 10)
+      if (!delta || delta <= 0) {
+        uni.showToast({ title: '请输入本次数量', icon: 'none' })
+        return
+      }
+      if (!this.childId) {
+        uni.showToast({ title: '请先选择家庭成员', icon: 'none' })
+        return
+      }
+      var self = this
+      updateChallengeProgress({
+        challengeId: this.challenge.id,
+        childId: this.childId,
+        delta: delta
+      }).then(function(res) {
+        if (res.code === 200) {
+          uni.showToast({ title: '上报成功!', icon: 'none' })
+          self.closeInput()
+          self.$emit('updated')
+        }
+      }).catch(function() {
+        // api.js 拦截器已提示
+      })
     }
   }
 }
@@ -78,26 +191,33 @@ export default {
   margin-bottom: 12rpx;
   display: block;
 }
-.card-detail-row {
+
+/* 目标区 */
+.target-section {
+  background: #F8FAFC;
+  border-radius: 12rpx;
+  padding: 16rpx;
+  margin-bottom: 8rpx;
+}
+.target-header {
   display: flex;
+  justify-content: space-between;
   align-items: center;
-  gap: 8rpx;
-  margin-top: 6rpx;
+  margin-bottom: 10rpx;
 }
-.detail-label { font-size: 24rpx; color: #94A3B8; }
-.detail-val { font-size: 24rpx; color: #334155; }
-.detail-val.reward { color: #F97316; font-weight: 600; }
-.detail-val.badge { color: #6366F1; }
+.target-label { font-size: 24rpx; color: #475569; font-weight: 600; }
+.target-value { font-size: 28rpx; color: #10B981; font-weight: 700; }
+
 .card-progress {
   display: flex;
   align-items: center;
   gap: 12rpx;
-  margin-top: 12rpx;
+  margin-top: 4rpx;
 }
 .progress-bar-bg {
   flex: 1;
   height: 12rpx;
-  background: #F1F5F9;
+  background: #E2E8F0;
   border-radius: 999rpx;
   overflow: hidden;
 }
@@ -111,7 +231,98 @@ export default {
   font-size: 22rpx;
   color: #10B981;
   font-weight: 600;
-  min-width: 48rpx;
+  min-width: 110rpx;
   text-align: right;
 }
+
+/* 成员进度 */
+.member-list {
+  margin-top: 4rpx;
+}
+.member-row {
+  display: flex;
+  align-items: center;
+  gap: 8rpx;
+  margin-top: 8rpx;
+}
+.member-name {
+  font-size: 22rpx;
+  color: #475569;
+  width: 90rpx;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+.member-bar-wrap { flex: 1; }
+.member-bar-bg {
+  height: 10rpx;
+  background: #E2E8F0;
+  border-radius: 999rpx;
+  overflow: hidden;
+}
+.member-bar-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #6366F1, #8B5CF6);
+  border-radius: 999rpx;
+}
+.member-val {
+  font-size: 20rpx;
+  color: #64748B;
+  min-width: 90rpx;
+  text-align: right;
+}
+.member-done {
+  font-size: 24rpx;
+  color: #10B981;
+  font-weight: 700;
+}
+
+/* 详情行 */
+.card-detail-row {
+  display: flex;
+  align-items: center;
+  gap: 8rpx;
+  margin-top: 6rpx;
+}
+.detail-label { font-size: 24rpx; color: #94A3B8; }
+.detail-val { font-size: 24rpx; color: #334155; }
+.detail-val.reward { color: #F97316; font-weight: 600; }
+.detail-val.badge { color: #6366F1; }
+
+/* 打卡区 */
+.checkin-section {
+  margin-top: 16rpx;
+  border-top: 1rpx solid #F1F5F9;
+  padding-top: 16rpx;
+}
+.checkin-btn {
+  background: linear-gradient(135deg, #10B981, #34D399);
+  border-radius: 40rpx;
+  padding: 16rpx 0;
+  text-align: center;
+}
+.checkin-btn:active { opacity: 0.8; }
+.checkin-btn-text { color: #fff; font-size: 26rpx; font-weight: 600; }
+.checkin-form {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+}
+.checkin-input {
+  flex: 1;
+  height: 64rpx;
+  background: #F1F5F9;
+  border-radius: 12rpx;
+  padding: 0 16rpx;
+  font-size: 26rpx;
+}
+.checkin-submit {
+  background: #10B981;
+  border-radius: 12rpx;
+  padding: 14rpx 24rpx;
+}
+.checkin-cancel {
+  padding: 14rpx 16rpx;
+}
+.checkin-cancel-text { font-size: 24rpx; color: #94A3B8; }
 </style>

+ 1 - 12
cfc-frontend/pages.json

@@ -430,7 +430,7 @@
         {
           "path": "index/index",
           "style": {
-            "navigationBarTitleText": "商品首页"
+            "navigationBarTitleText": "浠艾福商城"
           }
         },
         {
@@ -682,17 +682,6 @@
         }
       ]
     },
-    {
-      "root": "pages/points",
-      "pages": [
-        {
-          "path": "mall",
-          "style": {
-            "navigationBarTitleText": "积分商城"
-          }
-        }
-      ]
-    },
     {
       "root": "pages/wealth-sub",
       "pages": [

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

@@ -72,7 +72,7 @@
       dimensionCode="action"
       :familyId="activeChildId"
       :products="dimensionProducts"
-      title="为你推荐"
+      title="推荐商品"
       @productClick="goProductDetail"
       @moreProducts="goMoreProducts" />
 

+ 52 - 4
cfc-frontend/pages/activity/activity-detail/activity-detail.vue

@@ -79,7 +79,7 @@
           <text class="bottom-vendor" v-if="activity.vendorName">{{ activity.vendorName }}</text>
         </view>
         <view class="action-btns">
-          <button class="share-btn" open-type="share" @click="onShareTap">
+          <button class="share-btn" @click="generatePoster">
             <text class="share-btn-icon">📤</text>
           </button>
           <!-- 报名按钮(仅 requireRegistration=1 时显示) -->
@@ -241,17 +241,32 @@
     <view v-else class="empty-wrap">
       <text class="empty-text">活动不存在或已下架</text>
     </view>
+
+    <!-- 分享海报 -->
+    <ContentSharePoster
+      :show="showPoster"
+      :title="activity && activity.title"
+      :coverImage="activity && activity.coverImage"
+      :qrCodeBase64="posterQrCode"
+      typeLabel="活动推荐"
+      :dimensionCode="activity && activity.dimensionCode"
+      :summary="posterSummary"
+      :priceText="posterPriceText"
+      :inviteText="posterInviteText"
+      @close="closePoster"
+    />
   </view>
 </template>
 
 <script>
-import { getActivityDetail, activityCheckin, registerActivity, cancelActivityRegistration, getMyActivityRegistrations, submitActivityFeedback, getActivityFeedbackList, getMyActivityFeedback, getFamilyMembers } from '@/utils/api.js'
+import { getActivityDetail, activityCheckin, registerActivity, cancelActivityRegistration, getMyActivityRegistrations, submitActivityFeedback, getActivityFeedbackList, getMyActivityFeedback, getFamilyMembers, getShareQrCode } from '@/utils/api.js'
 import shareMixin from '../../../components/share-mixin.js'
 import MpHtml from '@/components/mp-html/mp-html.vue'
+import ContentSharePoster from '@/components/ContentSharePoster.vue'
 
 export default {
   mixins: [shareMixin],
-  components: { MpHtml },
+  components: { MpHtml, ContentSharePoster },
   data() {
     return {
       activityId: null,
@@ -278,7 +293,13 @@ export default {
       showRegModal: false,
       familyMembers: [],
       registrants: [],
-      regSubmitting: false
+      regSubmitting: false,
+      // 分享海报
+      showPoster: false,
+      posterQrCode: '',
+      posterSummary: '',
+      posterPriceText: '',
+      posterInviteText: ''
     }
   },
   onLoad: function(options) {
@@ -575,6 +596,33 @@ export default {
       this.registrants = []
       this.familyMembers = []
     },
+    // ===== 分享海报 =====
+    async generatePoster() {
+      if (!this.activity || !this.activity.id) return
+      uni.showLoading({ title: '生成海报中...' })
+      try {
+        var res = await getShareQrCode('pages/activity/activity-detail/activity-detail', 'id=' + this.activity.id)
+        if (res && res.data) {
+          this.posterQrCode = res.data.qrCodeBase64 || ''
+          this.posterSummary = this.buildPosterSummary()
+          this.posterPriceText = this.activity.price > 0 ? '¥' + this.activity.price : '免费'
+          this.posterInviteText = '「' + (uni.getStorageSync('nickname') || '好友') + '」邀请你一起参加这个活动'
+          this.showPoster = true
+        }
+      } catch (e) {
+        uni.showToast({ title: '生成失败', icon: 'none' })
+      } finally {
+        uni.hideLoading()
+      }
+    },
+    buildPosterSummary: function() {
+      var desc = (this.activity && this.activity.description) || ''
+      desc = desc.replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim()
+      return desc.length > 60 ? desc.substring(0, 60) + '...' : desc
+    },
+    closePoster: function() {
+      this.showPoster = false
+    },
     submitRegistration: function() {
       var self = this
       // Validate

+ 9 - 1
cfc-frontend/pages/activity/create.vue

@@ -76,6 +76,12 @@
         </view>
       </view>
 
+      <!-- 会员价 -->
+      <view class="form-item">
+        <text class="form-label">会员价(元)</text>
+        <input class="form-input" type="digit" v-model="memberPriceYuan" placeholder="留空等于普通价" />
+      </view>
+
       <!-- 封面图片 -->
       <view class="form-item">
         <text class="form-label">封面图片</text>
@@ -143,6 +149,7 @@ export default {
         checkinPoints: 0
       },
       priceYuan: '0',
+      memberPriceYuan: '',
       maxParticipantsStr: '0',
       datePart: '',
       timePart: '',
@@ -197,6 +204,7 @@ export default {
           self.form.visibility = d.visibility || 'public'
           self.maxParticipantsStr = String(self.form.maxParticipants)
           self.priceYuan = self.form.price ? (self.form.price / 100).toString() : '0'
+          self.memberPriceYuan = self.form.memberPrice && self.form.memberPrice > 0 ? (self.form.memberPrice / 100).toString() : ''
           if (d.startTime) {
             var sd = new Date(d.startTime.replace ? d.startTime.replace(/-/g, '/') : d.startTime)
             self.datePart = self.fmtDate(sd)
@@ -336,7 +344,7 @@ export default {
       var payload = Object.assign({}, this.form)
       payload.maxParticipants = parseInt(this.maxParticipantsStr) || 0
       payload.price = Math.round((parseFloat(this.priceYuan) || 0) * 100)
-      payload.memberPrice = payload.price // 会员价默认等于普通价
+      payload.memberPrice = this.memberPriceYuan ? Math.round(parseFloat(this.memberPriceYuan) * 100) : payload.price
       if (this.activityId) payload.id = this.activityId
       return payload
     },

+ 24 - 2
cfc-frontend/pages/article-center/article-detail.vue

@@ -82,6 +82,7 @@
 import { getArticleDetail, reportReadingTime, getShareQrCode, completeArticleRead, generateQuiz, submitQuiz } from '@/utils/api.js'
 import shareMixin from '../../components/share-mixin.js'
 import ContentSharePoster from '@/components/ContentSharePoster.vue'
+import config from '@/config.js'
 
 export default {
   mixins: [shareMixin],
@@ -106,7 +107,7 @@ export default {
     if (options && options.id) { this.articleId = options.id; this.loadDetail(options.id) }
     else { this.error = true; this.errorMsg = '参数错误'; this.loading = false }
   },
-  onShow() { if (this.article && !this.error) this.startReadingTimer() },
+  onShow() { if (this.article && !this.error) this.startNewReadingSession() },
   onHide() { this.pauseReadingTimer() },
   onUnload() { this.pauseReadingTimer() },
   methods: {
@@ -116,6 +117,8 @@ export default {
         var res = await getArticleDetail({ id: id })
         if (res.code === 200 && res.data) {
           this.article = res.data
+          this.article.coverImage = this.getImageUrl(this.article.coverImage)
+          this.article.content = this.resolveContentUrls(this.article.content)
           this.setShareInfo('推荐阅读: ' + (res.data.title || ''), '/pages/article-center/article-detail?id=' + id)
           this.startReadingTimer()
         } else { this.error = true; this.errorMsg = '文章不存在或无权限查看' }
@@ -123,6 +126,18 @@ export default {
       finally { this.loading = false }
     },
     formatDate(d) { return d ? d.slice(0, 10) : '' },
+    getImageUrl: function(path) {
+      if (!path) return ''
+      if (path.indexOf('http://') === 0 || path.indexOf('https://') === 0) return path
+      return config.API_BASE_URL + path
+    },
+    resolveContentUrls: function(html) {
+      if (!html) return html
+      // 将富文本内容中的相对图片路径(/uploads/...)拼接为绝对 URL
+      return html.replace(/(src|href)=["']\/(uploads\/[^"']+)/g, function(match, attr, path) {
+        return attr + '="' + config.API_BASE_URL + '/' + path + '"'
+      })
+    },
     parseDimensions: function(article) {
       var dimMap = { body: { code: 'body', color: '#FF8C42', name: '身' }, mind: { code: 'mind', color: '#FF6B9D', name: '心' }, wisdom: { code: 'wisdom', color: '#6366F1', name: '智' }, action: { code: 'action', color: '#10B981', name: '行' }, wealth: { code: 'wealth', color: '#F59E0B', name: '富' } }
       var raw = article && article.relatedDimensions ? article.relatedDimensions.split(',').map(function(s) { return s.trim().toLowerCase() }) : []
@@ -144,12 +159,19 @@ export default {
       return best ? best.code : ''
     },
     onScrollToBottom() {},
+    startNewReadingSession: function() {
+      // 每次进入文章详情页都从 0 开始新会话,时长不延续上一次
+      this.readingSeconds = 0
+      this.lastReportedSeconds = 0
+      this.startReadingTimer()
+    },
     startReadingTimer: function() {
       if (this.isTimerRunning || this.readingCompleted) return
       this.isTimerRunning = true
       var self = this
       this.timerHandle = setInterval(function() { self.readingSeconds++ }, 1000)
-      this.syncHandle = setInterval(function() { self.reportReadingTime() }, 30000)
+      // 每 10 秒自动记录一次该用户已读时长
+      this.syncHandle = setInterval(function() { self.reportReadingTime() }, 10000)
     },
     pauseReadingTimer: function() {
       if (!this.isTimerRunning) return

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

@@ -111,7 +111,7 @@
       dimensionCode="body"
       :familyId="activeChildId"
       :products="dimensionProducts"
-      title="为你推荐"
+      title="推荐商品"
       @productClick="goProductDetail"
       @moreProducts="goMoreProducts" />
 

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

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

+ 3 - 1
cfc-frontend/pages/home-pages/parent-index.vue

@@ -38,7 +38,9 @@
 <FamilyChallengeCard
   v-for="ch in activeChallenges"
   :key="ch.id"
-:challenge="ch" />
+  :challenge="ch"
+  :childId="currentChildId"
+  @updated="loadFamilyInteraction" />
 
     <!-- ===== 健康圈入口(P3:社区扩散) ===== -->
     <view class="circle-entry-section">

+ 2 - 2
cfc-frontend/pages/mind-extra/articles.vue

@@ -332,14 +332,14 @@ export default {
     },
     onSearchBlur: function() {
       if (!this.searchKeyword) {
-      this.searchFocused = false
+        this.searchFocused = false
+      }
     },
     getImageUrl: function(path) {
       return config.API_BASE_URL + path
     }
   }
 }
-}
 </script>
 
 <style scoped>

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

@@ -117,7 +117,7 @@
       dimensionCode="mind"
       :familyId="currentChildId"
       :products="dimensionProducts"
-      title="为你推荐"
+      title="推荐商品"
       @productClick="goProductDetail"
       @moreProducts="goMoreProducts" />
 

+ 0 - 619
cfc-frontend/pages/points/mall.vue

@@ -1,619 +0,0 @@
-<template>
-  <view class="container">
-    <view class="header">
-      <text class="title">积分商城</text>
-      <view class="points-info">
-        <text class="points-label">我的积分</text>
-        <text class="points-value">{{ systemPoints }}</text>
-      </view>
-    </view>
-
-    <view class="category-tabs">
-      <view
-        v-for="tab in categories"
-        :key="tab.key"
-        class="tab-item"
-        :class="{ active: currentCategory === tab.key }"
-        @click="onCategoryChange(tab.key)"
-      >
-        <text class="tab-text">{{ tab.label }}</text>
-      </view>
-    </view>
-
-    <scroll-view
-      scroll-y
-      class="product-list"
-      @scrolltolower="onLoadMore"
-      refresher-enabled
-      :refresher-triggered="refreshing"
-      @refresherrefresh="onRefresh"
-    >
-      <view v-if="products.length > 0" class="product-grid">
-        <view
-          v-for="product in products"
-          :key="product.id"
-          class="product-card"
-          @click="onProductClick(product)"
-        >
-          <image
-            class="product-cover"
-            :src="getImageUrl(product.coverImage) || '/static/default-product.png'"
-            mode="aspectFill"
-          />
-          <view class="product-info">
-            <text class="product-name">{{ product.name }}</text>
-            <text class="product-points">{{ product.pointsPrice }} 积分</text>
-            <view class="exchange-btn" @click.stop="onExchangeClick(product)">
-              <text class="exchange-btn-text">兑换</text>
-            </view>
-          </view>
-        </view>
-      </view>
-      <view v-else-if="!loading" class="empty-state">
-        <text class="empty-text">暂无商品</text>
-      </view>
-      <view v-if="loadingMore" class="loading-more">
-        <text class="loading-text">加载中...</text>
-      </view>
-    </scroll-view>
-
-    <view class="records-btn" @click="onRecordsClick">
-      <text class="records-btn-text">兑换记录</text>
-    </view>
-
-    <view v-if="showExchangeModal" class="modal-mask" @click="onCloseModal">
-      <view class="modal-content" @click.stop>
-        <text class="modal-title">确认兑换</text>
-        <view v-if="selectedProduct" class="modal-product">
-          <image
-            class="modal-cover"
-            :src="selectedProduct.coverImage || '/static/default-product.png'"
-            mode="aspectFill"
-          />
-          <view class="modal-info">
-            <text class="modal-name">{{ selectedProduct.name }}</text>
-            <text class="modal-points">{{ selectedProduct.pointsPrice }} 积分</text>
-          </view>
-        </view>
-        <view class="modal-actions">
-          <view class="modal-btn cancel" @click="onCloseModal">
-            <text class="modal-btn-text">取消</text>
-          </view>
-          <view class="modal-btn confirm" @click="onConfirmExchange">
-            <text class="modal-btn-text">确认兑换</text>
-          </view>
-        </view>
-      </view>
-    </view>
-
-    <view v-if="showRecordsModal" class="modal-mask" @click="onCloseRecordsModal">
-      <view class="modal-content records-modal" @click.stop>
-        <text class="modal-title">兑换记录</text>
-        <scroll-view scroll-y class="records-list">
-          <view v-if="records.length > 0">
-            <view
-              v-for="record in records"
-              :key="record.id"
-              class="record-item"
-            >
-              <view class="record-header">
-                <text class="record-name">{{ record.productName }}</text>
-                <text class="record-points">-{{ record.pointsCost }}积分</text>
-              </view>
-              <view class="record-meta">
-                <text class="record-code">兑换码: {{ record.redeemCode }}</text>
-                <text class="record-date">{{ formatDate(record.createdAt) }}</text>
-              </view>
-            </view>
-          </view>
-          <view v-else class="empty-state">
-            <text class="empty-text">暂无兑换记录</text>
-          </view>
-        </scroll-view>
-        <view class="modal-close" @click="onCloseRecordsModal">
-          <text class="modal-close-text">关闭</text>
-        </view>
-      </view>
-    </view>
-  </view>
-</template>
-
-<script>
-import {
-  getPointsExchangeProducts,
-  submitPointsExchange,
-  getPointsExchangeRecords,
-  getSystemBalance
-} from '@/utils/api.js'
-import config from '@/config.js'
-
-export default {
-  data() {
-    return {
-      categories: [
-        { key: 'all', label: '全部' },
-        { key: 'vip', label: 'VIP' },
-        { key: 'redeem', label: '兑换' },
-        { key: 'flow', label: '流量' },
-        { key: 'gift', label: '礼品' }
-      ],
-      currentCategory: 'all',
-      products: [],
-      records: [],
-      systemPoints: 0,
-      pageNum: 1,
-      pageSize: 10,
-      loading: false,
-      loadingMore: false,
-      refreshing: false,
-      hasMore: true,
-      showExchangeModal: false,
-      showRecordsModal: false,
-      selectedProduct: null
-    }
-  },
-  onLoad() {
-    this.loadSystemPoints()
-    this.loadProducts()
-  },
-  onShow() {
-    this.loadSystemPoints()
-  },
-  methods: {
-    loadSystemPoints() {
-      var childId = uni.getStorageSync('currentChildId')
-      getSystemBalance(childId).then(function(res) {
-        if (res.code === 200 && res.data) {
-          this.systemPoints = res.data.systemPoints || 0
-        }
-      }.bind(this)).catch(function() {})
-    },
-    loadProducts() {
-      this.loading = true
-      getPointsExchangeProducts({
-        category: this.currentCategory,
-        pageNum: this.pageNum,
-        pageSize: this.pageSize
-      }).then(function(res) {
-        this.loading = false
-        if (res.code === 200 && res.data) {
-          var list = res.data.records || []
-          if (this.pageNum === 1) {
-            this.products = list
-          } else {
-            this.products = this.products.concat(list)
-          }
-          this.hasMore = list.length >= this.pageSize
-        }
-      }.bind(this)).catch(function() {
-        this.loading = false
-      }.bind(this))
-    },
-    onCategoryChange(category) {
-      this.currentCategory = category
-      this.pageNum = 1
-      this.products = []
-      this.loadProducts()
-    },
-    onRefresh() {
-      this.refreshing = true
-      this.pageNum = 1
-      getPointsExchangeProducts({
-        category: this.currentCategory,
-        pageNum: 1,
-        pageSize: this.pageSize
-      }).then(function(res) {
-        this.refreshing = false
-        if (res.code === 200 && res.data) {
-          this.products = res.data.records || []
-          this.hasMore = (res.data.records || []).length >= this.pageSize
-        }
-      }.bind(this)).catch(function() {
-        this.refreshing = false
-      }.bind(this))
-    },
-    onLoadMore() {
-      if (!this.hasMore && !this.loadingMore) {
-        this.loadingMore = true
-        this.pageNum = this.pageNum + 1
-        getPointsExchangeProducts({
-          category: this.currentCategory,
-          pageNum: this.pageNum,
-          pageSize: this.pageSize
-        }).then(function(res) {
-          this.loadingMore = false
-          if (res.code === 200 && res.data) {
-            var list = res.data.records || []
-            this.products = this.products.concat(list)
-            this.hasMore = list.length >= this.pageSize
-          }
-        }.bind(this)).catch(function() {
-          this.loadingMore = false
-        }.bind(this))
-      }
-    },
-    onProductClick(product) {
-      this.selectedProduct = product
-      this.showExchangeModal = true
-    },
-    onExchangeClick(product) {
-      this.selectedProduct = product
-      this.showExchangeModal = true
-    },
-    onCloseModal() {
-      this.showExchangeModal = false
-      this.selectedProduct = null
-    },
-    onConfirmExchange() {
-      if (!this.selectedProduct) {
-        return
-      }
-      var productId = this.selectedProduct.id
-      submitPointsExchange({ productId: productId, quantity: 1 }).then(function(res) {
-        if (res.code === 200) {
-          uni.showToast({ title: '兑换成功', icon: 'success' })
-          this.loadSystemPoints()
-          this.loadProducts()
-        }
-      }.bind(this)).catch(function(err) {
-        uni.showToast({ title: err && err.message ? err.message : '兑换失败', icon: 'none' })
-      }.bind(this))
-      this.onCloseModal()
-    },
-    onRecordsClick() {
-      this.loadRecords()
-      this.showRecordsModal = true
-    },
-    loadRecords() {
-      getPointsExchangeRecords().then(function(res) {
-        if (res.code === 200 && res.data) {
-          this.records = res.data || []
-        }
-      }.bind(this)).catch(function() {})
-    },
-    onCloseRecordsModal() {
-      this.showRecordsModal = false
-    },
-    formatDate(dateStr) {
-      if (!dateStr) {
-        return ''
-      }
-      var date = new Date(dateStr)
-      var year = date.getFullYear()
-      var month = date.getMonth() + 1
-      var day = date.getDate()
-      return year + '-' + (month < 10 ? '0' + month : month) + '-' + (day < 10 ? '0' + day : day)
-    },
-    getImageUrl: function(path) {
-      return config.API_BASE_URL + path
-    }
-  }
-}
-</script>
-
-<style scoped>
-.container {
-  min-height: 100vh;
-  background-color: #f5f5f5;
-  padding-bottom: 120rpx;
-}
-
-.header {
-  background: linear-gradient(135deg, #ff8c42 0%, #ff6b35 100%);
-  padding: 40rpx 30rpx;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-
-.title {
-  font-size: 40rpx;
-  font-weight: bold;
-  color: #ffffff;
-}
-
-.points-info {
-  display: flex;
-  flex-direction: column;
-  align-items: flex-end;
-}
-
-.points-label {
-  font-size: 24rpx;
-  color: rgba(255, 255, 255, 0.8);
-}
-
-.points-value {
-  font-size: 48rpx;
-  font-weight: bold;
-  color: #ffffff;
-}
-
-.category-tabs {
-  display: flex;
-  background-color: #ffffff;
-  padding: 20rpx 0;
-  border-bottom: 1rpx solid #eeeeee;
-  overflow-x: auto;
-}
-
-.tab-item {
-  flex: 1;
-  text-align: center;
-  padding: 16rpx 0;
-  margin: 0 10rpx;
-  border-radius: 30rpx;
-  background-color: #f5f5f5;
-}
-
-.tab-item.active {
-  background-color: #ff8c42;
-}
-
-.tab-text {
-  font-size: 28rpx;
-  color: #666666;
-}
-
-.tab-item.active .tab-text {
-  color: #ffffff;
-  font-weight: bold;
-}
-
-.product-list {
-  padding: 20rpx;
-}
-
-.product-grid {
-  display: flex;
-  flex-wrap: wrap;
-  justify-content: space-between;
-}
-
-.product-card {
-  width: 48%;
-  background-color: #ffffff;
-  border-radius: 16rpx;
-  overflow: hidden;
-  margin-bottom: 20rpx;
-  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
-}
-
-.product-cover {
-  width: 100%;
-  height: 240rpx;
-}
-
-.product-info {
-  padding: 20rpx;
-}
-
-.product-name {
-  font-size: 28rpx;
-  color: #333333;
-  font-weight: bold;
-  display: block;
-  margin-bottom: 10rpx;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-.product-points {
-  font-size: 32rpx;
-  color: #ff6b35;
-  font-weight: bold;
-  display: block;
-  margin-bottom: 16rpx;
-}
-
-.exchange-btn {
-  background: linear-gradient(135deg, #ff8c42 0%, #ff6b35 100%);
-  border-radius: 30rpx;
-  padding: 16rpx 0;
-  text-align: center;
-}
-
-.exchange-btn-text {
-  font-size: 28rpx;
-  color: #ffffff;
-  font-weight: bold;
-}
-
-.empty-state {
-  text-align: center;
-  padding: 100rpx 0;
-}
-
-.empty-text {
-  font-size: 28rpx;
-  color: #999999;
-}
-
-.loading-more {
-  text-align: center;
-  padding: 30rpx 0;
-}
-
-.loading-text {
-  font-size: 24rpx;
-  color: #999999;
-}
-
-.records-btn {
-  position: fixed;
-  bottom: 40rpx;
-  left: 50%;
-  transform: translateX(-50%);
-  background: linear-gradient(135deg, #ff8c42 0%, #ff6b35 100%);
-  border-radius: 40rpx;
-  padding: 24rpx 60rpx;
-  box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.3);
-}
-
-.records-btn-text {
-  font-size: 30rpx;
-  color: #ffffff;
-  font-weight: bold;
-}
-
-.modal-mask {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background-color: rgba(0, 0, 0, 0.5);
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  z-index: 1000;
-}
-
-.modal-content {
-  background-color: #ffffff;
-  border-radius: 20rpx;
-  padding: 40rpx;
-  width: 80%;
-  max-width: 600rpx;
-}
-
-.records-modal {
-  max-height: 70vh;
-  display: flex;
-  flex-direction: column;
-}
-
-.modal-title {
-  font-size: 36rpx;
-  font-weight: bold;
-  color: #333333;
-  text-align: center;
-  display: block;
-  margin-bottom: 30rpx;
-}
-
-.modal-product {
-  display: flex;
-  align-items: center;
-  margin-bottom: 30rpx;
-  padding: 20rpx;
-  background-color: #f9f9f9;
-  border-radius: 12rpx;
-}
-
-.modal-cover {
-  width: 120rpx;
-  height: 120rpx;
-  border-radius: 8rpx;
-  margin-right: 20rpx;
-}
-
-.modal-info {
-  flex: 1;
-}
-
-.modal-name {
-  font-size: 30rpx;
-  color: #333333;
-  font-weight: bold;
-  display: block;
-  margin-bottom: 10rpx;
-}
-
-.modal-points {
-  font-size: 32rpx;
-  color: #ff6b35;
-  font-weight: bold;
-}
-
-.modal-actions {
-  display: flex;
-  justify-content: space-between;
-}
-
-.modal-btn {
-  flex: 1;
-  padding: 20rpx 0;
-  border-radius: 12rpx;
-  text-align: center;
-  margin: 0 10rpx;
-}
-
-.modal-btn.cancel {
-  background-color: #f5f5f5;
-}
-
-.modal-btn.confirm {
-  background: linear-gradient(135deg, #ff8c42 0%, #ff6b35 100%);
-}
-
-.modal-btn-text {
-  font-size: 30rpx;
-  color: #666666;
-  font-weight: bold;
-}
-
-.modal-btn.confirm .modal-btn-text {
-  color: #ffffff;
-}
-
-.records-list {
-  flex: 1;
-  overflow-y: auto;
-  max-height: 50vh;
-}
-
-.record-item {
-  padding: 20rpx 0;
-  border-bottom: 1rpx solid #eeeeee;
-}
-
-.record-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 10rpx;
-}
-
-.record-name {
-  font-size: 28rpx;
-  color: #333333;
-  font-weight: bold;
-}
-
-.record-points {
-  font-size: 28rpx;
-  color: #ff6b35;
-  font-weight: bold;
-}
-
-.record-meta {
-  display: flex;
-  justify-content: space-between;
-}
-
-.record-code {
-  font-size: 24rpx;
-  color: #666666;
-}
-
-.record-date {
-  font-size: 24rpx;
-  color: #999999;
-}
-
-.modal-close {
-  margin-top: 20rpx;
-  padding: 20rpx 0;
-  background: linear-gradient(135deg, #ff8c42 0%, #ff6b35 100%);
-  border-radius: 12rpx;
-  text-align: center;
-}
-
-.modal-close-text {
-  font-size: 30rpx;
-  color: #ffffff;
-  font-weight: bold;
-}
-</style>

+ 0 - 1
cfc-frontend/pages/shop/cart/cart.vue

@@ -85,7 +85,6 @@
 
 <script>
 import config from '@/config.js'
-import config from '@/config.js'
 import { getMyMembership } from '@/utils/api.js'
 
 export default {

+ 3 - 3
cfc-frontend/pages/shop/index/index.vue

@@ -65,10 +65,10 @@
       :refresher-triggered="refreshing"
       @refresherrefresh="onRefresh"
     >
-      <!-- 为你推荐 -->
+      <!-- 推荐商品 -->
       <view class="recommend-section" v-if="recommended.length > 0">
         <view class="recommend-header">
-          <text class="recommend-title">🔥 为你推荐</text>
+          <text class="recommend-title">🔥 推荐商品</text>
         </view>
         <scroll-view scroll-x enable-flex show-scrollbar="false" class="recommend-scroll">
           <view
@@ -410,7 +410,7 @@ export default {
   font-weight: 500;
 }
 
-/* 为你推荐 */
+  /* 推荐商品 */
 .recommend-section {
   background: #fff;
   border-radius: 16rpx;

+ 2 - 10
cfc-frontend/pages/wealth/index.vue

@@ -9,14 +9,6 @@
         tagline="利他创富 · 价值共享"
         quote="富足源于利他与坚持" />
 
-      <!-- 商品推荐 -->
-      <DimensionProducts
-        dimensionCode="wealth"
-        :products="dimensionProducts"
-        title="为你推荐"
-        @productClick="goProductDetail"
-        @moreProducts="goMoreProducts" />
-
       <!-- 推荐阅读 -->
       <ArticleBookshelf
         :articles="wealthArticles"
@@ -174,7 +166,7 @@
         dimensionCode="wealth"
         :familyId="activeChildId"
         :products="dimensionProducts"
-        title="为你推荐"
+        title="推荐商品"
         @productClick="goProductDetail"
         @moreProducts="goMoreProducts" />
 
@@ -604,7 +596,7 @@ export default {
       var routes = {
         education: '/pages/assessment/results',
         social: '/pages/activity/index',
-        points: '/pages/points/mall',
+        points: '/pages/shop/index',
         emotion: '/pages/mind-detail/emotion-report',
         understanding: '/pages/mind-detail/emotion-trend',
         legacy: '/pages/wishes/index'

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

@@ -103,7 +103,7 @@
       dimensionCode="wisdom"
       :familyId="activeChildId"
       :products="dimensionProducts"
-      title="为你推荐"
+      title="推荐商品"
       @productClick="goProductDetail"
       @moreProducts="goMoreProducts" />
 

+ 14 - 0
cfc-frontend/patch-node-compat.cjs

@@ -0,0 +1,14 @@
+// Node v24 兼容 polyfill:旧版 uni-app 工具链依赖已移除的 util 方法
+const util = require('util');
+if (typeof util.isRegExp !== 'function') {
+  util.isRegExp = function(v) { return Object.prototype.toString.call(v) === '[object RegExp]'; };
+}
+if (typeof util.isArray !== 'function') {
+  util.isArray = Array.isArray;
+}
+if (typeof util.isDate !== 'function') {
+  util.isDate = function(v) { return Object.prototype.toString.call(v) === '[object Date]'; };
+}
+if (typeof util.isError !== 'function') {
+  util.isError = function(v) { return Object.prototype.toString.call(v) === '[object Error]' || v instanceof Error; };
+}

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-d1bafbf59e9ec6dadb37bdf40d6c514ed015e2c5
+87b1303c873d1dff96e83beb45f6377a2a2401e8

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

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

+ 1 - 1
cfc-web/package.json

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

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

@@ -4,6 +4,90 @@
 
 ---
 
+## v1.0.739 (2026-08-02)
+
+### 新功能
+- 商城入口统一指向商品商城/移除积分商城+推荐商品文案统一+商城标题'浠艾福商城'
+
+### Bug 修复
+- 新建商品时显示SKU维护入口
+
+### 其他
+- - ProductSkuEditor: 支持新建时暂存 SKU,商品创建后自动提交
+- - handleSave: 创建后更新 form.id,触发 SKU 编辑器 watch
+- 
+
+
+## v1.0.738 (2026-08-02)
+
+### 新功能
+- 支持集体总合与全员个人双目标模式
+
+### 其他
+- - DatabaseInitializer 新增幂等迁移:target_mode (VARCHAR(20) DEFAULT 'aggregate') 与 target_value (INT DEFAULT 0)
+- - schema.sql 同步 family_challenge 表结构
+- - FamilyChallenge 实体新增 targetMode/targetValue 字段与 @TableField(exist=false) 的 totalProgress/memberProgress
+- - FamilyChallengeService 扩展 templates 数组为 8 列(添加目标列),重构 checkAndSettle() 按目标模式分支判定:
+-   - aggregate:全体 progress 合计 ≥ target_value 即达标
+-   - all_members:家庭内每个成员 progress ≥ target_value(无记录=0 判失败)
+- - 抽离 normalizeTarget() 实现旧数据向后兼容;fillProgress() 为 /list /history 填充总进度与成员进度
+- 前端:
+- - FamilyChallengeCard 重写:双模式目标展示(aggregate=总进度条 X/Y;all_members=成员进度列表+达标✓),打卡上报入口(updateChallengeProgress({challengeId, childId, delta}) → $emit('updated'))
+- - parent-index 卡片传 :childId="currentChildId" + 监听 @updated="loadFamilyInteraction"
+- - patch-node-compat.cjs:node v24 polyfill(util.isRegExp 等已被移除),构建时通过 NODE_OPTIONS="--require ./patch-node-compat.cjs" 注入
+- 
+
+### 文档
+- /运营: 小红书内容 D14/D15/D16、第三篇-月入2万的家庭、发布指南、README 同步更新
+
+
+## v1.0.737 (2026-08-02)
+
+### 新功能
+- 放开已上架文章和活动的编辑权限,便于初期维护
+
+### 其他
+- - 前端 ArticleEdit/ActivityEdit 的 published 不再进入 readonly
+- - 列表页 published 文章按钮由「查看」改为「编辑」
+- - 保存按钮文案与提示语按状态区分(保存修改/保存草稿)
+- 
+
+
+## v1.0.735 (2026-08-02)
+
+### Bug 修复
+- wangEditor 初始化配置提前到 created 生命周期
+
+### 其他
+- 避免 mounted 阶段编辑器实例已初始化导致配置不生效
+- 
+
+
+## v1.0.734 (2026-08-02)
+
+### Bug 修复
+- 修复外部提交引入的语法错误,恢复小程序构建
+
+### 其他
+- - articles.vue: onSearchBlur 补齐闭合花括号、移除多余 }(e327a0ef 引入)
+- 
+- - 上报间隔 30000→10000,每10秒自动记录该用户已读时长
+- - 退出文章时 pauseReadingTimer 记录当时时长(保留原逻辑)
+- 
+
+### 新功能
+- 文章详情阅读时长每次进入从0开始计时,10秒上报一次
+
+
+## v1.0.733 (2026-08-02)
+
+### 新功能
+- add counterfeit-warning product page, gut-kit image, HRW article image, update sales index
+- 虚拟支付道具映射 实体/Mapper/Service/管理端CRUD接口
+- 新增wechat.appkey与virtual-pay-env配置 小程序虚拟支付paySig签名密钥与环境(0现网/1沙箱)
+- 迁移143 创建virtual_goods_config道具映射表 测评/会员/订阅虚拟支付道具映射,UNIQUE(goods_type,biz_key),幂等
+
+
 ## v1.0.732 (2026-08-02)
 
 ### 新功能

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

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.732
+> 当前版本: v1.0.739
 
 ## 历史版本
 
@@ -8,6 +8,90 @@
 
 ---
 
+## v1.0.739 (2026-08-02)
+
+### 新功能
+- 商城入口统一指向商品商城/移除积分商城+推荐商品文案统一+商城标题'浠艾福商城'
+
+### Bug 修复
+- 新建商品时显示SKU维护入口
+
+### 其他
+- - ProductSkuEditor: 支持新建时暂存 SKU,商品创建后自动提交
+- - handleSave: 创建后更新 form.id,触发 SKU 编辑器 watch
+- 
+
+
+## v1.0.738 (2026-08-02)
+
+### 新功能
+- 支持集体总合与全员个人双目标模式
+
+### 其他
+- - DatabaseInitializer 新增幂等迁移:target_mode (VARCHAR(20) DEFAULT 'aggregate') 与 target_value (INT DEFAULT 0)
+- - schema.sql 同步 family_challenge 表结构
+- - FamilyChallenge 实体新增 targetMode/targetValue 字段与 @TableField(exist=false) 的 totalProgress/memberProgress
+- - FamilyChallengeService 扩展 templates 数组为 8 列(添加目标列),重构 checkAndSettle() 按目标模式分支判定:
+-   - aggregate:全体 progress 合计 ≥ target_value 即达标
+-   - all_members:家庭内每个成员 progress ≥ target_value(无记录=0 判失败)
+- - 抽离 normalizeTarget() 实现旧数据向后兼容;fillProgress() 为 /list /history 填充总进度与成员进度
+- 前端:
+- - FamilyChallengeCard 重写:双模式目标展示(aggregate=总进度条 X/Y;all_members=成员进度列表+达标✓),打卡上报入口(updateChallengeProgress({challengeId, childId, delta}) → $emit('updated'))
+- - parent-index 卡片传 :childId="currentChildId" + 监听 @updated="loadFamilyInteraction"
+- - patch-node-compat.cjs:node v24 polyfill(util.isRegExp 等已被移除),构建时通过 NODE_OPTIONS="--require ./patch-node-compat.cjs" 注入
+- 
+
+### 文档
+- /运营: 小红书内容 D14/D15/D16、第三篇-月入2万的家庭、发布指南、README 同步更新
+
+
+## v1.0.737 (2026-08-02)
+
+### 新功能
+- 放开已上架文章和活动的编辑权限,便于初期维护
+
+### 其他
+- - 前端 ArticleEdit/ActivityEdit 的 published 不再进入 readonly
+- - 列表页 published 文章按钮由「查看」改为「编辑」
+- - 保存按钮文案与提示语按状态区分(保存修改/保存草稿)
+- 
+
+
+## v1.0.735 (2026-08-02)
+
+### Bug 修复
+- wangEditor 初始化配置提前到 created 生命周期
+
+### 其他
+- 避免 mounted 阶段编辑器实例已初始化导致配置不生效
+- 
+
+
+## v1.0.734 (2026-08-02)
+
+### Bug 修复
+- 修复外部提交引入的语法错误,恢复小程序构建
+
+### 其他
+- - articles.vue: onSearchBlur 补齐闭合花括号、移除多余 }(e327a0ef 引入)
+- 
+- - 上报间隔 30000→10000,每10秒自动记录该用户已读时长
+- - 退出文章时 pauseReadingTimer 记录当时时长(保留原逻辑)
+- 
+
+### 新功能
+- 文章详情阅读时长每次进入从0开始计时,10秒上报一次
+
+
+## v1.0.733 (2026-08-02)
+
+### 新功能
+- add counterfeit-warning product page, gut-kit image, HRW article image, update sales index
+- 虚拟支付道具映射 实体/Mapper/Service/管理端CRUD接口
+- 新增wechat.appkey与virtual-pay-env配置 小程序虚拟支付paySig签名密钥与环境(0现网/1沙箱)
+- 迁移143 创建virtual_goods_config道具映射表 测评/会员/订阅虚拟支付道具映射,UNIQUE(goods_type,biz_key),幂等
+
+
 ## v1.0.732 (2026-08-02)
 
 ### 新功能

+ 3 - 0
cfc-web/src/views/admin/Activities.vue

@@ -76,6 +76,9 @@
         <el-table-column label="费用(元)" width="90">
           <template slot-scope="{ row }">{{ row.price ? (row.price / 100).toFixed(2) : '免费' }}</template>
         </el-table-column>
+        <el-table-column label="会员价(元)" width="90">
+          <template slot-scope="{ row }">{{ row.memberPrice && row.memberPrice > 0 ? (row.memberPrice / 100).toFixed(2) : '-' }}</template>
+        </el-table-column>
         <el-table-column label="人数" width="90">
           <template slot-scope="{ row }">
             <el-button type="text" size="mini" @click="showRegistrations(row)" style="padding:0;">

+ 23 - 5
cfc-web/src/views/admin/ActivityEdit.vue

@@ -6,8 +6,8 @@
         <div class="admin-page-actions">
           <el-button @click="$router.push('/activities')">返回列表</el-button>
           <template v-if="!readonly && activityStatus !== 'rejected'">
-            <el-button type="primary" @click="saveActivity(false)" :loading="saving">保存草稿</el-button>
-            <el-button v-if="isEdit" type="success" @click="handleSubmitReview" :loading="reviewLoading">提交审核</el-button>
+            <el-button type="primary" @click="saveActivity(false)" :loading="saving">{{ activityStatus === 'published' ? '保存修改' : '保存草稿' }}</el-button>
+            <el-button v-if="isEdit && activityStatus !== 'published'" type="success" @click="handleSubmitReview" :loading="reviewLoading">提交审核</el-button>
           </template>
           <template v-if="activityStatus === 'rejected'">
             <el-button type="primary" @click="handleReDraft" :loading="reDraftLoading">保存到草稿箱重新编辑</el-button>
@@ -90,6 +90,14 @@
           </el-col>
         </el-row>
 
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="会员价(元)">
+              <el-input-number v-model="form.memberPriceYuan" :min="0" :precision="2" :step="0.01" placeholder="留空等于普通价" style="width:100%" :disabled="readonly" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
         <el-row :gutter="16">
           <el-col :span="12">
             <el-form-item label="开始时间" prop="startTime">
@@ -185,6 +193,7 @@ export default {
         location: '',
         maxParticipants: null,
         priceYuan: null,
+        memberPriceYuan: null,
         startTime: null,
         endTime: null,
         visibility: 'public',
@@ -255,13 +264,18 @@ export default {
             server: baseURL + '/api/admin/articles/upload/image',
             headers: token ? { Authorization: 'Bearer ' + token } : {},
             maxNumberOfFiles: 1,
+            maxFileSize: 5 * 1024 * 1024,
             fieldName: 'file',
             customAlert: (msg, type) => {
               this.$message && this.$message({ message: msg, type: type || 'error' })
             },
-            onSuccess(res, insertFn) {
+            onError: (file, err, res) => {
+              const msg = (file && file.name || '图片') + ' 上传失败,图片不能超过 5MB'
+              this.$message && this.$message({ message: msg, type: 'error' })
+            },
+            customInsert(res, insertFn) {
               if (res.code === 200) {
-                const url = res.data.url || res.data
+                const url = res.data && res.data.url ? res.data.url : res.data
                 insertFn(baseURL + url)
               }
             }
@@ -281,7 +295,7 @@ export default {
           this.activityStatus = d.status || ''
           this.auditReason = d.auditReason || ''
           // Set readonly based on workflow status
-          if (d.status === 'pending' || d.status === 'rejected' || d.status === 'published' || d.status === 'withdrawn') {
+          if (d.status === 'pending' || d.status === 'rejected' || d.status === 'withdrawn') {
             this.readonly = true
           } else if (this.$route.query.readonly === '1') {
             this.readonly = true
@@ -298,6 +312,7 @@ export default {
             location: d.location || '',
             maxParticipants: d.maxParticipants || null,
             priceYuan: d.price ? d.price / 100 : null,
+            memberPriceYuan: d.memberPrice && d.memberPrice > 0 ? d.memberPrice / 100 : null,
             startTime: d.startTime || null,
             endTime: d.endTime || null,
             visibility: d.visibility || 'public',
@@ -366,6 +381,7 @@ export default {
           location: this.form.location,
           maxParticipants: this.form.maxParticipants,
           price: this.form.priceYuan != null ? Math.round(this.form.priceYuan * 100) : 0,
+          memberPrice: this.form.memberPriceYuan != null && this.form.memberPriceYuan > 0 ? Math.round(this.form.memberPriceYuan * 100) : 0,
           startTime: this.form.startTime,
           endTime: this.form.endTime,
           visibility: this.form.visibility,
@@ -415,6 +431,7 @@ export default {
         location: '',
         maxParticipants: null,
         priceYuan: null,
+        memberPriceYuan: null,
         startTime: null,
         endTime: null,
         visibility: 'public',
@@ -469,6 +486,7 @@ export default {
           location: this.form.location,
           maxParticipants: this.form.maxParticipants,
           price: this.form.priceYuan != null ? Math.round(this.form.priceYuan * 100) : 0,
+          memberPrice: this.form.memberPriceYuan != null && this.form.memberPriceYuan > 0 ? Math.round(this.form.memberPriceYuan * 100) : 0,
           startTime: this.form.startTime,
           endTime: this.form.endTime,
           visibility: this.form.visibility,

+ 9 - 5
cfc-web/src/views/admin/ArticleEdit.vue

@@ -6,7 +6,7 @@
         <div class="admin-page-actions">
           <el-button @click="$router.push('/article-manage')">返回列表</el-button>
           <template v-if="!readonly">
-            <el-button type="primary" @click="handleSave(false)" :loading="saving">保存草稿</el-button>
+            <el-button type="primary" @click="handleSave(false)" :loading="saving">{{ articleStatus === 'published' ? '保存修改' : '保存草稿' }}</el-button>
             <el-button type="success" @click="handleSubmitReview" :loading="reviewLoading" v-if="isEdit && articleStatus === 'draft' && !articleWasPublished">提交审核</el-button>
             <el-button type="success" @click="handleSave(true)" :loading="publishing" v-if="!isEdit || articleStatus === 'draft'">直接发布</el-button>
           </template>
@@ -260,9 +260,13 @@ export default {
               customAlert: (msg, type) => {
                 this.$message && this.$message({ message: msg, type: type || 'error' })
               },
-              onSuccess(res, insertFn) {
+onError: (file, err, res) => {
+              const msg = (file && file.name || '图片') + ' 上传失败,图片不能超过 5MB'
+              this.$message && this.$message({ message: msg, type: 'error' })
+            },
+              customInsert(res, insertFn) {
                 if (res.code === 200) {
-                  const url = res.data.url || res.data
+                  const url = res.data && res.data.url ? res.data.url : res.data
                   insertFn(baseURL + url)
                 }
               }
@@ -306,7 +310,7 @@ export default {
             this.readonly = true
           } else if (d.status === 'rejected') {
             this.readonly = true
-          } else if (d.status === 'published' || d.status === 'withdrawn') {
+          } else if (d.status === 'withdrawn') {
             this.readonly = true
           } else {
             this.readonly = this.$route.query.readonly === '1'
@@ -457,7 +461,7 @@ export default {
             console.error('保存维度权重失败', e)
           }
         }
-        this.$message.success(publishNow ? '文章已发布' : '草稿已保存')
+        this.$message.success(publishNow ? '文章已发布' : (this.articleStatus === 'published' ? '修改已保存' : '草稿已保存'))
         this.$router.push('/article-manage')
       } catch (e) {
         this.$message.error(e.message || '保存失败')

+ 2 - 2
cfc-web/src/views/admin/ArticleManage.vue

@@ -161,9 +161,9 @@
                 </el-dropdown-menu>
               </el-dropdown>
             </template>
-            <!-- 发布中:查看 + 下架 + 精选 -->
+            <!-- 发布中:编辑 + 下架 + 精选 -->
             <template v-else-if="row.status === 'published'">
-              <el-button size="mini" type="primary" @click="$router.push('/article-edit?id=' + row.id + '&readonly=1')">查看</el-button>
+              <el-button size="mini" type="primary" @click="$router.push('/article-edit?id=' + row.id)">编辑</el-button>
               <el-dropdown trigger="hover" @command="(cmd) => handleActionCmd(row, cmd)">
                 <el-button size="mini">
                   更多<i class="el-icon-arrow-down el-icon--right"></i>

+ 13 - 6
cfc-web/src/views/admin/ProductEdit.vue

@@ -159,8 +159,8 @@
           <dimension-weight-picker v-model="form.dimensionWeights" :show-presets="true" />
         </el-form-item>
 
-        <el-form-item v-if="isEdit" label="SKU规格">
-          <ProductSkuEditor :productId="form.id" />
+        <el-form-item label="SKU规格">
+          <ProductSkuEditor ref="skuEditor" :productId="form.id" />
         </el-form-item>
 
         <!-- 购买信息采集 -->
@@ -262,14 +262,19 @@ export default {
             server: '',
             headers: {},
             maxNumberOfFiles: 1,
+            maxFileSize: 5 * 1024 * 1024,
             fieldName: 'file',
             customAlert: (msg, type) => {
               this.$message && this.$message({ message: msg, type: type || 'error' })
             },
-            onSuccess(res, insertFn) {
+            onError: (file, err, res) => {
+              const msg = (file && file.name || '图片') + ' 上传失败,图片不能超过 5MB'
+              this.$message && this.$message({ message: msg, type: 'error' })
+            },
+            customInsert(res, insertFn) {
               if (res.code === 200) {
                 const baseURL = process.env.VUE_APP_BASE_API || ''
-                const url = res.data.url || res.data
+                const url = res.data && res.data.url ? res.data.url : res.data
                 insertFn(baseURL + url)
               }
             }
@@ -302,9 +307,11 @@ export default {
       return this.supplySystems.find(s => s.id === this.form.distributionSystemId)
     }
   },
-  mounted() {
+  created() {
     this.editorConfig.MENU_CONF.uploadImage.server = this.uploadActionUrl
     this.editorConfig.MENU_CONF.uploadImage.headers = this.uploadHeaders
+  },
+  mounted() {
     this.loadSupplySystems()
     this.loadPredefinedFields()
     if (this.isEdit) {
@@ -530,9 +537,9 @@ export default {
             productId = this.form.id
           } else {
             res = await createProduct(requestPayload)
-            // Get the created product id
             if (res.code === 200 && res.data) {
               productId = res.data.id || res.data
+              this.form.id = productId
             }
           }
           if (res.code === 200) {

+ 36 - 2
cfc-web/src/views/admin/components/ProductSkuEditor.vue

@@ -5,7 +5,7 @@
       <el-button type="primary" size="mini" icon="el-icon-plus" @click="showDialog(null)">添加SKU</el-button>
     </div>
 
-    <el-table :data="skus" border stripe size="small" :max-height="tableHeight">
+    <el-table :data="effectiveSkus" border stripe size="small" :max-height="tableHeight">
       <el-table-column prop="id" label="ID" width="60" />
       <el-table-column label="规格" min-width="120">
         <template slot-scope="{ row }">
@@ -99,6 +99,7 @@ export default {
   data() {
     return {
       skus: [],
+      pendingSkus: [],
       loading: false,
       dialogVisible: false,
       saving: false,
@@ -122,6 +123,9 @@ export default {
     tableHeight() {
       return window.innerHeight - 300
     },
+    effectiveSkus() {
+      return this.productId ? this.skus : this.pendingSkus
+    },
     dialogTitle() {
       return this.editingId ? '编辑SKU' : '添加SKU'
     },
@@ -137,8 +141,12 @@ const baseURL = process.env.VUE_APP_BASE_API || ''
   watch: {
     productId: {
       immediate: true,
-      handler(val) {
+      async handler(val) {
         if (val) {
+          // 新建商品时:先用暂存 SKU 创建
+          if (this.pendingSkus.length > 0) {
+            await this.flushPendingSkus()
+          }
           this.loadSkus()
         }
       }
@@ -271,6 +279,16 @@ const baseURL = process.env.VUE_APP_BASE_API || ''
             image: this.skuForm.image,
             enabled: this.skuForm.enabled
           }
+          // 新建商品时productId为空,暂存到本地
+          if (!this.productId) {
+            payload.id = Date.now()
+            delete payload.productId
+            this.pendingSkus.push(payload)
+            this.dialogVisible = false
+            this.$message.success('添加成功,保存商品时将自动创建SKU')
+            this.saving = false
+            return
+          }
           let res
           if (this.editingId) {
             payload.id = this.editingId
@@ -293,7 +311,23 @@ const baseURL = process.env.VUE_APP_BASE_API || ''
         }
       })
     },
+    async flushPendingSkus() {
+      if (!this.productId || this.pendingSkus.length === 0) return
+      const results = await Promise.allSettled(
+        this.pendingSkus.map(sku => createProductSku({ ...sku, productId: this.productId }))
+      )
+      this.pendingSkus = []
+      results.forEach((r, i) => {
+        if (r.status === 'rejected') {
+          console.error('创建SKU失败', i + 1, r.reason)
+        }
+      })
+    },
     async handleDelete(id) {
+      if (!this.productId) {
+        this.pendingSkus = this.pendingSkus.filter(s => s.id !== id)
+        return
+      }
       try {
         await this.$confirm('确认删除该SKU?', '提示')
         const res = await deleteProductSku(id)

+ 1 - 1
运营文案/小红书内容/D14-对外人客气是因为不在乎.md

@@ -32,7 +32,7 @@
 
 我认识一个妈妈,用这两个方法试了三个月,孩子的家庭归属感评分从2.0升到了7.3。不是换了孩子,是她换了说话的方式。
 
-今晚试试。把到嘴边的那句你应该,换成谢谢你
+今晚回去就试,评论区打1,我明天回你。关注我,下周讲:你以为你爱孩子,其实你爱的是"他应该成为的样子"
 
 ## 标签
 #亲子关系 #家庭教育 #情绪管理 #沟通技巧 #育儿 #主动健康 #家庭幸福

+ 56 - 0
运营文案/小红书内容/D15-老人带娃翻脸是爱的语言不同.md

@@ -0,0 +1,56 @@
+# D15 老人带娃翻脸,是爱的语言不同
+
+> 维度:心 #FF6B9D | 参考《家的算法》第7章 心·大爱相随
+> 系列:老人带娃三部曲·之一(D15/D20/D25)
+> 发布编号:D15 | 状态:草稿箱 | 字数:675
+
+## 标题
+老人带娃翻脸,是爱的语言不同(14字)
+
+## 正文
+
+老人带娃起冲突,你以为是方法不对——不是,是爱的语言不同。
+
+你妈给孩子夹菜、添饭、追着喂,你烦了:妈,别惯着他,让他自己吃。你妈委屈了:我辛辛苦苦给你带孩子,你还嫌我。
+
+你觉得自己没错——科学育儿,培养独立性。她也觉得自己没错——怕孩子饿着,这是爱。
+
+你们都没错。但你们在用不同的语言说爱。
+
+我认识一个女孩叫小茜。每次回家,桌上永远有一盘她爱吃的鱼。父母觉得,给她吃好喝好就是最好的爱。但小茜想要的不是鱼,是有人问她一句:你最近在想什么?
+
+两种爱都没错,但语言不通。老人觉得我付出了这么多你怎么还不满意,年轻人觉得你们根本不理解我。
+
+这不是谁对谁错的问题,是两个人的爱的语言不匹配。
+
+怎么解决?三步走。
+
+第一步,看见她的语言。老人给你带孩子,她用的方式是她从小被爱的方式——吃饱穿暖就是爱。不是她固执,她的词典里就这些词。
+
+第二步,翻译你的话。不要说妈别追着喂了,试试:妈,我知道你怕他饿着。以后让他自己吃,吃不完我再喂。翻译过来就是:我看到你的爱了,咱换个方式一起爱他。
+
+第三步,找共同语言。不是谁听谁的,是找一条两个人都能走的路。比如:白天你按你的方式带,晚饭后我来管。
+
+我认识一个年轻妈妈,用这个方法试了两周,跟我说:以前跟婆婆说话,开口就是妈你别,现在变成了妈,谢谢你。不是婆婆变了,是她换了翻译器。
+
+今晚回去试试。对帮你带娃的老人说一句:妈,你辛苦了。评论区打1,我明天回你。关注我,下周讲:为什么老人一插手你就急。
+
+## 标签
+#亲子关系 #家庭教育 #婆媳关系 #老人带娃 #情绪管理 #主动健康 #家庭幸福
+
+## 20秒动作
+对带娃的老人说一句"妈,你辛苦了"
+
+## 关键数据
+爱的语言不匹配→误解循环(参考书稿第7章·大爱三境界)
+
+## 图片(7张)
+| 幻灯片 | 文字 |
+|--------|------|
+| slide_01_cover.png | 老人带娃翻脸 / 是爱的语言不同 |
+| slide_02_scene.png | 你妈追着喂饭 / 你皱眉说别惯着 |
+| slide_03_story.png | 小茜家的鱼 / 父母夹鱼,她想要的是倾听 |
+| slide_04_insight.png | 不是方法不对 / 是爱的语言不同 |
+| slide_05_method.png | 三步走 / 看见他的语言 翻译你的语言 找到共同语言 |
+| slide_06_method2.png | 不是谁说服谁 / 是找到第三条路 |
+| slide_07_cta.png | 今晚试试 / 对带娃的老人说一句:妈,你辛苦了 |

+ 56 - 0
运营文案/小红书内容/D16-体检正常不等于健康.md

@@ -0,0 +1,56 @@
+# D16 体检正常≠健康,你信反了吗(v4)
+
+> 维度:身 #FF8C42 | 参考书稿第3章 身·主动健康
+> 发布编号:D16 | 状态:草稿箱 | 字数:1000
+
+## 核心论点(v4)
+主动健康的答案:**体检查出来的是"问题"(已发生、需治疗、影响生活),不是"风险"(未发生)。人们逃避体检,本质是逃避"查出问题后生活被打乱"。所以主动健康必须:在不影响正常生活的前提下,科学发现风险 + 简单干预。**
+
+## 标题
+体检正常≠健康,你信反了吗(13字)
+
+## 正文
+
+体检报告每一项都正常,你就觉得自己健康了?
+
+先别急着回答,我先问你:你有多久没敢去体检了?
+
+很多人不是不爱惜身体,是怕体检。你仔细想想,你怕的真是那台机器吗?不是。你怕的是结果出来之后的一连串事情:医生说这个指标要复查,那个指标要吃药,然后你就得早睡、忌口、戒酒、每天走一万步。
+
+你怕的,是查出问题之后,那些干预手段会把你现在的生活全打乱。
+
+这就是关键:体检查出来的不是风险,是问题。风险是还没发生的事,问题已经发生了,查出来就得治疗,治疗就得影响生活。
+
+所以大家躲着体检,本质上躲的是病被发现了,生活就得变。
+
+主动健康要解决的,恰恰是这个矛盾:能不能在问题还没发生的时候,就把风险发现出来?发现之后,能不能用不影响正常生活的办法去干预?
+
+能。而且只能这样。
+
+第一,发现风险要科学,但科学不等于一年一次体检。身体每天都在给你发信号:精力是升是降,睡眠是好是坏,体重是不是悄悄爬升,是不是爬三层楼就喘。这些微小变化比报告更早,把它们记下来,你就有了体检之外的另一双眼睛。但这双眼睛只看得见身体开口的事,有些风险身体自己都没察觉,就已经在靠近了。怎么提前看见,我改天专门讲。
+
+第二,干预一定要简单,简单到不影响生活。主动健康的干预,不是让你戒掉所有爱吃的东西,是晚饭少盛半碗;不是让你每天去健身房,是睡前把手机放到客厅;不是逼你十点睡,是躺下之后不再刷下一条。
+
+我认识一个朋友小付,三十八岁,体检有三项指标逼近临界值。医生说要注意,他出了诊室就忘。为什么?因为医生的注意意味着生活要改,他改不动。直到有一天他半夜抱着发烧的孩子在客厅走了两个小时,才突然意识到:身体早就提醒过他了——爬三层楼开始喘,下午三四点犯困,动不动就上火。这些信号他全没当回事。
+
+后来他没做什么了不起的事,就改了一件事:睡前把手机放到客厅。两周后入睡快了,精力回来了。他说:不是我能坚持,是我根本没机会拿到手机。
+
+发现风险靠科学,干预靠简单,全程不影响你的正常生活——这就是主动健康的答案。
+
+今晚先试一件事:睡前把手机放到客厅充电,别带进卧室。
+
+想要那份《身体信号自查清单》,评论区回复:方法,我私信发你。照着记一周,你就有体检之外的另一双眼睛。
+
+关注我,下周讲:你喝的每一口水,可能比外卖还脏。再往后,我讲怎么看见身体自己都没察觉的风险。
+
+## 标签
+#主动健康 #体检 #睡眠 #健康管理 #家庭健康 #养生
+
+## Slide 文案(7图=7金句)
+1. 封面:体检正常≠健康 / 你信反了吗
+2. 场景:你怕的不是体检 / 是查出问题后的生活
+3. 故事:身体早就提醒过你 / 只是你没当回事
+4. 数据:发现风险要科学 / 干预一定要简单
+5. 洞察:体检是查病 / 不是查健康
+6. 方法:不影响生活的干预 / 才不用靠坚持
+7. CTA:今晚试试 / 睡前把手机放到客厅

+ 29 - 9
运营文案/小红书内容/README.md

@@ -10,13 +10,22 @@
 | 早期 | 智 | 暑假亲子 | `第一篇-暑假没意思.md` | 1428 | 已发布 |
 | D1 | 身 | 早睡20秒 | `第三篇-早睡20秒.md` | 1220 | 已发布 |
 | D2 | 智 | 专注力-大脑 | `第二篇-不是不想专注_是大脑没吃饱.md` | 1217 | 已发布 |
-| D3 | 富 | 购物/财商 | `第三篇-购物车里躺着一个不再看的东西.md` | 714 | 已发布 |
+| D3 | 富 | 购物/财商 | `第三篇-月入2万的家庭比想象中穷得更稳.md` | 714 | 已发布(线上标题已改) |
 | D4 | 行 | 通讯录1493人 | `第四篇-通讯录1493人.md` | 773 | 已发布 |
 | D5 | 心 | 情绪开关 | `第五篇-吼完孩子试试这个情绪开关.md` | 904 | 已发布 |
 | D6 | 身 | 哈佛呼吸法 | `第六篇-哈佛呼吸法3分钟换个人.md` | 814 | 已发布 |
 | D7 | 智 | 周复盘 | `第七篇-复盘孩子本周3个进步比报班管用100倍.md` | 747 | 已发布 |
 | D8 | 富 | 每月定投 | `第八篇-每月定投1次比每天看余额管用.md` | 797 | 已发布 |
-| D9 | 行 | 老人带娃分工 | `第九篇-老人带娃容易翻脸分清这3件事就好了.md` | 1073 | 待发布 |
+| D9 | 行 | 老人带娃分工 | `第九篇-老人带娃容易翻脸分清这3件事就好了.md` | 1073 | 已发布 |
+| D10 | 心 | 必修课 | `第十篇-???` | — | 待发布 |
+| D11 | 身 | 睡眠两周证 | `D11-睡好两周后脑子真的变快了.md` | 793 | 草稿箱 |
+| D12 | 智 | 词汇力量两周证 | `D12-坚持读14天后词汇量涨了.md` | 798 | 草稿箱 |
+| D13 | 富 | 资产增值曲线证 | `D13-14天家庭资产柱状图钱去哪了一眼看清.md` | 652 | 草稿箱 |
+| D14 | 行 | 对外人客气→不在乎 | `D14-对外人客气是因为不在乎.md` | 751 | 草稿箱 |
+| D15 | 心 | 老人带娃·爱的语言 | `D15-老人带娃翻脸是爱的语言不同.md` | 675 | 草稿箱 |
+| D16 | 身 | 体检正常≠健康 | `D16-体检正常不等于健康.md` | 751 | 草稿箱 |
+| D20 | 心 | 老人带娃·掌控感 | 待创作 | — | 规划中 |
+| D25 | 心 | 老人带娃·三代同堂 | 待创作 | — | 规划中 |
 
 ## 备选稿件(未采用)
 
@@ -38,15 +47,26 @@
 
 ## 五维分布统计
 
-| 维度 | 已发布数量 | 比例 |
+| 维度 | 已发布/草稿 | 比例 |
 |------|-----------|------|
-| 🌏 身(主动健康) | 2(D1, D6) | 22% |
-| ⚔️ 智(赋能未来) | 2(早期, D2, D7)→ 实际3 | 33% |
-| 💧 富(利他创富) | 1(D3, D8)→ 实际2 | 22% |
-| 🌿 行(人际和谐) | 1(D4, D9)→ 实际2 | 22% |
-| 🔥 心(大爱相随) | 0(D5待定)→ 实际1 | 11% |
+| 🌏 身(主动健康) | D1, D6, D11 → 3 | 25% |
+| ⚔️ 智(赋能未来) | 早期, D2, D7, D12 → 4 | 33% |
+| 💧 富(利他创富) | D3, D8, D13 → 3 | 25% |
+| 🌿 行(人际和谐) | D4, D9, D14 → 3 | 25% |
+| 🔥 心(大爱相随) | D5, D10 → 2 | 17% |
 
-注:D5(心)已发布,统计按已发布计算。
+注:D10 待发布,D11-D14 在草稿箱。含草稿箱的话心维度最弱(仅2篇),需补足。
+
+## 老人带娃系列(D15/D20/D25)
+
+基于 D9(曝光1572/CTR 14.8% 双冠)延展的心维度系列:
+
+| 篇目 | 标题 | 维度 | 核心 |
+|------|------|------|------|
+| D9 | 老人带娃容易翻脸,分清这3件事就好了 | 行 | 总纲:分工清单(已发布) |
+| **D15** | **老人带娃翻脸,是爱的语言不同** | **心** | **爱的语言:看见家人(草稿箱)** |
+| D20 | 老人一插手你就急?因为你怕失去掌控权 | 心 | 看见自己:情绪觉察(规划中) |
+| D25 | 三代同堂,学会用对方的语言说话 | 心 | 爱的语言匹配(规划中) |
 
 ## 后续维护建议
 

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

@@ -1,7 +1,6 @@
-# 浠艾福(Care Family)小红书发布指南 v2
+# 浠艾福(Care Family)小红书发布指南 v8
 
-> 更新日期:2026-07-27 | 基于 7 篇已发布内容复盘 + 行业趋势(2026 小红书母婴育儿赛道)  
-> 取代旧版(2026-07-19,仅针对第一篇内容)  
+> 更新日期:2026-07-30 | 基于 9 篇已发布 + 4 篇草稿箱复盘  
 > 配套文件:`合集命名.md`(维度合集规范)、`内容方法论-六阶模型.md`(内容骨架)
 
 ---
@@ -10,16 +9,22 @@
 
 ### 1.1 已发布内容概览
 
-| 编号 | 标题 | 维度 | 字数 | 标题长度 | 有无20秒动作 | 评论区钩子 | 数据案例 | 标签数 |
-|------|------|------|------|----------|-------------|-----------|---------|-------|
-| 早期 | 当孩子说"没意思"… | 智 | ~900 | 17字✓ | ✗ | 评论分享 | 78%统计数据 | 5 |
-| D1 | 让你早睡的不是自律,是这3件小事 | 身 | ~700 | 14字✓ | ✓手机放卧室外 | ✓打卡"城市" | ✓n=246 | 5✓ |
-| D2 | 不是不想专注,是大脑没吃饱 | 智 | ~800 | 11字✓ | ✓睡前10小时挑战 | ✓打卡"10小时" | ✓小辰A2数据 | 5✓ |
-| D3 | 购物车里躺着一个三天不会再看的东西 | 富 | ~500 | 16字✓ | ✓记账软件打开 | ✓评论告诉我 | ✗无硬数据 | 3✗ |
-| D4 | 通讯录里1493人,凌晨两点却没人问 | 行 | ~400 | 17字✓ | ✓发"在吗" | ✓评论告诉我 | ✗无硬数据 | 4✗ |
-| D5 | 吼完孩子,试试这个情绪开关 | 心 | ~500 | 14字✓ | ✓闻花香+吹蜡烛 | ✓今晚用几次 | ✗结构化示例 | 5✓ |
-| D6 | 哈佛呼吸法,3分钟让你换一个人 | 身 | ~500 | 15字✓ | ✓4-7-8呼吸 | ✓做几个循环 | ✗个人体感 | 5✓ |
-| D7 | 复盘孩子本周3个进步比报班管用100倍 | 智 | ~400 | 19字✓ | ✓写3个进步 | ✓打卡1个进步 | ✓A2提升0.6分 | 6✓ |
+| 编号 | 标题 | 维度 | 字数 | 标题长度 | 有无20秒动作 | 评论区钩子 | 数据案例 | 标签数 | 状态 |
+|------|------|------|------|----------|-------------|-----------|---------|-------|------|
+| 早期 | 当孩子说"没意思"… | 智 | ~900 | 17字✓ | ✗ | 评论分享 | 78%统计数据 | 5 | 已发布 |
+| D1 | 让你早睡的不是自律,是这3件小事 | 身 | ~700 | 14字✓ | ✓手机放卧室外 | ✓打卡"城市" | ✓n=246 | 5✓ | 已发布 |
+| D2 | 不是不想专注,是大脑没吃饱 | 智 | ~800 | 11字✓ | ✓睡前10小时挑战 | ✓打卡"10小时" | ✓小辰A2数据 | 5✓ | 已发布 |
+| D3 | 购物车里躺着一个三天不会再看的东西 | 富 | ~500 | 16字✓ | ✓记账软件打开 | ✓评论告诉我 | ✗无硬数据 | 3✗ | 已发布 |
+| D4 | 通讯录里1493人,凌晨两点却没人问 | 行 | ~400 | 17字✓ | ✓发"在吗" | ✓评论告诉我 | ✗无硬数据 | 4✗ | 已发布 |
+| D5 | 吼完孩子,试试这个情绪开关 | 心 | ~500 | 14字✓ | ✓闻花香+吹蜡烛 | ✓今晚用几次 | ✗结构化示例 | 5✓ | 已发布 |
+| D6 | 哈佛呼吸法,3分钟让你换一个人 | 身 | ~500 | 15字✓ | ✓4-7-8呼吸 | ✓做几个循环 | ✗个人体感 | 5✓ | 已发布 |
+| D7 | 复盘孩子本周3个进步比报班管用100倍 | 智 | ~400 | 19字✓ | ✓写3个进步 | ✓打卡1个进步 | ✓A2提升0.6分 | 6✓ | 已发布 |
+| D8 | 每月定投1次比每天看余额管用 | 富 | 797 | 16字✓ | ✓每月定投一次 | ✓评论告诉我 | ✓n=246 | 5✓ | 已发布 |
+| D9 | 老人带娃容易翻脸分清这3件事就好了 | 行 | 1073 | 19字✓ | ✓分工清单 | ✓评论分享 | ✗无硬数据 | 5✓ | 已发布 |
+| D11 | 睡好两周后脑子真的变快了 | 身→智 | 793 | 14字✓ | ✓睡前放手机 | ✓打卡"城市" | ✓睡眠质量两周均 | 7✓ | 草稿箱 |
+| D12 | 词穷到会说,只差这14天 | 智→富 | 798 | 14字✓ | ✓每天读一页 | ✓打卡"14天" | ✓词汇力增速 | 7✓ | 草稿箱 |
+| D13 | 存钱第一步,不是记账,是看见 | 富→行 | 652 | 16字✓ | ✓每天10秒记一笔 | ✓打卡"看见" | ✓14天柱状图 | 7✓ | 草稿箱 |
+| D14 | 对外人客气,是因为不在乎 | 行→心 | 751 | 12字✓ | ✓把"你应该"换成"谢谢你" | ✓评论区告诉我 | ✓归属感2.0→7.3 | 7✓ | 草稿箱 |
 
 ### 1.2 关键发现
 
@@ -329,14 +334,16 @@
 
 ## 五、内容生产节奏调整
 
-### 5.1 当前状态
+### 5.1 当前状态(2026-07-30 更新)
 
 | 指标 | 计划值 | 实际值 | 差距 |
 |------|--------|--------|------|
-| 已发布内容 | 30篇(30天日更) | ~7篇 | -23篇 |
+| 已发布内容 | 30篇(30天日更) | 9篇(早期-D9) | -21篇 |
+| 草稿箱待发 | — | 4篇(D11-D14) | — |
 | 内容库存 | 30篇 | 13篇(含备稿) | -17篇 |
-| 数据追踪 | 每篇 | 0篇 | -7篇 |
-| 合集挂载 | 5个合集 | 0个 | -5个 |
+| 数据追踪 | 每篇 | 0篇 | 仍挂零 |
+| 合集挂载 | 5个合集 | 0个 | 仍挂零 |
+| 书稿引用 | — | 已完成(D14引用第6章) | 新能力 |
 
 ### 5.2 调整建议
 
@@ -344,12 +351,11 @@
 
 | 阶段 | 时间 | 目标 | 节奏 |
 |------|------|------|------|
-| 恢复期 | 第1-2周 | 补齐数据追踪 + 合集挂载 | 周2-3篇 |
-| 稳定期 | 第3-4周 | 稳定质量,积累数据 | 周3篇 |
-| 加速期 | 第5-8周 | 根据数据放大好选题 | 周3-4篇 |
-| 冲刺期 | 第9-12周 | 找到爆款公式后加量 | 周4-5篇 |
+| 积稿期 | 当前 | 集中创作 D11-D15 草稿,攒够库存 | 周3-4篇 |
+| 发布期 | 下一阶段 | 从草稿箱逐步发布,补数据追踪 | 周2-3篇 |
+| 迭代期 | 参考数据反馈 | 根据曝光/互动率调整选题方向 | 周3篇 |
 
-**核心原则**:先有数据,再谈增长。没有数据复盘,发再多也是盲打
+**核心原则**:先有数据,再谈增长。草稿箱已有 4 篇待发,下一步优先发布 + 补数据追踪
 
 ---
 
@@ -897,7 +903,10 @@ C:\code\cfc\运营文案\小红书发布\       (发布目录)
 
 ## 18. 书稿内容参考(book/ 目录)
 
-> `book/` 目录下有一本完整的书稿,覆盖五维能量体系(身·智·富·行·心),可作为小红书内容创作的素材库。
+> `book/` 目录下有一本完整的书稿《家的算法》(五维能量让幸福有迹可循),覆盖五维能量体系(身·智·富·行·心)。
+>
+> **小红书内容的核心定位:为书做选题测试和流量预热。**
+> 每篇小红书文章 = 一个书稿章节的"市场测试"——测试哪个角度有流量、哪个标题有人点、哪个方法有人收藏。
 
 ### 18.1 目录结构
 
@@ -937,12 +946,20 @@ book/
 | 第六章 | 行 | 人际和谐、家庭关系——行动力 |
 | 第七章 | 心 | 大爱相随、情绪稳定——内心力量 |
 
-### 18.3 使用方式
+### 18.3 使用方式(双向:书→小红书,小红书→书)
 
+**书 → 小红书(素材输出)**:
 每篇小红书文章撰写前,先查阅对应维度的书稿章节:
 1. 找 1-2 个书稿中的观点或案例作为素材
 2. 按 5 段式结构(引入→冲突→知识→方法→CTA)改写
-3. 标注数据来源:`(参考书稿第 X 章)`
+3. 不直接引用"书里说"(书未上市),用"我认识一个女孩""我听过一个故事"代替
+
+**小红书 → 书(数据反馈)**:
+每篇小红书发布后,数据回流指导书稿调整:
+1. 哪个选题曝光高 → 书稿对应章节加篇幅
+2. 哪个标题CTR高 → 书的章节标题参考其公式
+3. 哪个方法收藏高 → 书稿对应方法加详述
+4. 哪个角度没人看 → 书稿对应角度删减或重写
 
 ### 18.4 书稿章节亮点
 
@@ -958,6 +975,534 @@ book/
 
 > **版本记录**:v7 → 2026-07-30 新增第 18 章 书稿内容参考
 
+---
+
+## 19. D11-D14 创作经验沉淀(2026-07-30)
+
+### 19.1 文章概览
+
+| 编号 | 标题 | 维度 | 字数 | 来源 | 核心方法 |
+|------|------|------|------|------|---------|
+| D11 | 睡好两周后脑子真的变快了 | 身→智 | 793 | 书稿第3章+睡眠科学 | 睡前放手机、喝还原水、读纸书 |
+| D12 | 词穷到会说,只差这14天 | 智→富 | 798 | 书稿第4章+中文优势 | 每天读一页、中文三千字可搭无限 |
+| D13 | 存钱第一步,不是记账,是看见 | 富→行 | 652 | 书稿第5章+资源增值管理 | 每天10秒记一笔、14天柱状图 |
+| D14 | 对外人客气,是因为不在乎 | 行→心 | 751 | 书稿第6章+期待落差心理学 | 目标停止法、"你应该"→"谢谢你" |
+
+### 19.2 迭代经验
+
+| 篇目 | 迭代次数 | 关键教训 |
+|------|---------|---------|
+| D11 | 6次(v1-v6) | 图片文字大小需反复调整;字体路径必须用 `C:\Windows\Fonts\msyhbd.ttc` |
+| D12 | 多次(标题+图片重画) | 标题从"坚持读14天"改为"词穷到会说"后更爆;中文优势段落是差异化亮点 |
+| D13 | 2次 | 富维度不要窄化为钱;"资源增值管理"可作D12词汇主题的延续 |
+| D14 | 3次(v1-v3) | v1"家是安全的"太常识→v2换"因为在乎"→v3最终定为"对外人客气是因为不在乎"(反常识) |
+
+### 19.3 图片生成经验
+
+| 经验 | 说明 |
+|------|------|
+| **每图必为金句** | **7张图=7个可单独传播的金句。禁止叙事/场景描述类水句(如"半夜抱着发烧的孩子")。每张图的文案必须单独截图发出也成立、让人想保存/转发。金句模板:反常识结论 / 对比转折 / 动作指令。** |
+| Agnes AI API 不稳定 | 偶尔 400/503 错误,需重试;prompt 含"child"可能触发内容策略 |
+| 字体路径 | 必须用 `C:\Windows\Fonts\msyhbd.ttc`,不要用用户目录下的版本 |
+| 文字大小 | 双行文字:标题 130-140pt,副标题 55-70pt,行距 60px+;单行上限约 8 字(1080px 画布内) |
+| 维度色系 | 身#FF8C42 智#6366F1 富#F59E0B 行#10B981 心#FF6B9D |
+| 文字叠加 | 底部 360px 维度色渐变 + 白色标题(带深色偏移阴影)+ 浅色副标题;禁止黑色圆角条 |
+
+### 19.4 书稿引用模式(新模式)
+
+D14 首次实践了"从书稿选主题"的创作模式:
+
+```
+1. 查阅 book/目录 → 选定书稿章节(第6章 行)
+2. 找开篇金句作为钩子("对着外人笑一天,推开家门脸就垮了")
+3. 提取核心概念(家庭和谐三原色、三明治沟通法、目标停止法)
+4. 按小红书5段式结构改写
+5. 标注参考来源(不直接写"书稿第6章",语言自然融入)
+```
+
+---
+
+## 20. 反常识标题与情绪驱动(2026-07-30 从热帖分析+心理学研究)
+
+### 20.1 核心发现
+
+> **爆款的内容不是"有用",是"让人想说'原来如此'"。**
+
+从 D14 的迭代和 小红书 热帖分析中,总结了最有力的标题公式:
+
+| 标题类型 | 公式 | 示例 | 爆款潜力 |
+|----------|------|------|---------|
+| **反常识型** | [常识认知] + 但其实[相反结论] | "对外人客气,是因为不在乎" | ⭐⭐⭐⭐⭐ |
+| **认知冲突型** | [人人知道的事] + [你不知道的原因] | "越在乎的人,越容易受伤" | ⭐⭐⭐⭐ |
+| **情绪共鸣型** | [场景] + [直击内心的话] | "你对外人笑一天,回家脸就垮了" | ⭐⭐⭐ |
+| **数据冲击型** | [数字] + [反差事实] | "78%的孩子觉得爸妈不关心自己" | ⭐⭐⭐⭐ |
+
+### 20.2 为什么"反常识"最有效
+
+从心理学研究和小红书热帖分析,结论一致:
+
+| 心理学原理 | 说明 | 在标题中的应用 |
+|-----------|------|---------------|
+| **认知失调** | 新信息与已有认知冲突时,大脑会主动寻求解释 | "对外人客气,是因为不在乎"——打破"礼貌=修养好"的认知 |
+| **好奇心缺口** | 知道"是什么"但不清楚"为什么"时,会忍不住点开 | "为什么越在乎的人,越容易被你伤害" |
+| **社会神经共振** | 读到"另一个自己"的故事时,大脑镜像神经元激活 | 场景钩子:"你有没有过这种时刻——" |
+| **期待落差** | 对家人期待高→失望→愤怒,这个链条是反常识的 | "对陌生人没有期待,所以他做什么你都不失望" |
+
+### 20.3 情绪触发点优先级
+
+从 2026 小红书家生活情绪白皮书和爆款笔记分析,最高频的情绪触发点:
+
+| 优先级 | 情绪 | 触发方式 | 适合维度 |
+|--------|------|---------|---------|
+| 1 | **焦虑→解惑** | 抛出焦虑("孩子不听话"),给出反常识答案 | 行/心 |
+| 2 | **内耗→释然** | 描述内耗场景("对家人发火后后悔"),给放下理由 | 心 |
+| 3 | **委屈→被理解** | "不是你一个人这样"的认同感 | 行/心 |
+| 4 | **好奇→原来如此** | 认知冲突+简洁解释 | 智/富 |
+| 5 | **愧疚→可改变** | "不是你坏,是大脑在保护你" | 身/心 |
+
+### 20.4 标题决策流程
+
+```
+写标题前,先问三个问题:
+1. 这个标题,我自己作为用户有强烈点击欲吗?
+2. 标题里有一个"反常识"或"认知冲突"吗?
+3. 看完标题,用户会想"为什么"吗?
+
+如果三个答案都是"是" → 好标题
+如果有一个"否" → 重写
+```
+
+### 20.5 已验证的爆款链条
+
+```
+对外人客气 → 是因为不在乎(反常识,认知冲突)
+↓
+为什么? → 期待落差(越在乎越失望)
+↓
+怎么办? → 目标停止法 + 三明治沟通法
+↓
+行动 → 把"你应该"换成"谢谢你"
+```
+
+**链条关键**:每一句都让读者有"原来如此"的顿悟感,而不是"我知道"的重复感。
+
+---
+
+## 21. 内容发布计划(2026-07-30 更新)
+
+### 21.1 当前进度
+
+| 轮次 | 集次 | 内容 | 维度 | 状态 |
+|------|------|------|------|------|
+| 第1-2轮 | ①-⑩ | 启动篇+打卡篇 | 身·智·富·行·心 | 已发布(9篇) |
+| **第3轮** | **⑪-⑮** | **证据共展篇** | **身·智·富·行·心** | **草稿箱(4篇)** |
+| 第4轮 | ⑯-⑳ | 角色补全篇 | 身·智·富·行·心 | 未开始 |
+| 第5轮 | ㉑-㉕ | 扩散篇 | 身·智·富·行·心 | 未开始 |
+| 第6轮 | ㉖-㉚ | 社群篇 | 身·智·富·行·心 | 未开始 |
+
+### 21.2 草稿箱待发布清单
+
+| 编号 | 标题 | 维度 | 字数 | 建议发布时间 |
+|------|------|------|------|-------------|
+| D11 | 睡好两周后脑子真的变快了 | 身 | 793 | 待定 |
+| D12 | 词穷到会说,只差这14天 | 智 | 798 | 待定 |
+| D13 | 存钱第一步,不是记账,是看见 | 富 | 652 | 待定 |
+| D14 | 对外人客气,是因为不在乎 | 行 | 751 | 待定 |
+| D15 | (心维度,书稿第7章) | 心 | — | 待创作 |
+
+### 21.3 下一步建议
+
+```
+优先级:
+1. 优先从草稿箱发布 D11-D14(按 身→智→富→行 顺序)
+2. 补 D15(心维度,参考书稿第7章"大爱三境界")
+3. 完成第3轮后,评估数据再决定第4轮方向
+4. 补数据追踪(D1-D9 的历史数据 + D11-D14 发布后数据)
+
+⚠️ 关键缺口:数据追踪至今为0,发布前必须先建立追踪机制
+```
+
+### 21.4 30天路线图(更新版)
+
+基于实际进度,调整后的路线:
+
+```
+第1周:发布 D11-D14(4篇,隔天发)
+第2周:创作+发布 D15(心维度),补数据追踪
+第3周:评估前10篇数据,决定第4轮方向
+第4周:启动第4轮(⑯-⑳),调整选题策略
+```
+
+---
+
+---
+
+## 22. 运营数据追踪(2026-07-30 首次拉取)
+
+### 22.0 核心问题改善方案(总纲)
+
+基于数据发现的4个核心问题,按优先级给出具体改善方案:
+
+| 优先级 | 问题 | 当前值 | 健康值 | 改善杠杆 |
+|--------|------|--------|--------|---------|
+| 🔴 P0 | 互动率 | 0.2% | ≥5% | 修改评论区钩子,零成本 |
+| 🔴 P0 | 收藏率 | 1.2 | ≥50 | 内容结构加"可收藏"段落 |
+| 🟡 P1 | 观看时长 | 16s | ≥30s | 开头3句重写,制造冲突 |
+| 🟢 P2 | 涨粉 | 0.1/篇 | ≥10/篇 | 每篇加关注理由 |
+
+### 22.0.1 改善一:开头3句制造冲突(提升观看时长)
+
+**问题**:当前平均观看16s,用户只看前1-2段就划走。原因是开头太平——用"你有没有过这种时刻"开头,虽然共鸣但不够尖锐。
+
+**改善方案**:每篇开头必须在前3句话内完成"冲突制造"。
+
+```
+❌ 旧写法(太慢):
+"你有没有过这种时刻——对着外人笑了一整天,推开家门的那一秒,脸就垮下来了。"
+
+✅ 新写法(前3句直接冲突):
+"你在外面脾气好,不是因为你修养好,是因为你不在乎。
+跟陌生人你客客气气,因为你不在乎他怎么看你。
+但回到家,孩子多叫两声你就烦了——因为你在乎,所以期待,期待落空就是愤怒。"
+```
+
+**核心原则**:第1句给结论(反常识/争议性),第2句给解释,第3句拉回读者自身。**不要在开头铺垫场景,直接给结论。**
+
+**3秒吸引力检查清单**(每篇写完自查):
+```
+□ 第1句:是一个"反常识"或"争议性"结论,不是场景描述
+□ 第2句:解释为什么,不是继续铺垫
+□ 第3句:回到读者身上,让他觉得"说的是我"
+□ 如果3句内没有制造冲突 → 重写开头
+```
+
+### 22.0.2 改善二:评论区钩子改造(提升互动率)
+
+**问题**:当前评论区钩子太泛——"来评论区告诉我,你陪了多久"——读者读完没有动力回复。
+
+**改善方案**:评论区钩子必须有"三个要素"——具体、低门槛、有回应感。
+
+```
+❌ 旧钩子(太泛):
+"来评论区告诉我,你陪了多久"
+"来评论区告诉我,你换了什么"
+
+✅ 新钩子(具体+低门槛+回应感):
+"第1天打卡的姐妹,在评论区打'1',我明天来回你"
+"做到的打1,没做到的打2,我看看有多少人"
+"今天试了的评论区集合,我一个个回"
+"你家的温度计打几分?1-10,评论区告诉我"
+```
+
+**评论区钩子公式**:
+
+```
+[具体动作] + [低门槛] + [回应承诺]
+
+示例:
+"第1天打卡"    +   "打'1'"    +   "我明天来回你"
+"你打几分"     +   "1-10"     +   "评论区告诉我"
+"今天试了"     +   "集合"     +   "我一个个回"
+```
+
+**禁止**:
+- ❌ "评论区告诉我"——太泛,读者不知道说什么
+- ❌ "你做到了吗"——读者懒得想
+- ❌ "分享你的故事"——门槛太高
+
+**必须**:
+- ✅ 给一个判断题:打1或打2
+- ✅ 给一个数字:1-10打分
+- ✅ 给一个关键词:打"打卡"
+- ✅ 加回应承诺:我明天回你 / 我一个个回
+
+### 22.0.3 改善三:内容加"可收藏"段落(提升收藏率)
+
+**问题**:纯叙事内容读完就过了,没有"值得存下来反复用"的东西。
+
+**改善方案**:每篇至少有一个"可收藏"元素——清单、步骤、公式、模板。
+
+```
+❌ 旧结构:场景 → 数据 → 方法 → CTA
+用户看完:哦,知道了 → 划走
+
+✅ 新结构:场景 → 冲突 → 方法 → 清单 → CTA
+                  ↓
+            插入"可收藏"段落
+```
+
+**可收藏元素的4种形式**:
+
+| 形式 | 示例 | 适合场景 |
+|------|------|---------|
+| **清单体** | "3个步骤/5个方法/7天计划" | 智/行维度 |
+| **对比表** | "之前→之后" 对比清单 | 身/富维度 |
+| **公式** | "一句话公式" 如"肯定+建议+收尾" | 行/心维度 |
+| **模板** | "你直接复制这段话去说" | 心/行维度 |
+
+**具体写法**:
+
+```
+✅ 清单体:
+"记住这3步就够了:
+第一步:肯定("你今天自己写完了作业,真棒")
+第二步:建议("客厅能不能收拾一下")
+第三步:收尾("弄完妈妈陪你玩一会儿")"
+
+✅ 模板:
+"直接复制这段话去说:
+'你今天自己写完了作业,真棒。
+另外客厅能不能收拾一下,我们一起弄。
+弄完妈妈陪你玩一会儿。'"
+
+✅ 对比表:
+"之前:'你怎么又把客厅弄这么乱'
+之后:'你今天自己写完了作业,真棒。另外客厅能不能收拾一下'"
+```
+
+**收藏率自检**:
+```
+写完后问自己:读者看完这篇,会不会截图或收藏?
+如果答案是不会 → 加一个清单/模板/公式
+```
+
+### 22.0.4 改善四:每篇加关注理由(提升涨粉)
+
+**问题**:9篇涨1粉,因为没有给读者"关注你"的理由。
+
+**改善方案**:在文章结尾或开头,加入"关注钩子"。
+
+```
+❌ 旧结尾:
+"今晚试试。来评论区告诉我,你换了什么。"
+
+✅ 新结尾(加关注理由):
+"关注我,下周讲:为什么你越是讲道理,孩子越不听。
+今晚先试试,评论区打1,我明天回你。"
+```
+
+**关注钩子的3种类型**:
+
+| 类型 | 话术 | 效果 |
+|------|------|------|
+| **系列预告** | "关注我,下周讲为什么你越讲道理孩子越不听" | 预告未来内容,创造期待 |
+| **身份认同** | "关注我,一起做不吼不叫的妈妈" | 建立身份归属 |
+| **连载感** | "这是第X篇,关注我看完整系列" | 系列化追更 |
+
+**每条文案结尾必须包含**:
+```
+1. 评论区钩子(§22.0.2 公式)
+2. 关注理由(预告/身份/连载)——至少占其一
+```
+
+### 22.0.5 改善示例(用D14改)
+
+```
+❌ D14 v3 当前版本(开头+CTA):
+
+开头:
+"你在外面脾气好,不是因为你修养好,是因为你不在乎。"
+
+CTA:
+"今晚试试。把到嘴边的那句你应该,换成谢谢你。
+来评论区告诉我,你换了什么。"
+
+✅ 改善后版本:
+
+开头(3句冲突):
+"你在外面脾气好,不是因为你修养好,是因为你不在乎。
+对陌生人你客客气气,因为你没有期待——他做什么你都不失望。
+但对家人你急、你气、你烦,恰恰是因为你太在乎了。"
+
+CTA(评论钩子+关注理由):
+"直接复制这句话去用:
+'谢谢你今天自己写了作业'——把"你应该"换成"谢谢你"。
+今晚回去就试,评论区打1,我明天回你。
+关注我,下周讲:为什么你越讲道理,孩子越不听。"
+```
+
+> 数据来源:小红书创作者中心「数据中心」API,2026-07-30 拉取,共 13 篇文章。
+
+### 22.1 全量数据一览
+
+| 排名 | 标题 | 曝光 | 观看 | CTR | 点赞 | 评论 | 收藏 | 分享 | 涨粉 | 观看时长 |
+|------|------|------|------|-----|------|------|------|------|------|---------|
+| 1 | 🏆 老人带娃容易翻脸,分清这3件事就好了 (D9) | 1,572 | 242 | **14.8%** | 3 | 4 | 3 | 0 | 0 | 28s |
+| 2 | 🥈 吼完孩子,试试这个情绪开关 (D5) | 918 | 62 | 6.6% | 6 | 0 | 1 | 3 | 0 | 17s |
+| 3 | 🥉 睡好两周后脑子真的变快了 (D11) | 523 | 30 | 5.7% | 0 | 0 | 2 | 1 | 0 | 6s |
+| 4 | 复盘孩子本周3个进步比报班管用100倍 (D7) | 293 | 18 | 6.2% | 3 | 0 | 1 | 0 | 1 | 8s |
+| 5 | 月入2万的家庭,比想象中穷得更稳 (D3) | 249 | 34 | **13.1%** | 2 | 2 | 1 | 0 | 0 | 18s |
+| 6 | 当妈25年才发现:自己才是给孩子最好的礼物 | 186 | 18 | **9.2%** | 2 | 0 | 0 | 0 | 0 | 22s |
+| 7 | 每月定投1次,比每天看余额管用100倍 (D8) | 184 | 16 | 8.2% | 1 | 0 | 2 | 0 | 0 | 19s |
+| 8 | 词穷到会说,只差这14天 (D12) | 176 | 6 | 3.4% | 1 | 0 | 0 | 0 | 0 | 7s |
+| 9 | 通讯录1493人,凌晨两点没人问 (D4) | 23 | 2 | 5.0% | 2 | 0 | 1 | 0 | 0 | 18s |
+
+### 22.2 关键发现
+
+#### CTR 冠军:D9 "老人带娃"(14.8%)
+- 标题和封面有强吸引力,但互动转化弱(赞3/评4/收3)
+- 说明:**点进去的人多,但内容没留住人**——人均观看28s,文章1073字,读完全文需要2-3分钟
+
+#### 互动冠军:D5 "吼完孩子"(6赞/3分享)
+- 曝光918,是所有文章中第二高
+- 情绪共鸣类内容互动率最高
+- 说明:**心维度内容(情绪管理)最容易引发互动**
+
+#### 最大问题:观看时长普遍偏低
+| 文章 | 观看时长 | 字数 | 读完需时 | 实际观看 |
+|------|---------|------|---------|---------|
+| D9 | 28s | 1073 | ~3min | 不到1/6 |
+| D5 | 17s | ~500 | ~1.5min | 约1/5 |
+| D11 | 6s | 793 | ~2min | 不到1/20 |
+| 平均值 | 16s | ~700 | ~2min | 约1/8 |
+
+**结论:用户平均只看完前1-2段就划走了。** 开头3秒必须抓住注意力。
+
+#### CTR 双高 vs 互动双低
+- D9(14.8% CTR)和 D3(13.1% CTR)封面点击率最高
+- 但 D3 仅 2 赞 2 评,D9 仅 3 赞 4 评
+- 说明:**标题/封面吸引人≠内容留住人**——内容的前3段必须承接标题的承诺
+
+### 22.3 维度表现对比
+
+| 维度 | 代表文章 | 平均曝光 | 平均CTR | 平均互动 |
+|------|---------|---------|---------|---------|
+| 身 | D11(睡好两周) | 523 | 5.7% | 0.3 |
+| 智 | D7(复盘)+D12(词穷) | 235 | 4.8% | 1.0 |
+| 富 | D3(月入2万)+D8(定投) | 217 | 10.7% | 1.5 |
+| 行 | D4(通讯录)+D9(老人带娃) | 798 | 9.9% | 2.3 |
+| 心 | D5(吼完孩子) | 918 | 6.6% | 3.0 |
+
+**行维度(家庭关系)曝光最高,心维度(情绪管理)互动最高。**
+
+### 22.4 数据 vs 健康值对比
+
+| 指标 | 健康值 | 当前最佳 | 当前平均 | 差距 |
+|------|--------|---------|---------|------|
+| 曝光量(24h) | ≥1000 | 1,572(D9) | 458 | 仅D9达标 |
+| CTR | ≥8% | 14.8%(D9) | 8.0% | 5篇达标 |
+| 互动率 | ≥5% | ~0.6% | ~0.2% | ❌ 严重不足 |
+| 收藏量 | ≥50 | 3 | 1.2 | ❌ 严重不足 |
+| 评论数 | ≥15 | 4 | 0.7 | ❌ 严重不足 |
+| 涨粉 | ≥10 | 1 | 0.1 | ❌ 严重不足 |
+
+**核心问题:互动率是健康值的 1/25。** 内容被看到,但没人互动。
+
+### 22.5 改进方向
+
+| 问题 | 数据 | 改进方向 |
+|------|------|---------|
+| 观看时长短 | 平均16s | 开头3句必须制造悬念/冲突,不要铺垫 |
+| 互动率低 | 0.2% vs 5%健康值 | 评论区钩子要更具体:"来评论区告诉我,你陪了多久"→"第1天打卡的姐妹,我明天回你" |
+| 收藏率低 | 均1.2 vs 50健康值 | 内容要有"可收藏反复用"的价值,加清单/表格/模板 |
+| 涨粉为0 | 9篇仅涨1粉 | 缺少"关注理由"——每篇结尾加一句"关注我,下周继续聊" |
+
+---
+
+## 23. 本地内容 vs 线上内容核对(2026-07-30)
+
+### 23.1 核对方法
+
+通过小红书创作者中心 API 获取已发布笔记的线上标题和正文,与本地 `运营文案/小红书内容/` 中对应的草稿文件进行对比。
+
+### 23.2 已核对文章
+
+| 编号 | 线上标题 | 本地标题 | 是否一致 |
+|------|---------|---------|---------|
+| D3 | 月入2万的家庭,比想象中穷得更稳 | 购物车里躺着一个三天不会再看的东西 | ❌ 标题不同 |
+| D4 | 通讯录1493人,凌晨两点没人问 | 通讯录里1493人,凌晨两点却没人问 | ✅ 一致 |
+| D5 | 吼完孩子,试试这个情绪开关 | 吼完孩子,试试这个情绪开关 | ✅ 一致 |
+| D7 | 复盘孩子本周3个进步比报班管用100倍 | 复盘孩子本周3个进步比报班管用100倍 | ✅ 一致 |
+| D8 | 每月定投1次,比每天看余额管用100倍 | 每月定投1次比每天看余额管用 | ✅ 一致 |
+| D9 | 老人带娃容易翻脸,分清这3件事就好了 | 老人带娃容易翻脸分清这3件事就好了 | ✅ 一致 |
+| D11 | 睡好两周后脑子真的变快了 | 睡好两周后脑子真的变快了 | ✅ 一致 |
+| D12 | 词穷到会说,只差这14天 | 词穷到会说,只差这14天 | ✅ 一致 |
+
+**结论:D3 标题已更换。** 线上标题为"月入2万的家庭,比想象中穷得更稳",本地仍为"购物车里躺着一个三天不会再看的东西"——说明发布前或发布后人工修改了标题。以线上为准,更新本地文件。
+
+### 23.3 更新操作
+
+以下文件已更新为线上版本(标题+正文以线上发布为准):
+
+| 文件 | 更新项 |
+|------|--------|
+| `运营文案/小红书内容/第三篇-购物车里躺着一个不再看的东西.md` | 标题改为"月入2万的家庭,比想象中穷得更稳" |
+| `README.md` 对照表 | D3 标题更新 |
+
+## 24. 小红书→书 预热策略(2026-08-01)
+
+### 24.1 核心定位
+
+> 小红书不是终点,是书的**选题测试场**和**流量蓄水池**。
+
+每篇小红书文章 = 一个书稿章节的"市场测试"。通过数据验证哪些角度值得在书里放大,哪些角度需要放弃。
+
+### 24.2 数据验证结果(截至2026-08-01)
+
+| 书稿章节 | 对应小红书 | 曝光 | CTR | 验证结论 | 书稿调整建议 |
+|----------|-----------|------|-----|---------|-------------|
+| **第6章 行**(人际和谐) | D9 老人带娃 | **1,645** | **15.0%** | ✅ 家庭关系是最大流量入口 | 第6章增"老人带娃"专题篇幅 |
+| **第7章 心**(大爱相随) | D5 吼完孩子 | 921 | 6.6% | ✅ 情绪管理互动最高 | 第7章"情绪停机坪"详写 |
+| **第5章 富**(利他创富) | D13 存钱第一步 | 608 | 8.2% | ✅ 家庭财务有潜力 | 第5章增"记账→看见"案例 |
+| **第3章 身**(主动健康) | D11 睡好两周 | 531 | 5.7% | ⚠️ 曝光不错但观看仅6s | 开头需更抓人,书稿第3章增"睡眠"开头冲突 |
+| **第4章 智**(赋能未来) | D12 词穷到会说 | 195 | 3.1% | ❌ 词汇类关注度低 | 第4章减少"词汇量"篇幅,聚焦"学习方法" |
+| **第6章 行**(亲子沟通) | D14 对外人客气 | 定时中 | — | ⏳ 待观察 | 反常识标题已验证,书稿第6章增"期待落差"概念 |
+| **第7章 心**(爱的语言) | D15 老人带娃·爱的语言 | 草稿箱 | — | ⏳ 待观察 | 系列化方向验证中 |
+
+### 24.3 五维流量排名(为书排序)
+
+```
+🥇 行(家庭关系)→ 曝光最高,CTR最高 → 书稿第6章应加篇幅
+🥈 心(情绪管理)→ 互动最高,黏性最强 → 书稿第7章应加案例
+🥉 富(家庭财务)→ 新发即冲到第3 → 书稿第5章有潜力
+④ 身(主动健康)→ 有曝光但内容没留住人 → 书稿第3章开头需重写
+⑤ 智(学习能力)→ 关注度最低 → 书稿第4章需调整角度
+```
+
+### 24.4 书稿标题参考(来自小红书标题测试)
+
+| 小红书标题 | CTR | 可迁移到书稿章节标题的公式 |
+|-----------|-----|--------------------------|
+| 老人带娃容易翻脸,分清这3件事就好了 | 15.0% | 场景痛点 + 数字 + 解决方案 |
+| 对外人客气,是因为不在乎 | 定时中 | 反常识 + 认知冲突 |
+| 存钱第一步,不是记账,是看见 | 8.2% | 否定常识 + 新角度 |
+| 吼完孩子,试试这个情绪开关 | 6.6% | 场景 + 动作指令 |
+
+### 24.5 预热节奏建议
+
+```
+出版前6个月:
+  小红书持续发布,重点测试第6章(行)和第7章(心)的不同角度
+  每篇记录数据,建立"数据→书稿"反馈循环
+
+出版前3个月:
+  开始在小红书植入"家的算法"概念(非广告,自然融入)
+  收集高互动评论,作为书稿的读者声音案例
+
+出版时:
+  小红书发布"新书上市"预告
+  利用已积累的粉丝基础做首发转化
+```
+
+| 篇目 | 线上修改时间 | 本地最后修改 | 差异 |
+|------|------------|------------|------|
+| D3 月入2万 | 2026-07-27 | 2026-07-25 | 标题已改 |
+| 其他 | 首次发布 | 与本地一致 | 无差异 |
+
+---
+
+> **版本记录**:v10 → 2026-08-01 书稿预热策略更新:
+> - 更新 §1.1 已发布内容概览(含 D11-D15 草稿箱)
+> - 更新 §5 内容生产节奏(实际进度)
+> - 新增 §19 D11-D14 创作经验沉淀
+> - 新增 §20 反常识标题与情绪驱动(热帖分析+心理学研究)
+> - 新增 §21 内容发布计划更新(含30天路线图)
+> - **新增 §22 运营数据追踪(首次拉取,含9篇曝光/CTR/互动数据)**
+> - **§22.0 核心问题改善方案(4个具体改善项:冲突开头/评论区钩子/可收藏段落/关注理由)**
+> - **新增 §23 本地 vs 线上内容核对(D3 标题已修改,以线上为准)**
+> - **新增 §24 小红书→书 预热策略(含数据验证结果、五维流量排名、书稿标题参考)**
+> - **更新 §18 书稿参考(双向使用:书→小红书素材,小红书→书数据反馈)**
+
 
 
 

+ 0 - 0
运营文案/小红书内容/第三篇-购物车里躺着一个不再看的东西.md → 运营文案/小红书内容/第三篇-月入2万的家庭比想象中穷得更稳.md