feat: 重构代码生成模块并优化定时任务功能

refactor(backend): 清理冗余模板文件并统一模板结构
fix(backend): 修复定时任务状态显示与操作逻辑
perf(backend): 优化数据库连接日志和初始化流程
style(frontend): 调整应用列表空状态显示样式
docs: 更新README中的命令说明和环境配置
chore: 添加banner.txt和统一环境变量格式
This commit is contained in:
zhangtao
2025-11-02 18:01:05 +08:00
parent 0f1916e544
commit 2c7174db32
46 changed files with 1286 additions and 1786 deletions
-28
View File
@@ -1,29 +1 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter
# 系统管理模块
from .module_system import SystemRouter
# 监控管理模块
from .module_monitor import MonitorRouter
# 通用模块
from .module_common import CommonRouter
# 应用模块
from .module_application import ApplicationRouter
# 代码生成模块
from .module_generator import GeneratorRouter
# 创建主路由
router = APIRouter()
# 注册各模块路由
router.include_router(SystemRouter)
router.include_router(MonitorRouter)
router.include_router(CommonRouter)
router.include_router(ApplicationRouter)
router.include_router(GeneratorRouter)
@@ -1,15 +1 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from .myapp.controller import MyAppRouter
from .ai.controller import AIRouter
from .job.controller import JobRouter
ApplicationRouter = APIRouter(prefix="/application")
# 包含所有子路由
ApplicationRouter.include_router(MyAppRouter)
ApplicationRouter.include_router(AIRouter)
ApplicationRouter.include_router(JobRouter)
# -*- coding: utf-8 -*-
@@ -149,4 +149,13 @@ class JobLogCRUD(CRUDBase[JobLogModel, JobLogCreateSchema, JobLogUpdateSchema]):
参数:
- ids (List[int]): 定时任务日志ID列表
"""
return await self.delete(ids=ids)
return await self.delete(ids=ids)
async def clear_obj_log_crud(self) -> None:
"""
清除定时任务日志
注意:
- 此操作会删除所有定时任务日志,请谨慎操作
"""
return await self.clear()
@@ -63,10 +63,10 @@ class JobService:
exist_obj = await JobCRUD(auth).get(name=data.name)
if exist_obj:
raise CustomException(msg='创建失败,该定时任务已存在')
if data.trigger == 'cron' and data.trigger_args and not CronUtil.validate_cron_expression(data.trigger_args):
raise CustomException(msg=f'新增定时任务{data.name}失败, Cron表达式不正确')
obj = await JobCRUD(auth).create_obj_crud(data=data)
if not obj:
raise CustomException(msg='创建失败,该数据定时任务不存在')
SchedulerUtil().add_job(job_info=obj)
return JobOutSchema.model_validate(obj).model_dump()
@@ -109,6 +109,10 @@ class JobService:
exist_obj = await JobCRUD(auth).get_obj_by_id_crud(id=id)
if not exist_obj:
raise CustomException(msg='删除失败,该数据定时任务不存在')
obj = await JobLogCRUD(auth).get(job_id=id)
if obj:
raise CustomException(msg=f'删除失败,该定时任务存 {exist_obj.name} 在日志记录')
SchedulerUtil.remove_job(job_id=id)
await JobCRUD(auth).delete_obj_crud(ids=ids)
@@ -122,6 +126,7 @@ class JobService:
- auth (AuthSchema): 认证信息模型
"""
SchedulerUtil().clear_jobs()
await JobLogCRUD(auth).clear_obj_log_crud()
await JobCRUD(auth).clear_obj_crud()
@classmethod
+1 -11
View File
@@ -1,11 +1 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from .file.controller import FileRouter
CommonRouter = APIRouter(prefix="/common")
# 包含所有子路由
CommonRouter.include_router(FileRouter)
# -*- coding: utf-8 -*-
@@ -1,12 +1 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from .gencode.controller import GenRouter
from .demo.controller import DemoRouter
# 创建代码生成模块路由
GeneratorRouter = APIRouter(prefix="/generator")
# 包含代码生成路由
GeneratorRouter.include_router(GenRouter)
GeneratorRouter.include_router(DemoRouter)
@@ -103,9 +103,8 @@ async def gen_table_detail_controller(
返回:
- JSONResponse: 包含业务表详细信息的JSON响应
"""
gen_table = await GenTableService.get_gen_table_by_id_service(auth, table_id)
gen_tables = await GenTableService.get_gen_table_all_service(auth)
gen_table_detail_result = dict(info=gen_table.model_dump(), rows=gen_table.model_dump()['columns'], tables=[gen_table.model_dump() for gen_table in gen_tables])
# 统一走服务层的聚合逻辑,避免控制器拼装重复代码
gen_table_detail_result = await GenTableService.get_gen_table_detail_service(auth, table_id)
logger.info(f'获取table_id为{table_id}的信息成功')
return SuccessResponse(data=gen_table_detail_result, msg="获取业务表详细信息成功")
@@ -188,18 +187,8 @@ async def batch_gen_code_controller(
返回:
- StreamResponse: 包含批量生成代码的ZIP文件流响应
"""
# 检查table_names是否为空
if not table_names:
logger.error('表名列表不能为空')
# 返回一个空的StreamResponse,包含错误信息
error_content = bytes(f'{RET.ERROR.msg}: 表名列表不能为空', 'utf-8')
return StreamResponse(
data=bytes2file_response(error_content),
media_type='text/plain',
headers={'Content-Disposition': 'attachment; filename=error.txt'}
)
batch_gen_code_result = await GenTableService.batch_gen_code_service(auth, table_names)
logger.info('批量生成代码成功')
logger.info(f'批量生成代码成功,表名列表:{table_names}')
return StreamResponse(
data=bytes2file_response(batch_gen_code_result),
media_type='application/zip',
@@ -223,7 +212,7 @@ async def gen_code_local_controller(
- JSONResponse: 包含生成结果的JSON响应
"""
result = await GenTableService.generate_code_service(auth, table_name)
logger.info('生成代码到指定路径成功')
logger.info(f'生成代码,表名:{table_name},到指定路径成功')
return SuccessResponse(msg="生成代码到指定路径成功", data=result)
@@ -243,7 +232,7 @@ async def preview_code_controller(
- JSONResponse: 包含预览代码的JSON响应
"""
preview_code_result = await GenTableService.preview_code_service(auth, table_id)
logger.info('预览代码成功')
logger.info(f'预览代码,表id{table_id},成功')
return SuccessResponse(data=preview_code_result, msg="预览代码成功")
@@ -263,5 +252,5 @@ async def sync_db_controller(
- JSONResponse: 包含同步数据库结果的JSON响应
"""
result = await GenTableService.sync_db_service(auth, table_name)
logger.info('同步数据库成功')
logger.info(f'同步数据库,表名:{table_name},成功')
return SuccessResponse(msg="同步数据库成功", data=result)
@@ -1,9 +1,7 @@
# -*- coding:utf-8 -*-
from sqlalchemy.engine.row import Row
from sqlalchemy import and_, delete, select, text, update
from sqlalchemy.orm import selectinload
from sqlglot.expressions import Expression
from sqlalchemy import and_, select, text
from typing import List, Optional, Sequence, Dict, Union, Any
from app.core.logger import logger
@@ -45,19 +43,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
返回:
- GenTableModel | None: 业务表信息对象。
"""
gen_table = (
(
await self.db.execute(
select(GenTableModel)
.options(selectinload(GenTableModel.columns))
.where(GenTableModel.id == table_id)
)
)
.scalars()
.first()
)
return gen_table
return await self.get(id=table_id, preload=preload)
async def get_gen_table_by_name(self, table_name: str, preload: Optional[List[Union[str, Any]]] = None) -> Optional[GenTableModel]:
"""
@@ -70,20 +56,8 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
返回:
- GenTableModel | None: 业务表信息对象。
"""
gen_table = (
(
await self.db.execute(
select(GenTableModel)
.options(selectinload(GenTableModel.columns))
.where(GenTableModel.table_name == table_name)
)
)
.scalars()
.first()
)
return await self.get(table_name=table_name, preload=preload)
return gen_table
async def get_gen_table_all(self, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[GenTableModel]:
"""
获取所有业务表信息。
@@ -94,14 +68,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
返回:
- Sequence[GenTableModel]: 所有业务表信息列表。
"""
gen_table_all = (
await self.db.execute(
select(GenTableModel)
.options(selectinload(GenTableModel.columns))
)
).scalars().all()
return gen_table_all
return await self.list(preload=preload)
async def get_gen_table_list(self, search: Optional[GenTableQueryParam] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[GenTableModel]:
"""
@@ -114,20 +81,13 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
返回:
- Sequence[GenTableModel]: 业务表列表信息。
"""
# 获取所有数据
result = await self.db.execute(
select(GenTableModel)
.options(selectinload(GenTableModel.columns))
.where(
GenTableModel.table_name.like(f"%{search.table_name}%") if search and search.table_name else GenTableModel.id.isnot(None),
GenTableModel.table_comment.like(f"%{search.table_comment}%") if search and search.table_comment else GenTableModel.id.isnot(None),
)
.order_by(GenTableModel.created_at.desc())
.distinct()
)
gen_table_all = result.scalars().all()
return gen_table_all
# 使用基础CRUD的list与like检索
search_dict: Dict = {}
if search and search.table_name:
search_dict["table_name"] = ("like", search.table_name)
if search and search.table_comment:
search_dict["table_comment"] = ("like", search.table_comment)
return await self.list(search=search_dict, order_by=[{"created_at": "desc"}], preload=preload)
async def add_gen_table(self, add_model: GenTableSchema) -> GenTableModel:
"""
@@ -139,13 +99,8 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
返回:
- GenTableModel: 新增的业务表信息对象。
"""
gen_table = GenTableModel(
**add_model.model_dump(exclude_unset=True, exclude={"sub", "tree", "crud"})
)
self.db.add(gen_table)
await self.db.flush()
return gen_table
return await self.create(add_model.model_dump(exclude_unset=True, exclude={"sub", "tree", "crud"}))
async def edit_gen_table(self, table_id: int, edit_model: GenTableSchema) -> GenTableSchema:
"""
修改业务表信息。
@@ -157,15 +112,8 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
返回:
- GenTableSchema: 修改后的业务表信息模型。
"""
edit_dict_data = edit_model.model_dump(exclude_unset=True)
await self.db.execute(
update(GenTableModel)
.where(GenTableModel.id == table_id)
.values(**edit_dict_data)
)
await self.db.flush()
await self.db.commit()
return edit_model
obj = await self.update(id=table_id, data=edit_model)
return GenTableSchema.model_validate(obj)
async def delete_gen_table(self, ids: List[int]) -> None:
"""
@@ -174,11 +122,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
参数:
- ids (List[int]): 业务表ID列表。
"""
await self.db.execute(
delete(GenTableModel)
.where(GenTableModel.id.in_(ids))
)
await self.db.flush()
await self.delete(ids=ids)
async def get_db_table_list(self, search: Optional[GenTableQueryParam] = None) -> list[Dict]:
"""
@@ -193,19 +137,25 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
# 使用更健壮的方式检测数据库方言
if settings.DATABASE_TYPE == "postgresql":
# 修复:PostgreSQL不提供table_comment,使用pg_catalog获取注释
query_sql = (
select(
text("table_catalog as database_name"),
text("table_name as table_name"),
text("table_type as table_type"),
text("table_comment as table_comment"),
text("t.table_catalog as database_name"),
text("t.table_name as table_name"),
text("t.table_type as table_type"),
text("pd.description as table_comment"),
)
.select_from(text("information_schema.tables"))
.select_from(text(
"information_schema.tables t \n"
"LEFT JOIN pg_catalog.pg_class c ON c.relname = t.table_name \n"
"LEFT JOIN pg_catalog.pg_namespace n ON n.nspname = t.table_schema AND c.relnamespace = n.oid \n"
"LEFT JOIN pg_catalog.pg_description pd ON pd.objoid = c.oid AND pd.objsubid = 0"
))
.where(
and_(
text("table_catalog = (select current_database())"),
text("is_insertable_into = 'YES'"),
text("table_schema = 'public'"),
text("t.table_catalog = (select current_database())"),
text("t.is_insertable_into = 'YES'"),
text("t.table_schema = 'public'"),
)
)
)
@@ -291,19 +241,25 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
"""
# 使用更健壮的方式检测数据库方言
if settings.DATABASE_TYPE == "postgresql":
# 修复:PostgreSQL不提供table_comment,使用pg_catalog获取注释
query_sql = (
select(
text("table_catalog as database_name"),
text("table_name as table_name"),
text("table_type as table_type"),
text("table_comment as table_comment"),
text("t.table_catalog as database_name"),
text("t.table_name as table_name"),
text("t.table_type as table_type"),
text("pd.description as table_comment"),
)
.select_from(text("information_schema.tables"))
.select_from(text(
"information_schema.tables t \n"
"LEFT JOIN pg_catalog.pg_class c ON c.relname = t.table_name \n"
"LEFT JOIN pg_catalog.pg_namespace n ON n.nspname = t.table_schema AND c.relnamespace = n.oid \n"
"LEFT JOIN pg_catalog.pg_description pd ON pd.objoid = c.oid AND pd.objsubid = 0"
))
.where(
and_(
text("table_catalog = (select current_database())"),
text("is_insertable_into = 'YES'"),
text("table_schema = 'public'"),
text("t.table_catalog = (select current_database())"),
text("t.is_insertable_into = 'YES'"),
text("t.table_schema = 'public'"),
)
)
)
@@ -348,12 +304,10 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
)
gen_db_table_list = (await self.db.execute(query_sql)).fetchall()
else:
# MySQL和PostgreSQL使用:table_names占位符
# MySQL和PostgreSQL使用IN拼接(注意已在上方限定schema范围)
query_sql = query_sql.where(
text(f"table_name IN ('{table_names_str}')")
)
# 使用params方法正确绑定参数
query_sql = query_sql.params(table_names=tuple(table_names))
gen_db_table_list = (await self.db.execute(query_sql)).fetchall()
else:
gen_db_table_list = (await self.db.execute(query_sql)).fetchall()
@@ -459,21 +413,29 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
# 兼容SQLite和MySQL/PostgreSQL
if settings.DATABASE_TYPE == "postgresql":
# 修复:PostgreSQL的主键/自增/注释需要关联系统表
query_sql = """
SELECT
column_name,
(CASE WHEN (is_nullable = 'no' AND column_key != 'PRI') THEN '1' ELSE '0' END) AS is_required,
(CASE WHEN column_key = 'PRI' THEN '1' ELSE '0' END) AS is_pk,
ordinal_position AS sort,
column_comment,
(CASE WHEN extra = 'auto_increment' THEN '1' ELSE '0' END) AS is_increment,
column_type
FROM
information_schema.columns
WHERE
table_catalog = (select current_database())
AND table_schema = 'public'
AND table_name = :table_name
c.column_name,
(CASE WHEN (c.is_nullable = 'NO' AND (tc.constraint_type IS DISTINCT FROM 'PRIMARY KEY')) THEN '1' ELSE '0' END) AS is_required,
(CASE WHEN (tc.constraint_type = 'PRIMARY KEY') THEN '1' ELSE '0' END) AS is_pk,
c.ordinal_position AS sort,
COALESCE(pgd.description, '') AS column_comment,
(CASE WHEN c.column_default LIKE 'nextval%' THEN '1' ELSE '0' END) AS is_increment,
c.udt_name AS column_type
FROM information_schema.columns c
LEFT JOIN information_schema.key_column_usage kcu
ON c.table_name = kcu.table_name AND c.column_name = kcu.column_name AND kcu.table_schema = c.table_schema
LEFT JOIN information_schema.table_constraints tc
ON tc.constraint_name = kcu.constraint_name AND tc.table_name = c.table_name AND tc.table_schema = c.table_schema
LEFT JOIN pg_catalog.pg_statio_all_tables st
ON st.relname = c.table_name
LEFT JOIN pg_catalog.pg_description pgd
ON pgd.objoid = st.relid AND pgd.objsubid = c.ordinal_position
WHERE c.table_catalog = current_database()
AND c.table_schema = 'public'
AND c.table_name = :table_name
ORDER BY c.ordinal_position
"""
elif settings.DATABASE_TYPE == "mysql":
query_sql = """
@@ -507,9 +469,7 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
"""
query = text(query_sql).bindparams(table_name=table_name)
rows = (
await self.db.execute(query)
).fetchall()
rows = (await self.db.execute(query)).fetchall()
result = [
GenTableColumnOutSchema(
column_name=row[0],
@@ -5,7 +5,10 @@ from fastapi import Query
class GenTableQueryParam:
"""代码生成业务表查询参数"""
"""代码生成业务表查询参数
- 支持按`table_name`、`table_comment`进行模糊检索(由CRUD层实现like)。
- 空值将被忽略,不参与过滤。
"""
def __init__(
self,
@@ -18,11 +21,13 @@ class GenTableQueryParam:
class GenTableColumnQueryParam:
"""代码生成业务表字段查询参数"""
"""代码生成业务表字段查询参数
- `column_name`按like规则模糊查询(透传到CRUD层)。
"""
def __init__(
self,
column_name: Optional[str] = Query(None, description="列名称"),
) -> None:
# 模糊查询字段
# 模糊查询字段:约定("like", 值)格式,便于CRUD解析
self.column_name = ("like", column_name)
@@ -8,6 +8,10 @@ from app.core.base_schema import BaseSchema
class GenTableOptionSchema(BaseModel):
"""代码生成表的附加选项(存入`options`字段的JSON)。
- parent_menu_id:菜单归属;树模板依赖。
- tree_*:树形结构必需的编码/父编码/名称字段。
"""
model_config = ConfigDict(from_attributes=True)
@@ -18,6 +22,9 @@ class GenTableOptionSchema(BaseModel):
class GenDBTableSchema(BaseModel):
"""数据库中的表信息(跨方言统一结构)。
- 供“导入表结构”与“同步结构”环节使用。
"""
model_config = ConfigDict(from_attributes=True)
@@ -28,8 +35,9 @@ class GenDBTableSchema(BaseModel):
class GenTableBaseSchema(BaseModel):
"""
代码生成业务表创建模型
"""代码生成业务表基础模型(创建/更新共享字段)。
- 说明:`params`为前端结构体,后端持久化为`options`的JSON。
- 模板:`tpl_category` 区分 CRUD/Tree/Sub`tpl_web_type` 区分 element-plus 等。
"""
model_config = ConfigDict(from_attributes=True)
@@ -55,8 +63,9 @@ class GenTableBaseSchema(BaseModel):
class GenTableSchema(GenTableBaseSchema):
"""
代码生成业务表更新模型
"""代码生成业务表更新模型(扩展聚合字段)。
- 聚合:`columns`字段包含字段列表;`pk_column`主键字段;子表结构`sub_table`。
- 便捷:`sub/tree/crud`基于`tpl_category`自动推导布尔标记。
"""
pk_column: Optional['GenTableColumnOutSchema'] = Field(default=None, description='主键信息')
@@ -80,11 +89,16 @@ class GenTableSchema(GenTableBaseSchema):
class GenTableOutSchema(GenTableSchema, BaseSchema):
"""业务表输出模型(面向控制器/前端)。
- 清洗:统一处理None值,保证`columns`为列表;文本字段为空字符串。
- 兼容:既支持传入ORM对象,也支持字典输入。
"""
model_config = ConfigDict(from_attributes=True)
# 添加数据验证和转换的root_validator
@model_validator(mode='before')
def handle_null_values(cls, values):
"""将关键字段的None转换为安全默认值,避免前端渲染异常。"""
# 处理None值,转换为空字符串或适当的默认值
# 检查values是否为对象而非字典
if hasattr(values, '__dict__'):
@@ -108,8 +122,10 @@ class GenTableOutSchema(GenTableSchema, BaseSchema):
class GenTableColumnSchema(BaseModel):
"""
代码生成业务表字段创建模型
"""代码生成业务表字段创建模型(原始字段+生成配置)。
- 原始:`column_name/column_type/column_comment` 等。
- 生成:`python_type/html_type/query_type/dict_type` 等由工具初始化。
- 标记:所有 is_* 字段默认使用字符串'1'表示启用,便于前端和模板处理。
"""
model_config = ConfigDict(from_attributes=True)
@@ -135,6 +151,10 @@ class GenTableColumnSchema(BaseModel):
class GenTableColumnOutSchema(GenTableColumnSchema, BaseSchema):
"""业务表字段输出模型(布尔派生+便捷字段)。
- 布尔:将字符串 is_* 转为布尔 `pk/increment/...`,供前端/模板快捷使用。
- 便捷:`cap_python_field` 存放大驼峰字段名(模板场景常用)。
"""
model_config = ConfigDict(from_attributes=True)
cap_python_field: Optional[str] = Field(default=None, description='字段大写形式')
@@ -151,8 +171,8 @@ class GenTableColumnOutSchema(GenTableColumnSchema, BaseSchema):
class GenTableColumnDeleteSchema(BaseModel):
"""
删除代码生成业务表字段模型
"""删除代码生成业务表字段模型(批量)。
- 说明:仅包含待删除的字段ID列表。
"""
model_config = ConfigDict(from_attributes=True)
@@ -9,34 +9,24 @@ from sqlglot.expressions import Add, Alter, Create, Delete, Drop, Expression, In
from sqlglot import parse as sqlglot_parse
from app.config.setting import settings
from app.core.logger import logger
from app.core.exceptions import CustomException
from app.common.constant import GenConstant
from app.common.response import SuccessResponse
from app.api.v1.module_system.auth.schema import AuthSchema
from app.utils.gen_util import GenUtils
from app.utils.jinja2_template_util import Jinja2TemplateInitializerUtil, Jinja2TemplateUtil
from .schema import GenTableOptionSchema, GenTableSchema, GenTableOutSchema, GenTableOutSchema, GenTableColumnSchema, GenTableColumnOutSchema, GenTableColumnDeleteSchema
from .schema import GenTableOptionSchema, GenTableSchema, GenTableOutSchema, GenTableColumnSchema, GenTableColumnOutSchema, GenTableColumnDeleteSchema
from .param import GenTableQueryParam
from .crud import GenTableColumnCRUD, GenTableCRUD
# 定义默认的GenConfig值
GEN_PATH = "generated_code" # 默认生成路径
class GenTableService:
"""代码生成业务表服务层"""
@classmethod
async def get_gen_table_detail_service(cls, auth: AuthSchema, table_id: int) -> Dict:
"""获取业务表详细信息。
参数:
- auth (AuthSchema): 认证信息。
- table_id (int): 业务表ID。
返回:
- Dict: 包含业务表详细信息、字段列表和所有业务表的字典。
"""获取业务表详细信息(含字段与其他表列表)
- 备注:优先解析`options`为`GenTableOptionSchema`,设置`parent_menu_id`等选项;保证`columns`与`tables`结构完整。
"""
gen_table = await cls.get_gen_table_by_id_service(auth, table_id)
gen_tables = await cls.get_gen_table_all_service(auth)
@@ -64,30 +54,16 @@ class GenTableService:
@classmethod
async def get_gen_db_table_list_service(cls, auth: AuthSchema, search: GenTableQueryParam, order_by: Optional[List[Dict[str, str]]] = None) -> list[Any]:
"""获取数据库列表信息
参数:
- auth (AuthSchema): 认证信息。
- search (GenTableQueryParam): 查询参数模型。
- order_by (Optional[List[Dict[str, str]]]): 排序参数列表,默认值为None。
返回:
- list[Any]: 包含数据库列表信息的任意类型列表。
"""获取数据库列表(跨方言)
- 备注:返回已转换为字典的结构,适用于前端直接展示;排序参数保留扩展位但当前未使用。
"""
# 确保db是AsyncSession类型
gen_db_table_list_result = await GenTableCRUD(auth=auth).get_db_table_list(search)
return gen_db_table_list_result
@classmethod
async def get_gen_db_table_list_by_name_service(cls, auth: AuthSchema, table_names: List[str]) -> List[GenTableOutSchema]:
"""根据表名称组获取数据库表信息。
参数:
- auth (AuthSchema): 认证信息。
- table_names (List[str]): 业务表名称列表。
返回:
- List[GenTableOutSchema]: 包含业务表列表信息的模型列表。
"""根据表名称组获取数据库表信息。
- 校验:如有不存在的表名,抛出明确异常;返回统一的`GenTableOutSchema`列表。
"""
gen_db_table_list_result = await GenTableCRUD(auth=auth).get_db_table_list_by_names(table_names)
@@ -106,17 +82,8 @@ class GenTableService:
@classmethod
async def import_gen_table_service(cls, auth: AuthSchema, gen_table_list: List[GenTableOutSchema]) -> Literal[True] | None:
"""导入表结构
参数:
- auth (AuthSchema): 认证对象。
- gen_table_list (List[GenTableOutSchema]): 要导入的业务表列表。
返回:
- Literal[True] | None: 导入成功返回True,否则返回None。
异常:
- CustomException: 当没有可导入的表结构、表已存在或导入过程中发生错误时抛出。
"""导入表结构到生成器(持久化并初始化列)。
- 备注:避免重复导入;为每列调用`GenUtils.init_column_field`填充默认属性,保留语义一致性。
"""
# 检查是否有表需要导入
if not gen_table_list:
@@ -171,17 +138,8 @@ class GenTableService:
@classmethod
async def create_table_service(cls, auth: AuthSchema, sql: str) -> Literal[True] | None:
"""创建表结构。
参数:
- auth (AuthSchema): 认证信息。
- sql (str): 包含建表SQL语句的字符串。
返回:
- Literal[True] | None: 创建成功返回True,否则返回None。
异常:
- CustomException: 当SQL语句不是合法的建表语句、创建表失败或导入表结构失败时抛出。
"""创建表结构并导入至代码生成模块
- 校验:使用`sqlglot`确保仅包含`CREATE TABLE`语句;失败抛出明确异常。
"""
try:
sql_statements = sqlglot_parse(sql, dialect=settings.DATABASE_TYPE)
@@ -243,24 +201,15 @@ class GenTableService:
@classmethod
async def update_gen_table_service(cls, auth: AuthSchema, data: GenTableSchema, table_id: int) -> Dict[str, Any]:
"""编辑业务表信息。
参数:
- auth (AuthSchema): 认证信息。
- data (GenTableSchema): 包含业务表更新信息的模型。
- table_id (int): 业务表ID。
返回:
- Dict[str, Any]: 更新后的业务表信息字典。
异常:
- CustomException: 当业务表不存在、更新失败或处理字段参数时抛出。
"""编辑业务表信息(含选项与字段)
- 备注:将`params`序列化写入`options`以持久化;仅更新存在`id`的列,避免误创建。
"""
edit_gen_table = data.model_dump(exclude_unset=True, by_alias=True)
gen_table_info = await cls.get_gen_table_by_id_service(auth, table_id)
if gen_table_info.id:
try:
# 处理params为None的情况
edit_gen_table = data.model_dump(exclude_unset=True, by_alias=True)
params = edit_gen_table.get('params')
if params:
edit_gen_table['options'] = json.dumps(params)
@@ -275,89 +224,39 @@ class GenTableService:
await GenTableColumnCRUD(auth).update_gen_table_column_crud(gen_table_column.id, gen_table_column)
return result.model_dump()
except Exception as e:
raise CustomException(msg=f'更新失败: {str(e)}')
raise CustomException(msg=str(e))
else:
raise CustomException(msg='业务表不存在')
@classmethod
async def delete_gen_table_service(cls, auth: AuthSchema, ids: List[int]) -> None:
"""删除业务表信息
参数:
- auth (AuthSchema): 认证信息。
- ids (List[int]): 业务表ID列表。
返回:
- None
异常:
- CustomException: 当删除失败时抛出。
"""
"""删除业务表信息(先删字段,再删表)。"""
try:
# 先删除相关的字段信息
await GenTableColumnCRUD(auth=auth).delete_gen_table_column_by_table_id_dao(ids)
# 再删除表信息
await GenTableCRUD(auth=auth).delete_gen_table(ids)
except Exception as e:
raise CustomException(msg=f'删除失败: {str(e)}')
raise CustomException(msg=str(e))
@classmethod
async def get_gen_table_by_id_service(cls, auth: AuthSchema, table_id: int) -> GenTableOutSchema:
"""获取需要生成代码的业务表详细信息。
参数:
- auth (AuthSchema): 认证信息。
- table_id (int): 业务表ID。
返回:
- GenTableOutSchema: 包含业务表详细信息的模型。
异常:
- CustomException: 当业务表不存在时抛出。
- 备注:去除SQLAlchemy内部状态;将`None`值转为适配前端的默认值;解析`options`补充选项。
"""
gen_table = await GenTableCRUD(auth=auth).get_gen_table_by_id(table_id)
if gen_table:
# 使用更直接的转换方式
result_dict = gen_table.__dict__.copy()
result_dict.pop('_sa_instance_state', None)
# 确保columns正确加载
if hasattr(gen_table, 'columns') and gen_table.columns:
columns_list = []
for column in gen_table.columns:
column_dict = column.__dict__.copy()
column_dict.pop('_sa_instance_state', None)
# 处理None值,转换为空字符串或适当的默认值
for key, value in column_dict.items():
if value is None:
column_dict[key] = ''
columns_list.append(column_dict)
result_dict['columns'] = columns_list
else:
result_dict['columns'] = []
# 处理其他None值,特殊处理creator_id和creator字段
for key, value in result_dict.items():
if value is None:
# 对于creator_id和creator字段,保持为None而不是转换为空字符串
if key not in ['creator_id', 'creator']:
result_dict[key] = ''
# 手动创建GenTableOutSchema对象
result = GenTableOutSchema(**result_dict)
# 设置额外选项
result = await cls.set_table_from_options(result)
return result
else:
if not gen_table:
raise CustomException(msg='业务表不存在')
result = GenTableOutSchema.model_validate(gen_table)
# 设置额外选项
result = await cls.set_table_from_options(result)
return result
@classmethod
async def get_gen_table_all_service(cls, auth: AuthSchema) -> List[GenTableOutSchema]:
"""获取所有业务表信息
参数:
- auth (AuthSchema): 认证信息。
返回:
- List[GenTableOutSchema]: 包含所有业务表详细信息的模型列表。
"""
"""获取所有业务表信息(列表)。"""
gen_table_all = await GenTableCRUD(auth=auth).get_gen_table_all()
gen_table_all_dict = [GenTableOutSchema.model_validate(gen_table).model_dump() for gen_table in gen_table_all]
result = [GenTableOutSchema(**gen_table) for gen_table in gen_table_all_dict]
@@ -366,14 +265,8 @@ class GenTableService:
@classmethod
async def preview_code_service(cls, auth: AuthSchema, table_id: int) -> Dict[Any, Any]:
"""
预览代码。
参数:
- auth (AuthSchema): 认证对象。
- table_id (int): 业务表ID。
返回:
- Dict[Any, Any]: 模版文件名到渲染内容的映射。
预览代码(根据模板渲染内存结果)
- 备注:构建Jinja2上下文;根据模板类型与前端类型选择模板清单;返回文件名到内容映射。
"""
gen_table = GenTableOutSchema.model_validate(
await GenTableCRUD(auth).get_gen_table_by_id(table_id)
@@ -384,7 +277,7 @@ class GenTableService:
context = Jinja2TemplateUtil.prepare_context(gen_table)
# 处理tpl_category和tpl_web_type为None的情况
tpl_category = gen_table.tpl_category or ''
tpl_web_type = gen_table.tpl_web_type or ''
tpl_web_type = gen_table.tpl_web_type or 'element-plus'
template_list = Jinja2TemplateUtil.get_template_list(tpl_category, tpl_web_type)
preview_code_result = {}
for template in template_list:
@@ -393,62 +286,37 @@ class GenTableService:
return preview_code_result
@classmethod
async def generate_code_service(cls, auth: AuthSchema, table_name: str) -> SuccessResponse:
"""生成代码至指定路径。
参数:
- auth (AuthSchema): 认证对象。
- table_name (str): 业务表名称。
返回:
- SuccessResponse: 成功响应模型。
异常:
- CustomException: 当渲染模板失败时抛出。
async def generate_code_service(cls, auth: AuthSchema, table_name: str) -> bool:
"""生成代码至指定路径(安全写入+可跳过覆盖)
- 安全:限制写入在项目根目录内;越界路径自动回退到项目根目录。
- 覆盖:尊重`settings.allow_overwrite`,不允许时跳过写入。
"""
if not settings.allow_overwrite:
logger.error('【系统预设】不允许生成文件覆盖到本地')
raise CustomException(msg='【系统预设】不允许生成文件覆盖到本地')
env = Jinja2TemplateInitializerUtil.init_jinja2()
render_info = await cls.__get_gen_render_info(auth, table_name)
gen_table_schema = render_info[3]
skipped = 0
for template in render_info[0]:
try:
render_content = await env.get_template(template).render_async(**render_info[2])
gen_path = cls.__get_gen_path(gen_table_schema, template)
if gen_path:
# 只允许写入到项目根目录及其子目录
project_root = os.path.realpath(str(settings.BASE_DIR.parent))
target_path = os.path.realpath(gen_path)
if not target_path.startswith(project_root):
raise CustomException(msg='生成路径不允许,请选择项目目录内路径')
if not gen_path:
raise CustomException(msg='【代码生成】生成路径为空')
os.makedirs(os.path.dirname(gen_path), exist_ok=True)
os.makedirs(os.path.dirname(gen_path), exist_ok=True)
# 覆盖控制:存在且不允许覆盖则跳过
if os.path.exists(gen_path) and not settings.allow_overwrite:
skipped += 1
continue
with open(gen_path, 'w', encoding='utf-8') as f:
f.write(render_content)
with open(gen_path, 'w', encoding='utf-8') as f:
f.write(render_content)
except Exception as e:
raise CustomException(msg=f'渲染模板失败,表名:{gen_table_schema.table_name},详细错误信息:{str(e)}')
msg = '生成代码成功'
if skipped:
msg += f'(已跳过 {skipped} 个已存在文件)'
return SuccessResponse(msg=msg)
return True
@classmethod
async def batch_gen_code_service(cls, auth: AuthSchema, table_names: List[str]) -> bytes:
"""
批量生成代码并打包为ZIP。
参数:
- auth (AuthSchema): 认证对象。
- table_names (List[str]): 业务表名称组。
返回:
- bytes: 下载代码的ZIP二进制数据。
- 备注:内存生成并压缩,兼容多模板类型;供下载使用。
"""
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
@@ -465,17 +333,8 @@ class GenTableService:
@classmethod
async def sync_db_service(cls, auth: AuthSchema, table_name: str) -> None:
"""同步数据库表结构。
参数:
- auth (AuthSchema): 认证对象。
- table_name (str): 业务表名称。
返回:
- None
异常:
- CustomException: 当业务表不存在时抛出。
"""同步数据库表结构至生成器(保留用户配置)
- 备注:按数据库实际字段重建或更新生成器字段;保留字典/查询/展示等用户自定义属性;清理已删除字段。
"""
gen_table = await GenTableCRUD(auth).get_gen_table_by_name(table_name)
if not gen_table:
@@ -537,18 +396,7 @@ class GenTableService:
@classmethod
async def set_sub_table(cls, auth: AuthSchema, gen_table: GenTableOutSchema) -> None:
"""设置主子表信息
参数:
- auth (AuthSchema): 认证对象。
- gen_table (GenTableOutSchema): 业务表详细信息模型。
返回:
- None
异常:
- CustomException: 当子表不存在时抛出。
"""
"""设置主子表信息(如存在子表则补充其结构)。"""
if gen_table.sub_table_name:
gen_table_dao = GenTableCRUD(auth=auth)
sub_table = await gen_table_dao.get_gen_table_by_name(gen_table.sub_table_name)
@@ -557,17 +405,12 @@ class GenTableService:
@classmethod
async def set_pk_column(cls, gen_table: GenTableOutSchema) -> None:
"""设置主键列信息。
参数:
- gen_table (GenTableOutSchema): 业务表详细信息模型。
返回:
- None
"""设置主键列信息(主表/子表)
- 备注:同时兼容`pk`布尔与`is_pk == '1'`字符串两种标识。
"""
if gen_table.columns:
for column in gen_table.columns:
if column.pk:
if getattr(column, 'pk', None) or getattr(column, 'is_pk', '') == '1':
gen_table.pk_column = column
break
if gen_table.pk_column is None and gen_table.columns:
@@ -575,7 +418,7 @@ class GenTableService:
if gen_table.tpl_category == GenConstant.TPL_SUB and gen_table.sub_table:
if gen_table.sub_table.columns:
for column in gen_table.sub_table.columns:
if column.pk:
if getattr(column, 'pk', None) or getattr(column, 'is_pk', '') == '1':
gen_table.sub_table.pk_column = column
break
if gen_table.sub_table.pk_column is None and gen_table.sub_table.columns:
@@ -583,22 +426,8 @@ class GenTableService:
@classmethod
async def set_table_from_options(cls, gen_table: GenTableOutSchema) -> GenTableOutSchema:
"""设置代码生成其他选项值
参数:
- gen_table (GenTableOutSchema): 业务表详细信息模型。
返回:
- GenTableOutSchema: 更新后的业务表详细信息模型。
"""
# 处理gen_table.options为None的情况
if gen_table.options:
try:
params_obj = json.loads(gen_table.options)
except json.JSONDecodeError:
params_obj = {}
else:
params_obj = {}
"""设置代码生成其他选项值(从options反序列化)。"""
params_obj = json.loads(gen_table.options) if gen_table.options else None
if params_obj:
gen_table.tree_code = params_obj.get(GenConstant.TREE_CODE)
@@ -611,17 +440,7 @@ class GenTableService:
@classmethod
async def validate_edit(cls, edit_gen_table: GenTableSchema) -> None:
"""编辑保存参数校验
参数:
- edit_gen_table (GenTableSchema): 编辑后的业务表模型。
返回:
- None
异常:
- CustomException: 当参数校验失败时抛出。
"""
"""编辑保存参数校验(树/子表约束)。"""
if edit_gen_table.tpl_category == GenConstant.TPL_TREE:
# 从options字段获取参数,而不是params
if not edit_gen_table.options:
@@ -660,57 +479,36 @@ class GenTableService:
异常:
- CustomException: 当业务表不存在或数据转换失败时抛出。
"""
gen_table = await GenTableCRUD(auth=auth).get_gen_table_by_name(table_name)
gen_table_model = await GenTableCRUD(auth=auth).get_gen_table_by_name(table_name)
# 检查表是否存在
if gen_table is None:
if gen_table_model is None:
raise CustomException(msg=f"业务表 {table_name} 不存在")
# 确保CamelCaseUtil.transform_result返回的是字典
transformed_result = gen_table
if transformed_result is None:
raise CustomException(msg=f"业务表 {table_name} 数据转换失败")
gen_table_schema = GenTableOutSchema.model_validate(transformed_result)
await cls.set_sub_table(auth, gen_table_schema)
await cls.set_pk_column(gen_table_schema)
context = Jinja2TemplateUtil.prepare_context(gen_table_schema)
gen_table = GenTableOutSchema.model_validate(gen_table_model)
await cls.set_sub_table(auth, gen_table)
await cls.set_pk_column(gen_table)
context = Jinja2TemplateUtil.prepare_context(gen_table)
template_list = Jinja2TemplateUtil.get_template_list(
gen_table_schema.tpl_category or "",
gen_table_schema.tpl_web_type or ""
gen_table.tpl_category or "",
gen_table.tpl_web_type or ""
)
# 修复:确保get_file_name返回的文件名不为空
output_files = []
for template in template_list:
file_name = Jinja2TemplateUtil.get_file_name(template, gen_table_schema)
if file_name: # 只有当文件名不为空时才添加到列表中
output_files.append(file_name)
output_files = [Jinja2TemplateUtil.get_file_name(template, gen_table) for template in template_list]
return [template_list, output_files, context, gen_table_schema]
return [template_list, output_files, context, gen_table]
@classmethod
def __get_gen_path(cls, gen_table: GenTableOutSchema, template: str) -> Optional[str]:
"""根据GenTableOutSchema对象和模板名称生成路径。
"""根据GenTableOutSchema对象和模板名称生成路径。"""
gen_path = (gen_table.gen_path or '').strip()
file_name = Jinja2TemplateUtil.get_file_name(template, gen_table)
# 默认写入到项目根目录(backend的上一级)
project_root = str(settings.BASE_DIR.parent)
if gen_path in ['', '/']:
return os.path.join(project_root, file_name)
else:
return os.path.join(gen_path, file_name)
参数:
- gen_table (GenTableOutSchema): 业务表详细信息模型。
- template (str): 模板名称。
返回:
- Optional[str]: 生成的文件路径,若失败则返回None。
"""
try:
gen_path = (gen_table.gen_path or '').strip()
file_name = Jinja2TemplateUtil.get_file_name(template, gen_table)
if not file_name:
return None
# 默认写入到项目根目录(backend的上一级)
project_root = str(settings.BASE_DIR.parent)
if gen_path in ['', '/']:
return os.path.join(project_root, file_name)
else:
return os.path.join(gen_path, file_name)
except Exception:
return None
class GenTableColumnService:
@@ -718,15 +516,7 @@ class GenTableColumnService:
@classmethod
async def get_gen_table_column_list_by_table_id_service(cls, auth: AuthSchema, table_id: int) -> List[GenTableColumnOutSchema]:
"""获取业务表字段列表信息
参数:
- auth (AuthSchema): 认证对象。
- table_id (int): 业务表ID。
返回:
- List[GenTableColumnOutSchema]: 业务表字段详细信息模型列表。
"""
"""获取业务表字段列表信息(输出模型)。"""
gen_table_column_list_result = await GenTableColumnCRUD(auth).list_gen_table_column_crud({"table_id": table_id})
return [
GenTableColumnOutSchema.model_validate(gen_table_column)
+1 -17
View File
@@ -1,17 +1 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from .cache.controller import CacheRouter
from .online.controller import OnlineRouter
from .server.controller import ServerRouter
from .resource.controller import ResourceRouter
MonitorRouter = APIRouter(prefix="/monitor")
# 包含所有子路由
MonitorRouter.include_router(CacheRouter)
MonitorRouter.include_router(OnlineRouter)
MonitorRouter.include_router(ServerRouter)
MonitorRouter.include_router(ResourceRouter)
# -*- coding: utf-8 -*-
+1 -26
View File
@@ -1,26 +1 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from .auth.controller import AuthRouter
from .user.controller import UserRouter
from .role.controller import RoleRouter
from .menu.controller import MenuRouter
from .dept.controller import DeptRouter
from .position.controller import PositionRouter
from .dict.controller import DictRouter
from .params.controller import ParamsRouter
from .notice.controller import NoticeRouter
from .log.controller import LogRouter
SystemRouter = APIRouter(prefix="/system")
SystemRouter.include_router(AuthRouter)
SystemRouter.include_router(UserRouter)
SystemRouter.include_router(RoleRouter)
SystemRouter.include_router(MenuRouter)
SystemRouter.include_router(DeptRouter)
SystemRouter.include_router(PositionRouter)
SystemRouter.include_router(DictRouter)
SystemRouter.include_router(ParamsRouter)
SystemRouter.include_router(NoticeRouter)
SystemRouter.include_router(LogRouter)
# -*- coding: utf-8 -*-