# 文章阅读追踪增强 实现计划 > **面向 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 wrapper = new LambdaQueryWrapper() .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.
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 { @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> 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> getArticleReadingRecords(Long articleId, int page, int size) { long total = articleReadingRecordMapper.countByArticle(articleId); if (total == 0) { Page> empty = new Page<>(page, size); empty.setTotal(0); empty.setRecords(Collections.emptyList()); return empty; } int offset = (page - 1) * size; List> records = articleReadingRecordMapper.selectByArticle(articleId, offset, size); Page> 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>> readingRecords( @RequestBody Map 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` 之前新增阅读记录弹窗** 在 `` 之前插入: ```html
关闭
``` **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:在操作列每个状态模板的"复制"按钮旁边新增"阅读记录"按钮** 在每个状态的 `` 或操作区末尾插入(以发布中状态为例,其他状态按需添加): ```html 阅读记录 ``` 注意:所有状态下(草稿箱、待审核、已驳回、发布中、已撤回、其他)都应有此按钮,统一放在操作区域。 - [ ] **步骤 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 ⭐ {{ item.favCount || 0 }} ``` 改为: ```html ⭐ {{ item.favCount || 0 }} | 👁 {{ item.viewCount || 0 }} ``` 追加 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 ✅ 阅读完成 ``` 改为: ```html ✅ 阅读完成 | {{ article.viewCount || 0 }}人阅读 ``` 追加 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 {{ formatReadingTime(readingSeconds) }} ✅ 阅读完成 ``` 在 data 中新增一个计算属性或直接用已存储的值,这里利用已存在逻辑——匿名用户 `memberId` 为空时计时器不启动,`readingSeconds` 始终为 0,徽章本身也不应显示。最简单:在 data 中加一个 `currentChildId` 字段,或在模板中直接读取 storage: ```html {{ formatReadingTime(readingSeconds) }} ``` > 注意:小程序模板中不能直接调用 `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 文章阅读追踪增强条目" ```