Sfoglia il codice sorgente

docs: 文章推荐排序(精选顺序可定义)设计规格

iwt 2 settimane fa
parent
commit
2e08b96bfc

+ 0 - 2
cfc-backend/src/main/java/com/etotem/cfc/entity/HealthReport.java

@@ -83,8 +83,6 @@ public class HealthReport implements Serializable {
     /** 报告文件URL */
     private String fileUrl;
 
-    private String reportFormat;
-
     /** 状态: active/archived */
     private String status;
 

+ 181 - 0
docs/superpowers/specs/2026-09-04-article-sort-order-design.md

@@ -0,0 +1,181 @@
+# 文章推荐排序(精选顺序可定义)设计
+
+> 日期:2026-09-04
+> 状态:已批准(用户确认:不做独立置顶,仅使用精选 + 列表内上移/下移)
+
+## 背景与目标
+
+首页「推荐阅读」与知识中心「书册式推荐」当前展示的内容由 `is_featured=1`(精选)决定,但**顺序完全由 `published_at` 决定**,运营无法控制哪些精选文章排在前面。
+
+目标:让运营在管理端**定义精选文章在首页/知识中心的展示顺序**,从而做到"首页放的内容可定义"。
+
+## 现状梳理
+
+### 后端
+
+| 方法 | 位置 | 当前排序逻辑 |
+|------|------|-------------|
+| `getFeatured()` | `ArticleService.java:125` | `WHERE status=published AND is_featured=1 ORDER BY published_at DESC LIMIT 50` |
+| `getPublicList()` | `ArticleService.java:86` | `WHERE status=published ORDER BY is_featured DESC, published_at DESC` |
+| `toggleFeatured()` | `ArticleService.java:542` | 直接写 `is_featured`,无顺序关联 |
+| 管理端列表 `getAdminList()` | `ArticleService.java:386` | `ORDER BY created_at DESC` + `SortUtil.applySort` |
+
+### 管理端(cfc-web)
+
+- `ArticleManage.vue`:文章管理列表,发布中的行下拉菜单已有「精选/取消精选」
+- `api/article.js`:已有 `adminArticleToggleFeatured`
+- 接口:`POST /api/admin/articles/toggle-featured`
+
+### 小程序端(cfc-frontend)
+
+- 首页「推荐阅读」:`getFeaturedArticles({ size: 5 })` → `getFeatured`
+- 知识中心「书册式推荐」:`getFeaturedArticles({ size: 8 })` → `getFeatured`
+- 顺序完全由后端返回决定,**前端无需改动**
+
+## 需求确认(用户决策)
+
+1. **不做**独立的置顶(is_top)字段 → 继续使用 `is_featured` 精选标记"要上首页的内容"
+2. **新增**排序能力 → 精选文章顺序可由运营控制
+3. **操作方式**:管理端文章列表内 **上移/下移** 按钮
+
+## 设计
+
+### 1. 数据库(迁移 293)
+
+`articles` 表新增一列:
+
+```sql
+sort_order INT NOT NULL DEFAULT 0 COMMENT '推荐排序值(越小越靠前)'
+```
+
+- 迁移位置:`DatabaseInitializer.runMigrations()`,使用 `ensureColumn("articles", "sort_order", "INT NOT NULL DEFAULT 0 COMMENT '推荐排序值(越小越靠前)'")`(幂等)
+- 同步更新 `schema.sql` 的 `CREATE TABLE articles`(在 `is_featured` 后加 `sort_order` 列)
+
+### 2. 后端
+
+#### 2a. 实体 `Article.java`
+新增字段:
+```java
+/** 推荐排序值(越小越靠前,仅精选文章参与排序) */
+private Integer sortOrder;
+```
+
+#### 2b. `ArticleService.getFeatured()`
+排序由 `ORDER BY published_at DESC` 改为:
+
+```
+ORDER BY sort_order ASC, published_at DESC
+```
+
+效果:精选文章顺序由运营的 sort_order 控制,未显式设置过 sort_order 的精选(默认 0)排在前面,与新精选的追加逻辑配合避免"新精选抢占首位"。
+
+#### 2c. `ArticleService.getPublicList()`
+排序由 `ORDER BY is_featured DESC, published_at DESC` 改为:
+
+```
+ORDER BY is_featured DESC, sort_order ASC, published_at DESC
+```
+
+效果:知识中心"全部"列表中精选文章也按运营顺序靠前排布。
+
+#### 2d. `ArticleService.toggleFeatured()`
+设为精选(`isFeatured=1`)时,自动将 `sort_order` 置为 `当前最大 sort_order + 1`,使新精选**追加到队列末尾**,不抢占既有顺序。
+
+```java
+if (isFeatured == 1) {
+    // 新精选追加到队尾,不抢占已有顺序
+    Integer maxSort = getMaxSortOrder();
+    article.setSortOrder((maxSort == null ? 0 : maxSort) + 1);
+}
+```
+
+辅助方法 `getMaxSortOrder()`:查询 `articles WHERE status='published' AND is_featured=1` 的 `MAX(sort_order)`。
+
+#### 2e. 新增接口 `POST /api/admin/articles/move`
+
+在 `AdminArticleController` 新增:
+
+```
+POST /api/admin/articles/move
+body: { id: Long, direction: 'up' | 'down' }
+```
+
+**逻辑(`ArticleService.moveSortOrder(id, direction)`):**
+
+1. 查询"当前已发布且精选"文章列表,按 `sort_order ASC, published_at ASC` 排序(注意:与展示序一致取 ASC,保证交换后顺序与前端一致)
+2. 找到目标文章位置 `idx`
+3. `direction=up` 且 `idx <= 0` → 返回「已在最前面」;`direction=down` 且 `idx >= len-1` → 返回「已在最后面」
+4. 与相邻元素交换 sort_order
+5. 交换后对整个列表**重新编号** `sort_order = 1..N`(消除并列与空洞,保证顺序绝对稳定)
+6. 批量更新
+
+**实现细节**:交换相邻的 sort_order 可能出现并列——为防止并列排序不稳定,执行重编号(步骤 5)。
+
+**响应**:`Result<String>`,边界返回 `Result.error(400, "已在最前/最后")`。
+
+### 3. 管理端(cfc-web)
+
+#### 3a. `api/article.js`
+新增:
+```js
+export function adminArticleMove(data) {
+  return request({ url: '/api/admin/articles/move', method: 'post', data })
+}
+```
+
+#### 3b. `ArticleManage.vue`
+- 发布中下拉菜单「精选/取消精选」旁,当 `row.isFeatured === 1` 时增加「上移」「下移」菜单项(或行内小按钮)
+- 操作列:`row.status === 'published' && row.isFeatured === 1` 时显示上移/下移
+- 点击调用 `adminArticleMove({ id, direction })` 成功后刷新列表
+- 标题区精选标签旁显示顺序序号:`精选 ①`(用 `sortOrder` 换算:非零序号直接显示,0 显示为队尾追加的未明确序号可不显示或显示 `精选`)
+
+**边界**:非精选文章不显示上移/下移;列表排序按 `created_at DESC` 展示,与推荐顺序无关(运营通过按钮逐个调整)。
+
+### 4. 小程序端(cfc-frontend)
+
+无需改动。首页「推荐阅读」与知识中心书册数据均由 `getFeatured()` 返回,顺序自动受控。
+
+## 数据流
+
+```
+运营在管理端点「上移/下移」
+    → POST /api/admin/articles/move {id, direction}
+    → ArticleService.moveSortOrder() 交换 + 重编号 sort_order=1..N
+    → 小程序 getFeatured() 按 sort_order ASC 返回
+    → 首页「推荐阅读」/知识中心书册顺序更新
+```
+
+## 边界与异常处理
+
+| 场景 | 处理 |
+|------|------|
+| 目标文章不是精选 | `move` 拒绝:「仅精选文章可调整顺序」 |
+| 已是最前/最后 | 返回 400 提示「已在最前面/最后面」 |
+| 文章不存在 | 返回 400「文章不存在」 |
+| 方向非法 | 返回 400「direction 只能是 up/down」 |
+| 并发操作 | 事务 + 重编号天然收敛(最后一次操作者胜出) |
+| 新设精选 | 自动追加队尾,不抢占 |
+
+## 验证
+
+1. 后端:`mvn clean compile` 通过
+2. 管理端:`npm run build` 通过
+3. 手工验证流程:
+   - 设 2+ 篇文章为精选 → 首页顺序按发布时间的从新到旧(sort_order 追加序)
+   - 管理端点「上移/下移」→ 刷新列表顺位变化
+   - 首页/知识中心书册顺序与调整后一致
+   - 边界:第一文章点上移 → 提示;最后文章点下移 → 提示
+4. 迁移幂等:重复执行 `runMigrations()` 不报错
+
+## 文件改动清单
+
+| 文件 | 改动 |
+|------|------|
+| `cfc-backend/.../config/DatabaseInitializer.java` | 迁移293 加 sort_order 列 |
+| `cfc-backend/.../resources/schema.sql` | articles 建表加 sort_order 列 |
+| `cfc-backend/.../entity/Article.java` | 加 sortOrder 字段 |
+| `cfc-backend/.../service/ArticleService.java` | getFeatured/getPublicList 排序、toggleFeatured 追加队尾、新增 moveSortOrder + getMaxSortOrder |
+| `cfc-backend/.../controller/admin/AdminArticleController.java` | 新增 POST /move |
+| `cfc-web/src/api/article.js` | 新增 adminArticleMove |
+| `cfc-web/src/views/admin/ArticleManage.vue` | 上移/下移按钮 + 精选序号展示 |
+| `docs/superpowers/api/API_REFERENCE.md` | 同步记录 /move 接口 |