refactor(module): 重构模块路径和数据库配置

- 将前端API路径从`system`和`monitor`重命名为`module_system`和`module_monitor`
- 移除MongoDB相关配置和依赖
- 统一数据库类型命名为`mysql`和`postgres`
- 修复代码生成模板中的字段命名问题
- 更新依赖项并移除不必要的包
- 优化数据库连接配置
- 添加新的文档和Redoc视图组件
- 修复SQL脚本中的字段注释
This commit is contained in:
zhangtao
2025-11-10 01:43:23 +08:00
parent bbde7b8c6e
commit 3df4f5a45a
89 changed files with 964 additions and 1531 deletions
@@ -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