Files
djangoadmin-wiki/DjangoAdmin_Tornado_EleVue/zh/volume3/extensibility.md
T
2026-09-16 16:36:49 +08:00

540 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 扩展性设计
本章详细描述系统的扩展性设计,包括模块可插拔、数据库驱动可切换、中间件可插拔三大扩展维度。
:::tip 扩展性设计原则
遵循开闭原则(OCP):对扩展开放,对修改关闭。新功能通过添加新模块实现,而非修改现有代码。核心框架通过基类、装饰器、中间件等机制提供扩展点,业务模块只需声明差异即可接入。
:::
## 一、模块可插拔设计
### 模块标准结构
每个业务模块遵循固定的四文件结构,新增模块只需创建这四个文件即可接入系统:
```
src/modules/{group}/{name}/
├── models.py # ORM 模型
├── repository.py # 数据访问层
├── schemas.py # 请求校验
└── service.py # 业务逻辑层
```
### 新增模块步骤
以新增「培训管理(Training)」模块为例。
**步骤一:创建模块目录和文件**
创建 `src/modules/training/` 目录,包含以下四个文件:
`models.py` — ORM 模型:
```python
# src/modules/training/models.py
from sqlalchemy import Column, String, Integer, text
from core.base_model import BaseModel
from core.base_model import base_model
from core.config import DB_PREFIX
class Training(BaseModel):
__tablename__ = DB_PREFIX + "training"
__table_comment__ = "培训记录表"
title = Column(String(200), nullable=False, comment="培训标题")
content = Column(String(2000), nullable=True, comment="培训内容")
trainer = Column(String(50), nullable=True, comment="培训讲师")
status = Column(Integer, default=1, server_default=text('1'), comment="状态")
sort = Column(Integer, default=0, server_default=text('0'), comment="排序")
```
`repository.py` — 数据访问层:
```python
# src/modules/training/repository.py
from training.models import Training
from core.base_repository import BaseRepository
class TrainingRepository(BaseRepository(Training]):
pass
training_repo = TrainingRepository(Training)
```
`schemas.py` — 请求校验:
```python
# src/modules/training/schemas.py
from pydantic import BaseModel, Field
from core.base_schema import BaseSchema
class TrainingForm(BaseSchema):
title: str = Field(..., min_length=1, max_length=200, description="培训标题")
content: str = Field(None, max_length=2000, description="培训内容")
trainer: str = Field(None, max_length=50, description="培训讲师")
status: int = Field(..., ge=1, le=2, description="状态")
sort: int = Field(..., ge=0, le=99999, description="排序")
class TrainingStatusForm(BaseModel):
id: int = Field(..., gt=0, description="培训ID")
status: int = Field(..., ge=1, le=2, description="状态")
```
`service.py` — 业务逻辑层:
```python
# src/modules/training/service.py
from core.base_service import BaseService
from training.models import Training
from training.repository import training_repo
class TrainingService(BaseService(Training]):
repo = training_repo
model = Training
page_like_fields = ('title', 'trainer')
page_eq_fields = ('status',)
page_order_by = (('sort', 'asc'),)
unique_fields = {'title': '培训标题不能重复'}
training_service = TrainingService()
```
**步骤二:创建 Handler**
创建 `src/modules/training/handlers.py`:
```python
# src/modules/training/handlers.py
from common.middleware.access_decorators import check_demo, permission_required
from common.middleware.operation_log import operation_log
from core import response as R
from core.base_handler import BaseHandler
from modules.training.service import training_service
class TrainingPageHandler(BaseHandler):
@permission_required("sys:training:page")
async def get(self):
return await training_service.get_page(self)
class TrainingDetailHandler(BaseHandler):
@permission_required("sys:training:detail")
async def get(self, id):
return R.ok(self, data=training_service.get_detail(id))
class TrainingAddHandler(BaseHandler):
@permission_required("sys:training:add")
@check_demo
@operation_log("培训管理", "添加")
async def post(self):
return await training_service.add(self)
class TrainingUpdateHandler(BaseHandler):
@permission_required("sys:training:update")
@check_demo
@operation_log("培训管理", "更新")
async def put(self):
return await training_service.update(self)
class TrainingDeleteHandler(BaseHandler):
@permission_required("sys:training:delete")
@check_demo
@operation_log("培训管理", "删除")
async def delete(self, id):
return await training_service.delete(self, id)
class TrainingStatusHandler(BaseHandler):
@permission_required("sys:training:status")
@check_demo
@operation_log("培训管理", "设置状态")
async def put(self):
return await training_service.update_status(self)
class TrainingBatchDeleteHandler(BaseHandler):
@permission_required("sys:training:delete")
@check_demo
@operation_log("培训管理", "批量删除")
async def post(self):
return await training_service.batch_delete(self)
```
**步骤三:注册路由**
在 `src/api/v1/` 下创建路由文件,并在 `router.py` 中注册:
```python
# src/api/v1/training.py
from modules.training.handlers import (
TrainingAddHandler, TrainingBatchDeleteHandler, TrainingDeleteHandler,
TrainingDetailHandler, TrainingPageHandler, TrainingStatusHandler,
TrainingUpdateHandler,
)
routes = [
(r"/training/page", TrainingPageHandler),
(r"/training/detail/(\d+)", TrainingDetailHandler),
(r"/training/add", TrainingAddHandler),
(r"/training/update", TrainingUpdateHandler),
(r"/training/delete/(\d+)", TrainingDeleteHandler),
(r"/training/status", TrainingStatusHandler),
(r"/training/batchDelete", TrainingBatchDeleteHandler),
]
```
```python
# src/api/v1/router.py
from api.v1 import training
_RESOURCE_MODULES = [
# ... 其他模块 ...
training,
]
```
**步骤四:配置菜单权限**
在数据库 `tornado_menu` 表中插入对应的菜单和权限节点记录。权限标识格式为 `sys:{module}:{action}`,如 `sys:training:add`、`sys:training:page`。
### 模块解耦
模块之间通过以下方式解耦:
| 解耦方式 | 说明 | 示例 |
|:---|:---|:---|
| **Repository 单例** | 模块级单例,通过 import 引用 | `from modules.user.repository import UserRepository` |
| **Service 单例** | 模块级单例,通过 import 引用 | `from user.service import user_service` |
| **跨模块引用** | 通过 Repository 引用其他模块的模型 | PositionService 引用 User 模型检查引用 |
| **事件机制** | 暂未实现(未来可通过事件总线解耦) | — |
### 复杂模块扩展
对于业务逻辑复杂的模块(如 User、Role、Menu),在继承 `BaseService` 的基础上添加自定义方法:
```python
# src/modules/user/service.py
class UserService(BaseService(User]):
repo = user_repo
model = User
page_like_fields = ('username', 'realname')
page_eq_fields = ('status', 'dept_id')
# 自定义方法:用户详情(含角色列表)
def get_user_detail(self, user_id):
user = self.repo.get_by_id(user_id)
if not user:
return None
data = user.to_dict()
data['roleIds'] = get_user_role_ids(user_id)
data['roleNames'] = get_user_role_names(user_id)
return data
# 自定义方法:修改密码
def change_password(self, user_id, old_pwd, new_pwd):
user = self.repo.get_by_id(user_id)
if not verify_password(old_pwd, user.password):
return R.failed("原密码错误")
user.password = hash_password(new_pwd)
user_repo.create(user)
return R.ok(msg="密码修改成功")
# 覆盖钩子:新增前哈希密码
def _before_add(self, request, data):
data.password = hash_password(data.password or DEFAULT_PASSWORD)
# 覆盖钩子:删除前检查
def _before_delete(self, ids):
if 1 in parse_id_list(str(ids)):
return "超级管理员不能删除"
return None
```
## 二、数据库驱动可切换
### 驱动注册机制
项目通过 `_SUPPORTED_DRIVERS` 声明支持的驱动,`_normalize_driver()` 处理别名映射和校验,`build_database_url()` 按驱动构建连接串:
```python
# src/config/database.py(简化)
import os
from urllib.parse import quote_plus
# 支持的数据库驱动
_SUPPORTED_DRIVERS = ("mysql", "postgresql", "mssql", "sqlite", "oracle")
def _normalize_driver(driver: str) -> str:
"""归一化 DB_DRIVER:别名映射 + 校验"""
name = str(driver).strip().lower()
alias_map = {
'postgres': 'postgresql',
'pg': 'postgresql',
'sqlserver': 'mssql',
'sql_server': 'mssql',
'sqlite3': 'sqlite',
}
name = alias_map.get(name, name)
if name not in _SUPPORTED_DRIVERS:
raise ValueError(f"不支持的 DB_DRIVER: {driver!r}")
return name
DB_DRIVER = _normalize_driver(os.getenv('DB_DRIVER', 'mysql'))
DB_HOST = os.getenv('DB_HOST', '127.0.0.1')
DB_PORT = int(os.getenv('DB_PORT', '3306'))
DB_DATABASE = os.getenv('DB_DATABASE', 'djangoadmin.tornado.elevue')
DB_USERNAME = os.getenv('DB_USERNAME', 'root')
DB_PASSWORD = os.getenv('DB_PASSWORD', '')
def build_database_url(driver: str = None) -> str:
"""按驱动构建 SQLAlchemy 连接串"""
drv = _normalize_driver(driver if driver is not None else DB_DRIVER)
if drv == 'sqlite':
return 'sqlite:///./' + DB_DATABASE
base = quote_plus(DB_USERNAME) + ':' + quote_plus(DB_PASSWORD) + '@' + DB_HOST + ':' + str(DB_PORT)
if drv == 'mysql':
return 'mysql+pymysql://' + base + '/' + DB_DATABASE + '?charset=utf8mb4'
if drv == 'postgresql':
return 'postgresql+psycopg://' + base + '/' + DB_DATABASE
if drv == 'oracle':
return 'oracle+oracledb://' + base + '/?service_name=' + quote_plus(DB_DATABASE)
# mssql
return 'mssql+pymssql://' + base + '/' + DB_DATABASE + '?charset=utf8'
```
各驱动的连接串格式:
| 驱动 | 连接串格式 | Python 包 |
|:----|:----|:----|
| mysql | `mysql+pymysql://user:pass@host:port/db?charset=utf8mb4` | PyMySQL |
| postgresql | `postgresql+psycopg://user:pass@host:port/db` | psycopg[binary] |
| mssql | `mssql+pymssql://user:pass@host:port/db?charset=utf8` | pymssql |
| oracle | `oracle+oracledb://user:pass@host:port/?service_name=db` | oracledb |
| sqlite | `sqlite:///./path/to/db` | 内置 |
### 切换方式
只需修改 `.env` 文件中的 `DB_DRIVER` 和对应的连接参数:
```bash
# MySQL(默认)
DB_DRIVER=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=djangoadmin
DB_USER=root
DB_PASSWORD=your_password
# PostgreSQL
DB_DRIVER=postgresql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_NAME=djangoadmin
DB_USER=postgres
DB_PASSWORD=your_password
# SQLite
DB_DRIVER=sqlite
DB_DATABASE=./data.db
# SQL Server
DB_DRIVER=mssql
DB_HOST=127.0.0.1
DB_PORT=1433
DB_NAME=djangoadmin
DB_USER=sa
DB_PASSWORD=your_password
```
:::warning 驱动切换注意事项
1. 切换驱动后需安装对应的 Python 包(如 `psycopg[binary]` for PostgreSQL)
2. 数据迁移使用 `scripts/migrate_db.py` 工具
3. SQLite 不支持并发写入,仅适用于开发/测试环境
4. Oracle 需要额外的客户端库配置
:::
### 跨库数据迁移
```bash
# 干跑:核对迁移计划
python scripts/migrate_db.py --src-driver mysql --src-host ... --dst-driver postgresql --dst-host ... --dry-run
# 执行迁移
python scripts/migrate_db.py --src-driver mysql --src-host ... --dst-driver postgresql --dst-host ...
# 幂等重跑(先删除目标库数据)
python scripts/migrate_db.py --src-driver mysql --src-host ... --dst-driver postgresql --dst-host ... --drop-target-first
```
## 三、横切关注点扩展
### Handler 生命周期
Tornado 通过 Handler 生命周期方法实现横切关注点:
```python
# src/core/base_handler.py
class BaseHandler(tornado.web.RequestHandler):
def set_default_headers(self):
"""设置响应头:CORS + trace_id"""
cros_required(self)
apply_trace_id(self)
@login_required
def prepare(self):
"""请求预处理:JWT 认证"""
pass
def on_finish(self):
"""请求结束后:释放数据库会话"""
db.close()
```
### 自定义装饰器
通过 `permission_required` 和 `check_demo` 的模式,可以创建自定义装饰器:
```python
# src/common/middleware/operation_log.py
def operation_log(module: str, action: str):
"""操作日志记录装饰器"""
def decorator(func):
@wraps(func)
async def wrapper(self, *args, **kwargs):
start = time.time()
result = await func(self, *args, **kwargs)
duration = time.time() - start
save_log(module, action, duration)
return result
return wrapper
return decorator
```
使用示例:
```python
class TrainingAddHandler(BaseHandler):
@permission_required("sys:training:add")
@check_demo
@operation_log("培训管理", "添加")
async def post(self):
return await training_service.add(self)
```
装饰器执行顺序(从外到内):`@permission_required` → `@check_demo` → `@operation_log` → 方法体。
## 四、装饰器扩展
### 自定义装饰器
通过 `permission_required` 和 `check_demo` 的模式,可以创建自定义装饰器:
```python
# src/common/decorators.py
# 自定义装饰器示例:操作日志记录
def log_operation(module: str, action: str):
"""
操作日志记录装饰器
自动记录接口调用的模块、操作类型、执行耗时
Args:
module: 所属模块名称,如 "培训管理"
action: 操作类型,如 "新增"、"编辑"、"删除"
"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# 记录开始时间,用于计算接口执行耗时
start = time.time()
# 执行原业务函数
result = await func(*args, **kwargs)
# 计算耗时(秒)
duration = time.time() - start
# 异步记录操作日志到数据库(包含模块、操作、耗时等信息)
save_log(module, action, duration)
return result
return wrapper
return decorator
# src/modules/training/handlers.py
# 使用示例:多个装饰器叠加使用
# 执行顺序:从外到内(先 permission_required,再 check_demo,再 operation_log)
class TrainingAddHandler(BaseHandler):
@permission_required("sys:training:add") # 权限校验
@check_demo # 演示环境拦截
@operation_log("培训管理", "添加") # 操作日志记录
async def post(self):
"""新增培训记录"""
return await training_service.add(self)
```
## 五、配置扩展
### 环境变量驱动
所有可配置项通过 `.env` 文件管理,新增配置项只需:
1. 在 `.env` 文件中添加变量
2. 在 `src/config/` 对应模块中读取
3. 在业务代码中引用
```bash
# .env
CUSTOM_FEATURE_ENABLED=true
CUSTOM_TIMEOUT=30
```
```python
# src/config/custom.py
CUSTOM_FEATURE_ENABLED = os.getenv("CUSTOM_FEATURE_ENABLED", "false").lower() == "true"
CUSTOM_TIMEOUT = int(os.getenv("CUSTOM_TIMEOUT", "30"))
```
## 六、前端扩展
### 新增页面
1. 在 `ui/src/api/` 创建 API 文件
2. 在 `ui/src/views/` 创建页面组件
3. 在数据库 `tornado_menu` 表中配置菜单路由
### 新增组件
公共组件放在 `ui/src/components/` 目录,遵循 Vue3 组件规范。
## 扩展性矩阵
| 扩展维度 | 扩展方式 | 修改范围 | 复杂度 |
|:---|:---|:---|:---|
| 新增 CRUD 模块 | 创建 4 文件 + 注册路由 | 新增文件,不改现有代码 | 低 |
| 新增复杂模块 | 继承基类 + 自定义方法 | 新增文件,不改现有代码 | 中 |
| 切换数据库驱动 | 修改 .env 配置 | 仅改配置 | 低 |
| 新增中间件 | 创建中间件 + 注册 | 新增文件 + 改 app.py | 低 |
| 新增装饰器 | 创建装饰器 + 应用 | 新增文件 | 低 |
| 新增配置项 | .env + config 模块 | 新增配置 | 低 |
| 新增前端页面 | API + Views + Menu | 新增文件 + 改数据库 | 中 |
## 总结
通过分层架构、基类模板方法、装饰器、中间件、环境变量配置等机制,实现了高度的可扩展性。新增 CRUD 模块只需 4 个标准文件 + 1 行路由注册,不修改任何现有代码。数据库驱动通过环境变量切换,中间件通过注册机制插拔。这种设计使得系统在功能持续增长的同时,保持代码结构的清晰和稳定。