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

Merge remote-tracking branch 'origin/cfclub' into cfclub

# Conflicts:
#	cfc-frontend/pages/health/gut-flora-detail.vue
E2E Test Bot 2 месяцев назад
Родитель
Сommit
73d213462c

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java

@@ -543,6 +543,21 @@ public class HealthReportService {
             // 通用展示块(blocks 为空时前端回退旧字段)
             detail.put("blocks", reportBlockService.getBlocks(report.getReportType(), reportId));
 
+            // 报告解读(前端 gut-flora-detail 读取 reportInterpretation.gutAgeAssessment)
+            Map<String, Object> interp = new HashMap<>();
+            StringBuilder sb = new StringBuilder();
+            if (report.getGutAge() != null && !report.getGutAge().isEmpty()) {
+                sb.append("您的肠道年龄为 ").append(report.getGutAge()).append(" 岁");
+            }
+            if (report.getGutType() != null && !report.getGutType().isEmpty()) {
+                if (sb.length() > 0) sb.append(",");
+                sb.append("肠道类型为 ").append(report.getGutType());
+            }
+            if (sb.length() > 0) {
+                interp.put("gutAgeAssessment", sb.toString());
+                detail.put("reportInterpretation", interp);
+            }
+
             // 编辑回填: payload_json 快照 → 嵌套 payload 结构
             if (report.getPayloadJson() != null && !report.getPayloadJson().isEmpty()) {
                 try {

+ 60 - 26
cfc-frontend/pages/health/gut-flora-detail.vue

@@ -10,12 +10,16 @@
           <text class="meta-item" v-if="report.age">{{ report.age }}岁</text>
         </view>
         <view class="report-meta">
-          <text class="meta-item" v-if="report.reportDate">日期: {{ report.reportDate }}</text>
+          <text class="meta-item" v-if="report.reportDate">日期: {{ reportDateText }}</text>
           <text class="meta-item" v-if="report.reportNumber">编号: {{ report.reportNumber }}</text>
         </view>
-        <view class="badge-row" v-if="report.gutAge || report.gutType">
+        <view class="badge-row" v-if="report.gutAge || report.gutType || report.shannonIndex">
           <text class="badge" v-if="report.gutAge">肠龄: {{ report.gutAge }}</text>
           <text class="badge" v-if="report.gutType">肠型: {{ report.gutType }}</text>
+          <text class="badge" v-if="report.shannonIndex">多样性指数: {{ report.shannonIndex }}</text>
+        </view>
+        <view class="rating-line" v-if="report.healthRating">
+          <text class="rating-text">综合评级: {{ report.healthRating }}</text>
         </view>
         <!-- 报告解读 -->
         <view class="interpretation-box" v-if="reportInterpretation && reportInterpretation.gutAgeAssessment">
@@ -133,16 +137,18 @@
           <text class="accordion-arrow">{{ accordionOpen.gut ? '收起' : '展开' }}</text>
         </view>
         <view class="accordion-body" v-if="accordionOpen.gut">
-          <view class="indicator-card" v-for="(item, idx) in gutIndicators" :key="idx">
-            <view class="indicator-row">
-              <text class="indicator-name" @tap="showKnowledge(indicatorType(item), item.indicatorName)">
-                {{ item.indicatorName }}
-                <text class="arrow-up" v-if="item.status === '偏高' || item.status === '过多'"> ↑</text>
-                <text class="arrow-down" v-else-if="item.status === '偏低' || item.status === '缺乏' || item.status === '不足'"> ↓</text>
-                <text class="arrow-warn" v-else-if="item.status === '异常'"> ⚠</text>
-              </text>
-              <view class="indicator-value" v-if="!isEditing">
+            <view class="indicator-card" v-for="(item, idx) in gutIndicators" :key="idx">
+              <view class="indicator-row">
+<text class="ind-help-btn" @tap.stop="showKnowledge(indicatorType(item), item.indicatorName)">?</text>
+                <text class="indicator-name" @tap="showKnowledge(indicatorType(item), item.indicatorName)">
+                  {{ item.indicatorName }}
+                  <text class="arrow-up" v-if="item.status === '偏高' || item.status === '过多'"> ↑</text>
+                  <text class="arrow-down" v-else-if="item.status === '偏低' || item.status === '缺乏' || item.status === '不足'"> ↓</text>
+                  <text class="arrow-warn" v-else-if="item.status === '异常'"> ⚠</text>
+                </text>
+                <view class="indicator-value" v-if="!isEditing">
                 <text :class="'val-text val-' + (item._cssClass || 'normal')">{{ item.indicatorValue || '--' }}</text>
+                <text class="indicator-unit" v-if="item.unit">{{ item.unit }}</text>
               </view>
               <view class="indicator-edit" v-if="isEditing">
                 <input class="edit-input" type="text" v-model="item.indicatorValue" />
@@ -152,6 +158,7 @@
               </view>
             </view>
             <text class="indicator-range" v-if="item.refRange">参考: {{ item.refRange }}</text>
+            <text class="indicator-symptoms" v-if="item.symptoms">症状: {{ item.symptoms }}</text>
           </view>
           <view class="empty-hint" v-if="gutIndicators.length === 0">暂无数据</view>
         </view>
@@ -167,16 +174,18 @@
         <view class="accordion-body" v-if="accordionOpen.nutri">
           <view class="sub-group" v-for="(group, gIdx) in nutritionGroups" :key="gIdx">
             <text class="sub-group-title">{{ group.category }}</text>
-            <view class="indicator-card" v-for="(item, idx) in group.items" :key="idx">
-              <view class="indicator-row">
-                <text class="indicator-name" @tap="showKnowledge(indicatorType(item), item.indicatorName)">
-                  {{ item.indicatorName }}
-                  <text class="arrow-up" v-if="item.status === '偏高' || item.status === '过多'"> ↑</text>
-                  <text class="arrow-down" v-else-if="item.status === '偏低' || item.status === '缺乏' || item.status === '不足'"> ↓</text>
-                  <text class="arrow-warn" v-else-if="item.status === '异常'"> ⚠</text>
-                </text>
-                <view class="indicator-value" v-if="!isEditing">
+              <view class="indicator-card" v-for="(item, idx) in group.items" :key="idx">
+                <view class="indicator-row">
+<text class="ind-help-btn" @tap.stop="showKnowledge(indicatorType(item), item.indicatorName)">?</text>
+                  <text class="indicator-name" @tap="showKnowledge(indicatorType(item), item.indicatorName)">
+                    {{ item.indicatorName }}
+                    <text class="arrow-up" v-if="item.status === '偏高' || item.status === '过多'"> ↑</text>
+                    <text class="arrow-down" v-else-if="item.status === '偏低' || item.status === '缺乏' || item.status === '不足'"> ↓</text>
+                    <text class="arrow-warn" v-else-if="item.status === '异常'"> ⚠</text>
+                  </text>
+                  <view class="indicator-value" v-if="!isEditing">
                   <text :class="'val-text val-' + (item._cssClass || 'normal')">{{ item.indicatorValue || '--' }}</text>
+                  <text class="indicator-unit" v-if="item.unit">{{ item.unit }}</text>
                 </view>
                 <view class="indicator-edit" v-if="isEditing">
                   <input class="edit-input" type="text" v-model="item.indicatorValue" />
@@ -185,7 +194,8 @@
                   </picker>
                 </view>
               </view>
-              <text class="indicator-range" v-if="item.refRange">参考: {{ item.refRange }}</text>
+<text class="indicator-range" v-if="item.refRange">参考: {{ item.refRange }}</text>
+            <text class="indicator-symptoms" v-if="item.symptoms">症状: {{ item.symptoms }}</text>
             </view>
           </view>
           <view class="empty-hint" v-if="nutritionGroups.length === 0">暂无数据</view>
@@ -233,6 +243,7 @@
 import { getReportDetail, editHealthReport, queryKnowledgeBase, queryIndicatorKnowledge, getGutFloraAnalysis, confirmReportPreview, discardReportDraft, parseReportDraft, getHealthFoods, getFoodCautionList } from '../../utils/api.js'
 import HealthKnowledgePopup from '../../components/health-knowledge-popup.vue'
 import FeedbackPopup from '../../components/feedback-popup.vue'
+import { parseDate } from '../../utils/format.js'
 
 var INDICATOR_TYPE_MAP = {
   '维生素': 'vitamin',
@@ -317,6 +328,17 @@ export default {
   },
   methods: {
     goBack: function() { uni.navigateBack() },
+    /** 报告日期仅显示 yyyy-MM-dd(去掉时间部分) */
+    reportDateText: function() {
+      var d = this.report.reportDate
+      if (!d) return ''
+      var date = parseDate(d)
+      if (!date) return String(d).substring(0, 10)
+      var y = date.getFullYear()
+      var m = ('' + (date.getMonth() + 1)).padStart(2, '0')
+      var day = ('' + date.getDate()).padStart(2, '0')
+      return y + '-' + m + '-' + day
+    },
     /** 状态 → CSS安全英文(兼容中英文,与 report-confirm 保持一致) */
     _cssForStatus: function(s) {
       var k = String(s || '').trim()
@@ -415,8 +437,9 @@ export default {
         report.personName = s.personName || ''
         report.reportNumber = s.reportNumber || ''
         report.gutAge = s.gutAge || ''
-        report.gutType = s.gutType || ''
-        report.overallScore = s.overallScore
+report.gutType = s.gutType || ''
+      report.shannonIndex = s.shannonIndex || ''
+      report.overallScore = s.overallScore
         report.gutHealthScore = s.gutHealthScore
         report.chronicDiseaseScore = s.chronicDiseaseScore
         report.nutritionScore = s.nutritionScore
@@ -732,14 +755,25 @@ export default {
 .val-text { font-size: 28rpx; font-weight: 600; color: #333; }
 .val-high { color: #C62828; }
 .val-excess { color: #C62828; }
-.val-low { color: #E65100; }
-.val-deficient { color: #E65100; }
-.val-insufficient { color: #E65100; }
+.val-low { color: #C62828; }
+.val-deficient { color: #C62828; }
+.val-insufficient { color: #C62828; }
 .val-abnormal { color: #C62828; }
 .val-warning { color: #F59E0B; }
 
 .indicator-range { font-size: 22rpx; color: #bbb; display: block; margin-top: 4rpx; }
 
+/* 指标知识库 "?" 按钮(与通用报告页一致) */
+.ind-help-btn { display: inline-flex; align-items: center; justify-content: center; width: 36rpx; height: 36rpx; border-radius: 50%; background: #E3F2FD; color: #1976D2; font-size: 22rpx; font-weight: 600; margin-left: 8rpx; flex-shrink: 0; }
+
+/* 综合评级行 */
+.rating-line { margin: 12rpx 0 0; }
+.rating-text { font-size: 24rpx; color: #666; display: block; }
+
+/* 指标单位与症状 */
+.indicator-unit { font-size: 22rpx; color: #999; margin-left: 6rpx; }
+.indicator-symptoms { font-size: 22rpx; color: #E65100; display: block; margin-top: 4rpx; }
+
 .indicator-edit { display: flex; gap: 8rpx; align-items: center; }
 .edit-input { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 6rpx 10rpx; font-size: 26rpx; width: 100rpx; }
 .edit-picker { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 6rpx 10rpx; font-size: 24rpx; color: #333; }

+ 3 - 3
cfc-frontend/pages/health/gut-flora-species-detail.vue

@@ -241,9 +241,9 @@ export default {
 .flora-name { font-size: 28rpx; font-weight: 600; color: #333; flex: 1; }
 .flora-name.status-high { color: #C62828; }
 .flora-name.status-excess { color: #C62828; }
-.flora-name.status-low { color: #E65100; }
-.flora-name.status-deficient { color: #E65100; }
-.flora-name.status-insufficient { color: #E65100; }
+.flora-name.status-low { color: #C62828; }
+.flora-name.status-deficient { color: #C62828; }
+.flora-name.status-insufficient { color: #C62828; }
 .arrow-up { color: #C62828; font-size: 22rpx; }
 .arrow-down { color: #E65100; font-size: 22rpx; }
 .flora-level-tag { padding: 2rpx 12rpx; background: #E3F2FD; border-radius: 16rpx; font-size: 20rpx; color: #4A9BD7; }

+ 36 - 30
cfc-frontend/pages/health/report-list.vue

@@ -27,26 +27,25 @@
 
     <!-- 报告列表 -->
     <view class="report-list" v-else-if="reports.length > 0">
-      <view
-        class="report-card"
-        v-for="item in reports"
-        :key="item.id"
-        @click="goToDetail(item.id, item.reportType)"
-      >
-        <view class="report-card-left">
-          <view class="report-type-badge" :class="'badge-' + (item.reportType === 'gut_flora' ? 'gut' : item.reportType === 'tongue' ? 'tongue' : 'exam')">
-            <text>{{ item.reportType === 'gut_flora' ? '肠道检测' : item.reportType === 'tongue' ? '舌诊' : '体检报告' }}</text>
+      <swipe-action v-for="item in reports" :key="item.id">
+        <view class="report-card" @click="goToDetail(item.id, item.reportType)">
+          <view class="report-card-left">
+            <view class="report-type-badge" :class="'badge-' + (item.reportType === 'gut_flora' ? 'gut' : item.reportType === 'tongue' ? 'tongue' : 'exam')">
+              <text>{{ item.reportType === 'gut_flora' ? '肠道检测' : item.reportType === 'tongue' ? '舌诊' : '体检报告' }}</text>
+            </view>
+            <text class="report-person" v-if="item.personName">{{ item.personName }}</text>
+            <text class="report-date">报告日期:{{ formatDate(item.reportDate) }}</text>
+            <text class="report-upload-date">上传日期:{{ formatDate(item.createdAt) }}</text>
+          </view>
+          <view class="report-card-right">
+            <text class="report-score">{{ item.overallScore || '--' }}<text class="score-unit">分</text></text>
+            <text class="report-arrow">›</text>
           </view>
-          <text class="report-person" v-if="item.personName">{{ item.personName }}</text>
-          <text class="report-date">报告日期:{{ formatDate(item.reportDate) }}</text>
-          <text class="report-upload-date">上传日期:{{ formatDate(item.createdAt) }}</text>
-        </view>
-        <view class="report-card-right">
-          <text class="report-score">{{ item.overallScore || '--' }}<text class="score-unit">分</text></text>
-          <view class="report-del" @click.stop="confirmDelete(item)"><text class="report-del-text">删除</text></view>
-          <text class="report-arrow">›</text>
         </view>
-      </view>
+        <template #right>
+          <button class="swipe-delete" @click.stop="confirmDelete(item)">删除</button>
+        </template>
+      </swipe-action>
     </view>
 
     <!-- 空状态 -->
@@ -297,18 +296,6 @@ export default {
   align-items: center;
   gap: 12rpx;
 }
-.report-del {
-  padding: 8rpx 20rpx;
-  border: 1rpx solid #F56C6C;
-  border-radius: 24rpx;
-}
-.report-del:active {
-  background: #FEF0F0;
-}
-.report-del-text {
-  font-size: 22rpx;
-  color: #F56C6C;
-}
 .report-score {
   font-size: 36rpx;
   font-weight: bold;
@@ -359,6 +346,25 @@ export default {
   opacity: 0.8;
 }
 
+/* ===== 滑动删除 ===== */
+.swipe-delete {
+  height: 100%;
+  width: 160rpx;
+  background: #F56C6C;
+  color: #fff;
+  font-size: 28rpx;
+  border: none;
+  border-radius: 0;
+}
+
+/* swipe-action 组件内部样式覆盖 */
+/deep/ .uni-swipe-action__content {
+  border-radius: 16rpx;
+}
+/deep/ .uni-swipe-action__button {
+  border-radius: 0 16rpx 16rpx 0;
+}
+
 /* ===== 底部 ===== */
 .bottom-spacer {
   height: 60rpx;

+ 17 - 4
cfc-frontend/pages/index-home/index.vue

@@ -1203,12 +1203,25 @@ export default {
         })
       }, 500)
     },
-    getImageUrl: function(path) {
+getImageUrl: function(path) {
       return config.API_BASE_URL + path
     },
-
-    
-
+    /** 转发给朋友/群 */
+    onShareAppMessage: function() {
+      return {
+        title: '浠艾福 · 家庭五维幸福成长',
+        path: '/pages/index-home/index',
+        imageUrl: '/static/share-cover.png'
+      }
+    },
+    /** 分享到朋友圈 */
+    onShareTimeline: function() {
+      return {
+        title: '浠艾福 · 家庭五维幸福成长',
+        query: '',
+        imageUrl: '/static/share-cover.png'
+      }
+    }
   }
 }
 </script>

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-905e0d80e386d6cad9da31ddcd78f606ddb1edb4
+72b1036290612581de4887424675c8364639d9a0

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

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1255",
+  "version": "1.0.1258",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1255",
+      "version": "1.0.1258",
       "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.1256",
+  "version": "1.0.1259",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

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

@@ -4,6 +4,59 @@
 
 ---
 
+## v1.0.1259 (2026-08-30)
+
+### 新功能
+- 肠道菌群报告/健康报告列表/首页优化
+
+### 其他
+-   - 报告日期仅显示 yyyy-MM-dd,去掉时间部分
+-   - 指标名称前添加 '?' 知识库按钮,点击弹出说明
+-   - 补全缺失数据:shannonIndex 徽章、healthRating 评级行、指标单位 unit、症状 symptoms
+-   - 修复报告解读框死代码:后端返回 reportInterpretation.gutAgeAssessment
+- - 健康报告列表 (report-list.vue):
+-   - 滑动删除功能修复:移除组件层 @click 冲突,仅用 #right slot 实现左滑出现删除
+-   - 点击卡片正常跳转详情
+- - 首页 (index-home/index.vue):
+-   - 新增 onShareAppMessage/onShareTimeline 分享配置
+- - 后端 HealthReportService:
+-   - getReportDetail 新增 reportInterpretation 字段,基于 gutAge/gutType 生成评估文案
+- 
+
+
+## v1.0.1258 (2026-08-30)
+
+### Bug 修复
+- 菌群报告异常指标统一红色显示
+
+### 其他
+- 
+
+
+## v1.0.1257 (2026-08-30)
+
+### 新功能
+- 用户管理列表新增会员级别和推荐人列,编辑支持修改推荐人
+
+### 其他
+- - 列表新增 referrerName 列(通过searchUsers解析referrerId显示推荐人名称)
+- - 编辑弹窗新增推荐人下拉选择(远程搜索昵称/手机号/ID)
+- - 新增 resolveReferrerNames 批量解析方法,loadUsers后自动调用
+- 
+-   防止编辑旧格式报告(菌X构成/中文level)时原样写回脏数据
+- - getReportDetail: 读回时加 normalizeGutFloraRead 后处理,
+-   一次性兼容归一化前入库的历史数据(无需迁移脚本)
+- - normalizeFloraLevel 的 category 参数保持传入以维持语义完整性
+- 至此方案A覆盖完整主链路:
+-   上传/确认入库(write)✅
+-   编辑保存(write)✅
+-   再次查看展示(read)✅
+- 
+
+### Bug 修复
+- 修复编辑路径与读路径的菌群分类归一化缺口
+
+
 ## v1.0.1256 (2026-08-30)
 
 ### Bug 修复

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

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1256
+> 当前版本: v1.0.1259
 
 ## 历史版本
 
@@ -8,6 +8,59 @@
 
 ---
 
+## v1.0.1259 (2026-08-30)
+
+### 新功能
+- 肠道菌群报告/健康报告列表/首页优化
+
+### 其他
+-   - 报告日期仅显示 yyyy-MM-dd,去掉时间部分
+-   - 指标名称前添加 '?' 知识库按钮,点击弹出说明
+-   - 补全缺失数据:shannonIndex 徽章、healthRating 评级行、指标单位 unit、症状 symptoms
+-   - 修复报告解读框死代码:后端返回 reportInterpretation.gutAgeAssessment
+- - 健康报告列表 (report-list.vue):
+-   - 滑动删除功能修复:移除组件层 @click 冲突,仅用 #right slot 实现左滑出现删除
+-   - 点击卡片正常跳转详情
+- - 首页 (index-home/index.vue):
+-   - 新增 onShareAppMessage/onShareTimeline 分享配置
+- - 后端 HealthReportService:
+-   - getReportDetail 新增 reportInterpretation 字段,基于 gutAge/gutType 生成评估文案
+- 
+
+
+## v1.0.1258 (2026-08-30)
+
+### Bug 修复
+- 菌群报告异常指标统一红色显示
+
+### 其他
+- 
+
+
+## v1.0.1257 (2026-08-30)
+
+### 新功能
+- 用户管理列表新增会员级别和推荐人列,编辑支持修改推荐人
+
+### 其他
+- - 列表新增 referrerName 列(通过searchUsers解析referrerId显示推荐人名称)
+- - 编辑弹窗新增推荐人下拉选择(远程搜索昵称/手机号/ID)
+- - 新增 resolveReferrerNames 批量解析方法,loadUsers后自动调用
+- 
+-   防止编辑旧格式报告(菌X构成/中文level)时原样写回脏数据
+- - getReportDetail: 读回时加 normalizeGutFloraRead 后处理,
+-   一次性兼容归一化前入库的历史数据(无需迁移脚本)
+- - normalizeFloraLevel 的 category 参数保持传入以维持语义完整性
+- 至此方案A覆盖完整主链路:
+-   上传/确认入库(write)✅
+-   编辑保存(write)✅
+-   再次查看展示(read)✅
+- 
+
+### Bug 修复
+- 修复编辑路径与读路径的菌群分类归一化缺口
+
+
 ## v1.0.1256 (2026-08-30)
 
 ### Bug 修复

+ 90 - 7
cfc-web/src/views/Users.vue

@@ -93,6 +93,14 @@
           </template>
         </el-table-column>
         <el-table-column prop="familyId" label="家庭ID" width="100"></el-table-column>
+        <el-table-column label="会员级别" width="100">
+          <template slot-scope="{ row }">
+            <el-tag :type="memberLevelType(row.memberLevel)" size="small">{{ memberLevelLabel(row.memberLevel) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="推荐人" width="140">
+          <template slot-scope="{ row }">{{ row.referrerName || '—' }}</template>
+        </el-table-column>
         <el-table-column prop="createdAt" label="创建时间" width="180">
           <template slot-scope="scope">
             {{ formatDate(scope.row.createdAt) }}
@@ -146,6 +154,26 @@
         <el-form-item label="手机号" prop="phone">
           <el-input v-model="editForm.phone" placeholder="请输入手机号"></el-input>
         </el-form-item>
+        <el-form-item label="推荐人">
+          <el-select
+            v-model="editForm.referrerId"
+            filterable
+            remote
+            reserve-keyword
+            clearable
+            placeholder="搜索推荐人(昵称/手机号/ID)"
+            :remote-method="searchUsersForReferrer"
+            :loading="userSearchLoading"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="u in userSearchResults"
+              :key="u.id"
+              :label="(u.nickname || u.realName || '') + ' (ID:' + u.id + ')'"
+              :value="u.id"
+            ></el-option>
+          </el-select>
+        </el-form-item>
         <el-form-item label="角色">
           <el-checkbox-group v-model="editForm.roles">
             <el-checkbox
@@ -200,7 +228,7 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
-import { getUserList, updateUser, deleteUser, resetUserPassword } from '@/api/admin'
+import { getUserList, updateUser, deleteUser, resetUserPassword, searchUsers } from '@/api/admin'
 
 import { getRoleLabel } from '@/utils/permissions'
 
@@ -241,7 +269,8 @@ export default {
         nickname: '',
         realName: '',
         phone: '',
-        roles: []
+        roles: [],
+        referrerId: null
       },
       editRules: {
         nickname: [
@@ -258,7 +287,9 @@ export default {
         { value: 'nutritionist', label: '营养师', disabled: false },
         { value: 'supplier', label: '供应商', disabled: false },
         { value: 'activity_manager', label: '活动管理员', disabled: false },
-      ]
+      ],
+      userSearchResults: [],
+      userSearchLoading: false,
     }
   },
   mounted() {
@@ -286,9 +317,10 @@ export default {
         }
         if (this.sortState.length > 0) params.sort = this.sortState
 
-        const res = await getUserList(params)
-        this.tableData = res.data.records || res.data || []
-        this.pagination.total = res.data.total || this.tableData.length
+const res = await getUserList(params)
+          this.tableData = res.data.records || res.data || []
+          this.pagination.total = res.data.total || this.tableData.length
+          await this.resolveReferrerNames(this.tableData)
       } catch (error) {
         this.$message.error(error.message || '加载用户列表失败')
       } finally {
@@ -325,7 +357,8 @@ export default {
         nickname: row.nickname,
         realName: row.realName || '',
         phone: row.phone || '',
-        roles: row.roles ? [...row.roles] : (row.role ? [row.role] : [])
+        roles: row.roles ? [...row.roles] : (row.role ? [row.role] : []),
+        referrerId: row.referrerId || null
       }
       this.editDialogVisible = true
     },
@@ -412,6 +445,56 @@ export default {
     formatDate(dateStr) {
       if (!dateStr) return '-'
       return String(dateStr).replace('T', ' ').substring(0, 19)
+    },
+    memberLevelLabel(level) {
+      const map = { FREE: '免费', FAMILY: '家庭会员', PROVIDER: '服务商' }
+      return map[level] || '免费'
+    },
+    memberLevelType(level) {
+      const map = { FREE: 'info', FAMILY: 'success', PROVIDER: 'warning' }
+      return map[level] || 'info'
+    },
+    async searchUsersForReferrer(query) {
+      if (!query) {
+        this.userSearchResults = []
+        return
+      }
+      this.userSearchLoading = true
+      try {
+        const res = await searchUsers({ keyword: query })
+        this.userSearchResults = res.data || []
+      } catch (e) {
+        this.userSearchResults = []
+      } finally {
+        this.userSearchLoading = false
+      }
+    },
+    resolveReferrerNames(items) {
+      if (!items || items.length === 0) return Promise.resolve()
+      var ids = []
+      items.forEach(function(item) {
+        if (item.referrerId && ids.indexOf(item.referrerId) === -1) ids.push(item.referrerId)
+      })
+      if (ids.length === 0) return Promise.resolve()
+      var nameMap = {}
+      return Promise.all(ids.map(function(id) {
+        return searchUsers({ keyword: String(id) })
+          .then(function(res) {
+            if (res.data && res.data.length > 0) {
+              var u = res.data[0]
+              nameMap[id] = u.nickname || u.realName || '用户' + id
+            } else {
+              nameMap[id] = '用户' + id
+            }
+          })
+          .catch(function() {
+            nameMap[id] = '用户' + id
+          })
+      })).then(function() {
+        items.forEach(function(item) {
+          item.referrerName = nameMap[item.referrerId] || ''
+        })
+      })
     }
   }
 }

+ 543 - 0
docs/superpowers/plans/2026-08-30-article-reading-tracking-enhancement.md

@@ -0,0 +1,543 @@
+# 文章阅读追踪增强 实现计划
+
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
+
+**目标:** 完善文章阅读追踪——前端列表/详情页显示阅读数量,匿名用户不显示阅读时长;后端新增 admin 接口按文章查询阅读记录(含昵称和时长),供后台界面展示。
+
+**架构:** 现有 `article_reading_records` 表已有 `article_id` 和累加逻辑,本次补充:1) `reportReadingTime` 首次上报时递增 `articles.view_count`(覆盖匿名/登录用户);2) 新增 `POST /api/admin/articles/reading-records` 返回每篇文章的阅读用户明细(含昵称);3) 前端列表/详情页补上阅读数显示;4) 后台弹窗展示阅读记录查询界面。
+
+**技术栈:** Spring Boot 2.7 + MyBatis-Plus / uni-app Vue 2 小程序 / Vue 2 + Element UI 管理端
+
+**设计文档:** `docs/superpowers/specs/2026-07-28-article-reading-time-tracking-design.md`
+
+## 文件变更清单
+
+| 文件 | 变更类型 | 说明 |
+|------|---------|------|
+| `cfc-backend/.../service/ArticleService.java` | 修改 | `reportReadingTime` 首次上报时递增 `view_count`;新增 `getArticleReadingRecords` |
+| `cfc-backend/.../controller/admin/AdminArticleController.java` | 修改 | 新增 `POST /api/admin/articles/reading-records` |
+| `cfc-backend/.../mapper/ArticleReadingRecordMapper.java` | 修改 | 新增 `selectByArticle` 查询方法 |
+| `docs/superpowers/api/API_REFERENCE.md` | 修改 | 新增 `POST /api/admin/articles/reading-records` 接口文档 |
+| `cfc-web/src/api/article.js` | 修改 | 新增 `adminArticleReadingRecords` 函数 |
+| `cfc-web/src/views/admin/ArticleManage.vue` | 修改 | 每行加"阅读记录"按钮 + 阅读记录弹窗 |
+| `cfc-frontend/pages/article-center/index.vue` | 修改 | 列表卡片底部显示阅读数 |
+| `cfc-frontend/pages/article-center/article-detail.vue` | 修改 | 详情页 meta 显示阅读数;匿名用户不启动计时器 |
+
+---
+
+### Task 1: 后端 — `reportReadingTime` 首次上报时递增 `view_count`
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java` (第 318-345 行)
+
+- [ ] **步骤 1:修改 `reportReadingTime` 方法,在 insert 路径递增 view_count**
+
+在 `ArticleService.reportReadingTime` 方法中,当创建新记录(else 分支)时,调用 `articleMapper` 递增 `view_count`:
+
+```java
+public void reportReadingTime(Long articleId, Long userId, Long memberId, int durationSeconds) {
+    if (memberId == null) return;
+
+    // 查找当日已有记录(同文章+同孩子)
+    LambdaQueryWrapper<ArticleReadingRecord> wrapper = new LambdaQueryWrapper<ArticleReadingRecord>()
+            .eq(ArticleReadingRecord::getArticleId, articleId)
+            .eq(ArticleReadingRecord::getChildId, memberId)
+            .apply("DATE(read_at) = CURDATE()")
+            .last("LIMIT 1");
+    ArticleReadingRecord existing = articleReadingRecordMapper.selectOne(wrapper);
+
+    if (existing != null) {
+        existing.setDurationSeconds(existing.getDurationSeconds() + durationSeconds);
+        existing.setUpdatedAt(new Date());
+        articleReadingRecordMapper.updateById(existing);
+    } else {
+        ArticleReadingRecord record = new ArticleReadingRecord();
+        record.setArticleId(articleId);
+        record.setUserId(userId);
+        record.setChildId(memberId);
+        record.setDurationSeconds(durationSeconds);
+        record.setReadAt(new Date());
+        record.setCreatedAt(new Date());
+        articleReadingRecordMapper.insert(record);
+        // 首次阅读:递增文章 view_count(匿名或登录用户均计)
+        articleMapper.update(null, Wrappers.<Article>lambdaUpdate()
+                .eq(Article::getId, articleId)
+                .setSql("view_count = view_count + 1"));
+    }
+}
+```
+
+注意:`articleMapper` 已通过 `@Resource` 注入,`Wrappers` 已通过 `import com.baomidou.mybatisplus.core.toolkit.Wrappers` 引入(ArticleService 已有该 import)。
+
+- [ ] **步骤 2:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+
+预期输出:`BUILD SUCCESS`
+
+- [ ] **步骤 3:Commit**
+
+```bash
+cd /sc-data/cfc && git add cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java && \
+  git commit -m "feat(article): reportReadingTime 首次上报时递增 view_count"
+```
+
+---
+
+### Task 2: 后端 — 新增文章阅读记录查询 API + 接口文档
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/mapper/ArticleReadingRecordMapper.java`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java`
+- 修改:`docs/superpowers/api/API_REFERENCE.md`
+
+- [ ] **步骤 1:新增 Mapper 查询方法**
+
+在 `ArticleReadingRecordMapper.java` 末尾新增方法(替换现有内容):
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ArticleReadingRecord;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.ResultMap;
+import org.apache.ibatis.annotations.Select;
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface ArticleReadingRecordMapper extends BaseMapper<ArticleReadingRecord> {
+
+    @Select("SELECT COALESCE(SUM(duration_seconds), 0) FROM article_reading_records " +
+            "WHERE child_id = #{childId} AND read_at >= #{since}")
+    Integer selectTotalDurationByChildSince(@Param("childId") Long childId,
+                                            @Param("since") java.util.Date since);
+
+    /**
+     * 按文章 ID 分页查询阅读记录,JOIN family_members 获取昵称,按最后阅读时间倒序
+     */
+    @Select("SELECT " +
+            "  r.id, " +
+            "  r.user_id, " +
+            "  r.child_id, " +
+            "  r.duration_seconds, " +
+            "  r.read_at, " +
+            "  r.updated_at, " +
+            "  m.nickname, " +
+            "  DATE(r.read_at) AS read_date " +
+            "FROM article_reading_records r " +
+            "LEFT JOIN family_members m ON r.child_id = m.id " +
+            "WHERE r.article_id = #{articleId} " +
+            "ORDER BY r.updated_at DESC, r.read_at DESC " +
+            "LIMIT #{limit} OFFSET #{offset}")
+    @ResultMap("readRecordResultMap")
+    List<Map<String, Object>> selectByArticle(
+            @Param("articleId") Long articleId,
+            @Param("offset") int offset,
+            @Param("limit") int limit);
+
+    /**
+     * 统计某文章总阅读记录数(用于分页)
+     */
+    @Select("SELECT COUNT(*) FROM article_reading_records WHERE article_id = #{articleId}")
+    long countByArticle(@Param("articleId") Long articleId);
+}
+```
+
+- [ ] **步骤 2:新增 Service 方法**
+
+在 `ArticleService.java` 中新增方法(放在 `reportReadingTime` 方法之后):
+
+```java
+/**
+ * 按文章 ID 分页查询阅读记录,返回用户昵称、时长、最后阅读时间
+ */
+public Page<Map<String, Object>> getArticleReadingRecords(Long articleId, int page, int size) {
+    long total = articleReadingRecordMapper.countByArticle(articleId);
+    if (total == 0) {
+        Page<Map<String, Object>> empty = new Page<>(page, size);
+        empty.setTotal(0);
+        empty.setRecords(Collections.emptyList());
+        return empty;
+    }
+    int offset = (page - 1) * size;
+    List<Map<String, Object>> records = articleReadingRecordMapper.selectByArticle(articleId, offset, size);
+    Page<Map<String, Object>> result = new Page<>(page, size);
+    result.setTotal(total);
+    result.setRecords(records);
+    return result;
+}
+```
+
+注意:`Page` 来自 `com.baomidou.mybatisplus.extension.plugins.pagination.Page`,`Collections` 来自 `java.util.Collections`,均已可复用。
+
+- [ ] **步骤 3:新增 Admin Controller 端点**
+
+在 `AdminArticleController.java` 末尾的 `categoryDelete` 方法之前插入:
+
+```java
+    @PostMapping("/reading-records")
+    public Result<Page<Map<String, Object>>> readingRecords(
+            @RequestBody Map<String, Object> body) {
+        Long articleId = ParamUtils.getLong(body.get("articleId"));
+        if (articleId == null) return Result.error("articleId不能为空");
+        int page = body.get("page") != null ? Integer.parseInt(body.get("page").toString()) : 1;
+        int size = body.get("size") != null ? Integer.parseInt(body.get("size").toString()) : 20;
+        return Result.success(articleService.getArticleReadingRecords(articleId, page, size));
+    }
+```
+
+- [ ] **步骤 4:更新 API_REFERENCE.md**
+
+在 `docs/superpowers/api/API_REFERENCE.md` 的"📰 文章管理 API"章节(搜索 `## 📰 文章管理 API` 或类似标题)末尾追加:
+
+```markdown
+### 文章阅读记录
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| POST | `/api/admin/articles/reading-records` | 按文章查询阅读记录明细 |
+
+**请求体:**
+```json
+{ "articleId": 123, "page": 1, "size": 20 }
+```
+
+**响应 data 字段(分页):**
+```json
+{
+  "total": 5,
+  "records": [
+    {
+      "id": 10,
+      "user_id": 42,
+      "child_id": 88,
+      "nickname": "小明",
+      "duration_seconds": 450,
+      "read_at": "2026-08-30T10:30:00",
+      "updated_at": "2026-08-30T10:30:00",
+      "read_date": "2026-08-30"
+    }
+  ]
+}
+```
+```
+
+- [ ] **步骤 5:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+
+预期:`BUILD SUCCESS`
+
+- [ ] **步骤 6:Commit**
+
+```bash
+cd /sc-data/cfc && git add \
+  cfc-backend/src/main/java/com/etotem/cfc/mapper/ArticleReadingRecordMapper.java \
+  cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java \
+  cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java \
+  docs/superpowers/api/API_REFERENCE.md && \
+  git commit -m "feat(article): 新增文章阅读记录 admin API"
+```
+
+---
+
+### Task 3: cfc-web 管理端 — 阅读记录弹窗
+
+**文件:**
+- 修改:`cfc-web/src/api/article.js`
+- 修改:`cfc-web/src/views/admin/ArticleManage.vue`
+
+- [ ] **步骤 1:新增 API 封装函数**
+
+在 `cfc-web/src/api/article.js` 末尾追加:
+
+```javascript
+// ===== 文章阅读记录 =====
+export function adminArticleReadingRecords(data) {
+  return request({ url: '/api/admin/articles/reading-records', method: 'post', data })
+}
+```
+
+- [ ] **步骤 2:修改 ArticleManage.vue**
+
+**a) data 中新增状态变量**(`rejectDialogVisible` 之后):
+
+```javascript
+      rejectDialogVisible: false,
+      rejectReason: '',
+      currentAuditRow: null,
+      // 阅读记录弹窗
+      readRecordVisible: false,
+      readRecordArticleId: null,
+      readRecordList: [],
+      readRecordTotal: 0,
+      readRecordPage: 1,
+      readRecordSize: 20,
+      readRecordLoading: false
+```
+
+**b) template:在"操作"列的操作按钮之后、`rejectDialog` 之前新增阅读记录弹窗**
+
+在 `<el-dialog title="驳回原因" ...>` 之前插入:
+
+```html
+<!-- 阅读记录弹窗 -->
+<el-dialog title="文章阅读记录" :visible.sync="readRecordVisible" width="800px" destroy-on-close>
+  <div v-loading="readRecordLoading" style="min-height: 200px;">
+    <el-table :data="readRecordList" border stripe size="small">
+      <el-table-column prop="nickname" label="孩子昵称" width="120" />
+      <el-table-column prop="duration_seconds" label="累计时长(秒)" width="120">
+        <template slot-scope="{ row }">
+          {{ formatDuration(row.duration_seconds) }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="read_date" label="最后阅读日期" width="130">
+        <template slot-scope="{ row }">
+          {{ row.read_date || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="read_at" label="最后阅读时间" width="170">
+        <template slot-scope="{ row }">
+          {{ formatTime(row.read_at) }}
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      v-if="readRecordTotal > 0"
+      layout="total, prev, pager, next"
+      :current-page="readRecordPage"
+      :page-size="readRecordSize"
+      :total="readRecordTotal"
+      @current-change="onReadRecordPageChange"
+      style="margin-top: 16px; text-align: right;"
+    />
+  </div>
+  <span slot="footer">
+    <el-button @click="readRecordVisible = false">关闭</el-button>
+  </span>
+</el-dialog>
+```
+
+**c) methods:新增方法**
+
+在现有 methods 末尾插入:
+
+```javascript
+    async openReadRecords(row) {
+      this.readRecordArticleId = row.id
+      this.readRecordPage = 1
+      this.readRecordList = []
+      this.readRecordTotal = 0
+      this.readRecordVisible = true
+      await this.loadReadRecords()
+    },
+    async loadReadRecords() {
+      this.readRecordLoading = true
+      try {
+        const res = await adminArticleReadingRecords({
+          articleId: this.readRecordArticleId,
+          page: this.readRecordPage,
+          size: this.readRecordSize
+        })
+        if (res.data) {
+          this.readRecordList = res.data.records || []
+          this.readRecordTotal = res.data.total || 0
+        }
+      } catch (e) {
+        this.$message.error('加载阅读记录失败')
+      } finally {
+        this.readRecordLoading = false
+      }
+    },
+    onReadRecordPageChange(page) {
+      this.readRecordPage = page
+      this.loadReadRecords()
+    },
+    formatDuration(seconds) {
+      if (!seconds) return '0秒'
+      var min = Math.floor(seconds / 60)
+      var sec = seconds % 60
+      if (min > 0 && sec > 0) return min + '分' + sec + '秒'
+      if (min > 0) return min + '分钟'
+      return sec + '秒'
+    }
+```
+
+**d) template:在操作列每个状态模板的"复制"按钮旁边新增"阅读记录"按钮**
+
+在每个状态的 `<el-button size="mini" type="primary">` 或操作区末尾插入(以发布中状态为例,其他状态按需添加):
+
+```html
+<el-button size="mini" @click="openReadRecords(row)">阅读记录</el-button>
+```
+
+注意:所有状态下(草稿箱、待审核、已驳回、发布中、已撤回、其他)都应有此按钮,统一放在操作区域。
+
+- [ ] **步骤 3:编译验证**
+
+```bash
+cd cfc-web && npm run build
+```
+
+预期:构建成功(无 TypeScript/ESLint 错误)
+
+- [ ] **步骤 4:Commit**
+
+```bash
+cd /sc-data/cfc && git add \
+  cfc-web/src/api/article.js \
+  cfc-web/src/views/admin/ArticleManage.vue && \
+  git commit -m "feat(web): 文章管理页新增阅读记录弹窗"
+```
+
+---
+
+### Task 4: 小程序前端 — 阅读数显示 + 匿名用户处理
+
+**文件:**
+- 修改:`cfc-frontend/pages/article-center/index.vue`
+- 修改: `cfc-frontend/pages/article-center/article-detail.vue`
+
+- [ ] **步骤 1:列表页显示阅读数**
+
+在 `cfc-frontend/pages/article-center/index.vue` 的卡片 footer(第 105-110 行附近)中,在 `ac-card-fav` 后追加阅读数:
+
+将:
+```html
+              <text class="ac-card-fav">&#x2B50; {{ item.favCount || 0 }}</text>
+```
+改为:
+```html
+              <text class="ac-card-fav">&#x2B50; {{ item.favCount || 0 }}</text>
+              <text class="ac-card-sep">|</text>
+              <text class="ac-card-readcount">&#x1F441; {{ item.viewCount || 0 }}</text>
+```
+
+追加 CSS(在 `.ac-card-fav` 样式块之后):
+```css
+.ac-card-readcount {
+  color: #ccc;
+}
+```
+
+- [ ] **步骤 2:详情页 meta 显示阅读数**
+
+在 `cfc-frontend/pages/article-center/article-detail.vue` 的 meta 行(第 16-24 行)中,在现有 meta 项之后追加:
+
+将:
+```html
+           <text class="reading-time-badge completed" v-else>✅ 阅读完成</text>
+        </view>
+```
+改为:
+```html
+           <text class="reading-time-badge completed" v-else>✅ 阅读完成</text>
+           <text class="meta-sep">|</text>
+           <text class="meta-readcount">{{ article.viewCount || 0 }}人阅读</text>
+        </view>
+```
+
+追加 CSS:
+```css
+.meta-readcount {
+  font-size: 20rpx;
+  color: #bbb;
+}
+```
+
+- [ ] **步骤 3:匿名用户不启动计时器**
+
+在 `article-detail.vue` 中,`startReadingTimer` 方法加一个匿名用户检查。当前 `startReadingTimer` 已有 `if (this.isTimerRunning || this.readingCompleted) return`,在其前追加:
+
+```javascript
+    startReadingTimer: function() {
+      var memberId = uni.getStorageSync('currentChildId')
+      if (!memberId) return
+      if (this.isTimerRunning || this.readingCompleted) return
+      // ... 原有逻辑
+```
+
+同样,在 `onShow` 和 `startNewReadingSession` 中也加入此检查,防止匿名用户启动计时:
+
+```javascript
+    onShow: function() {
+      var memberId = uni.getStorageSync('currentChildId')
+      if (!this.article || this.error || !memberId) return
+      this.startNewReadingSession()
+    },
+```
+
+同时,在模板中阅读时长徽章(第 22-23 行)已有 `v-if="!readingCompleted"` 条件,但匿名用户应完全不显示该徽章,改为:
+
+```html
+          <text class="reading-time-badge" v-if="!readingCompleted && currentChildId">{{ formatReadingTime(readingSeconds) }}</text>
+          <text class="reading-time-badge completed" v-else>✅ 阅读完成</text>
+```
+
+在 data 中新增一个计算属性或直接用已存储的值,这里利用已存在逻辑——匿名用户 `memberId` 为空时计时器不启动,`readingSeconds` 始终为 0,徽章本身也不应显示。最简单:在 data 中加一个 `currentChildId` 字段,或在模板中直接读取 storage:
+
+```html
+          <text class="reading-time-badge" v-if="!readingCompleted && !!currentChildId">{{ formatReadingTime(readingSeconds) }}</text>
+```
+
+> 注意:小程序模板中不能直接调用 `uni.getStorageSync`,需从 `data` 中取值。在 `onLoad` 中赋值:
+```javascript
+    onLoad(options) {
+      // ... 原有代码
+      this.currentChildId = uni.getStorageSync('currentChildId') || null
+    },
+```
+在 data 中加 `currentChildId: null`。
+
+- [ ] **步骤 4:编译验证**
+
+```bash
+cd cfc-frontend && node -e "require('fs').readFileSync('pages/article-center/index.vue','utf8')" && \
+  node -e "require('fs').readFileSync('pages/article-center/article-detail.vue','utf8')"
+```
+
+预期:文件内容可读,无语法错误。
+
+- [ ] **步骤 5:Commit**
+
+```bash
+cd /sc-data/cfc && git add \
+  cfc-frontend/pages/article-center/index.vue \
+  cfc-frontend/pages/article-center/article-detail.vue && \
+  git commit -m "feat(frontend): 文章列表/详情页显示阅读数,匿名用户不启动计时"
+```
+
+---
+
+### Task 5: 规格文档同步更新 + 最终验证
+
+- [ ] **步骤 1:更新 PROJECT-OVERVIEW.md**
+
+在 `docs/superpowers/PROJECT-OVERVIEW.md` 的"4.1 文章与内容运营"章节追加新条目:
+
+```markdown
+| 文章阅读追踪增强(阅读数+后台查询) | Phase 6 | 🚧 实施中 | 本文档(2026-08-30) | `specs/2026-07-28-article-reading-time-tracking-design.md` + 本计划 |
+```
+
+- [ ] **步骤 2:最终编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+
+预期:`BUILD SUCCESS`
+
+- [ ] **步骤 3:Commit 文档变更**
+
+```bash
+cd /sc-data/cfc && git add docs/superpowers/PROJECT-OVERVIEW.md && \
+  git commit -m "docs: 更新 PROJECT-OVERVIEW.md 文章阅读追踪增强条目"
+```