feat(auth): 支持通过小程序码「绑定小程序」到当前账号
- WxQrSessionStore 支持 bind_user_id(绑定态)+message; bind_user() 访问器 - POST /auth/mp-qr/bind-start(当前账号发起,返小程序码); confirm 检测绑定态: 把openid绑到目标账号(若该微信属另一账号则按手机号/微信合并) - poll 返回 message(bound); repositories 加 set_wx_mini_openid
This commit is contained in:
+49
-4
@@ -777,13 +777,38 @@ async def mp_qr_start(request: Request, db: Database = Depends(get_db)):
|
||||
async def mp_qr_poll(scene: str):
|
||||
result = wx_qr_store.poll(scene)
|
||||
if result["status"] == "done":
|
||||
return {"status": "done", "token": result["token"], "profile": result["profile"]}
|
||||
return {"status": result["status"]}
|
||||
return {"status": "done", "token": result["token"], "profile": result["profile"], "message": result["message"]}
|
||||
return {"status": result["status"], "message": ""}
|
||||
|
||||
|
||||
@router.post("/mp-qr/confirm", summary="小程序内确认扫码登录")
|
||||
@router.get("/mp-qr/bind-start", summary="发起「绑定小程序」")
|
||||
async def mp_qr_bind_start(request: Request, db: Database = Depends(get_db), user: dict = Depends(get_current_user)):
|
||||
"""把当前账号与小程序(微信身份)绑定:返回小程序码;微信扫码自动打开小程序确认页。
|
||||
|
||||
与登录不同,这是「绑定」——确认后把该微信身份挂到**当前账号**(若该微信已属另一账号则合并)。
|
||||
"""
|
||||
if not config.AUTH_WECHAT_LOGIN:
|
||||
raise HTTPException(status_code=503, detail="小程序登录未开启")
|
||||
if not config.AUTH_ENABLED:
|
||||
raise HTTPException(status_code=403, detail="认证未开启")
|
||||
scene = f"mp_{secrets.token_hex(10)}"
|
||||
wx_qr_store.start(scene, bind_user_id=user["id"])
|
||||
try:
|
||||
png = await wechat.get_wxacode(scene, page="pages/scan-login/index")
|
||||
b64 = base64.b64encode(png).decode("ascii")
|
||||
return {"scene": scene, "qr_image": f"data:image/png;base64,{b64}", "mp_enabled": True}
|
||||
except wechat.WechatError:
|
||||
qr_url = f"https://opc.pinesound.cn/mp/bind?scene={scene}"
|
||||
return {"scene": scene, "qr_url": qr_url, "mp_enabled": False}
|
||||
|
||||
|
||||
@router.post("/mp-qr/confirm", summary="小程序内确认扫码登录/绑定")
|
||||
async def mp_qr_confirm(request: Request, req: MpQrConfirmRequest, db: Database = Depends(get_db)):
|
||||
"""小程序内 wx.login 后调用:以 scene 关联 web 端会话,回写已登录令牌。"""
|
||||
"""小程序内 wx.login 后调用:以 scene 关联 Web 端会话。
|
||||
|
||||
- 登录态(scene 无 bind_user_id):复用登录换取令牌回写 scene;
|
||||
- 绑定态(scene 有 bind_user_id):把该微信身份绑到 bind_user_id 账号(若该微信属另一账号则合并)。
|
||||
"""
|
||||
if not config.AUTH_ENABLED:
|
||||
raise HTTPException(status_code=403, detail="认证未开启")
|
||||
try:
|
||||
@@ -791,6 +816,26 @@ async def mp_qr_confirm(request: Request, req: MpQrConfirmRequest, db: Database
|
||||
except wechat.WechatError as exc:
|
||||
raise HTTPException(status_code=401, detail=str(exc))
|
||||
source = _client_source(request, "mini_program", "wx_openid")
|
||||
|
||||
# 绑定态:把 openid 绑到 scene 记录的目标账号
|
||||
bind_user_id = wx_qr_store.bind_user(req.scene)
|
||||
if bind_user_id:
|
||||
target = await db.users.get_by_id(bind_user_id)
|
||||
if target is None:
|
||||
raise HTTPException(status_code=404, detail="目标账号不存在,请重新发起绑定")
|
||||
owner = await db.users.get_by_wx_mini_openid(openid)
|
||||
if owner and owner["id"] != target["id"]:
|
||||
# 该微信身份已属另一账号 → 合并到当前账号(同一账号不新建)
|
||||
merged = await db.users.merge_accounts(target["id"], owner["id"])
|
||||
if not merged or not merged.get("wx_mini_openid"):
|
||||
merged = await db.users.set_wx_mini_openid(target["id"], openid)
|
||||
target = merged
|
||||
elif not (target.get("wx_mini_openid") or ""):
|
||||
target = await db.users.set_wx_mini_openid(target["id"], openid)
|
||||
wx_qr_store.complete(req.scene, message="bound")
|
||||
return {"ok": True, "bound": True, "username": (target or {}).get("username", "")}
|
||||
|
||||
# 登录态:登录换令牌回写 scene
|
||||
user = await db.users.get_by_wx_mini_openid(openid)
|
||||
if user is None:
|
||||
user = await db.users.create(
|
||||
|
||||
Reference in New Issue
Block a user