rainycode 1.4.4__tar.gz → 1.5.7__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 (40) hide show
  1. {rainycode-1.4.4 → rainycode-1.5.7}/PKG-INFO +2 -1
  2. rainycode-1.5.7/common_base/aiorequests.py +55 -0
  3. {rainycode-1.4.4 → rainycode-1.5.7}/common_base/exception.py +8 -6
  4. {rainycode-1.4.4 → rainycode-1.5.7}/common_depends/auth_depend.py +9 -7
  5. {rainycode-1.4.4 → rainycode-1.5.7}/common_models/__init__.py +2 -1
  6. rainycode-1.5.7/common_models/file_model.py +25 -0
  7. {rainycode-1.4.4 → rainycode-1.5.7}/common_servers/api/auth_api.py +20 -6
  8. {rainycode-1.4.4 → rainycode-1.5.7}/common_servers/api/wechat_api.py +3 -2
  9. {rainycode-1.4.4 → rainycode-1.5.7}/common_servers/router.py +2 -1
  10. {rainycode-1.4.4 → rainycode-1.5.7}/common_servers/service/auth_svc.py +10 -9
  11. rainycode-1.5.7/common_servers/service/file_svc.py +52 -0
  12. {rainycode-1.4.4 → rainycode-1.5.7}/common_utils/datetime_util.py +2 -2
  13. {rainycode-1.4.4 → rainycode-1.5.7}/common_utils/ip_util.py +8 -0
  14. {rainycode-1.4.4 → rainycode-1.5.7}/common_utils/jwt_util.py +3 -1
  15. {rainycode-1.4.4 → rainycode-1.5.7}/common_utils/snowflake_util.py +1 -1
  16. {rainycode-1.4.4 → rainycode-1.5.7}/common_utils/wechat_util.py +3 -3
  17. {rainycode-1.4.4 → rainycode-1.5.7}/core/base_config.py +10 -1
  18. {rainycode-1.4.4 → rainycode-1.5.7}/core/start.py +1 -1
  19. rainycode-1.5.7/pyproject.toml +61 -0
  20. {rainycode-1.4.4 → rainycode-1.5.7}/rainycode.egg-info/PKG-INFO +2 -1
  21. {rainycode-1.4.4 → rainycode-1.5.7}/rainycode.egg-info/SOURCES.txt +2 -0
  22. {rainycode-1.4.4 → rainycode-1.5.7}/rainycode.egg-info/requires.txt +1 -0
  23. rainycode-1.4.4/common_base/aiorequests.py +0 -39
  24. rainycode-1.4.4/pyproject.toml +0 -36
  25. {rainycode-1.4.4 → rainycode-1.5.7}/common_base/consts.py +0 -0
  26. {rainycode-1.4.4 → rainycode-1.5.7}/common_base/logging.py +0 -0
  27. {rainycode-1.4.4 → rainycode-1.5.7}/common_base/response.py +0 -0
  28. {rainycode-1.4.4 → rainycode-1.5.7}/common_models/base_model.py +0 -0
  29. {rainycode-1.4.4 → rainycode-1.5.7}/common_models/user_model.py +0 -0
  30. {rainycode-1.4.4 → rainycode-1.5.7}/common_models/wechat_model.py +0 -0
  31. {rainycode-1.4.4 → rainycode-1.5.7}/common_servers/schemas/auth_schema.py +0 -0
  32. {rainycode-1.4.4 → rainycode-1.5.7}/common_servers/service/wechat_svc.py +0 -0
  33. {rainycode-1.4.4 → rainycode-1.5.7}/common_utils/bcrypt_util.py +0 -0
  34. {rainycode-1.4.4 → rainycode-1.5.7}/common_utils/captcha_util.py +0 -0
  35. {rainycode-1.4.4 → rainycode-1.5.7}/core/databases/aiodb.py +0 -0
  36. {rainycode-1.4.4 → rainycode-1.5.7}/core/databases/aioredis.py +0 -0
  37. {rainycode-1.4.4 → rainycode-1.5.7}/core/middleware/http_middleware.py +0 -0
  38. {rainycode-1.4.4 → rainycode-1.5.7}/rainycode.egg-info/dependency_links.txt +0 -0
  39. {rainycode-1.4.4 → rainycode-1.5.7}/rainycode.egg-info/top_level.txt +0 -0
  40. {rainycode-1.4.4 → rainycode-1.5.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rainycode
3
- Version: 1.4.4
3
+ Version: 1.5.7
4
4
  Summary: FastAPI base modules
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: aerich==0.9.2
@@ -17,6 +17,7 @@ Requires-Dist: pycryptodome==3.23.0
17
17
  Requires-Dist: uvicorn[standard]==0.41.0
18
18
  Requires-Dist: tortoise-orm[asyncmy]==0.25.4
19
19
  Requires-Dist: pydantic-settings==2.13.1
20
+ Requires-Dist: python-multipart==0.0.32
20
21
  Provides-Extra: release
21
22
  Requires-Dist: build==1.4.0; extra == "release"
22
23
  Requires-Dist: twine==6.1.0; extra == "release"
@@ -0,0 +1,55 @@
1
+ import json
2
+ import ssl
3
+
4
+ import aiohttp
5
+ import certifi
6
+
7
+ # 模块级别 SSL 上下文,使用 certifi 证书,避免重复创建
8
+ _SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where())
9
+
10
+ # 连接池:跨请求复用 keep-alive 长连接(而非每次新建 connector)。
11
+ # limit: 全池同时最大连接数(含活跃与 keep-alive 空闲复用)
12
+ # limit_per_host: 单个 host 端点同时最大连接数,限制对单一对端的并发/占用
13
+ # 惰性初始化:TCPConnector 构造需要运行中的事件循环,只能在首次请求(async 上下文)时创建,
14
+ # 不能在模块 import 时同步 create(否则抛 RuntimeError: no running event loop)。
15
+ _CONNECTOR: aiohttp.TCPConnector | None = None
16
+
17
+
18
+ def _get_connector() -> aiohttp.TCPConnector:
19
+ global _CONNECTOR
20
+ if _CONNECTOR is None:
21
+ _CONNECTOR = aiohttp.TCPConnector(ssl=_SSL_CONTEXT, limit=300, limit_per_host=100)
22
+ return _CONNECTOR
23
+
24
+
25
+ def parse_url(url, params=None, suffix=None):
26
+ if params:
27
+ query = "&".join([f"{k}={params[k]}" for k in sorted(params.keys())])
28
+ if "?" in url:
29
+ url += "&" + query
30
+ else:
31
+ url += "?" + query
32
+ if suffix:
33
+ url += suffix
34
+ return url
35
+
36
+
37
+ async def get(url, params=None, headers=None, suffix=None):
38
+ url = parse_url(url, params, suffix)
39
+ async with (
40
+ aiohttp.ClientSession(connector=_get_connector(), trust_env=True) as session,
41
+ session.get(url, headers=headers) as response,
42
+ ):
43
+ text = await response.text()
44
+ return json.loads(text)
45
+
46
+
47
+ async def post(url, data=None, headers=None):
48
+ if isinstance(data, dict):
49
+ data = json.dumps(data)
50
+ async with (
51
+ aiohttp.ClientSession(connector=_get_connector(), trust_env=True) as session,
52
+ session.post(url, data=data, headers=headers) as response,
53
+ ):
54
+ text = await response.text()
55
+ return json.loads(text)
@@ -1,18 +1,19 @@
1
1
  from typing import cast
