feat(auth): 平台成唯一账号源——小程序微信登录进 users + OPC 报名资料字段
统一用户账号体系(阶段1,非破坏): - UserRepository 新增 get_by_wx_mini_openid。 - /auth/wx-login:小程序登录(X-Client=miniprogram)按 wx_mini_openid 建/复用 users 账号(role=opc_member, source=mini_program, auth_type=wx_openid), 并带 nickName/avatarUrl;非小程序仍走 wx_openid。 - /auth/me + 登录响应补齐小程序契约字段 name/phone/phoneBound。 - users 增 opc_status/topics 列(alembic 0010);desktop/web profile 也输出 name/phone/phoneBound/status/topics,且 update-profile 支持 name/status/topics 映射。 - scripts/db/seed.py 增 accounts→users 幂等迁移:旧小程序账号并入全局 users (wx_mini_openid/phone 关联 + opc_member 身份),由用户执行 migrate+seed。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+42
-4
@@ -7,6 +7,7 @@
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import secrets
|
||||
|
||||
@@ -36,6 +37,21 @@ from ...services import sms, wechat
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
def _parse_topics(raw) -> list:
|
||||
"""topics(JSON 文本/list) → list;失败兜底 []。"""
|
||||
if not raw:
|
||||
return []
|
||||
if isinstance(raw, list):
|
||||
return raw
|
||||
if isinstance(raw, str):
|
||||
try:
|
||||
v = json.loads(raw)
|
||||
return v if isinstance(v, list) else []
|
||||
except (TypeError, ValueError):
|
||||
return []
|
||||
return []
|
||||
|
||||
|
||||
# 用户来源(X-Client 头,回退 body.source;未指认则按 auth_type 推断)
|
||||
_SOURCE_BY_CLIENT = {
|
||||
"miniprogram": "mini_program", "mini_program": "mini_program", "mp": "mini_program",
|
||||
@@ -147,6 +163,11 @@ async def _profile_for(
|
||||
region_id = (identity or user).get("region_id")
|
||||
profile = {f: user.get(f, "") for f in _PROFILE_FIELDS}
|
||||
profile.update(username=user.get("username", ""))
|
||||
profile["name"] = user.get("nickname") or user.get("username", "")
|
||||
profile["phone"] = user.get("phone", "")
|
||||
profile["phoneBound"] = bool(user.get("phone", ""))
|
||||
profile["status"] = user.get("opc_status", "")
|
||||
profile["topics"] = _parse_topics(user.get("topics", ""))
|
||||
profile["source"] = user.get("source", "")
|
||||
profile["auth_type"] = user.get("auth_type", "")
|
||||
profile["company"] = user.get("company", "")
|
||||
@@ -312,6 +333,14 @@ async def update_profile(
|
||||
raise HTTPException(status_code=400, detail="Password cannot be empty")
|
||||
|
||||
profile_updates = {k: payload[k] for k in _PROFILE_FIELDS if k in payload}
|
||||
# 小程序契约字段 → users 列映射(name→nickname,status→opc_status,topics→JSON文本)
|
||||
if "name" in payload:
|
||||
profile_updates["nickname"] = payload["name"]
|
||||
if "status" in payload:
|
||||
profile_updates["opc_status"] = payload["status"]
|
||||
if "topics" in payload:
|
||||
t = payload["topics"]
|
||||
profile_updates["topics"] = json.dumps(t, ensure_ascii=False) if isinstance(t, list) else (t or "")
|
||||
changing_credentials = new_username is not None or new_password is not None
|
||||
if not profile_updates and not changing_credentials:
|
||||
raise HTTPException(status_code=400, detail="Nothing to update")
|
||||
@@ -467,13 +496,22 @@ async def wx_login(req: WxLoginRequest, request: Request, db: Database = Depends
|
||||
except wechat.WechatError as exc:
|
||||
raise HTTPException(status_code=401, detail=str(exc))
|
||||
|
||||
user = await db.users.get_by_wx_openid(openid)
|
||||
source = _client_source(request, getattr(req, "source", ""), "wx_openid")
|
||||
is_mini = source == "mini_program"
|
||||
# 小程序登录存 wx_mini_openid(与网页/公众号 openid 不同);否则存 wx_openid
|
||||
if is_mini:
|
||||
user = await db.users.get_by_wx_mini_openid(openid) or await db.users.get_by_wx_openid(openid)
|
||||
else:
|
||||
user = await db.users.get_by_wx_openid(openid)
|
||||
if user is None:
|
||||
user = await db.users.create(
|
||||
f"wx_{openid[:24]}", password=secrets.token_hex(16),
|
||||
wx_openid=openid, role="opc_member",
|
||||
source=_client_source(request, getattr(req, "source", ""), "wx_openid"),
|
||||
auth_type="wx_openid", register_ip=_client_ip(request),
|
||||
wx_openid=openid if not is_mini else "",
|
||||
wx_mini_openid=openid if is_mini else "",
|
||||
nickname=str(getattr(req, "nickName", "") or ""),
|
||||
avatar=str(getattr(req, "avatarUrl", "") or ""),
|
||||
role="opc_member", source=source, auth_type="wx_openid",
|
||||
register_ip=_client_ip(request),
|
||||
)
|
||||
if user.get("status") != "active":
|
||||
raise HTTPException(status_code=403, detail="账号已禁用")
|
||||
|
||||
@@ -39,6 +39,10 @@ class UpdateProfileRequest(BaseModel):
|
||||
expires_in: int | None = None
|
||||
# 演示用户资料字段(账户管理页可编辑)
|
||||
nickname: str | None = None
|
||||
# 小程序契约字段(name→nickname;status→opc_status;topics→JSON 列表)
|
||||
name: str | None = None
|
||||
status: str | None = None
|
||||
topics: list[str] | None = None
|
||||
account: str | None = None
|
||||
company: str | None = None
|
||||
room: str | None = None
|
||||
@@ -66,6 +70,9 @@ class UserProfile(BaseModel):
|
||||
|
||||
username: str = ""
|
||||
nickname: str = ""
|
||||
name: str = "" # 别名 = nickname(小程序契约:显示名 / 报名资料姓名)
|
||||
phone: str = "" # 已绑定手机号
|
||||
phoneBound: bool = False # 是否已绑手机(小程序「已绑定手机」徽标/绑定门槛)
|
||||
account: str = ""
|
||||
company: str = ""
|
||||
room: str = ""
|
||||
@@ -122,6 +129,8 @@ class PhoneLoginRequest(BaseModel):
|
||||
|
||||
class WxLoginRequest(BaseModel):
|
||||
code: str = Field(description="wx.login 返回的 code(未配置 appid 时当作 openid 直通)")
|
||||
nickName: str = ""
|
||||
avatarUrl: str = ""
|
||||
|
||||
|
||||
class WxPhoneRequest(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user