refactor(module_generator): 重构代码生成模块结构

重构代码生成模块,删除冗余文件并优化结构:
1. 删除旧的form、page、table等相关文件
2. 新增gencode模块基础结构
3. 移动jinja2工具类和模板引擎初始化器到utils目录
4. 优化数据库模型关系定义
5. 更新nginx配置支持WebSocket
6. 添加正则验证和加密工具类
7. 修复菜单和部门模型的循环引用问题
8. 优化初始化脚本和配置加载逻辑

调整初始化流程,使用统一会话管理:
1. 修改数据库初始化使用AsyncSessionLocal
2. 优化定时任务初始化逻辑
3. 统一配置和字典服务初始化方式
4. 修复模型关系定义导致的循环导入问题

其他改进:
1. 更新requirements.txt添加依赖
2. 调整IP白名单配置
3. 优化数据库连接日志输出
4. 修复模型继承关系问题
This commit is contained in:
zhangtao
2025-09-12 01:05:07 +08:00
parent 08a9f19923
commit 714896161f
63 changed files with 1928 additions and 2494 deletions
@@ -1,15 +0,0 @@
class GenConfig:
def __init__(self):
pass
# 作者
author: str = 'Richard'
# 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool
packageName: str = ''
# 自动去除表前缀,默认是false
autoRemovePre: bool = False
# 表前缀(生成类名不会包含表前缀,多个用逗号分隔)
tablePrefix: str = 'sys_'
# 是否允许生成文件覆盖到本地(自定义路径),默认不允许
allowOverwrite: bool = False
@@ -1,211 +0,0 @@
class GenConstants:
"""代码生成通用常量"""
# 单表(增删改查)
TPL_CRUD = "crud"
# 树表(增删改查)
TPL_TREE = "tree"
# 主子表(增删改查)
TPL_SUB = "sub"
# 树编码字段
TREE_CODE = "treeCode"
# 树父编码字段
TREE_PARENT_CODE = "treeParentCode"
# 树名称字段
TREE_NAME = "treeName"
# 上级菜单ID字段
PARENT_MENU_ID = "parentMenuId"
# 上级菜单名称字段
PARENT_MENU_NAME = "parentMenuName"
# 数据库字符串类型
TYPE_STRING = ["char", "varchar", "nvarchar", "varchar2"]
# 数据库文本类型
TYPE_TEXT = ["tinytext", "text", "mediumtext", "longtext"]
# 数据库时间类型
TYPE_DATE_TIME = ["datetime", "time", "date", "timestamp" ]
# 数据库数字类型
TYPE_NUMBER = ["tinyint", "smallint", "mediumint", "int", "number", "integer", "bigint", "float", "float", "double", "decimal"]
# 页面不需要编辑字段
COLUMN_NAME_NOT_EDIT = ["id", "create_by", "dept_id", "create_time", "del_flag", "update_time"]
# 页面不需要显示的列表字段
COLUMN_NAME_NOT_LIST = ["id", "create_by", "dept_id", "create_time", "del_flag", "update_by"]
# 页面不需要查询字段
COLUMN_NAME_NOT_QUERY = ["id", "create_by", "dept_id", "create_time", "del_flag", "update_by", "update_time", "remark"]
DAO_COLUMN_NOT_EDIT = ["create_by", "dept_id", "create_time", "del_flag", "update_time"]
# Entity基类字段
BASE_ENTITY = ['id', 'create_time', 'update_time', "create_by", "dept_id", 'del_flag']
# Tree基类字段
TREE_ENTITY = ["parentName", "parentId", "orderNum", "ancestors"]
# 文本框
HTML_INPUT = "input"
# 文本域
HTML_TEXTAREA = "textarea"
# 下拉框
HTML_SELECT = "select"
# 单选框
HTML_RADIO = "radio"
# 复选框
HTML_CHECKBOX = "checkbox"
# 日期控件
HTML_DATETIME = "datetime"
# 图片上传控件
HTML_IMAGE_UPLOAD = "imageUpload"
# 文件上传控件
HTML_FILE_UPLOAD = "fileUpload"
# 富文本控件
HTML_EDITOR = "editor"
# 模糊查询
QUERY_LIKE = "LIKE"
# 相等查询
QUERY_EQ = "EQ"
# 需要
REQUIRE = "1"
# MySQL -> SQLAlchemy 类型映射
MYSQL_TO_SQLALCHEMY = {
# Numeric Types
"TINYINT": "SmallInteger",
"SMALLINT": "SmallInteger",
"MEDIUMINT": "Integer",
"INT": "Integer",
"INTEGER": "Integer",
"BIGINT": "BigInteger",
"FLOAT": "Float",
"DOUBLE": "Float",
"DECIMAL": "Numeric",
"NUMERIC": "Numeric",
# String Types
"CHAR": "String",
"VARCHAR": "String",
"TEXT": "Text",
"TINYTEXT": "Text",
"MEDIUMTEXT": "Text",
"LONGTEXT": "Text",
"BLOB": "LargeBinary",
"TINYBLOB": "LargeBinary",
"MEDIUMBLOB": "LargeBinary",
"LONGBLOB": "LargeBinary",
# Date and Time Types
"DATE": "Date",
"DATETIME": "DateTime",
"TIMESTAMP": "DateTime",
"TIME": "Time",
"YEAR": "Integer", # MySQL YEAR type is commonly represented as Integer in SQLAlchemy
# Binary Types
"BINARY": "Binary",
"VARBINARY": "Binary",
# Enum and Set Types
"ENUM": "Enum",
"SET": "Enum", # Set can be represented using Enum type in SQLAlchemy
# JSON Types
"JSON": "JSON", # SQLAlchemy supports JSON type from 1.3.0 version
# Spatial Types (less common in typical usage)
"GEOMETRY": "String", # Can be represented as String or Binary
"POINT": "String", # Represented as String in SQLAlchemy
"LINESTRING": "String", # Represented as String in SQLAlchemy
"POLYGON": "String", # Represented as String in SQLAlchemy
# Other Types
"BIT": "Boolean",
"BOOL": "Boolean",
"UUID": "String", # UUIDs in SQLAlchemy can be represented as String
"BINARY": "Binary", # MySQL BINARY type corresponds to SQLAlchemy's Binary
}
MYSQL_TO_PYTHON = {
# 字符串类型
"VARCHAR": "str",
"CHAR": "str",
"TEXT": "str",
"TINYTEXT": "str",
"MEDIUMTEXT": "str",
"LONGTEXT": "str",
# 数值类型
"INT": "int",
"TINYINT": "int",
"SMALLINT": "int",
"MEDIUMINT": "int",
"BIGINT": "int",
"FLOAT": "float",
"DOUBLE": "float",
"DECIMAL": "float",
"NUMERIC": "float",
"BIT": "bool", # 位字段,0 或 1
# 日期和时间类型
"DATETIME": "datetime",
"TIMESTAMP": "datetime",
"DATE": "datetime.date",
"TIME": "datetime.time",
"YEAR": "int", # 存储年份
"TINYINT UNSIGNED": "int", # 无符号小整数类型
# 布尔类型
"BOOLEAN": "bool",
"BOOL": "bool", # 布尔类型,通常与 BOOLEAN 相同
# JSON 数据类型
"JSON": "dict", # JSON 数据存储为字典
# 二进制类型
"BLOB": "bytes",
"TINYBLOB": "bytes",
"MEDIUMBLOB": "bytes",
"LONGBLOB": "bytes",
# 枚举和集合类型
"ENUM": "str", # 枚举类型作为字符串
"SET": "list", # 集合类型作为列表
# 时间单位类型
"DATE": "datetime.date", # 仅日期
"TIME": "datetime.time", # 仅时间
# 大文本类型
"LONGTEXT": "str",
"MEDIUMTEXT": "str",
"TINYTEXT": "str",
# UUID
"UUID": "str", # UUID 一般作为字符串
# 用于二进制数据
"BINARY": "bytes", # 固定长度的二进制数据
"VARBINARY": "bytes", # 可变长度的二进制数据
# 其他数据类型
"GEOMETRY": "bytes", # 空间数据类型,通常存储为字节流
"POINT": "bytes", # 点数据类型
"LINESTRING": "bytes", # 线数据类型
"POLYGON": "bytes", # 多边形数据类型
"MULTIPOINT": "bytes", # 多点数据类型
"MULTILINESTRING": "bytes", # 多线数据类型
"MULTIPOLYGON": "bytes", # 多多边形数据类型
"GEOMETRYCOLLECTION": "bytes", # 几何集合类型
}
@@ -1,145 +0,0 @@
from typing import List
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.requests import Request
from config.get_db import get_db
from module_admin.aspect.data_scope import GetDataScope
from module_admin.aspect.interface_auth import CheckUserInterfaceAuth
from module_admin.service.login_service import LoginService
from module_gen.entity.vo.gen_table_vo import GenTableModel, GenTablePageModel, GenTableIdsModel
from module_gen.service.gen_table_service import GenTableService
from utils.response_util import ResponseUtil
gen1Controller = APIRouter(prefix="/tool/gen", tags=["代码生成"], dependencies=[Depends(LoginService.get_current_user)])
"""代码生成操作处理"""
@gen1Controller.get('/list', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:list'))])
async def gen_list(request: Request,
gen_table: GenTablePageModel = Depends(GenTablePageModel.as_query),
query_db: AsyncSession = Depends(get_db),
data_scope_sql: str = Depends(GetDataScope('SysDept'))):
"""查询代码生成列表"""
table_list = await GenTableService.select_gen_table_list(gen_table, query_db, data_scope_sql)
return ResponseUtil.success(model_content=table_list)
@gen1Controller.get('/db/list', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:list'))])
async def gen_db_list(request: Request,
gen_table: GenTablePageModel = Depends(GenTablePageModel.as_query),
query_db: AsyncSession = Depends(get_db),
data_scope_sql: str = Depends(GetDataScope('SysDept'))):
"""查询数据库列表"""
db_list = await GenTableService.select_db_table_list(gen_table, query_db, data_scope_sql)
return ResponseUtil.success(model_content=db_list)
@gen1Controller.post('/importTable', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:import'))])
async def import_table(request: Request, tables: str = Query(None),
query_db: AsyncSession = Depends(get_db),
data_scope_sql: str = Depends(GetDataScope('SysDept'))):
"""导入表结构"""
tables_array = tables.split(',') if tables else []
operate_log = "导入表" + ",".join(tables_array)
await GenTableService.import_gen_table(tables_array, query_db)
return ResponseUtil.success(operate_log)
@gen1Controller.get('/getById/{tableId}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:query'))])
async def get_info(request: Request, tableId: int,
query_db: AsyncSession = Depends(get_db),
data_scope_sql: str = Depends(GetDataScope('SysDept'))):
"""查询表详细信息"""
table_info = await GenTableService.select_gen_table_by_id(tableId, query_db, data_scope_sql)
all_gen_tables = await GenTableService.select_all_gen_table_list(query_db, data_scope_sql)
result = {
"info": table_info,
"rows": table_info.columns,
"tables": all_gen_tables
}
return ResponseUtil.success(data=result)
@gen1Controller.get('/tableInfo/{tableName}')
async def get_table_info(request: Request, tableName: str,
query_db: AsyncSession = Depends(get_db),
data_scope_sql: str = Depends(GetDataScope('SysDept'))):
"""获取表详细信息"""
table_info = GenTableService.select_gen_table_by_name(tableName, query_db)
return ResponseUtil.success(data=table_info)
@gen1Controller.put('', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:edit'))])
async def update_save(request: Request, gen_table: GenTableModel,
query_db: AsyncSession = Depends(get_db),
data_scope_sql: str = Depends(GetDataScope('SysDept'))):
"""修改保存代码生成业务"""
await GenTableService.validate_edit(gen_table)
await GenTableService.update_gen_table(query_db, gen_table)
return ResponseUtil.success()
@gen1Controller.delete('/{tableIds}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:remove'))])
async def delete(request: Request, tableIds: str,
query_db: AsyncSession = Depends(get_db),
data_scope_sql: str = Depends(GetDataScope('SysDept'))):
"""删除代码生成"""
tableIdsArray = tableIds.split(',') if tableIds else []
await GenTableService.delete_gen_table_by_ids(query_db, tableIdsArray)
return ResponseUtil.success()
@gen1Controller.get('/preview/{tableId}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:preview'))])
async def preview(request: Request, tableId: int,
query_db: AsyncSession = Depends(get_db),
data_scope_sql: str = Depends(GetDataScope('SysDept'))):
"""预览代码"""
result, table = await GenTableService.preview_code(query_db, tableId, data_scope_sql)
return ResponseUtil.success(data=result)
# @gen1Controller.get('/download/{tableName}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:code'))])
# async def download(request: Request, table_name: str,
# query_db: AsyncSession = Depends(get_db),
# data_scope_sql: str = Depends(GetDataScope('SysDept'))):
# """生成代码(下载方式)"""
# # 查询表信息
# table_info = await GenTableService.select_gen_table_by_name(table_name, query_db)
# #生成代码
# byte_data = await GenTableService.download_code(table_info)
#
# # 生成zip文件
# return create_zip_file(byte_data, f"{table_info.table_comment}.zip")
# @gen1Controller.get('/genCode/{tableName}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:code'))])
# async def generate_code(request: Request, table_name: str,
# query_db: AsyncSession = Depends(get_db),
# data_scope_sql: str = Depends(GetDataScope('SysDept'))):
# """生成代码(自定义路径)"""
# # 生成代码
# await GenTableService.generate_code(table_name, query_db)
# return ResponseUtil.success()
@gen1Controller.get('/synchDb/{tableName}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:edit'))])
async def sync_db(request: Request, tableName: str,
query_db: AsyncSession = Depends(get_db),
data_scope_sql: str = Depends(GetDataScope('SysDept'))):
"""同步数据库"""
await GenTableService.sync_db(query_db, tableName, data_scope_sql)
return ResponseUtil.success()
@gen1Controller.get('/batchGenCode', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:code'))])
async def batch_generate_code(request: Request, ids_model: GenTableIdsModel = Depends(GenTableIdsModel.as_query),
query_db: AsyncSession = Depends(get_db),
data_scope_sql: str = Depends(GetDataScope('SysDept'))):
"""批量生成代码"""
# 查询表信息
table_id_array = ids_model.tb_ids.split(',') if ids_model.tb_ids else []
# 生成zip包
byte_data = await GenTableService.batch_generate_code(query_db, data_scope_sql, table_id_array)
return ResponseUtil.streaming(data=byte_data)
@gen1Controller.post('/createTable', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:import'))])
async def import_table(request: Request, sql: str = Query(None),
query_db: AsyncSession = Depends(get_db),
data_scope_sql: str = Depends(GetDataScope('SysDept'))):
"""创建表结构"""
success = await GenTableService.create_table(query_db, sql)
if success:
return ResponseUtil.success()
else:
return ResponseUtil.failure(msg="创建失败,请检查语法是否符合mysql标准,并检查后端日志")
@@ -1,192 +0,0 @@
# -*- coding:utf-8 -*-
from datetime import datetime, time
from typing import List
from sqlalchemy import and_, delete, desc, func, or_, select, update, text, case, asc
from sqlalchemy.ext.asyncio import AsyncSession
from module_gen.entity.do.gen_table_column_do import GenTableColumn
from module_gen.entity.vo.gen_table_column_vo import GenTableColumnPageModel, GenTableColumnModel
from utils.page_util import PageUtil, PageResponseModel
class GenTableColumnDao:
@classmethod
async def get_by_id(cls, db: AsyncSession, gen_table_column_id: int) -> GenTableColumn:
"""根据主键获取单条记录"""
gen_table_column = (((await db.execute(
select(GenTableColumn)
.where(GenTableColumn.column_id == gen_table_column_id)))
.scalars())
.first())
return gen_table_column
@classmethod
async def get_list_by_table_id(cls, db: AsyncSession, table_id: int) -> list[GenTableColumn]:
"""根据主键获取单条记录"""
gen_table_columns = (((await db.execute(
select(GenTableColumn)
.where(GenTableColumn.table_id == table_id)))
.scalars())
.all())
return list(gen_table_columns)
"""
查询
"""
@classmethod
async def get_gen_table_column_list(cls, db: AsyncSession,
query_object: GenTableColumnPageModel,
data_scope_sql: str = None,
is_page: bool = False) -> PageResponseModel|list:
query = (
select(GenTableColumn)
.where(
GenTableColumn.column_id == query_object.column_id if query_object.column_id else True,
GenTableColumn.table_id == query_object.table_id if query_object.table_id else True,
GenTableColumn.column_name.like(f"%{query_object.column_name}%") if query_object.column_name else True,
GenTableColumn.column_comment.like(f"%{query_object.column_comment}%") if query_object.column_comment else True,
GenTableColumn.column_type.like(f"%{query_object.column_type}%") if query_object.column_type else True,
GenTableColumn.python_type.like(f"%{query_object.python_type}%") if query_object.python_type else True,
GenTableColumn.python_field.like(f"%{query_object.python_field}%") if query_object.python_field else True,
GenTableColumn.is_pk.like(f"%{query_object.is_pk}%") if query_object.is_pk else True,
GenTableColumn.is_increment.like(f"%{query_object.is_increment}%") if query_object.is_increment else True,
GenTableColumn.is_required.like(f"%{query_object.is_required}%") if query_object.is_required else True,
GenTableColumn.is_insert.like(f"%{query_object.is_insert}%") if query_object.is_insert else True,
GenTableColumn.is_edit.like(f"%{query_object.is_edit}%") if query_object.is_edit else True,
GenTableColumn.is_list.like(f"%{query_object.is_list}%") if query_object.is_list else True,
GenTableColumn.is_query.like(f"%{query_object.is_query}%") if query_object.is_query else True,
GenTableColumn.query_type.like(f"%{query_object.query_type}%") if query_object.query_type else True,
GenTableColumn.html_type.like(f"%{query_object.html_type}%") if query_object.html_type else True,
GenTableColumn.dict_type.like(f"%{query_object.dict_type}%") if query_object.dict_type else True,
GenTableColumn.sort == query_object.sort if query_object.sort else True,
GenTableColumn.create_by.like(f"%{query_object.create_by}%") if query_object.create_by else True,
GenTableColumn.create_time == query_object.create_time if query_object.create_time else True,
GenTableColumn.update_by.like(f"%{query_object.update_by}%") if query_object.update_by else True,
GenTableColumn.update_time == query_object.update_time if query_object.update_time else True,
eval(data_scope_sql) if data_scope_sql else True,
)
.order_by(asc(GenTableColumn.column_name))
.distinct()
)
gen_table_column_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page)
return gen_table_column_list
@classmethod
async def add_gen_table_column(cls, db: AsyncSession, add_model: GenTableColumnModel) -> GenTableColumn:
"""
增加
"""
gen_table_column = GenTableColumn(**add_model.model_dump(exclude_unset=True))
db.add(gen_table_column)
await db.flush()
return gen_table_column
@classmethod
async def edit_gen_table_column(cls, db: AsyncSession, edit_model: GenTableColumnModel, auto_commit: bool = True, exclude_unset=False):
"""
修改
"""
edit_dict_data = edit_model.model_dump(exclude_unset=exclude_unset)
await db.execute(update(GenTableColumn), [edit_dict_data])
await db.flush()
if auto_commit:
await db.commit()
return edit_model
@classmethod
async def del_gen_table_column(cls, db: AsyncSession, del_model: GenTableColumnModel, soft_del: bool = True, auto_commit: bool = True):
"""
删除
"""
if soft_del:
await db.execute(update(GenTableColumn).where(GenTableColumn.column_id == del_model.column_id).values(del_flag='2'))
else:
await db.execute(delete(GenTableColumn).where(GenTableColumn.column_id == del_model.column_id))
await db.flush()
if auto_commit:
await db.commit()
@classmethod
async def del_gen_table_column_by_table_ids(cls, db: AsyncSession, table_ids: List[int], soft_del: bool = True, auto_commit: bool = True):
"""
删除
"""
if soft_del:
await db.execute(update(GenTableColumn).where(GenTableColumn.table_id.in_(table_ids)).values(del_flag='2'))
else:
await db.execute(delete(GenTableColumn).where(GenTableColumn.table_id.in_(table_ids)))
await db.flush()
if auto_commit:
await db.commit()
@classmethod
async def select_db_table_columns_by_name(cls, session: AsyncSession, table_name: str)-> List[GenTableColumnModel]:
"""
查询指定表的列信息。
:param session: AsyncSession 数据库会话
:param table_name: 表名
:return: 查询结果映射到 GenTableColumnResult 对象的列表
"""
# 检查表名是否为空
if not table_name:
raise ValueError("Table name cannot be empty.")
# 基础查询
query = text("""
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_schema = (SELECT DATABASE())
AND table_name = :table_name
ORDER BY ordinal_position
""")
# 执行查询并传递参数
result = await session.execute(query, {"table_name": table_name})
rows = result.fetchall()
# 将结果映射到 GenTableColumnResult 对象
return [
GenTableColumnModel(
columnName=row[0],
isRequired=row[1],
isPk=row[2],
sort=row[3],
columnComment=row[4],
isIncrement=row[5],
columnType=row[6]
)
for row in rows
]
@@ -1,240 +0,0 @@
# -*- coding:utf-8 -*-
from datetime import datetime, time
from typing import List
from sqlalchemy import and_, delete, desc, func, or_, select, update, MetaData, text, not_, Table, Column, String, \
DateTime
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from module_gen.entity.do.gen_table_do import GenTable
from module_gen.entity.vo.gen_table_vo import GenTablePageModel, GenTableModel
from utils.common_util import CamelCaseUtil
from utils.page_util import PageUtil, PageResponseModel
class GenTableDao:
@classmethod
async def get_by_id(cls, db: AsyncSession, gen_table_id: int) -> GenTable:
"""根据主键获取单条记录"""
gen_table = (((await db.execute(
select(GenTable)
.where(GenTable.table_id == gen_table_id)))
.scalars())
.first())
return gen_table
@classmethod
async def get_by_table_name(cls, db: AsyncSession, table_name: str) -> GenTable:
"""根据名称获取单条记录"""
gen_table = (((await db.execute(
select(GenTable)
.options(selectinload(GenTable.columns))
.where(GenTable.table_name == table_name)))
.scalars())
.first())
return gen_table
"""
查询
"""
@classmethod
async def get_gen_table_list(cls, db: AsyncSession,
query_object: GenTablePageModel,
data_scope_sql: str,
is_page: bool = False) -> PageResponseModel:
query = (
select(GenTable)
.options(selectinload(GenTable.columns))
.where(
GenTable.table_id == query_object.table_id if query_object.table_id else True,
GenTable.table_name.like(f"%{query_object.table_name}%") if query_object.table_name else True,
GenTable.table_comment.like(f"%{query_object.table_comment}%") if query_object.table_comment else True,
GenTable.sub_table_name.like(f"%{query_object.sub_table_name}%") if query_object.sub_table_name else True,
GenTable.sub_table_fk_name.like(f"%{query_object.sub_table_fk_name}%") if query_object.sub_table_fk_name else True,
GenTable.class_name.like(f"%{query_object.class_name}%") if query_object.class_name else True,
GenTable.tpl_category.like(f"%{query_object.tpl_category}%") if query_object.tpl_category else True,
GenTable.tpl_web_type.like(f"%{query_object.tpl_web_type}%") if query_object.tpl_web_type else True,
GenTable.package_name.like(f"%{query_object.package_name}%") if query_object.package_name else True,
GenTable.module_name.like(f"%{query_object.module_name}%") if query_object.module_name else True,
GenTable.business_name.like(f"%{query_object.business_name}%") if query_object.business_name else True,
GenTable.function_name.like(f"%{query_object.function_name}%") if query_object.function_name else True,
GenTable.function_author.like(f"%{query_object.function_author}%") if query_object.function_author else True,
GenTable.gen_type.like(f"%{query_object.gen_type}%") if query_object.gen_type else True,
GenTable.gen_path.like(f"%{query_object.gen_path}%") if query_object.gen_path else True,
GenTable.options.like(f"%{query_object.options}%") if query_object.options else True,
GenTable.create_by.like(f"%{query_object.create_by}%") if query_object.create_by else True,
GenTable.create_time == query_object.create_time if query_object.create_time else True,
GenTable.update_by.like(f"%{query_object.update_by}%") if query_object.update_by else True,
GenTable.update_time == query_object.update_time if query_object.update_time else True,
GenTable.remark.like(f"%{query_object.remark}%") if query_object.remark else True,
eval(data_scope_sql),
)
.order_by(desc(GenTable.create_time))
.distinct()
)
gen_table_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page)
return gen_table_list
@classmethod
async def add_gen_table(cls, db: AsyncSession, add_model: GenTableModel) -> GenTable:
"""
增加
"""
gen_table = GenTable(**add_model.model_dump(exclude_unset=True, exclude={'sub', 'tree', 'crud'}))
db.add(gen_table)
await db.flush()
return gen_table
@classmethod
async def edit_gen_table(cls, db: AsyncSession, edit_model: GenTableModel, auto_commit: bool = True):
"""
修改
"""
edit_dict_data = edit_model.model_dump(exclude_unset=True)
await db.execute(update(GenTable), [edit_dict_data])
await db.flush()
if auto_commit:
await db.commit()
return edit_model
@classmethod
async def del_gen_table(cls, db: AsyncSession, del_model: GenTableModel, soft_del: bool = True, auto_commit: bool = True):
"""
删除
"""
if soft_del:
await db.execute(update(GenTable).where(GenTable.table_id == del_model.id).values(del_flag='2'))
else:
await db.execute(delete(GenTable).where(GenTable.table_id == del_model.id))
await db.flush()
if auto_commit:
await db.commit()
@classmethod
async def del_gen_table_by_ids(cls, db: AsyncSession, ids: List[int], soft_del: bool = True, auto_commit: bool = True):
"""
批量删除
"""
if soft_del:
await db.execute(update(GenTable).where(GenTable.table_id.in_(ids)).values(del_flag='2'))
else:
await db.execute(delete(GenTable).where(GenTable.table_id.in_(ids)))
await db.flush()
if auto_commit:
await db.commit()
# 定义查询方法
@classmethod
async def select_db_table_list(cls, session: AsyncSession, gen_table: GenTablePageModel, is_page = False) -> PageResponseModel:
"""
查询数据库中的表信息,根据 GenTable 入参动态添加过滤条件。
"""
"""查询数据库表列表"""
query = (
select(
text("table_name"),
text("table_comment"),
text("create_time"),
text("update_time"),
)
.select_from(text("information_schema.tables"))
.where(
and_(
text("table_schema = (select database())"),
text("table_name NOT LIKE 'qrtz\\_%'"),
text("table_name NOT LIKE 'gen\\_%'"),
text("table_name NOT IN (select table_name from gen_table)"),
)
)
)
# 动态条件构造
if gen_table.table_name:
query = query.where(
text("lower(table_name) like lower(:table_name)")
)
if gen_table.table_comment:
query = query.where(
text("lower(table_comment) like lower(:table_comment)")
)
# 排序
query = query.order_by(text("create_time DESC"))
# 参数绑定
params = {}
if gen_table.table_name:
params["table_name"] = f"%{gen_table.table_name}%"
if gen_table.table_comment:
params["table_comment"] = f"%{gen_table.table_comment}%"
rows = await PageUtil.paginate(session, query.params(**params), gen_table.page_num, gen_table.page_size, is_page)
return rows
@classmethod
async def select_db_table_list_by_names(cls, session: AsyncSession, table_names: List[str]):
"""根据表名称查询数据库表信息"""
table_str = ",".join([f"'{item}'" for item in table_names])
if not table_names:
return []
query = select(
text("table_name"),
text("table_comment"),
text("create_time"),
text("update_time"),
).select_from(text('information_schema.tables')).where(
and_(
text("table_name NOT LIKE 'qrtz\\_%'"),
text("table_name NOT LIKE 'gen\\_%'"),
text("table_name NOT IN (select table_name from gen_table)"),
text(f"table_name IN ({ table_str })"),
text("table_schema = (select database())")
)
)
result = await session.execute(query)
rows = result.fetchall()
# return CamelCaseUtil.transform_result(rows)
return [
GenTableModel(tableName=row[0], tableComment=row[1], createTime=row[2], updateTime=row[3]) for row in rows
]
@classmethod
async def create_table(cls, session: AsyncSession, sql: str) -> bool:
""" 创建表 """
try:
await session.execute(text(sql))
# 提交事务
await session.commit()
await session.flush()
return True
except Exception as e:
# 如果发生异常,回滚事务
await session.rollback()
print(f"创建表时发生错误: {e}")
return False
@@ -1,61 +0,0 @@
# -*- coding:utf-8 -*-
import datetime
from sqlalchemy import Column, ForeignKey, BigInteger, DateTime, Integer, String, text
from sqlalchemy.orm import relationship
from config.database import BaseMixin, Base
class GenTableColumn(Base):
__tablename__ = "gen_table_column"
__table_args__ = ({'comment': '代码生成器-表字段'})
column_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='编号')
table_id = Column(Integer, ForeignKey('gen_table.table_id'), nullable=True, comment='归属表编号')
column_name = Column(String(length=200), comment='列名称')
column_comment = Column(String(length=500), comment='列描述')
column_type = Column(String(length=100), comment='列类型')
python_type = Column(String(length=500), comment='python类型')
python_field = Column(String(length=200), comment='python字段名')
is_pk = Column(String(length=1), comment='是否主键(1是)')
is_increment = Column(String(length=1), comment='是否自增(1是)')
is_required = Column(String(length=1), comment='是否必填(1是)')
is_insert = Column(String(length=1), comment='是否为插入字段(1是)')
is_edit = Column(String(length=1), comment='是否编辑字段(1是)')
is_list = Column(String(length=1), comment='是否列表字段(1是)')
is_query = Column(String(length=1), comment='是否查询字段(1是)')
query_type = Column(String(length=200), default='EQ', comment='查询方式(等于、不等于、大于、小于、范围)')
html_type = Column(String(length=200), comment='显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)')
dict_type = Column(String(length=200), default='', comment='字典类型')
sort = Column(Integer, comment='排序')
create_by = Column(String(length=64), default='', comment='创建者')
update_by = Column(String(length=64), default='', comment='更新者')
del_flag = Column(String(1), nullable=False, default='0', server_default=text("'0'"), comment='删除标志(0代表存在 2代表删除)')
create_time = Column(DateTime, nullable=False, default=datetime.datetime.now, comment='创建时间')
update_time = Column(DateTime, nullable=False, default=datetime.datetime.now, onupdate=datetime.datetime.now, index=True, comment='更新时间')
tables = relationship('GenTable', back_populates='columns')
@@ -1,57 +0,0 @@
# -*- coding:utf-8 -*-
import datetime
from sqlalchemy import Column, ForeignKey, BigInteger, DateTime, String, text
from sqlalchemy.orm import relationship
from config.database import Base
class GenTable(Base):
__tablename__ = "gen_table"
table_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='编号')
table_name = Column(String(length=200), default='', comment='表名称')
table_comment = Column(String(length=500), default='', comment='表描述')
sub_table_name = Column(String(length=64), comment='关联子表的表名')
sub_table_fk_name = Column(String(length=64), comment='子表关联的外键名')
class_name = Column(String(length=100), default='', comment='实体类名称')
tpl_category = Column(String(length=200), default='crud', comment='使用的模板(crud单表操作 tree树表操作)')
tpl_web_type = Column(String(length=30), default='', comment='前端模板类型(element-ui模版 element-plus模版)')
package_name = Column(String(length=100), comment='生成包路径')
module_name = Column(String(length=30), comment='生成模块名')
business_name = Column(String(length=30), comment='生成业务名')
function_name = Column(String(length=50), comment='生成功能名')
function_author = Column(String(length=50), comment='生成功能作者')
gen_type = Column(String(length=1), default='0', comment='生成代码方式(0zip压缩包 1自定义路径)')
gen_path = Column(String(length=200), default='/', comment='生成路径(不填默认项目路径)')
options = Column(String(length=1000), comment='其它生成选项')
create_by = Column(String(length=64), default='', comment='创建者')
update_by = Column(String(length=64), default='', comment='更新者')
remark = Column(String(length=500), comment='备注')
del_flag = Column(String(1), nullable=False, default='0', server_default=text("'0'"), comment='删除标志(0代表存在 2代表删除)')
create_time = Column(DateTime, nullable=False, default=datetime.datetime.now, comment='创建时间')
update_time = Column(DateTime, nullable=False, default=datetime.datetime.now, onupdate=datetime.datetime.now, index=True, comment='更新时间')
columns = relationship('GenTableColumn', order_by='GenTableColumn.sort', back_populates='tables')
@@ -1,67 +0,0 @@
# -*- coding:utf-8 -*-
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
from typing import List, Literal, Optional, Union
from module_admin.annotation.pydantic_annotation import as_query
class GenTableColumnModel(BaseModel):
"""
表对应pydantic模型
"""
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
column_id: Optional[int] = Field(default=None, description='编号')
table_id: Optional[int] = Field(default=None, description='归属表编号')
column_name: Optional[str] = Field(default=None, description='列名称')
column_comment: Optional[str] = Field(default=None, description='列描述')
column_type: Optional[str] = Field(default=None, description='列类型')
python_type: Optional[str] = Field(default=None, description='python类型')
python_field: Optional[str] = Field(default=None, description='python字段名')
is_pk: Optional[str] = Field(default=None, description='是否主键(1是)')
is_increment: Optional[str] = Field(default=None, description='是否自增(1是)')
is_required: Optional[str] = Field(default=None, description='是否必填(1是)')
is_insert: Optional[str] = Field(default=None, description='是否为插入字段(1是)')
is_edit: Optional[str] = Field(default=None, description='是否编辑字段(1是)')
is_list: Optional[str] = Field(default=None, description='是否列表字段(1是)')
is_query: Optional[str] = Field(default=None, description='是否查询字段(1是)')
query_type: Optional[str] = Field(default=None, description='查询方式(等于、不等于、大于、小于、范围)')
html_type: Optional[str] = Field(default=None, description='显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)')
dict_type: Optional[str] = Field(default=None, description='字典类型')
sort: Optional[int] = Field(default=None, description='排序')
create_by: Optional[str] = Field(default=None, description='创建者')
create_time: Optional[datetime] = Field(default=None, description='创建时间')
update_by: Optional[str] = Field(default=None, description='更新者')
update_time: Optional[datetime] = Field(default=None, description='更新时间')
@as_query
class GenTableColumnPageModel(GenTableColumnModel):
"""
分页查询模型
"""
page_num: int = Field(default=1, description='当前页码')
page_size: int = Field(default=10, description='每页记录数')
@@ -1,17 +0,0 @@
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
class GenTableOptionModel(BaseModel):
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
parent_menu_id: Optional[int] = Field(default=None, description='所属父级分类')
tree_code: Optional[str] = Field(default=None, description='tree_code')
tree_name: Optional[str] = Field(default=None, description='tree_name')
tree_parent_code: Optional[str] = Field(default=None, description='tree_parent_code')
@@ -1,100 +0,0 @@
# -*- coding:utf-8 -*-
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field, model_validator
from pydantic.alias_generators import to_camel
from typing import List, Literal, Optional, Union, Dict, Any, Set
from module_admin.annotation.pydantic_annotation import as_query
from module_gen.constants.gen_constants import GenConstants
from module_gen.entity.vo.gen_table_column_vo import GenTableColumnModel
class GenTableBaseModel(BaseModel):
"""
表对应pydantic模型
"""
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
table_id: Optional[int] = Field(default=None, description='编号')
table_name: Optional[str] = Field(default=None, description='表名称')
table_comment: Optional[str] = Field(default=None, description='表描述')
sub_table_name: Optional[str] = Field(default=None, description='关联子表的表名')
sub_table_fk_name: Optional[str] = Field(default=None, description='子表关联的外键名')
class_name: Optional[str] = Field(default=None, description='实体类名称')
tpl_category: Optional[str] = Field(default=None, description='使用的模板(crud单表操作 tree树表操作)')
tpl_web_type: Optional[str] = Field(default=None, description='前端模板类型(element-ui模版 element-plus模版)')
package_name: Optional[str] = Field(default=None, description='生成包路径')
module_name: Optional[str] = Field(default=None, description='生成模块名')
business_name: Optional[str] = Field(default=None, description='生成业务名')
function_name: Optional[str] = Field(default=None, description='生成功能名')
function_author: Optional[str] = Field(default=None, description='生成功能作者')
gen_type: Optional[str] = Field(default=None, description='生成代码方式(0zip压缩包 1自定义路径)')
gen_path: Optional[str] = Field(default=None, description='生成路径(不填默认项目路径)')
options: Optional[str] = Field(default=None, description='其它生成选项')
create_by: Optional[str] = Field(default=None, description='创建者')
create_time: Optional[datetime] = Field(default=None, description='创建时间')
update_by: Optional[str] = Field(default=None, description='更新者')
update_time: Optional[datetime] = Field(default=None, description='更新时间')
remark: Optional[str] = Field(default=None, description='备注')
params: Optional[Any] = Field(default=None, description='前端传递过来的表附加信息,转换成json字符串后放到options')
class GenTableModel(GenTableBaseModel):
"""
代码生成业务表模型
"""
pk_column: Optional['GenTableColumnModel'] = Field(default=None, description='主键信息')
sub_table: Optional['GenTableModel'] = Field(default=None, description='子表信息')
columns: Optional[List['GenTableColumnModel']] = Field(default=None, description='表列信息')
tree_code: Optional[str] = Field(default=None, description='树编码字段')
tree_parent_code: Optional[str] = Field(default=None, description='树父编码字段')
tree_name: Optional[str] = Field(default=None, description='树名称字段')
parent_menu_id: Optional[int] = Field(default=None, description='解析出options里面的parentMenuId给前端用')
parent_menu_name: Optional[str] = Field(default=None, description='上级菜单名称字段')
sub: Optional[bool] = Field(default=None, description='是否为子表')
tree: Optional[bool] = Field(default=None, description='是否为树表')
crud: Optional[bool] = Field(default=None, description='是否为单表')
@model_validator(mode='after')
def check_some_is(self) -> 'GenTableModel':
self.sub = True if self.tpl_category and self.tpl_category == GenConstants.TPL_SUB else False
self.tree = True if self.tpl_category and self.tpl_category == GenConstants.TPL_TREE else False
self.crud = True if self.tpl_category and self.tpl_category == GenConstants.TPL_CRUD else False
return self
@as_query
class GenTablePageModel(GenTableBaseModel):
"""
分页查询模型
"""
page_num: int = Field(default=1, description='当前页码')
page_size: int = Field(default=10, description='每页记录数')
@as_query
class GenTableIdsModel(BaseModel):
"""表的table_ids, 逗号分隔"""
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
tb_ids: Optional[str] = Field(description='当前页码')
@@ -1,22 +0,0 @@
# -*- coding:utf-8 -*-
from sqlalchemy import Boolean, Column, ForeignKey, String, Integer, Text, DateTime
from app.core.base_model import BaseMixin
class FormModel(BaseMixin):
__tablename__ = "gen_form"
content = Column(Text, nullable=False, comment='表单代码')
form_conf = Column(Text, nullable=False, comment='表单配置')
form_data = Column(Text, nullable=False, comment='表单内容')
generate_conf = Column(Text, nullable=False, comment='生成配置')
name = Column(String(255), nullable=False, comment='表单名称')
drawing_list = Column(Text, nullable=False, comment='字段列表')
@@ -1,58 +0,0 @@
# -*- coding:utf-8 -*-
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
from typing import List, Literal, Optional, Union
from module_admin.annotation.pydantic_annotation import as_query
class SysTableModel(BaseModel):
"""
表对应pydantic模型
"""
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
align: Optional[str] = Field(default=None, description='对其方式')
create_time: Optional[datetime] = Field(default=None, description='创建时间')
del_flag: Optional[str] = Field(default=None, description='删除标志')
field_name: Optional[str] = Field(default=None, description='字段名')
fixed: Optional[str] = Field(default=None, description='固定表头')
id: Optional[int] = Field(default=None, description='ID')
label: Optional[str] = Field(default=None, description='字段标签')
label_tip: Optional[str] = Field(default=None, description='字段标签解释')
prop: Optional[str] = Field(default=None, description='驼峰属性')
show: Optional[str] = Field(default=None, description='可见')
sortable: Optional[str] = Field(default=None, description='可排序')
table_name: Optional[str] = Field(default=None, description='表名')
tooltip: Optional[str] = Field(default=None, description='超出隐藏')
update_by: Optional[int] = Field(default=None, description='更新者')
update_by_name: Optional[str] = Field(default=None, description='更新者')
update_time: Optional[datetime] = Field(default=None, description='更新时间')
width: Optional[int] = Field(default=None, description='宽度')
sequence: Optional[int] = Field(default=None, description='字段顺序')
@as_query
class SysTablePageModel(SysTableModel):
"""
分页查询模型
"""
page_num: int = Field(default=1, description='当前页码')
page_size: int = Field(default=10, description='每页记录数')
@as_query
class DbTablePageModel(BaseModel):
"""
分页查询模型
"""
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
table_name: Optional[str] = Field(default=None, description='表名')
table_comment: Optional[str] = Field(default=None, description='表描述')
page_num: int = Field(default=1, description='当前页码')
page_size: int = Field(default=10, description='每页记录数')
class SysTableColumnIdsModel(BaseModel):
"""
列排序
"""
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
ids: Optional[List[int]] = Field()
@@ -1,192 +0,0 @@
# -*- coding: utf-8 -*-
import io
from typing import Any, List, Dict
from fastapi import UploadFile
import pandas as pd
from app.api.v1.schemas.system.auth_schema import AuthSchema
from app.api.v1.schemas.demo.example_schema import ExampleCreateSchema, ExampleUpdateSchema, ExampleOutSchema
from app.core.base_schema import BatchSetAvailable
from app.api.v1.params.demo.example_param import ExampleQueryParams
from app.api.v1.cruds.demo.example_crud import ExampleCRUD
from app.core.exceptions import CustomException
from app.utils.excel_util import ExcelUtil
from app.core.logger import logger
class ExampleService:
"""
示例管理模块服务层
"""
@classmethod
async def get_example_detail_service(cls, auth: AuthSchema, id: int) -> Dict:
"""详情"""
obj = await ExampleCRUD(auth).get_by_id_crud(id=id)
return ExampleOutSchema.model_validate(obj).model_dump()
@classmethod
async def get_demo_list_service(cls, auth: AuthSchema, search: ExampleQueryParams = None, order_by: List[Dict[str, str]] = None) -> List[Dict]:
"""列表查询"""
if order_by:
order_by = eval(order_by)
obj_list = await ExampleCRUD(auth).get_list_crud(search=search.__dict__, order_by=order_by)
return [ExampleOutSchema.model_validate(obj).model_dump() for obj in obj_list]
@classmethod
async def create_example_service(cls, auth: AuthSchema, data: ExampleCreateSchema) -> Dict:
"""创建"""
obj = await ExampleCRUD(auth).get(name=data.name)
if obj:
raise CustomException(msg='创建失败,名称已存在')
obj = await ExampleCRUD(auth).create_crud(data=data)
return ExampleOutSchema.model_validate(obj).model_dump()
@classmethod
async def update_example_service(cls, auth: AuthSchema, data: ExampleUpdateSchema) -> Dict:
"""更新"""
obj = await ExampleCRUD(auth).get_by_id_crud(id=data.id)
if not obj:
raise CustomException(msg='更新失败,该数据不存在')
exist_obj = await ExampleCRUD(auth).get(name=data.name)
if exist_obj and exist_obj.id != data.id:
raise CustomException(msg='更新失败,名称重复')
obj = await ExampleCRUD(auth).update_crud(id=data.id, data=data)
return ExampleOutSchema.model_validate(obj).model_dump()
@classmethod
async def delete_example_service(cls, auth: AuthSchema, ids: list[int]) -> None:
"""删除"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
for id in ids:
obj = await ExampleCRUD(auth).get_by_id_crud(id=id)
if not obj:
raise CustomException(msg='删除失败,该数据不存在')
await ExampleCRUD(auth).delete_crud(ids=ids)
@classmethod
async def set_example_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
"""批量设置状态"""
await ExampleCRUD(auth).set_available_crud(ids=data.ids, status=data.status)
@classmethod
async def batch_export_service(cls, obj_list: List[Dict[str, Any]]) -> bytes:
"""批量导出"""
mapping_dict = {
'id': '编号',
'name': '名称',
'status': '状态',
'description': '备注',
'created_at': '创建时间',
'updated_at': '更新时间',
'creator': '创建者',
}
# 复制数据并转换状态
data = obj_list.copy()
for item in data:
# 处理状态
item['status'] = '正常' if item.get('status') else '停用'
# 处理公告类型
item['creator'] = item.get('creator', {}).get('name', '未知') if isinstance(item.get('creator'), dict) else '未知'
return ExcelUtil.export_list2excel(list_data=obj_list, mapping_dict=mapping_dict)
@classmethod
async def batch_import_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
"""批量导入"""
header_dict = {
'名称': 'name',
'状态': 'status',
'描述': 'description'
}
try:
# 读取Excel文件
contents = await file.read()
df = pd.read_excel(io.BytesIO(contents))
await file.close()
if df.empty:
raise CustomException(msg="导入文件为空")
# 检查表头是否完整
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
if missing_headers:
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
# 重命名列名
df.rename(columns=header_dict, inplace=True)
# 验证必填字段
required_fields = ['name', 'status']
for field in required_fields:
if df[field].isnull().any():
missing_rows = df[df[field].isnull()].index.tolist()
raise CustomException(msg=f"{[k for k,v in header_dict.items() if v == field][0]}不能为空,第{[i+1 for i in missing_rows]}")
error_msgs = []
success_count = 0
# 处理每一行数据
for index, row in df.iterrows():
try:
# 数据转换前的类型检查
try:
name = str(row['name'])
except ValueError:
error_msgs.append(f"{index+1}行: 名称必须是字符串")
continue
try:
status = True if row['status'] == '正常' else False
except ValueError:
error_msgs.append(f"{index+1}行: 状态必须是'正常''停用'")
continue
# 构建用户数据
data = {
"name": name,
"status": status,
"description": str(row['description']).strip() if not pd.isna(row['description']) else None,
}
# 处理用户导入
exists_user = await ExampleCRUD(auth).get(name=data["name"])
if exists_user:
if update_support:
await ExampleCRUD(auth).update(id=exists_user.id, data=data)
success_count += 1
else:
error_msgs.append(f"{index+1}行: 用户 {data['username']} 已存在")
else:
await ExampleCRUD(auth).create(data=data)
success_count += 1
except Exception as e:
error_msgs.append(f"{index+1}行: {str(e)}")
continue
# 返回详细的导入结果
result = f"成功导入 {success_count} 条数据"
if error_msgs:
result += "\n错误信息:\n" + "\n".join(error_msgs)
return result
except Exception as e:
logger.error(f"批量导入用户失败: {str(e)}")
raise CustomException(msg=f"导入失败: {str(e)}")
@classmethod
async def import_template_download_service(cls) -> bytes:
"""下载导入模板"""
header_list = ['名称', '状态', '描述']
selector_header_list = ['状态']
option_list = [{'状态': ['正常', '停用']}]
return ExcelUtil.get_excel_template(
header_list=header_list,
selector_header_list=selector_header_list,
option_list=option_list
)
@@ -1,2 +0,0 @@
# -*- coding: utf-8 -*-
@@ -1,18 +0,0 @@
# -*- coding:utf-8 -*-
from sqlalchemy import Boolean, Column, ForeignKey, String, Integer, Text, DateTime
from sqlalchemy.orm import relationship
from app.core.base_model import BaseMixin
class FormData(BaseMixin):
__tablename__ = "gen_form_data"
form_data = Column(Text, nullable=False, comment='表单数据')
form_id = Column(Integer, ForeignKey(SysForm.id), nullable=False, comment='表单ID')
form_name = Column(String(255), nullable=False, comment='表单名称')
@@ -1,31 +0,0 @@
# -*- coding:utf-8 -*-
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
from typing import List, Literal, Optional, Union
from module_admin.annotation.pydantic_annotation import as_query
class SysFormDataModel(BaseModel):
"""
表对应pydantic模型
"""
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
create_by: Optional[int] = Field(default=None, description='创建者')
create_time: Optional[datetime] = Field(default=None, description='创建时间')
del_flag: Optional[str] = Field(default=None, description='删除标志')
dept_id: Optional[int] = Field(default=None, description='部门id')
form_data: Optional[str] = Field(default=None, description='表单数据')
form_id: Optional[int] = Field(default=None, description='表单ID')
form_name: Optional[str] = Field(default=None, description='表单名称')
id: Optional[int] = Field(default=None, description='id')
update_time: Optional[datetime] = Field(default=None, description='更新时间')
@as_query
class SysFormDataPageModel(SysFormDataModel):
"""
分页查询模型
"""
page_num: int = Field(default=1, description='当前页码')
page_size: int = Field(default=10, description='每页记录数')
@@ -1,192 +0,0 @@
# -*- coding: utf-8 -*-
import io
from typing import Any, List, Dict
from fastapi import UploadFile
import pandas as pd
from app.api.v1.schemas.system.auth_schema import AuthSchema
from app.api.v1.schemas.demo.example_schema import ExampleCreateSchema, ExampleUpdateSchema, ExampleOutSchema
from app.core.base_schema import BatchSetAvailable
from app.api.v1.params.demo.example_param import ExampleQueryParams
from app.api.v1.cruds.demo.example_crud import ExampleCRUD
from app.core.exceptions import CustomException
from app.utils.excel_util import ExcelUtil
from app.core.logger import logger
class ExampleService:
"""
示例管理模块服务层
"""
@classmethod
async def get_example_detail_service(cls, auth: AuthSchema, id: int) -> Dict:
"""详情"""
obj = await ExampleCRUD(auth).get_by_id_crud(id=id)
return ExampleOutSchema.model_validate(obj).model_dump()
@classmethod
async def get_demo_list_service(cls, auth: AuthSchema, search: ExampleQueryParams = None, order_by: List[Dict[str, str]] = None) -> List[Dict]:
"""列表查询"""
if order_by:
order_by = eval(order_by)
obj_list = await ExampleCRUD(auth).get_list_crud(search=search.__dict__, order_by=order_by)
return [ExampleOutSchema.model_validate(obj).model_dump() for obj in obj_list]
@classmethod
async def create_example_service(cls, auth: AuthSchema, data: ExampleCreateSchema) -> Dict:
"""创建"""
obj = await ExampleCRUD(auth).get(name=data.name)
if obj:
raise CustomException(msg='创建失败,名称已存在')
obj = await ExampleCRUD(auth).create_crud(data=data)
return ExampleOutSchema.model_validate(obj).model_dump()
@classmethod
async def update_example_service(cls, auth: AuthSchema, data: ExampleUpdateSchema) -> Dict:
"""更新"""
obj = await ExampleCRUD(auth).get_by_id_crud(id=data.id)
if not obj:
raise CustomException(msg='更新失败,该数据不存在')
exist_obj = await ExampleCRUD(auth).get(name=data.name)
if exist_obj and exist_obj.id != data.id:
raise CustomException(msg='更新失败,名称重复')
obj = await ExampleCRUD(auth).update_crud(id=data.id, data=data)
return ExampleOutSchema.model_validate(obj).model_dump()
@classmethod
async def delete_example_service(cls, auth: AuthSchema, ids: list[int]) -> None:
"""删除"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
for id in ids:
obj = await ExampleCRUD(auth).get_by_id_crud(id=id)
if not obj:
raise CustomException(msg='删除失败,该数据不存在')
await ExampleCRUD(auth).delete_crud(ids=ids)
@classmethod
async def set_example_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
"""批量设置状态"""
await ExampleCRUD(auth).set_available_crud(ids=data.ids, status=data.status)
@classmethod
async def batch_export_service(cls, obj_list: List[Dict[str, Any]]) -> bytes:
"""批量导出"""
mapping_dict = {
'id': '编号',
'name': '名称',
'status': '状态',
'description': '备注',
'created_at': '创建时间',
'updated_at': '更新时间',
'creator': '创建者',
}
# 复制数据并转换状态
data = obj_list.copy()
for item in data:
# 处理状态
item['status'] = '正常' if item.get('status') else '停用'
# 处理公告类型
item['creator'] = item.get('creator', {}).get('name', '未知') if isinstance(item.get('creator'), dict) else '未知'
return ExcelUtil.export_list2excel(list_data=obj_list, mapping_dict=mapping_dict)
@classmethod
async def batch_import_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
"""批量导入"""
header_dict = {
'名称': 'name',
'状态': 'status',
'描述': 'description'
}
try:
# 读取Excel文件
contents = await file.read()
df = pd.read_excel(io.BytesIO(contents))
await file.close()
if df.empty:
raise CustomException(msg="导入文件为空")
# 检查表头是否完整
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
if missing_headers:
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
# 重命名列名
df.rename(columns=header_dict, inplace=True)
# 验证必填字段
required_fields = ['name', 'status']
for field in required_fields:
if df[field].isnull().any():
missing_rows = df[df[field].isnull()].index.tolist()
raise CustomException(msg=f"{[k for k,v in header_dict.items() if v == field][0]}不能为空,第{[i+1 for i in missing_rows]}")
error_msgs = []
success_count = 0
# 处理每一行数据
for index, row in df.iterrows():
try:
# 数据转换前的类型检查
try:
name = str(row['name'])
except ValueError:
error_msgs.append(f"{index+1}行: 名称必须是字符串")
continue
try:
status = True if row['status'] == '正常' else False
except ValueError:
error_msgs.append(f"{index+1}行: 状态必须是'正常''停用'")
continue
# 构建用户数据
data = {
"name": name,
"status": status,
"description": str(row['description']).strip() if not pd.isna(row['description']) else None,
}
# 处理用户导入
exists_user = await ExampleCRUD(auth).get(name=data["name"])
if exists_user:
if update_support:
await ExampleCRUD(auth).update(id=exists_user.id, data=data)
success_count += 1
else:
error_msgs.append(f"{index+1}行: 用户 {data['username']} 已存在")
else:
await ExampleCRUD(auth).create(data=data)
success_count += 1
except Exception as e:
error_msgs.append(f"{index+1}行: {str(e)}")
continue
# 返回详细的导入结果
result = f"成功导入 {success_count} 条数据"
if error_msgs:
result += "\n错误信息:\n" + "\n".join(error_msgs)
return result
except Exception as e:
logger.error(f"批量导入用户失败: {str(e)}")
raise CustomException(msg=f"导入失败: {str(e)}")
@classmethod
async def import_template_download_service(cls) -> bytes:
"""下载导入模板"""
header_list = ['名称', '状态', '描述']
selector_header_list = ['状态']
option_list = [{'状态': ['正常', '停用']}]
return ExcelUtil.get_excel_template(
header_list=header_list,
selector_header_list=selector_header_list,
option_list=option_list
)
@@ -0,0 +1,158 @@
from datetime import datetime
from fastapi import APIRouter, Depends, Query, Request
from pydantic_validation_decorator import ValidateFields
from sqlalchemy.ext.asyncio import AsyncSession
from config.enums import BusinessType
from config.env import GenConfig
from config.get_db import get_db
from module_admin.annotation.log_annotation import Log
from module_admin.aspect.interface_auth import CheckRoleInterfaceAuth, CheckUserInterfaceAuth
from module_admin.service.login_service import LoginService
from module_admin.entity.vo.user_vo import CurrentUserModel
from module_generator.entity.vo.gen_vo import DeleteGenTableModel, EditGenTableModel, GenTablePageQueryModel
from module_generator.service.gen_service import GenTableColumnService, GenTableService
from utils.common_util import bytes2file_response
from utils.log_util import logger
from utils.page_util import PageResponseModel
from utils.response_util import ResponseUtil
genController = APIRouter(prefix='/tool/gen', dependencies=[Depends(LoginService.get_current_user)])
@genController.get(
'/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:list'))]
)
async def get_gen_table_list(
request: Request,
gen_page_query: GenTablePageQueryModel = Depends(GenTablePageQueryModel.as_query),
query_db: AsyncSession = Depends(get_db),
):
# 获取分页数据
gen_page_query_result = await GenTableService.get_gen_table_list_services(query_db, gen_page_query, is_page=True)
logger.info('获取成功')
return ResponseUtil.success(model_content=gen_page_query_result)
@genController.get(
'/db/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:list'))]
)
async def get_gen_db_table_list(
request: Request,
gen_page_query: GenTablePageQueryModel = Depends(GenTablePageQueryModel.as_query),
query_db: AsyncSession = Depends(get_db),
):
# 获取分页数据
gen_page_query_result = await GenTableService.get_gen_db_table_list_services(query_db, gen_page_query, is_page=True)
logger.info('获取成功')
return ResponseUtil.success(model_content=gen_page_query_result)
@genController.post('/importTable', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:import'))])
@Log(title='代码生成', business_type=BusinessType.IMPORT)
async def import_gen_table(
request: Request,
tables: str = Query(),
query_db: AsyncSession = Depends(get_db),
current_user: CurrentUserModel = Depends(LoginService.get_current_user),
):
table_names = tables.split(',') if tables else []
add_gen_table_list = await GenTableService.get_gen_db_table_list_by_name_services(query_db, table_names)
add_gen_table_result = await GenTableService.import_gen_table_services(query_db, add_gen_table_list, current_user)
logger.info(add_gen_table_result.message)
return ResponseUtil.success(msg=add_gen_table_result.message)
@genController.put('', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:edit'))])
@ValidateFields(validate_model='edit_gen_table')
@Log(title='代码生成', business_type=BusinessType.UPDATE)
async def edit_gen_table(
request: Request,
edit_gen_table: EditGenTableModel,
query_db: AsyncSession = Depends(get_db),
current_user: CurrentUserModel = Depends(LoginService.get_current_user),
):
edit_gen_table.update_by = current_user.user.user_name
edit_gen_table.update_time = datetime.now()
await GenTableService.validate_edit(edit_gen_table)
edit_gen_result = await GenTableService.edit_gen_table_services(query_db, edit_gen_table)
logger.info(edit_gen_result.message)
return ResponseUtil.success(msg=edit_gen_result.message)
@genController.delete('/{table_ids}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:remove'))])
@Log(title='代码生成', business_type=BusinessType.DELETE)
async def delete_gen_table(request: Request, table_ids: str, query_db: AsyncSession = Depends(get_db)):
delete_gen_table = DeleteGenTableModel(tableIds=table_ids)
delete_gen_table_result = await GenTableService.delete_gen_table_services(query_db, delete_gen_table)
logger.info(delete_gen_table_result.message)
return ResponseUtil.success(msg=delete_gen_table_result.message)
@genController.post('/createTable', dependencies=[Depends(CheckRoleInterfaceAuth('admin'))])
@Log(title='创建表', business_type=BusinessType.OTHER)
async def create_table(
request: Request,
sql: str = Query(),
query_db: AsyncSession = Depends(get_db),
current_user: CurrentUserModel = Depends(LoginService.get_current_user),
):
create_table_result = await GenTableService.create_table_services(query_db, sql, current_user)
logger.info(create_table_result.message)
return ResponseUtil.success(msg=create_table_result.message)
@genController.get('/batchGenCode', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:code'))])
@Log(title='代码生成', business_type=BusinessType.GENCODE)
async def batch_gen_code(request: Request, tables: str = Query(), query_db: AsyncSession = Depends(get_db)):
table_names = tables.split(',') if tables else []
batch_gen_code_result = await GenTableService.batch_gen_code_services(query_db, table_names)
logger.info('生成代码成功')
return ResponseUtil.streaming(data=bytes2file_response(batch_gen_code_result))
@genController.get('/genCode/{table_name}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:code'))])
@Log(title='代码生成', business_type=BusinessType.GENCODE)
async def gen_code_local(request: Request, table_name: str, query_db: AsyncSession = Depends(get_db)):
if not GenConfig.allow_overwrite:
logger.error('【系统预设】不允许生成文件覆盖到本地')
return ResponseUtil.error('【系统预设】不允许生成文件覆盖到本地')
gen_code_local_result = await GenTableService.generate_code_services(query_db, table_name)
logger.info(gen_code_local_result.message)
return ResponseUtil.success(msg=gen_code_local_result.message)
@genController.get('/{table_id}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:query'))])
async def query_detail_gen_table(request: Request, table_id: int, query_db: AsyncSession = Depends(get_db)):
gen_table = await GenTableService.get_gen_table_by_id_services(query_db, table_id)
gen_tables = await GenTableService.get_gen_table_all_services(query_db)
gen_columns = await GenTableColumnService.get_gen_table_column_list_by_table_id_services(query_db, table_id)
gen_table_detail_result = dict(info=gen_table, rows=gen_columns, tables=gen_tables)
logger.info(f'获取table_id为{table_id}的信息成功')
return ResponseUtil.success(data=gen_table_detail_result)
@genController.get('/preview/{table_id}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:preview'))])
async def preview_code(request: Request, table_id: int, query_db: AsyncSession = Depends(get_db)):
preview_code_result = await GenTableService.preview_code_services(query_db, table_id)
logger.info('获取预览代码成功')
return ResponseUtil.success(data=preview_code_result)
@genController.get('/synchDb/{table_name}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:edit'))])
@Log(title='代码生成', business_type=BusinessType.UPDATE)
async def sync_db(request: Request, table_name: str, query_db: AsyncSession = Depends(get_db)):
sync_db_result = await GenTableService.sync_db_services(query_db, table_name)
logger.info(sync_db_result.message)
return ResponseUtil.success(data=sync_db_result.message)
@@ -0,0 +1,393 @@
from datetime import datetime, time
from sqlalchemy import delete, func, select, text, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from sqlglot.expressions import Expression
from typing import List
from config.env import DataBaseConfig
from module_generator.entity.do.gen_do import GenTable, GenTableColumn
from module_generator.entity.vo.gen_vo import (
GenTableBaseModel,
GenTableColumnBaseModel,
GenTableColumnModel,
GenTableModel,
GenTablePageQueryModel,
)
from utils.page_util import PageUtil
class GenTableDao:
"""
代码生成业务表模块数据库操作层
"""
@classmethod
async def get_gen_table_by_id(cls, db: AsyncSession, table_id: int):
"""
根据业务表id获取需要生成的业务表信息
:param db: orm对象
:param table_id: 业务表id
:return: 需要生成的业务表信息对象
"""
gen_table_info = (
(
await db.execute(
select(GenTable).options(selectinload(GenTable.columns)).where(GenTable.table_id == table_id)
)
)
.scalars()
.first()
)
return gen_table_info
@classmethod
async def get_gen_table_by_name(cls, db: AsyncSession, table_name: str):
"""
根据业务表名称获取需要生成的业务表信息
:param db: orm对象
:param table_name: 业务表名称
:return: 需要生成的业务表信息对象
"""
gen_table_info = (
(
await db.execute(
select(GenTable).options(selectinload(GenTable.columns)).where(GenTable.table_name == table_name)
)
)
.scalars()
.first()
)
return gen_table_info
@classmethod
async def get_gen_table_all(cls, db: AsyncSession):
"""
获取所有业务表信息
:param db: orm对象
:return: 所有业务表信息
"""
gen_table_all = (await db.execute(select(GenTable).options(selectinload(GenTable.columns)))).scalars().all()
return gen_table_all
@classmethod
async def create_table_by_sql_dao(cls, db: AsyncSession, sql_statements: List[Expression]):
"""
根据sql语句创建表结构
:param db: orm对象
:param sql_statements: sql语句的ast列表
:return:
"""
for sql_statement in sql_statements:
sql = sql_statement.sql(dialect=DataBaseConfig.sqlglot_parse_dialect)
await db.execute(text(sql))
@classmethod
async def get_gen_table_list(cls, db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False):
"""
根据查询参数获取代码生成业务表列表信息
:param db: orm对象
:param query_object: 查询参数对象
:param is_page: 是否开启分页
:return: 代码生成业务表列表信息对象
"""
query = (
select(GenTable)
.options(selectinload(GenTable.columns))
.where(
func.lower(GenTable.table_name).like(f'%{query_object.table_name.lower()}%')
if query_object.table_name
else True,
func.lower(GenTable.table_comment).like(f'%{query_object.table_comment.lower()}%')
if query_object.table_comment
else True,
GenTable.create_time.between(
datetime.combine(datetime.strptime(query_object.begin_time, '%Y-%m-%d'), time(00, 00, 00)),
datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)),
)
if query_object.begin_time and query_object.end_time
else True,
)
.distinct()
)
gen_table_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page)
return gen_table_list
@classmethod
async def get_gen_db_table_list(cls, db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False):
"""
根据查询参数获取数据库列表信息
:param db: orm对象
:param query_object: 查询参数对象
:param is_page: 是否开启分页
:return: 数据库列表信息对象
"""
if DataBaseConfig.db_type == 'postgresql':
query_sql = """
table_name as table_name,
table_comment as table_comment,
create_time as create_time,
update_time as update_time
from
list_table
where
table_name not like 'apscheduler_%'
and table_name not like 'gen_%'
and table_name not in (select table_name from gen_table)
"""
else:
query_sql = """
table_name as table_name,
table_comment as table_comment,
create_time as create_time,
update_time as update_time
from
information_schema.tables
where
table_schema = (select database())
and table_name not like 'apscheduler\_%'
and table_name not like 'gen\_%'
and table_name not in (select table_name from gen_table)
"""
if query_object.table_name:
query_sql += """and lower(table_name) like lower(concat('%', :table_name, '%'))"""
if query_object.table_comment:
query_sql += """and lower(table_comment) like lower(concat('%', :table_comment, '%'))"""
if query_object.begin_time:
if DataBaseConfig.db_type == 'postgresql':
query_sql += """and create_time::date >= to_date(:begin_time, 'yyyy-MM-dd')"""
else:
query_sql += """and date_format(create_time, '%Y%m%d') >= date_format(:begin_time, '%Y%m%d')"""
if query_object.end_time:
if DataBaseConfig.db_type == 'postgresql':
query_sql += """and create_time::date <= to_date(:end_time, 'yyyy-MM-dd')"""
else:
query_sql += """and date_format(create_time, '%Y%m%d') >= date_format(:end_time, '%Y%m%d')"""
query_sql += """order by create_time desc"""
query = select(
text(query_sql).bindparams(
**{
k: v
for k, v in query_object.model_dump(exclude_none=True, exclude={'page_num', 'page_size'}).items()
}
)
)
gen_db_table_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page)
return gen_db_table_list
@classmethod
async def get_gen_db_table_list_by_names(cls, db: AsyncSession, table_names: List[str]):
"""
根据业务表名称组获取数据库列表信息
:param db: orm对象
:param table_names: 业务表名称组
:return: 数据库列表信息对象
"""
if DataBaseConfig.db_type == 'postgresql':
query_sql = """
select
table_name as table_name,
table_comment as table_comment,
create_time as create_time,
update_time as update_time
from
list_table
where
table_name not like 'qrtz_%'
and table_name not like 'gen_%'
and table_name = any(:table_names)
"""
else:
query_sql = """
select
table_name as table_name,
table_comment as table_comment,
create_time as create_time,
update_time as update_time
from
information_schema.tables
where
table_name not like 'qrtz\_%'
and table_name not like 'gen\_%'
and table_schema = (select database())
and table_name in :table_names
"""
query = text(query_sql).bindparams(table_names=tuple(table_names))
gen_db_table_list = (await db.execute(query)).fetchall()
return gen_db_table_list
@classmethod
async def add_gen_table_dao(cls, db: AsyncSession, gen_table: GenTableModel):
"""
新增业务表数据库操作
:param db: orm对象
:param gen_table: 业务表对象
:return:
"""
db_gen_table = GenTable(**GenTableBaseModel(**gen_table.model_dump(by_alias=True)).model_dump())
db.add(db_gen_table)
await db.flush()
return db_gen_table
@classmethod
async def edit_gen_table_dao(cls, db: AsyncSession, gen_table: dict):
"""
编辑业务表数据库操作
:param db: orm对象
:param gen_table: 需要更新的业务表字典
:return:
"""
await db.execute(update(GenTable), [GenTableBaseModel(**gen_table).model_dump()])
@classmethod
async def delete_gen_table_dao(cls, db: AsyncSession, gen_table: GenTableModel):
"""
删除业务表数据库操作
:param db: orm对象
:param gen_table: 业务表对象
:return:
"""
await db.execute(delete(GenTable).where(GenTable.table_id.in_([gen_table.table_id])))
class GenTableColumnDao:
"""
代码生成业务表字段模块数据库操作层
"""
@classmethod
async def get_gen_table_column_list_by_table_id(cls, db: AsyncSession, table_id: int):
"""
根据业务表id获取需要生成的业务表字段列表信息
:param db: orm对象
:param table_id: 业务表id
:return: 需要生成的业务表字段列表信息对象
"""
gen_table_column_list = (
(
await db.execute(
select(GenTableColumn).where(GenTableColumn.table_id == table_id).order_by(GenTableColumn.sort)
)
)
.scalars()
.all()
)
return gen_table_column_list
@classmethod
async def get_gen_db_table_columns_by_name(cls, db: AsyncSession, table_name: str):
"""
根据业务表名称获取业务表字段列表信息
:param db: orm对象
:param table_name: 业务表名称
:return: 业务表字段列表信息对象
"""
if DataBaseConfig.db_type == 'postgresql':
query_sql = """
select
column_name, is_required, is_pk, sort, column_comment, is_increment, column_type
from
list_column
where
table_name = :table_name
"""
else:
query_sql = """
select
column_name as 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 as column_comment,
case
when extra = 'auto_increment' then '1'
else '0'
end as is_increment,
column_type as column_type
from
information_schema.columns
where
table_schema = (select database())
and table_name = :table_name
order by
ordinal_position
"""
query = text(query_sql).bindparams(table_name=table_name)
gen_db_table_columns = (await db.execute(query)).fetchall()
return gen_db_table_columns
@classmethod
async def add_gen_table_column_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel):
"""
新增业务表字段数据库操作
:param db: orm对象
:param gen_table_column: 岗位对象
:return:
"""
db_gen_table_column = GenTableColumn(
**GenTableColumnBaseModel(**gen_table_column.model_dump(by_alias=True)).model_dump()
)
db.add(db_gen_table_column)
await db.flush()
return db_gen_table_column
@classmethod
async def edit_gen_table_column_dao(cls, db: AsyncSession, gen_table_column: dict):
"""
编辑业务表字段数据库操作
:param db: orm对象
:param gen_table_column: 需要更新的业务表字段字典
:return:
"""
await db.execute(update(GenTableColumn), [GenTableColumnBaseModel(**gen_table_column).model_dump()])
@classmethod
async def delete_gen_table_column_by_table_id_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel):
"""
通过业务表id删除业务表字段数据库操作
:param db: orm对象
:param gen_table_column: 业务表字段对象
:return:
"""
await db.execute(delete(GenTableColumn).where(GenTableColumn.table_id.in_([gen_table_column.table_id])))
@classmethod
async def delete_gen_table_column_by_column_id_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel):
"""
通过业务字段id删除业务表字段数据库操作
:param db: orm对象
:param post: 业务表字段对象
:return:
"""
await db.execute(delete(GenTableColumn).where(GenTableColumn.column_id.in_([gen_table_column.column_id])))
@@ -0,0 +1,95 @@
from datetime import datetime
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from config.database import Base
from sqlalchemy import Boolean, Column, ForeignKey, String, Integer, Text, DateTime
from app.core.base_model import BaseMixin
from sqlalchemy import Boolean, Column, ForeignKey, String, Integer, Text, DateTime
from sqlalchemy.orm import relationship
from app.core.base_model import BaseMixin
from sqlalchemy import Boolean, Column, ForeignKey, String, Integer, Text, DateTime
from app.core.base_model import BaseMixin
class PageModel(BaseMixin):
__tablename__ = "gen_page"
page_name = Column(String(length=255), comment='页面名称')
keywords = Column(String(length=500), comment='页面关键词')
title = Column(String(length=500), comment='页面title标题')
class GenTable(Base):
"""
代码生成业务表
"""
__tablename__ = 'gen_table'
table_id = Column(Integer, primary_key=True, autoincrement=True, comment='编号')
table_name = Column(String(200), nullable=True, default='', comment='表名称')
table_comment = Column(String(500), nullable=True, default='', comment='表描述')
sub_table_name = Column(String(64), nullable=True, comment='关联子表的表名')
sub_table_fk_name = Column(String(64), nullable=True, comment='子表关联的外键名')
class_name = Column(String(100), nullable=True, default='', comment='实体类名称')
tpl_category = Column(String(200), nullable=True, default='crud', comment='使用的模板(crud单表操作 tree树表操作)')
tpl_web_type = Column(
String(30), nullable=True, default='', comment='前端模板类型(element-ui模版 element-plus模版)'
)
package_name = Column(String(100), nullable=True, comment='生成包路径')
module_name = Column(String(30), nullable=True, comment='生成模块名')
business_name = Column(String(30), nullable=True, comment='生成业务名')
function_name = Column(String(100), nullable=True, comment='生成功能名')
function_author = Column(String(100), nullable=True, comment='生成功能作者')
gen_type = Column(String(1), nullable=True, default='0', comment='生成代码方式(0zip压缩包 1自定义路径)')
gen_path = Column(String(200), nullable=True, default='/', comment='生成路径(不填默认项目路径)')
options = Column(String(1000), nullable=True, comment='其它生成选项')
create_by = Column(String(64), default='', comment='创建者')
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
update_by = Column(String(64), default='', comment='更新者')
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
remark = Column(String(500), nullable=True, default=None, comment='备注')
columns = relationship('GenTableColumn', order_by='GenTableColumn.sort', back_populates='tables')
class GenTableColumn(Base):
"""
代码生成业务表字段
"""
__tablename__ = 'gen_table_column'
column_id = Column(Integer, primary_key=True, autoincrement=True, comment='编号')
table_id = Column(Integer, ForeignKey('gen_table.table_id'), nullable=True, comment='归属表编号')
column_name = Column(String(200), nullable=True, comment='列名称')
column_comment = Column(String(500), nullable=True, comment='列描述')
column_type = Column(String(100), nullable=True, comment='列类型')
python_type = Column(String(500), nullable=True, comment='PYTHON类型')
python_field = Column(String(200), nullable=True, comment='PYTHON字段名')
is_pk = Column(String(1), nullable=True, comment='是否主键(1是)')
is_increment = Column(String(1), nullable=True, comment='是否自增(1是)')
is_required = Column(String(1), nullable=True, comment='是否必填(1是)')
is_unique = Column(String(1), nullable=True, comment='是否唯一(1是)')
is_insert = Column(String(1), nullable=True, comment='是否为插入字段(1是)')
is_edit = Column(String(1), nullable=True, comment='是否编辑字段(1是)')
is_list = Column(String(1), nullable=True, comment='是否列表字段(1是)')
is_query = Column(String(1), nullable=True, comment='是否查询字段(1是)')
query_type = Column(String(200), nullable=True, default='EQ', comment='查询方式(等于、不等于、大于、小于、范围)')
html_type = Column(
String(200), nullable=True, comment='显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)'
)
dict_type = Column(String(200), nullable=True, default='', comment='字典类型')
sort = Column(Integer, nullable=True, comment='排序')
create_by = Column(String(64), default='', comment='创建者')
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
update_by = Column(String(64), default='', comment='更新者')
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
tables = relationship('GenTable', back_populates='columns')
@@ -0,0 +1,274 @@
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field, model_validator
from pydantic.alias_generators import to_camel
from pydantic_validation_decorator import NotBlank
from typing import List, Literal, Optional
from config.constant import GenConstant
from module_admin.annotation.pydantic_annotation import as_query
from utils.string_util import StringUtil
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
from typing import List, Literal, Optional, Union
from module_admin.annotation.pydantic_annotation import as_query
# -*- coding:utf-8 -*-
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
from typing import List, Literal, Optional, Union
from module_admin.annotation.pydantic_annotation import as_query
class GenTableBaseModel(BaseModel):
"""
代码生成业务表对应pydantic模型
"""
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
table_id: Optional[int] = Field(default=None, description='编号')
table_name: Optional[str] = Field(default=None, description='表名称')
table_comment: Optional[str] = Field(default=None, description='表描述')
sub_table_name: Optional[str] = Field(default=None, description='关联子表的表名')
sub_table_fk_name: Optional[str] = Field(default=None, description='子表关联的外键名')
class_name: Optional[str] = Field(default=None, description='实体类名称')
tpl_category: Optional[str] = Field(default=None, description='使用的模板(crud单表操作 tree树表操作)')
tpl_web_type: Optional[str] = Field(default=None, description='前端模板类型(element-ui模版 element-plus模版)')
package_name: Optional[str] = Field(default=None, description='生成包路径')
module_name: Optional[str] = Field(default=None, description='生成模块名')
business_name: Optional[str] = Field(default=None, description='生成业务名')
function_name: Optional[str] = Field(default=None, description='生成功能名')
function_author: Optional[str] = Field(default=None, description='生成功能作者')
gen_type: Optional[Literal['0', '1']] = Field(default=None, description='生成代码方式(0zip压缩包 1自定义路径)')
gen_path: Optional[str] = Field(default=None, description='生成路径(不填默认项目路径)')
options: Optional[str] = Field(default=None, description='其它生成选项')
create_by: Optional[str] = Field(default=None, description='创建者')
create_time: Optional[datetime] = Field(default=None, description='创建时间')
update_by: Optional[str] = Field(default=None, description='更新者')
update_time: Optional[datetime] = Field(default=None, description='更新时间')
remark: Optional[str] = Field(default=None, description='备注')
@NotBlank(field_name='table_name', message='表名称不能为空')
def get_table_name(self):
return self.table_name
@NotBlank(field_name='table_comment', message='表描述不能为空')
def get_table_comment(self):
return self.table_comment
@NotBlank(field_name='class_name', message='实体类名称不能为空')
def get_class_name(self):
return self.class_name
@NotBlank(field_name='package_name', message='生成包路径不能为空')
def get_package_name(self):
return self.package_name
@NotBlank(field_name='module_name', message='生成模块名不能为空')
def get_module_name(self):
return self.module_name
@NotBlank(field_name='business_name', message='生成业务名不能为空')
def get_business_name(self):
return self.business_name
@NotBlank(field_name='function_name', message='生成功能名不能为空')
def get_function_name(self):
return self.function_name
@NotBlank(field_name='function_author', message='生成功能作者不能为空')
def get_function_author(self):
return self.function_author
def validate_fields(self):
self.get_table_name()
self.get_table_comment()
self.get_class_name()
self.get_package_name()
self.get_module_name()
self.get_business_name()
self.get_function_name()
self.get_function_author()
class GenTableModel(GenTableBaseModel):
"""
代码生成业务表模型
"""
pk_column: Optional['GenTableColumnModel'] = Field(default=None, description='主键信息')
sub_table: Optional['GenTableModel'] = Field(default=None, description='子表信息')
columns: Optional[List['GenTableColumnModel']] = Field(default=None, description='表列信息')
tree_code: Optional[str] = Field(default=None, description='树编码字段')
tree_parent_code: Optional[str] = Field(default=None, description='树父编码字段')
tree_name: Optional[str] = Field(default=None, description='树名称字段')
parent_menu_id: Optional[int] = Field(default=None, description='上级菜单ID字段')
parent_menu_name: Optional[str] = Field(default=None, description='上级菜单名称字段')
sub: Optional[bool] = Field(default=None, description='是否为子表')
tree: Optional[bool] = Field(default=None, description='是否为树表')
crud: Optional[bool] = Field(default=None, description='是否为单表')
@model_validator(mode='after')
def check_some_is(self) -> 'GenTableModel':
self.sub = True if self.tpl_category and self.tpl_category == GenConstant.TPL_SUB else False
self.tree = True if self.tpl_category and self.tpl_category == GenConstant.TPL_TREE else False
self.crud = True if self.tpl_category and self.tpl_category == GenConstant.TPL_CRUD else False
return self
class EditGenTableModel(GenTableModel):
"""
修改代码生成业务表模型
"""
params: Optional['GenTableParamsModel'] = Field(default=None, description='业务表参数')
class GenTableParamsModel(BaseModel):
"""
代码生成业务表参数模型
"""
model_config = ConfigDict(alias_generator=to_camel)
tree_code: Optional[str] = Field(default=None, description='树编码字段')
tree_parent_code: Optional[str] = Field(default=None, description='树父编码字段')
tree_name: Optional[str] = Field(default=None, description='树名称字段')
parent_menu_id: Optional[int] = Field(default=None, description='上级菜单ID字段')
class GenTableQueryModel(GenTableBaseModel):
"""
代码生成业务表不分页查询模型
"""
begin_time: Optional[str] = Field(default=None, description='开始时间')
end_time: Optional[str] = Field(default=None, description='结束时间')
@as_query
class GenTablePageQueryModel(GenTableQueryModel):
"""
代码生成业务表分页查询模型
"""
page_num: int = Field(default=1, description='当前页码')
page_size: int = Field(default=10, description='每页记录数')
class DeleteGenTableModel(BaseModel):
"""
删除代码生成业务表模型
"""
model_config = ConfigDict(alias_generator=to_camel)
table_ids: str = Field(description='需要删除的代码生成业务表ID')
class GenTableColumnBaseModel(BaseModel):
"""
代码生成业务表字段对应pydantic模型
"""
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
column_id: Optional[int] = Field(default=None, description='编号')
table_id: Optional[int] = Field(default=None, description='归属表编号')
column_name: Optional[str] = Field(default=None, description='列名称')
column_comment: Optional[str] = Field(default=None, description='列描述')
column_type: Optional[str] = Field(default=None, description='列类型')
python_type: Optional[str] = Field(default=None, description='PYTHON类型')
python_field: Optional[str] = Field(default=None, description='PYTHON字段名')
is_pk: Optional[str] = Field(default=None, description='是否主键(1是)')
is_increment: Optional[str] = Field(default=None, description='是否自增(1是)')
is_required: Optional[str] = Field(default=None, description='是否必填(1是)')
is_unique: Optional[str] = Field(default=None, description='是否唯一(1是)')
is_insert: Optional[str] = Field(default=None, description='是否为插入字段(1是)')
is_edit: Optional[str] = Field(default=None, description='是否编辑字段(1是)')
is_list: Optional[str] = Field(default=None, description='是否列表字段(1是)')
is_query: Optional[str] = Field(default=None, description='是否查询字段(1是)')
query_type: Optional[str] = Field(default=None, description='查询方式(等于、不等于、大于、小于、范围)')
html_type: Optional[str] = Field(
default=None, description='显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)'
)
dict_type: Optional[str] = Field(default=None, description='字典类型')
sort: Optional[int] = Field(default=None, description='排序')
create_by: Optional[str] = Field(default=None, description='创建者')
create_time: Optional[datetime] = Field(default=None, description='创建时间')
update_by: Optional[str] = Field(default=None, description='更新者')
update_time: Optional[datetime] = Field(default=None, description='更新时间')
@NotBlank(field_name='python_field', message='Python属性不能为空')
def get_python_field(self):
return self.python_field
def validate_fields(self):
self.get_python_field()
class GenTableColumnModel(GenTableColumnBaseModel):
"""
代码生成业务表字段模型
"""
cap_python_field: Optional[str] = Field(default=None, description='字段大写形式')
pk: Optional[bool] = Field(default=None, description='是否主键')
increment: Optional[bool] = Field(default=None, description='是否自增')
required: Optional[bool] = Field(default=None, description='是否必填')
unique: Optional[bool] = Field(default=None, description='是否唯一')
insert: Optional[bool] = Field(default=None, description='是否为插入字段')
edit: Optional[bool] = Field(default=None, description='是否编辑字段')
list: Optional[bool] = Field(default=None, description='是否列表字段')
query: Optional[bool] = Field(default=None, description='是否查询字段')
super_column: Optional[bool] = Field(default=None, description='是否为基类字段')
usable_column: Optional[bool] = Field(default=None, description='是否为基类字段白名单')
@model_validator(mode='after')
def check_some_is(self) -> 'GenTableModel':
self.cap_python_field = self.python_field[0].upper() + self.python_field[1:] if self.python_field else None
self.pk = True if self.is_pk and self.is_pk == '1' else False
self.increment = True if self.is_increment and self.is_increment == '1' else False
self.required = True if self.is_required and self.is_required == '1' else False
self.unique = True if self.is_unique and self.is_unique == '1' else False
self.insert = True if self.is_insert and self.is_insert == '1' else False
self.edit = True if self.is_edit and self.is_edit == '1' else False
self.list = True if self.is_list and self.is_list == '1' else False
self.query = True if self.is_query and self.is_query == '1' else False
self.super_column = (
True
if StringUtil.equals_any_ignore_case(self.python_field, GenConstant.TREE_ENTITY + GenConstant.BASE_ENTITY)
else False
)
self.usable_column = (
True if StringUtil.equals_any_ignore_case(self.python_field, ['parentId', 'orderNum', 'remark']) else False
)
return self
class GenTableColumnQueryModel(GenTableColumnBaseModel):
"""
代码生成业务表字段不分页查询模型
"""
begin_time: Optional[str] = Field(default=None, description='开始时间')
end_time: Optional[str] = Field(default=None, description='结束时间')
@as_query
class GenTableColumnPageQueryModel(GenTableColumnQueryModel):
"""
代码生成业务表字段分页查询模型
"""
page_num: int = Field(default=1, description='当前页码')
page_size: int = Field(default=10, description='每页记录数')
class DeleteGenTableColumnModel(BaseModel):
"""
删除代码生成业务表字段模型
"""
model_config = ConfigDict(alias_generator=to_camel)
column_ids: str = Field(description='需要删除的代码生成业务表字段ID')
@@ -0,0 +1,499 @@
import io
import json
import os
import zipfile
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from sqlglot import parse as sqlglot_parse
from sqlglot.expressions import Add, Alter, Create, Delete, Drop, Expression, Insert, Table, TruncateTable, Update
from typing import List
from config.constant import GenConstant
from config.env import DataBaseConfig, GenConfig
from exceptions.exception import ServiceException
from module_admin.entity.vo.common_vo import CrudResponseModel
from module_admin.entity.vo.user_vo import CurrentUserModel
from module_generator.entity.vo.gen_vo import (
DeleteGenTableModel,
EditGenTableModel,
GenTableColumnModel,
GenTableModel,
GenTablePageQueryModel,
)
from module_generator.dao.gen_dao import GenTableColumnDao, GenTableDao
from utils.common_util import CamelCaseUtil
from utils.gen_util import GenUtils
from utils.template_util import TemplateInitializer, TemplateUtils
class GenTableService:
"""
代码生成业务表服务层
"""
@classmethod
async def get_gen_table_list_services(
cls, query_db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False
):
"""
获取代码生成业务表列表信息service
:param query_db: orm对象
:param query_object: 查询参数对象
:param is_page: 是否开启分页
:return: 代码生成业务列表信息对象
"""
gen_table_list_result = await GenTableDao.get_gen_table_list(query_db, query_object, is_page)
return gen_table_list_result
@classmethod
async def get_gen_db_table_list_services(
cls, query_db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False
):
"""
获取数据库列表信息service
:param query_db: orm对象
:param query_object: 查询参数对象
:param is_page: 是否开启分页
:return: 数据库列表信息对象
"""
gen_db_table_list_result = await GenTableDao.get_gen_db_table_list(query_db, query_object, is_page)
return gen_db_table_list_result
@classmethod
async def get_gen_db_table_list_by_name_services(cls, query_db: AsyncSession, table_names: List[str]):
"""
根据表名称组获取数据库列表信息service
:param query_db: orm对象
:param table_names: 表名称组
:return: 数据库列表信息对象
"""
gen_db_table_list_result = await GenTableDao.get_gen_db_table_list_by_names(query_db, table_names)
return [GenTableModel(**gen_table) for gen_table in CamelCaseUtil.transform_result(gen_db_table_list_result)]
@classmethod
async def import_gen_table_services(
cls, query_db: AsyncSession, gen_table_list: List[GenTableModel], current_user: CurrentUserModel
):
"""
导入表结构service
:param query_db: orm对象
:param gen_table_list: 导入表列表
:param current_user: 当前用户信息对象
:return: 导入结果
"""
try:
for table in gen_table_list:
table_name = table.table_name
GenUtils.init_table(table, current_user.user.user_name)
add_gen_table = await GenTableDao.add_gen_table_dao(query_db, table)
if add_gen_table:
table.table_id = add_gen_table.table_id
gen_table_columns = await GenTableColumnDao.get_gen_db_table_columns_by_name(query_db, table_name)
for column in [
GenTableColumnModel(**gen_table_column)
for gen_table_column in CamelCaseUtil.transform_result(gen_table_columns)
]:
GenUtils.init_column_field(column, table)
await GenTableColumnDao.add_gen_table_column_dao(query_db, column)
await query_db.commit()
return CrudResponseModel(is_success=True, message='导入成功')
except Exception as e:
await query_db.rollback()
raise ServiceException(message=f'导入失败, {str(e)}')
@classmethod
async def edit_gen_table_services(cls, query_db: AsyncSession, page_object: EditGenTableModel):
"""
编辑业务表信息service
:param query_db: orm对象
:param page_object: 编辑业务表对象
:return: 编辑业务表校验结果
"""
edit_gen_table = page_object.model_dump(exclude_unset=True, by_alias=True)
gen_table_info = await cls.get_gen_table_by_id_services(query_db, page_object.table_id)
if gen_table_info.table_id:
try:
edit_gen_table['options'] = json.dumps(edit_gen_table.get('params'))
await GenTableDao.edit_gen_table_dao(query_db, edit_gen_table)
for gen_table_column in page_object.columns:
gen_table_column.update_by = page_object.update_by
gen_table_column.update_time = datetime.now()
await GenTableColumnDao.edit_gen_table_column_dao(
query_db, gen_table_column.model_dump(by_alias=True)
)
await query_db.commit()
return CrudResponseModel(is_success=True, message='更新成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='业务表不存在')
@classmethod
async def delete_gen_table_services(cls, query_db: AsyncSession, page_object: DeleteGenTableModel):
"""
删除业务表信息service
:param query_db: orm对象
:param page_object: 删除业务表对象
:return: 删除业务表校验结果
"""
if page_object.table_ids:
table_id_list = page_object.table_ids.split(',')
try:
for table_id in table_id_list:
await GenTableDao.delete_gen_table_dao(query_db, GenTableModel(tableId=table_id))
await GenTableColumnDao.delete_gen_table_column_by_table_id_dao(
query_db, GenTableColumnModel(tableId=table_id)
)
await query_db.commit()
return CrudResponseModel(is_success=True, message='删除成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='传入业务表id为空')
@classmethod
async def get_gen_table_by_id_services(cls, query_db: AsyncSession, table_id: int):
"""
获取需要生成的业务表详细信息service
:param query_db: orm对象
:param table_id: 需要生成的业务表id
:return: 需要生成的业务表id对应的信息
"""
gen_table = await GenTableDao.get_gen_table_by_id(query_db, table_id)
result = await cls.set_table_from_options(GenTableModel(**CamelCaseUtil.transform_result(gen_table)))
return result
@classmethod
async def get_gen_table_all_services(cls, query_db: AsyncSession):
"""
获取所有业务表信息service
:param query_db: orm对象
:return: 所有业务表信息
"""
gen_table_all = await GenTableDao.get_gen_table_all(query_db)
result = [GenTableModel(**gen_table) for gen_table in CamelCaseUtil.transform_result(gen_table_all)]
return result
@classmethod
async def create_table_services(cls, query_db: AsyncSession, sql: str, current_user: CurrentUserModel):
"""
创建表结构service
:param query_db: orm对象
:param sql: 建表语句
:param current_user: 当前用户信息对象
:return: 创建表结构结果
"""
sql_statements = sqlglot_parse(sql, dialect=DataBaseConfig.sqlglot_parse_dialect)
if cls.__is_valid_create_table(sql_statements):
try:
table_names = cls.__get_table_names(sql_statements)
await GenTableDao.create_table_by_sql_dao(query_db, sql_statements)
gen_table_list = await cls.get_gen_db_table_list_by_name_services(query_db, table_names)
await cls.import_gen_table_services(query_db, gen_table_list, current_user)
return CrudResponseModel(is_success=True, message='创建表结构成功')
except Exception as e:
raise ServiceException(message=f'创建表结构异常,详细错误信息:{str(e)}')
else:
raise ServiceException(message='建表语句不合法')
@classmethod
def __is_valid_create_table(cls, sql_statements: List[Expression]):
"""
校验sql语句是否为合法的建表语句
:param sql_statements: sql语句的ast列表
:return: 校验结果
"""
validate_create = [isinstance(sql_statement, Create) for sql_statement in sql_statements]
validate_forbidden_keywords = [
isinstance(
sql_statement,
(Add, Alter, Delete, Drop, Insert, TruncateTable, Update),
)
for sql_statement in sql_statements
]
if not any(validate_create) or any(validate_forbidden_keywords):
return False
return True
@classmethod
def __get_table_names(cls, sql_statements: List[Expression]):
"""
获取sql语句中所有的建表表名
:param sql_statements: sql语句的ast列表
:return: 建表表名列表
"""
table_names = []
for sql_statement in sql_statements:
if isinstance(sql_statement, Create):
table_names.append(sql_statement.find(Table).name)
return table_names
@classmethod
async def preview_code_services(cls, query_db: AsyncSession, table_id: int):
"""
预览代码service
:param query_db: orm对象
:param table_id: 业务表id
:return: 预览数据列表
"""
gen_table = GenTableModel(
**CamelCaseUtil.transform_result(await GenTableDao.get_gen_table_by_id(query_db, table_id))
)
await cls.set_sub_table(query_db, gen_table)
await cls.set_pk_column(gen_table)
env = TemplateInitializer.init_jinja2()
context = TemplateUtils.prepare_context(gen_table)
template_list = TemplateUtils.get_template_list(gen_table.tpl_category, gen_table.tpl_web_type)
preview_code_result = {}
for template in template_list:
render_content = env.get_template(template).render(**context)
preview_code_result[template] = render_content
return preview_code_result
@classmethod
async def generate_code_services(cls, query_db: AsyncSession, table_name: str):
"""
生成代码至指定路径service
:param query_db: orm对象
:param table_name: 业务表名称
:return: 生成代码结果
"""
env = TemplateInitializer.init_jinja2()
render_info = await cls.__get_gen_render_info(query_db, table_name)
for template in render_info[0]:
try:
render_content = env.get_template(template).render(**render_info[2])
gen_path = cls.__get_gen_path(render_info[3], template)
os.makedirs(os.path.dirname(gen_path), exist_ok=True)
with open(gen_path, 'w', encoding='utf-8') as f:
f.write(render_content)
except Exception as e:
raise ServiceException(
message=f'渲染模板失败,表名:{render_info[3].table_name},详细错误信息:{str(e)}'
)
return CrudResponseModel(is_success=True, message='生成代码成功')
@classmethod
async def batch_gen_code_services(cls, query_db: AsyncSession, table_names: List[str]):
"""
批量生成代码service
:param query_db: orm对象
:param table_names: 业务表名称组
:return: 下载代码结果
"""
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for table_name in table_names:
env = TemplateInitializer.init_jinja2()
render_info = await cls.__get_gen_render_info(query_db, table_name)
for template_file, output_file in zip(render_info[0], render_info[1]):
render_content = env.get_template(template_file).render(**render_info[2])
zip_file.writestr(output_file, render_content)
zip_data = zip_buffer.getvalue()
zip_buffer.close()
return zip_data
@classmethod
async def __get_gen_render_info(cls, query_db: AsyncSession, table_name: str):
"""
获取生成代码渲染模板相关信息
:param query_db: orm对象
:param table_name: 业务表名称
:return: 生成代码渲染模板相关信息
"""
gen_table = GenTableModel(
**CamelCaseUtil.transform_result(await GenTableDao.get_gen_table_by_name(query_db, table_name))
)
await cls.set_sub_table(query_db, gen_table)
await cls.set_pk_column(gen_table)
context = TemplateUtils.prepare_context(gen_table)
template_list = TemplateUtils.get_template_list(gen_table.tpl_category, gen_table.tpl_web_type)
output_files = [TemplateUtils.get_file_name(template, gen_table) for template in template_list]
return [template_list, output_files, context, gen_table]
@classmethod
def __get_gen_path(cls, gen_table: GenTableModel, template: str):
"""
根据GenTableModel对象和模板名称生成路径
:param gen_table: GenTableModel对象
:param template: 模板名称
:return: 生成的路径
"""
gen_path = gen_table.gen_path
if gen_path == '/':
return os.path.join(os.getcwd(), GenConfig.GEN_PATH, TemplateUtils.get_file_name(template, gen_table))
else:
return os.path.join(gen_path, TemplateUtils.get_file_name(template, gen_table))
@classmethod
async def sync_db_services(cls, query_db: AsyncSession, table_name: str):
"""
同步数据库service
:param query_db: orm对象
:param table_name: 业务表名称
:return: 同步数据库结果
"""
gen_table = await GenTableDao.get_gen_table_by_name(query_db, table_name)
table = GenTableModel(**CamelCaseUtil.transform_result(gen_table))
table_columns = table.columns
table_column_map = {column.column_name: column for column in table_columns}
query_db_table_columns = await GenTableColumnDao.get_gen_db_table_columns_by_name(query_db, table_name)
db_table_columns = [
GenTableColumnModel(**column) for column in CamelCaseUtil.transform_result(query_db_table_columns)
]
if not db_table_columns:
raise ServiceException('同步数据失败,原表结构不存在')
db_table_column_names = [column.column_name for column in db_table_columns]
try:
for column in db_table_columns:
GenUtils.init_column_field(column, table)
if column.column_name in table_column_map:
prev_column = table_column_map[column.column_name]
column.column_id = prev_column.column_id
if column.list:
column.dict_type = prev_column.dict_type
column.query_type = prev_column.query_type
if (
prev_column.is_required != ''
and not column.pk
and (column.insert or column.edit)
and (column.usable_column or column.super_column)
):
column.is_required = prev_column.is_required
column.html_type = prev_column.html_type
await GenTableColumnDao.edit_gen_table_column_dao(query_db, column.model_dump(by_alias=True))
else:
await GenTableColumnDao.add_gen_table_column_dao(query_db, column)
del_columns = [column for column in table_columns if column.column_name not in db_table_column_names]
if del_columns:
for column in del_columns:
await GenTableColumnDao.delete_gen_table_column_by_column_id_dao(query_db, column)
await query_db.commit()
return CrudResponseModel(is_success=True, message='同步成功')
except Exception as e:
await query_db.rollback()
raise e
@classmethod
async def set_sub_table(cls, query_db: AsyncSession, gen_table: GenTableModel):
"""
设置主子表信息
:param query_db: orm对象
:param gen_table: 业务表信息
:return:
"""
if gen_table.sub_table_name:
sub_table = await GenTableDao.get_gen_table_by_name(query_db, gen_table.sub_table_name)
gen_table.sub_table = GenTableModel(**CamelCaseUtil.transform_result(sub_table))
@classmethod
async def set_pk_column(cls, gen_table: GenTableModel):
"""
设置主键列信息
:param gen_table: 业务表信息
:return:
"""
for column in gen_table.columns:
if column.pk:
gen_table.pk_column = column
break
if gen_table.pk_column is None:
gen_table.pk_column = gen_table.columns[0]
if gen_table.tpl_category == GenConstant.TPL_SUB:
for column in gen_table.sub_table.columns:
if column.pk:
gen_table.sub_table.pk_column = column
break
if gen_table.sub_table.columns is None:
gen_table.sub_table.pk_column = gen_table.sub_table.columns[0]
@classmethod
async def set_table_from_options(cls, gen_table: GenTableModel):
"""
设置代码生成其他选项值
:param gen_table: 生成对象
:return: 设置后的生成对象
"""
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)
gen_table.tree_parent_code = params_obj.get(GenConstant.TREE_PARENT_CODE)
gen_table.tree_name = params_obj.get(GenConstant.TREE_NAME)
gen_table.parent_menu_id = params_obj.get(GenConstant.PARENT_MENU_ID)
gen_table.parent_menu_name = params_obj.get(GenConstant.PARENT_MENU_NAME)
return gen_table
@classmethod
async def validate_edit(cls, edit_gen_table: EditGenTableModel):
"""
编辑保存参数校验
:param edit_gen_table: 编辑业务表对象
"""
if edit_gen_table.tpl_category == GenConstant.TPL_TREE:
params_obj = edit_gen_table.params.model_dump(by_alias=True)
if GenConstant.TREE_CODE not in params_obj:
raise ServiceException(message='树编码字段不能为空')
elif GenConstant.TREE_PARENT_CODE not in params_obj:
raise ServiceException(message='树父编码字段不能为空')
elif GenConstant.TREE_NAME not in params_obj:
raise ServiceException(message='树名称字段不能为空')
elif edit_gen_table.tpl_category == GenConstant.TPL_SUB:
if not edit_gen_table.sub_table_name:
raise ServiceException(message='关联子表的表名不能为空')
elif not edit_gen_table.sub_table_fk_name:
raise ServiceException(message='子表关联的外键名不能为空')
class GenTableColumnService:
"""
代码生成业务表字段服务层
"""
@classmethod
async def get_gen_table_column_list_by_table_id_services(cls, query_db: AsyncSession, table_id: int):
"""
获取业务表字段列表信息service
:param query_db: orm对象
:param table_id: 业务表格id
:return: 业务表字段列表信息对象
"""
gen_table_column_list_result = await GenTableColumnDao.get_gen_table_column_list_by_table_id(query_db, table_id)
return [
GenTableColumnModel(**gen_table_column)
for gen_table_column in CamelCaseUtil.transform_result(gen_table_column_list_result)
]
@@ -1,2 +0,0 @@
# -*- coding: utf-8 -*-
@@ -1,15 +0,0 @@
# -*- coding:utf-8 -*-
from sqlalchemy import Boolean, Column, ForeignKey, String, Integer, Text, DateTime
from app.core.base_model import BaseMixin
class PageModel(BaseMixin):
__tablename__ = "gen_page"
page_name = Column(String(length=255), comment='页面名称')
keywords = Column(String(length=500), comment='页面关键词')
title = Column(String(length=500), comment='页面title标题')
@@ -1,15 +0,0 @@
# -*- coding:utf-8 -*-
from sqlalchemy import Boolean, Column, ForeignKey, String, Integer, Text, DateTime
from app.core.base_model import BaseMixin
class PageModel(BaseMixin):
__tablename__ = "gen_page"
page_name = Column(String(length=255), comment='页面名称')
keywords = Column(String(length=500), comment='页面关键词')
title = Column(String(length=500), comment='页面title标题')
@@ -1,192 +0,0 @@
# -*- coding: utf-8 -*-
import io
from typing import Any, List, Dict
from fastapi import UploadFile
import pandas as pd
from app.api.v1.schemas.system.auth_schema import AuthSchema
from app.api.v1.schemas.demo.example_schema import ExampleCreateSchema, ExampleUpdateSchema, ExampleOutSchema
from app.core.base_schema import BatchSetAvailable
from app.api.v1.params.demo.example_param import ExampleQueryParams
from app.api.v1.cruds.demo.example_crud import ExampleCRUD
from app.core.exceptions import CustomException
from app.utils.excel_util import ExcelUtil
from app.core.logger import logger
class ExampleService:
"""
示例管理模块服务层
"""
@classmethod
async def get_example_detail_service(cls, auth: AuthSchema, id: int) -> Dict:
"""详情"""
obj = await ExampleCRUD(auth).get_by_id_crud(id=id)
return ExampleOutSchema.model_validate(obj).model_dump()
@classmethod
async def get_demo_list_service(cls, auth: AuthSchema, search: ExampleQueryParams = None, order_by: List[Dict[str, str]] = None) -> List[Dict]:
"""列表查询"""
if order_by:
order_by = eval(order_by)
obj_list = await ExampleCRUD(auth).get_list_crud(search=search.__dict__, order_by=order_by)
return [ExampleOutSchema.model_validate(obj).model_dump() for obj in obj_list]
@classmethod
async def create_example_service(cls, auth: AuthSchema, data: ExampleCreateSchema) -> Dict:
"""创建"""
obj = await ExampleCRUD(auth).get(name=data.name)
if obj:
raise CustomException(msg='创建失败,名称已存在')
obj = await ExampleCRUD(auth).create_crud(data=data)
return ExampleOutSchema.model_validate(obj).model_dump()
@classmethod
async def update_example_service(cls, auth: AuthSchema, data: ExampleUpdateSchema) -> Dict:
"""更新"""
obj = await ExampleCRUD(auth).get_by_id_crud(id=data.id)
if not obj:
raise CustomException(msg='更新失败,该数据不存在')
exist_obj = await ExampleCRUD(auth).get(name=data.name)
if exist_obj and exist_obj.id != data.id:
raise CustomException(msg='更新失败,名称重复')
obj = await ExampleCRUD(auth).update_crud(id=data.id, data=data)
return ExampleOutSchema.model_validate(obj).model_dump()
@classmethod
async def delete_example_service(cls, auth: AuthSchema, ids: list[int]) -> None:
"""删除"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
for id in ids:
obj = await ExampleCRUD(auth).get_by_id_crud(id=id)
if not obj:
raise CustomException(msg='删除失败,该数据不存在')
await ExampleCRUD(auth).delete_crud(ids=ids)
@classmethod
async def set_example_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
"""批量设置状态"""
await ExampleCRUD(auth).set_available_crud(ids=data.ids, status=data.status)
@classmethod
async def batch_export_service(cls, obj_list: List[Dict[str, Any]]) -> bytes:
"""批量导出"""
mapping_dict = {
'id': '编号',
'name': '名称',
'status': '状态',
'description': '备注',
'created_at': '创建时间',
'updated_at': '更新时间',
'creator': '创建者',
}
# 复制数据并转换状态
data = obj_list.copy()
for item in data:
# 处理状态
item['status'] = '正常' if item.get('status') else '停用'
# 处理公告类型
item['creator'] = item.get('creator', {}).get('name', '未知') if isinstance(item.get('creator'), dict) else '未知'
return ExcelUtil.export_list2excel(list_data=obj_list, mapping_dict=mapping_dict)
@classmethod
async def batch_import_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
"""批量导入"""
header_dict = {
'名称': 'name',
'状态': 'status',
'描述': 'description'
}
try:
# 读取Excel文件
contents = await file.read()
df = pd.read_excel(io.BytesIO(contents))
await file.close()
if df.empty:
raise CustomException(msg="导入文件为空")
# 检查表头是否完整
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
if missing_headers:
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
# 重命名列名
df.rename(columns=header_dict, inplace=True)
# 验证必填字段
required_fields = ['name', 'status']
for field in required_fields:
if df[field].isnull().any():
missing_rows = df[df[field].isnull()].index.tolist()
raise CustomException(msg=f"{[k for k,v in header_dict.items() if v == field][0]}不能为空,第{[i+1 for i in missing_rows]}")
error_msgs = []
success_count = 0
# 处理每一行数据
for index, row in df.iterrows():
try:
# 数据转换前的类型检查
try:
name = str(row['name'])
except ValueError:
error_msgs.append(f"{index+1}行: 名称必须是字符串")
continue
try:
status = True if row['status'] == '正常' else False
except ValueError:
error_msgs.append(f"{index+1}行: 状态必须是'正常''停用'")
continue
# 构建用户数据
data = {
"name": name,
"status": status,
"description": str(row['description']).strip() if not pd.isna(row['description']) else None,
}
# 处理用户导入
exists_user = await ExampleCRUD(auth).get(name=data["name"])
if exists_user:
if update_support:
await ExampleCRUD(auth).update(id=exists_user.id, data=data)
success_count += 1
else:
error_msgs.append(f"{index+1}行: 用户 {data['username']} 已存在")
else:
await ExampleCRUD(auth).create(data=data)
success_count += 1
except Exception as e:
error_msgs.append(f"{index+1}行: {str(e)}")
continue
# 返回详细的导入结果
result = f"成功导入 {success_count} 条数据"
if error_msgs:
result += "\n错误信息:\n" + "\n".join(error_msgs)
return result
except Exception as e:
logger.error(f"批量导入用户失败: {str(e)}")
raise CustomException(msg=f"导入失败: {str(e)}")
@classmethod
async def import_template_download_service(cls) -> bytes:
"""下载导入模板"""
header_list = ['名称', '状态', '描述']
selector_header_list = ['状态']
option_list = [{'状态': ['正常', '停用']}]
return ExcelUtil.get_excel_template(
header_list=header_list,
selector_header_list=selector_header_list,
option_list=option_list
)
@@ -1,27 +0,0 @@
from typing import List
from module_gen.dao.gen_table_column_dao import GenTableColumnDao
from module_gen.entity.do.gen_table_column_do import GenTableColumn
from module_gen.entity.vo.gen_table_column_vo import GenTableColumnPageModel
class GenTableColumnService:
"""代码生成业务字段 服务层实现"""
# @classmethod
# async def select_gen_table_column_by_table_id(cls, table_id: int, query_db) -> List[GenTableColumn]:
# """查询业务字段列表"""
# return await GenTableColumnDao.get_gen_table_column_list(query_db, GenTableColumnPageModel(tableId=table_id))
#
#
# async def insert_gen_table_column(cls, gen_table_column: GenTableColumn) -> int:
# """新增业务字段"""
# return await GenTableColumnDao.insert_gen_table_column(gen_table_column)
#
# async def update_gen_table_column(cls, gen_table_column: GenTableColumn) -> int:
# """修改业务字段"""
# return await GenTableColumnDao.update_gen_table_column(gen_table_column)
#
# async def delete_gen_table_column_by_ids(cls, ids: List[int]) -> int:
# """删除业务字段信息"""
# return await GenTableColumnDao.delete_gen_table_column_by_ids(ids)
@@ -1,210 +0,0 @@
import io
import json
import zipfile
from io import BytesIO
from math import trunc
from typing import List, Optional, Dict
from watchfiles import awatch
from module_gen.dao.gen_table_dao import GenTableDao
from module_gen.dao.gen_table_column_dao import GenTableColumnDao
from module_gen.entity.do.gen_table_column_do import GenTableColumn
from module_gen.entity.do.gen_table_do import GenTable
from module_gen.entity.vo.gen_table_options_vo import GenTableOptionModel
from module_gen.entity.vo.gen_table_vo import GenTablePageModel, GenTableModel
from module_gen.utils.gen_utils import GenUtils
from module_gen.utils.velocity_utils import VelocityUtils
from module_gen.entity.vo.gen_table_column_vo import GenTableColumnModel, GenTableColumnPageModel
from utils.common_util import CamelCaseUtil, SnakeCaseUtil
from utils.page_util import PageResponseModel
class GenTableService:
"""代码生成 服务层实现"""
@classmethod
async def select_gen_table_list(cls, gen_table: GenTablePageModel, query_db, data_scope_sql) -> PageResponseModel:
"""查询业务信息"""
return await GenTableDao.get_gen_table_list(query_db, gen_table, data_scope_sql, is_page=True)
@classmethod
async def select_all_gen_table_list(cls, query_db, data_scope_sql) -> PageResponseModel:
"""查询业务信息"""
return await GenTableDao.get_gen_table_list(query_db, GenTablePageModel(), data_scope_sql, is_page=False)
@classmethod
async def select_gen_table_by_id(cls, table_id: int, query_db, data_scope_sql) -> Optional[GenTableModel]:
"""查询业务信息"""
gen_table = await GenTableDao.get_by_id(query_db, table_id)
columns = await GenTableColumnDao.get_gen_table_column_list(query_db,
GenTableColumnPageModel(tableId=table_id),
data_scope_sql)
result = GenTableModel(**CamelCaseUtil.transform_result(gen_table))
if result.options:
table_options = GenTableOptionModel(**json.loads(result.options))
result.parent_menu_id = table_options.parent_menu_id
result.columns = columns
return result
@classmethod
async def select_gen_table_by_name(cls, table_name: str, query_db) -> Optional[GenTableModel]:
"""查询表名称业务信息"""
gen_table = await GenTableDao.get_by_table_name(query_db, table_name)
columns = await GenTableColumnDao.get_gen_table_column_list(query_db,
GenTableColumnPageModel(
tableId=gen_table.table_id))
result = GenTableModel(**CamelCaseUtil.transform_result(gen_table))
if result.options:
table_options = GenTableOptionModel(**json.loads(result.options))
result.parent_menu_id = table_options.parent_menu_id
result.columns = columns
return result
@classmethod
async def select_db_table_list(cls, gen_table: GenTablePageModel, query_db, data_scope_sql) -> PageResponseModel:
"""查询数据库列表"""
return await GenTableDao.select_db_table_list(query_db, gen_table, is_page=True)
@classmethod
async def select_db_table_list_by_names(cls, table_names: List[str], query_db) -> List[GenTableModel]:
"""查询数据库列表"""
return await GenTableDao.select_db_table_list_by_names(query_db, table_names)
@classmethod
async def import_gen_table(cls, table_list: List[str], query_db) -> None:
"""导入表结构"""
tables = await GenTableService.select_db_table_list_by_names(table_list, query_db)
for table in tables:
gen_table = GenTableModel()
gen_table.table_name = table.table_name
gen_table.table_comment = table.table_comment
# 查询表列信息
columns = await GenTableColumnDao.select_db_table_columns_by_name(query_db, table.table_name)
GenUtils.init_table(gen_table, columns)
# 添加表信息
gen_table_result = await GenTableDao.add_gen_table(query_db, gen_table)
# 添加列信息
for i, column in enumerate(columns):
column.table_id = gen_table_result.table_id
await GenTableColumnDao.add_gen_table_column(query_db, column)
await query_db.commit()
@classmethod
async def validate_edit(cls, gen_table: GenTableModel) -> None:
"""验证编辑"""
if gen_table.tpl_category == "tree":
if not all([gen_table.tree_code, gen_table.tree_parent_code, gen_table.tree_name]):
raise ValueError("树表配置必须填写树编码字段、树父编码字段和树名称字段")
@classmethod
async def update_gen_table(cls, query_db, gen_table: GenTableModel) -> None:
"""业务信息"""
columns_dicts = gen_table.columns
# columns = [GenTableColumnModel(**columns_dict) for columns_dict in columns_dicts]
gen_table.options = json.dumps(gen_table.params)
await GenTableDao.edit_gen_table(query_db, gen_table)
if gen_table.columns:
for column in gen_table.columns:
column.table_id = gen_table.table_id
await GenTableColumnDao.edit_gen_table_column(query_db, column, exclude_unset=False)
@classmethod
async def delete_gen_table_by_ids(cls, query_db, ids: List[int]) -> None:
"""删除业务对象"""
await GenTableDao.del_gen_table_by_ids(query_db, ids, soft_del=False)
await GenTableColumnDao.del_gen_table_column_by_table_ids(query_db, ids, soft_del=False)
# @classmethod
# async def generate_code(cls, table_name: str, query_db) -> None:
# """生成代码(自定义路径)"""
# # 查询表信息
# table = await GenTableService.select_gen_table_by_name(table_name, query_db)
# # 生成代码
# if table:
# # 获取模板列表
# templates = GenUtils.get_template_path(table.tpl_category)
# context = await VelocityUtils.get_render_params(table, query_db)
# # 生成代码
# for template_name, template_path in templates.items():
# # 渲染模板
#
#
# # 获取生成路径
# file_name = GenUtils.get_file_name(template_name, table)
# if file_name:
# try:
# file_path = table.gen_path + "/" + file_name
# # 写入文件
# VelocityUtils.write_file(template_path, context, file_path)
# except Exception as e:
# raise RuntimeError(f"渲染模板失败,表名:{table.table_name}")
@classmethod
async def sync_db(cls, query_db, table_name: str, data_scope_sql) -> None:
table = await GenTableDao.get_by_table_name(query_db, table_name)
table_columns_dicts = await GenTableColumnDao.get_gen_table_column_list(query_db,
GenTableColumnPageModel(tableName=table_name),
data_scope_sql)
table_columns = [GenTableColumnModel(**tcd) for tcd in table_columns_dicts]
db_table_columns = await GenTableColumnDao.select_db_table_columns_by_name(query_db, table_name)
if not db_table_columns or len(db_table_columns) == 0:
return None
for i, db_table_column in enumerate(db_table_columns):
GenUtils.init_column_field(db_table_column, GenTableModel(**CamelCaseUtil.transform_result(table)))
prev_column = next((table_column for table_column in table_columns if table_column.column_name == db_table_column.column_name), None)
if prev_column:
db_table_column.column_id = prev_column.column_id
if db_table_column.is_list:
db_table_column.dict_type = prev_column.dict_type
db_table_column.query_type = prev_column.query_type
db_table_column.is_required = prev_column.is_required
db_table_column.html_type = prev_column.html_type
await GenTableColumnDao.edit_gen_table_column(query_db, db_table_column, auto_commit=False, exclude_unset=True)
else:
await GenTableColumnDao.add_gen_table_column(query_db, db_table_column)
dbc_names = {dbc.column_name for dbc in db_table_columns}
del_columns = [t_column for t_column in table_columns if t_column.column_name not in dbc_names]
for i, del_column in enumerate(del_columns):
await GenTableColumnDao.del_gen_table_column(query_db, del_column, auto_commit=False, soft_del=False)
await query_db.commit()
@classmethod
async def preview_code(cls, query_db, table_id: int, data_scope_sql) -> (Dict[str, str], GenTableModel):
"""预览模板代码"""
table = await cls.select_gen_table_by_id(table_id, query_db, data_scope_sql)
render_params = await VelocityUtils.get_render_params(table, query_db)
templates = GenUtils.get_template_path(table.tpl_category)
preview_result = {}
for template_name, template_path in templates.items():
template = VelocityUtils.get_template(template_path)
content = template.render(**render_params)
preview_result[template_name] = content
return preview_result, table
@classmethod
async def batch_generate_code(cls, query_db, data_scope_sql, table_id_array:List[str]) -> BytesIO:
"""批量下载生成代码"""
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
for i, table_id in enumerate(table_id_array):
preview_result, table = await cls.preview_code(query_db, int(table_id), data_scope_sql)
for filename, content in preview_result.items():
target_file_name = GenUtils.get_file_name(filename, table)
zip_file.writestr(target_file_name, content)
zip_buffer.seek(0)
return zip_buffer
@classmethod
async def create_table(cls, query_db, sql) -> bool:
"""数据库表创建"""
return await GenTableDao.create_table(query_db, sql)
@@ -1,2 +0,0 @@
# -*- coding: utf-8 -*-
@@ -1,44 +0,0 @@
# -*- coding:utf-8 -*-
import datetime
from sqlalchemy import Boolean, Column, ForeignKey, String, Integer, Text, DateTime
from app.core.base_model import BaseMixin
class SysTable(BaseMixin):
__tablename__ = "gen_table"
id = Column(Integer, primary_key=True, autoincrement=True)
create_time = Column(DateTime, nullable=False, default=datetime.datetime.now, comment='创建时间')
update_time = Column(DateTime, nullable=False, default=datetime.datetime.now, onupdate=datetime.datetime.now, index=True, comment='更新时间')
del_flag = Column(String(1), nullable=False, default='0', server_default=text("'0'"), comment='删除标志(0代表存在 2代表删除)')
align = Column(String(255), nullable=False, default='left', comment='对其方式')
field_name = Column(String(255), nullable=False, comment='字段名')
fixed = Column(String(1), nullable=False, default='0', comment='固定表头')
label = Column(String(255), nullable=False, comment='字段标签')
label_tip = Column(String(255), comment='字段标签解释')
prop = Column(String(255), nullable=False, comment='驼峰属性')
show = Column(String(1), nullable=False, default='1', comment='可见')
sortable = Column(String(1), nullable=False, default='0', comment='可排序')
table_name = Column(String(255), nullable=False, comment='表名')
tooltip = Column(String(1), nullable=False, default='1', comment='超出隐藏')
update_by = Column(Integer, comment='更新者')
update_by_name = Column(String(255), comment='更新者')
width = Column(Integer, nullable=False, default=150, comment='宽度')
sequence = Column(Integer, nullable=False, default=0, comment='字段顺序')
@@ -1,44 +0,0 @@
# -*- coding:utf-8 -*-
import datetime
from sqlalchemy import Boolean, Column, ForeignKey, String, Integer, Text, DateTime
from app.core.base_model import BaseMixin
class SysTable(BaseMixin):
__tablename__ = "gen_table"
id = Column(Integer, primary_key=True, autoincrement=True)
create_time = Column(DateTime, nullable=False, default=datetime.datetime.now, comment='创建时间')
update_time = Column(DateTime, nullable=False, default=datetime.datetime.now, onupdate=datetime.datetime.now, index=True, comment='更新时间')
del_flag = Column(String(1), nullable=False, default='0', server_default=text("'0'"), comment='删除标志(0代表存在 2代表删除)')
align = Column(String(255), nullable=False, default='left', comment='对其方式')
field_name = Column(String(255), nullable=False, comment='字段名')
fixed = Column(String(1), nullable=False, default='0', comment='固定表头')
label = Column(String(255), nullable=False, comment='字段标签')
label_tip = Column(String(255), comment='字段标签解释')
prop = Column(String(255), nullable=False, comment='驼峰属性')
show = Column(String(1), nullable=False, default='1', comment='可见')
sortable = Column(String(1), nullable=False, default='0', comment='可排序')
table_name = Column(String(255), nullable=False, comment='表名')
tooltip = Column(String(1), nullable=False, default='1', comment='超出隐藏')
update_by = Column(Integer, comment='更新者')
update_by_name = Column(String(255), comment='更新者')
width = Column(Integer, nullable=False, default=150, comment='宽度')
sequence = Column(Integer, nullable=False, default=0, comment='字段顺序')
@@ -1,192 +0,0 @@
# -*- coding: utf-8 -*-
import io
from typing import Any, List, Dict
from fastapi import UploadFile
import pandas as pd
from app.api.v1.schemas.system.auth_schema import AuthSchema
from app.api.v1.schemas.demo.example_schema import ExampleCreateSchema, ExampleUpdateSchema, ExampleOutSchema
from app.core.base_schema import BatchSetAvailable
from app.api.v1.params.demo.example_param import ExampleQueryParams
from app.api.v1.cruds.demo.example_crud import ExampleCRUD
from app.core.exceptions import CustomException
from app.utils.excel_util import ExcelUtil
from app.core.logger import logger
class ExampleService:
"""
示例管理模块服务层
"""
@classmethod
async def get_example_detail_service(cls, auth: AuthSchema, id: int) -> Dict:
"""详情"""
obj = await ExampleCRUD(auth).get_by_id_crud(id=id)
return ExampleOutSchema.model_validate(obj).model_dump()
@classmethod
async def get_demo_list_service(cls, auth: AuthSchema, search: ExampleQueryParams = None, order_by: List[Dict[str, str]] = None) -> List[Dict]:
"""列表查询"""
if order_by:
order_by = eval(order_by)
obj_list = await ExampleCRUD(auth).get_list_crud(search=search.__dict__, order_by=order_by)
return [ExampleOutSchema.model_validate(obj).model_dump() for obj in obj_list]
@classmethod
async def create_example_service(cls, auth: AuthSchema, data: ExampleCreateSchema) -> Dict:
"""创建"""
obj = await ExampleCRUD(auth).get(name=data.name)
if obj:
raise CustomException(msg='创建失败,名称已存在')
obj = await ExampleCRUD(auth).create_crud(data=data)
return ExampleOutSchema.model_validate(obj).model_dump()
@classmethod
async def update_example_service(cls, auth: AuthSchema, data: ExampleUpdateSchema) -> Dict:
"""更新"""
obj = await ExampleCRUD(auth).get_by_id_crud(id=data.id)
if not obj:
raise CustomException(msg='更新失败,该数据不存在')
exist_obj = await ExampleCRUD(auth).get(name=data.name)
if exist_obj and exist_obj.id != data.id:
raise CustomException(msg='更新失败,名称重复')
obj = await ExampleCRUD(auth).update_crud(id=data.id, data=data)
return ExampleOutSchema.model_validate(obj).model_dump()
@classmethod
async def delete_example_service(cls, auth: AuthSchema, ids: list[int]) -> None:
"""删除"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
for id in ids:
obj = await ExampleCRUD(auth).get_by_id_crud(id=id)
if not obj:
raise CustomException(msg='删除失败,该数据不存在')
await ExampleCRUD(auth).delete_crud(ids=ids)
@classmethod
async def set_example_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
"""批量设置状态"""
await ExampleCRUD(auth).set_available_crud(ids=data.ids, status=data.status)
@classmethod
async def batch_export_service(cls, obj_list: List[Dict[str, Any]]) -> bytes:
"""批量导出"""
mapping_dict = {
'id': '编号',
'name': '名称',
'status': '状态',
'description': '备注',
'created_at': '创建时间',
'updated_at': '更新时间',
'creator': '创建者',
}
# 复制数据并转换状态
data = obj_list.copy()
for item in data:
# 处理状态
item['status'] = '正常' if item.get('status') else '停用'
# 处理公告类型
item['creator'] = item.get('creator', {}).get('name', '未知') if isinstance(item.get('creator'), dict) else '未知'
return ExcelUtil.export_list2excel(list_data=obj_list, mapping_dict=mapping_dict)
@classmethod
async def batch_import_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
"""批量导入"""
header_dict = {
'名称': 'name',
'状态': 'status',
'描述': 'description'
}
try:
# 读取Excel文件
contents = await file.read()
df = pd.read_excel(io.BytesIO(contents))
await file.close()
if df.empty:
raise CustomException(msg="导入文件为空")
# 检查表头是否完整
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
if missing_headers:
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
# 重命名列名
df.rename(columns=header_dict, inplace=True)
# 验证必填字段
required_fields = ['name', 'status']
for field in required_fields:
if df[field].isnull().any():
missing_rows = df[df[field].isnull()].index.tolist()
raise CustomException(msg=f"{[k for k,v in header_dict.items() if v == field][0]}不能为空,第{[i+1 for i in missing_rows]}")
error_msgs = []
success_count = 0
# 处理每一行数据
for index, row in df.iterrows():
try:
# 数据转换前的类型检查
try:
name = str(row['name'])
except ValueError:
error_msgs.append(f"{index+1}行: 名称必须是字符串")
continue
try:
status = True if row['status'] == '正常' else False
except ValueError:
error_msgs.append(f"{index+1}行: 状态必须是'正常''停用'")
continue
# 构建用户数据
data = {
"name": name,
"status": status,
"description": str(row['description']).strip() if not pd.isna(row['description']) else None,
}
# 处理用户导入
exists_user = await ExampleCRUD(auth).get(name=data["name"])
if exists_user:
if update_support:
await ExampleCRUD(auth).update(id=exists_user.id, data=data)
success_count += 1
else:
error_msgs.append(f"{index+1}行: 用户 {data['username']} 已存在")
else:
await ExampleCRUD(auth).create(data=data)
success_count += 1
except Exception as e:
error_msgs.append(f"{index+1}行: {str(e)}")
continue
# 返回详细的导入结果
result = f"成功导入 {success_count} 条数据"
if error_msgs:
result += "\n错误信息:\n" + "\n".join(error_msgs)
return result
except Exception as e:
logger.error(f"批量导入用户失败: {str(e)}")
raise CustomException(msg=f"导入失败: {str(e)}")
@classmethod
async def import_template_download_service(cls) -> bytes:
"""下载导入模板"""
header_list = ['名称', '状态', '描述']
selector_header_list = ['状态']
option_list = [{'状态': ['正常', '停用']}]
return ExcelUtil.get_excel_template(
header_list=header_list,
selector_header_list=selector_header_list,
option_list=option_list
)
@@ -1,186 +0,0 @@
import os
from typing import List, Dict, Any
from click.types import convert_type
from sqlalchemy import Boolean
from module_gen.constants.gen_constants import GenConstants
from module_gen.entity.do.gen_table_column_do import GenTableColumn
from module_gen.entity.do.gen_table_do import GenTable
from module_gen.entity.vo.gen_table_vo import GenTableModel
from module_gen.entity.vo.gen_table_column_vo import GenTableColumnModel
class GenUtils:
"""代码生成器 工具类"""
@classmethod
def init_table(cls, table: GenTableModel, columns: List[GenTableColumnModel]) -> None:
"""初始化表信息"""
table.class_name = cls.convert_class_name(table.table_name)
table.package_name = cls.get_package_name(table.table_name)
table.module_name = cls.get_module_name(table.table_name)
table.business_name = cls.get_business_name(table.table_name)
table.function_name = table.table_comment
table.function_author = "FluxAdmin"
# 初始化列属性字段
for column in columns:
cls.init_column_field(column, table)
# 设置主键列信息
# for column in columns:
# if column.is_pk == "1":
# table.pk_column = column
# break
@classmethod
def init_column_field(cls, column: GenTableColumnModel, table: GenTableModel):
data_type = cls.get_db_type(column.column_type)
column_name = column.column_name
column.table_id = table.table_id
# 设置python字段名
column.python_field = column_name
# 设置默认类型
column.python_type = GenConstants.MYSQL_TO_PYTHON.get(data_type.upper(), "Any")
column.query_type = GenConstants.QUERY_EQ
if data_type in GenConstants.TYPE_STRING or data_type in GenConstants.TYPE_TEXT:
# 字符串长度超过500设置为文本域
column_length = cls.get_column_length(column.column_type)
html_type = GenConstants.HTML_TEXTAREA if column_length >= 500 or (data_type in GenConstants.TYPE_TEXT) \
else GenConstants.HTML_INPUT
column.html_type = html_type
elif data_type in GenConstants.TYPE_DATE_TIME:
column.html_type = GenConstants.HTML_DATETIME
elif data_type in GenConstants.TYPE_NUMBER:
column.html_type = GenConstants.HTML_INPUT
# 插入字段
if column.column_name not in GenConstants.COLUMN_NAME_NOT_EDIT and not column.is_pk == '1':
column.is_insert = GenConstants.REQUIRE
# 编辑字段
if column.column_name not in GenConstants.COLUMN_NAME_NOT_EDIT and not column.is_pk == '1':
column.is_edit = GenConstants.REQUIRE
# 列表字段
if column.column_name not in GenConstants.COLUMN_NAME_NOT_LIST and not column.is_pk == '1':
column.is_list = GenConstants.REQUIRE
# 查询字段
if column.column_name not in GenConstants.COLUMN_NAME_NOT_QUERY and not column.is_pk == '1':
column.is_query = GenConstants.REQUIRE
@classmethod
def convert_html_type(cls, column_name: str) -> str:
# 状态字段初始化
if column_name.lower().endswith('_status'):
return GenConstants.HTML_RADIO
# 类型字段初始化
elif column_name.lower().endswith('_type'):
return GenConstants.HTML_SELECT
# 内容字段初始化
elif column_name.lower().endswith('_content'):
return GenConstants.HTML_EDITOR
# 文件字段初始化
elif column_name.lower().endswith('_file'):
return GenConstants.HTML_FILE_UPLOAD
# 图片字段初始化
elif column_name.lower().endswith('_image'):
return GenConstants.HTML_IMAGE_UPLOAD
else:
return GenConstants.HTML_INPUT
@classmethod
def get_db_type(cls, column_type):
# 解析数据库类型逻辑,示例返回列的类型
return column_type.split('(')[0]
@classmethod
def get_column_length(cls, column_type):
# 获取列的长度逻辑,这里简化为返回一个默认值
if '(' in column_type:
return int(column_type.split('(')[1].split(')')[0])
return 0
@classmethod
def convert_class_name(cls, table_name: str) -> str:
"""表名转换成Java类名"""
return ''.join(word.title() for word in table_name.lower().split('_'))
@classmethod
def convert_python_field(cls, column_name: str) -> str:
"""列名转换成Python属性名"""
# words = column_name.lower().split('_')
# return words[0] + ''.join(word.title() for word in words[1:])
return column_name.lower()
@classmethod
def get_package_name(cls, table_name: str) -> str:
"""获取包名"""
return "module_admin" # 可配置的包名
@classmethod
def get_module_name(cls, table_name: str) -> str:
"""获取模块名"""
return table_name.split('_')[0]
@classmethod
def get_business_name(cls, table_name: str) -> str:
"""获取业务名"""
words = table_name.split('_')
return words[1] if len(words) > 1 else words[0]
@classmethod
def get_template_path(cls, tpl_category: str) -> Dict[str, str]:
"""获取模板信息"""
templates = {
# Python相关模板
'controller.py': 'python/controller_template.j2',
'do.py': 'python/model_do_template.j2',
'vo.py': 'python/model_vo_template.j2',
'service.py': 'python/service_template.j2',
'dao.py': 'python/dao_template.j2',
# Vue相关模板
'index.vue': 'vue/index.vue.j2',
'api.js': 'vue/api.js.j2',
# SQL脚本模板
'sql': 'sql/sql.j2',
}
# 树表特殊处理
# if tpl_category == "tree":
# templates.update({
# 'entity': 'java/tree_entity.java.vm',
# 'mapper': 'java/tree_mapper.java.vm',
# 'service': 'java/tree_service.java.vm',
# 'service_impl': 'java/tree_service_impl.java.vm',
# 'controller': 'java/tree_controller.java.vm'
# })
return templates
@classmethod
def get_file_name(cls, template_name: str, table) -> str:
"""获取文件名"""
target_file_name = "unknown_file_name"
if template_name.endswith("controller.py"):
target_file_name = f"python/controller/{table.table_name}_{template_name}"
elif template_name.endswith("do.py"):
target_file_name = f"python/entity/do/{table.table_name}_{template_name}"
elif template_name.endswith("vo.py"):
target_file_name = f"python/entity/vo/{table.table_name}_{template_name}"
elif template_name.endswith("service.py"):
target_file_name = f"python/service/{table.table_name}_{template_name}"
elif template_name.endswith("dao.py"):
target_file_name = f"python/dao/{table.table_name}_{template_name}"
elif template_name.endswith('index.vue'):
target_file_name = f'vue/views/{table.module_name}/{table.business_name}/index.vue'
if template_name.endswith('api.js'):
target_file_name = f'vue/api/{table.module_name}/{table.business_name}.js'
if template_name.endswith('sql'):
target_file_name = f'sql/{table.business_name}.sql'
return target_file_name
@@ -1,53 +0,0 @@
import re
from module_gen.constants.gen_constants import GenConstants
def snake_to_pascal_case(value):
"""将下划线命名 (snake_case) 转换大驼峰"""
return ''.join(word.capitalize() for word in value.split('_'))
def snake_to_camel(snake_str):
"""将下划线命名 (snake_case) 转换小驼峰"""
components = snake_str.split('_')
return components[0] + ''.join(x.title() for x in components[1:])
def snake_2_colon(snake_str: str) -> str:
"""将下划线命名 (snake_case) 转换冒号分隔"""
return snake_str.replace('_', ':')
def is_base_column(column_name: str) -> bool:
"""判断是否是基础字段"""
return column_name in GenConstants.BASE_ENTITY
def get_sqlalchemy_type(mysql_field_type: str) -> str:
"""mysql_field_type 转sqlalchemy类型"""
if mysql_field_type:
base_type = mysql_field_type.split("(", 1)[0]
if base_type.upper() in GenConstants.MYSQL_TO_SQLALCHEMY.keys():
sqlalchemy_type = GenConstants.MYSQL_TO_SQLALCHEMY[base_type.upper()]
if sqlalchemy_type == 'String' :
match = re.search(r'\((.*?)\)', mysql_field_type)
if match:
return f'{sqlalchemy_type}({match.group(1)})'
else:
return f'{sqlalchemy_type}'
else:
return f'{sqlalchemy_type}'
return "String"
def get_column_options(col) -> str:
options = []
# 主键
if col['isPk'] == "1":
options.append("primary_key=True")
# 是否允许为空
if col['isRequired'] == "1":
options.append("nullable=False")
# 自增
if col["isIncrement"] == "1":
options.append("autoincrement=True")
# 注释
options.append(f"comment='{col['columnComment']}'")
return ", ".join(options)
@@ -1,45 +0,0 @@
from jinja2 import Environment, FileSystemLoader, select_autoescape
import os
from module_gen.utils.jinja2_tools import snake_to_pascal_case, snake_to_camel, snake_2_colon, is_base_column, \
get_sqlalchemy_type, get_column_options
class VelocityInitializer:
"""模板引擎初始化器"""
@staticmethod
def init_velocity() -> Environment:
"""初始化模板引擎"""
try:
# 设置模板加载器
template_dir = os.path.abspath(os.path.join(os.getcwd(), 'module_gen/templates'))
loader = FileSystemLoader(template_dir)
# 创建Jinja2环境
env = Environment(
loader=loader,
autoescape=select_autoescape(['html', 'xml']),
trim_blocks=True,
lstrip_blocks=True
)
# 添加自定义过滤器
env.filters.update({
'capitalize': lambda x: x.capitalize(),
'lower': lambda x: x.lower(),
'upper': lambda x: x.upper(),
'camelcase': lambda x: ''.join(word.title() for word in x.split('_')),
'snakecase': lambda x: '_'.join(x.lower().split()),
'snake_to_pascal_case': snake_to_pascal_case,
'snake_to_camel': snake_to_camel,
'snake_2_colon': snake_2_colon,
'is_base_column': is_base_column,
'get_sqlalchemy_type': get_sqlalchemy_type,
'get_column_options': get_column_options,
})
return env
except Exception as e:
raise RuntimeError(f"初始化模板引擎失败: {str(e)}")
@@ -1,198 +0,0 @@
from typing import Dict, Any, List
from jinja2 import Template
from module_gen.constants.gen_constants import GenConstants
from module_gen.dao.gen_table_column_dao import GenTableColumnDao
from module_gen.dao.gen_table_dao import GenTableDao
from module_gen.entity.do.gen_table_column_do import GenTableColumn
from module_gen.entity.do.gen_table_do import GenTable
from module_gen.entity.vo.gen_table_column_vo import GenTableColumnPageModel, GenTableColumnModel
from module_gen.entity.vo.gen_table_vo import GenTableModel
from module_gen.utils.velocity_initializer import VelocityInitializer
import os
from utils.common_util import CamelCaseUtil
class VelocityUtils:
"""模板处理工具类"""
# 默认上级菜单,系统工具
DEFAULT_PARENT_MENU_ID = "3"
# 环境对象
_env = None
@classmethod
def get_env(cls):
"""获取模板环境对象"""
if cls._env is None:
cls._env = VelocityInitializer.init_velocity()
return cls._env
@classmethod
def get_template(cls, template_path: str) -> Template:
"""获取模板"""
return cls.get_env().get_template(template_path)
@classmethod
async def get_render_params(cls, gen_table: GenTableModel, query_db) -> Dict[str, Any]:
"""设置模板变量信息"""
# 设置python文件路径
sub_table = None
if gen_table.sub_table_name:
# 子表信息
sub_table = await GenTableDao.get_by_table_name(query_db, gen_table.sub_table_name)
# 设置主子表信息
await cls.set_sub_table_value(query_db, gen_table, sub_table)
# 设置主键列信息
table_columns_dicts = await GenTableColumnDao.get_gen_table_column_list(query_db, GenTableColumnPageModel(tableId=gen_table.table_id))
table_columns = [GenTableColumnModel(**tcd) for tcd in table_columns_dicts]
pk_column = None
for column in table_columns:
if column.is_pk == "1":
pk_column = column
break
context = {
# 文件名称
"tableName": gen_table.table_name,
# 小写类名
"className": gen_table.class_name.lower(),
# 大写类名
"ClassName": gen_table.class_name,
# 包路径
"packageName": gen_table.package_name,
# 模块名
"moduleName": gen_table.module_name,
# 业务名
"businessName": gen_table.business_name,
# 业务名(首字母大写)
"BusinessName": gen_table.business_name.capitalize(),
# 功能名称
"functionName": gen_table.function_name,
# 作者
"author": gen_table.function_author,
# 主键字段
"pkColumn": pk_column.model_dump(by_alias=True) if pk_column else None,
# 导入sqlalchemy需要导入的类型字段
"importList": cls.get_import_list(table_columns),
# 列集合
"columns": [tcn.model_dump(by_alias=True) for tcn in table_columns],
# 生成路径
"genPath": gen_table.gen_path,
# 表描述
"tableComment": gen_table.table_comment,
# 权限前缀
"permissionPrefix": cls.get_permission_prefix(gen_table.module_name, gen_table.business_name),
# 是否包含主键
"hasPk": cls.has_pk_column(table_columns),
# 是否包含Bigdecimal
"hasBigDecimal": cls.has_column_big_decimal(table_columns),
# 是否包含时间类型
"hasDateTime": cls.has_column_datetime(table_columns),
# 主键是否自增
"auto": cls.is_pk_auto(table_columns),
# 父级菜单ID
"parentMenuId": gen_table.parent_menu_id,
# 字段关联的字典名
"dicts": cls.get_column_related_dicts(table_columns),
}
if gen_table.tpl_category == "tree":
context.update({
"treeCode": gen_table.tree_code,
"treeParentCode": gen_table.tree_parent_code,
"treeName": gen_table.tree_name,
"expandColumn": gen_table.tree_name,
"tree_parent_code": gen_table.tree_parent_code,
"tree_name": gen_table.tree_name
})
if gen_table.tpl_category == "sub":
context.update({
"subTable": sub_table,
"subTableName": gen_table.sub_table_name,
"subTableFkName": gen_table.sub_table_fk_name,
"subClassName": sub_table.class_name,
"subclassName": sub_table.class_name.lower(),
"subImportList": cls.get_import_list(sub_table.columns)
})
return context
@classmethod
def get_permission_prefix(cls, module_name: str, business_name: str) -> str:
"""获取权限前缀"""
return f"{module_name}:{business_name}"
@classmethod
async def set_sub_table_value(cls, query_db, gen_table: GenTableModel, sub_table: GenTable):
"""设置主子表信息"""
table_columns = await GenTableColumnDao.get_list_by_table_id(query_db, sub_table.table_id)
for column in table_columns:
if column.is_pk == "1":
gen_table.pk_column = column
break
@classmethod
def get_import_list(cls, table_columns: List[GenTableColumnModel]) -> str:
"""获取需要导入的包列表"""
sqlalchemy_types = []
for i, table_column in enumerate(table_columns):
if table_column.column_type:
mysql_type = table_column.column_type.split("(")[0]
if mysql_type.upper() in GenConstants.MYSQL_TO_SQLALCHEMY.keys():
temp_type = GenConstants.MYSQL_TO_SQLALCHEMY[mysql_type.upper()]
if temp_type not in sqlalchemy_types:
sqlalchemy_types.append(temp_type)
return ", ".join(sqlalchemy_types)
@staticmethod
def has_column_datetime(columns: List[GenTableColumnModel]) -> bool:
"""判断是否包含datetime"""
return any(column.python_type == "Date" for column in columns)
@staticmethod
def has_column_big_decimal(columns: List[GenTableColumnModel]) -> bool:
"""判断是否包含BigDecimal"""
return any(column.python_type == "BigDecimal" for column in columns)
@staticmethod
def has_pk_column(columns: List[GenTableColumnModel]) -> bool:
"""判断是否包含主键"""
return any(column.is_pk == "1" for column in columns)
@staticmethod
def is_pk_auto(columns: List[GenTableColumnModel]) -> bool:
"""判断主键是否自增"""
return any(column.is_pk == "1" and column.is_increment == "1" for column in columns)
@classmethod
def write_file(cls, template_path: str, context: Dict[str, Any], file_path: str) -> None:
"""渲染模板并写入文件"""
try:
# 获取生成文件的目录
out_dir = os.path.dirname(file_path)
if not os.path.exists(out_dir):
os.makedirs(out_dir)
# 渲染模板
template = cls.get_template(template_path)
content = template.render(**context)
# 写入文件
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
except Exception as e:
raise RuntimeError(f"渲染模板失败,模板路径:{template_path}") from e
@classmethod
def get_column_related_dicts(cls, table_columns) -> str:
dicts = []
for table_column in table_columns:
if table_column.dict_type:
dicts.append(f"'{table_column.dict_type}'")
return ", ".join(dicts)
@@ -9,6 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from redis.asyncio.client import Redis
from app.common.enums import RedisInitKeyConfig
from app.core.database import AsyncSessionLocal
from app.core.redis_crud import RedisCURD
from app.utils.excel_util import ExcelUtil
from app.utils.upload_util import UploadUtil
@@ -158,27 +159,28 @@ class ConfigService:
).model_dump()
@classmethod
async def init_config_service(cls, redis: Redis, db: AsyncSession) -> bool:
auth = AuthSchema(db=db)
config_obj = await ConfigCRUD(auth).get_obj_list_crud()
if not config_obj:
raise CustomException(msg="系统配置不存在")
try:
# 保存到Redis并设置过期时间
for config in config_obj:
redis_key = (f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:{config.config_key}")
config_obj_dict = ConfigOutSchema.model_validate(config).model_dump()
value = json.dumps(config_obj_dict, ensure_ascii=False)
result = await RedisCURD(redis).set(
key=redis_key,
value=value,
)
if not result:
logger.error(f"初始化系统配置失败: {config_obj_dict}")
raise CustomException(msg="初始化系统配置失败")
except Exception as e:
logger.error(f"初始化系统配置失败: {e}")
raise CustomException(msg="初始化系统配置失败")
async def init_config_service(cls, redis: Redis) -> bool:
async with AsyncSessionLocal() as session:
auth = AuthSchema(db=session)
config_obj = await ConfigCRUD(auth).get_obj_list_crud()
if not config_obj:
raise CustomException(msg="系统配置不存在")
try:
# 保存到Redis并设置过期时间
for config in config_obj:
redis_key = (f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:{config.config_key}")
config_obj_dict = ConfigOutSchema.model_validate(config).model_dump()
value = json.dumps(config_obj_dict, ensure_ascii=False)
result = await RedisCURD(redis).set(
key=redis_key,
value=value,
)
if not result:
logger.error(f"初始化系统配置失败: {config_obj_dict}")
raise CustomException(msg="初始化系统配置失败")
except Exception as e:
logger.error(f"初始化系统配置失败: {e}")
raise CustomException(msg="初始化系统配置失败")
@classmethod
async def get_init_config_service(cls, redis: Redis) -> Dict:
@@ -17,9 +17,12 @@ class DeptModel(ModelMixin):
name: Mapped[str] = mapped_column(String(40),nullable=False,unique=True,comment="部门名称")
order: Mapped[int] = mapped_column(Integer,nullable=False,default=999,comment="显示排序")
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("system_dept.id", ondelete="CASCADE", onupdate="CASCADE"), nullable=True, index=True, comment="父级部门ID")
parent: Mapped[Optional["DeptModel"]] = relationship("DeptModel", cascade="all, delete-orphan", uselist=False)
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("system_dept.id", ondelete="SET NULL", onupdate="CASCADE"), default=None, index=True, comment="父级部门ID")
# parent: Mapped[Optional["DeptModel"]] = relationship("DeptModel", cascade="all, delete-orphan", uselist=False)
parent: Mapped[Optional['DeptModel']] = relationship(init=False, back_populates='children', remote_side=[id])
children: Mapped[Optional[list['DeptModel']]] = relationship(init=False, back_populates='parent')
# 角色关联关系
roles: Mapped[List["RoleModel"]] = relationship(secondary="system_role_depts", back_populates="depts", lazy="selectin")
@@ -27,6 +30,4 @@ class DeptModel(ModelMixin):
users: Mapped[List["UserModel"]] = relationship(back_populates="dept", lazy="selectin")
# code: Mapped[Optional[str]] = mapped_column(String(20),nullable=True,unique=True,comment="部门编码")
# leader_id: Mapped[Optional[int]] = mapped_column(Integer,nullable=True,comment="负责人ID")
# parent: Mapped[Optional["DeptModel"]] = relationship(back_populates="children",remote_side=[id],lazy="select",uselist=False,foreign_keys=[parent_id])
# children: Mapped[List["DeptModel"]] = relationship(back_populates="parent",lazy="select",cascade="all, delete-orphan",foreign_keys=[parent_id])
# leader_id: Mapped[Optional[int]] = mapped_column(Integer,nullable=True,comment="负责人ID")
@@ -7,6 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.common.enums import RedisInitKeyConfig
from app.utils.excel_util import ExcelUtil
from app.core.database import AsyncSessionLocal
from app.core.base_schema import BatchSetAvailable
from app.core.redis_crud import RedisCURD
from app.core.exceptions import CustomException
@@ -187,33 +188,33 @@ class DictDataService:
@classmethod
async def init_dict_service(cls, redis: Redis, db: AsyncSession):
"""应用初始化: 获取所有字典类型对应的字典数据信息并缓存service"""
auth = AuthSchema(db=db)
obj_list = await DictTypeCRUD(auth).get_obj_list_crud()
if not obj_list:
logger.warning("未找到任何字典类型数据")
return
for obj in obj_list:
dict_type = obj.dict_type
dict_data_list = await DictDataCRUD(auth).get_obj_list_crud(search={'dict_type': dict_type})
if not dict_data_list:
logger.warning(f"字典类型 {dict_type} 未找到对应的字典数据")
continue
dict_data = [DictDataOutSchema.model_validate(row).model_dump() for row in dict_data_list if row]
# 保存到Redis并设置过期时间
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{dict_type}"
try:
value = json.dumps(dict_data, ensure_ascii=False)
await RedisCURD(redis).set(
key=redis_key,
value=value,
)
except Exception as e:
logger.error(f"初始化字典数据失败: {e}")
raise CustomException(msg=f"初始化字典数据失败 {e}")
async with AsyncSessionLocal() as session:
auth = AuthSchema(db=session)
obj_list = await DictTypeCRUD(auth).get_obj_list_crud()
if not obj_list:
logger.warning("未找到任何字典类型数据")
return
for obj in obj_list:
dict_type = obj.dict_type
dict_data_list = await DictDataCRUD(auth).get_obj_list_crud(search={'dict_type': dict_type})
if not dict_data_list:
logger.warning(f"字典类型 {dict_type} 未找到对应的字典数据")
continue
dict_data = [DictDataOutSchema.model_validate(row).model_dump() for row in dict_data_list if row]
# 保存到Redis并设置过期时间
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{dict_type}"
try:
value = json.dumps(dict_data, ensure_ascii=False)
await RedisCURD(redis).set(
key=redis_key,
value=value,
)
except Exception as e:
logger.error(f"初始化字典数据失败: {e}")
raise CustomException(msg=f"初始化字典数据失败 {e}")
@classmethod
async def get_init_dict_service(cls, redis: Redis, dict_type: str)->List[Dict]:
@@ -42,12 +42,12 @@ class MenuModel(ModelMixin):
affix: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, comment='是否固定标签页(True:是 False:否)')
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey('system_menu.id', ondelete='SET NULL'), default=None, index=True, comment='父菜单ID')
parent: Mapped[Optional['MenuModel']] = relationship(cascade='all, delete-orphan', primaryjoin="MenuModel.parent_id == MenuModel.id", uselist=False)
# parent: Mapped[Optional['MenuModel']] = relationship(cascade='all, delete-orphan', primaryjoin="MenuModel.parent_id == MenuModel.id", uselist=False)
# 角色关联关系
roles: Mapped[List["RoleModel"]] = relationship(secondary="system_role_menus", back_populates="menus", lazy="selectin")
# link: Mapped[Optional[str]] = mapped_column(String(255), comment='外链地址')
# iframe: Mapped[Optional[str]] = mapped_column(String(255), comment='内嵌iframe地址')
# parent: Mapped[Optional['MenuModel']] = relationship(init=False, back_populates='children', remote_side=[id])
# children: Mapped[Optional[list['MenuModel']]] = relationship(init=False, back_populates='parent')
parent: Mapped[Optional['MenuModel']] = relationship(init=False, back_populates='children', remote_side=[id])
children: Mapped[Optional[list['MenuModel']]] = relationship(init=False, back_populates='parent')