Files
zkxw/backend/app/api/mini_auth.py
T

233 lines
8.3 KiB
Python

"""小程序鉴权:微信登录 / 绑定 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": "电脑端即将登录,可关闭本页"})