rainycode 1.0.0__py3-none-any.whl

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.
common/aiorequests.py ADDED
@@ -0,0 +1,31 @@
1
+ import json
2
+ import aiohttp
3
+
4
+
5
+ def parse_url(url, params=None, suffix=None):
6
+ if params:
7
+ query = "&".join(["{}={}".format(k, params[k]) for k in sorted(params.keys())])
8
+ if "?" in url:
9
+ url += "&" + query
10
+ else:
11
+ url += "?" + query
12
+ if suffix:
13
+ url += suffix
14
+ return url
15
+
16
+
17
+ async def get(url, params=None, headers=None, suffix=None):
18
+ url = parse_url(url, params, suffix)
19
+ async with aiohttp.ClientSession(trust_env=True) as session:
20
+ async with session.get(url, headers=headers) as response:
21
+ text = await response.text()
22
+ return json.loads(text)
23
+
24
+
25
+ async def post(url, data=None, headers=None):
26
+ async with aiohttp.ClientSession(trust_env=True) as session:
27
+ if isinstance(data, dict):
28
+ data = json.dumps(data)
29
+ async with session.post(url, data=data, headers=headers) as response:
30
+ text = await response.text()
31
+ return json.loads(text)
common/consts.py ADDED
@@ -0,0 +1,3 @@
1
+ REDIS_PREFIX_REFRESH_TOKEN: str = "auth:refresh:"
2
+ REDIS_PREFIX_USER_INFO: str = "user:info:"
3
+ REDIS_PREFIX_BLOCKLIST: str = "auth:blocklist:"
common/exception.py ADDED
@@ -0,0 +1,68 @@
1
+ from common.logging import logger
2
+ from typing import cast
3
+ from common.response import ErrorReturn
4
+ from fastapi import HTTPException, Request, status
5
+ from fastapi.exceptions import RequestValidationError, ResponseValidationError
6
+ from starlette.responses import JSONResponse
7
+
8
+
9
+ class CustomException(Exception):
10
+ """自定义异常"""
11
+ def __init__(
12
+ self,
13
+ msg: str | None = None,
14
+ code: int = 0,
15
+ data: object | None = None,
16
+ status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR
17
+ ) -> None:
18
+ super().__init__(msg)
19
+ self.msg = msg
20
+ self.code = code
21
+ self.data = data
22
+ self.status_code = status_code
23
+
24
+ def __str__(self) -> str:
25
+ return f'{self.msg}'
26
+
27
+ async def CustomExceptionHandler(request: Request, exc: CustomException) -> JSONResponse:
28
+ """自定义异常处理器"""
29
+ exc = cast(CustomException, exc)
30
+ logger.error(f"请求地址: {request.url}, 错误信息: {exc.msg}, 错误详情: {exc.data}")
31
+ return ErrorReturn(data=exc.data, msg=exc.msg, code=exc.code, status_code=exc.status_code)
32
+
33
+
34
+ async def HttpExceptionHandler(request: Request, exc: HTTPException) -> JSONResponse:
35
+ """HTTP异常处理器"""
36
+ logger.error(f"请求地址: {request.url}, 错误详情: {exc.detail}")
37
+ return ErrorReturn(data=None, msg=exc.detail)
38
+
39
+ async def RequestValidationHandle(request: Request, exc: RequestValidationError):
40
+ """请求参数验证异常处理器"""
41
+ error_mapping = {
42
+ "Field required": "请求失败,缺少必填项!",
43
+ "value is not a valid list": "类型错误,提交参数应该为列表!",
44
+ "value is not a valid int": "类型错误,提交参数应该为整数!",
45
+ "value could not be parsed to a boolean": "类型错误,提交参数应该为布尔值!",
46
+ "Input should be a valid list": "类型错误,输入应该是一个有效的列表!"
47
+ }
48
+ msg = error_mapping.get(exc.errors()[0].get('msg'), exc.errors()[0].get('msg'))
49
+ logger.error(f"请求地址: {request.url}, 错误信息: {msg}, 错误详情: {exc}")
50
+ return ErrorReturn(data=None, msg=msg, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
51
+
52
+
53
+ async def ResponseValidationHandle(request: Request, exc: ResponseValidationError) -> JSONResponse:
54
+ """响应参数验证异常处理器"""
55
+ logger.error(f"请求地址: {request.url}, 错误详情: {exc}")
56
+ return ErrorReturn(data=exc.body, msg=str(exc), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
57
+
58
+
59
+ async def ValueExceptionHandler(request: Request, exc: ValueError) -> JSONResponse:
60
+ """值异常处理器"""
61
+ logger.error(f"请求地址: {request.url}, 错误详情: {exc}")
62
+ return ErrorReturn(data=None, msg=str(exc))
63
+
64
+
65
+ async def AllExceptionHandler(request: Request, exc: Exception) -> JSONResponse:
66
+ """全局异常处理器"""
67
+ logger.error(f"请求地址: {request.url}, 错误详情: {exc}")
68
+ return ErrorReturn(data=str(exc), msg='服务器内部错误', status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
common/logging.py ADDED
@@ -0,0 +1,84 @@
1
+ import os
2
+ import sys
3
+ import logging
4
+ import traceback
5
+
6
+
7
+ def logging_config(app_name: str):
8
+ # 日志目录
9
+ log_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../log"))
10
+ if not os.path.exists(log_dir):
11
+ os.mkdir(log_dir)
12
+ file_path = log_dir + '/' + app_name + '.log'
13
+
14
+ return {
15
+ "version": 1,
16
+ # disable_existing_loggers 表示弃用已经存在的日志,True表示弃用,False表示不弃用。
17
+ "disable_existing_loggers": False,
18
+ "formatters": {
19
+ "default": {
20
+ "()": "uvicorn.logging.DefaultFormatter",
21
+ "fmt": "%(asctime)s %(levelname)s %(message)s",
22
+ "use_colors": None
23
+ },
24
+ "access": {
25
+ "()": "uvicorn.logging.AccessFormatter",
26
+ "fmt": "%(asctime)s %(levelname)s %(client_addr)s - \"%(request_line)s\" %(status_code)s"
27
+ },
28
+ 'standard': {
29
+ 'format': '%(asctime)s %(levelname)s %(message)s'
30
+ },
31
+ },
32
+ "handlers": {
33
+ "default": {
34
+ "formatter": "default",
35
+ "class": "logging.StreamHandler",
36
+ "stream": "ext://sys.stderr",
37
+ },
38
+ "access": {
39
+ "formatter": "access",
40
+ "class": "logging.StreamHandler",
41
+ "stream": "ext://sys.stdout",
42
+ },
43
+ "logfile": {
44
+ "level": "INFO",
45
+ "class": "logging.handlers.TimedRotatingFileHandler", # 保存到文件,自动切
46
+ "filename": file_path, # 日志文件的位置
47
+ "when": "midnight", # 间间隔的类型
48
+ "interval": 1, # 时间间隔
49
+ "backupCount": 60, # 能留几个日志文件
50
+ "formatter": "standard", # 使用哪种日志格式
51
+ "encoding": "utf-8", # 保存的格式
52
+ }
53
+ },
54
+ "loggers": {
55
+ "uvicorn": {
56
+ "handlers": ["default", "logfile"],
57
+ "level": "INFO",
58
+ "propagate": False # 向不向更高级别的logger传递
59
+ },
60
+ "uvicorn.error": {
61
+ "level": "INFO"
62
+ },
63
+ "uvicorn.access": {
64
+ # 隐藏该日志,防止和自定义的access日志冲突
65
+ # "handlers": ["access", "logfile"],
66
+ "level": "INFO",
67
+ "propagate": False
68
+ },
69
+ "fastapi": {
70
+ "handlers": ["default", "logfile"],
71
+ "level": "INFO",
72
+ "propagate": False
73
+ }
74
+ }
75
+ }
76
+
77
+
78
+ def log_exc(e):
79
+ typ, value, tb = sys.exc_info()
80
+ logger.error(f"Uncaught exception {repr(value)}\nTraceback:{''.join(traceback.format_tb(tb))}")
81
+
82
+
83
+ logger = logging.getLogger('fastapi')
84
+
common/response.py ADDED
@@ -0,0 +1,63 @@
1
+ from pydantic import BaseModel, Field
2
+ from starlette.background import BackgroundTask
3
+ from starlette.responses import ContentStream, JSONResponse, StreamingResponse
4
+
5
+
6
+ class ResponseSchema(BaseModel):
7
+ """响应模型"""
8
+ data: object | None = Field(default=None, description="响应数据")
9
+ msg: str | None = Field(description="响应消息")
10
+ code: int = Field(description="业务状态码")
11
+
12
+
13
+ class SuccessReturn(JSONResponse):
14
+ """成功响应类"""
15
+ def __init__(
16
+ self,
17
+ data: object | None = None,
18
+ msg: str | None = None,
19
+ code: int = 0,
20
+ status_code: int = 200
21
+ ) -> None:
22
+ content = ResponseSchema(
23
+ code=code,
24
+ msg=msg,
25
+ data=data,
26
+ ).model_dump()
27
+ super().__init__(content=content, status_code=status_code)
28
+
29
+
30
+ class ErrorReturn(JSONResponse):
31
+ """错误响应类"""
32
+ def __init__(
33
+ self,
34
+ data: object | None = None,
35
+ msg: str | None = None,
36
+ code: int = -1,
37
+ status_code: int = 500,
38
+ ) -> None:
39
+ content = ResponseSchema(
40
+ code=code,
41
+ msg=msg,
42
+ data=data,
43
+ ).model_dump()
44
+ super().__init__(content=content, status_code=status_code)
45
+
46
+
47
+ class StreamReturn(StreamingResponse):
48
+ """流式响应类"""
49
+ def __init__(
50
+ self,
51
+ data: ContentStream,
52
+ status_code: int = 200,
53
+ headers: dict[str, str] | None = None,
54
+ media_type: str | None = None,
55
+ background: BackgroundTask | None = None
56
+ ) -> None:
57
+ super().__init__(
58
+ content=data,
59
+ status_code=status_code,
60
+ media_type=media_type, # 文件类型
61
+ headers=headers, # 文件名
62
+ background=background # 文件大小
63
+ )
@@ -0,0 +1,75 @@
1
+ import json
2
+ from fastapi import Depends, Request
3
+ from fastapi.security import OAuth2PasswordBearer, SecurityScopes
4
+ from common.consts import REDIS_PREFIX_BLOCKLIST, REDIS_PREFIX_USER_INFO
5
+ from common.exception import CustomException
6
+ from common_model.user_model import User
7
+ from common_utlis.jwt_util import JwtUtil
8
+ from core.databases.aioredis import AioRedis
9
+
10
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth_center/login")
11
+
12
+ async def get_current_user(request: Request, token: str, security_scopes: SecurityScopes) -> User:
13
+ """
14
+ 获取当前登录用户
15
+ """
16
+ payload = JwtUtil.decode_token(token)
17
+ if not payload:
18
+ raise CustomException(msg="无效的令牌")
19
+
20
+ # 检查是否在黑名单中
21
+ if await AioRedis.get(f"{REDIS_PREFIX_BLOCKLIST}{token}"):
22
+ raise CustomException(msg="令牌已失效")
23
+
24
+ user_id = payload.get("sub")
25
+ if not user_id:
26
+ raise CustomException(msg="无效的令牌")
27
+
28
+ # 1. 尝试从缓存获取
29
+ user = None
30
+ user_data = await AioRedis.get(f"{REDIS_PREFIX_USER_INFO}{user_id}")
31
+ if user_data:
32
+ user = User(**json.loads(user_data))
33
+
34
+ # 2. 缓存未命中,查库
35
+ if not user:
36
+ user = await User.get_or_none(id=user_id)
37
+ if user:
38
+ user_data = await user.model_dump()
39
+ await AioRedis.set(f"{REDIS_PREFIX_USER_INFO}{user_id}", json.dumps(user_data), ex=60 * 60 * 24)
40
+
41
+ if not user:
42
+ raise CustomException(msg="用户不存在")
43
+
44
+ if not user.is_active:
45
+ raise CustomException(msg="用户已被禁用")
46
+
47
+ permissions = []
48
+ if user.is_superuser:
49
+ permissions.append('*.*.*')
50
+ else:
51
+ # TODO 从数据库中查询用户权限
52
+ pass
53
+
54
+ # 设置请求上下文
55
+ request.state.user = user
56
+ return user
57
+
58
+
59
+ async def login_require(request: Request, security_scopes: SecurityScopes, token=Depends(oauth2_scheme)) -> User:
60
+ """获取用户,获取不到报错"""
61
+ if not token:
62
+ raise CustomException('请先登录')
63
+
64
+ return await get_current_user(request, token, security_scopes)
65
+
66
+
67
+ async def login_or_none(request: Request, security_scopes: SecurityScopes, token=Depends(oauth2_scheme)) -> User | None:
68
+ """获取用户,获取不到返回空"""
69
+ if not token:
70
+ return None
71
+
72
+ try:
73
+ return await get_current_user(request, token, security_scopes)
74
+ except Exception as e:
75
+ return None
@@ -0,0 +1,200 @@
1
+ import uuid
2
+ from collections.abc import Iterable
3
+ from typing_extensions import Self
4
+ from typing import Optional, Any
5
+ from tortoise import fields, BaseDBAsyncClient
6
+ from tortoise.expressions import Q
7
+ from tortoise.queryset import BulkCreateQuery, QuerySet, QuerySetSingle
8
+ from common_utlis.snowflake_util import get_snowflake_id
9
+ from core.databases.aiodb import MySQLUtils
10
+
11
+
12
+ class BaseModel(MySQLUtils):
13
+
14
+ @classmethod
15
+ async def create(
16
+ cls,
17
+ using_db: Optional[BaseDBAsyncClient] = None,
18
+ **kwargs: Any
19
+ ) -> Self:
20
+ if not kwargs.get('id', 0) and 'id' in cls._meta.fields:
21
+ # uuid生成id
22
+ if kwargs.get('id_uuid', False):
23
+ kwargs['id'] = str(uuid.uuid4()).replace('-', '')
24
+ # 雪花算法生成id
25
+ if kwargs.get('id_snowflake', False):
26
+ kwargs['id'] = get_snowflake_id()
27
+ return await super().create(using_db=using_db, **kwargs)
28
+
29
+ @classmethod
30
+ def get(
31
+ cls,
32
+ *args: Q,
33
+ using_db: BaseDBAsyncClient | None = None,
34
+ **kwargs: Any
35
+ ) -> QuerySetSingle[Self]:
36
+ if 'deleted' in cls._meta.fields and kwargs.get('deleted', None) is None:
37
+ kwargs['deleted'] = False
38
+ return super().get(*args, using_db=using_db, **kwargs)
39
+
40
+ @classmethod
41
+ def filter(
42
+ cls,
43
+ *args: Q,
44
+ **kwargs: Any
45
+ ) -> QuerySet[Self]:
46
+ if 'deleted' in cls._meta.fields and kwargs.get('deleted', None) is None:
47
+ kwargs['deleted'] = False
48
+ return cls._meta.manager.get_queryset().filter(*args, **kwargs)
49
+
50
+ @classmethod
51
+ def bulk_create(
52
+ cls,
53
+ objects: Iterable[Self],
54
+ batch_size: Optional[int] = None,
55
+ ignore_conflicts: bool = False,
56
+ update_fields: Optional[Iterable[str]] = None,
57
+ on_conflict: Optional[Iterable[str]] = None,
58
+ using_db: Optional[BaseDBAsyncClient] = None,
59
+ **kwargs: Any
60
+ ) -> "BulkCreateQuery[Self]":
61
+ if 'id' in cls._meta.fields:
62
+ for obj in objects:
63
+ if not getattr(obj, 'id', 0):
64
+ # uuid生成id
65
+ if kwargs.get('id_uuid', False):
66
+ obj.id = str(uuid.uuid4()).replace('-', '')
67
+ # 雪花算法生成id
68
+ if kwargs.get('id_snowflake', False):
69
+ obj.id = get_snowflake_id()
70
+
71
+ params = {
72
+ "objects": objects,
73
+ "batch_size": batch_size,
74
+ "ignore_conflicts": ignore_conflicts,
75
+ "update_fields": update_fields,
76
+ "on_conflict": on_conflict,
77
+ "using_db": using_db,
78
+ }
79
+ return super().bulk_create(**params)
80
+
81
+ async def save(
82
+ self,
83
+ using_db: Optional[BaseDBAsyncClient] = None,
84
+ update_fields: Optional[Iterable[str]] = None,
85
+ force_create: bool = False,
86
+ force_update: bool = False,
87
+ **kwargs: Any
88
+ ) -> None:
89
+ if not getattr(self, 'id', 0) and kwargs.get('uuid', False) and 'id' in self._meta.fields:
90
+ setattr(self, 'id', str(uuid.uuid4()).replace('-', ''))
91
+ return await super().save(using_db=using_db, update_fields=update_fields, force_create=force_create,
92
+ force_update=force_update)
93
+
94
+
95
+ class CreateModel(BaseModel):
96
+ deleted = fields.BooleanField(default=False, description='是否已删除')
97
+ create_time = fields.DatetimeField(auto_now_add=True, description='创建时间')
98
+ creator = fields.CharField(max_length=50, null=True, description="创建人")
99
+
100
+ class Meta:
101
+ abstract = True
102
+
103
+
104
+ class UpdateModel(BaseModel):
105
+ deleted = fields.BooleanField(default=False, description='是否已删除')
106
+ create_time = fields.DatetimeField(auto_now_add=True, description='创建时间')
107
+ update_time = fields.DatetimeField(auto_now=True, description='更新时间')
108
+ creator = fields.CharField(max_length=50, null=True, description="创建人")
109
+ updater = fields.CharField(max_length=50, null=True, description="更新人")
110
+
111
+ class Meta:
112
+ abstract = True
113
+
114
+
115
+ class DeleteModel(BaseModel):
116
+ deleted = fields.BooleanField(default=False, description="是否已删除")
117
+ create_time = fields.DatetimeField(auto_now_add=True, description="创建时间")
118
+ update_time = fields.DatetimeField(auto_now=True, description="更新时间")
119
+
120
+
121
+ # class CreateRelatedMeta(type(Model)):
122
+ # """元类:更新外键字段,解决用户related_name重复问题"""
123
+ # def __new__(cls, name: str, bases: tuple, attrs: dict):
124
+ # if name == 'UpdateModel':
125
+ # return super().__new__(cls, name, bases, attrs)
126
+
127
+ # creator_related_name = f"{name.lower()}_creator"
128
+ # attrs["creator"] = fields.ForeignKeyField(
129
+ # 'main.SystemUserModel',
130
+ # null=True,
131
+ # related_name=creator_related_name,
132
+ # db_constraint=False,
133
+ # description="创建人"
134
+ # )
135
+
136
+ # new_class = super().__new__(cls, name, bases, attrs)
137
+ # return new_class
138
+
139
+
140
+ # class CreateModel(BaseModel, metaclass=CreateRelatedMeta):
141
+ # deleted = fields.BooleanField(default=False, description='是否已删除')
142
+ # create_time = fields.DatetimeField(auto_now_add=True, description='创建时间')
143
+ # creator = fields.ForeignKeyField(
144
+ # 'main.SystemUserModel',
145
+ # null=True,
146
+ # db_constraint=False,
147
+ # description="创建人"
148
+ # )
149
+
150
+ # class Meta:
151
+ # abstract = True
152
+
153
+
154
+ # class UpdateRelatedMeta(type(Model)):
155
+ # """元类:更新外键字段,解决用户related_name重复问题"""
156
+ # def __new__(cls, name: str, bases: tuple, attrs: dict):
157
+ # if name == 'UpdateModel':
158
+ # return super().__new__(cls, name, bases, attrs)
159
+
160
+ # creator_related_name = f"{name.lower()}_creator"
161
+ # attrs["creator"] = fields.ForeignKeyField(
162
+ # 'main.SystemUserModel',
163
+ # null=True,
164
+ # related_name=creator_related_name,
165
+ # db_constraint=False,
166
+ # description="创建人"
167
+ # )
168
+
169
+ # updater_related_name = f"{name.lower()}_updater"
170
+ # attrs["updater"] = fields.ForeignKeyField(
171
+ # 'main.SystemUserModel',
172
+ # null=True,
173
+ # related_name=updater_related_name,
174
+ # db_constraint=False,
175
+ # description="更新人"
176
+ # )
177
+
178
+ # new_class = super().__new__(cls, name, bases, attrs)
179
+ # return new_class
180
+
181
+ # class UpdateModel(BaseModel, metaclass=UpdateRelatedMeta):
182
+ # deleted = fields.BooleanField(default=False, description='是否已删除')
183
+ # create_time = fields.DatetimeField(auto_now_add=True, description='创建时间')
184
+ # update_time = fields.DatetimeField(auto_now=True, description='更新时间')
185
+ # creator = fields.ForeignKeyField(
186
+ # 'main.SystemUserModel',
187
+ # null=True,
188
+ # db_constraint=False,
189
+ # description="创建人"
190
+ # )
191
+
192
+ # updater = fields.ForeignKeyField(
193
+ # 'main.SystemUserModel',
194
+ # null=True,
195
+ # db_constraint=False,
196
+ # description="更新人"
197
+ # )
198
+
199
+ # class Meta:
200
+ # abstract = True
@@ -0,0 +1,25 @@
1
+ from tortoise import fields
2
+ from common_model.base_model import UpdateModel
3
+
4
+
5
+ class User(UpdateModel):
6
+ """
7
+ 用户模型
8
+ """
9
+ id = fields.IntField(pk=True, description="主键")
10
+ username = fields.CharField(max_length=50, unique=True, description="用户名")
11
+ password = fields.CharField(max_length=128, description="密码(加密)")
12
+ email = fields.CharField(max_length=100, null=True, unique=True, description="邮箱")
13
+ phone = fields.CharField(max_length=20, null=True, unique=True, description="手机号")
14
+ nickname = fields.CharField(max_length=50, null=True, description="昵称")
15
+ avatar = fields.CharField(max_length=255, null=True, description="头像URL")
16
+ is_active = fields.BooleanField(default=True, description="是否激活")
17
+ is_superuser = fields.BooleanField(default=False, description="是否超级管理员")
18
+ last_login = fields.DatetimeField(null=True, description="最后登录时间")
19
+
20
+ class Meta:
21
+ table = "user"
22
+ table_description = "用户表"
23
+
24
+ class PydanticMeta:
25
+ exclude = ["password"]
@@ -0,0 +1,43 @@
1
+ from tortoise import fields
2
+ from enum import Enum
3
+ from common_model.base_model import CreateModel
4
+
5
+ class AppTypeEnum(str, Enum):
6
+ MINI_PROGRAM = "mini_program"
7
+ OFFICIAL_ACCOUNT = "official_account"
8
+
9
+
10
+ class WechatApp(CreateModel):
11
+ """
12
+ 微信应用配置表
13
+ """
14
+ id = fields.IntField(pk=True, description="主键")
15
+ app_name = fields.CharField(max_length=50, description="应用名称")
16
+ app_id = fields.CharField(max_length=50, description="微信AppID")
17
+ app_secret = fields.CharField(max_length=100, description="微信AppSecret")
18
+ token = fields.CharField(max_length=32, null=True, description="消息校验Token")
19
+ msg_secret = fields.CharField(max_length=43, null=True, description="消息加解密Key")
20
+ app_type = fields.CharEnumField(AppTypeEnum, description="应用类型")
21
+
22
+ class Meta:
23
+ table = "wechat_app"
24
+ table_description = "微信应用配置表"
25
+
26
+
27
+ class WechatUser(CreateModel):
28
+ """
29
+ 微信用户关联表
30
+ """
31
+ id = fields.IntField(pk=True, description="主键")
32
+ user = fields.ForeignKeyField("main.User", related_name="wechat_users", description="关联用户")
33
+ wechat_app = fields.ForeignKeyField("main.WechatApp", related_name="wechat_users", description="关联微信应用")
34
+ openid = fields.CharField(max_length=64, index=True, description="微信OpenID")
35
+ unionid = fields.CharField(max_length=64, null=True, index=True, description="微信UnionID")
36
+ session_key = fields.CharField(max_length=128, null=True, description="会话密钥(仅小程序)")
37
+ nickname = fields.CharField(max_length=64, null=True, description="微信昵称")
38
+ avatar = fields.CharField(max_length=255, null=True, description="微信头像")
39
+
40
+ class Meta:
41
+ table = "wechat_user"
42
+ table_description = "微信用户关联表"
43
+ unique_together = (("openid", "wechat_app"),)
@@ -0,0 +1,63 @@
1
+ from passlib.context import CryptContext
2
+
3
+
4
+ # 密码加密配置
5
+ PwdContext = CryptContext(
6
+ schemes=["bcrypt"],
7
+ deprecated="auto",
8
+ bcrypt__rounds=12 # 设置加密轮数,增加安全性
9
+ )
10
+
11
+
12
+ class PwdUtil:
13
+ """
14
+ 密码工具类,提供密码加密和验证功能
15
+ """
16
+
17
+ @classmethod
18
+ def verify_password(cls, plain_password: str, password_hash: str) -> bool:
19
+ """
20
+ 校验密码是否匹配
21
+
22
+ Args:
23
+ plain_password: 明文密码
24
+ password_hash: 加密后的密码哈希值
25
+
26
+ Returns:
27
+ bool: 密码是否匹配
28
+ """
29
+ return PwdContext.verify(plain_password, password_hash)
30
+
31
+ @classmethod
32
+ def set_password_hash(cls, password: str) -> str:
33
+ """
34
+ 对密码进行加密
35
+
36
+ Args:
37
+ password: 明文密码
38
+
39
+ Returns:
40
+ str: 加密后的密码哈希值
41
+ """
42
+ return PwdContext.hash(password)
43
+
44
+ @classmethod
45
+ def check_password_strength(cls, password: str) -> str | None:
46
+ """
47
+ 检查密码强度
48
+
49
+ Args:
50
+ password: 明文密码
51
+
52
+ Returns:
53
+ str: 如果密码强度不够返回提示信息,否则返回None
54
+ """
55
+ if len(password) < 6:
56
+ return "密码长度至少6位"
57
+ if not any(c.isupper() for c in password):
58
+ return "密码需要包含大写字母"
59
+ if not any(c.islower() for c in password):
60
+ return "密码需要包含小写字母"
61
+ if not any(c.isdigit() for c in password):
62
+ return "密码需要包含数字"
63
+ return None