Become a sponsor

本章详细描述系统的扩展性设计,包括模块可插拔、数据库驱动可切换、中间件可插拔三大扩展维度。
扩展性设计原则
遵循开闭原则(OCP):对扩展开放,对修改关闭。新功能通过添加新模块实现,而非修改现有代码。核心框架通过基类、装饰器、中间件等机制提供扩展点,业务模块只需声明差异即可接入。
每个业务模块遵循固定的四文件结构,新增模块只需创建这四个文件即可接入系统:
src/modules/{group}/{name}/
├── models.py # ORM 模型
├── repository.py # 数据访问层
├── schemas.py # 请求校验
└── service.py # 业务逻辑层以新增「培训管理(Training)」模块为例。
步骤一:创建模块目录和文件
创建 src/modules/training/ 目录,包含以下四个文件:
models.py — ORM 模型:
# 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 — 数据访问层:
# 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 — 请求校验:
# 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 — 业务逻辑层:
# 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:
# 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 中注册:
# 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),
]# 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 的基础上添加自定义方法:
# 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() 按驱动构建连接串:
# 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.antdvue')
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 和对应的连接参数:
# 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驱动切换注意事项
psycopg[binary] for PostgreSQL)scripts/migrate_db.py 工具# 干跑:核对迁移计划
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-firstTornado 通过 Handler 生命周期方法实现横切关注点:
# 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 的模式,可以创建自定义装饰器:
# 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使用示例:
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 的模式,可以创建自定义装饰器:
# 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 文件管理,新增配置项只需:
.env 文件中添加变量src/config/ 对应模块中读取# .env
CUSTOM_FEATURE_ENABLED=true
CUSTOM_TIMEOUT=30# src/config/custom.py
CUSTOM_FEATURE_ENABLED = os.getenv("CUSTOM_FEATURE_ENABLED", "false").lower() == "true"
CUSTOM_TIMEOUT = int(os.getenv("CUSTOM_TIMEOUT", "30"))ui/src/api/ 创建 API 文件ui/src/views/ 创建页面组件tornado_menu 表中配置菜单路由公共组件放在 ui/src/components/ 目录,遵循 Vue3 组件规范。
| 扩展维度 | 扩展方式 | 修改范围 | 复杂度 |
|---|---|---|---|
| 新增 CRUD 模块 | 创建 4 文件 + 注册路由 | 新增文件,不改现有代码 | 低 |
| 新增复杂模块 | 继承基类 + 自定义方法 | 新增文件,不改现有代码 | 中 |
| 切换数据库驱动 | 修改 .env 配置 | 仅改配置 | 低 |
| 新增中间件 | 创建中间件 + 注册 | 新增文件 + 改 app.py | 低 |
| 新增装饰器 | 创建装饰器 + 应用 | 新增文件 | 低 |
| 新增配置项 | .env + config 模块 | 新增配置 | 低 |
| 新增前端页面 | API + Views + Menu | 新增文件 + 改数据库 | 中 |
通过分层架构、基类模板方法、装饰器、中间件、环境变量配置等机制,实现了高度的可扩展性。新增 CRUD 模块只需 4 个标准文件 + 1 行路由注册,不修改任何现有代码。数据库驱动通过环境变量切换,中间件通过注册机制插拔。这种设计使得系统在功能持续增长的同时,保持代码结构的清晰和稳定。