| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- """小程序鉴权:微信登录 / 绑定 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.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})
|