cfc-backend(后端)+ cfc-frontend(微信小程序)首页(pages/index-home/index.vue)在"开通会员"卡片下方有一排 6 个硬编码快捷入口:任务、互动、天盘、成员、圈子、商城。
当前问题:
目标:
| 问题 | 结论 |
|---|---|
| 频度调整机制 | 自动排序但可手动覆盖:系统按频度排序,但用户手动锁定后以锁定为准 |
| 配置与频度存储 | 服务端存储(换设备同步、频度累计准确) |
| 适用人群 | 所有登录用户(非会员与会员一致) |
| 首页重复功能 | 已在首页显著位置的功能,从配置列表中排除 |
| 应用库范围 | 全部候选功能都入库 |
| 锁定的语义 | 锁定 = 置顶,不会被点击次数挤掉;但用户仍可手动移除 |
| 锁定操作入口 | 只在应用库配置页管理,首页不显示锁图标 |
| 自动填充逻辑 | 锁定占位 + 频度填满剩余 |
| 锁定应用内部排序 | 支持手动调整锁定应用之间的顺序 |
| 频度计数触发 | 任意入口进入都计数(含 TabBar 页) |
采用 混合方案:
utils/app-library.js,图标用 emoji(与现有 6 个快捷入口一致,零静态资源成本)。user_shortcut_config(用户快捷配置)+ user_app_usage(使用频度)。理由:
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的应用参与频度填充。
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)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。
@PostMapping + Result<T>,JWT 认证)POST /api/home/app-library返回应用库 + 我的快捷配置 + 频度,合并为一次请求(避免 N+1)。
响应 data:
{
"apps": [
{ "key": "task", "name": "任务", "icon": "📋", "route": "/pages/tasks/tasks", "canShortcut": true }
],
"myShortcuts": [
{ "appKey": "task", "locked": true, "sortOrder": 0 }
],
"usage": { "task": 12, "shop": 8 }
}
POST /api/home/shortcut/save保存用户配置(全量覆盖用户对该用户的快捷配置)。
{ "items": [ { "appKey": "task", "locked": true, "sortOrder": 0 } ] }user_shortcut_config 记录,批量插入新的 items(每个 item 一条记录)。Result<Void>POST /api/home/app/enter记录进入次数。
{ "appKey": "task" }user_app_usage 的 enter_count +1,更新 last_enter_at(不存在则插入)。Result<Void>POST /api/home/shortcut/reset重置为默认(清除锁定,回到系统按频度排序)。
user_shortcut_config 记录。Result<Void>pages/index-home/index.vue)<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)。pages/home/app-library)pages.json 注册(复用或新建分包 pages/home)。canShortcut=false 的置灰标注"已在首页"。POST /api/home/shortcut/savePOST /api/home/shortcut/reset)utils/api.js)按 AGENTS.md 规则 2(同 URL 唯一入口)新增:
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', {})
在每个应用入口页的 onLoad(非 onShow,避免 Tab 切换重复计数)中调用 reportAppEnter(appKey)。
_onLoadFired 模式:onLoad 只触发一次,天然适合计数。canShortcut=true 应用的入口页(约 16 个页面),在各自 onLoad 里加一行 reportAppEnter('xxx')(静默失败,不阻塞页面)。输入: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一起按频度排序,因此仍有机会显示(频度高时),也可能被更高频应用挤掉。
| 文件 | 说明 |
|---|---|
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) |
业务逻辑 |
DatabaseInitializer.runMigrations())新增迁移编号 327,建两张表(幂等,CREATE TABLE IF NOT EXISTS),并同步 schema.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)
);
@PostMapping、Result<T>、@Resource DI、JWT 认证(拦截 /api/**)。@RequestAttribute("role")(所有登录角色可用,无需角色限制)。HomeController 需确认不与现有类重名)。sort_order 前 6 个锁定应用。app/enter 用 UPDATE ... SET enter_count = enter_count + 1 原子更新,避免覆盖。onLoad 只触发一次,天然去重;后端对同 key 同用户做 upsert。HomeShortcutService 排序算法(锁定占位 + 频度填充、锁定>6、候选不足)。utils/app-library.js 导出结构校验;首页快捷区 v-for 渲染(含空态)。node scripts/audit-duplicate-api-calls.js 通过(新页面 onLoad 埋点不重复打同一 URL)。utils/app-library.js + utils/api.js 4 个封装 + 首页改造 + 应用库配置页 + 16 个入口页埋点。