2
2
 
3
- from common_base.logging import logger
4
- from common_base.response import ErrorReturn
5
3
  from fastapi import HTTPException, Request, status
6
4
  from fastapi.exceptions import RequestValidationError, ResponseValidationError
7
5
  from starlette.responses import JSONResponse
8
6
 
7
+ from common_base.logging import logger
8
+ from common_base.response import ErrorReturn
9
+
9
10
 
10
11
  class CustomException(Exception):
11
12
  """自定义异常"""
12
13
  def __init__(
13
- self,
14
+ self,
14
15
  msg: str | None = None,
15
- code: int = 0,
16
+ code: int = -1,
16
17
  data: object | None = None,
17
18
  status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR
18
19
  ) -> None:
@@ -41,10 +42,11 @@ async def RequestValidationHandle(request: Request, exc: RequestValidationError)
41
42
  """请求参数验证异常处理器"""
42
43
  error_mapping = {
43
44
  "Field required": "请求失败,缺少必填项!",
44
- "value is not a valid list": "类型错误,提交参数应该为列表!",
45
+ "value is not a valid list": "类型错误,提交参数应该为列表!",
45
46
  "value is not a valid int": "类型错误,提交参数应该为整数!",
46
47
  "value could not be parsed to a boolean": "类型错误,提交参数应该为布尔值!",
47
- "Input should be a valid list": "类型错误,输入应该是一个有效的列表!"
48
+ "Input should be a valid list": "类型错误,输入应该是一个有效的列表!",
49
+ "String should have at least 6 characters": "密码长度不能小于6位!"
48
50
  }
