# 文章评论审核系统 实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. **Goal:** 补充文章评论审核能力:后端权限加固、Web管理端审核页完善、小程序端文章作者审核 **Architecture:** 后端通过校验文章作者或 admin 角色来加固审核权限,同时新增管理端分页查询接口和增强现有接口返回数据;Web管理端重写 CommentReview.vue 增加筛选/分页/驳回原因;小程序端 article-detail 增加作者审核面板。 **Tech Stack:** Spring Boot 2.7 + MyBatis-Plus / Vue 2 + Element UI / uni-app Vue 2 小程序 ## Global Constraints - 后端接口统一使用 `@PostMapping` - 小程序禁止可选链 `?.`(用 `&&` 替代) - Vue 2 Options API - DI 使用 `@Resource` - 响应统一 `Result` --- ### Task 1: 后端 — 审核权限校验 **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/ArticleCommentService.java` (auditComment 方法) - Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/content/ArticleCommentController.java` (auditComment 方法) **Interfaces:** - Consumes: `ArticleCommentMapper`, `ArticleMapper`, `UserMapper` (已有注入) - Produces: `auditComment(commentId, auditorId, status, role)` — 新增 role 参数;拒绝时抛 `RuntimeException("无权限审核该评论")` - [ ] **Step 1: ArticleCommentService.auditComment() 增加权限校验** 在 `auditComment()` 方法开头增加逻辑: 1. 根据 commentId 查 `ArticleComment`,获取 articleId 2. 根据 articleId 查 `Article`,获取 `createdBy`(文章作者ID) 3. 如果 `auditorId != article.createdBy` 且角色不是 `admin`,抛出异常 ```java // 权限校验:只有文章作者或管理员可审核 ArticleComment comment = articleCommentMapper.selectById(commentId); if (comment == null) return false; Article article = articleMapper.selectById(comment.getArticleId()); if (article == null) return false; if (!article.getCreatedBy().equals(auditorId) && !"admin".equals(role)) { throw new RuntimeException("无权限审核该评论"); } ``` 注意:这段代码要放在方法现有逻辑的最前面。方法签名改为: ```java public boolean auditComment(Long commentId, Long auditorId, String status, String role) ``` - [ ] **Step 2: ArticleCommentController.auditComment() 传递 role** 添加 `@RequestAttribute("role") String role` 参数,调用 service 时传入: ```java return articleCommentService.auditComment(commentId, userId, status, role) ? Result.success("审核完成") : Result.error("评论不存在"); ``` - [ ] **Step 3: 编译验证** ```bash cd /app/cfc/cfc-backend && mvn clean compile ``` --- ### Task 2: 后端 — 待审核列表增强 **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/ArticleCommentService.java` (getPendingComments 方法) - Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/content/ArticleCommentController.java` **Interfaces:** - Produces: `getPendingComments(articleId)` 返回 `List>`,每个 map 包含 `id, articleId, articleTitle, content, userName, userId, status, parentId, replyToId, createdAt` - [ ] **Step 1: 修改 ArticleCommentService.getPendingComments() 返回增强数据** 将返回类型从 `List` 改为 `List>`,方法内查询文章标题并返回: ```java public List> getPendingComments(Long articleId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(ArticleComment::getStatus, "pending"); if (articleId != null) { wrapper.eq(ArticleComment::getArticleId, articleId); } wrapper.orderByDesc(ArticleComment::getCreatedAt); List comments = articleCommentMapper.selectList(wrapper); Map articleTitleCache = new HashMap<>(); Map userCache = new HashMap<>(); List> result = new ArrayList<>(); for (ArticleComment c : comments) { Map item = new HashMap<>(); item.put("id", c.getId()); item.put("articleId", c.getArticleId()); item.put("content", c.getContent()); item.put("status", c.getStatus()); item.put("parentId", c.getParentId()); item.put("replyToId", c.getReplyToId()); item.put("createdAt", c.getCreatedAt()); // 文章标题 if (!articleTitleCache.containsKey(c.getArticleId())) { Article a = articleMapper.selectById(c.getArticleId()); articleTitleCache.put(c.getArticleId(), a != null ? a.getTitle() : ""); } item.put("articleTitle", articleTitleCache.get(c.getArticleId())); // 用户名 User user = getUserCached(c.getUserId(), userCache); item.put("userName", user != null ? (user.getNickname() != null ? user.getNickname() : user.getPhone()) : "匿名"); item.put("userId", c.getUserId()); result.add(item); } return result; } ``` 注意:方法内已有 `getUserCached()` 辅助方法,可直接复用。 - [ ] **Step 2: 更新 ArticleCommentController.getPendingComments() 签名** ```java @PostMapping("/comments/pending") public Result>> getPendingComments(@RequestBody Map params) { Long articleId = params.get("articleId") != null ? Long.valueOf(params.get("articleId").toString()) : null; return Result.success(articleCommentService.getPendingComments(articleId)); } ``` - [ ] **Step 3: 编译验证** ```bash cd /app/cfc/cfc-backend && mvn clean compile ``` --- ### Task 3: 后端 — 新增管理端评论列表接口 **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/ArticleCommentService.java` (新增 getAdminList 方法) - Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java` (新增接口) **Interfaces:** - Produces: `getAdminList(status, articleId, keyword, page, size)` → `Page>` - 新 API: `POST /api/admin/articles/comments/list` - [ ] **Step 1: ArticleCommentService 新增 getAdminList 方法** ```java public Page> getAdminList(String status, Long articleId, String keyword, int page, int size) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); if (status != null && !status.isEmpty()) { wrapper.eq(ArticleComment::getStatus, status); } if (articleId != null) { wrapper.eq(ArticleComment::getArticleId, articleId); } if (keyword != null && !keyword.trim().isEmpty()) { wrapper.like(ArticleComment::getContent, keyword.trim()); } wrapper.orderByDesc(ArticleComment::getCreatedAt); Page p = articleCommentMapper.selectPage(new Page<>(page, size), wrapper); Map articleTitleCache = new HashMap<>(); Map userCache = new HashMap<>(); List> records = new ArrayList<>(); for (ArticleComment c : p.getRecords()) { Map item = new HashMap<>(); item.put("id", c.getId()); item.put("articleId", c.getArticleId()); item.put("content", c.getContent()); item.put("status", c.getStatus()); item.put("parentId", c.getParentId()); item.put("replyToId", c.getReplyToId()); item.put("auditBy", c.getAuditBy()); item.put("auditAt", c.getAuditAt()); item.put("createdAt", c.getCreatedAt()); if (!articleTitleCache.containsKey(c.getArticleId())) { Article a = articleMapper.selectById(c.getArticleId()); articleTitleCache.put(c.getArticleId(), a != null ? a.getTitle() : ""); } item.put("articleTitle", articleTitleCache.get(c.getArticleId())); User user = getUserCached(c.getUserId(), userCache); item.put("userName", user != null ? (user.getNickname() != null ? user.getNickname() : user.getPhone()) : "匿名"); item.put("userId", c.getUserId()); records.add(item); } Page> result = new Page<>(page, size); result.setTotal(p.getTotal()); result.setRecords(records); return result; } ``` - [ ] **Step 2: AdminArticleController 新增接口** ```java @PostMapping("/comments/list") public Result>> commentList(@RequestBody Map body) { String status = (String) body.get("status"); Long articleId = body.get("articleId") != null ? Long.valueOf(body.get("articleId").toString()) : null; String keyword = (String) body.get("keyword"); 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(articleCommentService.getAdminList(status, articleId, keyword, page, size)); } ``` 需要在 `AdminArticleController` 中注入 `ArticleCommentService`: ```java @Resource private ArticleCommentService articleCommentService; ``` - [ ] **Step 3: 编译验证** ```bash cd /app/cfc/cfc-backend && mvn clean compile ``` --- ### Task 4: Web 管理端 — API 封装 **Files:** - Modify: `cfc-web/src/api/article.js` - [ ] **Step 1: 新增评论管理 API** 在 article.js 末尾添加: ```javascript // 评论管理 export const getCommentList(data) { return request({ url: '/api/admin/articles/comments/list', method: 'post', data }) } export const auditComment(data) { return request({ url: '/api/articles/comments/audit', method: 'post', data }) } ``` 检查 `CommentReview.vue` 中的现有 `request` 调用方式,保持风格一致。 --- ### Task 5: Web 管理端 — 评论审核页面重写 **Files:** - Rewrite: `cfc-web/src/views/admin/CommentReview.vue` - [ ] **Step 1: 重写 CommentReview.vue** 完整实现包含: 1. 筛选区:状态下拉框 + 文章搜索输入框 + 搜索按钮 2. 表格:ID、文章标题(可点击)、评论用户、评论内容、状态标签、审核原因、时间、操作 3. 分页组件 4. 驳回弹窗含原因输入 5. 调用新的 `/api/admin/articles/comments/list` 接口 ```vue ``` --- ### Task 6: 小程序端 — 文章作者审核 **Files:** - Modify: `cfc-frontend/pages/article-center/article-detail.vue` - Modify: `cfc-frontend/utils/api.js` - [ ] **Step 1: api.js 新增 pending 评论接口** 在 `utils/api.js` 中 `getArticleComments` 附近添加: ```javascript export const getPendingComments = (data) => request('/api/articles/comments/pending', 'POST', data) ``` - [ ] **Step 2: article-detail.vue 增加作者审核能力** 在 data 中新增: ```javascript isAuthor: false, pendingComments: [], showPendingSection: false ``` 在 `loadDetail` 成功后增加: ```javascript var currentUserId = uni.getStorageSync('userId') this.isAuthor = res.data.createdBy == currentUserId if (this.isAuthor) this.loadPendingComments() ``` 新增方法: ```javascript async loadPendingComments() { if (!this.articleId) return try { var res = await getPendingComments({ articleId: this.articleId }) if (res.code === 200) this.pendingComments = res.data || [] } catch (e) {} }, async auditComment(commentId, status) { var reason = '' if (status === 'rejected') { var r = await new Promise(function(resolve) { uni.showModal({ title: '驳回评论', editable: true, placeholderText: '请输入驳回原因...', success: function(res) { resolve(res.confirm ? res.content : '') } }) }) if (!r) return // 用户取消 reason = r } try { var res = await createArticleComment // 需要新建 audit API } } ``` etc. More details. Wait - the frontend doesn't have an import for the audit API yet. The `article-detail.vue` already imports APIs from `@/utils/api.js`. I need to also add audit API to api.js. Let me refine this: In api.js add: ```javascript export const auditArticleComment = (data) => request('/api/articles/comments/audit', 'POST', data) ``` In article-detail.vue, add to imports: `auditArticleComment` And the template should show pending comments section when `isAuthor && pendingComments.length > 0`. - [ ] **Step 3: 完善 article-detail.vue 模板** 在评论区的 `comment-section` 内部,已审核评论之前,添加: ```html 待审核评论 ({{ pendingComments.length }}) {{ (c.userName || '?').charAt(0) }} {{ c.userName || '匿名' }} {{ formatDate(c.createdAt) }} {{ c.content }} ``` 样式添加: ```css .pending-item { background: #FFFBF0; border-radius: 8rpx; padding: 16rpx; margin-bottom: 12rpx; } .audit-actions { display: flex; gap: 16rpx; padding-left: 52rpx; margin-top: 12rpx; } .audit-btn { height: 56rpx; line-height: 56rpx; padding: 0 24rpx; border-radius: 28rpx; font-size: 24rpx; border: none; } .audit-btn.approve { background: #10B981; color: #fff; } .audit-btn.reject { background: #EF4444; color: #fff; } ``` --- ### 执行顺序 1. Task 1(后端权限) → Task 2(列表增强) → Task 3(管理端接口) 2. Task 4(Web API 封装)+ Task 5(Web 页面重写) 3. Task 6(小程序端) Task 4 和 5 可并行执行(都在 cfc-web 中),Task 6 独立。