feat(mini-auth): allow silent wechat login when openid already bound
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
e0cfd41b1f
commit
405808f4dd
3 files changed
+125
-12
No files matched your search
@@ -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]]:
|
||||
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user