"""小程序鉴权:微信登录 / 绑定 openid。""" from __future__ import annotations from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy.orm import Session from app.core.auth import get_current_user_by_token from app.core.database import get_db from app.core.response import error_response, success_response from app.crud.user import user_crud from app.services import wx_qr_session from app.services.issue_user_tokens import issue_tokens_for_user from app.services.wechat_login_service import ( resolve_wechat_login_by_openid_only, resolve_wechat_login_user, ) from app.services.wechat_mini_auth import ( WechatMiniAuthError, code2session, get_phone_number, ) from app.services.wechat_mp_auth import WechatMpAuthError, oauth_code_to_openid router = APIRouter(prefix="/mini/auth", tags=["小程序鉴权"]) async def _resolve_user_id(request: Request, db: Session) -> str: authorization = request.headers.get("Authorization", "") if not authorization.startswith("Bearer "): raise HTTPException(status_code=401, detail="未授权") token = authorization.replace("Bearer ", "") current = await get_current_user_by_token(token=token, db=db) user_id = current.get("user_id") or current.get("sub") if not user_id: raise HTTPException(status_code=401, detail="未授权") return user_id @router.post("/wechat-login", response_model=dict) async def wechat_login(body: dict, db: Session = Depends(get_db)): """微信登录:openid 已绑定时可静默换票;否则须授权手机号且与账号 phone_no 一致。""" login_code = (body.get("loginCode") or "").strip() phone_code = (body.get("phoneCode") or "").strip() or None try: sess = code2session(login_code) except WechatMiniAuthError as exc: return error_response(400, str(exc)) if not phone_code: user, err_msg, err_code = resolve_wechat_login_by_openid_only( db, openid=sess["openid"] ) if err_msg: return error_response(err_code or 400, err_msg) else: try: phone = get_phone_number(phone_code) except WechatMiniAuthError as exc: return error_response(400, str(exc)) user, err_msg, err_code = resolve_wechat_login_user( db, openid=sess["openid"], phone=phone ) if err_msg: return error_response(err_code or 400, err_msg) unionid = (sess.get("unionid") or "").strip() if unionid and not (getattr(user, "wx_unionid", None) or "").strip(): user.wx_unionid = unionid user.update_time = datetime.now() db.add(user) db.commit() db.refresh(user) try: data = await issue_tokens_for_user(db, user) except ValueError as exc: return error_response(403, str(exc)) return success_response(data) @router.post("/wechat-bind", response_model=dict) async def wechat_bind( request: Request, body: dict, db: Session = Depends(get_db), ): """密码登录后绑定当前微信 openid(需 Bearer)。""" try: user_id = await _resolve_user_id(request, db) except HTTPException: return error_response(401, "未授权") login_code = (body.get("loginCode") or "").strip() try: sess = code2session(login_code) except WechatMiniAuthError as exc: return error_response(400, str(exc)) openid = sess["openid"] user = user_crud.get(db, user_id) if not user or user.del_flag != 0: return error_response(401, "用户不存在") other = user_crud.get_by_wx_openid(db, openid) if other and other.id != user.id: return error_response(409, "该微信已绑定其他账号") existing_oid = (getattr(user, "wx_openid", None) or "").strip() if existing_oid == openid: return success_response({"bound": True, "message": "已绑定"}) if existing_oid and existing_oid != openid: return error_response(409, "该账号已绑定其他微信,请联系管理员") user.wx_openid = openid user.update_time = datetime.now() db.add(user) db.commit() return success_response({"bound": True, "message": "绑定成功"}) @router.get("/mp-bind-status", response_model=dict) async def mp_bind_status(request: Request, db: Session = Depends(get_db)): """当前用户是否已绑定微信服务号 openid。""" try: user_id = await _resolve_user_id(request, db) except HTTPException: return error_response(401, "未授权") user = user_crud.get(db, user_id) bound = bool(user and (getattr(user, "wx_mp_openid", None) or "").strip()) return success_response({"bound": bound}) @router.post("/mp-bind", response_model=dict) async def mp_bind(request: Request, body: dict, db: Session = Depends(get_db)): """服务号网页授权 code 绑定当前用户(需 Bearer)。""" try: user_id = await _resolve_user_id(request, db) except HTTPException: return error_response(401, "未授权") code = (body.get("code") or "").strip() try: info = oauth_code_to_openid(code) except WechatMpAuthError as exc: return error_response(400, str(exc)) mp_openid = info["openid"] other = user_crud.get_by_wx_mp_openid(db, mp_openid) if other and other.id != user_id: return error_response(409, "该微信服务号已绑定其他账号") user = user_crud.get(db, user_id) if not user or user.del_flag != 0: return error_response(401, "用户不存在") user.wx_mp_openid = mp_openid if info.get("unionid"): user.wx_unionid = info["unionid"] user.wx_mp_bind_time = datetime.now() user.update_time = datetime.now() db.add(user) db.commit() return success_response({"bound": True}) @router.post("/web-qr-scanned", response_model=dict) async def web_qr_scanned(body: dict): raw = (body.get("sessionId") or body.get("scene") or "").strip() sid = wx_qr_session.resolve_session_id(raw) or raw data = wx_qr_session.get_session(sid) if not data or data.get("status") in ("expired", "consumed"): return error_response(400, "二维码已过期,请在电脑上刷新后重扫") wx_qr_session.mark_scanned(sid) return success_response({"ok": True}) @router.post("/web-qr-confirm", response_model=dict) async def web_qr_confirm(body: dict, db: Session = Depends(get_db)): raw = (body.get("sessionId") or body.get("scene") or "").strip() sid = wx_qr_session.resolve_session_id(raw) or raw data = wx_qr_session.get_session(sid) if not data or data.get("status") in ("expired", "consumed", "failed"): return error_response(400, "二维码已过期,请在电脑上刷新后重扫") if data.get("status") == "success": return success_response({"ok": True, "message": "电脑端即将登录"}) wx_qr_session.mark_scanned(sid) login_code = (body.get("loginCode") or "").strip() phone_code = (body.get("phoneCode") or "").strip() if not login_code or not phone_code: wx_qr_session.mark_failed(sid, "需要授权手机号才能完成扫码登录") return error_response(400, "需要授权手机号才能完成扫码登录") try: sess = code2session(login_code) phone = get_phone_number(phone_code) except WechatMiniAuthError as exc: wx_qr_session.mark_failed(sid, str(exc)) return error_response(400, str(exc)) user, err_msg, err_code = resolve_wechat_login_user( db, openid=sess["openid"], phone=phone ) if err_msg: wx_qr_session.mark_failed(sid, err_msg) return error_response(err_code or 400, err_msg) unionid = (sess.get("unionid") or "").strip() if unionid and not (getattr(user, "wx_unionid", None) or "").strip(): user.wx_unionid = unionid user.update_time = datetime.now() db.add(user) db.commit() db.refresh(user) try: tokens = await issue_tokens_for_user(db, user) except ValueError as exc: wx_qr_session.mark_failed(sid, str(exc)) return error_response(403, str(exc)) if not wx_qr_session.mark_success(sid, tokens): return error_response(400, "二维码已过期,请在电脑上刷新后重扫") return success_response({"ok": True, "message": "电脑端即将登录,可关闭本页"})