refactor(training): 用户识别支持手机号身份并规范同步旁路注释

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Pine
2026-08-30 22:29:44 +08:00
parent 7493383f6a
commit 9f57ef25d6
+46 -36
View File
@@ -24,7 +24,6 @@ from contextlib import asynccontextmanager
from ..infrastructure.db import AsyncSessionLocal
from ..infrastructure.repositories import Database
from ..services.task_service import TaskService
from ..api.routers.auth import _ensure_opc_identity
from ..jwt import decode_access_token
from ..services import sms as platform_sms
@@ -154,7 +153,11 @@ def require_auth(authorization: str):
def _current_user(payload: dict) -> dict | None:
"""按平台 JWT( sub/username )取平台 users 行(统一账号源)。"""
"""按平台 JWT( sub/username )取平台 users 行(统一账号源)。
⚠️ 同步旁路(直连 sqlite),仅限确认与平台同库的场景;业务端点一律用
`_current_user_async`(平台 Database,与 /auth 同源 DATABASE_URL)。
"""
u = None
if payload.get("sub"):
u = db.fetch_one("users", id=payload["sub"])
@@ -163,6 +166,25 @@ def _current_user(payload: dict) -> dict | None:
return u
async def _current_user_async(payload: dict) -> dict | None:
"""按平台 JWT( sub/username )取平台 users 行——走平台 Database(与 /auth 同源)。
旧实现直连 sqlite `serverdata/data/app.db`,与平台 `DATABASE_URL`(可配
MySQL)不同源,平台侧创建/合并的账号(如运营端绑定账号)在此查不到 →
报名/接单等全部 401「登录状态异常」。统一改走平台仓储。
"""
pdb = Database()
try:
u = None
if payload.get("sub"):
u = await pdb.users.get_by_id(payload["sub"])
if u is None and payload.get("username"):
u = await pdb.users.get_by_username(payload["username"])
return u
finally:
await pdb.close()
# ================= 工具 =================
def read_answers(body: dict, key="answers"):
a = body.get(key)
@@ -258,26 +280,17 @@ async def tasks_claim_by_code(request: Request):
async with AsyncSessionLocal() as session:
pdb = Database(session=session)
payload = require_auth(auth)
username = payload.get("username") or payload.get("sub") or ""
acct = await asyncio.to_thread(db.fetch_one, "accounts", username=username)
if not acct:
# 统一账号:直接按平台 JWT 定位 users 行(accounts 表已废弃清空,勿再查)。
user = await _current_user_async(payload)
if not user:
raise HTTPException(status_code=401, detail="请先登录")
key = (acct.get("phone") or "").strip() or username
user = await pdb.users.get_by_username(key)
if user is None:
user = await pdb.users.create(
key, password=secrets.token_hex(16),
phone=(acct.get("phone") or "").strip() or "",
nickname=(acct.get("name") or "").strip() or "",
role="opc_member", source="mini_program", auth_type="phone",
)
identity = await _ensure_opc_identity(pdb, user["id"])
# 叠加制身份:OPC 为所有账号基础权限,任何角色均可领单(role=user.role)。
actor = dict(user)
actor["id"] = user["id"]
actor["username"] = user["username"]
actor["nickname"] = user.get("nickname") or user.get("username")
actor["port"] = "opc"
actor["role"] = identity.get("role") or "opc_member"
actor["role"] = user.get("role") or "opc_member"
task = await TaskService(pdb).claim(code, actor, source="scan")
return {"ok": True, "task": task}
@@ -289,14 +302,10 @@ async def tasks_my_tasks(request: Request):
async with AsyncSessionLocal() as session:
pdb = Database(session=session)
payload = require_auth(auth)
username = payload.get("username") or payload.get("sub") or ""
acct = await asyncio.to_thread(db.fetch_one, "accounts", username=username)
if not acct:
# 统一账号:按平台 JWT 定位 users 行(accounts 表已废弃清空,勿再查)。
user = await _current_user_async(payload)
if not user:
raise HTTPException(status_code=401, detail="请先登录")
key = (acct.get("phone") or "").strip() or username
user = await pdb.users.get_by_username(key)
if user is None:
return {"items": []}
items = await pdb.tasks.list(status="claimed") + await pdb.tasks.list(status="doing")
mine = [t for t in items if t.get("claimed_by") == user["id"]]
return {"items": mine}
@@ -404,7 +413,7 @@ async def wx_login():
@app.get("/api/auth/me")
async def me(authorization: str = Header(default="")):
payload = require_auth(authorization)
u = await asyncio.to_thread(_current_user, payload)
u = await _current_user_async(payload)
if not u:
raise HTTPException(404, "账号不存在")
return {"ok": True, "user": _user_payload(u)}
@@ -415,7 +424,7 @@ async def update_profile(req: Request, authorization: str = Header(default="")):
"""更新当前用户资料:昵称/头像 + 报名资料(status/topics/source"""
payload = require_auth(authorization)
b = await req.json()
u = await asyncio.to_thread(_current_user, payload)
u = await _current_user_async(payload)
if not u:
raise HTTPException(404, "账号不存在")
patch = {}
@@ -432,7 +441,7 @@ async def update_profile(req: Request, authorization: str = Header(default="")):
patch["topics"] = json.dumps(b["topics"], ensure_ascii=False) if isinstance(b["topics"], list) else str(b["topics"])
if patch:
await asyncio.to_thread(db.update_row, "users", u["id"], patch)
u = await asyncio.to_thread(_current_user, payload)
u = await _current_user_async(payload)
return {"ok": True, "user": _user_payload(u)}
@@ -458,7 +467,7 @@ async def wx_phone(req: Request, authorization: str = Header(default="")):
phone = (data.get("phone_info") or {}).get("purePhoneNumber", "")
if not phone:
raise HTTPException(400, "未获取到手机号")
u = await asyncio.to_thread(_current_user, payload)
u = await _current_user_async(payload)
if not u:
raise HTTPException(404, "账号不存在")
await asyncio.to_thread(_set_user_phone, u["username"], phone)
@@ -475,7 +484,7 @@ async def bind_phone(req: Request, authorization: str = Header(default="")):
raise HTTPException(400, "请输入正确的 11 位手机号")
if not platform_sms.verify(phone, code):
raise HTTPException(401, "验证码错误或已过期")
u = await asyncio.to_thread(_current_user, payload)
u = await _current_user_async(payload)
if not u:
raise HTTPException(404, "账号不存在")
await asyncio.to_thread(_set_user_phone, u["username"], phone)
@@ -636,14 +645,15 @@ async def create_booking(req: Request, authorization: str = Header(default="")):
# 强制登录:必须携带有效 token(无登录 → 401)
payload = require_auth(authorization)
acct = await asyncio.to_thread(_current_user, payload)
acct = await _current_user_async(payload)
if not acct:
raise HTTPException(401, "登录状态异常,请重新登录")
# 报名必须已绑定手机号(登录后自动/引导绑定),保证报名有联系方式
if not acct.get("phone"):
raise HTTPException(400, "请先绑定手机号后再报名")
username = payload["username"]
# 关联键取平台 users 行的 usernameJWT 断言可能缺失,且保证与 /api/bookings/mine 等一致)
username = acct.get("username") or payload.get("username") or ""
# 报名资料(姓名/状态/主题/来源)取账号中已保存的个人中心设置,报名区无需再填;question 按场次从请求体取
name = (acct.get("nickname") or "") or (str(b.get("name", "")).strip() or username)
status_label = (acct.get("opc_status") or "") or str(b.get("status", "") or "").strip()
@@ -678,7 +688,7 @@ def _decode_bk(row):
@app.get("/api/bookings/mine")
async def my_bookings(authorization: str = Header(default="")):
payload = require_auth(authorization)
acct = await asyncio.to_thread(_current_user, payload)
acct = await _current_user_async(payload)
ids = {payload["username"]}
if acct and acct.get("phone"):
ids.add(acct["phone"])
@@ -716,7 +726,7 @@ async def my_bookings(authorization: str = Header(default="")):
async def my_booking_for_event(event_id: str, authorization: str = Header(default="")):
"""查当前登录用户是否已报名该活动及其审核状态(详情页判断用)"""
payload = require_auth(authorization)
acct = await asyncio.to_thread(_current_user, payload)
acct = await _current_user_async(payload)
ids = {payload["username"]}
if acct and acct.get("phone"):
ids.add(acct["phone"])
@@ -738,7 +748,7 @@ async def my_booking_for_event(event_id: str, authorization: str = Header(defaul
async def checkin(req: Request, authorization: str = Header(default="")):
payload = require_auth(authorization)
b = await req.json()
acct = await asyncio.to_thread(_current_user, payload)
acct = await _current_user_async(payload)
ids = {payload["username"]}
if acct and acct.get("phone"):
ids.add(acct["phone"])
@@ -1045,7 +1055,7 @@ async def park_admission_submit(request: Request, authorization: str = Header(de
if docs:
from ..infrastructure.oss import to_object_path
docs = {k: to_object_path(v) for k, v in docs.items()}
u = _current_user(auth)
u = await _current_user_async(auth)
now = now_iso()
rec = {
"id": f"adm_{secrets.token_hex(8)}",
@@ -1082,7 +1092,7 @@ async def park_admission_mine(authorization: str = Header(default="")):
async def park_admission_list(authorization: str = Header(default="")):
"""运营方:全部入驻申请(审核列表)。"""
auth = require_auth(authorization)
u = _current_user(auth)
u = await _current_user_async(auth)
if not u or u.get("role") != "operator":
raise HTTPException(403, "仅运营方可查看")
items = [_admission_view(i) for i in db.list_all("park_admissions")]
@@ -1094,7 +1104,7 @@ async def park_admission_list(authorization: str = Header(default="")):
async def park_admission_review(aid: str, request: Request, authorization: str = Header(default="")):
"""运营方:审核入驻申请(approved/rejected/reviewing + 意见)。"""
auth = require_auth(authorization)
u = _current_user(auth)
u = await _current_user_async(auth)
if not u or u.get("role") != "operator":
raise HTTPException(403, "仅运营方可审核")
target = db.fetch_by_id("park_admissions", aid)