reglow 0.3.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.
- reglow/README.md +162 -0
- reglow/common/__init__.py +1 -0
- reglow/common/exception_handler.py +205 -0
- reglow/common/exceptions.py +136 -0
- reglow/common/i18n.py +77 -0
- reglow/common/middleware.py +323 -0
- reglow/common/response.py +33 -0
- reglow/common/utils/__init__.py +4 -0
- reglow/common/utils/date_utils.py +27 -0
- reglow/common/utils/parse_user_agent.py +27 -0
- reglow/common/utils/string_utils.py +29 -0
- reglow/core/__init__.py +1 -0
- reglow/core/cache.py +19 -0
- reglow/core/config.py +112 -0
- reglow/core/database.py +98 -0
- reglow/core/dependencies.py +150 -0
- reglow/core/dependencies_client.py +46 -0
- reglow/core/identity.py +27 -0
- reglow/core/logging_config.py +154 -0
- reglow/core/observability.py +100 -0
- reglow/core/plugin.py +93 -0
- reglow/core/protocols.py +43 -0
- reglow/core/rate_limit.py +28 -0
- reglow/core/redis_client.py +130 -0
- reglow/core/redis_keys.py +51 -0
- reglow/core/security.py +50 -0
- reglow/core/sms.py +313 -0
- reglow/modules/__init__.py +1 -0
- reglow/modules/agreement/__init__.py +1 -0
- reglow/modules/agreement/controller.py +723 -0
- reglow/modules/agreement/model.py +43 -0
- reglow/modules/agreement/router.py +4 -0
- reglow/modules/agreement/schema.py +125 -0
- reglow/modules/ai/__init__.py +1 -0
- reglow/modules/ai/controller.py +1084 -0
- reglow/modules/ai/router.py +2 -0
- reglow/modules/article/__init__.py +0 -0
- reglow/modules/article/controller.py +243 -0
- reglow/modules/article/model.py +44 -0
- reglow/modules/article/repository.py +193 -0
- reglow/modules/article/router.py +2 -0
- reglow/modules/article/schema.py +127 -0
- reglow/modules/article/service.py +224 -0
- reglow/modules/auth/__init__.py +1 -0
- reglow/modules/auth/controller.py +132 -0
- reglow/modules/auth/router.py +4 -0
- reglow/modules/auth/schema.py +46 -0
- reglow/modules/auth/service.py +805 -0
- reglow/modules/config/__init__.py +1 -0
- reglow/modules/config/controller.py +316 -0
- reglow/modules/config/model.py +25 -0
- reglow/modules/config/router.py +3 -0
- reglow/modules/config/schema.py +34 -0
- reglow/modules/customer_service/__init__.py +0 -0
- reglow/modules/customer_service/controller.py +278 -0
- reglow/modules/customer_service/model.py +78 -0
- reglow/modules/customer_service/repository.py +223 -0
- reglow/modules/customer_service/router.py +4 -0
- reglow/modules/customer_service/schema.py +173 -0
- reglow/modules/customer_service/service.py +662 -0
- reglow/modules/dept/__init__.py +1 -0
- reglow/modules/dept/controller.py +104 -0
- reglow/modules/dept/model.py +15 -0
- reglow/modules/dept/repository.py +206 -0
- reglow/modules/dept/router.py +2 -0
- reglow/modules/dept/schema.py +62 -0
- reglow/modules/dept/service.py +81 -0
- reglow/modules/dict/__init__.py +1 -0
- reglow/modules/dict/controller.py +97 -0
- reglow/modules/dict/model.py +26 -0
- reglow/modules/dict/repository.py +95 -0
- reglow/modules/dict/router.py +2 -0
- reglow/modules/dict/schema.py +67 -0
- reglow/modules/dict/service.py +74 -0
- reglow/modules/employee/__init__.py +1 -0
- reglow/modules/employee/controller.py +111 -0
- reglow/modules/employee/employee_dept_model.py +20 -0
- reglow/modules/employee/employee_role_model.py +16 -0
- reglow/modules/employee/model.py +44 -0
- reglow/modules/employee/repository.py +154 -0
- reglow/modules/employee/router.py +4 -0
- reglow/modules/employee/schema.py +206 -0
- reglow/modules/employee/service.py +206 -0
- reglow/modules/log/__init__.py +1 -0
- reglow/modules/log/controller.py +60 -0
- reglow/modules/log/model.py +36 -0
- reglow/modules/log/router.py +2 -0
- reglow/modules/log/schema.py +35 -0
- reglow/modules/material/__init__.py +1 -0
- reglow/modules/material/browser_extractor.py +474 -0
- reglow/modules/material/controller.py +910 -0
- reglow/modules/material/model.py +42 -0
- reglow/modules/material/router.py +3 -0
- reglow/modules/material/schema.py +119 -0
- reglow/modules/menu/__init__.py +1 -0
- reglow/modules/menu/controller.py +56 -0
- reglow/modules/menu/model.py +20 -0
- reglow/modules/menu/repository.py +51 -0
- reglow/modules/menu/router.py +2 -0
- reglow/modules/menu/schema.py +56 -0
- reglow/modules/menu/service.py +60 -0
- reglow/modules/message/__init__.py +0 -0
- reglow/modules/message/controller.py +270 -0
- reglow/modules/message/model.py +41 -0
- reglow/modules/message/repository.py +192 -0
- reglow/modules/message/router.py +3 -0
- reglow/modules/message/schema.py +98 -0
- reglow/modules/message/service.py +190 -0
- reglow/modules/notice/__init__.py +0 -0
- reglow/modules/notice/controller.py +147 -0
- reglow/modules/notice/model.py +27 -0
- reglow/modules/notice/repository.py +176 -0
- reglow/modules/notice/router.py +2 -0
- reglow/modules/notice/schema.py +45 -0
- reglow/modules/notice/service.py +151 -0
- reglow/modules/photo/__init__.py +0 -0
- reglow/modules/photo/controller.py +711 -0
- reglow/modules/photo/model.py +48 -0
- reglow/modules/photo/router.py +2 -0
- reglow/modules/photo/schema.py +136 -0
- reglow/modules/post/__init__.py +1 -0
- reglow/modules/post/controller.py +44 -0
- reglow/modules/post/model.py +12 -0
- reglow/modules/post/repository.py +56 -0
- reglow/modules/post/router.py +2 -0
- reglow/modules/post/schema.py +27 -0
- reglow/modules/post/service.py +32 -0
- reglow/modules/role/__init__.py +1 -0
- reglow/modules/role/controller.py +95 -0
- reglow/modules/role/model.py +34 -0
- reglow/modules/role/repository.py +93 -0
- reglow/modules/role/router.py +3 -0
- reglow/modules/role/schema.py +43 -0
- reglow/modules/role/service.py +53 -0
- reglow/modules/user/__init__.py +1 -0
- reglow/modules/user/controller.py +90 -0
- reglow/modules/user/controller_client.py +120 -0
- reglow/modules/user/model.py +49 -0
- reglow/modules/user/repository.py +90 -0
- reglow/modules/user/router.py +5 -0
- reglow/modules/user/schema.py +158 -0
- reglow/modules/user/service.py +233 -0
- reglow/modules/video/__init__.py +0 -0
- reglow/modules/video/controller.py +266 -0
- reglow/modules/video/model.py +43 -0
- reglow/modules/video/repository.py +190 -0
- reglow/modules/video/router.py +2 -0
- reglow/modules/video/schema.py +121 -0
- reglow/modules/video/service.py +228 -0
- reglow-0.3.0.dist-info/METADATA +404 -0
- reglow-0.3.0.dist-info/RECORD +154 -0
- reglow-0.3.0.dist-info/WHEEL +5 -0
- reglow-0.3.0.dist-info/licenses/LICENSE +201 -0
- reglow-0.3.0.dist-info/top_level.txt +1 -0
reglow/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Reglow Python 后端基础库
|
|
2
|
+
|
|
3
|
+
> **包名**:`reglow` | **版本**:0.3.0 | **Python**:>=3.12
|
|
4
|
+
|
|
5
|
+
## 概述
|
|
6
|
+
|
|
7
|
+
`reglow` 是基于 FastAPI + SQLAlchemy 2.0(async)的全栈后台基础库,提供 19 个即用业务模块,通过 `pip install -e .` 安装后可直接导入使用。
|
|
8
|
+
|
|
9
|
+
## 目录结构
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
reglow/
|
|
13
|
+
├── core/ # 框架核心层
|
|
14
|
+
│ ├── config.py # Settings 配置(pydantic-settings,自动读 .env)
|
|
15
|
+
│ ├── database.py # 异步引擎/会话/Base/AutoBigInt/Mixin
|
|
16
|
+
│ ├── security.py # 密码哈希、JWT 签发/验证
|
|
17
|
+
│ ├── dependencies.py # get_current_employee / PermissionChecker
|
|
18
|
+
│ ├── redis_client.py # Redis 客户端
|
|
19
|
+
│ ├── cache.py # 缓存封装
|
|
20
|
+
│ ├── rate_limit.py # 限流
|
|
21
|
+
│ ├── sms.py # 短信发送
|
|
22
|
+
│ ├── observability.py # 健康检查路由
|
|
23
|
+
│ └── logging_config.py # 日志配置
|
|
24
|
+
│
|
|
25
|
+
├── common/ # 跨模块共享层
|
|
26
|
+
│ ├── response.py # ApiResponse[T] / PageData[T] 统一响应
|
|
27
|
+
│ ├── exceptions.py # AppException / ErrorCode 枚举
|
|
28
|
+
│ ├── exception_handler.py # 全局异常处理注册
|
|
29
|
+
│ ├── middleware.py # 中间件注册(CORS/日志/i18n)
|
|
30
|
+
│ └── i18n.py # 国际化(Accept-Language 头)
|
|
31
|
+
│
|
|
32
|
+
└── modules/ # 业务模块层(19 个模块)
|
|
33
|
+
├── auth/ # 认证(登录/注册/验证码/Token刷新)
|
|
34
|
+
├── employee/ # 员工管理
|
|
35
|
+
├── user/ # 用户管理(注册用户,区别于员工)
|
|
36
|
+
├── role/ # 角色管理(含数据范围)
|
|
37
|
+
├── menu/ # 菜单管理(树形)
|
|
38
|
+
├── dept/ # 部门管理(树形)
|
|
39
|
+
├── post/ # 岗位管理
|
|
40
|
+
├── dict/ # 字典管理(类型+数据)
|
|
41
|
+
├── config/ # 参数配置
|
|
42
|
+
├── log/ # 日志(登录+操作)
|
|
43
|
+
├── material/ # 素材中心(图片/视频/文件)
|
|
44
|
+
├── agreement/ # 协议管理
|
|
45
|
+
├── notice/ # 通知公告
|
|
46
|
+
├── message/ # 系统消息+模板+收件箱
|
|
47
|
+
├── article/ # 文章管理(含分类+文集)
|
|
48
|
+
├── photo/ # 照片管理(含分类+相册)
|
|
49
|
+
├── video/ # 视频管理(含分类+视频集)
|
|
50
|
+
├── ai/ # AI(对话/图片生成/视频生成)
|
|
51
|
+
└── customer_service/ # 智能客服(知识库/会话/SDK)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 模块六层架构
|
|
55
|
+
|
|
56
|
+
每个模块遵循统一的分层结构,依赖单向向下:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
model.py ORM 实体(继承 Base + Mixin)
|
|
60
|
+
↑
|
|
61
|
+
schema.py Pydantic 契约(CreateRequest / UpdateRequest / Response)
|
|
62
|
+
↑
|
|
63
|
+
repository.py 数据访问层(AsyncSession,find_/create_/update_)
|
|
64
|
+
↑
|
|
65
|
+
service.py 业务逻辑层(编排 repository,抛 AppException)
|
|
66
|
+
↑
|
|
67
|
+
controller.py 接口控制层(APIRouter + 权限校验 + ApiResponse)
|
|
68
|
+
↑
|
|
69
|
+
router.py 路由导出(from .controller import router)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 安装
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# 可编辑模式(开发)
|
|
76
|
+
cd reglow && pip install -e .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 使用
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# main.py — 消费方应用入口
|
|
83
|
+
from fastapi import FastAPI, APIRouter
|
|
84
|
+
from reglow.common.middleware import register_middlewares
|
|
85
|
+
from reglow.common.exception_handler import register_exception_handlers
|
|
86
|
+
|
|
87
|
+
# 导入需要的模块路由
|
|
88
|
+
from reglow.modules.auth.router import router as auth_router
|
|
89
|
+
from reglow.modules.employee.router import router as employee_router
|
|
90
|
+
# ... 按需导入
|
|
91
|
+
|
|
92
|
+
app = FastAPI(title="My App")
|
|
93
|
+
register_middlewares(app)
|
|
94
|
+
register_exception_handlers(app)
|
|
95
|
+
|
|
96
|
+
admin_api = APIRouter(prefix="/admin/api/v1")
|
|
97
|
+
admin_api.include_router(auth_router)
|
|
98
|
+
admin_api.include_router(employee_router)
|
|
99
|
+
app.include_router(admin_api)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 核心基础设施
|
|
103
|
+
|
|
104
|
+
### 统一响应
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from reglow.common.response import ApiResponse, PageData
|
|
108
|
+
|
|
109
|
+
# 成功
|
|
110
|
+
return ApiResponse.success(data)
|
|
111
|
+
return ApiResponse.success(PageData(items=..., total=..., page=1, size=20))
|
|
112
|
+
|
|
113
|
+
# 失败
|
|
114
|
+
return ApiResponse.fail("操作失败")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 异常处理
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from reglow.common.exceptions import AppException, ErrorCode
|
|
121
|
+
|
|
122
|
+
raise AppException(ErrorCode.NOT_FOUND, "留言不存在", 404)
|
|
123
|
+
raise AppException(ErrorCode.BAD_REQUEST, "参数错误", 400)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 权限校验
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from reglow.core.dependencies import PermissionChecker, get_current_employee
|
|
130
|
+
|
|
131
|
+
@router.get("/feedbacks", dependencies=[Depends(PermissionChecker("business:feedback:list"))])
|
|
132
|
+
async def list_feedbacks(db: AsyncSession = Depends(get_db)):
|
|
133
|
+
...
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 数据库模型
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from reglow.core.database import AutoBigInt, Base, TimestampMixin, SoftDeleteMixin
|
|
140
|
+
from sqlalchemy import Column, String
|
|
141
|
+
|
|
142
|
+
class Feedback(TimestampMixin, SoftDeleteMixin, Base):
|
|
143
|
+
__tablename__ = "biz_feedback"
|
|
144
|
+
id = Column(AutoBigInt, primary_key=True, autoincrement=True)
|
|
145
|
+
name = Column(String(100), nullable=False, comment="名称")
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## 配置(.env)
|
|
149
|
+
|
|
150
|
+
```env
|
|
151
|
+
APP_NAME=MyApp
|
|
152
|
+
DEBUG=True
|
|
153
|
+
DB_DRIVER=sqlite # 或 mysql
|
|
154
|
+
DB_NAME=myapp
|
|
155
|
+
JWT_SECRET=your-secret
|
|
156
|
+
REDIS_URL=redis://localhost:6379/0
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## 参考文档
|
|
160
|
+
|
|
161
|
+
- [Reglow 基础库改造与集成手册](../reglow-docs/content/2.tutorials/3.framework-intro/Reglow基础库改造与集成小白实操手册.md)
|
|
162
|
+
- [reglow-app/backend 示例项目](../reglow-app/backend/)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Common - 跨模块共享"""
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""全局异常处理器 - 一个兜底,万事大吉"""
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from fastapi import Request
|
|
5
|
+
from fastapi.responses import JSONResponse
|
|
6
|
+
from fastapi.exceptions import RequestValidationError
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
|
|
9
|
+
from reglow.common.exceptions import AppException
|
|
10
|
+
from reglow.common.exceptions import ErrorCode
|
|
11
|
+
from reglow.common.i18n import get_locale_from_request, translate
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
_ERROR_MSG = {
|
|
16
|
+
"zh-CN": {
|
|
17
|
+
"missing": "字段不能为空",
|
|
18
|
+
"value_error.missing": "字段不能为空",
|
|
19
|
+
"string_type": "请输入字符串",
|
|
20
|
+
"int_type": "请输入整数",
|
|
21
|
+
"float_type": "请输入数字",
|
|
22
|
+
"bool_type": "请输入布尔值",
|
|
23
|
+
"list_type": "请输入列表",
|
|
24
|
+
"string_too_short": "长度不能少于 {min_length} 个字符",
|
|
25
|
+
"string_too_long": "长度不能超过 {max_length} 个字符",
|
|
26
|
+
"int_too_small": "值不能小于 {ge}",
|
|
27
|
+
"int_too_big": "值不能大于 {le}",
|
|
28
|
+
"float_too_small": "值不能小于 {ge}",
|
|
29
|
+
"float_too_big": "值不能大于 {le}",
|
|
30
|
+
"greater_than_equal": "值不能小于 {ge}",
|
|
31
|
+
"less_than_equal": "值不能大于 {le}",
|
|
32
|
+
"pattern_regex": "格式不正确",
|
|
33
|
+
"value_error": "{error}",
|
|
34
|
+
"json_invalid": "JSON 格式不正确",
|
|
35
|
+
"url_parsing": "URL 格式不正确",
|
|
36
|
+
"missing_sole": "字段不能为空",
|
|
37
|
+
},
|
|
38
|
+
"en-US": {
|
|
39
|
+
"missing": " is required",
|
|
40
|
+
"value_error.missing": " is required",
|
|
41
|
+
"string_type": " must be a string",
|
|
42
|
+
"int_type": " must be an integer",
|
|
43
|
+
"float_type": " must be a number",
|
|
44
|
+
"bool_type": " must be a boolean",
|
|
45
|
+
"list_type": " must be a list",
|
|
46
|
+
"string_too_short": " length must be at least {min_length} characters",
|
|
47
|
+
"string_too_long": " length must be at most {max_length} characters",
|
|
48
|
+
"int_too_small": " must be greater than or equal to {ge}",
|
|
49
|
+
"int_too_big": " must be less than or equal to {le}",
|
|
50
|
+
"float_too_small": " must be greater than or equal to {ge}",
|
|
51
|
+
"float_too_big": " must be less than or equal to {le}",
|
|
52
|
+
"greater_than_equal": " must be greater than or equal to {ge}",
|
|
53
|
+
"less_than_equal": " must be less than or equal to {le}",
|
|
54
|
+
"pattern_regex": " format is invalid",
|
|
55
|
+
"value_error": ": {error}",
|
|
56
|
+
"json_invalid": "Invalid JSON format",
|
|
57
|
+
"url_parsing": " URL format is invalid",
|
|
58
|
+
"missing_sole": " is required",
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_FIELD_NAME = {
|
|
63
|
+
"zh-CN": {
|
|
64
|
+
"name": "名称",
|
|
65
|
+
"code": "编码",
|
|
66
|
+
"type": "类型",
|
|
67
|
+
"icon": "图标",
|
|
68
|
+
"path": "路径",
|
|
69
|
+
"component": "组件",
|
|
70
|
+
"permission": "权限标识",
|
|
71
|
+
"sort": "排序",
|
|
72
|
+
"status": "状态",
|
|
73
|
+
"description": "描述",
|
|
74
|
+
"remark": "备注",
|
|
75
|
+
"username": "用户名",
|
|
76
|
+
"password": "密码",
|
|
77
|
+
"new_password": "新密码",
|
|
78
|
+
"real_name": "真实姓名",
|
|
79
|
+
"nickname": "昵称",
|
|
80
|
+
"phone": "手机号",
|
|
81
|
+
"email": "邮箱",
|
|
82
|
+
"sms_code": "短信验证码",
|
|
83
|
+
"captcha_key": "验证码key",
|
|
84
|
+
"captcha_code": "验证码",
|
|
85
|
+
"slide_x": "滑动位置",
|
|
86
|
+
"employee_no": "工号",
|
|
87
|
+
"gender": "性别",
|
|
88
|
+
"dept_id": "部门",
|
|
89
|
+
"superior_id": "直属上级",
|
|
90
|
+
"role_ids": "角色",
|
|
91
|
+
"post_ids": "岗位",
|
|
92
|
+
"employment_type": "用工类型",
|
|
93
|
+
"job_level": "职级",
|
|
94
|
+
"job_title": "职称",
|
|
95
|
+
"entry_date": "入职日期",
|
|
96
|
+
"work_status": "在职状态",
|
|
97
|
+
"avatar": "头像",
|
|
98
|
+
"data_scope": "数据范围",
|
|
99
|
+
"menu_ids": "菜单列表",
|
|
100
|
+
"dept_ids": "部门列表",
|
|
101
|
+
"parent_id": "父级",
|
|
102
|
+
"visible": "是否显示",
|
|
103
|
+
"cache": "是否缓存",
|
|
104
|
+
"label": "标签",
|
|
105
|
+
"value": "值",
|
|
106
|
+
"source": "来源",
|
|
107
|
+
"group_ids": "分组",
|
|
108
|
+
"key": "键名",
|
|
109
|
+
"content": "内容",
|
|
110
|
+
"agreement_type": "协议类型",
|
|
111
|
+
},
|
|
112
|
+
"en-US": {
|
|
113
|
+
"name": "Name",
|
|
114
|
+
"code": "Code",
|
|
115
|
+
"type": "Type",
|
|
116
|
+
"icon": "Icon",
|
|
117
|
+
"path": "Path",
|
|
118
|
+
"component": "Component",
|
|
119
|
+
"permission": "Permission",
|
|
120
|
+
"sort": "Sort",
|
|
121
|
+
"status": "Status",
|
|
122
|
+
"description": "Description",
|
|
123
|
+
"remark": "Remark",
|
|
124
|
+
"username": "Username",
|
|
125
|
+
"password": "Password",
|
|
126
|
+
"new_password": "New password",
|
|
127
|
+
"real_name": "Real name",
|
|
128
|
+
"nickname": "Nickname",
|
|
129
|
+
"phone": "Phone",
|
|
130
|
+
"email": "Email",
|
|
131
|
+
"sms_code": "SMS code",
|
|
132
|
+
"captcha_key": "Captcha key",
|
|
133
|
+
"captcha_code": "Captcha code",
|
|
134
|
+
"slide_x": "Slide position",
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _translate_validation_error(error: dict, locale: str = "zh-CN") -> str:
|
|
140
|
+
"""将单个 Pydantic 错误翻译为当前语言"""
|
|
141
|
+
err_type = error.get("type", "")
|
|
142
|
+
loc = error.get("loc", ())
|
|
143
|
+
field = str(loc[-1]) if loc else ""
|
|
144
|
+
field_name = _FIELD_NAME.get(locale, _FIELD_NAME["zh-CN"]).get(field, field)
|
|
145
|
+
ctx = error.get("ctx", {})
|
|
146
|
+
|
|
147
|
+
if err_type == "value_error":
|
|
148
|
+
custom_msg = error.get("msg", "")
|
|
149
|
+
if custom_msg and not custom_msg.startswith(("Value error", "Assertion failed")):
|
|
150
|
+
return f"{field_name}: {custom_msg}"
|
|
151
|
+
return f"{field_name}: {ctx.get('error', translate('common.validation_failed', locale))}"
|
|
152
|
+
|
|
153
|
+
template = _ERROR_MSG.get(locale, _ERROR_MSG["zh-CN"]).get(err_type)
|
|
154
|
+
if template:
|
|
155
|
+
try:
|
|
156
|
+
msg = template.format(**ctx)
|
|
157
|
+
except (KeyError, IndexError):
|
|
158
|
+
msg = template
|
|
159
|
+
return f"{field_name}{msg}"
|
|
160
|
+
|
|
161
|
+
return f"{field_name}: {error.get('msg', translate('common.validation_failed', locale))}"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
async def app_exception_handler(request: Request, exc: AppException):
|
|
165
|
+
"""统一处理业务异常"""
|
|
166
|
+
locale = get_locale_from_request(request)
|
|
167
|
+
return JSONResponse(
|
|
168
|
+
status_code=exc.http_status,
|
|
169
|
+
content={"code": exc.code, "msg": translate(exc.msg, locale), "data": None},
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def validation_exception_handler(request: Request, exc: ValidationError):
|
|
174
|
+
"""统一处理 Pydantic 验证异常 - 多语言友好提示"""
|
|
175
|
+
errors = exc.errors()
|
|
176
|
+
locale = get_locale_from_request(request)
|
|
177
|
+
logger.warning(
|
|
178
|
+
"[Validation] %s %s errors=%s",
|
|
179
|
+
request.method,
|
|
180
|
+
request.url.path,
|
|
181
|
+
errors,
|
|
182
|
+
)
|
|
183
|
+
msg = translate("common.validation_failed", locale) if not errors else _translate_validation_error(errors[0], locale)
|
|
184
|
+
return JSONResponse(
|
|
185
|
+
status_code=422,
|
|
186
|
+
content={"code": ErrorCode.PARAM_ERROR, "msg": msg, "data": None},
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def global_exception_handler(request: Request, exc: Exception):
|
|
191
|
+
"""兜底 - 处理所有未预期的异常"""
|
|
192
|
+
logger.error(f"未预期错误: {exc}", exc_info=True)
|
|
193
|
+
locale = get_locale_from_request(request)
|
|
194
|
+
return JSONResponse(
|
|
195
|
+
status_code=500,
|
|
196
|
+
content={"code": ErrorCode.UNKNOWN, "msg": translate("common.server_error", locale), "data": None},
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def register_exception_handlers(app):
|
|
201
|
+
"""注册所有异常处理器"""
|
|
202
|
+
app.add_exception_handler(AppException, app_exception_handler)
|
|
203
|
+
app.add_exception_handler(RequestValidationError, validation_exception_handler)
|
|
204
|
+
app.add_exception_handler(ValidationError, validation_exception_handler)
|
|
205
|
+
app.add_exception_handler(Exception, global_exception_handler)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""统一异常码体系 - 按模块分段"""
|
|
2
|
+
from enum import IntEnum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ErrorCode(IntEnum):
|
|
6
|
+
"""统一错误码 - 前端拿到 code 就知道具体什么错误"""
|
|
7
|
+
|
|
8
|
+
# 通用 1000-1999
|
|
9
|
+
SUCCESS = 0
|
|
10
|
+
UNKNOWN = 1000
|
|
11
|
+
PARAM_ERROR = 1001
|
|
12
|
+
NOT_FOUND = 1002
|
|
13
|
+
|
|
14
|
+
# 认证 2000-2999
|
|
15
|
+
UNAUTHORIZED = 2000
|
|
16
|
+
TOKEN_EXPIRED = 2001
|
|
17
|
+
FORBIDDEN = 2002
|
|
18
|
+
|
|
19
|
+
# 员工 3000-3999
|
|
20
|
+
EMPLOYEE_NOT_FOUND = 3000
|
|
21
|
+
EMPLOYEE_USERNAME_EXISTS = 3001
|
|
22
|
+
EMPLOYEE_PHONE_EXISTS = 3002
|
|
23
|
+
EMPLOYEE_PASSWORD_ERROR = 3003
|
|
24
|
+
EMPLOYEE_DISABLED = 3004
|
|
25
|
+
|
|
26
|
+
# 角色 4000-4999
|
|
27
|
+
ROLE_NOT_FOUND = 4000
|
|
28
|
+
ROLE_CODE_EXISTS = 4001
|
|
29
|
+
ROLE_IN_USE = 4002
|
|
30
|
+
|
|
31
|
+
# 菜单 4100-4199
|
|
32
|
+
MENU_NOT_FOUND = 4100
|
|
33
|
+
MENU_HAS_CHILDREN = 4101
|
|
34
|
+
|
|
35
|
+
# 部门 4200-4299
|
|
36
|
+
DEPT_NOT_FOUND = 4200
|
|
37
|
+
DEPT_HAS_CHILDREN = 4201
|
|
38
|
+
DEPT_HAS_EMPLOYEES = 4202
|
|
39
|
+
|
|
40
|
+
# 岗位 4300-4399
|
|
41
|
+
POST_NOT_FOUND = 4300
|
|
42
|
+
POST_CODE_EXISTS = 4301
|
|
43
|
+
POST_IN_USE = 4302
|
|
44
|
+
|
|
45
|
+
# 字典 5000-5999
|
|
46
|
+
DICT_TYPE_NOT_FOUND = 5000
|
|
47
|
+
DICT_CODE_EXISTS = 5001
|
|
48
|
+
DICT_TYPE_IN_USE = 5002
|
|
49
|
+
DICT_SYSTEM_PROTECTED = 5003 # 系统内置字典禁止修改/删除
|
|
50
|
+
|
|
51
|
+
# 素材 6000-6999
|
|
52
|
+
MATERIAL_NOT_FOUND = 6000
|
|
53
|
+
MATERIAL_UPLOAD_FAILED = 6001
|
|
54
|
+
MATERIAL_GROUP_NOT_FOUND = 6002
|
|
55
|
+
|
|
56
|
+
# 配置 7000-7999
|
|
57
|
+
CONFIG_NOT_FOUND = 7000
|
|
58
|
+
CONFIG_KEY_EXISTS = 7001
|
|
59
|
+
|
|
60
|
+
# 通知 8000-8999
|
|
61
|
+
NOTICE_NOT_FOUND = 8000
|
|
62
|
+
SMS_SEND_FAILED = 8001
|
|
63
|
+
|
|
64
|
+
# 日志 9000-9999
|
|
65
|
+
LOG_NOT_FOUND = 9000
|
|
66
|
+
|
|
67
|
+
# 协议 9100-9199
|
|
68
|
+
AGREEMENT_NOT_FOUND = 9100
|
|
69
|
+
AGREEMENT_CODE_EXISTS = 9101
|
|
70
|
+
AGREEMENT_NOT_PUBLISHED = 9102
|
|
71
|
+
AGREEMENT_ALREADY_AGREED = 9103
|
|
72
|
+
AGREEMENT_HISTORY_NOT_FOUND = 9104
|
|
73
|
+
AGREEMENT_HISTORY_HAS_USERS = 9105
|
|
74
|
+
AGREEMENT_HAS_USERS = 9106
|
|
75
|
+
AGREEMENT_NEEDS_RECONFIRM = 9107
|
|
76
|
+
|
|
77
|
+
# 用户 9200-9299
|
|
78
|
+
USER_NOT_FOUND = 9200
|
|
79
|
+
USER_PHONE_EXISTS = 9201
|
|
80
|
+
USER_EMAIL_EXISTS = 9202
|
|
81
|
+
USER_PASSWORD_ERROR = 9203
|
|
82
|
+
USER_DISABLED = 9204
|
|
83
|
+
|
|
84
|
+
# 文章 9300-9399
|
|
85
|
+
ARTICLE_NOT_FOUND = 9300
|
|
86
|
+
ARTICLE_CATEGORY_NOT_FOUND = 9310
|
|
87
|
+
ARTICLE_CATEGORY_CODE_EXISTS = 9311
|
|
88
|
+
ARTICLE_CATEGORY_IN_USE = 9312
|
|
89
|
+
ARTICLE_COLLECTION_NOT_FOUND = 9320
|
|
90
|
+
ARTICLE_COLLECTION_NOT_EMPTY = 9321
|
|
91
|
+
|
|
92
|
+
# 相册/照片 9400-9499
|
|
93
|
+
ALBUM_NOT_FOUND = 9400
|
|
94
|
+
ALBUM_NOT_EMPTY = 9401
|
|
95
|
+
PHOTO_NOT_FOUND = 9410
|
|
96
|
+
PHOTO_COVER_EXISTS = 9411
|
|
97
|
+
|
|
98
|
+
# 视频 9500-9599
|
|
99
|
+
VIDEO_NOT_FOUND = 9500
|
|
100
|
+
VIDEO_CATEGORY_NOT_FOUND = 9510
|
|
101
|
+
VIDEO_CATEGORY_CODE_EXISTS = 9511
|
|
102
|
+
VIDEO_CATEGORY_IN_USE = 9512
|
|
103
|
+
VIDEO_COLLECTION_NOT_FOUND = 9520
|
|
104
|
+
VIDEO_COLLECTION_NOT_EMPTY = 9521
|
|
105
|
+
|
|
106
|
+
# 智能客服 9600-9699
|
|
107
|
+
CS_CONFIG_NOT_FOUND = 9600
|
|
108
|
+
CS_KB_NOT_FOUND = 9610
|
|
109
|
+
CS_CHUNK_NOT_FOUND = 9620
|
|
110
|
+
CS_SESSION_NOT_FOUND = 9630
|
|
111
|
+
CS_CONFIG_INACTIVE = 9640
|
|
112
|
+
CS_EMBEDDING_FAILED = 9650
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class AppException(Exception):
|
|
116
|
+
"""应用异常 - 业务代码只管 raise,不管怎么返回"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, code: ErrorCode, msg: str | None = None, http_status: int = 400):
|
|
119
|
+
self.code = code
|
|
120
|
+
self.msg = msg or code.name
|
|
121
|
+
self.http_status = http_status
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class NotFoundException(AppException):
|
|
125
|
+
def __init__(self, msg: str = "资源不存在"):
|
|
126
|
+
super().__init__(ErrorCode.NOT_FOUND, msg, 404)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class UnauthorizedException(AppException):
|
|
130
|
+
def __init__(self, msg: str = "未登录"):
|
|
131
|
+
super().__init__(ErrorCode.UNAUTHORIZED, msg, 401)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ForbiddenException(AppException):
|
|
135
|
+
def __init__(self, msg: str = "无权限"):
|
|
136
|
+
super().__init__(ErrorCode.FORBIDDEN, msg, 403)
|
reglow/common/i18n.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""轻量国际化工具"""
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
DEFAULT_LOCALE = "zh-CN"
|
|
5
|
+
SUPPORTED_LOCALES: set[str] = {"zh-CN", "en-US"}
|
|
6
|
+
|
|
7
|
+
_MESSAGES: dict[str, dict[str, str]] = {
|
|
8
|
+
"zh-CN": {
|
|
9
|
+
"common.success": "操作成功",
|
|
10
|
+
"common.fail": "操作失败",
|
|
11
|
+
"common.server_error": "服务器内部错误",
|
|
12
|
+
"common.validation_failed": "参数校验失败",
|
|
13
|
+
"auth.login_success": "登录成功",
|
|
14
|
+
"auth.logout_success": "退出成功",
|
|
15
|
+
"auth.captcha_error": "验证码错误",
|
|
16
|
+
"auth.slide_verify_failed": "滑动验证失败,请重试",
|
|
17
|
+
"auth.sms_sent": "验证码发送成功",
|
|
18
|
+
},
|
|
19
|
+
"en-US": {
|
|
20
|
+
"common.success": "Success",
|
|
21
|
+
"common.fail": "Failed",
|
|
22
|
+
"common.server_error": "Internal server error",
|
|
23
|
+
"common.validation_failed": "Validation failed",
|
|
24
|
+
"auth.login_success": "Login successful",
|
|
25
|
+
"auth.logout_success": "Logout successful",
|
|
26
|
+
"auth.captcha_error": "Invalid verification code",
|
|
27
|
+
"auth.slide_verify_failed": "Slide verification failed, please try again",
|
|
28
|
+
"auth.sms_sent": "Verification code sent",
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def register_locale(locale: str, messages: dict[str, str]) -> None:
|
|
34
|
+
"""注册新的语言或追加翻译消息。
|
|
35
|
+
|
|
36
|
+
如果 locale 已存在,messages 会合并到已有翻译中(新 key 覆盖旧 key)。
|
|
37
|
+
如果 locale 不存在,则新增该语言。
|
|
38
|
+
"""
|
|
39
|
+
SUPPORTED_LOCALES.add(locale)
|
|
40
|
+
if locale in _MESSAGES:
|
|
41
|
+
_MESSAGES[locale].update(messages)
|
|
42
|
+
else:
|
|
43
|
+
_MESSAGES[locale] = messages
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def add_messages(locale: str, messages: dict[str, str]) -> None:
|
|
47
|
+
"""向已有语言追加翻译消息(register_locale 的别名)"""
|
|
48
|
+
register_locale(locale, messages)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def normalize_locale(value: str | None) -> str:
|
|
52
|
+
if not value:
|
|
53
|
+
return DEFAULT_LOCALE
|
|
54
|
+
locale = value.split(",", 1)[0].split(";", 1)[0].strip()
|
|
55
|
+
if locale in SUPPORTED_LOCALES:
|
|
56
|
+
return locale
|
|
57
|
+
lower = locale.lower()
|
|
58
|
+
if lower.startswith("en"):
|
|
59
|
+
return "en-US"
|
|
60
|
+
if lower.startswith("zh"):
|
|
61
|
+
return "zh-CN"
|
|
62
|
+
return DEFAULT_LOCALE
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_locale_from_request(request: Any) -> str:
|
|
66
|
+
headers = getattr(request, "headers", {}) or {}
|
|
67
|
+
return normalize_locale(headers.get("X-Locale") or headers.get("Accept-Language"))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def translate(key: str, locale: str = DEFAULT_LOCALE, **params: Any) -> str:
|
|
71
|
+
current = normalize_locale(locale)
|
|
72
|
+
text = _MESSAGES.get(current, {}).get(key) or _MESSAGES[DEFAULT_LOCALE].get(key) or key
|
|
73
|
+
return text.format(**params) if params else text
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def translate_request(request: Any, key: str, **params: Any) -> str:
|
|
77
|
+
return translate(key, get_locale_from_request(request), **params)
|