feat(backend): add wechat mini auth helpers and user phone lookups.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
7e7f9226cf
commit
3d003e8d37
6 files changed
+14101
-11770
No files matched your search
@@ -47,6 +47,29 @@ class _UserCRUDCompat(CRUDBase[SysUser]):
|
||||
.first()
|
||||
)
|
||||
|
||||
def get_by_wx_openid(self, db: Session, wx_openid: str) -> Optional[SysUser]:
|
||||
oid = (wx_openid or "").strip()
|
||||
if not oid:
|
||||
return None
|
||||
return (
|
||||
db.query(SysUser)
|
||||
.filter(SysUser.wx_openid == oid, SysUser.del_flag == 0)
|
||||
.first()
|
||||
)
|
||||
|
||||
def list_active_by_phone_no(self, db: Session, phone_no: str) -> List[SysUser]:
|
||||
from app.services.wechat_mini_auth import normalize_cn_mobile
|
||||
|
||||
phone = normalize_cn_mobile(phone_no)
|
||||
if not phone:
|
||||
return []
|
||||
rows = (
|
||||
db.query(SysUser)
|
||||
.filter(SysUser.del_flag == 0, SysUser.status == "1")
|
||||
.all()
|
||||
)
|
||||
return [u for u in rows if normalize_cn_mobile(getattr(u, "phone_no", None)) == phone]
|
||||
|
||||
|
||||
user_crud = _UserCRUDCompat(SysUser)
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"""微信小程序登录:code2session、手机号快速验证。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_WX_CODE2SESSION = "https://api.weixin.qq.com/sns/jscode2session"
|
||||
_WX_TOKEN = "https://api.weixin.qq.com/cgi-bin/token"
|
||||
_WX_PHONE = "https://api.weixin.qq.com/wxa/business/getuserphonenumber"
|
||||
|
||||
|
||||
class WechatMiniAuthError(Exception):
|
||||
def __init__(self, message: str, *, wx_errcode: Optional[int] = None):
|
||||
super().__init__(message)
|
||||
self.wx_errcode = wx_errcode
|
||||
|
||||
|
||||
def normalize_cn_mobile(raw: Optional[str]) -> str:
|
||||
if raw is None:
|
||||
return ""
|
||||
s = re.sub(r"\s+", "", str(raw).strip())
|
||||
if s.startswith("+86"):
|
||||
s = s[3:]
|
||||
elif s.startswith("86") and len(s) == 13:
|
||||
s = s[2:]
|
||||
return s
|
||||
|
||||
|
||||
def phones_match(account_phone: Optional[str], authorized_phone: Optional[str]) -> bool:
|
||||
"""账号手机号与本次微信授权手机号是否一致(规范化后比较)。"""
|
||||
a = normalize_cn_mobile(account_phone)
|
||||
b = normalize_cn_mobile(authorized_phone)
|
||||
return bool(a) and bool(b) and a == b
|
||||
|
||||
|
||||
def _require_mini_credentials() -> tuple:
|
||||
appid = (settings.WECHAT_MP_APPID or "").strip()
|
||||
secret = (settings.WECHAT_MP_SECRET or "").strip()
|
||||
if not appid or not secret:
|
||||
raise WechatMiniAuthError("未配置小程序凭证(WECHAT_MP_APPID / WECHAT_MP_SECRET)")
|
||||
return appid, secret
|
||||
|
||||
|
||||
def code2session(login_code: str) -> Dict[str, Any]:
|
||||
code = (login_code or "").strip()
|
||||
if not code:
|
||||
raise WechatMiniAuthError("loginCode 不能为空")
|
||||
appid, secret = _require_mini_credentials()
|
||||
with httpx.Client(timeout=15.0) as client:
|
||||
r = client.get(
|
||||
_WX_CODE2SESSION,
|
||||
params={
|
||||
"appid": appid,
|
||||
"secret": secret,
|
||||
"js_code": code,
|
||||
"grant_type": "authorization_code",
|
||||
},
|
||||
)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
if data.get("errcode"):
|
||||
logger.warning("code2session failed: %s", data)
|
||||
raise WechatMiniAuthError(
|
||||
"微信登录凭证无效或已过期", wx_errcode=data.get("errcode")
|
||||
)
|
||||
openid = data.get("openid")
|
||||
if not openid:
|
||||
raise WechatMiniAuthError("微信未返回 openid")
|
||||
return {
|
||||
"openid": openid,
|
||||
"session_key": data.get("session_key"),
|
||||
"unionid": data.get("unionid"),
|
||||
}
|
||||
|
||||
|
||||
def _get_access_token() -> str:
|
||||
appid, secret = _require_mini_credentials()
|
||||
with httpx.Client(timeout=15.0) as client:
|
||||
r = client.get(
|
||||
_WX_TOKEN,
|
||||
params={
|
||||
"grant_type": "client_credential",
|
||||
"appid": appid,
|
||||
"secret": secret,
|
||||
},
|
||||
)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
token = data.get("access_token")
|
||||
if not token:
|
||||
logger.warning("get access_token failed: %s", data)
|
||||
raise WechatMiniAuthError(
|
||||
"获取微信 access_token 失败", wx_errcode=data.get("errcode")
|
||||
)
|
||||
return token
|
||||
|
||||
|
||||
def get_phone_number(phone_code: str) -> str:
|
||||
code = (phone_code or "").strip()
|
||||
if not code:
|
||||
raise WechatMiniAuthError("phoneCode 不能为空")
|
||||
token = _get_access_token()
|
||||
with httpx.Client(timeout=15.0) as client:
|
||||
r = client.post(
|
||||
f"{_WX_PHONE}?access_token={token}",
|
||||
json={"code": code},
|
||||
)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
if data.get("errcode") not in (0, None):
|
||||
logger.warning("getuserphonenumber failed: %s", data)
|
||||
raise WechatMiniAuthError(
|
||||
"获取手机号失败,请改用账号密码登录",
|
||||
wx_errcode=data.get("errcode"),
|
||||
)
|
||||
info = data.get("phone_info") or {}
|
||||
phone = normalize_cn_mobile(
|
||||
info.get("purePhoneNumber") or info.get("phoneNumber")
|
||||
)
|
||||
if not phone:
|
||||
raise WechatMiniAuthError("微信未返回手机号")
|
||||
return phone
|
||||
File diff suppressed because it is too large.
Load diff
+13052
-10894
File diff suppressed because it is too large.
Load diff
@@ -52,8 +52,8 @@
|
||||
"hash": "3f7807dffdde603ff65eeeed5883f115"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py": {
|
||||
"mtime": 1789271051.6514022,
|
||||
"hash": "16f4ff37be052148d727a759e08f555f"
|
||||
"mtime": 1789373467.5495024,
|
||||
"hash": "5b2c6168e163e58236ada6b5ab8dd071"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_pay.py": {
|
||||
"mtime": 1780108930.0,
|
||||
@@ -188,8 +188,8 @@
|
||||
"hash": "2ef1c6a59c58f94b060fb9d538fa11f7"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py": {
|
||||
"mtime": 1789266659.770212,
|
||||
"hash": "4da8e7e16181d4d2f50476e1e961565f"
|
||||
"mtime": 1789562398.1298687,
|
||||
"hash": "1457235b06908fcf04cac0ecb324f432"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py": {
|
||||
"mtime": 1786161240.0,
|
||||
@@ -280,8 +280,8 @@
|
||||
"hash": "ad4432fdb117062d34913e058e82fdf1"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py": {
|
||||
"mtime": 1787724338.0,
|
||||
"hash": "cca47dbed4c57f3d6738fecae5f0500b"
|
||||
"mtime": 1789654781.2961106,
|
||||
"hash": "6e959d77a734e75998475c564350b090"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/io_order.py": {
|
||||
"mtime": 1787744772.0,
|
||||
@@ -292,8 +292,8 @@
|
||||
"hash": "02d09e09744437c12c4ffb0f552e0c51"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/vehicle_fuel.py": {
|
||||
"mtime": 1789221269.290904,
|
||||
"hash": "27aee602b6b2ecdfb12e0e308799cab0"
|
||||
"mtime": 1789655928.1170447,
|
||||
"hash": "95cd29428afaa23d9a79ad21b5c62b8d"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py": {
|
||||
"mtime": 1789266820.0755212,
|
||||
@@ -564,8 +564,8 @@
|
||||
"hash": "4d8029bbd8b95070a0409e5e772997fe"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/quote.py": {
|
||||
"mtime": 1788874861.3405318,
|
||||
"hash": "c8adf2d5a279ab53ac4d12794f879036"
|
||||
"mtime": 1789698217.9624789,
|
||||
"hash": "5e03ed7c553061e69023a9ae17da6936"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/customer_follow.py": {
|
||||
"mtime": 1787300292.0,
|
||||
@@ -620,8 +620,8 @@
|
||||
"hash": "b472eb2a15bcb6efe9cf68398f882908"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py": {
|
||||
"mtime": 1788663105.0063384,
|
||||
"hash": "f3bd549dd200248954b83fc8bacfccf5"
|
||||
"mtime": 1789698215.9374895,
|
||||
"hash": "959c2ff7b9e1c284aa2ca152ecacaba9"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/customer.py": {
|
||||
"mtime": 1788874749.2997048,
|
||||
@@ -636,8 +636,8 @@
|
||||
"hash": "ccc481ae99fc6b0989f93f7b21524969"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py": {
|
||||
"mtime": 1789032008.3066862,
|
||||
"hash": "24153ba7a6120e394e6d51284856bfd3"
|
||||
"mtime": 1789698224.7728891,
|
||||
"hash": "8a04615af4dfaaa3715d571035751fd5"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py": {
|
||||
"mtime": 1787300292.0,
|
||||
@@ -648,8 +648,8 @@
|
||||
"hash": "f0cbbf78fec330a5d81ec209b85f6e1e"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py": {
|
||||
"mtime": 1786197474.0,
|
||||
"hash": "0b474dfb38e4ddb5accea2aeb84ef9e9"
|
||||
"mtime": 1789373465.851525,
|
||||
"hash": "99670ea1643d1babca1dcde6e4361718"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/monitor.py": {
|
||||
"mtime": 1787304454.0,
|
||||
@@ -720,8 +720,8 @@
|
||||
"hash": "553abb42d49d2ade2aa7db70c25f22c5"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py": {
|
||||
"mtime": 1788683786.9565156,
|
||||
"hash": "111746cda147d43cb70e1f822083ea47"
|
||||
"mtime": 1789655264.0642653,
|
||||
"hash": "db82470c353c0e50d862e0cefcff5faa"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py": {
|
||||
"mtime": 1785969013.0,
|
||||
@@ -736,8 +736,8 @@
|
||||
"hash": "6c178157eaa345c13147d09b80d1cc79"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/vehicle_fuel.py": {
|
||||
"mtime": 1789221269.2909205,
|
||||
"hash": "c664a58dad1400c8e9d959bb3d41adeb"
|
||||
"mtime": 1789655928.977467,
|
||||
"hash": "1a9e05df666dc78296cef7dfce92d50b"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/ocr.py": {
|
||||
"mtime": 1784035957.0,
|
||||
@@ -876,8 +876,8 @@
|
||||
"hash": "7c83274934dc5fe587de43643535183f"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_transport_stop_feedback_service.py": {
|
||||
"mtime": 1787808558.0,
|
||||
"hash": "b7195140f69e1b3ab637c66d4c8c41e5"
|
||||
"mtime": 1789539990.9049704,
|
||||
"hash": "b9a8a882a152a708c50068d9d3d51ac3"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db_inventory_site_token.py": {
|
||||
"mtime": 1782225270.0,
|
||||
@@ -960,8 +960,8 @@
|
||||
"hash": "06bfe0225c40d171b259a4c24eafdc24"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py": {
|
||||
"mtime": 1788683736.9360983,
|
||||
"hash": "b87f8c208b79698dfa7e301beb330979"
|
||||
"mtime": 1789655260.6589975,
|
||||
"hash": "2c7b4a4285d6c599339b156618ad059b"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_user_bind.py": {
|
||||
"mtime": 1789266818.3101022,
|
||||
@@ -976,8 +976,8 @@
|
||||
"hash": "392b1a2c65bd876ab2c40a94c453a10c"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_fuel_service.py": {
|
||||
"mtime": 1789221269.290894,
|
||||
"hash": "9e000ee8c420ba0b1bf6cee39f244354"
|
||||
"mtime": 1789656506.347816,
|
||||
"hash": "31db87a5e292761d05000c843adb10ed"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py": {
|
||||
"mtime": 1788878764.9085488,
|
||||
@@ -1008,8 +1008,8 @@
|
||||
"hash": "4222656212dd4cfef49a44ba32ff815e"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/mp_webview_ticket.py": {
|
||||
"mtime": 1789373254.4287198,
|
||||
"hash": "c301a6d2083fab1fc22c71db6187211c"
|
||||
"mtime": 1789536234.6766896,
|
||||
"hash": "099be22c3a8dfcc28f96c6101a17cbdb"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_io_order_service.py": {
|
||||
"mtime": 1787746009.0,
|
||||
@@ -1096,8 +1096,8 @@
|
||||
"hash": "e319775ece357114b4666c37f8943869"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl/freight_list_filters.py": {
|
||||
"mtime": 1788683495.549828,
|
||||
"hash": "b80f49991ee8d3bdd2f152b2412e176d"
|
||||
"mtime": 1789655228.7287679,
|
||||
"hash": "b731b081543fcb8c18b7f8b8ceb91ab9"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl/personnel_list_filters.py": {
|
||||
"mtime": 1788685222.151241,
|
||||
@@ -1328,8 +1328,8 @@
|
||||
"hash": "cf0d903888bed6da71ace26f6ff97032"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_vehicle_fuel_schema.py": {
|
||||
"mtime": 1789221269.2908795,
|
||||
"hash": "26ee0e0e0a2ceab5950337e8a567c696"
|
||||
"mtime": 1789656023.2436283,
|
||||
"hash": "f275f6098d88d15bbc4bb5358af79871"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_id_card_age.py": {
|
||||
"mtime": 1787140680.0,
|
||||
@@ -1356,8 +1356,8 @@
|
||||
"hash": "ab0e4f34ba23352b1539217b86477810"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_mp_webview_ticket.py": {
|
||||
"mtime": 1789373251.195948,
|
||||
"hash": "44804276b962376e2aca5864936b93f7"
|
||||
"mtime": 1789536211.8726232,
|
||||
"hash": "b032f53e541f0c032ae3c02c2fcfb9fc"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_delete_guard.py": {
|
||||
"mtime": 1782687611.0,
|
||||
@@ -1583,6 +1583,10 @@
|
||||
"mtime": 1788679318.068224,
|
||||
"hash": "1ff8f4da2f762c1bd1a2afe12039d86a"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_mp_webview_auth_api.py": {
|
||||
"mtime": 1789373470.859986,
|
||||
"hash": "8598410535959c1483f7e8f4623e7fb5"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pound_qr_label_items.py": {
|
||||
"mtime": 1789262900.8196542,
|
||||
"hash": "5845c144fa1f5400a06613dfff66c0d1"
|
||||
@@ -1655,6 +1659,10 @@
|
||||
"mtime": 1788603988.6160898,
|
||||
"hash": "d07391879a9f64397d26b87ae364e0ce"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_resolve_product_id_no_auto_create.py": {
|
||||
"mtime": 1789698225.0437787,
|
||||
"hash": "c2a15309f66bc4c9d8fcfc00575484af"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_dict_ensure.py": {
|
||||
"mtime": 1787296131.0,
|
||||
"hash": "6e2dc0f1608b406a93d23b1094f994e9"
|
||||
@@ -1664,8 +1672,8 @@
|
||||
"hash": "8fcd74a7c9ceeed56cc26842259dc8b9"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_freight_list_filters.py": {
|
||||
"mtime": 1788683495.7270317,
|
||||
"hash": "0aab363e9f7486938094fe8bc4507543"
|
||||
"mtime": 1789655317.1733906,
|
||||
"hash": "35ac689504792e5d04cc178c4fccd203"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_brand_archive.py": {
|
||||
"mtime": 1785453323.0,
|
||||
@@ -1743,6 +1751,10 @@
|
||||
"mtime": 1785469153.0,
|
||||
"hash": "4274cc4a7467069efbd04d4215ecbf15"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/import_vehicle_fuel_register.py": {
|
||||
"mtime": 1789562392.6117399,
|
||||
"hash": "cd6902eba05c9319d01672c4adb85061"
|
||||
},
|
||||
"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_quote.py": {
|
||||
"mtime": 1785455807.0,
|
||||
"hash": "ebe7d542c8254547db2b7176baff2e47"
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
from app.services.wechat_mini_auth import normalize_cn_mobile, phones_match
|
||||
|
||||
|
||||
def test_normalize_strips_plus86():
|
||||
assert normalize_cn_mobile("+8613800138000") == "13800138000"
|
||||
|
||||
|
||||
def test_normalize_strips_86_prefix_13_digits():
|
||||
assert normalize_cn_mobile("8613800138000") == "13800138000"
|
||||
|
||||
|
||||
def test_phones_match_equal():
|
||||
assert phones_match("13800138000", "13800138000") is True
|
||||
|
||||
|
||||
def test_phones_match_normalizes():
|
||||
assert phones_match("13800138000", "+8613800138000") is True
|
||||
|
||||
|
||||
def test_phones_match_mismatch():
|
||||
assert phones_match("13800138000", "13900139000") is False
|
||||
|
||||
|
||||
def test_phones_match_empty_fails():
|
||||
assert phones_match("", "13800138000") is False
|
||||
assert phones_match(None, "13800138000") is False
|
||||
Reference in new issue
Block a user