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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-13 18:13:09 +08:00
1 parent e0cfd41b1f
commit 405808f4dd
3 files changed
+125 -12

No files matched your search

+27 -12
View File
@@ -11,7 +11,10 @@ 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_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,
@@ -36,7 +39,7 @@ async def _resolve_user_id(request: Request, db: Session) -> str:
@router.post("/wechat-login", response_model=dict)
async def wechat_login(body: dict, db: Session = Depends(get_db)):
"""微信授权登录:须授权手机号;openid 已绑定也须与账号 phone_no 一致。"""
"""微信登录:openid 已绑定时可静默换票;否则须授权手机号且与账号 phone_no 一致。"""
login_code = (body.get("loginCode") or "").strip()
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))
if not phone_code:
return error_response(400, "请授权手机号登录,或使用账号密码登录")
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))
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_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)
@@ -11,6 +11,21 @@ from app.models.models import SysUser
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(
db: Session, *, openid: str, phone: str
) -> Tuple[Optional[SysUser], Optional[str], Optional[int]]:
+83
View File
@@ -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"