# 首页快捷方式自定义 + 应用库 + 频度追踪 设计规格 - 日期: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`,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` ### 6.3 `POST /api/home/app/enter` 记录进入次数。 - 请求 body:`{ "appKey": "task" }` - 语义:`user_app_usage` 的 `enter_count` +1,更新 `last_enter_at`(不存在则插入)。 - 响应:`Result` ### 6.4 `POST /api/home/shortcut/reset` 重置为默认(清除锁定,回到系统按频度排序)。 - 请求:无 body - 语义:删除该用户的 `user_shortcut_config` 记录。 - 响应:`Result` --- ## 7. 前端改造 ### 7.1 首页快捷区(`pages/index-home/index.vue`) - 将 6 个硬编码 `` 改为 `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`、`@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,后续有需求再扩展)。