rainycode 1.1.0__tar.gz → 1.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rainycode-1.1.0 → rainycode-1.1.2}/PKG-INFO +1 -1
- {rainycode-1.1.0 → rainycode-1.1.2}/common_base/consts.py +0 -1
- {rainycode-1.1.0 → rainycode-1.1.2}/common_depends/auth_depend.py +1 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/common_servers/api/auth_api.py +5 -16
- {rainycode-1.1.0 → rainycode-1.1.2}/common_servers/service/auth_svc.py +126 -42
- {rainycode-1.1.0 → rainycode-1.1.2}/common_utils/bcrypt_util.py +2 -4
- {rainycode-1.1.0 → rainycode-1.1.2}/pyproject.toml +1 -1
- {rainycode-1.1.0 → rainycode-1.1.2}/rainycode.egg-info/PKG-INFO +1 -1
- {rainycode-1.1.0 → rainycode-1.1.2}/rainycode.egg-info/SOURCES.txt +0 -1
- rainycode-1.1.0/common_servers/managers/wechat_user_manager.py +0 -98
- {rainycode-1.1.0 → rainycode-1.1.2}/common_base/aiorequests.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/common_base/exception.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/common_base/logging.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/common_base/response.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/common_models/__init__.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/common_models/base_model.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/common_models/user_model.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/common_models/wechat_model.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/common_servers/api/wechat_api.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/common_servers/router.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/common_servers/schemas/__init__.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/common_servers/schemas/auth_schema.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/common_servers/service/wechat_svc.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/common_utils/captcha_util.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/common_utils/ip_util.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/common_utils/jwt_util.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/common_utils/snowflake_util.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/common_utils/wechat_util.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/core/base_config.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/core/databases/aiodb.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/core/databases/aioredis.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/core/middleware/http_middleware.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/core/start.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/rainycode.egg-info/dependency_links.txt +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/rainycode.egg-info/requires.txt +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/rainycode.egg-info/top_level.txt +0 -0
- {rainycode-1.1.0 → rainycode-1.1.2}/setup.cfg +0 -0
|
@@ -4,6 +4,5 @@ REDIS_PREFIX_USER_INFO: str = "user:info:"
|
|
|
4
4
|
REDIS_PREFIX_OAUTH_STATE: str = "oauth:state:"
|
|
5
5
|
REDIS_PREFIX_WECHAT_CODE: str = "wechat:code:"
|
|
6
6
|
REDIS_PREFIX_WECHAT_OAUTH_CODE: str = "wechat:oauth:code:"
|
|
7
|
-
REDIS_PREFIX_TEMP_CODE: str = "oauth:temp_code:"
|
|
8
7
|
REDIS_PREFIX_LOGIN_LOCK_USER: str = "auth:login_lock:user:"
|
|
9
8
|
REDIS_PREFIX_LOGIN_LOCK_IP: str = "auth:login_lock:ip:"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from common_models.user_model import User
|
|
2
|
-
from fastapi import APIRouter, Security, Body,
|
|
2
|
+
from fastapi import APIRouter, Security, Body, Query, Request
|
|
3
3
|
from common_base.response import SuccessReturn
|
|
4
|
-
from common_depends.auth_depend import login_require
|
|
4
|
+
from common_depends.auth_depend import login_require
|
|
5
5
|
from common_servers.schemas.auth_schema import *
|
|
6
6
|
from common_servers.service.auth_svc import AuthService
|
|
7
7
|
|
|
@@ -30,10 +30,11 @@ async def refresh_token(refresh_token: str = Body(..., embed=True)):
|
|
|
30
30
|
|
|
31
31
|
@router.post("/logout", summary="登出")
|
|
32
32
|
async def logout(
|
|
33
|
-
|
|
33
|
+
request: Request,
|
|
34
34
|
refresh_token: str = Body(..., embed=True),
|
|
35
35
|
_: User = Security(login_require),
|
|
36
36
|
):
|
|
37
|
+
token = request.state.token
|
|
37
38
|
await AuthService.logout(token, refresh_token)
|
|
38
39
|
return SuccessReturn(msg="登出成功")
|
|
39
40
|
|
|
@@ -69,16 +70,4 @@ async def get_wechat_oauth_url(
|
|
|
69
70
|
- 如果用户未注册,自动重定向到 snsapi_userinfo 授权
|
|
70
71
|
"""
|
|
71
72
|
authorize_url = await AuthService.get_wechat_oauth_url(app_id, redirect_uri, state)
|
|
72
|
-
return SuccessReturn(data={"authorize_url": authorize_url})
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
@router.get("/wechat/token", summary="通过临时授权码获取 token")
|
|
76
|
-
async def get_token_by_temp_code(code: str = Query(..., description="临时授权码")):
|
|
77
|
-
"""
|
|
78
|
-
OAuth 回调后,前端使用临时码换取 token
|
|
79
|
-
|
|
80
|
-
- 临时码有效期:120 秒
|
|
81
|
-
- 一次性使用,获取后立即失效
|
|
82
|
-
"""
|
|
83
|
-
token_data = await AuthService.get_token_by_temp_code(code)
|
|
84
|
-
return SuccessReturn(data=token_data, msg="获取成功")
|
|
73
|
+
return SuccessReturn(data={"authorize_url": authorize_url})
|
|
@@ -11,11 +11,11 @@ from common_base.consts import (
|
|
|
11
11
|
REDIS_PREFIX_USER_INFO,
|
|
12
12
|
REDIS_PREFIX_WECHAT_CODE,
|
|
13
13
|
REDIS_PREFIX_WECHAT_OAUTH_CODE,
|
|
14
|
-
REDIS_PREFIX_TEMP_CODE,
|
|
15
14
|
REDIS_PREFIX_LOGIN_LOCK_USER,
|
|
16
15
|
REDIS_PREFIX_LOGIN_LOCK_IP,
|
|
17
16
|
)
|
|
18
17
|
from common_base.exception import CustomException
|
|
18
|
+
from common_base.logging import logger
|
|
19
19
|
from common_models.user_model import User
|
|
20
20
|
from common_models.wechat_model import WechatApp, WechatUser, AppTypeEnum
|
|
21
21
|
from common_utils.bcrypt_util import PwdUtil
|
|
@@ -23,7 +23,6 @@ from common_utils.jwt_util import JwtUtil
|
|
|
23
23
|
from common_utils.wechat_util import WechatUtil
|
|
24
24
|
from core.databases.aioredis import AioRedis
|
|
25
25
|
from common_servers.schemas.auth_schema import UserRegister, UserLogin, WechatLogin
|
|
26
|
-
from common_servers.managers.wechat_user_manager import WechatUserManager
|
|
27
26
|
from tortoise.transactions import atomic
|
|
28
27
|
from tortoise.expressions import Q
|
|
29
28
|
from core.base_config import base_config
|
|
@@ -33,7 +32,6 @@ from core.base_config import base_config
|
|
|
33
32
|
LOGIN_FAIL_LIMIT = 5 # 最大失败次数
|
|
34
33
|
LOGIN_LOCK_SECONDS = 900 # 锁定时间(15分钟)
|
|
35
34
|
IP_FAIL_LIMIT = 20 # 单 IP 最大失败次数
|
|
36
|
-
TEMP_CODE_EXPIRE_SECONDS = 120 # 临时授权码有效期
|
|
37
35
|
|
|
38
36
|
|
|
39
37
|
class AuthService:
|
|
@@ -75,6 +73,7 @@ class AuthService:
|
|
|
75
73
|
phone=user_in.phone,
|
|
76
74
|
nickname=user_in.nickname
|
|
77
75
|
)
|
|
76
|
+
logger.info(f"用户注册成功: username={user.username}, user_id={user.id}")
|
|
78
77
|
return user
|
|
79
78
|
|
|
80
79
|
@classmethod
|
|
@@ -83,7 +82,9 @@ class AuthService:
|
|
|
83
82
|
用户密码登录
|
|
84
83
|
"""
|
|
85
84
|
user = await cls._authenticate_password(user_in, client_ip)
|
|
86
|
-
|
|
85
|
+
token_data = await cls._create_tokens(user)
|
|
86
|
+
logger.info(f"用户登录成功: username={user.username}, user_id={user.id}, ip={client_ip}")
|
|
87
|
+
return token_data
|
|
87
88
|
|
|
88
89
|
@classmethod
|
|
89
90
|
async def login_wechat_mini(cls, wechat_in: WechatLogin) -> dict:
|
|
@@ -99,9 +100,12 @@ class AuthService:
|
|
|
99
100
|
刷新 Token
|
|
100
101
|
"""
|
|
101
102
|
user_id = await AioRedis.get(f"{REDIS_PREFIX_REFRESH_TOKEN}{refresh_token}")
|
|
102
|
-
user_id = int(user_id) if user_id else None
|
|
103
103
|
if not user_id:
|
|
104
104
|
raise CustomException(msg="无效或已过期的刷新令牌")
|
|
105
|
+
try:
|
|
106
|
+
user_id = int(user_id)
|
|
107
|
+
except (ValueError, TypeError):
|
|
108
|
+
raise CustomException(msg="无效的刷新令牌")
|
|
105
109
|
|
|
106
110
|
user = await User.get_or_none(id=user_id)
|
|
107
111
|
if not user or not user.is_active:
|
|
@@ -109,7 +113,9 @@ class AuthService:
|
|
|
109
113
|
|
|
110
114
|
# Token 轮换:删除旧的,生成新的
|
|
111
115
|
await AioRedis.delete(f"{REDIS_PREFIX_REFRESH_TOKEN}{refresh_token}")
|
|
112
|
-
|
|
116
|
+
token_data = await cls._create_tokens(user)
|
|
117
|
+
logger.info(f"Token刷新成功: username={user.username}, user_id={user.id}")
|
|
118
|
+
return token_data
|
|
113
119
|
|
|
114
120
|
@classmethod
|
|
115
121
|
async def logout(cls, access_token: str, refresh_token: str):
|
|
@@ -133,6 +139,12 @@ class AuthService:
|
|
|
133
139
|
if refresh_token:
|
|
134
140
|
await AioRedis.delete(f"{REDIS_PREFIX_REFRESH_TOKEN}{refresh_token}")
|
|
135
141
|
|
|
142
|
+
# 记录登出日志
|
|
143
|
+
if payload:
|
|
144
|
+
user_id = payload.get("sub")
|
|
145
|
+
username = payload.get("username")
|
|
146
|
+
logger.info(f"用户登出: user_id={user_id}, username={username}")
|
|
147
|
+
|
|
136
148
|
@classmethod
|
|
137
149
|
async def get_wechat_oauth_url(cls, app_id: str, redirect_uri: str, state: str | None = None) -> str:
|
|
138
150
|
"""
|
|
@@ -241,8 +253,8 @@ class AuthService:
|
|
|
241
253
|
|
|
242
254
|
user_info = await WechatUtil.get_user_info(access_token, openid)
|
|
243
255
|
|
|
244
|
-
# 使用
|
|
245
|
-
user = await
|
|
256
|
+
# 使用 _bind_or_register_wechat_user 创建或绑定用户
|
|
257
|
+
user = await cls._bind_or_register_wechat_user(
|
|
246
258
|
openid=openid,
|
|
247
259
|
app_config=app_config,
|
|
248
260
|
unionid=wx_res.get("unionid"),
|
|
@@ -255,7 +267,7 @@ class AuthService:
|
|
|
255
267
|
|
|
256
268
|
# 新用户,需要 snsapi_userinfo 授权
|
|
257
269
|
# 缓存 state 信息供二次回调使用
|
|
258
|
-
new_state =
|
|
270
|
+
new_state = str(uuid.uuid4())
|
|
259
271
|
await AioRedis.set(f"{REDIS_PREFIX_OAUTH_STATE}{new_state}", cache_value, ex=300)
|
|
260
272
|
|
|
261
273
|
# 构建 snsapi_userinfo 授权链接
|
|
@@ -269,22 +281,6 @@ class AuthService:
|
|
|
269
281
|
|
|
270
282
|
return RedirectResponse(url=userinfo_auth_url, status_code=302)
|
|
271
283
|
|
|
272
|
-
@classmethod
|
|
273
|
-
async def get_token_by_temp_code(cls, temp_code: str) -> dict:
|
|
274
|
-
"""
|
|
275
|
-
通过临时授权码获取 token
|
|
276
|
-
"""
|
|
277
|
-
cache_key = f"{REDIS_PREFIX_TEMP_CODE}{temp_code}"
|
|
278
|
-
cache_value = await AioRedis.get(cache_key)
|
|
279
|
-
|
|
280
|
-
if not cache_value:
|
|
281
|
-
raise CustomException(msg="临时授权码已过期,请重新授权")
|
|
282
|
-
|
|
283
|
-
# 删除临时码(一次性使用)
|
|
284
|
-
await AioRedis.delete(cache_key)
|
|
285
|
-
|
|
286
|
-
return json.loads(cache_value)
|
|
287
|
-
|
|
288
284
|
# ==================== 私有方法 ====================
|
|
289
285
|
|
|
290
286
|
@classmethod
|
|
@@ -296,17 +292,19 @@ class AuthService:
|
|
|
296
292
|
user = await User.get_or_none(username=data.username)
|
|
297
293
|
if not user:
|
|
298
294
|
await cls._record_login_fail(data.username, client_ip)
|
|
295
|
+
logger.warning(f"登录失败: 用户名不存在, username={data.username}, ip={client_ip}")
|
|
299
296
|
raise CustomException(msg="用户名或密码错误")
|
|
300
297
|
|
|
301
298
|
if not PwdUtil.verify_password(data.password, user.password):
|
|
302
299
|
await cls._record_login_fail(data.username, client_ip)
|
|
300
|
+
logger.warning(f"登录失败: 密码错误, username={data.username}, ip={client_ip}")
|
|
303
301
|
raise CustomException(msg="用户名或密码错误")
|
|
304
302
|
|
|
305
303
|
if not user.is_active:
|
|
306
304
|
raise CustomException(msg="用户已被禁用")
|
|
307
305
|
|
|
308
306
|
# 清除失败记录
|
|
309
|
-
await cls._clear_login_fail(data.username)
|
|
307
|
+
await cls._clear_login_fail(data.username, client_ip)
|
|
310
308
|
|
|
311
309
|
# 更新最后登录时间
|
|
312
310
|
user.last_login = datetime.now()
|
|
@@ -357,8 +355,8 @@ class AuthService:
|
|
|
357
355
|
if phone and not re.match(r'^1[3-9]\d{9}$', phone):
|
|
358
356
|
raise CustomException(msg="手机号格式不正确")
|
|
359
357
|
|
|
360
|
-
# 6. 使用
|
|
361
|
-
return await
|
|
358
|
+
# 6. 使用 _bind_or_register_wechat_user 处理用户绑定/注册
|
|
359
|
+
return await cls._bind_or_register_wechat_user(
|
|
362
360
|
openid=openid,
|
|
363
361
|
app_config=app_config,
|
|
364
362
|
unionid=unionid,
|
|
@@ -397,9 +395,10 @@ class AuthService:
|
|
|
397
395
|
await AioRedis.expire(ip_key, LOGIN_LOCK_SECONDS)
|
|
398
396
|
|
|
399
397
|
@classmethod
|
|
400
|
-
async def _clear_login_fail(cls, username: str):
|
|
398
|
+
async def _clear_login_fail(cls, username: str, client_ip: str):
|
|
401
399
|
"""清除登录失败记录"""
|
|
402
400
|
await AioRedis.delete(f"{REDIS_PREFIX_LOGIN_LOCK_USER}{username}")
|
|
401
|
+
await AioRedis.delete(f"{REDIS_PREFIX_LOGIN_LOCK_IP}{client_ip}")
|
|
403
402
|
|
|
404
403
|
@classmethod
|
|
405
404
|
async def _create_tokens(cls, user: User) -> dict:
|
|
@@ -425,22 +424,107 @@ class AuthService:
|
|
|
425
424
|
}
|
|
426
425
|
|
|
427
426
|
@classmethod
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
427
|
+
@atomic("main")
|
|
428
|
+
async def _bind_or_register_wechat_user(
|
|
429
|
+
cls,
|
|
430
|
+
openid: str,
|
|
431
|
+
app_config: WechatApp,
|
|
432
|
+
unionid: str | None = None,
|
|
433
|
+
phone: str | None = None,
|
|
434
|
+
nickname: str | None = None,
|
|
435
|
+
avatar: str | None = None,
|
|
436
|
+
session_key: str | None = None,
|
|
437
|
+
) -> User:
|
|
438
|
+
"""
|
|
439
|
+
绑定已有用户或创建新用户(微信登录专用)
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
openid: 微信 OpenID
|
|
443
|
+
app_config: 微信应用配置
|
|
444
|
+
unionid: 微信 UnionID(可选,用于跨应用关联)
|
|
445
|
+
phone: 手机号(可选,小程序一键登录获取)
|
|
446
|
+
nickname: 微信昵称
|
|
447
|
+
avatar: 微信头像
|
|
448
|
+
session_key: 会话密钥(仅小程序)
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
User: 用户对象
|
|
452
|
+
"""
|
|
453
|
+
# 1. 检查 WechatUser 是否已存在
|
|
454
|
+
wechat_user = await WechatUser.get_or_none(
|
|
455
|
+
openid=openid, wechat_app=app_config
|
|
456
|
+
).select_related("user")
|
|
457
|
+
|
|
458
|
+
if wechat_user:
|
|
459
|
+
# 已存在关联,更新信息
|
|
460
|
+
if session_key:
|
|
461
|
+
wechat_user.session_key = session_key
|
|
462
|
+
if nickname:
|
|
463
|
+
wechat_user.nickname = nickname
|
|
464
|
+
if avatar:
|
|
465
|
+
wechat_user.avatar = avatar
|
|
466
|
+
if phone and not wechat_user.user.phone:
|
|
467
|
+
wechat_user.user.phone = phone
|
|
468
|
+
await wechat_user.user.save()
|
|
469
|
+
await wechat_user.save()
|
|
470
|
+
return wechat_user.user
|
|
471
|
+
|
|
472
|
+
# 2. 检查 User 是否存在(通过 phone 或 unionid)
|
|
473
|
+
user = None
|
|
474
|
+
if phone:
|
|
475
|
+
user = await User.get_or_none(phone=phone)
|
|
476
|
+
|
|
477
|
+
# 如果有 UnionID,检查其他应用下的 WechatUser 是否关联了 User
|
|
478
|
+
if not user and unionid:
|
|
479
|
+
other_wechat_user = (
|
|
480
|
+
await WechatUser.filter(unionid=unionid)
|
|
481
|
+
.exclude(wechat_app=app_config)
|
|
482
|
+
.first()
|
|
483
|
+
.select_related("user")
|
|
484
|
+
)
|
|
485
|
+
if other_wechat_user:
|
|
486
|
+
user = other_wechat_user.user
|
|
487
|
+
|
|
488
|
+
# 3. 创建 User(如不存在)
|
|
489
|
+
if not user:
|
|
490
|
+
# 使用 UUID 避免并发冲突
|
|
491
|
+
username = f"wx_{uuid.uuid4().hex[:8]}"
|
|
492
|
+
user = await User.create(
|
|
493
|
+
username=username,
|
|
494
|
+
password="", # 微信用户无密码
|
|
495
|
+
phone=phone,
|
|
496
|
+
nickname=nickname or f"用户{username[-4:]}",
|
|
497
|
+
avatar=avatar,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
# 4. 创建 WechatUser 关联
|
|
501
|
+
await WechatUser.create(
|
|
502
|
+
user=user,
|
|
503
|
+
wechat_app=app_config,
|
|
504
|
+
openid=openid,
|
|
505
|
+
unionid=unionid,
|
|
506
|
+
session_key=session_key,
|
|
507
|
+
nickname=nickname,
|
|
508
|
+
avatar=avatar,
|
|
438
509
|
)
|
|
439
510
|
|
|
440
|
-
|
|
441
|
-
|
|
511
|
+
return user
|
|
512
|
+
|
|
513
|
+
@classmethod
|
|
514
|
+
async def _build_redirect_response(
|
|
515
|
+
cls, redirect_uri: str, token_data: dict, state: str | None = None
|
|
516
|
+
) -> RedirectResponse:
|
|
517
|
+
"""构建带 token 的重定向响应(使用 URL Fragment)"""
|
|
518
|
+
params = {
|
|
519
|
+
"access_token": token_data["access_token"],
|
|
520
|
+
"refresh_token": token_data["refresh_token"],
|
|
521
|
+
"token_type": token_data["token_type"],
|
|
522
|
+
"expires_in": str(token_data["expires_in"]),
|
|
523
|
+
}
|
|
442
524
|
if state:
|
|
443
525
|
params["state"] = state
|
|
444
526
|
|
|
445
|
-
|
|
527
|
+
# 使用 # fragment 传递 token,避免出现在服务器日志中
|
|
528
|
+
fragment = urlencode(params)
|
|
529
|
+
redirect_url = f"{redirect_uri}#{fragment}"
|
|
446
530
|
return RedirectResponse(url=redirect_url, status_code=302)
|
|
@@ -44,7 +44,7 @@ class PwdUtil:
|
|
|
44
44
|
@classmethod
|
|
45
45
|
def check_password_strength(cls, password: str) -> str | None:
|
|
46
46
|
"""
|
|
47
|
-
|
|
47
|
+
检查密码强度(不含长度检查,由 Schema min_length 处理)
|
|
48
48
|
|
|
49
49
|
Args:
|
|
50
50
|
password: 明文密码
|
|
@@ -52,12 +52,10 @@ class PwdUtil:
|
|
|
52
52
|
Returns:
|
|
53
53
|
str: 如果密码强度不够返回提示信息,否则返回None
|
|
54
54
|
"""
|
|
55
|
-
if len(password) < 6:
|
|
56
|
-
return "密码长度至少6位"
|
|
57
55
|
if not any(c.isupper() for c in password):
|
|
58
56
|
return "密码需要包含大写字母"
|
|
59
57
|
if not any(c.islower() for c in password):
|
|
60
|
-
return "密码需要包含小写字母"
|
|
58
|
+
return "密码需要包含小写字母"
|
|
61
59
|
if not any(c.isdigit() for c in password):
|
|
62
60
|
return "密码需要包含数字"
|
|
63
61
|
return None
|
|
@@ -12,7 +12,6 @@ common_models/wechat_model.py
|
|
|
12
12
|
common_servers/router.py
|
|
13
13
|
common_servers/api/auth_api.py
|
|
14
14
|
common_servers/api/wechat_api.py
|
|
15
|
-
common_servers/managers/wechat_user_manager.py
|
|
16
15
|
common_servers/schemas/__init__.py
|
|
17
16
|
common_servers/schemas/auth_schema.py
|
|
18
17
|
common_servers/service/auth_svc.py
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import uuid
|
|
2
|
-
from tortoise.transactions import atomic
|
|
3
|
-
from common_models.user_model import User
|
|
4
|
-
from common_models.wechat_model import WechatApp, WechatUser
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class WechatUserManager:
|
|
8
|
-
"""
|
|
9
|
-
微信用户绑定/注册统一处理
|
|
10
|
-
合并小程序和公众号的用户处理逻辑
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
@classmethod
|
|
14
|
-
@atomic("main")
|
|
15
|
-
async def bind_or_register(
|
|
16
|
-
cls,
|
|
17
|
-
openid: str,
|
|
18
|
-
app_config: WechatApp,
|
|
19
|
-
unionid: str | None = None,
|
|
20
|
-
phone: str | None = None,
|
|
21
|
-
nickname: str | None = None,
|
|
22
|
-
avatar: str | None = None,
|
|
23
|
-
session_key: str | None = None,
|
|
24
|
-
) -> User:
|
|
25
|
-
"""
|
|
26
|
-
绑定已有用户或创建新用户
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
openid: 微信 OpenID
|
|
30
|
-
app_config: 微信应用配置
|
|
31
|
-
unionid: 微信 UnionID(可选,用于跨应用关联)
|
|
32
|
-
phone: 手机号(可选,小程序一键登录获取)
|
|
33
|
-
nickname: 微信昵称
|
|
34
|
-
avatar: 微信头像
|
|
35
|
-
session_key: 会话密钥(仅小程序)
|
|
36
|
-
|
|
37
|
-
Returns:
|
|
38
|
-
User: 用户对象
|
|
39
|
-
"""
|
|
40
|
-
# 1. 检查 WechatUser 是否已存在
|
|
41
|
-
wechat_user = await WechatUser.get_or_none(
|
|
42
|
-
openid=openid, wechat_app=app_config
|
|
43
|
-
).select_related("user")
|
|
44
|
-
|
|
45
|
-
if wechat_user:
|
|
46
|
-
# 已存在关联,更新信息
|
|
47
|
-
if session_key:
|
|
48
|
-
wechat_user.session_key = session_key
|
|
49
|
-
if nickname:
|
|
50
|
-
wechat_user.nickname = nickname
|
|
51
|
-
if avatar:
|
|
52
|
-
wechat_user.avatar = avatar
|
|
53
|
-
if phone and not wechat_user.user.phone:
|
|
54
|
-
wechat_user.user.phone = phone
|
|
55
|
-
await wechat_user.user.save()
|
|
56
|
-
await wechat_user.save()
|
|
57
|
-
return wechat_user.user
|
|
58
|
-
|
|
59
|
-
# 2. 检查 User 是否存在(通过 phone 或 unionid)
|
|
60
|
-
user = None
|
|
61
|
-
if phone:
|
|
62
|
-
user = await User.get_or_none(phone=phone)
|
|
63
|
-
|
|
64
|
-
# 如果有 UnionID,检查其他应用下的 WechatUser 是否关联了 User
|
|
65
|
-
if not user and unionid:
|
|
66
|
-
other_wechat_user = (
|
|
67
|
-
await WechatUser.filter(unionid=unionid)
|
|
68
|
-
.exclude(wechat_app=app_config)
|
|
69
|
-
.first()
|
|
70
|
-
.select_related("user")
|
|
71
|
-
)
|
|
72
|
-
if other_wechat_user:
|
|
73
|
-
user = other_wechat_user.user
|
|
74
|
-
|
|
75
|
-
# 3. 创建 User(如不存在)
|
|
76
|
-
if not user:
|
|
77
|
-
# 使用 UUID 避免并发冲突
|
|
78
|
-
username = f"wx_{uuid.uuid4().hex[:8]}"
|
|
79
|
-
user = await User.create(
|
|
80
|
-
username=username,
|
|
81
|
-
password="", # 微信用户无密码
|
|
82
|
-
phone=phone,
|
|
83
|
-
nickname=nickname or f"用户{username[-4:]}",
|
|
84
|
-
avatar=avatar,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
# 4. 创建 WechatUser 关联
|
|
88
|
-
await WechatUser.create(
|
|
89
|
-
user=user,
|
|
90
|
-
wechat_app=app_config,
|
|
91
|
-
openid=openid,
|
|
92
|
-
unionid=unionid,
|
|
93
|
-
session_key=session_key,
|
|
94
|
-
nickname=nickname,
|
|
95
|
-
avatar=avatar,
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
return user
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|