Ver código fonte

feat: add web auth wx-qr create and status endpoints

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 1 mês atrás
pai
commit
38b11cce35
2 arquivos alterados com 76 adições e 0 exclusões
  1. 46 0
      backend/app/api/public.py
  2. 30 0
      backend/tests/test_wx_qr_api.py

+ 46 - 0
backend/app/api/public.py

@@ -1,4 +1,5 @@
 """公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/zkxw.sql 基线)"""
+import base64
 import os
 from datetime import datetime
 from typing import Optional
@@ -39,6 +40,8 @@ from app.services.web_auth_roles import (
 from app.schemas.auth import ProfileUpdateRequest
 from app.crud.user import update_user, change_password
 from app.services.issue_user_tokens import build_user_info_payload
+from app.services import wx_qr_session
+from app.services.wechat_mini_auth import get_unlimited_wxacode, WechatMiniAuthError
 
 router = APIRouter(prefix="", tags=["公共接口"])
 
@@ -155,6 +158,49 @@ async def web_login(
     )
 
 
+@router.post("/web/auth/wx-qr/create", response_model=dict)
+async def wx_qr_create():
+    session_id, short = wx_qr_session.create_session()
+    page = (settings.WX_QR_PAGE or "pages/login/web-qr/index").strip()
+    try:
+        raw = get_unlimited_wxacode(
+            scene=short,
+            page=page,
+            env_version=settings.WX_QR_ENV_VERSION,
+        )
+    except WechatMiniAuthError as exc:
+        wx_qr_session.invalidate(session_id)
+        return error_response(400, str(exc))
+    b64 = base64.b64encode(raw).decode("ascii")
+    mime = "image/jpeg" if raw[:2] == b"\xff\xd8" else "image/png"
+    return success_response(
+        {
+            "sessionId": session_id,
+            "expiresIn": settings.WX_QR_SESSION_TTL_SEC,
+            "qrImageBase64": f"data:{mime};base64,{b64}",
+        }
+    )
+
+
+@router.get("/web/auth/wx-qr/status", response_model=dict)
+async def wx_qr_status(sessionId: str = ""):
+    sid = (sessionId or "").strip()
+    data = wx_qr_session.get_session(sid)
+    if not data:
+        return success_response({"status": "expired", "message": "二维码已过期,请刷新"})
+    status = data.get("status") or "expired"
+    if status == "success":
+        tokens = wx_qr_session.consume_success(sid)
+        if not tokens:
+            return success_response({"status": "expired", "message": "登录凭证已失效,请刷新二维码"})
+        return success_response({"status": "success", **tokens})
+    if status == "failed":
+        return success_response({"status": "failed", "message": data.get("message") or "登录失败"})
+    if status in ("expired", "consumed"):
+        return success_response({"status": "expired", "message": "二维码已过期,请刷新"})
+    return success_response({"status": status})
+
+
 @router.post("/web/auth/refresh-token", response_model=dict)
 async def refresh_token(body: dict, db: Session = Depends(get_db)):
     """刷新 Token"""

+ 30 - 0
backend/tests/test_wx_qr_api.py

@@ -0,0 +1,30 @@
+"""web wx-qr create/status API。"""
+from unittest.mock import patch
+
+from fastapi.testclient import TestClient
+
+from app.main import app
+from app.services import wx_qr_session
+
+client = TestClient(app)
+
+
+def setup_function():
+    wx_qr_session._memory_clear_for_tests()
+
+
+def test_wx_qr_create_and_poll_pending():
+    with patch("app.services.wx_qr_session._redis", return_value=None), patch(
+        "app.api.public.get_unlimited_wxacode",
+        return_value=b"\x89PNG\r\n\x1a\n",
+    ):
+        r = client.post("/api/web/auth/wx-qr/create")
+        assert r.status_code == 200
+        body = r.json()
+        assert body.get("code") == 200
+        data = body["data"]
+        sid = data["sessionId"]
+        assert data["qrImageBase64"].startswith("data:image/")
+        st = client.get("/api/web/auth/wx-qr/status", params={"sessionId": sid})
+    assert st.status_code == 200
+    assert st.json()["data"]["status"] == "pending"