mini_auth.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. """小程序鉴权:微信登录 / 绑定 openid。"""
  2. from __future__ import annotations
  3. from datetime import datetime
  4. from fastapi import APIRouter, Depends, HTTPException, Request
  5. from sqlalchemy.orm import Session
  6. from app.core.auth import get_current_user_by_token
  7. from app.core.database import get_db
  8. from app.core.response import error_response, success_response
  9. from app.crud.user import user_crud
  10. from app.services.issue_user_tokens import issue_tokens_for_user
  11. from app.services.wechat_login_service import (
  12. resolve_wechat_login_by_openid_only,
  13. resolve_wechat_login_user,
  14. )
  15. from app.services.wechat_mini_auth import (
  16. WechatMiniAuthError,
  17. code2session,
  18. get_phone_number,
  19. )
  20. router = APIRouter(prefix="/mini/auth", tags=["小程序鉴权"])
  21. async def _resolve_user_id(request: Request, db: Session) -> str:
  22. authorization = request.headers.get("Authorization", "")
  23. if not authorization.startswith("Bearer "):
  24. raise HTTPException(status_code=401, detail="未授权")
  25. token = authorization.replace("Bearer ", "")
  26. current = await get_current_user_by_token(token=token, db=db)
  27. user_id = current.get("user_id") or current.get("sub")
  28. if not user_id:
  29. raise HTTPException(status_code=401, detail="未授权")
  30. return user_id
  31. @router.post("/wechat-login", response_model=dict)
  32. async def wechat_login(body: dict, db: Session = Depends(get_db)):
  33. """微信登录:openid 已绑定时可静默换票;否则须授权手机号且与账号 phone_no 一致。"""
  34. login_code = (body.get("loginCode") or "").strip()
  35. phone_code = (body.get("phoneCode") or "").strip() or None
  36. try:
  37. sess = code2session(login_code)
  38. except WechatMiniAuthError as exc:
  39. return error_response(400, str(exc))
  40. if not phone_code:
  41. user, err_msg, err_code = resolve_wechat_login_by_openid_only(
  42. db, openid=sess["openid"]
  43. )
  44. if err_msg:
  45. return error_response(err_code or 400, err_msg)
  46. else:
  47. try:
  48. phone = get_phone_number(phone_code)
  49. except WechatMiniAuthError as exc:
  50. return error_response(400, str(exc))
  51. user, err_msg, err_code = resolve_wechat_login_user(
  52. db, openid=sess["openid"], phone=phone
  53. )
  54. if err_msg:
  55. return error_response(err_code or 400, err_msg)
  56. unionid = (sess.get("unionid") or "").strip()
  57. if unionid and not (getattr(user, "wx_unionid", None) or "").strip():
  58. user.wx_unionid = unionid
  59. user.update_time = datetime.now()
  60. db.add(user)
  61. db.commit()
  62. db.refresh(user)
  63. try:
  64. data = await issue_tokens_for_user(db, user)
  65. except ValueError as exc:
  66. return error_response(403, str(exc))
  67. return success_response(data)
  68. @router.post("/wechat-bind", response_model=dict)
  69. async def wechat_bind(
  70. request: Request,
  71. body: dict,
  72. db: Session = Depends(get_db),
  73. ):
  74. """密码登录后绑定当前微信 openid(需 Bearer)。"""
  75. try:
  76. user_id = await _resolve_user_id(request, db)
  77. except HTTPException:
  78. return error_response(401, "未授权")
  79. login_code = (body.get("loginCode") or "").strip()
  80. try:
  81. sess = code2session(login_code)
  82. except WechatMiniAuthError as exc:
  83. return error_response(400, str(exc))
  84. openid = sess["openid"]
  85. user = user_crud.get(db, user_id)
  86. if not user or user.del_flag != 0:
  87. return error_response(401, "用户不存在")
  88. other = user_crud.get_by_wx_openid(db, openid)
  89. if other and other.id != user.id:
  90. return error_response(409, "该微信已绑定其他账号")
  91. existing_oid = (getattr(user, "wx_openid", None) or "").strip()
  92. if existing_oid == openid:
  93. return success_response({"bound": True, "message": "已绑定"})
  94. if existing_oid and existing_oid != openid:
  95. return error_response(409, "该账号已绑定其他微信,请联系管理员")
  96. user.wx_openid = openid
  97. user.update_time = datetime.now()
  98. db.add(user)
  99. db.commit()
  100. return success_response({"bound": True, "message": "绑定成功"})