49
51
  msg = error_mapping.get(exc.errors()[0].get('msg'), exc.errors()[0].get('msg'))
50
52
  logger.error(f"请求地址: {request.url}, 错误信息: {msg}, 错误详情: {exc}")
@@ -1,5 +1,8 @@
1
1
  import json
2
2
 
3
+ from fastapi import Depends, Request
4
+ from fastapi.security import OAuth2PasswordBearer, SecurityScopes
5
+
3
6
  from common_base.consts import (
4
7
  REDIS_PREFIX_BLOCKLIST,
5
8
  REDIS_PREFIX_USER_INFO,
@@ -8,10 +11,9 @@ from common_base.exception import CustomException
8
11
  from common_models.user_model import User
9
12
  from common_utils.jwt_util import JwtUtil
10
13
  from core.databases.aioredis import AioRedis
11
- from fastapi import Depends, Request
12
- from fastapi.security import OAuth2PasswordBearer, SecurityScopes
14
+ from core.start import base_config
13
15
 
14
- oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth_center/login")
16
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"/api/{base_config.app_name}/auth/login", auto_error=False)
15
17
 
16
18
  async def get_current_user(request: Request, token: str, security_scopes: SecurityScopes) -> User:
17
19
  """
@@ -19,15 +21,15 @@ async def get_current_user(request: Request, token: str, security_scopes: Securi
19
21
  """
20
22
  payload = JwtUtil.decode_token(token)
21
23
  if not payload:
22
- raise CustomException(msg="无效的令牌")
24
+ raise CustomException(msg="无效的令牌", status_code=401)
23
25
 
24
26
  # 检查是否在黑名单中
25
27
  if await AioRedis.get(f"{REDIS_PREFIX_BLOCKLIST}{token}"):
26
- raise CustomException(msg="令牌已失效")
28
+ raise CustomException(msg="令牌已失效", status_code=401)
27
29
 
28
30
  user_id = payload.get("sub")
29
31
  if not user_id:
30
- raise CustomException(msg="无效的令牌")
32
+ raise CustomException(msg="无效的令牌", status_code=401)
31
33
 
32
34
  # 1. 尝试从缓存获取
33
35
  user = None
@@ -64,7 +66,7 @@ async def get_current_user(request: Request, token: str, security_scopes: Securi
64
66
  async def login_require(request: Request, security_scopes: SecurityScopes, token=Depends(oauth2_scheme)) -> User:
65
67
  """获取用户,获取不到报错"""
66
68
  if not token:
67
- raise CustomException('请先登录')
69
+ raise CustomException('请先登录', status_code=401)
68
70
 
69
71
  return await get_current_user(request, token, security_scopes)
70
72
 
@@ -1,4 +1,5 @@
1
+ from common_models.file_model import File
1
2
  from common_models.user_model import User
2
3
  from common_models.wechat_model import WechatApp, WechatUser
3
4
 
4
- __all__ = ["User", "WechatApp", "WechatUser"]
5
+ __all__ = ["File", "User", "WechatApp", "WechatUser"]
@@ -0,0 +1,25 @@
1
+ from tortoise import fields
2
+
3
+ from common_models.base_model import CreateModel
4
+ from core.base_config import base_config
5
+
6
+
7
+ class File(CreateModel):
8
+ """
9
+ 文件表
10
+ """
11
+ id = fields.IntField(primary_key=True, description="主键")
12
+ name = fields.CharField(max_length=255, description="原始文件名")
13
+ url = fields.CharField(max_length=255, description="相对路径(不含前缀, 如 dishes/xxx.jpg)")
14
+ size = fields.IntField(default=0, description="文件字节数")
15
+ ext = fields.CharField(max_length=10, description="扩展名")
16
+ md5 = fields.CharField(max_length=32, null=True, description="文件内容 MD5(用于识别相同文件)")
17
+
18
+ class Meta:
19
+ table = "file"
20
+ table_description = "上传文件表"
21
+
22
+ def fix_data(self, result: dict) -> None:
23
+ """序列化后钩子(库 model2dict 调用):若含 url 则拼接可配置前缀。"""
24
+ if 'url' in result:
25
+ result['url'] = f"{base_config.upload_url_prefix}/{result['url']}"
@@ -1,13 +1,27 @@
1
+ from fastapi import APIRouter, Body, Depends, Query, Request, Security
2
+
3
+ from common_base.exception import CustomException
1
4
  from common_base.response import SuccessReturn
2
5
  from common_depends.auth_depend import login_require
3
6
  from common_models.user_model import User
4
- from common_servers.schemas.auth_schema import *
5
- from common_servers.service.auth_svc import AuthService
6
- from fastapi import APIRouter, Body, Query, Request, Security
7
+ from common_servers.schemas.auth_schema import TokenResponse, UserInfo
8
+ from common_servers.service.auth_svc import AuthService, UserLogin, UserRegister, WechatLogin
9
+ from core.base_config import base_config
7
10
 
8
11
  router = APIRouter(prefix="/auth")
9
12
 
10
- @router.post("/register", summary="用户注册")
13
+
14
+ def password_login_require() -> None:
15
+ """账号密码登录门禁。
16
+
17
+ 以 APP_DEBUG 区分环境:生产(APP_DEBUG=False)关闭 /auth/register、/auth/login,
18
+ 仅保留微信网页授权与小程序登录;本地开发/测试(APP_DEBUG=True)不受影响。
19
+ """
20
+ if not base_config.app_debug:
21
+ raise CustomException(msg="账号密码登录已停用,请使用微信登录", status_code=403)
22
+
23
+
24
+ @router.post("/register", summary="用户注册", dependencies=[Depends(password_login_require)])
11
25
  async def register(user_in: UserRegister):
12
26
  user = await AuthService.register(user_in)
13
27
  return SuccessReturn(
@@ -15,7 +29,7 @@ async def register(user_in: UserRegister):
15
29
  )
16
30
 
17
31
 
18
- @router.post("/login", summary="用户密码登录")
32
+ @router.post("/login", summary="用户密码登录", dependencies=[Depends(password_login_require)])
19
33
  async def login(user_in: UserLogin, request: Request):
20
34
  client_ip = request.client.host if request.client else "unknown"
21
35
  token_data = await AuthService.login(user_in, client_ip)
@@ -70,4 +84,4 @@ async def get_wechat_oauth_url(
70
84
  - 如果用户未注册,自动重定向到 snsapi_userinfo 授权
71
85
  """
72
86
  authorize_url = await AuthService.get_wechat_oauth_url(app_pk, redirect_uri, state)
73
- return SuccessReturn(data={"authorize_url": authorize_url})
87
+ return SuccessReturn(data={"authorize_url": authorize_url})
@@ -1,13 +1,14 @@
1
1
  import json
2
2
  from urllib.parse import urlencode
3
3
 
4
+ from fastapi import APIRouter, Query, Request, Response
5
+ from fastapi.responses import RedirectResponse
6
+
4
7
  from common_base.consts import REDIS_PREFIX_OAUTH_STATE
5
8
  from common_base.exception import CustomException
6
9
  from common_servers.service.auth_svc import AuthService
7
10
  from common_servers.service.wechat_svc import WechatService
8
11
  from core.databases.aioredis import AioRedis
9
- from fastapi import APIRouter, Query, Request, Response
10
- from fastapi.responses import RedirectResponse
11
12
 
12
13
  router = APIRouter(prefix="/wechat")
13
14
 
@@ -1,6 +1,7 @@
1
+ from fastapi import APIRouter
2
+
1
3
  from common_servers.api.auth_api import router as auth_router
2
4
  from common_servers.api.wechat_api import router as wechat_router
3
- from fastapi import APIRouter
4
5
 
5
6
  base_router = APIRouter()
6
7
 
@@ -4,6 +4,10 @@ import uuid
4
4
  from datetime import datetime, timedelta
5
5
  from urllib.parse import urlencode
6
6
 
7
+ from fastapi.responses import RedirectResponse
8
+ from tortoise.expressions import Q
9
+ from tortoise.transactions import atomic
10
+
7
11
  from common_base.consts import (
8
12
  REDIS_PREFIX_BLOCKLIST,
9
13
  REDIS_PREFIX_LOGIN_LOCK_IP,
@@ -28,9 +32,6 @@ from common_utils.jwt_util import JwtUtil
28
32
  from common_utils.wechat_util import WechatUtil
29
33
  from core.base_config import base_config
30
34
  from core.databases.aioredis import AioRedis
31
- from fastapi.responses import RedirectResponse
32
- from tortoise.expressions import Q
33
- from tortoise.transactions import atomic
34
35
 
35
36
  # 登录限流配置
36
37
  LOGIN_FAIL_LIMIT = 5 # 最大失败次数
@@ -105,11 +106,11 @@ class AuthService:
105
106
  """
106
107
  user_id = await AioRedis.get(f"{REDIS_PREFIX_REFRESH_TOKEN}{refresh_token}")
107
108
  if not user_id:
108
- raise CustomException(msg="无效或已过期的刷新令牌")
109
+ raise CustomException(msg="无效或已过期的刷新令牌", status_code=401)
109
110
  try:
110
111
  user_id = int(user_id)
111
112
  except (ValueError, TypeError):
112
- raise CustomException(msg="无效的刷新令牌")
113
+ raise CustomException(msg="无效的刷新令牌", status_code=401) from None
113
114
 
114
115
  user = await User.get_or_none(id=user_id)
115
116
  if not user or not user.is_active:
@@ -178,7 +179,7 @@ class AuthService:
178
179
 
179
180
  # 构建回调地址
180
181
  api_prefix = f"/api/{base_config.app_name}" if base_config.app_name else ""
181
- callback_url = f"{base_config.base_url}{api_prefix}/wechat/oauth/callback"
182
+ callback_url = f"{base_config.base_url}{api_prefix}/wechat/callback"
182
183
 
183
184
  return WechatUtil.build_oauth_url(
184
185
  appid=app_config.app_id,
@@ -208,8 +209,8 @@ class AuthService:
208
209
  cached_data = json.loads(cache_value)
209
210
  redirect_uri = cached_data.get("redirect_uri")
210
211
  app_pk = cached_data.get("app_pk")
211
- except (json.JSONDecodeError, TypeError):
212
- raise CustomException(msg="授权数据格式错误")
212
+ except (json.JSONDecodeError, TypeError) as e:
213
+ raise CustomException(msg="授权数据格式错误") from e
213
214
 
214
215
  if not redirect_uri or not app_pk:
215
216
  raise CustomException(msg="授权数据不完整")
@@ -277,7 +278,7 @@ class AuthService:
277
278
 
278
279
  # 构建 snsapi_userinfo 授权链接
279
280
  api_prefix = f"/api/{base_config.app_name}" if base_config.app_name else ""
280
- callback_url = f"{base_config.base_url}{api_prefix}/wechat/oauth/callback"
281
+ callback_url = f"{base_config.base_url}{api_prefix}/wechat/callback"
281
282
  userinfo_auth_url = WechatUtil.build_oauth_url(
282
283
  appid=wechat_app.app_id,
283
284
  redirect_uri=callback_url,
@@ -0,0 +1,52 @@
1
+ import hashlib
2
+ import uuid
3
+ from pathlib import Path
4
+
5
+ from common_base.exception import CustomException
6
+ from common_models.file_model import File
7
+ from common_models.user_model import User
8
+ from core.base_config import base_config
9
+
10
+
11
+ class FileService:
12
+ """通用文件上传服务:校验格式/大小、写盘、落 File 注册表、按 md5 去重。"""
13
+
14
+ @classmethod
15
+ async def upload(
16
+ cls,
17
+ user: User,
18
+ file,
19
+ *,
20
+ subdir: str,
21
+ allowed_exts: set[str] = base_config.upload_allowed_image_exts,
22
+ max_bytes: int = base_config.upload_max_bytes,
23
+ ) -> dict:
24
+ ext = file.filename.rsplit('.', 1)[-1].lower() if '.' in file.filename else ''
25
+ if ext not in allowed_exts:
26
+ raise CustomException(msg="不支持的文件格式")
27
+
28
+ content = await file.read()
29
+ if len(content) > max_bytes:
30
+ raise CustomException(msg="文件大小超限")
31
+
32
+ md5 = hashlib.md5(content).hexdigest()
33
+ existing = await File.get_or_none(md5=md5)
34
+ if existing:
35
+ # 相同文件已存在,直接复用(不重复写盘),仅返回其注册信息
36
+ return await File.model2dict(existing)
37
+
38
+ filename = f"{uuid.uuid4().hex}.{ext}"
39
+ relative = f"{subdir}/{filename}" # 磁盘相对路径 & File.url(不含前缀)
40
+ path = Path(base_config.upload_root) / relative
41
+ path.parent.mkdir(parents=True, exist_ok=True)
42
+ path.write_bytes(content)
43
+
44
+ row = await File.create(
45
+ name=file.filename or filename,
46
+ url=relative,
47
+ size=len(content),
48
+ ext=ext,
49
+ md5=md5,
50
+ creator_id=user.id,
51
+ )
52
+ return await File.model2dict(row)
@@ -7,6 +7,6 @@ from zoneinfo import ZoneInfo
7
7
  CN_TZ = ZoneInfo('Asia/Shanghai')
8
8
 
9
9
 
10
- def now_app() -> datetime:
11
- """返回当前时刻的 aware Asia/Shanghai datetime(与 DB 读回值时间基准一致)。"""
10
+ def chinese_now() -> datetime:
11
+ """返回当前时刻的 aware Asia/Shanghai datetime"""
12
12
  return datetime.now(CN_TZ)
@@ -81,3 +81,11 @@ class IpUtil:
81
81
  except Exception as e:
82
82
  # 获取IP归属地失败
83
83
  return "未知"
84
+
85
+ @staticmethod
86
+ def get_client_ip(request) -> str:
87
+ """从 Request 解析客户端 IP:代理场景优先 X-Forwarded-For,否则取直连 client.host。"""
88
+ forwarded = request.headers.get('x-forwarded-for')
89
+ if forwarded:
90
+ return forwarded.split(',')[0].strip()
91
+ return request.client.host if request.client else "unknown"
@@ -1,7 +1,9 @@
1
1
  from datetime import datetime, timedelta
2
2
  from typing import Any
3
+ from zoneinfo import ZoneInfo
3
4
 
4
5
  import jwt
6
+
5
7
  from core.base_config import base_config
6
8
 
7
9
 
@@ -25,7 +27,7 @@ class JwtUtil:
25
27
  str: JWT Token
26
28
  """
27
29
  to_encode = data.copy()
28
- expire = datetime.now() + expires_delta
30
+ expire = datetime.now(ZoneInfo('Asia/Shanghai')) + expires_delta
29
31
 
30
32
  to_encode.update({"exp": expire})
31
33
  encoded_jwt = jwt.encode(to_encode, cls.SECRET_KEY, algorithm=cls.ALGORITHM)
@@ -144,7 +144,7 @@ def get_machine_info() -> tuple[int, int]:
144
144
 
145
145
  return datacenter_id, machine_id
146
146
  except Exception as e:
147
- raise RuntimeError(f"获取机器信息失败: {e}")
147
+ raise RuntimeError(f"获取机器信息失败: {e}") from e
148
148
 
149
149
 
150
150
  # 单例模式,默认实例
@@ -50,9 +50,9 @@ class WechatUtil:
50
50
  return data
51
51
  except Exception as e:
52
52
  if isinstance(e, CustomException):
53
- raise e
53
+ raise e from e
54
54
  logger.error(f"Wechat API request error: {e}")
55
- raise CustomException(msg="微信服务暂时不可用")
55
+ raise CustomException(msg="微信服务暂时不可用") from None
56
56
 
57
57
  @classmethod
58
58
  async def get_oauth_access_token(cls, appid: str, secret: str, code: str) -> dict:
@@ -116,4 +116,4 @@ class WechatUtil:
116
116
  return data
117
117
  except Exception as e:
118
118
  logger.error(f"Decrypt wechat data failed: {e}")
119
- raise CustomException(msg="微信数据解密失败")
119
+ raise CustomException(msg="微信数据解密失败") from None
@@ -13,6 +13,11 @@ class BaseConfig(BaseSettings):
13
13
  # 基础 URL
14
14
  base_url: str = ""
15
15
 
16
+ # 文件上传:本地存储根目录 + 访问 URL 前缀。
17
+ # upload_url_prefix 可为相对路径(/uploads)或完整 URL(https://cdn.xxx.com/uploads),
18
+ upload_root: str = 'uploads'
19
+ upload_url_prefix: str = '/uploads'
20
+
16
21
  # 跨域
17
22
  cors_origins: list[str] = ['*']
18
23
  cors_allow_credentials: bool = True
@@ -23,10 +28,14 @@ class BaseConfig(BaseSettings):
23
28
  jwt_secret_key: str = "bgb0tnl9d58+6n-6h-ea&u^1#s0ccp!794=krylacjq75vzps$"
24
29
 
25
30
  # Access Token 有效期
26
- access_token_expire_minutes: int = 30
31
+ access_token_expire_minutes: int = 1440
27
32
  # Refresh Token 有效期
28
33
  refresh_token_expire_days: int = 30
29
34
 
35
+ # 文件上传约束(common_servers/service/file_svc.py)
36
+ upload_max_bytes: int = 5 * 1024 * 1024 # 5MB
37
+ upload_allowed_image_exts: set[str] = {'jpg', 'jpeg', 'png', 'webp'}
38
+
30
39
  model_config = SettingsConfigDict(
31
40
  env_file=('envs/dev.env', 'envs/pro.env'),
32
41
  env_file_encoding="utf-8",
@@ -59,7 +59,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
59
59
  logger.info(f'【{config.app_title}】服务成功启动...')
60
60
  except Exception as e:
61
61
  logger.error(f'【{config.app_title}】服务启动失败: {str(e)}')
62
- raise e
62
+ raise e from e
63
63
 
64
64
  yield
65
65
 
@@ -0,0 +1,61 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "rainycode"
7
+ version = "1.5.7"
8
+ description = "FastAPI base modules"
9
+ requires-python = ">=3.10"
10
+
11
+ dependencies = [
12
+ "aerich == 0.9.2",
13
+ "fastapi == 0.133.1",
14
+ "aiohttp == 3.13.3",
15
+ "passlib==1.7.4",
16
+ "redis == 7.2.1",
17
+ "pyjwt == 2.11.0",
18
+ "pillow == 12.1.1",
19
+ "bcrypt == 4.0.1",
20
+ "certifi == 2026.2.25",
21
+ "cryptography == 46.0.5",
22
+ "pycryptodome == 3.23.0",
23
+ "uvicorn[standard] == 0.41.0",
24
+ "tortoise-orm[asyncmy] == 0.25.4",
25
+ "pydantic-settings == 2.13.1",
26
+ "python-multipart==0.0.32"
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ release = [
31
+ "build == 1.4.0",
32
+ "twine == 6.1.0",
33
+ ]
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["."]
37
+ include = ["core*", "common*"]
38
+
39
+
40
+ [tool.ruff]
41
+ target-version = "py312" # 按 Python 3.12 语法判断,UP 规则才不会误降级
42
+ line-length = 120 # 中文注释 + FastAPI 调用链较长,120 比 88 舒服
43
+
44
+ [tool.ruff.lint]
45
+ select = [
46
+ "E", "W", # pycodestyle:基础风格(空格/空行/换行)
47
+ "F", # pyflakes:未用 import / 变量、未定义名
48
+ "I", # isort:import 自动排序分组
49
+ "B", # bugbear:常见 bug 模式(可变默认值、裸 except 等)
50
+ "UP", # pyupgrade:语法现代化(Optional[X] → X | None 等)
51
+ ]
52
+ ignore = [
53
+ "B008", # 函数调用作为默认值 —— FastAPI 的 Security/Depends/Query/Body 必须这么写
54
+ ]
55
+
56
+ [tool.ruff.lint.per-file-ignores]
57
+ "migrations/*" = ["ALL"] # aerich 自动生成的迁移文件,不检查
58
+ "__init__.py" = ["F401"] # 模块聚合导出,允许未使用 import
59
+
60
+ [tool.ruff.lint.isort]
61
+ known-first-party = ["servers", "models"] # 本项目模块,排序时与第三方库分组
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rainycode
3
- Version: 1.4.4
3
+ Version: 1.5.7
4
4
  Summary: FastAPI base modules
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: aerich==0.9.2
@@ -17,6 +17,7 @@ Requires-Dist: pycryptodome==3.23.0
17
17
  Requires-Dist: uvicorn[standard]==0.41.0
18
18
  Requires-Dist: tortoise-orm[asyncmy]==0.25.4
19
19
  Requires-Dist: pydantic-settings==2.13.1
20
+ Requires-Dist: python-multipart==0.0.32
20
21
  Provides-Extra: release
21
22
  Requires-Dist: build==1.4.0; extra == "release"
22
23
  Requires-Dist: twine==6.1.0; extra == "release"
@@ -7,6 +7,7 @@ common_base/response.py
7
7
  common_depends/auth_depend.py
8
8
  common_models/__init__.py
9
9
  common_models/base_model.py
10
+ common_models/file_model.py
10
11
  common_models/user_model.py
11
12
  common_models/wechat_model.py
12
13
  common_servers/router.py
@@ -14,6 +15,7 @@ common_servers/api/auth_api.py
14
15
  common_servers/api/wechat_api.py
15
16
  common_servers/schemas/auth_schema.py
16
17
  common_servers/service/auth_svc.py
18
+ common_servers/service/file_svc.py
17
19
  common_servers/service/wechat_svc.py
18
20
  common_utils/bcrypt_util.py
19
21
  common_utils/captcha_util.py
@@ -12,6 +12,7 @@ pycryptodome==3.23.0
12
12
  uvicorn[standard]==0.41.0
13
13
  tortoise-orm[asyncmy]==0.25.4
14
14
  pydantic-settings==2.13.1
15
+ python-multipart==0.0.32
15
16
 
16
17
  [release]
17
18
  build==1.4.0
@@ -1,39 +0,0 @@
1
- import json
2
- import ssl
3
-
4
- import aiohttp
5
- import certifi
6
-
7
- # 模块级别 SSL 上下文,使用 certifi 证书,避免重复创建
8
- _SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where())
9
-
10
-
11
- def parse_url(url, params=None, suffix=None):
12
- if params:
13
- query = "&".join([f"{k}={params[k]}" for k in sorted(params.keys())])
14
- if "?" in url:
15
- url += "&" + query
16
- else:
17
- url += "?" + query
18
- if suffix:
19
- url += suffix
20
- return url
21
-
22
-
23
- async def get(url, params=None, headers=None, suffix=None):
24
- url = parse_url(url, params, suffix)
25
- connector = aiohttp.TCPConnector(ssl=_SSL_CONTEXT)
26
- async with aiohttp.ClientSession(connector=connector, trust_env=True) as session:
27
- async with session.get(url, headers=headers) as response:
28
- text = await response.text()
29
- return json.loads(text)
30
-
31
-
32
- async def post(url, data=None, headers=None):
33
- connector = aiohttp.TCPConnector(ssl=_SSL_CONTEXT)
34
- async with aiohttp.ClientSession(connector=connector, trust_env=True) as session:
35
- if isinstance(data, dict):
36
- data = json.dumps(data)
37
- async with session.post(url, data=data, headers=headers) as response:
38
- text = await response.text()
39
- return json.loads(text)
@@ -1,36 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools>=61.0", "wheel"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "rainycode"
7
- version = "1.4.4"
8
- description = "FastAPI base modules"
9
- requires-python = ">=3.10"
10
-
11
- dependencies = [
12
- "aerich == 0.9.2",
13
- "fastapi == 0.133.1",
14
- "aiohttp == 3.13.3",
15
- "passlib==1.7.4",
16
- "redis == 7.2.1",
17
- "pyjwt == 2.11.0",
18
- "pillow == 12.1.1",
19
- "bcrypt == 4.0.1",
20
- "certifi == 2026.2.25",
21
- "cryptography == 46.0.5",
22
- "pycryptodome == 3.23.0",
23
- "uvicorn[standard] == 0.41.0",
24
- "tortoise-orm[asyncmy] == 0.25.4",
25
- "pydantic-settings == 2.13.1",
26
- ]
27
-
28
- [project.optional-dependencies]
29
- release = [
30
- "build == 1.4.0",
31
- "twine == 6.1.0",
32
- ]
33
-
34
- [tool.setuptools.packages.find]
35
- where = ["."]
36
- include = ["core*", "common*"]
File without changes