Files
FastapiAdmin/backend/app/scripts/initialize.py
T
zhangtao 211fddd6e0 refactor: 完成项目大规模代码重构与依赖清理
这是一次综合性的重构更新,包含以下主要变更:
1.  升级Python版本到3.12,更新依赖配置
2.  替换旧的.j2模板为.jinja2格式,新增代码生成模板
3.  重构权限过滤策略,更新权限枚举与模型配置
4.  移除Prefect依赖,替换为自研拓扑并行执行引擎
5.  重构认证与上下文管理,拆分租户/请求上下文
6.  简化响应模型、CRUD与服务层代码
7.  清理废弃的支付网关模块,重构订单定时任务
8.  更新在线用户、监控等模块的接口与路由
9.  优化邮件模板与工具类,新增邮件模板文件
10. 修复数据库会话配置与类型提示
2026-06-21 06:02:05 +08:00

242 lines
10 KiB
Python
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.
"""
数据库初始化与种子数据管理。
简化策略:每张表为空时一次性插入种子数据,已有数据则跳过。
改 JSON → 清空对应表 → 重启即可。
"""
import asyncio
import json
import re
from datetime import datetime, time
from typing import Any
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.v1.module_platform.email.model import EmailConfigModel, EmailTemplateModel
from app.api.v1.module_platform.invoice.model import InvoiceModel
from app.api.v1.module_platform.menu.model import MenuModel
from app.api.v1.module_platform.order.model import OrderModel, PaymentRecordModel, RefundModel
from app.api.v1.module_platform.package.model import PackageMenuModel, PackageModel
from app.api.v1.module_platform.plugin.model import PluginModel, TenantPluginModel
from app.api.v1.module_platform.tenant.model import TenantModel, TenantUserModel
from app.api.v1.module_system.dept.model import DeptModel
from app.api.v1.module_system.dict.model import DictDataModel, DictTypeModel
from app.api.v1.module_system.log.model import LoginLogModel, OperationLogModel
from app.api.v1.module_system.notice.model import NoticeModel, NoticeReadModel
from app.api.v1.module_system.params.model import ParamsModel
from app.api.v1.module_system.position.model import PositionModel
from app.api.v1.module_system.role.model import RoleModel
from app.api.v1.module_system.ticket.model import TicketModel
from app.api.v1.module_system.user.model import UserModel, UserRolesModel
from app.config.path_conf import SCRIPT_DIR
from app.core.database import async_db_session, create_tables
from app.core.logger import logger
from app.plugin.module_example.demo.model import DemoModel
from app.plugin.module_task.cronjob.node.model import NodeModel
from app.plugin.module_task.workflow.nodes.model import WorkflowNodeTypeModel
class InitializeData:
"""初始化数据库和基础数据"""
_DATETIME_RE = re.compile(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$")
_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
_TIME_RE = re.compile(r"^\d{2}:\d{2}:\d{2}(\.\d+)?$")
# 按依赖关系排序:先基础表,再关联表
prepare_init_models: list[type] = [
# ── 平台管理:基础表 ──
PackageModel,
TenantModel,
PluginModel,
MenuModel,
# ── 系统管理:基础表 ──
ParamsModel,
DeptModel,
RoleModel,
DictTypeModel,
DictDataModel,
PositionModel,
UserModel,
# ── 平台管理:依赖用户的表 ──
EmailConfigModel,
EmailTemplateModel,
OrderModel,
InvoiceModel,
PaymentRecordModel,
RefundModel,
# ── 关联表 ──
UserRolesModel,
TenantUserModel,
PackageMenuModel,
TenantPluginModel,
# ── 其他系统/业务表 ──
NoticeModel,
NoticeReadModel,
TicketModel,
# ── 日志表(追加写入) ──
LoginLogModel,
OperationLogModel,
# ── 插件表 ──
NodeModel,
WorkflowNodeTypeModel,
DemoModel,
]
# 树形模型:JSON 含嵌套 children,需递归创建对象
_RECURSIVE_TABLES: set[str] = {"platform_menu", "sys_dept"}
async def init_db(self) -> None:
"""建表并导入种子数据"""
try:
await create_tables()
except asyncio.exceptions.TimeoutError:
logger.error("❌️ 数据库表结构初始化超时")
raise
async with async_db_session() as session:
async with session.begin():
await self.__init_data(session)
async def __init_data(self, db: AsyncSession) -> None:
"""按依赖顺序初始化各表种子数据"""
dict_type_mapping: dict[str, Any] = {} # dict_type → DictTypeModel 实例
for model in self.prepare_init_models:
table_name = model.__tablename__
data = await self.__load_json(table_name)
if not data:
logger.warning(f"⚠️ 跳过 {table_name} 表,无初始化数据")
continue
try:
# 树形表(platform_menu / sys_dept):递归创建含 children 的对象
if table_name in self._RECURSIVE_TABLES:
count = await db.execute(select(func.count()).select_from(model))
if count.scalar():
logger.warning(f"⚠️ 跳过 {table_name} 表数据初始化(表已有数据)")
continue
objs = self.__create_objects_with_children(data, model)
db.add_all(objs)
await db.flush()
logger.info(f"✅️ 已向 {table_name} 写入初始化数据")
continue
# 字典类型表:存储类型映射供字典数据使用
if table_name == "sys_dict_type":
count = await db.execute(select(func.count()).select_from(model))
if count.scalar():
logger.warning(f"⚠️ 跳过 {table_name} 表数据初始化(表已有数据)")
continue
objs = []
for item in data:
obj = model(**item)
objs.append(obj)
dict_type_mapping[item["dict_type"]] = obj
db.add_all(objs)
await db.flush()
logger.info(f"✅️ 已向 {table_name} 写入初始化数据")
continue
# 字典数据表:关联 dict_type_id
if table_name == "sys_dict_data":
count = await db.execute(select(func.count()).select_from(model))
if count.scalar():
logger.warning(f"⚠️ 跳过 {table_name} 表数据初始化(表已有数据)")
continue
objs = []
for item in data:
dict_type_str = item.get("dict_type")
if dict_type_str not in dict_type_mapping:
logger.warning(f"⚠️ 未找到字典类型 {dict_type_str},跳过")
continue
item["dict_type_id"] = dict_type_mapping[dict_type_str].id
objs.append(model(**item))
db.add_all(objs)
await db.flush()
logger.info(f"✅️ 已向 {table_name} 写入初始化数据")
continue
# 日志表:追加写入,已有数据跳过
if table_name in ("sys_login_log", "sys_operation_log"):
count = await db.execute(select(func.count()).select_from(model))
if count.scalar():
logger.info(f"⏭️ {table_name} 已有数据,跳过")
continue
objs = [model(**item) for item in data]
db.add_all(objs)
await db.flush()
logger.info(f"✅️ 已向 {table_name} 写入 {len(objs)} 条")
continue
# 普通表:空表时插入,已有数据跳过
count = await db.execute(select(func.count()).select_from(model))
if count.scalar():
logger.warning(f"⚠️ 跳过 {table_name} 表数据初始化(表已有数据)")
continue
objs = [model(**item) for item in data]
db.add_all(objs)
await db.flush()
logger.info(f"✅️ 已向 {table_name} 写入初始化数据")
except Exception:
logger.error(f"❌️ 初始化 {table_name} 表数据失败")
raise
@staticmethod
def __create_objects_with_children(data: list[dict], model_class: type) -> list:
"""递归创建树形模型实例,处理嵌套 children 并注入 parent_id"""
def _create(obj_data: dict) -> Any:
children_data = obj_data.pop("children", [])
# JSON 中子节点 parent_id 通常为 null,先按原始值创建
obj = model_class(**obj_data)
if children_data:
obj.children = [_create(child) for child in children_data]
return obj
return [_create(item) for item in data]
async def __load_json(self, filename: str) -> list[dict]:
"""读取并解析种子数据 JSON 文件"""
json_path = SCRIPT_DIR / f"{filename}.json"
if not json_path.exists():
return []
try:
with open(json_path, encoding="utf-8") as f:
raw = json.loads(f.read())
return [self._parse_date_strings(item) for item in raw]
except json.JSONDecodeError as e:
logger.error(f"❌️ 解析 {json_path} 失败: {e!s}")
raise
except Exception as e:
logger.error(f"❌️ 读取 {json_path} 失败: {e!s}")
raise
@classmethod
def _parse_date_strings(cls, data: dict) -> dict:
"""递归转换 JSON 中的日期时间字符串为 datetime 对象(兼容 PostgreSQL"""
result = {}
for key, value in data.items():
if isinstance(value, str):
if cls._DATETIME_RE.match(value):
result[key] = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
elif cls._DATE_RE.match(value):
result[key] = datetime.strptime(value, "%Y-%m-%d").date()
elif cls._TIME_RE.match(value):
result[key] = time.fromisoformat(value)
else:
result[key] = value
elif isinstance(value, dict):
result[key] = cls._parse_date_strings(value)
else:
result[key] = value
return result