diff --git a/app/training/main.py b/app/training/main.py index 86e59a8..5df686d 100644 --- a/app/training/main.py +++ b/app/training/main.py @@ -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 行的 username(JWT 断言可能缺失,且保证与 /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)