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 +31 -0
- common/consts.py +3 -0
- common/exception.py +68 -0
- common/logging.py +84 -0
- common/response.py +63 -0
- common_depend/auth_depend.py +75 -0
- common_model/base_model.py +200 -0
- common_model/user_model.py +25 -0
- common_model/wechat_model.py +43 -0
- common_utlis/bcrypt_util.py +63 -0
- common_utlis/captcha_util.py +130 -0
- common_utlis/ip_util.py +83 -0
- common_utlis/jwt_util.py +47 -0
- common_utlis/snowflake_util.py +205 -0
- core/base_config.py +28 -0
- core/databases/aiodb.py +360 -0
- core/databases/aioredis.py +279 -0
- core/middleware/http_middleware.py +28 -0
- core/start.py +93 -0
- rainycode-1.0.0.dist-info/METADATA +15 -0
- rainycode-1.0.0.dist-info/RECORD +23 -0
- rainycode-1.0.0.dist-info/WHEEL +5 -0
- rainycode-1.0.0.dist-info/top_level.txt +5 -0
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
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
|