Kaynağa Gözat

docs: 首页快捷方式自定义+应用库+频度追踪设计规格

iwt 1 gün önce
ebeveyn
işleme
003487ac99

+ 297 - 0
docs/superpowers/specs/2026-09-21-homepage-shortcut-application-library-design.md

@@ -0,0 +1,297 @@
+# 首页快捷方式自定义 + 应用库 + 频度追踪 设计规格
+
+- 日期:2026-09-21
+- 状态:待用户审查
+- 涉及模块:`cfc-backend`(后端)+ `cfc-frontend`(微信小程序)
+
+---
+
+## 1. 背景与目标
+
+首页(`pages/index-home/index.vue`)在"开通会员"卡片下方有一排 **6 个硬编码快捷入口**:任务、互动、天盘、成员、圈子、商城。
+
+当前问题:
+- 6 个入口写死在模板里,用户无法自定义。
+- 平台有大量"可直接进入"的功能页面(健康报告、饮食推荐、AI 管家、成长记录、心愿、测评、小游戏、活动、会员中心、财富等),散落在各分包,用户找不到。
+- 没有记录用户对每个功能的使用频度,无法做个性化推荐。
+
+目标:
+1. 把这 6 个快捷入口改造成**用户可自定义**(从应用库选择放哪些)。
+2. 为每个可直接进入的功能设计**图标**,形成**应用库**。
+3. 记录用户进入每个应用的**次数**。
+4. 根据**使用频度**自动调整首页快捷方式(锁定应用优先 + 频度填充)。
+5. 已在首页有显著入口的功能,**不进快捷配置列表**(避免重复)。
+
+---
+
+## 2. 需求澄清结论(已与用户确认)
+
+| 问题 | 结论 |
+|------|------|
+| 频度调整机制 | **自动排序但可手动覆盖**:系统按频度排序,但用户手动锁定后以锁定为准 |
+| 配置与频度存储 | **服务端存储**(换设备同步、频度累计准确) |
+| 适用人群 | **所有登录用户**(非会员与会员一致) |
+| 首页重复功能 | **已在首页显著位置的功能,从配置列表中排除** |
+| 应用库范围 | **全部候选功能都入库** |
+| 锁定的语义 | **锁定 = 置顶**,不会被点击次数挤掉;但用户仍可手动移除 |
+| 锁定操作入口 | **只在应用库配置页管理**,首页不显示锁图标 |
+| 自动填充逻辑 | **锁定占位 + 频度填满剩余** |
+| 锁定应用内部排序 | **支持**手动调整锁定应用之间的顺序 |
+| 频度计数触发 | **任意入口进入都计数**(含 TabBar 页) |
+
+---
+
+## 3. 架构方案
+
+采用 **混合方案**:
+
+- **应用库 = 前端常量文件** `utils/app-library.js`,图标用 **emoji**(与现有 6 个快捷入口一致,零静态资源成本)。
+- **后端建 2 张表**:`user_shortcut_config`(用户快捷配置)+ `user_app_usage`(使用频度)。
+
+理由:
+- 应用库本质是静态配置(key/名称/图标/路由/是否可快捷),前端维护最灵活,迭代快。
+- 新增应用需发版(可接受,功能迭代本就发版)。
+- 符合 YAGNI:应用库无"运营后台动态增删"需求,不做成后端表。
+
+---
+
+## 4. 数据模型
+
+### 4.1 `user_shortcut_config`(用户快捷配置)
+
+记录"用户把某个应用放到快捷区"的配置,**每应用一条记录**,`locked` 决定是否锁定。
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT PK AUTO | 主键 |
+| user_id | BIGINT NOT NULL | 用户 ID |
+| app_key | VARCHAR(64) NOT NULL | 应用 key(对应 app-library.js) |
+| locked | TINYINT DEFAULT 0 | 是否锁定(1=锁定/置顶,0=普通) |
+| sort_order | INT DEFAULT 0 | 手动排序序号(锁定的应用之间排序用) |
+| created_at | DATETIME | 创建时间 |
+| updated_at | DATETIME | 更新时间 |
+
+- **唯一索引**:`(user_id, app_key)`
+
+> 语义:`locked=1` 的应用在首页快捷区**始终保留**(不被频度排序挤掉),按 `sort_order` 升序排在最前;`locked=0` 的应用参与频度填充。
+
+### 4.2 `user_app_usage`(应用使用频度)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT PK AUTO | 主键 |
+| user_id | BIGINT NOT NULL | 用户 ID |
+| app_key | VARCHAR(64) NOT NULL | 应用 key |
+| enter_count | INT DEFAULT 0 | 累计进入次数 |
+| last_enter_at | DATETIME | 最近进入时间 |
+
+- **唯一索引**:`(user_id, app_key)`
+
+---
+
+## 5. 应用库清单(前端常量 `utils/app-library.js`)
+
+每个应用字段:`key`、`name`、`icon`(emoji)、`route`、`canShortcut`(是否可放首页快捷区)。
+
+| key | 名称 | 图标 | 路由 | canShortcut |
+|-----|------|------|------|:---:|
+| task | 任务 | 📋 | /pages/tasks/tasks | ✅ |
+| interaction | 互动 | 💬 | /pages/action-detail/interaction-log | ✅ |
+| tiandi | 天盘 | 🏠 | /pages/mind-detail/family-dashboard | ✅ |
+| members | 成员 | 👨‍👩‍👧‍👦 | /pages/profile-extra/family-members | ✅ |
+| circle | 圈子 | 🤝 | /pages/discover/circles | ✅ |
+| shop | 商城 | 🛒 | /pages/shop/index/index | ✅ |
+| diet | 饮食推荐 | 🥗 | /pages/diet/index | ✅ |
+| ai | AI管家 | 🤖 | /pages/ai/chat | ✅ |
+| growth | 成长记录 | 📈 | /pages/growth-main/index | ✅ |
+| wish | 心愿 | ⭐ | /pages/wishes/index | ✅ |
+| medal | 勋章 | 🏅 | /pages/rewards/rewards | ✅ |
+| game | 小游戏 | 🎮 | /pages/games/list | ✅ |
+| assess | 测评 | 🧪 | /pages/assessment/apply | ✅ |
+| activity | 活动 | 🎪 | /pages/activity/index | ✅ |
+| membership | 会员中心 | 👑 | /pages/membership/index | ✅ |
+| wealth | 财富 | 💰 | /pages/wealth/index | ✅ |
+| report | 健康报告 | 🩺 | /pages/health/report-upload | ❌ 已在首页 |
+| article | 文章中心 | 📖 | /pages/article-center/index | ❌ 已在首页(推荐阅读) |
+
+> `canShortcut=false` 的应用在应用库页面可见但**置灰 + 标注"已在首页"**,用户无法选入 6 格。
+
+**默认 6 格**(新用户 / 无配置时)= 当前硬编码的 6 个:task、interaction、tiandi、members、circle、shop。
+
+---
+
+## 6. API 设计(统一 `@PostMapping` + `Result<T>`,JWT 认证)
+
+### 6.1 `POST /api/home/app-library`
+返回应用库 + 我的快捷配置 + 频度,**合并为一次请求**(避免 N+1)。
+
+- 请求:无 body(从 JWT 取 user_id)
+- 响应 `data`:
+  ```json
+  {
+    "apps": [
+      { "key": "task", "name": "任务", "icon": "📋", "route": "/pages/tasks/tasks", "canShortcut": true }
+    ],
+    "myShortcuts": [
+      { "appKey": "task", "locked": true, "sortOrder": 0 }
+    ],
+    "usage": { "task": 12, "shop": 8 }
+  }
+  ```
+
+### 6.2 `POST /api/home/shortcut/save`
+保存用户配置(全量覆盖用户对该用户的快捷配置)。
+
+- 请求 body:`{ "items": [ { "appKey": "task", "locked": true, "sortOrder": 0 } ] }`
+- 语义:删除该用户旧的 `user_shortcut_config` 记录,批量插入新的 `items`(每个 item 一条记录)。
+- 响应:`Result<Void>`
+
+### 6.3 `POST /api/home/app/enter`
+记录进入次数。
+
+- 请求 body:`{ "appKey": "task" }`
+- 语义:`user_app_usage` 的 `enter_count` +1,更新 `last_enter_at`(不存在则插入)。
+- 响应:`Result<Void>`
+
+### 6.4 `POST /api/home/shortcut/reset`
+重置为默认(清除锁定,回到系统按频度排序)。
+
+- 请求:无 body
+- 语义:删除该用户的 `user_shortcut_config` 记录。
+- 响应:`Result<Void>`
+
+---
+
+## 7. 前端改造
+
+### 7.1 首页快捷区(`pages/index-home/index.vue`)
+
+- 将 6 个硬编码 `<view class="quick-item">` 改为 `v-for` 动态渲染,数据来自 `POST /api/home/app-library`。
+- 快捷区右上角加"编辑"小图标 → 跳转应用库配置页 `/pages/home/app-library`。
+- 快捷区点击某个应用 → 先 `navTo(route)`,再调用 `POST /api/home/app/enter` 记录频度(注意:频度计数是"任意入口",故记录逻辑放在各应用入口页 onLoad,而非仅首页点击处,见 7.4)。
+
+### 7.2 应用库配置页(新增 `pages/home/app-library`)
+
+- 在 `pages.json` 注册(复用或新建分包 `pages/home`)。
+- 页面结构:
+  1. 顶部:**当前首页快捷区**(6 格预览),支持:
+     - 添加/移除应用(点击应用库里的应用切换选中态)
+     - 拖动/上移下移调整锁定应用顺序(小程序用「长按排序」或「上移/下移」按钮,避免复杂拖拽)
+     - 每个锁定应用显示"🔒 已锁定"标识
+  2. 中部:**应用库网格**,展示所有应用(图标 + 名称 + 进入次数),`canShortcut=false` 的置灰标注"已在首页"。
+  3. 底部按钮:
+     - **保存** → `POST /api/home/shortcut/save`
+     - **一键按频度排序** → 清除锁定,回到系统自动排序(调 `POST /api/home/shortcut/reset`)
+
+### 7.3 前端 API 封装(`utils/api.js`)
+
+按 AGENTS.md 规则 2(同 URL 唯一入口)新增:
+```js
+export const getAppLibrary = () => request('/api/home/app-library', 'POST', {})
+export const saveShortcutConfig = (items) => request('/api/home/shortcut/save', 'POST', { items })
+export const reportAppEnter = (appKey) => request('/api/home/app/enter', 'POST', { appKey })
+export const resetShortcutConfig = () => request('/api/home/shortcut/reset', 'POST', {})
+```
+
+### 7.4 频度计数埋点("任意入口进入都计数")
+
+在每个应用入口页的 `onLoad`(非 onShow,避免 Tab 切换重复计数)中调用 `reportAppEnter(appKey)`。
+
+- 复用 `_onLoadFired` 模式:`onLoad` 只触发一次,天然适合计数。
+- 埋点页面 = 应用库清单中所有 `canShortcut=true` 应用的入口页(约 16 个页面),在各自 `onLoad` 里加一行 `reportAppEnter('xxx')`(静默失败,不阻塞页面)。
+
+---
+
+## 8. 首页快捷区排序算法(核心逻辑)
+
+**输入**:`myShortcuts`(含 locked 标记)+ `usage`(频度)。
+
+**输出**:6 个应用 key(有序)。
+
+```
+1. lockedApps = myShortcuts 中 locked=1 的应用,按 sort_order 升序
+2. if lockedApps.length >= 6:
+       取前 6 个
+   else:
+       remaining = 6 - lockedApps.length
+       candidates = app-library 中 canShortcut=true 且 不在 lockedApps 里的应用
+       fill = candidates 按 usage[appKey] 降序排序,取前 remaining 个
+       若候选不足 remaining,用默认顺序补齐(不足 6 格就少显示)
+   结果 = lockedApps + fill
+```
+
+> 说明:`myShortcuts` 里 `locked=0` 的应用本质上是"用户主动加入但未锁定"的,在填充阶段它们会被纳入 `candidates` 一起按频度排序,因此仍有机会显示(频度高时),也可能被更高频应用挤掉。
+
+---
+
+## 9. 后端实现要点
+
+### 9.1 文件
+
+| 文件 | 说明 |
+|------|------|
+| `entity/UserShortcutConfig.java` | 实体,`@TableName("user_shortcut_config")` |
+| `entity/UserAppUsage.java` | 实体,`@TableName("user_app_usage")` |
+| `mapper/UserShortcutConfigMapper.java` | MyBatis-Plus Mapper |
+| `mapper/UserAppUsageMapper.java` | MyBatis-Plus Mapper |
+| `controller/HomeController.java` | 4 个接口 |
+| `service/HomeShortcutService.java`(或并入 controller) | 业务逻辑 |
+
+### 9.2 迁移(`DatabaseInitializer.runMigrations()`)
+
+新增迁移编号 **327**,建两张表(幂等,`CREATE TABLE IF NOT EXISTS`),并同步 `schema.sql`:
+
+```sql
+CREATE TABLE IF NOT EXISTS user_shortcut_config (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  user_id BIGINT NOT NULL,
+  app_key VARCHAR(64) NOT NULL,
+  locked TINYINT DEFAULT 0,
+  sort_order INT DEFAULT 0,
+  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  UNIQUE KEY uk_user_app (user_id, app_key)
+);
+
+CREATE TABLE IF NOT EXISTS user_app_usage (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  user_id BIGINT NOT NULL,
+  app_key VARCHAR(64) NOT NULL,
+  enter_count INT DEFAULT 0,
+  last_enter_at DATETIME,
+  UNIQUE KEY uk_user_app (user_id, app_key)
+);
+```
+
+### 9.3 规范遵循
+
+- `@PostMapping`、`Result<T>`、`@Resource` DI、JWT 认证(拦截 `/api/**`)。
+- Controller 内手动校验 `@RequestAttribute("role")`(所有登录角色可用,无需角色限制)。
+- Bean 命名无冲突(`HomeController` 需确认不与现有类重名)。
+
+---
+
+## 10. 边界情况
+
+1. **锁定数 > 6**:取 `sort_order` 前 6 个锁定应用。
+2. **候选应用不足 6 个**:少显示即可(不报错)。
+3. **频度全为 0**:用默认 6 个顺序。
+4. **并发计数**:`app/enter` 用 `UPDATE ... SET enter_count = enter_count + 1` 原子更新,避免覆盖。
+5. **重复上报**:`onLoad` 只触发一次,天然去重;后端对同 key 同用户做 upsert。
+6. **换设备**:配置与频度都在服务端,登录后同步。
+
+---
+
+## 11. 测试
+
+- **后端单测**:`HomeShortcutService` 排序算法(锁定占位 + 频度填充、锁定>6、候选不足)。
+- **前端**:`utils/app-library.js` 导出结构校验;首页快捷区 `v-for` 渲染(含空态)。
+- **CI 门禁**:`node scripts/audit-duplicate-api-calls.js` 通过(新页面 onLoad 埋点不重复打同一 URL)。
+
+---
+
+## 12. 交付范围
+
+- 后端:2 张表迁移 + 2 实体 + 2 Mapper + 1 Controller + 排序逻辑。
+- 前端:`utils/app-library.js` + `utils/api.js` 4 个封装 + 首页改造 + 应用库配置页 + 16 个入口页埋点。
+- 不涉及:运营后台动态维护应用库(YAGNI,后续有需求再扩展)。