Files
shaguabijia-app-server/docs/database/onboarding_completion.md
T
zzhyyyyy dacb37e3e1 feat(onboarding): 新手引导完成按 设备+账号 去重(表/模型/仓储/端点/迁移/测试)
- 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>
2026-06-10 18:32:51 +08:00

4.4 KiB

onboarding_completion — 新手引导完成标记(按 设备 + 账号 去重)

模型 app/models/onboarding.py · 仓库 app/repositories/onboarding.py · 迁移 alembic/versions/onboarding_completion_table.py(revision onboarding_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-install DeviceId.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_iduser.id(多对一:一账号在 N 台设备各一条)。无硬 FK 约束(同 coupon_claim_record / coupon_prompt_engagement),靠业务字段对齐。
  • 判断维度 = (user_id, device_id) 组合等值查。
  • device_id 与客户端 DeviceId.hardwareId()(ANDROID_ID)同源;与领券侧 per-install device_id 互通。

索引与约束

  • PK id;index user_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)。