# 文章推荐排序(精选顺序可定义)实现计划 > **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 **目标:** 让运营在管理端可定义精选文章在首页/知识中心的展示顺序,提供上移/下移操作,无需前端额外开发。 **架构:** 在 `articles` 表新增 `sort_order` 列(整数,越小越靠前);`getFeatured()` 和 `getPublicList()` 查询改用 `sort_order ASC`;管理端新增 `/api/admin/articles/move` 接口支持相邻交换 + 全局重编号;管理端列表对精选文章显示上移/下移按钮和序号徽章。 **技术栈:** Spring Boot 2.7.18 + MyBatis-Plus、Vue 2 + Element UI、MySQL --- ## 文件清单 | 文件 | 操作 | 说明 | |------|------|------| | `cfc-backend/.../entity/Article.java` | 修改 | 加 `sortOrder` 字段 | | `cfc-backend/.../service/ArticleService.java` | 修改 | 改排序 + 改 toggleFeatured + 新增 moveSortOrder / getMaxSortOrder | | `cfc-backend/.../controller/admin/AdminArticleController.java` | 修改 | 新增 POST /move 接口 | | `cfc-backend/.../config/DatabaseInitializer.java` | 修改 | 迁移293 加 sort_order 列 | | `cfc-backend/src/main/resources/schema.sql` | 修改 | articles CREATE TABLE 加 sort_order 列 | | `cfc-web/src/api/article.js` | 修改 | 新增 adminArticleMove | | `cfc-web/src/views/admin/ArticleManage.vue` | 修改 | 精选行显示上移/下移 + 序号徽章 | | `docs/superpowers/api/API_REFERENCE.md` | 修改 | 新增 /move 接口文档 | --- ## 任务 1:数据库迁移 — 新增 sort_order 列 **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` - 修改:`cfc-backend/src/main/resources/schema.sql` - [ ] **步骤 1:在 DatabaseInitializer.java 末尾(迁移292 之后)追加迁移293** 打开 `DatabaseInitializer.java`,在: ``` // 迁移292: health_status 补 member_id 并改为成员级归档 ``` 块结束后(约第 10330 行,`}` 后)追加: ```java // 迁移293: articles 表加 sort_order 列(精选文章推荐排序,越小越靠前) ensureColumn("articles", "sort_order", "INT NOT NULL DEFAULT 0 COMMENT '推荐排序值(越小越靠前)'"); log.info("已添加 articles.sort_order 列(迁移293)"); ``` - [ ] **步骤 2:更新 schema.sql 的 articles 建表语句** 打开 `cfc-backend/src/main/resources/schema.sql`,找到 `CREATE TABLE IF NOT EXISTS articles (`(约第 1395 行),在 `is_featured` 列之后、`published_at` 之前插入: ```sql sort_order INT NOT NULL DEFAULT 0 COMMENT '推荐排序值(越小越靠前)', ``` 当前 schema.sql 原文(需要修改的位置): ```sql is_featured TINYINT DEFAULT 0 COMMENT '是否精选:1 精选/0 普通', published_at DATETIME DEFAULT NULL COMMENT '发布时间', ``` 改为: ```sql is_featured TINYINT DEFAULT 0 COMMENT '是否精选:1 精选/0 普通', sort_order INT NOT NULL DEFAULT 0 COMMENT '推荐排序值(越小越靠前)', published_at DATETIME DEFAULT NULL COMMENT '发布时间', ``` - [ ] **步骤 3:编译验证** ```bash cd /sc-data/cfc/cfc-backend && mvn clean compile -q 2>&1 | tail -5 ``` 预期:无 ERROR 行,退出码 0。 - [ ] **步骤 4:Commit** ```bash cd /sc-data/cfc git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java git add cfc-backend/src/main/resources/schema.sql git commit -m "feat(articles): 迁移293 为 articles 表添加 sort_order 列" ``` --- ## 任务 2:Article 实体加 sortOrder 字段 **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/entity/Article.java` - [ ] **步骤 1:在 Article.java 的 isFeatured 字段后加 sortOrder** 找到: ```java private Integer isFeatured; private Date publishedAt; ``` 改为: ```java private Integer isFeatured; /** 推荐排序值(越小越靠前,仅精选文章参与排序) */ private Integer sortOrder; private Date publishedAt; ``` - [ ] **步骤 2:编译验证** ```bash cd /sc-data/cfc/cfc-backend && mvn clean compile -q 2>&1 | tail -5 ``` 预期:无 ERROR 行,退出码 0。 - [ ] **步骤 3:Commit** ```bash cd /sc-data/cfc git add cfc-backend/src/main/java/com/etotem/cfc/entity/Article.java git commit -m "feat(articles): Article 实体加 sortOrder 字段" ``` --- ## 任务 3:ArticleService — 改排序逻辑 + toggleFeatured 追加队尾 + 新增 moveSortOrder **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java` ### 步骤 1:改 getFeatured() 排序 找到 `ArticleService.java` 第 125 行 `getFeatured` 方法,将: ```java LambdaQueryWrapper
wrapper = new LambdaQueryWrapper
() .eq(Article::getStatus, "published") .eq(Article::getIsFeatured, 1) .orderByDesc(Article::getPublishedAt) .last("LIMIT " + Math.max(size, 50)); ``` 改为: ```java LambdaQueryWrapper
wrapper = new LambdaQueryWrapper
() .eq(Article::getStatus, "published") .eq(Article::getIsFeatured, 1) .orderByAsc(Article::getSortOrder) .orderByDesc(Article::getPublishedAt) .last("LIMIT " + Math.max(size, 50)); ``` ### 步骤 2:改 getPublicList() 排序 找到 `ArticleService.java` 第 86 行 `getPublicList` 方法,将: ```java LambdaQueryWrapper
wrapper = new LambdaQueryWrapper
() .eq(Article::getStatus, "published") .orderByDesc(Article::getIsFeatured) .orderByDesc(Article::getPublishedAt); ``` 改为: ```java LambdaQueryWrapper
wrapper = new LambdaQueryWrapper
() .eq(Article::getStatus, "published") .orderByDesc(Article::getIsFeatured) .orderByAsc(Article::getSortOrder) .orderByDesc(Article::getPublishedAt); ``` ### 步骤 3:改 toggleFeatured() 设为精选时追加队尾 找到第 541-548 行 `toggleFeatured` 方法,将: ```java @Transactional public void toggleFeatured(Long id, int isFeatured) { Article article = articleMapper.selectById(id); if (article == null) return; article.setIsFeatured(isFeatured); article.setUpdatedAt(new Date()); articleMapper.updateById(article); } ``` 改为: ```java @Transactional public void toggleFeatured(Long id, int isFeatured) { Article article = articleMapper.selectById(id); if (article == null) return; article.setIsFeatured(isFeatured); article.setUpdatedAt(new Date()); if (isFeatured == 1) { // 设为精选时追加到队尾,不抢占既有顺序 Integer maxSort = getMaxSortOrder(); article.setSortOrder((maxSort == null ? 0 : maxSort) + 1); } articleMapper.updateById(article); } ``` ### 步骤 4:新增 getMaxSortOrder() 和 moveSortOrder() 两个方法 在 `toggleFeatured()` 方法**之后**(约第 548 行后)插入以下两个方法: ```java /** * 查询已发布且精选的文章中最大的 sort_order;若没有精选文章返回 null。 */ private Integer getMaxSortOrder() { LambdaQueryWrapper
wrapper = new LambdaQueryWrapper
() .eq(Article::getStatus, "published") .eq(Article::getIsFeatured, 1) .select(Article::getSortOrder) .orderByDesc(Article::getSortOrder) .last("LIMIT 1"); Article top = articleMapper.selectOne(wrapper); return top != null ? top.getSortOrder() : null; } /** * 上移/下移精选文章的推荐顺序。 * direction: 'up' 或 'down'。 * 边界(已在最前/最后)抛 RuntimeException,调用方会返回 400。 */ @Transactional public String moveSortOrder(Long id, String direction) { Article target = articleMapper.selectById(id); if (target == null) { throw new RuntimeException("文章不存在"); } if (!"published".equals(target.getStatus()) || !Integer.valueOf(1).equals(target.getIsFeatured())) { throw new RuntimeException("仅已发布且精选的文章可调整顺序"); } if (!"up".equals(direction) && !"down".equals(direction)) { throw new RuntimeException("direction 只能是 up 或 down"); } // 按展示序(sort_order ASC, published_at ASC)取全部精选文章列表 // published_at ASC 用于并列时稳定排序(sort_order 已唯一后不影响结果) LambdaQueryWrapper
listWrapper = new LambdaQueryWrapper
() .eq(Article::getStatus, "published") .eq(Article::getIsFeatured, 1) .orderByAsc(Article::getSortOrder) .orderByAsc(Article::getPublishedAt); List
featured = articleMapper.selectList(listWrapper); int idx = -1; for (int i = 0; i < featured.size(); i++) { if (featured.get(i).getId().equals(id)) { idx = i; break; } } if (idx == -1) { throw new RuntimeException("文章不存在"); } if ("up".equals(direction) && idx <= 0) { throw new RuntimeException("已在最前面"); } if ("down".equals(direction) && idx >= featured.size() - 1) { throw new RuntimeException("已在最后面"); } int swapIdx = "up".equals(direction) ? idx - 1 : idx + 1; Long swapId = featured.get(swapIdx).getId(); // 交换 sort_order(先交换,避免覆盖) Article a = featured.get(idx); Article b = featured.get(swapIdx); Integer tmpSort = a.getSortOrder(); a.setSortOrder(b.getSortOrder()); b.setSortOrder(tmpSort); a.setUpdatedAt(new Date()); b.setUpdatedAt(new Date()); articleMapper.updateById(a); articleMapper.updateById(b); // 全局重编号 1..N,消除并列与空洞 LambdaQueryWrapper
renumWrapper = new LambdaQueryWrapper
() .eq(Article::getStatus, "published") .eq(Article::getIsFeatured, 1) .orderByAsc(Article::getSortOrder) .orderByAsc(Article::getPublishedAt); List
sorted = articleMapper.selectList(renumWrapper); Date now = new Date(); for (int i = 0; i < sorted.size(); i++) { sorted.get(i).setSortOrder(i + 1); sorted.get(i).setUpdatedAt(now); articleMapper.updateById(sorted.get(i)); } return "已调整顺序"; } ``` - [ ] **步骤 5:编译验证** ```bash cd /sc-data/cfc/cfc-backend && mvn clean compile -q 2>&1 | tail -10 ``` 预期:无 ERROR 行,退出码 0。 - [ ] **步骤 6:Commit** ```bash cd /sc-data/cfc git add cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java git commit -m "feat(articles): getFeatured/getPublicList 改按 sort_order 排序,toggleFeatured 追加队尾,新增 moveSortOrder" ``` --- ## 任务 4:AdminArticleController 新增 POST /move 接口 **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java` - [ ] **步骤 1:在 toggleFeatured 方法后插入 move 接口** 找到 `AdminArticleController.java` 第 265-271 行 `toggleFeatured` 方法之后,插入: ```java @PostMapping("/move") public Result move(@RequestBody Map body, @RequestAttribute("role") String role) { if (!"admin".equals(role)) { return Result.error("无权限"); } Long id = ParamUtils.getLong(body.get("id")); String direction = (String) body.get("direction"); if (id == null) { return Result.error("缺少文章ID"); } if (direction == null || (!"up".equals(direction) && !"down".equals(direction))) { return Result.error("direction 只能是 up 或 down"); } try { articleService.moveSortOrder(id, direction); return Result.success("已调整顺序"); } catch (RuntimeException e) { return Result.error(e.getMessage()); } } ``` - [ ] **步骤 2:编译验证** ```bash cd /sc-data/cfc/cfc-backend && mvn clean compile -q 2>&1 | tail -10 ``` 预期:无 ERROR 行,退出码 0。 - [ ] **步骤 3:Commit** ```bash cd /sc-data/cfc git add cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java git commit -m "feat(articles): AdminArticleController 新增 POST /move 接口" ``` --- ## 任务 5:cfc-web — api/article.js 加 adminArticleMove **文件:** - 修改:`cfc-web/src/api/article.js` - [ ] **步骤 1:在 adminArticleToggleFeatured 后新增 adminArticleMove** 找到 `cfc-web/src/api/article.js` 中 `adminArticleToggleFeatured` 函数(约第 25-27 行),在其后插入: ```js export function adminArticleMove(data) { return request({ url: '/api/admin/articles/move', method: 'post', data }) } ``` 完整 api/article.js 关键片段(修改后): ```js export function adminArticleToggleFeatured(data) { return request({ url: '/api/admin/articles/toggle-featured', method: 'post', data }) } export function adminArticleMove(data) { return request({ url: '/api/admin/articles/move', method: 'post', data }) } ``` - [ ] **步骤 2:Commit** ```bash cd /sc-data/cfc git add cfc-web/src/api/article.js git commit -m "feat(web): article API 新增 adminArticleMove" ``` --- ## 任务 6:cfc-web — ArticleManage.vue 加上移/下移按钮与序号徽章 **文件:** - 修改:`cfc-web/src/views/admin/ArticleManage.vue` ### 步骤 1:在 data() 中引入 adminArticleMove 在 `