mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-20 20:39:55 +00:00
refactor(module): 重构模块路径和数据库配置
- 将前端API路径从`system`和`monitor`重命名为`module_system`和`module_monitor` - 移除MongoDB相关配置和依赖 - 统一数据库类型命名为`mysql`和`postgres` - 修复代码生成模板中的字段命名问题 - 更新依赖项并移除不必要的包 - 优化数据库连接配置 - 添加新的文档和Redoc视图组件 - 修复SQL脚本中的字段注释
This commit is contained in:
@@ -1,21 +1,20 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, Query, Body, Path
|
||||
from fastapi import APIRouter, Depends, Body, Path
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.common.response import SuccessResponse, ErrorResponse, StreamResponse
|
||||
from app.common.response import SuccessResponse, StreamResponse
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.common.request import PaginationService
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.common.constant import RET
|
||||
from .param import GenTableQueryParam
|
||||
from .schema import GenTableSchema, GenTableOutSchema
|
||||
from .service import GenTableColumnService, GenTableService
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.logger import logger
|
||||
from .param import GenTableQueryParam
|
||||
from .schema import GenTableSchema
|
||||
from .service import GenTableService
|
||||
|
||||
|
||||
GenRouter = APIRouter(route_class=OperationLogRoute, prefix='/gencode', tags=["代码生成模块"])
|
||||
@@ -110,7 +109,7 @@ async def gen_table_detail_controller(
|
||||
|
||||
@GenRouter.post("/create", summary="创建表结构", description="创建表结构")
|
||||
async def create_table_controller(
|
||||
sql: str = Body(..., description="SQL语句:CREATE TABLE user_demo (\n id INTEGER NOT NULL PRIMARY KEY,\n username VARCHAR(64) NOT NULL UNIQUE,\n);"),
|
||||
sql: str = Body(..., description="SQL语句,用于创建表结构"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["generator:gencode:create"])),
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
from sqlalchemy.engine.row import Row
|
||||
from sqlalchemy import and_, select, text
|
||||
from typing import List, Optional, Sequence, Dict, Union, Any
|
||||
from sqlglot.expressions import Expression
|
||||
|
||||
from app.core.logger import logger
|
||||
from app.config.setting import settings
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .param import GenTableQueryParam, GenTableColumnQueryParam
|
||||
from .param import GenTableQueryParam
|
||||
from .model import GenTableModel, GenTableColumnModel
|
||||
from .schema import (
|
||||
GenTableSchema,
|
||||
GenTableOutSchema,
|
||||
GenTableColumnSchema,
|
||||
GenTableColumnOutSchema,
|
||||
GenDBTableSchema,
|
||||
@@ -80,7 +80,6 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
||||
返回:
|
||||
- Sequence[GenTableModel]: 业务表列表信息。
|
||||
"""
|
||||
# 使用基础CRUD的list与like检索
|
||||
return await self.list(search=search.__dict__, order_by=[{"created_at": "desc"}], preload=preload)
|
||||
|
||||
async def add_gen_table(self, add_model: GenTableSchema) -> GenTableModel:
|
||||
@@ -93,9 +92,9 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
||||
返回:
|
||||
- GenTableModel: 新增的业务表信息对象。
|
||||
"""
|
||||
return await self.create(add_model.model_dump(exclude_unset=True, exclude={"sub", "tree", "crud"}))
|
||||
return await self.create(data=add_model)
|
||||
|
||||
async def edit_gen_table(self, table_id: int, edit_model: GenTableSchema) -> GenTableSchema:
|
||||
async def edit_gen_table(self, table_id: int, edit_model: GenTableSchema) -> GenTableModel:
|
||||
"""
|
||||
修改业务表信息。
|
||||
|
||||
@@ -107,9 +106,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
||||
- GenTableSchema: 修改后的业务表信息模型。
|
||||
"""
|
||||
# 排除嵌套对象字段,避免SQLAlchemy尝试直接将字典设置到模型实例上
|
||||
data_dict = edit_model.model_dump(exclude_unset=True, exclude={"columns", "pk_column", "sub_table", "sub"})
|
||||
obj = await self.update(id=table_id, data=data_dict)
|
||||
return GenTableSchema.model_validate(obj)
|
||||
return await self.update(id=table_id, data=edit_model.model_dump(exclude_unset=True, exclude={"columns"}))
|
||||
|
||||
async def delete_gen_table(self, ids: List[int]) -> None:
|
||||
"""
|
||||
@@ -132,8 +129,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
||||
"""
|
||||
|
||||
# 使用更健壮的方式检测数据库方言
|
||||
if settings.DATABASE_TYPE == "postgresql":
|
||||
# 修复:PostgreSQL不提供table_comment,使用pg_catalog获取注释
|
||||
if settings.DATABASE_TYPE == "postgres":
|
||||
query_sql = (
|
||||
select(
|
||||
text("t.table_catalog as database_name"),
|
||||
@@ -155,7 +151,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
||||
)
|
||||
)
|
||||
)
|
||||
elif settings.DATABASE_TYPE == "mysql":
|
||||
else:
|
||||
query_sql = (
|
||||
select(
|
||||
text("table_schema as database_name"),
|
||||
@@ -170,38 +166,19 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
query_sql = (
|
||||
select(
|
||||
text("'fastapiadmin' as database_name"), # SQLite没有数据库名概念,设为空字符串
|
||||
text("name as table_name"),
|
||||
text("type as table_type"),
|
||||
text("name as table_comment"), # SQLite中使用name作为表名和注释
|
||||
)
|
||||
.select_from(text("sqlite_master"))
|
||||
.where(
|
||||
and_(
|
||||
text("type = 'table'"),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# 动态条件构造
|
||||
params = {}
|
||||
if search and search.table_name:
|
||||
if settings.DATABASE_TYPE == "sqlite":
|
||||
query_sql = query_sql.where(
|
||||
text("lower(name) like lower(:table_name)")
|
||||
)
|
||||
else:
|
||||
query_sql = query_sql.where(
|
||||
text("lower(table_name) like lower(:table_name)")
|
||||
)
|
||||
query_sql = query_sql.where(
|
||||
text("lower(table_name) like lower(:table_name)")
|
||||
)
|
||||
params['table_name'] = f"%{search.table_name}%"
|
||||
if search and search.table_comment:
|
||||
if settings.DATABASE_TYPE == "sqlite":
|
||||
# 对于PostgreSQL,表注释字段是pd.description,而不是table_comment
|
||||
if settings.DATABASE_TYPE == "postgres":
|
||||
query_sql = query_sql.where(
|
||||
text("lower(name) like lower(:table_comment)")
|
||||
text("lower(pd.description) like lower(:table_comment)")
|
||||
)
|
||||
else:
|
||||
query_sql = query_sql.where(
|
||||
@@ -235,78 +212,58 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
||||
返回:
|
||||
- list[GenDBTableSchema]: 数据库表信息对象列表。
|
||||
"""
|
||||
# 处理空列表情况
|
||||
if not table_names:
|
||||
return []
|
||||
|
||||
# 使用更健壮的方式检测数据库方言
|
||||
if settings.DATABASE_TYPE == "postgresql":
|
||||
# 修复:PostgreSQL不提供table_comment,使用pg_catalog获取注释
|
||||
query_sql = (
|
||||
select(
|
||||
text("t.table_catalog as database_name"),
|
||||
text("t.table_name as table_name"),
|
||||
text("t.table_type as table_type"),
|
||||
text("pd.description as table_comment"),
|
||||
)
|
||||
.select_from(text(
|
||||
"information_schema.tables t \n"
|
||||
"LEFT JOIN pg_catalog.pg_class c ON c.relname = t.table_name \n"
|
||||
"LEFT JOIN pg_catalog.pg_namespace n ON n.nspname = t.table_schema AND c.relnamespace = n.oid \n"
|
||||
"LEFT JOIN pg_catalog.pg_description pd ON pd.objoid = c.oid AND pd.objsubid = 0"
|
||||
))
|
||||
.where(
|
||||
and_(
|
||||
text("t.table_catalog = (select current_database())"),
|
||||
text("t.is_insertable_into = 'YES'"),
|
||||
text("t.table_schema = 'public'"),
|
||||
)
|
||||
)
|
||||
)
|
||||
elif settings.DATABASE_TYPE == "mysql":
|
||||
query_sql = (
|
||||
select(
|
||||
text("table_schema as database_name"),
|
||||
text("table_name as table_name"),
|
||||
text("table_type as table_type"),
|
||||
text("table_comment as table_comment"),
|
||||
)
|
||||
.select_from(text("information_schema.tables"))
|
||||
.where(
|
||||
and_(
|
||||
text("table_schema = (select database())"),
|
||||
)
|
||||
)
|
||||
)
|
||||
if settings.DATABASE_TYPE == "postgres":
|
||||
# PostgreSQL使用ANY操作符和正确的参数绑定
|
||||
query_sql = """
|
||||
SELECT
|
||||
t.table_catalog as database_name,
|
||||
t.table_name as table_name,
|
||||
t.table_type as table_type,
|
||||
pd.description as table_comment
|
||||
FROM
|
||||
information_schema.tables t
|
||||
LEFT JOIN pg_catalog.pg_class c ON c.relname = t.table_name
|
||||
LEFT JOIN pg_catalog.pg_namespace n ON n.nspname = t.table_schema AND c.relnamespace = n.oid
|
||||
LEFT JOIN pg_catalog.pg_description pd ON pd.objoid = c.oid AND pd.objsubid = 0
|
||||
WHERE
|
||||
t.table_catalog = (select current_database())
|
||||
AND t.is_insertable_into = 'YES'
|
||||
AND t.table_schema = 'public'
|
||||
AND t.table_name = ANY(:table_names)
|
||||
"""
|
||||
else:
|
||||
query_sql = (
|
||||
select(
|
||||
text("'fastapiadmin' as database_name"), # SQLite没有数据库名概念,设为空字符串
|
||||
text("name as table_name"),
|
||||
text("type as table_type"),
|
||||
text("name as table_comment"), # SQLite中使用name作为表名和注释
|
||||
)
|
||||
.select_from(text("sqlite_master"))
|
||||
.where(
|
||||
and_(
|
||||
text("type = 'table'"),
|
||||
)
|
||||
)
|
||||
)
|
||||
query_sql = """
|
||||
SELECT
|
||||
table_schema as database_name,
|
||||
table_name as table_name,
|
||||
table_type as table_type,
|
||||
table_comment as table_comment
|
||||
FROM
|
||||
information_schema.tables
|
||||
WHERE
|
||||
table_schema = (select database())
|
||||
AND table_name IN :table_names
|
||||
"""
|
||||
|
||||
table_names_str = "','".join(table_names)
|
||||
# 修复SQL查询中的参数绑定问题
|
||||
if table_names:
|
||||
if settings.DATABASE_TYPE == "sqlite":
|
||||
# 对于SQLite,我们直接在SQL中使用表名,因为参数绑定有问题
|
||||
query_sql = query_sql.where(
|
||||
text(f"name IN ('{table_names_str}')")
|
||||
)
|
||||
gen_db_table_list = (await self.db.execute(query_sql)).fetchall()
|
||||
# 创建新的数据库会话上下文来执行查询,避免受外部事务状态影响
|
||||
try:
|
||||
# 去重表名列表,避免重复查询
|
||||
unique_table_names = list(set(table_names))
|
||||
|
||||
# 使用只读事务执行查询,不影响主事务
|
||||
if settings.DATABASE_TYPE == "postgres":
|
||||
gen_db_table_list = (await self.db.execute(text(query_sql), {"table_names": unique_table_names})).fetchall()
|
||||
else:
|
||||
# MySQL和PostgreSQL使用IN拼接(注意已在上方限定schema范围)
|
||||
query_sql = query_sql.where(
|
||||
text(f"table_name IN ('{table_names_str}')")
|
||||
)
|
||||
gen_db_table_list = (await self.db.execute(query_sql)).fetchall()
|
||||
else:
|
||||
gen_db_table_list = (await self.db.execute(query_sql)).fetchall()
|
||||
gen_db_table_list = (await self.db.execute(text(query_sql), {"table_names": tuple(unique_table_names)})).fetchall()
|
||||
except Exception as e:
|
||||
logger.error(f"查询表信息时发生错误: {e}")
|
||||
# 查询错误时直接抛出,不需要事务处理
|
||||
raise
|
||||
|
||||
# 将Row对象转换为字典列表,解决JSON序列化问题
|
||||
dict_data = []
|
||||
@@ -335,13 +292,8 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
||||
# 根据不同数据库类型使用不同的查询方式
|
||||
if settings.DATABASE_TYPE.lower() == 'mysql':
|
||||
query = text("SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = :table_name")
|
||||
elif settings.DATABASE_TYPE.lower() == 'postgresql':
|
||||
query = text("SELECT 1 FROM pg_tables WHERE tablename = :table_name")
|
||||
elif settings.DATABASE_TYPE.lower() == 'sqlite':
|
||||
query = text("SELECT name FROM sqlite_master WHERE type='table' AND name = :table_name")
|
||||
else:
|
||||
# 默认查询方式
|
||||
query = text("SELECT 1 FROM information_schema.tables WHERE table_name = :table_name")
|
||||
query = text("SELECT 1 FROM pg_tables WHERE tablename = :table_name")
|
||||
|
||||
result = await self.db.execute(query, {"table_name": table_name})
|
||||
return result.scalar() is not None
|
||||
@@ -350,7 +302,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
||||
# 出错时返回False,避免误报表已存在
|
||||
return False
|
||||
|
||||
async def create_table_by_sql(self, sql: str) -> bool:
|
||||
async def create_table_by_sql(self, sql_statements: List[Expression | None]) -> bool:
|
||||
"""
|
||||
根据SQL语句创建表结构。
|
||||
|
||||
@@ -360,10 +312,15 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
||||
返回:
|
||||
- bool: 是否创建成功。
|
||||
"""
|
||||
|
||||
|
||||
try:
|
||||
# 执行SQL但不手动提交事务,由框架管理事务生命周期
|
||||
await self.db.execute(text(sql))
|
||||
await self.db.flush()
|
||||
for sql_statement in sql_statements:
|
||||
if not sql_statement:
|
||||
continue
|
||||
sql = sql_statement.sql(dialect=settings.DATABASE_TYPE)
|
||||
await self.db.execute(text(sql))
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"创建表时发生错误: {e}")
|
||||
@@ -433,93 +390,105 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
|
||||
if not table_name:
|
||||
raise ValueError("数据表名称不能为空")
|
||||
|
||||
# 兼容SQLite和MySQL/PostgreSQL
|
||||
if settings.DATABASE_TYPE == "postgresql":
|
||||
# 修复:PostgreSQL的主键/自增/注释需要关联系统表
|
||||
query_sql = """
|
||||
SELECT
|
||||
c.column_name,
|
||||
(CASE WHEN (c.is_nullable = 'NO' AND (tc.constraint_type IS DISTINCT FROM 'PRIMARY KEY')) THEN '1' ELSE '0' END) AS is_required,
|
||||
(CASE WHEN (tc.constraint_type = 'PRIMARY KEY') THEN '1' ELSE '0' END) AS is_pk,
|
||||
(CASE WHEN EXISTS (SELECT 1 FROM information_schema.table_constraints uc JOIN information_schema.key_column_usage kcu ON uc.constraint_name = kcu.constraint_name WHERE uc.table_name = c.table_name AND uc.table_schema = c.table_schema AND uc.constraint_type = 'UNIQUE' AND kcu.column_name = c.column_name) THEN '1' ELSE '0' END) AS is_unique,
|
||||
c.ordinal_position AS sort,
|
||||
COALESCE(pgd.description, '') AS column_comment,
|
||||
(CASE WHEN c.column_default LIKE 'nextval%' THEN '1' ELSE '0' END) AS is_increment,
|
||||
c.udt_name AS column_type,
|
||||
c.character_maximum_length AS column_length,
|
||||
c.column_default AS column_default
|
||||
FROM information_schema.columns c
|
||||
LEFT JOIN information_schema.key_column_usage kcu
|
||||
ON c.table_name = kcu.table_name AND c.column_name = kcu.column_name AND kcu.table_schema = c.table_schema
|
||||
LEFT JOIN information_schema.table_constraints tc
|
||||
ON tc.constraint_name = kcu.constraint_name AND tc.table_name = c.table_name AND tc.table_schema = c.table_schema
|
||||
LEFT JOIN pg_catalog.pg_statio_all_tables st
|
||||
ON st.relname = c.table_name
|
||||
LEFT JOIN pg_catalog.pg_description pgd
|
||||
ON pgd.objoid = st.relid AND pgd.objsubid = c.ordinal_position
|
||||
WHERE c.table_catalog = current_database()
|
||||
AND c.table_schema = 'public'
|
||||
AND c.table_name = :table_name
|
||||
ORDER BY c.ordinal_position
|
||||
"""
|
||||
elif settings.DATABASE_TYPE == "mysql":
|
||||
query_sql = """
|
||||
SELECT
|
||||
c.column_name,
|
||||
(CASE WHEN (c.is_nullable = 'NO' AND c.column_key != 'PRI') THEN '1' ELSE '0' END) AS is_required,
|
||||
(CASE WHEN c.column_key = 'PRI' THEN '1' ELSE '0' END) AS is_pk,
|
||||
(CASE WHEN EXISTS (SELECT 1 FROM information_schema.statistics s WHERE s.table_schema = c.table_schema AND s.table_name = c.table_name AND s.column_name = c.column_name AND s.non_unique = 0 AND s.index_name != 'PRIMARY') THEN '1' ELSE '0' END) AS is_unique,
|
||||
c.ordinal_position AS sort,
|
||||
c.column_comment,
|
||||
(CASE WHEN c.extra = 'auto_increment' THEN '1' ELSE '0' end) AS is_increment,
|
||||
c.column_type,
|
||||
c.character_maximum_length AS column_length,
|
||||
c.column_default AS column_default
|
||||
FROM
|
||||
information_schema.columns c
|
||||
WHERE
|
||||
c.table_schema = (SELECT DATABASE())
|
||||
AND c.table_name = :table_name
|
||||
ORDER BY c.ordinal_position
|
||||
"""
|
||||
else:
|
||||
# 修复SQLite查询语句,使用PRAGMA获取表结构信息
|
||||
query_sql = """
|
||||
SELECT
|
||||
name as column_name,
|
||||
(CASE WHEN (type != '' AND pk != 1) THEN '1' ELSE '0' END) AS is_required,
|
||||
(CASE WHEN pk = 1 THEN '1' ELSE '0' END) AS is_pk,
|
||||
(CASE WHEN (SELECT COUNT(*) FROM pragma_index_list(:table_name) pil JOIN pragma_index_info(pil.name) pii ON 1=1 WHERE pil.unique = 1 AND pii.name = pragma_table_info.name AND pil.name NOT LIKE 'sqlite_%') > 0 THEN '1' ELSE '0' END) AS is_unique,
|
||||
cid AS sort,
|
||||
'' as column_comment,
|
||||
(CASE WHEN type LIKE '%AUTOINCREMENT%' THEN '1' ELSE '0' END) AS is_increment,
|
||||
type as column_type,
|
||||
(CASE WHEN type LIKE 'varchar(%' OR type LIKE 'char(%' THEN substr(type, instr(type, '(') + 1, instr(type, ')') - instr(type, '(') - 1) ELSE NULL END) AS column_length,
|
||||
dflt_value AS column_default
|
||||
FROM
|
||||
pragma_table_info(:table_name)
|
||||
ORDER BY cid
|
||||
"""
|
||||
|
||||
query = text(query_sql).bindparams(table_name=table_name)
|
||||
rows = (await self.db.execute(query)).fetchall()
|
||||
result = [
|
||||
GenTableColumnOutSchema(
|
||||
column_name=row[0],
|
||||
is_required=row[1],
|
||||
is_pk=row[2],
|
||||
is_unique=row[3],
|
||||
sort=row[4],
|
||||
column_comment=row[5],
|
||||
is_increment=row[6],
|
||||
column_type=row[7],
|
||||
column_length=str(row[8]) if row[8] is not None else None,
|
||||
column_default=str(row[9]) if row[9] is not None else None
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
return result
|
||||
try:
|
||||
if settings.DATABASE_TYPE == "mysql":
|
||||
query_sql = """
|
||||
SELECT
|
||||
c.column_name AS column_name,
|
||||
c.column_comment AS column_comment,
|
||||
c.column_type AS column_type,
|
||||
c.character_maximum_length AS column_length,
|
||||
c.column_default AS column_default,
|
||||
c.ordinal_position AS sort,
|
||||
(CASE WHEN c.column_key = 'PRI' THEN 1 ELSE 0 END) AS is_pk,
|
||||
(CASE WHEN c.extra = 'auto_increment' THEN 1 ELSE 0 END) AS is_increment,
|
||||
(CASE WHEN (c.is_nullable = 'NO' AND c.column_key != 'PRI') THEN 1 ELSE 0 END) AS is_nullable,
|
||||
(CASE
|
||||
WHEN c.column_name IN (
|
||||
SELECT k.column_name
|
||||
FROM information_schema.key_column_usage k
|
||||
JOIN information_schema.table_constraints t
|
||||
ON k.constraint_name = t.constraint_name
|
||||
WHERE k.table_schema = c.table_schema
|
||||
AND k.table_name = c.table_name
|
||||
AND t.constraint_type = 'UNIQUE'
|
||||
) THEN 1 ELSE 0
|
||||
END) AS is_unique
|
||||
FROM
|
||||
information_schema.columns c
|
||||
WHERE c.table_schema = (SELECT DATABASE())
|
||||
AND c.table_name = :table_name
|
||||
ORDER BY
|
||||
c.ordinal_position
|
||||
"""
|
||||
else:
|
||||
query_sql = """
|
||||
SELECT
|
||||
c.column_name AS column_name,
|
||||
COALESCE(pgd.description, '') AS column_comment,
|
||||
c.udt_name AS column_type,
|
||||
c.character_maximum_length AS column_length,
|
||||
c.column_default AS column_default,
|
||||
c.ordinal_position AS sort,
|
||||
(CASE WHEN EXISTS (
|
||||
SELECT 1 FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name
|
||||
WHERE tc.table_name = c.table_name
|
||||
AND tc.constraint_type = 'PRIMARY KEY'
|
||||
AND ccu.column_name = c.column_name
|
||||
) THEN 1 ELSE 0 END) AS is_pk,
|
||||
(CASE WHEN c.column_default LIKE 'nextval%' THEN 1 ELSE 0 END) AS is_increment,
|
||||
(CASE WHEN c.is_nullable = 'NO' THEN 1 ELSE 0 END) AS is_nullable,
|
||||
(CASE WHEN EXISTS (
|
||||
SELECT 1 FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name
|
||||
WHERE tc.table_name = c.table_name
|
||||
AND tc.constraint_type = 'UNIQUE'
|
||||
AND ccu.column_name = c.column_name
|
||||
) THEN 1 ELSE 0 END) AS is_unique
|
||||
FROM
|
||||
information_schema.columns c
|
||||
LEFT JOIN pg_catalog.pg_description pgd ON
|
||||
pgd.objoid = (SELECT oid FROM pg_class WHERE relname = c.table_name)
|
||||
AND pgd.objsubid = c.ordinal_position
|
||||
WHERE c.table_catalog = current_database()
|
||||
AND c.table_schema = 'public'
|
||||
AND c.table_name = :table_name
|
||||
ORDER BY
|
||||
c.ordinal_position
|
||||
"""
|
||||
|
||||
query = text(query_sql).bindparams(table_name=table_name)
|
||||
result = await self.db.execute(query)
|
||||
rows = result.fetchall() if result else []
|
||||
|
||||
# 确保rows是可迭代对象
|
||||
if not rows:
|
||||
return []
|
||||
|
||||
columns_list = []
|
||||
for row in rows:
|
||||
# 防御性编程:检查row是否有足够的元素
|
||||
if len(row) >= 10:
|
||||
columns_list.append(
|
||||
GenTableColumnOutSchema(
|
||||
column_name=row[0],
|
||||
column_comment=row[1],
|
||||
column_type=row[2],
|
||||
column_length=str(row[3]) if row[3] is not None else '',
|
||||
column_default=str(row[4]) if row[4] is not None else '',
|
||||
sort=row[5],
|
||||
is_pk=row[6],
|
||||
is_increment=row[7],
|
||||
is_nullable=row[8],
|
||||
is_unique=row[9],
|
||||
)
|
||||
)
|
||||
return columns_list
|
||||
except Exception as e:
|
||||
logger.error(f"获取表{table_name}的字段列表时出错: {str(e)}")
|
||||
# 确保即使出错也返回空列表而不是None
|
||||
raise
|
||||
|
||||
async def list_gen_table_column_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[GenTableColumnModel]:
|
||||
"""根据业务表字段查询业务表字段列表。
|
||||
@@ -559,7 +528,7 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
|
||||
data_dict = data.model_dump(exclude_unset=True)
|
||||
return await self.update(id=id, data=data_dict)
|
||||
|
||||
async def delete_gen_table_column_by_table_id_dao(self, table_ids: List[int]) -> None:
|
||||
async def delete_gen_table_column_by_table_id_crud(self, table_ids: List[int]) -> None:
|
||||
"""根据业务表ID批量删除业务表字段。
|
||||
|
||||
参数:
|
||||
@@ -577,7 +546,7 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
|
||||
if column_ids:
|
||||
await self.delete(ids=column_ids)
|
||||
|
||||
async def delete_gen_table_column_by_column_id_dao(self, column_ids: List[int]) -> None:
|
||||
async def delete_gen_table_column_by_column_id_crud(self, column_ids: List[int]) -> None:
|
||||
"""根据业务表字段ID批量删除业务表字段。
|
||||
|
||||
参数:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import String, Integer, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from typing import Optional, List, Dict, Any
|
||||
from sqlalchemy import String, Integer, ForeignKey, Boolean, JSON, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship, validates
|
||||
from sqlalchemy.sql import expression
|
||||
|
||||
from app.config.setting import settings
|
||||
from app.core.base_model import CreatorMixin
|
||||
@@ -17,25 +18,36 @@ class GenTableModel(CreatorMixin):
|
||||
__table_args__ = ({'comment': '代码生成表'})
|
||||
__loader_options__ = ["columns", "creator"]
|
||||
|
||||
table_name: Mapped[Optional[str]] = mapped_column(String(200), nullable=True, default='', comment='表名称')
|
||||
table_comment: Mapped[Optional[str]] = mapped_column(String(500), nullable=True, default='', comment='表描述')
|
||||
sub_table_name : Mapped[Optional[str]] = mapped_column(String(64), nullable=True, server_default=SqlalchemyUtil.get_server_default_null(settings.DATABASE_TYPE), comment='关联子表的表名',)
|
||||
sub_table_fk_name: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, server_default=SqlalchemyUtil.get_server_default_null(settings.DATABASE_TYPE), comment='子表关联的外键名',)
|
||||
class_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, default='', comment='实体类名称')
|
||||
table_name: Mapped[str] = mapped_column(String(200), nullable=False, default='', comment='表名称')
|
||||
table_comment: Mapped[Optional[str]] = mapped_column(String(500), nullable=True, comment='表描述')
|
||||
|
||||
class_name: Mapped[str] = mapped_column(String(100), nullable=False, default='', comment='实体类名称')
|
||||
package_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment='生成包路径')
|
||||
module_name: Mapped[Optional[str]] = mapped_column(String(30), nullable=True, comment='生成模块名')
|
||||
business_name: Mapped[Optional[str]] = mapped_column(String(30), nullable=True, comment='生成业务名')
|
||||
function_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment='生成功能名')
|
||||
gen_type: Mapped[Optional[str]] = mapped_column(String(1), nullable=True, default='0', comment='生成代码方式(0zip压缩包 1生成项目路径)')
|
||||
options: Mapped[Optional[str]] = mapped_column(String(1000), nullable=True, comment='其它生成选项')
|
||||
|
||||
sub_table_name: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, server_default=SqlalchemyUtil.get_server_default_null(settings.DATABASE_TYPE), comment='关联子表的表名')
|
||||
sub_table_fk_name: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, server_default=SqlalchemyUtil.get_server_default_null(settings.DATABASE_TYPE), comment='子表关联的外键名')
|
||||
|
||||
parent_menu_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, comment='父菜单ID')
|
||||
|
||||
# 关系定义
|
||||
columns: Mapped[List['GenTableColumnModel']] = relationship(
|
||||
'GenTableColumnModel',
|
||||
order_by='GenTableColumnModel.sort',
|
||||
back_populates='tables',
|
||||
cascade='all, delete-orphan'
|
||||
)
|
||||
columns: Mapped[List['GenTableColumnModel']] = relationship('GenTableColumnModel', order_by='GenTableColumnModel.sort', back_populates='table',cascade='all, delete-orphan')
|
||||
|
||||
@validates('table_name')
|
||||
def validate_table_name(self, key: str, table_name: str) -> str:
|
||||
"""验证表名不为空"""
|
||||
if not table_name or not table_name.strip():
|
||||
raise ValueError('表名称不能为空')
|
||||
return table_name.strip()
|
||||
|
||||
@validates('class_name')
|
||||
def validate_class_name(self, key: str, class_name: str) -> str:
|
||||
"""验证类名不为空"""
|
||||
if not class_name or not class_name.strip():
|
||||
raise ValueError('实体类名称不能为空')
|
||||
return class_name.strip()
|
||||
|
||||
|
||||
class GenTableColumnModel(CreatorMixin):
|
||||
@@ -44,38 +56,59 @@ class GenTableColumnModel(CreatorMixin):
|
||||
"""
|
||||
__tablename__ = 'gen_table_column'
|
||||
__table_args__ = ({'comment': '代码生成表字段'})
|
||||
__loader_options__ = ["tables", "creator"]
|
||||
__loader_options__ = ["table", "creator"]
|
||||
|
||||
column_name: Mapped[Optional[str]] = mapped_column(String(200), nullable=True, comment='列名称')
|
||||
# 数据库设计表字段
|
||||
column_name: Mapped[str] = mapped_column(String(200), nullable=False, comment='列名称')
|
||||
column_comment: Mapped[Optional[str]] = mapped_column(String(500), nullable=True, comment='列描述')
|
||||
column_type: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment='列类型')
|
||||
column_type: Mapped[str] = mapped_column(String(100), nullable=False, comment='列类型')
|
||||
column_length: Mapped[Optional[str]] = mapped_column(String(50), nullable=True, comment='列长度')
|
||||
column_default: Mapped[Optional[str]] = mapped_column(String(200), nullable=True, comment='列默认值')
|
||||
python_type: Mapped[Optional[str]] = mapped_column(String(500), nullable=True, comment='PYTHON类型')
|
||||
python_field: Mapped[Optional[str]] = mapped_column(String(200), nullable=True, comment='PYTHON字段名')
|
||||
is_pk: Mapped[Optional[str]] = mapped_column(String(1), nullable=True, comment='是否主键(1是)')
|
||||
is_increment: Mapped[Optional[str]] = mapped_column(String(1), nullable=True, comment='是否自增(1是)')
|
||||
is_required: Mapped[Optional[str]] = mapped_column(String(1), nullable=True, comment='是否必填(1是)')
|
||||
is_unique: Mapped[Optional[str]] = mapped_column(String(1), nullable=True, comment='是否唯一(1是)')
|
||||
is_insert: Mapped[Optional[str]] = mapped_column(String(1), nullable=True, comment='是否为插入字段(1是)')
|
||||
is_edit: Mapped[Optional[str]] = mapped_column(String(1), nullable=True, comment='是否编辑字段(1是)')
|
||||
is_list: Mapped[Optional[str]] = mapped_column(String(1), nullable=True, comment='是否列表字段(1是)')
|
||||
is_query: Mapped[Optional[str]] = mapped_column(String(1), nullable=True, comment='是否查询字段(1是)')
|
||||
query_type: Mapped[Optional[str]] = mapped_column(String(200), nullable=True, default='EQ', comment='查询方式(等于、不等于、大于、小于、范围)')
|
||||
html_type: Mapped[Optional[str]] = mapped_column(String(200), nullable=True, comment='显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)')
|
||||
is_pk: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=expression.false(), comment='是否主键')
|
||||
is_increment: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=expression.false(), comment='是否自增')
|
||||
is_nullable: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default=expression.true(), comment='是否允许为空')
|
||||
is_unique: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=expression.false(), comment='是否唯一')
|
||||
|
||||
# Python字段映射
|
||||
python_type: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment='Python类型')
|
||||
python_field: Mapped[Optional[str]] = mapped_column(String(200), nullable=True, comment='Python字段名')
|
||||
|
||||
# 序列化配置
|
||||
is_insert: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default=expression.true(), comment='是否为新增字段')
|
||||
is_edit: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default=expression.true(), comment='是否编辑字段')
|
||||
is_list: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default=expression.true(), comment='是否列表字段')
|
||||
is_query: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=expression.false(), comment='是否查询字段')
|
||||
query_type: Mapped[Optional[str]] = mapped_column(String(50), nullable=True, default=None, comment='查询方式')
|
||||
|
||||
# 前端展示配置
|
||||
html_type: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, default='input', comment='显示类型')
|
||||
dict_type: Mapped[Optional[str]] = mapped_column(String(200), nullable=True, default='', comment='字典类型')
|
||||
sort: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, comment='排序')
|
||||
|
||||
# 外键关系
|
||||
table_id: Mapped[Optional[int]] = mapped_column(
|
||||
|
||||
# 排序和扩展配置
|
||||
sort: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment='排序')
|
||||
|
||||
# 归属关系
|
||||
table_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey('gen_table.id', ondelete='CASCADE'),
|
||||
nullable=True,
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment='归属表编号'
|
||||
)
|
||||
|
||||
|
||||
# 关系定义
|
||||
tables: Mapped['GenTableModel'] = relationship(
|
||||
'GenTableModel',
|
||||
back_populates='columns'
|
||||
)
|
||||
table: Mapped['GenTableModel'] = relationship('GenTableModel', back_populates='columns')
|
||||
|
||||
@validates('column_name')
|
||||
def validate_column_name(self, key: str, column_name: str) -> str:
|
||||
"""验证列名不为空"""
|
||||
if not column_name or not column_name.strip():
|
||||
raise ValueError('列名称不能为空')
|
||||
return column_name.strip()
|
||||
|
||||
@validates('column_type')
|
||||
def validate_column_type(self, key: str, column_type: str) -> str:
|
||||
"""验证列类型不为空"""
|
||||
if not column_type or not column_type.strip():
|
||||
raise ValueError('列类型不能为空')
|
||||
return column_type.strip()
|
||||
@@ -22,7 +22,7 @@ class GenTableQueryParam:
|
||||
|
||||
class GenTableColumnQueryParam:
|
||||
"""代码生成业务表字段查询参数
|
||||
- `column_name`按like规则模糊查询(透传到CRUD层)。
|
||||
- `column_name`按like规则模糊查询(透传到CRUD层)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from typing import List, Literal, Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from app.core.base_schema import BaseSchema
|
||||
|
||||
|
||||
class GenTableOptionSchema(BaseModel):
|
||||
"""代码生成表的附加选项(存入`options`字段的JSON)。
|
||||
- parent_menu_id:菜单归属;树模板依赖。
|
||||
- tree_*:树形结构必需的编码/父编码/名称字段。
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
parent_menu_id: Optional[int] = Field(default=None, description='所属父级分类')
|
||||
|
||||
|
||||
|
||||
class GenDBTableSchema(BaseModel):
|
||||
"""数据库中的表信息(跨方言统一结构)。
|
||||
- 供“导入表结构”与“同步结构”环节使用。
|
||||
@@ -31,7 +19,10 @@ class GenDBTableSchema(BaseModel):
|
||||
table_comment: Optional[str] = Field(default=None, description='表描述')
|
||||
|
||||
|
||||
class GenTableBaseSchema(BaseModel):
|
||||
class GenTableSchema(BaseModel):
|
||||
"""代码生成业务表更新模型(扩展聚合字段)。
|
||||
- 聚合:`columns`字段包含字段列表;`pk_column`主键字段;子表结构`sub_table`。
|
||||
"""
|
||||
"""代码生成业务表基础模型(创建/更新共享字段)。
|
||||
- 说明:`params`为前端结构体,后端持久化为`options`的JSON。
|
||||
"""
|
||||
@@ -39,85 +30,69 @@ class GenTableBaseSchema(BaseModel):
|
||||
|
||||
table_name: str= Field(..., 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='实体类名称')
|
||||
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='生成功能名')
|
||||
gen_type: Optional[Literal['0', '1']] = Field(default=None, description='生成代码方式(0zip压缩包 1生成项目路径)')
|
||||
options: Optional[str] = Field(default=None, description='其它生成选项(JSON字符串)')
|
||||
description: 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='子表关联的外键名')
|
||||
parent_menu_id: Optional[int] = Field(default=None, description='所属父级分类,生成页面时候生成菜单使用')
|
||||
description: Optional[str] = Field(default=None, max_length=255, description="描述")
|
||||
|
||||
|
||||
class GenTableSchema(GenTableBaseSchema):
|
||||
"""代码生成业务表更新模型(扩展聚合字段)。
|
||||
- 聚合:`columns`字段包含字段列表;`pk_column`主键字段;子表结构`sub_table`。
|
||||
"""
|
||||
|
||||
pk_column: Optional['GenTableColumnOutSchema'] = Field(default=None, description='主键信息')
|
||||
sub_table: Optional['GenTableSchema'] = Field(default=None, description='子表信息')
|
||||
columns: Optional[List['GenTableColumnOutSchema']] = 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='是否为子表')
|
||||
|
||||
@field_validator('table_name')
|
||||
@classmethod
|
||||
def table_name_update(cls, v: str) -> str:
|
||||
"""更新表名称"""
|
||||
if not v:
|
||||
raise ValueError('表名称不能为空')
|
||||
return v
|
||||
|
||||
|
||||
class GenTableOutSchema(GenTableSchema, BaseSchema):
|
||||
"""业务表输出模型(面向控制器/前端)。
|
||||
- 清洗:统一处理None值,保证`columns`为列表;文本字段为空字符串。
|
||||
- 兼容:既支持传入ORM对象,也支持字典输入。
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
pk_column: Optional['GenTableColumnOutSchema'] = Field(default=None, description='主键信息')
|
||||
sub_table: Optional['GenTableSchema'] = Field(default=None, description='子表信息')
|
||||
sub: Optional[bool] = Field(default=None, description='是否为子表')
|
||||
|
||||
|
||||
class GenTableColumnSchema(BaseModel):
|
||||
"""代码生成业务表字段创建模型(原始字段+生成配置)。
|
||||
- 原始:`column_name/column_type/column_comment` 等。
|
||||
- 生成:`python_type/html_type/query_type/dict_type` 等由工具初始化。
|
||||
- 标记:所有 is_* 字段默认使用字符串'1'表示启用,便于前端和模板处理。
|
||||
- 从根本上解决问题:所有字段都设置了合理的默认值,避免None值问题
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
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='列类型')
|
||||
column_length: Optional[str] = Field(default=None, description='列长度')
|
||||
column_default: 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是)')
|
||||
table_id: int = Field(default=0, description='归属表编号')
|
||||
column_name: str = Field(default='', description='列名称')
|
||||
column_comment: Optional[str] = Field(default='', description='列描述')
|
||||
column_type: str = Field(default='varchar(255)', description='列类型')
|
||||
column_length: Optional[str] = Field(default='', description='列长度')
|
||||
column_default: Optional[str] = Field(default='', description='列默认值')
|
||||
is_pk: bool = Field(default=False, description='是否主键(True是 False否)')
|
||||
is_increment: bool = Field(default=False, description='是否自增(True是 False否)')
|
||||
is_nullable: bool = Field(default=True, description='是否允许为空(True是 False否)')
|
||||
is_unique: bool = Field(default=False, description='是否唯一(True是 False否)')
|
||||
python_type: Optional[str] = Field(default='str', description='python类型')
|
||||
python_field: Optional[str] = Field(default='', description='python字段名')
|
||||
is_insert: bool = Field(default=True, description='是否为插入字段(True是 False否)')
|
||||
is_edit: bool = Field(default=True, description='是否编辑字段(True是 False否)')
|
||||
is_list: bool = Field(default=True, description='是否列表字段(True是 False否)')
|
||||
is_query: bool = Field(default=True, description='是否查询字段(True是 False否)')
|
||||
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='排序')
|
||||
description: Optional[str] = Field(default=None, description='功能描述')
|
||||
html_type: Optional[str] = Field(default='input', description='显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)')
|
||||
dict_type: Optional[str] = Field(default='', description='字典类型')
|
||||
sort: int = Field(default=0, description='排序')
|
||||
|
||||
|
||||
class GenTableColumnOutSchema(GenTableColumnSchema, BaseSchema):
|
||||
"""业务表字段输出模型(布尔派生+便捷字段)。
|
||||
- 布尔:将字符串 is_* 转为布尔 `pk/increment/...`,供前端/模板快捷使用。
|
||||
- 便捷:`cap_python_field` 存放大驼峰字段名(模板场景常用)。
|
||||
"""
|
||||
业务表字段输出模型
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
cap_python_field: Optional[str] = Field(default=None, description='字段大写形式')
|
||||
pk: Optional[bool] = Field(default=False, description='是否主键')
|
||||
increment: Optional[bool] = Field(default=False, description='是否自增')
|
||||
required: Optional[bool] = Field(default=False, description='是否必填')
|
||||
unique: Optional[bool] = Field(default=False, description='是否唯一')
|
||||
insert: Optional[bool] = Field(default=False, description='是否为插入字段')
|
||||
edit: Optional[bool] = Field(default=False, description='是否编辑字段')
|
||||
list: Optional[bool] = Field(default=False, description='是否列表字段')
|
||||
query: Optional[bool] = Field(default=False, description='是否查询字段')
|
||||
super_column: Optional[bool] = Field(default=False, description='是否为基类字段')
|
||||
usable_column: Optional[bool] = Field(default=False, description='是否为基类字段白名单')
|
||||
super_column: Optional[str] = Field(default='0', description='是否为基类字段(1是 0否)')
|
||||
@@ -10,7 +10,6 @@ from sqlglot import parse as sqlglot_parse
|
||||
|
||||
from app.config.setting import settings
|
||||
from app.core.logger import logger
|
||||
from app.common.response import ErrorResponse
|
||||
from app.core.exceptions import CustomException
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.utils.gen_util import GenUtils
|
||||
@@ -41,10 +40,7 @@ class GenTableService:
|
||||
- 备注:优先解析`options`为`GenTableOptionSchema`,设置`parent_menu_id`等选项;保证`columns`与`tables`结构完整。
|
||||
"""
|
||||
gen_table = await cls.get_gen_table_by_id_service(auth, table_id)
|
||||
gen_tables = await cls.get_gen_table_all_service(auth)
|
||||
gen_columns = await GenTableColumnService.get_gen_table_column_list_by_table_id_service(auth, table_id)
|
||||
gen_table.columns = gen_columns
|
||||
return dict(info=gen_table, rows=gen_columns, tables=gen_tables)
|
||||
return GenTableOutSchema.model_validate(gen_table).model_dump()
|
||||
|
||||
@classmethod
|
||||
@handle_service_exception
|
||||
@@ -100,52 +96,38 @@ class GenTableService:
|
||||
"""
|
||||
# 检查是否有表需要导入
|
||||
if not gen_table_list:
|
||||
raise CustomException(msg="没有可导入的表结构")
|
||||
|
||||
# 检查表是否已存在
|
||||
existing_tables = []
|
||||
for table in gen_table_list:
|
||||
table_name = table.table_name
|
||||
# 检查表是否已存在
|
||||
existing_table = await GenTableCRUD(auth).get_gen_table_by_name(table_name)
|
||||
if existing_table:
|
||||
existing_tables.append(table_name)
|
||||
|
||||
# 如果有已存在的表,抛出异常
|
||||
if existing_tables:
|
||||
raise CustomException(msg=f"以下表已存在,不能重复导入: {', '.join(existing_tables)}")
|
||||
|
||||
raise CustomException(msg="导入的表结构不能为空")
|
||||
try:
|
||||
for table in gen_table_list:
|
||||
table_name = table.table_name
|
||||
# 检查表是否已存在
|
||||
existing_table = await GenTableCRUD(auth).get_gen_table_by_name(table_name)
|
||||
if existing_table:
|
||||
raise CustomException(msg=f"以下表已存在,不能重复导入: {table_name}")
|
||||
GenUtils.init_table(table)
|
||||
add_gen_table = await GenTableCRUD(auth).add_gen_table(table)
|
||||
if add_gen_table:
|
||||
if not table.columns:
|
||||
table.columns = []
|
||||
add_gen_table = await GenTableCRUD(auth).add_gen_table(GenTableSchema.model_validate(table.model_dump()))
|
||||
gen_table_columns = await GenTableColumnCRUD(auth).get_gen_db_table_columns_by_name(table_name)
|
||||
if len(gen_table_columns) > 0:
|
||||
table.id = add_gen_table.id
|
||||
# 获取数据库表的字段信息
|
||||
gen_table_columns = await GenTableColumnCRUD(auth).get_gen_db_table_columns_by_name(table_name)
|
||||
|
||||
# 为每个字段初始化并保存到数据库
|
||||
for column in gen_table_columns:
|
||||
# 将GenTableColumnOutSchema转换为GenTableColumnSchema,确保所有字段正确设置
|
||||
column_schema = GenTableColumnSchema(
|
||||
table_id=table.id,
|
||||
column_name=column.column_name,
|
||||
column_comment=column.column_comment,
|
||||
column_type=column.column_type,
|
||||
column_length=column.column_length if column.column_length is not None else '',
|
||||
column_default=column.column_default if column.column_default is not None else '',
|
||||
column_length=column.column_length,
|
||||
column_default=column.column_default,
|
||||
is_pk=column.is_pk,
|
||||
is_increment=column.is_increment,
|
||||
is_nullable=column.is_nullable,
|
||||
is_unique=column.is_unique,
|
||||
sort=column.sort,
|
||||
python_type=column.python_type,
|
||||
python_field=column.python_field,
|
||||
is_pk=str(column.is_pk) if column.is_pk is not None else '0',
|
||||
is_increment=str(column.is_increment) if column.is_increment is not None else '0',
|
||||
is_required=str(column.is_required) if column.is_required is not None else '0',
|
||||
is_unique=str(column.is_unique) if column.is_unique is not None else '0',
|
||||
sort=column.sort
|
||||
)
|
||||
# 初始化字段属性
|
||||
GenUtils.init_column_field(column_schema, table)
|
||||
# 保存到数据库
|
||||
await GenTableColumnCRUD(auth).create_gen_table_column_crud(column_schema)
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -165,16 +147,15 @@ class GenTableService:
|
||||
try:
|
||||
# 解析SQL语句
|
||||
sql_statements = sqlglot_parse(sql, dialect=settings.DATABASE_TYPE)
|
||||
if not sql_statements:
|
||||
raise CustomException(msg='无法解析SQL语句,请检查SQL语法')
|
||||
|
||||
# 校验sql语句是否为合法的建表语句
|
||||
if not cls.__is_valid_create_table(sql_statements):
|
||||
raise CustomException(msg='sql语句不是合法的建表语句')
|
||||
|
||||
# 获取要创建的表名
|
||||
table_names = cls.__get_table_names(sql_statements)
|
||||
if not table_names:
|
||||
raise CustomException(msg='无法从SQL语句中提取表名')
|
||||
|
||||
table_names = cls.__get_table_names(sql_statements)
|
||||
# 创建CRUD实例
|
||||
gen_table_crud = GenTableCRUD(auth=auth)
|
||||
|
||||
@@ -190,17 +171,19 @@ class GenTableService:
|
||||
raise CustomException(msg=f'表 {table_name} 已在代码生成模块中存在,请检查并修改表名后重试')
|
||||
|
||||
# 表不存在,执行SQL语句创建表
|
||||
await gen_table_crud.create_table_by_sql(sql)
|
||||
result = await gen_table_crud.create_table_by_sql(sql_statements)
|
||||
if not result:
|
||||
raise CustomException(msg=f'创建表 {table_names} 失败,请检查SQL语句')
|
||||
|
||||
# 导入表结构到代码生成模块
|
||||
# 导入表结构到代码生成模块 - 简化逻辑,移除多余的None检查
|
||||
gen_table_list = await cls.get_gen_db_table_list_by_name_service(auth, table_names)
|
||||
|
||||
import_result = await cls.import_gen_table_service(auth, gen_table_list)
|
||||
|
||||
return import_result
|
||||
except CustomException:
|
||||
# 直接传递已格式化的CustomException
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
raise CustomException(msg=f'创建表结构失败: {str(e)}。SQL预览: {sql}')
|
||||
raise CustomException(msg=f'创建表结构失败: {str(e)}')
|
||||
|
||||
@classmethod
|
||||
def __is_valid_create_table(cls, sql_statements: List[Expression | None]) -> bool:
|
||||
@@ -242,7 +225,7 @@ class GenTableService:
|
||||
table = sql_statement.find(Table)
|
||||
if table and table.name:
|
||||
table_names.append(table.name)
|
||||
return table_names
|
||||
return list(set(table_names))
|
||||
|
||||
@classmethod
|
||||
@handle_service_exception
|
||||
@@ -264,7 +247,7 @@ class GenTableService:
|
||||
if hasattr(gen_table_column, 'id') and gen_table_column.id:
|
||||
column_schema = GenTableColumnSchema(**gen_table_column.model_dump())
|
||||
await GenTableColumnCRUD(auth).update_gen_table_column_crud(gen_table_column.id, column_schema)
|
||||
return result.model_dump()
|
||||
return GenTableOutSchema.model_validate(result).model_dump()
|
||||
except Exception as e:
|
||||
raise CustomException(msg=str(e))
|
||||
else:
|
||||
@@ -280,7 +263,7 @@ class GenTableService:
|
||||
|
||||
try:
|
||||
# 先删除相关的字段信息
|
||||
await GenTableColumnCRUD(auth=auth).delete_gen_table_column_by_table_id_dao(ids)
|
||||
await GenTableColumnCRUD(auth=auth).delete_gen_table_column_by_table_id_crud(ids)
|
||||
# 再删除表信息
|
||||
await GenTableCRUD(auth=auth).delete_gen_table(ids)
|
||||
except Exception as e:
|
||||
@@ -297,21 +280,18 @@ class GenTableService:
|
||||
raise CustomException(msg='业务表不存在')
|
||||
|
||||
result = GenTableOutSchema.model_validate(gen_table)
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
@handle_service_exception
|
||||
async def get_gen_table_all_service(cls, auth: AuthSchema) -> List[GenTableOutSchema]:
|
||||
"""获取所有业务表信息(列表)。"""
|
||||
gen_table_all = await GenTableCRUD(auth=auth).get_gen_table_all()
|
||||
gen_table_all = await GenTableCRUD(auth=auth).get_gen_table_all() or []
|
||||
result = []
|
||||
for gen_table in gen_table_all:
|
||||
try:
|
||||
# 确保转换为输出模型,并处理可能的None值
|
||||
# 简化转换,利用schema层的默认值处理None情况
|
||||
table_out = GenTableOutSchema.model_validate(gen_table)
|
||||
if table_out.columns is None:
|
||||
table_out.columns = []
|
||||
result.append(table_out)
|
||||
except Exception as e:
|
||||
logger.warning(f"转换业务表时出错: {str(e)}")
|
||||
@@ -348,15 +328,11 @@ class GenTableService:
|
||||
async def generate_code_service(cls, auth: AuthSchema, table_name: str) -> bool:
|
||||
"""生成代码至指定路径(安全写入+可跳过覆盖)。
|
||||
- 安全:限制写入在项目根目录内;越界路径自动回退到项目根目录。
|
||||
- 覆盖:尊重`settings.allow_overwrite`,不允许时跳过写入。
|
||||
"""
|
||||
# 验证表名非空
|
||||
if not table_name or not table_name.strip():
|
||||
raise CustomException(msg='表名不能为空')
|
||||
|
||||
if not settings.allow_overwrite:
|
||||
logger.error('【系统预设】不允许生成文件覆盖到本地')
|
||||
raise CustomException(msg='【系统预设】不允许生成文件覆盖到本地')
|
||||
env = Jinja2TemplateUtil.get_env()
|
||||
render_info = await cls.__get_gen_render_info(auth, table_name)
|
||||
gen_table_schema = render_info[3]
|
||||
@@ -390,6 +366,9 @@ class GenTableService:
|
||||
zip_buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
||||
for table_name in table_names:
|
||||
if not table_name.strip():
|
||||
continue
|
||||
|
||||
try:
|
||||
env = Jinja2TemplateUtil.get_env()
|
||||
render_info = await cls.__get_gen_render_info(auth, table_name)
|
||||
@@ -419,51 +398,49 @@ class GenTableService:
|
||||
if not gen_table:
|
||||
raise CustomException(msg='业务表不存在')
|
||||
table = GenTableOutSchema.model_validate(gen_table)
|
||||
if not table.id:
|
||||
raise CustomException(msg='业务表ID不能为空')
|
||||
table_columns = table.columns or []
|
||||
table_column_map = {column.column_name: column for column in table_columns}
|
||||
db_table_columns = await GenTableColumnCRUD(auth).get_gen_db_table_columns_by_name(table_name)
|
||||
# 确保db_table_columns始终是列表类型,避免None值
|
||||
db_table_columns = await GenTableColumnCRUD(auth).get_gen_db_table_columns_by_name(table_name) or []
|
||||
db_table_columns = [col for col in db_table_columns if col is not None]
|
||||
db_table_column_names = [column.column_name for column in db_table_columns]
|
||||
try:
|
||||
for column in db_table_columns:
|
||||
# 仅在缺省时初始化默认属性(包含 table_id 关联)
|
||||
GenUtils.init_column_field(column, table)
|
||||
# 确保column_length和column_default字段有默认值
|
||||
if column.column_length is None:
|
||||
column.column_length = ''
|
||||
if column.column_default is None:
|
||||
column.column_default = ''
|
||||
# 利用schema层的默认值,移除多余的None检查
|
||||
if column.column_name in table_column_map:
|
||||
prev_column = table_column_map[column.column_name]
|
||||
# 复用旧记录ID,确保执行更新
|
||||
if hasattr(prev_column, 'id') and prev_column.id:
|
||||
column.id = prev_column.id
|
||||
|
||||
# 保留用户配置的显示与查询属性
|
||||
if getattr(prev_column, 'dict_type', None):
|
||||
# 保留用户配置的显示与查询属性 - 使用getattr确保安全访问
|
||||
if hasattr(prev_column, 'dict_type') and prev_column.dict_type:
|
||||
column.dict_type = prev_column.dict_type
|
||||
if getattr(prev_column, 'query_type', None):
|
||||
if hasattr(prev_column, 'query_type') and prev_column.query_type:
|
||||
column.query_type = prev_column.query_type
|
||||
if getattr(prev_column, 'html_type', None):
|
||||
if hasattr(prev_column, 'html_type') and prev_column.html_type:
|
||||
column.html_type = prev_column.html_type
|
||||
|
||||
# 保留 is_* 标志(旧值非空则保留),主键不设置必填
|
||||
def keep_str(orig, current):
|
||||
return orig if (orig is not None and orig != '') else current
|
||||
|
||||
is_pk_bool = bool(getattr(prev_column, 'pk', False)) or (prev_column.is_pk == '1')
|
||||
if not is_pk_bool:
|
||||
column.is_required = keep_str(prev_column.is_required, column.is_required)
|
||||
column.column_length = keep_str(prev_column.column_length, column.column_length)
|
||||
column.column_default = keep_str(prev_column.column_default, column.column_default)
|
||||
column.python_type = keep_str(prev_column.python_type, column.python_type)
|
||||
column.python_field = keep_str(prev_column.python_field, column.python_field)
|
||||
column.is_pk = keep_str(prev_column.is_pk, column.is_pk)
|
||||
column.is_increment = keep_str(prev_column.is_increment, column.is_increment)
|
||||
column.is_unique = keep_str(prev_column.is_unique, column.is_unique)
|
||||
column.is_insert = keep_str(prev_column.is_insert, column.is_insert)
|
||||
column.is_edit = keep_str(prev_column.is_edit, column.is_edit)
|
||||
column.is_list = keep_str(prev_column.is_list, column.is_list)
|
||||
column.is_query = keep_str(prev_column.is_query, column.is_query)
|
||||
# 保留关键用户自定义属性 - 安全处理is_pk
|
||||
is_pk_bool = False
|
||||
if hasattr(prev_column, 'is_pk'):
|
||||
# 处理不同类型的is_pk值
|
||||
if isinstance(prev_column.is_pk, bool):
|
||||
is_pk_bool = prev_column.is_pk
|
||||
else:
|
||||
is_pk_bool = str(prev_column.is_pk) == '1'
|
||||
|
||||
# 安全处理nullable属性
|
||||
if hasattr(prev_column, 'is_nullable') and not is_pk_bool:
|
||||
column.is_nullable = prev_column.is_nullable
|
||||
|
||||
# 保留其他重要用户设置
|
||||
if hasattr(prev_column, 'python_field'):
|
||||
column.python_field = prev_column.python_field or column.python_field
|
||||
|
||||
if hasattr(column, 'id') and column.id:
|
||||
await GenTableColumnCRUD(auth).update_gen_table_column_crud(column.id, column)
|
||||
@@ -477,7 +454,7 @@ class GenTableService:
|
||||
if del_columns:
|
||||
for column in del_columns:
|
||||
if hasattr(column, 'id') and column.id:
|
||||
await GenTableColumnCRUD(auth).delete_gen_table_column_by_column_id_dao([column.id])
|
||||
await GenTableColumnCRUD(auth).delete_gen_table_column_by_column_id_crud([column.id])
|
||||
except Exception as e:
|
||||
raise CustomException(msg=f'同步失败: {str(e)}')
|
||||
|
||||
@@ -550,47 +527,8 @@ class GenTableColumnService:
|
||||
|
||||
@classmethod
|
||||
@handle_service_exception
|
||||
async def get_gen_table_column_list_by_table_id_service(cls, auth: AuthSchema, table_id: int) -> List[GenTableColumnOutSchema]:
|
||||
async def get_gen_table_column_list_by_table_id_service(cls, auth: AuthSchema, table_id: int) -> List[Dict[str, Any]]:
|
||||
"""获取业务表字段列表信息(输出模型)。"""
|
||||
gen_table_column_list_result = await GenTableColumnCRUD(auth).list_gen_table_column_crud({"table_id": table_id})
|
||||
result = []
|
||||
for gen_table_column in gen_table_column_list_result:
|
||||
try:
|
||||
# 转换为输出模型前确保必要字段正确设置
|
||||
# 确保is_*字段为字符串格式
|
||||
if hasattr(gen_table_column, 'is_pk') and gen_table_column.is_pk is not None and not isinstance(gen_table_column.is_pk, str):
|
||||
gen_table_column.is_pk = str(gen_table_column.is_pk)
|
||||
if hasattr(gen_table_column, 'is_increment') and gen_table_column.is_increment is not None and not isinstance(gen_table_column.is_increment, str):
|
||||
gen_table_column.is_increment = str(gen_table_column.is_increment)
|
||||
if hasattr(gen_table_column, 'is_required') and gen_table_column.is_required is not None and not isinstance(gen_table_column.is_required, str):
|
||||
gen_table_column.is_required = str(gen_table_column.is_required)
|
||||
if hasattr(gen_table_column, 'is_unique') and gen_table_column.is_unique is not None and not isinstance(gen_table_column.is_unique, str):
|
||||
gen_table_column.is_unique = str(gen_table_column.is_unique)
|
||||
if hasattr(gen_table_column, 'is_insert') and gen_table_column.is_insert is not None and not isinstance(gen_table_column.is_insert, str):
|
||||
gen_table_column.is_insert = str(gen_table_column.is_insert)
|
||||
if hasattr(gen_table_column, 'is_edit') and gen_table_column.is_edit is not None and not isinstance(gen_table_column.is_edit, str):
|
||||
gen_table_column.is_edit = str(gen_table_column.is_edit)
|
||||
if hasattr(gen_table_column, 'is_list') and gen_table_column.is_list is not None and not isinstance(gen_table_column.is_list, str):
|
||||
gen_table_column.is_list = str(gen_table_column.is_list)
|
||||
if hasattr(gen_table_column, 'is_query') and gen_table_column.is_query is not None and not isinstance(gen_table_column.is_query, str):
|
||||
gen_table_column.is_query = str(gen_table_column.is_query)
|
||||
|
||||
|
||||
# 转换为输出模型
|
||||
column_out = GenTableColumnOutSchema.model_validate(gen_table_column)
|
||||
|
||||
# 确保输出模型中的布尔字段正确设置
|
||||
column_out.pk = column_out.is_pk == '1'
|
||||
column_out.increment = column_out.is_increment == '1'
|
||||
column_out.required = column_out.is_required == '1'
|
||||
column_out.unique = column_out.is_unique == '1'
|
||||
column_out.insert = column_out.is_insert == '1'
|
||||
column_out.edit = column_out.is_edit == '1'
|
||||
column_out.list = column_out.is_list == '1'
|
||||
column_out.query = column_out.is_query == '1'
|
||||
|
||||
result.append(column_out)
|
||||
except Exception as e:
|
||||
logger.warning(f"转换字段模型时出错: {str(e)}")
|
||||
continue
|
||||
result = [GenTableColumnOutSchema.model_validate(gen_table_column).model_dump() for gen_table_column in gen_table_column_list_result]
|
||||
return result
|
||||
@@ -1,12 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Request, UploadFile, Form
|
||||
from fastapi import APIRouter, Body, Depends, Query, Request, UploadFile, Form
|
||||
from fastapi.responses import JSONResponse, StreamingResponse, FileResponse
|
||||
from typing import List, Optional
|
||||
|
||||
# from oss2 import auth # 预留阿里云OSS,后期使用
|
||||
|
||||
from app.common.response import StreamResponse, SuccessResponse, ErrorResponse
|
||||
from app.common.response import StreamResponse, SuccessResponse
|
||||
from app.common.request import PaginationService
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
|
||||
Reference in New Issue
Block a user