feat(onboarding): force_onboarding 改设备维度 + admin 设备管理 + status 查询

废弃 force_onboarding(运营按用户强制引导,客户端不认——被 onboarding_completed 压过),
改用 onboarding_completion(设备+账号 维度),admin 直接增删该表记录控制重走。

- 删 force_onboarding: User 列 + auth/admin schema + admin 接口 + mutations + repo
  clear + /onboarding/complete 调用 + 3 测试;新增 alembic 迁移删列
- admin 设备管理: 列设备(按 device 聚合) / 重置单设备 / 全部重置(清 onboarding_completion)
- 新增 GET /api/v1/user/onboarding/status: 客户端已登录启动查"本设备是否要重走引导"

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 00:51:47 +08:00
parent 57ddcd356b
commit e8bd12cc1f
15 changed files with 227 additions and 147 deletions
-42
View File
@@ -160,48 +160,6 @@ def test_set_user_status_and_audit(admin_client: TestClient, operator_token: str
).status_code == 200
# ===== 一键开启新手引导 =====
def test_set_force_onboarding_and_audit(admin_client: TestClient, operator_token: str) -> None:
uid = _seed_user("13900000009")
r = admin_client.post(
f"/admin/api/users/{uid}/force-onboarding", json={"enabled": True},
headers=_auth(operator_token),
)
assert r.status_code == 200, r.text
db = SessionLocal()
try:
assert db.get(User, uid).force_onboarding is True
logs = db.execute(
select(AdminAuditLog).where(
AdminAuditLog.action == "user.force_onboarding.set",
AdminAuditLog.target_id == str(uid),
)
).scalars().all()
assert logs[0].detail == {"before": False, "after": True}
finally:
db.close()
# 取消(回到 false)
assert admin_client.post(
f"/admin/api/users/{uid}/force-onboarding", json={"enabled": False},
headers=_auth(operator_token),
).status_code == 200
db = SessionLocal()
try:
assert db.get(User, uid).force_onboarding is False
finally:
db.close()
def test_finance_cannot_force_onboarding(admin_client: TestClient, finance_token: str) -> None:
uid = _seed_user("13900000010")
r = admin_client.post(
f"/admin/api/users/{uid}/force-onboarding", json={"enabled": True},
headers=_auth(finance_token),
)
assert r.status_code == 403
# ===== 角色守卫 =====
def test_operator_cannot_grant_coins(admin_client: TestClient, operator_token: str) -> None: