rainycode 1.6.1__tar.gz → 1.6.3__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 (39) hide show
  1. {rainycode-1.6.1 → rainycode-1.6.3}/PKG-INFO +1 -1
  2. {rainycode-1.6.1 → rainycode-1.6.3}/common_depends/auth_depend.py +3 -3
  3. {rainycode-1.6.1 → rainycode-1.6.3}/common_servers/api/auth_api.py +1 -1
  4. {rainycode-1.6.1 → rainycode-1.6.3}/common_servers/api/wechat_api.py +4 -2
  5. {rainycode-1.6.1 → rainycode-1.6.3}/common_servers/service/auth_svc.py +13 -4
  6. {rainycode-1.6.1 → rainycode-1.6.3}/common_servers/service/file_svc.py +2 -2
  7. rainycode-1.6.3/common_utils/pagination.py +34 -0
  8. {rainycode-1.6.1 → rainycode-1.6.3}/core/base_config.py +13 -3
  9. {rainycode-1.6.1 → rainycode-1.6.3}/core/start.py +10 -10
  10. {rainycode-1.6.1 → rainycode-1.6.3}/pyproject.toml +1 -1
  11. {rainycode-1.6.1 → rainycode-1.6.3}/rainycode.egg-info/PKG-INFO +1 -1
  12. {rainycode-1.6.1 → rainycode-1.6.3}/rainycode.egg-info/SOURCES.txt +1 -0
  13. {rainycode-1.6.1 → rainycode-1.6.3}/common_base/aiorequests.py +0 -0
  14. {rainycode-1.6.1 → rainycode-1.6.3}/common_base/consts.py +0 -0
  15. {rainycode-1.6.1 → rainycode-1.6.3}/common_base/exception.py +0 -0
  16. {rainycode-1.6.1 → rainycode-1.6.3}/common_base/logging.py +0 -0
  17. {rainycode-1.6.1 → rainycode-1.6.3}/common_base/response.py +0 -0
  18. {rainycode-1.6.1 → rainycode-1.6.3}/common_models/__init__.py +0 -0
  19. {rainycode-1.6.1 → rainycode-1.6.3}/common_models/base_model.py +0 -0
  20. {rainycode-1.6.1 → rainycode-1.6.3}/common_models/file_model.py +0 -0
  21. {rainycode-1.6.1 → rainycode-1.6.3}/common_models/user_model.py +0 -0
  22. {rainycode-1.6.1 → rainycode-1.6.3}/common_models/wechat_model.py +0 -0
  23. {rainycode-1.6.1 → rainycode-1.6.3}/common_servers/router.py +0 -0
  24. {rainycode-1.6.1 → rainycode-1.6.3}/common_servers/schemas/auth_schema.py +0 -0
  25. {rainycode-1.6.1 → rainycode-1.6.3}/common_servers/service/wechat_svc.py +0 -0
  26. {rainycode-1.6.1 → rainycode-1.6.3}/common_utils/bcrypt_util.py +0 -0
  27. {rainycode-1.6.1 → rainycode-1.6.3}/common_utils/captcha_util.py +0 -0
  28. {rainycode-1.6.1 → rainycode-1.6.3}/common_utils/datetime_util.py +0 -0
  29. {rainycode-1.6.1 → rainycode-1.6.3}/common_utils/ip_util.py +0 -0
  30. {rainycode-1.6.1 → rainycode-1.6.3}/common_utils/jwt_util.py +0 -0
  31. {rainycode-1.6.1 → rainycode-1.6.3}/common_utils/snowflake_util.py +0 -0
  32. {rainycode-1.6.1 → rainycode-1.6.3}/common_utils/wechat_util.py +0 -0
  33. {rainycode-1.6.1 → rainycode-1.6.3}/core/databases/aiodb.py +0 -0
  34. {rainycode-1.6.1 → rainycode-1.6.3}/core/databases/aioredis.py +0 -0
  35. {rainycode-1.6.1 → rainycode-1.6.3}/core/middleware/http_middleware.py +0 -0
  36. {rainycode-1.6.1 → rainycode-1.6.3}/rainycode.egg-info/dependency_links.txt +0 -0
  37. {rainycode-1.6.1 → rainycode-1.6.3}/rainycode.egg-info/requires.txt +0 -0
  38. {rainycode-1.6.1 → rainycode-1.6.3}/rainycode.egg-info/top_level.txt +0 -0
  39. {rainycode-1.6.1 → rainycode-1.6.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rainycode
3
- Version: 1.6.1
3
+ Version: 1.6.3
4
4
  Summary: FastAPI base modules
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: aerich==0.9.2
@@ -11,7 +11,7 @@ from common_base.exception import CustomException
11
11
  from common_models.user_model import User
12
12
  from common_utils.jwt_util import JwtUtil
13
13
  from core.databases.aioredis import AioRedis
14
- from core.start import base_config
14
+ from core.base_config import base_config
15
15
 
16
16
  oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"/api/{base_config.app_name}/auth/login", auto_error=False)
