Files
FastapiAdmin/backend/app/scripts/initialize.py
T
zhangtao 8949f6dc46 refactor(test, config, router): 整理代码结构与配置,优化导入顺序
1. 调整多个测试文件中TestClient的导入顺序,统一放到conftest导入之后
2. 删除后端配置中无用的WebSocket限流配置,统一使用全局200次/10秒的限流策略
3. 精简平台、系统模块的路由初始化注释文档
4. 优化conftest.py中的数据库初始化导入顺序
5. 调整初始化日志输出格式,更清晰易懂
6. 前端开发环境配置切回本地后端地址,注释远程服务地址
7. 重构路由类的变量命名,提升可读性
8. 简化动态路由热重载逻辑,移除冗余缓存变量
9. 重构注册路由的代码,统一限流配置,调整导入顺序和挂载逻辑
10. 删除冗余的v1接口层统一导出文件
2026-06-21 21:47:32 +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.info(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.info(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.info(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.info(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.info(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