dacb37e3e1
- onboarding_completion 表(user_id+device_id 唯一约束)+ model + repository - POST /api/v1/user/onboarding/complete 标记完成;登录响应 TokenWithUser.onboarding_completed (按登录请求带的 device_id 判定是否已走过引导,跨卸载重装稳定) - alembic 迁移 onboarding_completion(rebase 到最新 main 后已链在 coupon_daily_completion 之后,单 head) - docs/database 文档 + tests/test_onboarding.py + run.bat(本地启动脚本) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.4 KiB
4.4 KiB
onboarding_completion — 新手引导完成标记(按 设备 + 账号 去重)
模型
app/models/onboarding.py· 仓库app/repositories/onboarding.py· 迁移alembic/versions/onboarding_completion_table.py(revisiononboarding_completion) · 读POST /api/v1/auth/jverify-login·/sms/login· 写POST /api/v1/user/onboarding/complete· 测试tests/test_onboarding.py· ← 索引 · 总览
记录「某账号在某台设备上已走完新手引导」,一条 (user_id, device_id)。用途单一:决定登录后要不要再展示新手引导教程——本表有这条 → 跳过引导直接进首页;没有 → 走引导。
为什么要落后端:此前「引导只跑一次」只存客户端本地 OnboardingPrefs(SharedPreferences),卸载重装即丢 → 重装会重弹引导。产品需求是「同设备 + 同账号只触发一次,且跨卸载重装」,本地存不住,故把"真相"放到服务端,按 账号 + 硬件设备号 去重。
去重维度 = 设备 + 账号(产品决议):
| 场景 | 命中本表? | 是否再展示引导 |
|---|---|---|
| 新账号(从未走过) | 否 | ✅ 展示 |
| 同账号 + 同设备(卸载重装 / 退登重登) | 是 | ❌ 跳过 |
| 同账号 + 换新设备 | 否 | ✅ 展示(新机需重新授权悬浮窗/无障碍等) |
⚠️
device_id用客户端硬件级稳定标识Settings.Secure.ANDROID_ID(util/DeviceId.hardwareId(),同签名 app 卸载重装不变、仅恢复出厂重置),不是领券/比价用的 per-installDeviceId.get()(存 SP、重装会变)。两套 device_id 不互通,别混用。
用在哪 / 增删改查
- R(读):登录
POST /auth/jverify-login、/auth/sms/login—— 客户端在登录请求体里带device_id,onboarding_repo.is_completed(user_id, device_id)查本表有无行,结果塞进登录响应TokenWithUser.onboarding_completed(true=已完成)。客户端AuthRepository.persist据此回写本地OnboardingPrefs(只回写 true),既有 gating(OnboardingHost/AppNavHost)逻辑不变。 - C(插入):走完引导最后一步(后台耗电屏)时,客户端
POST /api/v1/user/onboarding/complete(带 Bearer +device_id)→onboarding_repo.mark_completed插一行。幂等:同(user_id, device_id)已存在则撞唯一约束,IntegrityError被 rollback 吞掉、视为成功;device_id为空一律忽略不写。best-effort:客户端上报失败不阻断进首页(本地已先标记)。 - U / D:无。一台设备 + 一账号一条,完成即永久;账号注销(软删
user)不清本表(留着无害——user_id 指向的是已deleted的行;同手机号重新注册是新 user.id,自然重新走引导)。
字段
| 列 | 类型 | 约束 / 默认 | 说明(取值 / join) |
|---|---|---|---|
id |
Integer | PK, autoincrement | |
user_id |
Integer | index, NOT NULL | 归属账号 → user.id(语义外键,未建 DB 级 FK,同 coupon_* 设备表) |
device_id |
String(64) | NOT NULL | 硬件级设备标识 = 客户端 Settings.Secure.ANDROID_ID;卸载重装不变 |
completed_at |
DateTime(tz) | server_default now() | 标记完成的时间 |
关系 / Join Key
user_id→user.id(多对一:一账号在 N 台设备各一条)。无硬 FK 约束(同coupon_claim_record/coupon_prompt_engagement),靠业务字段对齐。- 判断维度 =
(user_id, device_id)组合等值查。 device_id与客户端DeviceId.hardwareId()(ANDROID_ID)同源;不与领券侧 per-installdevice_id互通。
索引与约束
- PK
id;indexuser_id(ix_onboarding_completion_user_id);UNIQUE(user_id,device_id) =uq_onboarding_user_device(防同账号同设备重复写 + 兜并发 upsert)。
注意
- 去重维度是「设备 + 账号」,不是纯账号:同一个人在新手机上登录会重新看引导(产品预期:新机要重新授权权限)。若日后想改「纯账号、换机也不弹」,把
is_completed/mark_completed去掉device_id维度即可。 device_id取不到 / 为已知脏值(9774d56d682e549c)时客户端退回 per-install id,这类极少数设备重装会重弹一次(尽力而为)。- 本地
OnboardingPrefs现仅作同安装快速跳过缓存;真相以本表为准,每次登录响应同步。 - 客户端实现:
ui/onboarding/OnboardingViewModel.kt(上报)、data/auth/AuthRepository.kt(读 + 同步本地)、util/DeviceId.kt(ANDROID_ID)。