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.
Files changed (37) hide show
  1. {rainycode-1.1.0 → rainycode-1.1.2}/PKG-INFO +1 -1
  2. {rainycode-1.1.0 → rainycode-1.1.2}/common_base/consts.py +0 -1
  3. {rainycode-1.1.0 → rainycode-1.1.2}/common_depends/auth_depend.py +1 -0
  4. {rainycode-1.1.0 → rainycode-1.1.2}/common_servers/api/auth_api.py +5 -16
  5. {rainycode-1.1.0 → rainycode-1.1.2}/common_servers/service/auth_svc.py +126 -42
  6. {rainycode-1.1.0 → rainycode-1.1.2}/common_utils/bcrypt_util.py +2 -4
  7. {rainycode-1.1.0 → rainycode-1.1.2}/pyproject.toml +1 -1
  8. {rainycode-1.1.0 → rainycode-1.1.2}/rainycode.egg-info/PKG-INFO +1 -1
  9. {rainycode-1.1.0 → rainycode-1.1.2}/rainycode.egg-info/SOURCES.txt +0 -1
  10. rainycode-1.1.0/common_servers/managers/wechat_user_manager.py +0 -98
  11. {rainycode-1.1.0 → rainycode-1.1.2}/common_base/aiorequests.py +0 -0
  12. {rainycode-1.1.0 → rainycode-1.1.2}/common_base/exception.py +0 -0
  13. {rainycode-1.1.0 → rainycode-1.1.2}/common_base/logging.py +0 -0
  14. {rainycode-1.1.0 → rainycode-1.1.2}/common_base/response.py +0 -0
  15. {rainycode-1.1.0 → rainycode-1.1.2}/common_models/__init__.py +0 -0
  16. {rainycode-1.1.0 → rainycode-1.1.2}/common_models/base_model.py +0 -0
  17. {rainycode-1.1.0 → rainycode-1.1.2}/common_models/user_model.py +0 -0
  18. {rainycode-1.1.0 → rainycode-1.1.2}/common_models/wechat_model.py +0 -0
  19. {rainycode-1.1.0 → rainycode-1.1.2}/common_servers/api/wechat_api.py +0 -0
  20. {rainycode-1.1.0 → rainycode-1.1.2}/common_servers/router.py +0 -0
  21. {rainycode-1.1.0 → rainycode-1.1.2}/common_servers/schemas/__init__.py +0 -0
  22. {rainycode-1.1.0 → rainycode-1.1.2}/common_servers/schemas/auth_schema.py +0 -0
  23. {rainycode-1.1.0 → rainycode-1.1.2}/common_servers/service/wechat_svc.py +0 -0
  24. {rainycode-1.1.0 → rainycode-1.1.2}/common_utils/captcha_util.py +0 -0
  25. {rainycode-1.1.0 → rainycode-1.1.2}/common_utils/ip_util.py +0 -0
  26. {rainycode-1.1.0 → rainycode-1.1.2}/common_utils/jwt_util.py +0 -0
  27. {rainycode-1.1.0 → rainycode-1.1.2}/common_utils/snowflake_util.py +0 -0
  28. {rainycode-1.1.0 → rainycode-1.1.2}/common_utils/wechat_util.py +0 -0
  29. {rainycode-1.1.0 → rainycode-1.1.2}/core/base_config.py +0 -0
  30. {rainycode-1.1.0 → rainycode-1.1.2}/core/databases/aiodb.py +0 -0
  31. {rainycode-1.1.0 → rainycode-1.1.2}/core/databases/aioredis.py +0 -0
  32. {rainycode-1.1.0 → rainycode-1.1.2}/core/middleware/http_middleware.py +0 -0
  33. {rainycode-1.1.0 → rainycode-1.1.2}/core/start.py +0 -0
  34. {rainycode-1.1.0 → rainycode-1.1.2}/rainycode.egg-info/dependency_links.txt +0 -0
  35. {rainycode-1.1.0 → rainycode-1.1.2}/rainycode.egg-info/requires.txt +0 -0
  36. {rainycode-1.1.0 → rainycode-1.1.2}/rainycode.egg-info/top_level.txt +0 -0
  37. {rainycode-1.1.0 → rainycode-1.1.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rainycode
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: FastAPI base modules
5
5
  Requires-Python: >=3.8
6
6
  Requires-Dist: build==1.4.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:"
@@ -53,6 +53,7 @@ async def get_current_user(request: Request, token: str, security_scopes: Securi
53
53
 
54
54
  # 设置请求上下文
55
55
  request.state.user = user
56
+ request.state.token = token
56
57
  return user
57
58
 
58
59
 
@@ -1,7 +1,7 @@
1
1
  from common_models.user_model import User
2
- from fastapi import APIRouter, Security, Body, Depends, Query, Request
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, oauth2_scheme
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
- token: str = Depends(oauth2_scheme),
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
- return await cls._create_tokens(user)
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
- return await cls._create_tokens(user)
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
- # 使用 WechatUserManager 创建或绑定用户
245
- user = await WechatUserManager.bind_or_register(
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 = f"{state}_userinfo"
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. 使用 WechatUserManager 处理用户绑定/注册
361
- return await WechatUserManager.bind_or_register(
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
- async def _build_redirect_response(cls, redirect_uri: str, token_data: dict, state: str | None = None) -> RedirectResponse:
429
- """构建带临时码的重定向响应"""
430
- # 生成临时授权码
431
- temp_code = str(uuid.uuid4())
432
-
433
- # 缓存 token 数据
434
- await AioRedis.set(
435
- f"{REDIS_PREFIX_TEMP_CODE}{temp_code}",
436
- json.dumps(token_data),
437
- ex=TEMP_CODE_EXPIRE_SECONDS
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
- # 构建重定向 URL
441
- params = {"temp_code": temp_code}
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
- redirect_url = f"{redirect_uri}?{urlencode(params)}"
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rainycode"
7
- version = "1.1.0"
7
+ version = "1.1.2"
8
8
  description = "FastAPI base modules"
9
9
  requires-python = ">=3.8"
10
10
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rainycode
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: FastAPI base modules
5
5
  Requires-Python: >=3.8
6
6
  Requires-Dist: build==1.4.0
@@ -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