17
17
 
@@ -21,7 +21,7 @@ async def get_current_user(request: Request, token: str, security_scopes: Securi
21
21
  """
22
22
  payload = JwtUtil.decode_token(token)
23
23
  if not payload:
24
- raise CustomException(msg=f"无效的令牌 {token}", status_code=401)
24
+ raise CustomException(msg="无效的令牌", status_code=401)
25
25
 
26
26
  # 检查是否在黑名单中
27
27
  if await AioRedis.get(f"{REDIS_PREFIX_BLOCKLIST}{token}"):
@@ -29,7 +29,7 @@ async def get_current_user(request: Request, token: str, security_scopes: Securi
29
29
 
30
30
  user_id = payload.get("sub")
31
31
  if not user_id:
32
- raise CustomException(msg=f"无效的令牌 {token}", status_code=401)
32
+ raise CustomException(msg="无效的令牌", status_code=401)
33
33
 
34
34
  # 1. 尝试从缓存获取
35
35
  user = None
@@ -55,7 +55,7 @@ async def logout(
55
55
 
56
56
  @router.get("/me", response_model=UserInfo, summary="获取当前用户信息")
57
57
  async def get_me(user: User = Security(login_require)):
58
- return SuccessReturn(data=user)
58
+ return SuccessReturn(data=await User.model2dict(user), msg="查询成功")
59
59
 
60
60
 
61
61
  @router.post("/wechat/mini", response_model=TokenResponse, summary="微信小程序登录")
@@ -89,8 +89,10 @@ async def wechat_oauth_callback(
89
89
  "error": error,
90
90
  "error_description": error_description or "用户拒绝授权",
91
91
  }
92
- # redirect_uri 现在可能已带目标路由(...#/pages/xxx),拼接符按需选,
93
- # 使错误信息落在该页自己的 query 上(路由之后)
92
+ # 拼接符按 redirect_uri 现状选:前端现在把目标页路由挂在 query 上
93
+ # (?dw_route=%2Fpages%2Fxxx,原因见 AuthService._build_redirect_response),
94
+ # 此时用 & 续接;存量授权链接仍是 #/pages/xxx 形态,用 ? 续接。
95
+ # 两种形态下错误信息都会落在该页自己的参数区,页面能取到并提示。
94
96
  sep = "&" if "?" in redirect_uri else "?"
95
97
  error_url = f"{redirect_uri}{sep}{urlencode(params)}"
96
98
  return RedirectResponse(url=error_url, status_code=302)
@@ -521,7 +521,7 @@ class AuthService:
521
521
  async def _build_redirect_response(
522
522
  cls, redirect_uri: str, token_data: dict, state: str | None = None
523
523
  ) -> RedirectResponse:
524
- """构建带 token 的重定向响应(使用 URL Fragment)"""
524
+ """构建带 token 的重定向响应(凭证拼在 redirect_uri 的 query 上)"""
525
525
  params = {
526
526
  "access_token": token_data["access_token"],
527
527
  "refresh_token": token_data["refresh_token"],
@@ -531,9 +531,18 @@ class AuthService:
531
531
  if state:
532
532
  params["state"] = state
533
533
 
534
- # 凭证一律挂在 query 上(token 不落 fragment):redirect_uri 由前端带上目标页
535
- # 路由(https://host/#/pages/xxx),拼出来即 ...#/pages/xxx?access_token=..
536
- # 它自身已带参数时(...#/pages/xxx?id=3)用 & 续接。
534
+ # 凭证一律拼在 query 上,绝不落到 `#` 之后。
535
+ #
536
+ # 为什么:PC 微信跟随 302 时会丢掉 `#` 之后整段(微信官方社区原话:fragment
537
+ #「仅用作浏览器端的处理,不会参与到服务端请求中,故回跳不会带上这部分信息」)。
538
+ # 凭证若落在 fragment,就等于在电脑端凭空消失,用户表现为「点了登录回到首页
539
+ # 且没登录」,后端日志里连 /user/profile 都不会出现。
540
+ # 因此前端 guard.js 连目标页路由也一并挪到 query 上(?dw_route=%2Fpages%2Fxxx),
541
+ # 由 index.html 在 app 启动前还原成 hash 路由。这里只按 redirect_uri 是否已带
542
+ # 参数来选拼接符:
543
+ # ?dw_route=..(当前形态,路由挂在 query)→ 用 & 续接
544
+ # #/pages/xxx(存量授权链接,路由挂在 hash)→ 用 ? 续接
545
+ # 注:变量名 fragment 是历史沿用,实际拼在 query 上。
537
546
  fragment = urlencode(params)
538
547
  sep = "&" if "?" in redirect_uri else "?"
539
548
  redirect_url = f"{redirect_uri}{sep}{fragment}"
@@ -16,8 +16,8 @@ class FileService:
16
16
  cls,
17
17
  user: User,
18
18
  file,
19
- *,
20
19
  subdir: str,
20
+ upload_root: str = base_config.upload_root,
21
21
  allowed_exts: set[str] = base_config.upload_allowed_image_exts,
22
22
  max_bytes: int = base_config.upload_max_bytes,
23
23
  ) -> dict:
@@ -37,7 +37,7 @@ class FileService:
37
37
 
38
38
  filename = f"{uuid.uuid4().hex}.{ext}"
39
39
  relative = f"{subdir}/{filename}" # 磁盘相对路径 & File.url(不含前缀)
40
- path = Path(base_config.upload_root) / relative
40
+ path = Path(upload_root) / relative
41
41
  path.parent.mkdir(parents=True, exist_ok=True)
42
42
  path.write_bytes(content)
43
43
 
@@ -0,0 +1,34 @@
1
+ class Pagination:
2
+ """
3
+ 对 Tortoise ORM 查询集执行分页,并返回总记录数和当前页数据。
4
+
5
+ 示例:
6
+ qs = Order.filter(couple_id=couple.id).order_by('-id')
7
+ page = await Pagination.page(qs, page_no=page_no, page_size=page_size)
8
+
9
+ 返回结构为 {'items': 当前页模型对象列表, 'count': 符合查询条件的总数}。
10
+ 本类只负责查询计数和 limit/offset,不负责序列化或组装业务字段。
11
+ 调用方应在需要时转换模型对象,并确保查询集具有稳定的排序规则。
12
+ """
13
+ DEFAULT_PAGE_SIZE = 20
14
+ MAX_PAGE_SIZE = 999
15
+
16
+ @classmethod
17
+ async def page(cls, qs, page_no: int | None = None, page_size: int | None = None) -> dict:
18
+ """获取指定页的数据。
19
+
20
+ Args:
21
+ qs: 已完成筛选及排序的 Tortoise ORM 查询集。
22
+ page_no: 页码,从 1 开始;未提供或小于 1 时按第 1 页处理。
23
+ page_size: 每页条数;未提供时使用默认值,并限制在 1 到 MAX_PAGE_SIZE 之间。
24
+
25
+ Returns:
26
+ 包含当前页模型对象列表 ``items`` 和匹配记录总数 ``count`` 的字典。
27
+ """
28
+ page_no = page_no or 1
29
+ page_no = page_no if page_no > 0 else 1
30
+ page_size = page_size or cls.DEFAULT_PAGE_SIZE
31
+ page_size = min(max(page_size, 1), cls.MAX_PAGE_SIZE)
32
+ count = await qs.count()
33
+ items = await qs.limit(page_size).offset((page_no - 1) * page_size)
34
+ return {'items': items, 'count': count}
@@ -1,5 +1,10 @@
1
+ import os
2
+
1
3
  from pydantic_settings import BaseSettings, SettingsConfigDict
2
4
 
5
+ # 包内 static 目录(core/ 的上一级),默认的 /static 挂载点
6
+ _STATIC_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../static"))
7
+
3
8
 
4
9
  class BaseConfig(BaseSettings):
5
10
  app_debug: bool = False
@@ -13,10 +18,15 @@ class BaseConfig(BaseSettings):
13
18
  # 基础 URL
14
19
  base_url: str = ""
15
20
 
21
+ # 静态文件:/static 的挂载根目录,默认包内 static;
22
+ # 想让静态资源落到项目目录,子类里覆盖这一项即可。
23
+ static_root: str = _STATIC_DIR
24
+
16
25
  # 文件上传:本地存储根目录 + 访问 URL 前缀。
17
- # upload_url_prefix 可为相对路径(/uploads)或完整 URL(https://cdn.xxx.com/uploads),
18
- upload_root: str = 'uploads'
19
- upload_url_prefix: str = '/uploads'
26
+ # upload_root 必须在 static_root 之下,否则会出现「写得进、读不到」。
27
+ # upload_url_prefix 可为相对路径(/static/uploads)或完整 URL(https://cdn.xxx.com/uploads),
28
+ upload_root: str = os.path.join(_STATIC_DIR, 'uploads')
29
+ upload_url_prefix: str = '/static/uploads'
20
30
 
21
31
  # 跨域
22
32
  cors_origins: list[str] = ['*']
@@ -13,7 +13,7 @@ from common_base.exception import (
13
13
  ValueExceptionHandler,
14
14
  )
15
15
  from common_base.logging import logger
16
- from core.base_config import BaseConfig, base_config
16
+ from core.base_config import BaseConfig
17
17
  from core.databases.aiodb import AioDb
18
18
  from core.middleware.http_middleware import RequestHttpMiddleware
19
19
  from fastapi import FastAPI, HTTPException
@@ -39,9 +39,9 @@ def start_server(config: BaseConfig):
39
39
  # 注册异常处理
40
40
  register_exception(app)
41
41
  # 注册中间件
42
- register_middlewares(app)
42
+ register_middlewares(app, config)
43
43
  # 注册静态文件
44
- register_files(app)
44
+ register_files(app, config)
45
45
 
46
46
  return app
47
47
 
@@ -79,25 +79,25 @@ def register_exception(app: FastAPI):
79
79
  app.add_exception_handler(Exception, AllExceptionHandler) # pyright: ignore[reportArgumentType]
80
80
 
81
81
 
82
- def register_middlewares(app: FastAPI) -> None:
82
+ def register_middlewares(app: FastAPI, config: BaseConfig) -> None:
83
83
  """
84
84
  注册中间件
85
85
  """
86
86
  app.add_middleware(RequestHttpMiddleware)
87
87
  app.add_middleware(
88
88
  CORSMiddleware,
89
- allow_origins=base_config.cors_origins,
90
- allow_credentials=base_config.cors_allow_credentials,
91
- allow_methods=base_config.cors_allow_methods,
92
- allow_headers=base_config.cors_allow_headers
89
+ allow_origins=config.cors_origins,
90
+ allow_credentials=config.cors_allow_credentials,
91
+ allow_methods=config.cors_allow_methods,
92
+ allow_headers=config.cors_allow_headers
93
93
  )
94
94
 
95
95
 
96
- def register_files(app: FastAPI) -> None:
96
+ def register_files(app: FastAPI, config: BaseConfig) -> None:
97
97
  """
98
98
  注册文件相关配置
99
99
  """
100
- static_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../static"))
100
+ static_dir = config.static_root
101
101
  if not os.path.exists(static_dir):
102
102
  os.mkdir(static_dir)
103
103
  app.mount('/static', StaticFiles(directory=static_dir))
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rainycode"
7
- version = "1.6.1"
7
+ version = "1.6.3"
8
8
  description = "FastAPI base modules"
9
9
  requires-python = ">=3.10"
10
10
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rainycode
3
- Version: 1.6.1
3
+ Version: 1.6.3
4
4
  Summary: FastAPI base modules
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: aerich==0.9.2
@@ -22,6 +22,7 @@ common_utils/captcha_util.py
22
22
  common_utils/datetime_util.py
23
23
  common_utils/ip_util.py
24
24
  common_utils/jwt_util.py
25
+ common_utils/pagination.py
25
26
  common_utils/snowflake_util.py
26
27
  common_utils/wechat_util.py
27
28
  core/base_config.py
File without changes