瀏覽代碼

feat(mini-auth): allow silent wechat login when openid already bound

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 月之前
父節點
當前提交
405808f4dd
共有 3 個文件被更改,包括 127 次插入 和 14 次删除
  1. 29 14
      backend/app/api/mini_auth.py
  2. 15 0
      backend/app/services/wechat_login_service.py
  3. 83 0
      backend/tests/test_wechat_login_silent.py

+ 29 - 14
backend/app/api/mini_auth.py

@@ -11,7 +11,10 @@ from app.core.database import get_db
 from app.core.response import error_response, success_response
 from app.core.response import error_response, success_response
 from app.crud.user import user_crud
 from app.crud.user import user_crud
 from app.services.issue_user_tokens import issue_tokens_for_user
 from app.services.issue_user_tokens import issue_tokens_for_user
-from app.services.wechat_login_service import resolve_wechat_login_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 (
 from app.services.wechat_mini_auth import (
     WechatMiniAuthError,
     WechatMiniAuthError,
     code2session,
     code2session,
@@ -36,7 +39,7 @@ async def _resolve_user_id(request: Request, db: Session) -> str:
 
 
 @router.post("/wechat-login", response_model=dict)
 @router.post("/wechat-login", response_model=dict)
 async def wechat_login(body: dict, db: Session = Depends(get_db)):
 async def wechat_login(body: dict, db: Session = Depends(get_db)):
-    """微信授权登录:须授权手机号;openid 已绑定也须与账号 phone_no 一致。"""
+    """微信登录:openid 已绑定时可静默换票;否则须授权手机号且与账号 phone_no 一致。"""
     login_code = (body.get("loginCode") or "").strip()
     login_code = (body.get("loginCode") or "").strip()
     phone_code = (body.get("phoneCode") or "").strip() or None
     phone_code = (body.get("phoneCode") or "").strip() or None
 
 
@@ -46,18 +49,30 @@ async def wechat_login(body: dict, db: Session = Depends(get_db)):
         return error_response(400, str(exc))
         return error_response(400, str(exc))
 
 
     if not phone_code:
     if not phone_code:
-        return error_response(400, "请授权手机号登录,或使用账号密码登录")
-
-    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)
+        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:
     try:
         data = await issue_tokens_for_user(db, user)
         data = await issue_tokens_for_user(db, user)

+ 15 - 0
backend/app/services/wechat_login_service.py

@@ -11,6 +11,21 @@ from app.models.models import SysUser
 from app.services.wechat_mini_auth import normalize_cn_mobile, phones_match
 from app.services.wechat_mini_auth import normalize_cn_mobile, phones_match
 
 
 
 
+def resolve_wechat_login_by_openid_only(
+    db: Session, *, openid: str
+) -> Tuple[Optional[SysUser], Optional[str], Optional[int]]:
+    """已绑定 openid 时静默换票(无需手机号授权)。"""
+    oid = (openid or "").strip()
+    if not oid:
+        return None, "微信未返回 openid", 400
+    bound = user_crud.get_by_wx_openid(db, oid)
+    if not bound:
+        return None, "请授权手机号登录,或使用账号密码登录", 400
+    if getattr(bound, "status", "1") != "1":
+        return None, "账号已停用", 403
+    return bound, None, None
+
+
 def resolve_wechat_login_user(
 def resolve_wechat_login_user(
     db: Session, *, openid: str, phone: str
     db: Session, *, openid: str, phone: str
 ) -> Tuple[Optional[SysUser], Optional[str], Optional[int]]:
 ) -> Tuple[Optional[SysUser], Optional[str], Optional[int]]:

+ 83 - 0
backend/tests/test_wechat_login_silent.py

@@ -0,0 +1,83 @@
+"""微信登录:openid 已绑定可无 phoneCode 静默换票。"""
+from types import SimpleNamespace
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pytest
+
+from app.services.wechat_login_service import resolve_wechat_login_by_openid_only
+
+
+def test_resolve_openid_bound_ok_without_phone():
+    bound = SimpleNamespace(
+        id="u1",
+        wx_openid="oid-1",
+        status="1",
+    )
+    db = MagicMock()
+    with patch(
+        "app.services.wechat_login_service.user_crud.get_by_wx_openid",
+        return_value=bound,
+    ):
+        user, err, code = resolve_wechat_login_by_openid_only(db, openid="oid-1")
+    assert err is None and code is None
+    assert user is bound
+
+
+def test_resolve_openid_not_bound_asks_phone():
+    db = MagicMock()
+    with patch(
+        "app.services.wechat_login_service.user_crud.get_by_wx_openid",
+        return_value=None,
+    ):
+        user, err, code = resolve_wechat_login_by_openid_only(db, openid="oid-x")
+    assert user is None
+    assert code == 400
+    assert "请授权手机号" in err
+
+
+def test_resolve_openid_bound_but_disabled():
+    bound = SimpleNamespace(
+        id="u2",
+        wx_openid="oid-2",
+        status="0",
+    )
+    db = MagicMock()
+    with patch(
+        "app.services.wechat_login_service.user_crud.get_by_wx_openid",
+        return_value=bound,
+    ):
+        user, err, code = resolve_wechat_login_by_openid_only(db, openid="oid-2")
+    assert user is None
+    assert code == 403
+    assert "停用" in err
+
+
+@pytest.mark.asyncio
+async def test_wechat_login_api_silent_when_openid_bound():
+    bound = SimpleNamespace(
+        id="u1",
+        wx_openid="oid-1",
+        status="1",
+        wx_unionid=None,
+    )
+    with (
+        patch(
+            "app.api.mini_auth.code2session",
+            return_value={"openid": "oid-1", "unionid": None},
+        ),
+        patch(
+            "app.api.mini_auth.resolve_wechat_login_by_openid_only",
+            return_value=(bound, None, None),
+        ) as resolve_silent,
+        patch(
+            "app.api.mini_auth.issue_tokens_for_user",
+            new_callable=AsyncMock,
+            return_value={"accessToken": "t"},
+        ),
+    ):
+        from app.api.mini_auth import wechat_login
+
+        resp = await wechat_login({"loginCode": "lc"}, db=MagicMock())
+    resolve_silent.assert_called_once()
+    assert resp["code"] == 200
+    assert resp["data"]["accessToken"] == "t"