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.
- {rainycode-1.6.1 → rainycode-1.6.3}/PKG-INFO +1 -1
- {rainycode-1.6.1 → rainycode-1.6.3}/common_depends/auth_depend.py +3 -3
- {rainycode-1.6.1 → rainycode-1.6.3}/common_servers/api/auth_api.py +1 -1
- {rainycode-1.6.1 → rainycode-1.6.3}/common_servers/api/wechat_api.py +4 -2
- {rainycode-1.6.1 → rainycode-1.6.3}/common_servers/service/auth_svc.py +13 -4
- {rainycode-1.6.1 → rainycode-1.6.3}/common_servers/service/file_svc.py +2 -2
- rainycode-1.6.3/common_utils/pagination.py +34 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/core/base_config.py +13 -3
- {rainycode-1.6.1 → rainycode-1.6.3}/core/start.py +10 -10
- {rainycode-1.6.1 → rainycode-1.6.3}/pyproject.toml +1 -1
- {rainycode-1.6.1 → rainycode-1.6.3}/rainycode.egg-info/PKG-INFO +1 -1
- {rainycode-1.6.1 → rainycode-1.6.3}/rainycode.egg-info/SOURCES.txt +1 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_base/aiorequests.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_base/consts.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_base/exception.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_base/logging.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_base/response.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_models/__init__.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_models/base_model.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_models/file_model.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_models/user_model.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_models/wechat_model.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_servers/router.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_servers/schemas/auth_schema.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_servers/service/wechat_svc.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_utils/bcrypt_util.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_utils/captcha_util.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_utils/datetime_util.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_utils/ip_util.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_utils/jwt_util.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_utils/snowflake_util.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/common_utils/wechat_util.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/core/databases/aiodb.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/core/databases/aioredis.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/core/middleware/http_middleware.py +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/rainycode.egg-info/dependency_links.txt +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/rainycode.egg-info/requires.txt +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/rainycode.egg-info/top_level.txt +0 -0
- {rainycode-1.6.1 → rainycode-1.6.3}/setup.cfg +0 -0
|
@@ -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.
|
|
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=
|
|
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=
|
|
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
|
|
93
|
-
#
|
|
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
|
|
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
|
-
#
|
|
535
|
-
#
|
|
536
|
-
#
|
|
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(
|
|
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
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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=
|
|
90
|
-
allow_credentials=
|
|
91
|
-
allow_methods=
|
|
92
|
-
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 =
|
|
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))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|