fix(gencode): 修复模型关系字段命名错误及优化用户服务逻辑

refactor(schema): 重构代码生成相关schema,增加可选字段和类型提示
refactor(crud): 优化CRUD操作,简化查询逻辑并增加类型安全
fix(user): 修复用户信息获取时的空指针异常及优化菜单权限查询
This commit is contained in:
zhangtao
2025-10-02 20:29:22 +08:00
parent ec18e92dcb
commit 7b9d8d7854
4 changed files with 172 additions and 196 deletions
@@ -8,10 +8,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from typing import List, Optional, Sequence, Any, Dict
from app.api.v1.module_system.params.schema import ParamsCreateSchema
from app.core.logger import logger
from .model import GenTableModel, GenTableColumnModel
from app.config.setting import settings
from app.common.request import PaginationService
from .schema import GenTableCreateSchema, GenTableUpdateSchema, GenTableOutSchema, GenTableDeleteSchema, GenTableColumnCreateSchema, GenTableColumnUpdateSchema, GenTableColumnOutSchema, GenTableColumnDeleteSchema
from .schema import GenTableCreateSchema, GenTableUpdateSchema, GenTableOutSchema, GenTableDeleteSchema, GenTableColumnCreateSchema, GenTableColumnUpdateSchema, GenTableColumnOutSchema, GenTableColumnDeleteSchema, GenDBTableSchema
from .param import GenTableQueryParam
from app.core.base_crud import CRUDBase
from app.api.v1.module_system.auth.schema import AuthSchema
@@ -28,41 +31,43 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableCreateSchema, GenTableUpdateS
"""
根据业务表id获取需要生成的业务表信息
:param db: orm对象
:param table_id: 业务表id
:return: 需要生成的业务表信息对象
"""
gen_table_info = (
gen_table = (
(
await self.db.execute(
select(GenTableModel).options(selectinload(GenTableModel.columns)).where(GenTableModel.id == table_id)
select(GenTableModel)
.options(selectinload(GenTableModel.columns))
.where(GenTableModel.id == table_id)
)
)
.scalars()
.first()
)
return gen_table_info
return gen_table
async def get_gen_table_by_name(self, table_name: str) -> Optional[GenTableModel]:
"""
根据业务表名称获取需要生成的业务表信息
:param db: orm对象
:param table_name: 业务表名称
:return: 需要生成的业务表信息对象
"""
gen_table_info = (
gen_table = (
(
await self.db.execute(
select(GenTableModel).options(selectinload(GenTableModel.columns)).where(GenTableModel.table_name == table_name)
select(GenTableModel)
.options(selectinload(GenTableModel.columns))
.where(GenTableModel.table_name == table_name)
)
)
.scalars()
.first()
)
return gen_table_info
return gen_table
async def get_gen_table_all(self) -> Sequence[GenTableModel]:
"""
@@ -71,11 +76,40 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableCreateSchema, GenTableUpdateS
:param db: orm对象
:return: 所有业务表信息
"""
gen_table_all = (await self.db.execute(select(GenTableModel).options(selectinload(GenTableModel.columns)))).scalars().all()
gen_table_all = (
await self.db.execute(
select(GenTableModel)
.options(selectinload(GenTableModel.columns)))
).scalars().all()
return gen_table_all
async def get_gen_table_list(self, search: Optional[GenTableQueryParam] = None):
"""
根据查询参数获取代码生成业务表列表信息
async def create_table_by_sql(self, sql_statements: List) -> None:
:param query_object: 查询参数对象
:return: 代码生成业务表列表信息对象
"""
# 构建查询条件
conditions = await self.__build_conditions(**search.__dict__) if search else []
query = (
select(GenTableModel)
.options(selectinload(GenTableModel.columns))
.where(
*conditions
)
.order_by(GenTableModel.created_at.desc())
.distinct()
)
# 获取所有数据
result = await self.db.execute(query)
gen_table_all = list(result.scalars().all())
return gen_table_all
async def create_table_by_sql(self, sql: str) -> bool:
"""
根据sql语句创建表结构
@@ -83,68 +117,47 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableCreateSchema, GenTableUpdateS
:param sql_statements: sql语句的ast列表
:return:
"""
for sql_statement in sql_statements:
sql = sql_statement.sql(dialect=settings.DATABASE_TYPE)
await self.db.execute(text(sql))
try:
await self.db.execute(text(sql))
# 提交事务
await self.db.commit()
await self.db.flush()
return True
except Exception as e:
# 如果发生异常,回滚事务
await self.db.rollback()
logger.error(f"创建表时发生错误: {e}")
return False
async def get_gen_table_list(self, query_object: GenTableQueryParam, is_page: bool = False):
async def add_gen_table(self, add_model: GenTableCreateSchema) -> GenTableModel:
"""
根据查询参数获取代码生成业务表列表信息
:param db: orm对象
:param query_object: 查询参数对象
:param is_page: 是否开启分页
:return: 代码生成业务表列表信息对象
增加
"""
# 构建查询条件
conditions = []
# 访问table_name属性
if getattr(query_object, 'table_name', None) and query_object.table_name[1]:
conditions.append(func.lower(GenTableModel.table_name).like(f'%{str(query_object.table_name[1]).lower()}%'))
# 访问table_comment属性
if getattr(query_object, 'table_comment', None):
conditions.append(func.lower(GenTableModel.table_comment).like(f'%{str(query_object.table_comment).lower()}%'))
# 访问created_at属性而不是start_time和end_time
if hasattr(query_object, 'created_at') and query_object.created_at:
if isinstance(query_object.created_at, tuple) and query_object.created_at[0] == "between":
conditions.append(GenTableModel.created_at.between(*query_object.created_at[1]))
gen_table = GenTableModel(**add_model.model_dump(exclude_unset=True, exclude={'sub', 'tree', 'crud'}))
self.db.add(gen_table)
await self.db.flush()
return gen_table
async def delete_gen_table(self, delete_model: GenTableDeleteSchema) -> None:
"""
删除
"""
await self.db.execute(delete(GenTableModel).where(GenTableModel.id.in_(delete_model.table_ids)))
await self.db.flush()
async def edit_gen_table(self, table_id: int, edit_model: GenTableUpdateSchema, auto_commit: bool = True):
"""
修改
"""
edit_dict_data = edit_model.model_dump(exclude_unset=True)
await self.db.execute(update(GenTableModel).where(GenTableModel.id == table_id).values(**edit_dict_data))
await self.db.flush()
if auto_commit:
await self.db.commit()
return edit_model
query = (
select(GenTableModel)
.options(selectinload(GenTableModel.columns))
.where(*conditions)
.order_by(GenTableModel.created_at.desc())
.distinct()
)
# 获取所有数据
result = await self.db.execute(query)
all_data = list(result.scalars().all())
# 使用PaginationService.paginate进行分页
# 注意:这里假设query_object有page_no和page_size属性,如果没有需要从其他地方获取
page_no = getattr(query_object, 'page_no', None)
page_size = getattr(query_object, 'page_size', None)
if is_page and page_no is not None and page_size is not None:
paginated_result = await PaginationService.paginate(
data_list=all_data,
page_no=page_no,
page_size=page_size
)
return paginated_result
else:
return {
"items": all_data,
"total": len(all_data),
"page_no": None,
"page_size": None,
"has_next": False
}
async def get_gen_db_table_list(self, search: GenTableQueryParam, order_by: Optional[List[Dict[str, str]]] = None) -> list[Any]:
async def get_gen_db_table_list(self, table_name: Optional[str] = None) -> list[Any]:
"""
根据查询参数获取数据库列表信息
@@ -153,55 +166,6 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableCreateSchema, GenTableUpdateS
:param order_by: 排序字段
:return: 数据库列表信息对象
"""
# pg数据库
# {
# "table_catalog": "fastapiadmin",
# "table_schema": "pg_catalog",
# "table_name": "pg_foreign_table",
# "table_type": "BASE TABLE",
# "self_referencing_column_name": null,
# "reference_generation": null,
# "user_defined_type_catalog": null,
# "user_defined_type_schema": null,
# "user_defined_type_name": null,
# "is_insertable_into": "YES",
# "is_typed": "NO",
# "commit_action": null
# }
# mysql
# {
# "TABLE_CATALOG": "def",
# "TABLE_SCHEMA": "fastapiadmin",
# "TABLE_NAME": "ai_mcp",
# "TABLE_TYPE": "BASE TABLE",
# "ENGINE": "InnoDB",
# "VERSION": 10,
# "ROW_FORMAT": "Dynamic",
# "TABLE_ROWS": 0,
# "AVG_ROW_LENGTH": 0,
# "DATA_LENGTH": 16384,
# "MAX_DATA_LENGTH": 0,
# "INDEX_LENGTH": 16384,
# "DATA_FREE": 0,
# "AUTO_INCREMENT": null,
# "CREATE_TIME": "2025-10-02T03:43:02",
# "UPDATE_TIME": null,
# "CHECK_TIME": null,
# "TABLE_COLLATION": "utf8mb4_0900_ai_ci",
# "CHECKSUM": null,
# "CREATE_OPTIONS": "",
# "TABLE_COMMENT": "MCP 服务器表"
# },
# sqlite
# {
# "type": "table",
# "name": "system_users",
# "tbl_name": "system_users",
# "rootpage": 47,
# "sql": "CREATE TABLE system_users (\n\tusername VARCHAR(32) NOT NULL, \n\tpassword VARCHAR(255) NOT NULL, \n\tname VARCHAR(32) NOT NULL, \n\tstatus BOOLEAN NOT NULL, \n\tmobile VARCHAR(20), \n\temail VARCHAR(64), \n\tgender VARCHAR(1), \n\tavatar VARCHAR(500), \n\tis_superuser BOOLEAN NOT NULL, \n\tlast_login DATETIME, \n\tdept_id INTEGER, \n\tcreator_id INTEGER, \n\tid INTEGER NOT NULL, \n\tdescription TEXT, \n\tcreated_at DATETIME, \n\tupdated_at DATETIME, \n\tPRIMARY KEY (id), \n\tUNIQUE (username), \n\tUNIQUE (mobile), \n\tUNIQUE (email), \n\tFOREIGN KEY(dept_id) REFERENCES system_dept (id) ON DELETE SET NULL ON UPDATE CASCADE\n)"
# },
# 使用更健壮的方式检测数据库方言
if settings.DATABASE_TYPE == 'postgresql':
@@ -256,10 +220,13 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableCreateSchema, GenTableUpdateS
# 检查row是否为Row对象
if isinstance(row, Row):
# 使用._mapping获取字典
dict_row = dict(row._mapping)
dict_row = GenDBTableSchema(**dict(row._mapping)).model_dump()
if table_name:
dict_row['table_name'] = table_name
dict_data.append(dict_row)
else:
dict_data.append(row)
dict_row = GenDBTableSchema(**dict(row)).model_dump()
dict_data.append(dict_row)
return dict_data
async def get_gen_db_table_list_by_names(self, table_names: List[str]):
@@ -326,22 +293,43 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnCreateSchem
"""初始化CRUD"""
super().__init__(model=GenTableColumnModel, auth=auth)
async def get_by_id_crud(self, column_id: int) -> Optional[GenTableColumnModel]:
"""详情"""
return await self.get(id=column_id)
async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[GenTableColumnModel]:
"""列表查询"""
return await self.list(search=search, order_by=order_by)
async def create_crud(self, data: GenTableColumnCreateSchema) -> Optional[GenTableColumnModel]:
"""创建"""
return await self.create(data=data)
async def update_crud(self, id: int, data: GenTableColumnUpdateSchema) -> Optional[GenTableColumnModel]:
"""更新"""
return await self.update(id=id, data=data)
async def delete_crud(self, data: GenTableColumnDeleteSchema) -> None:
"""批量删除"""
return await self.delete(ids=data.column_ids)
async def get_gen_table_column_list_by_table_id_crud(self, table_id: int) -> Sequence[GenTableColumnModel]:
"""根据业务表id获取需要生成的业务表字段列表信息"""
return await self.list(search={"table_id": table_id})
async def get_gen_table_column_list_by_table_id(self, db: AsyncSession, table_id: int) -> Sequence[GenTableColumnModel]:
async def get_gen_table_column_list_by_table_id(self, table_id: int) -> Sequence[GenTableColumnModel]:
"""
根据业务表id获取需要生成的业务表字段列表信息
:param db: orm对象
:param table_id: 业务表id
:return: 需要生成的业务表字段列表信息对象
"""
gen_table_column_list = (
(
await db.execute(
select(GenTableColumnModel).where(GenTableColumnModel.table_id == table_id).order_by(GenTableColumnModel.sort)
await self.db.execute(
select(GenTableColumnModel)
.where(GenTableColumnModel.table_id == table_id)
.order_by(GenTableColumnModel.sort)
)
)
.scalars()
@@ -2,7 +2,7 @@
from typing import Optional, List
from sqlalchemy import String, Integer, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship, declared_attr
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.base_model import CreatorMixin
@@ -34,7 +34,7 @@ class GenTableModel(CreatorMixin):
columns: Mapped[List['GenTableColumnModel']] = relationship(
'GenTableColumnModel',
order_by='GenTableColumnModel.sort',
back_populates='table',
back_populates='tables',
cascade='all, delete-orphan'
)
@@ -73,7 +73,7 @@ class GenTableColumnModel(CreatorMixin):
)
# 关系定义
table: Mapped['GenTableModel'] = relationship(
tables: Mapped['GenTableModel'] = relationship(
'GenTableModel',
back_populates='columns'
)
@@ -9,27 +9,47 @@ from app.common.constant import GenConstant
from app.core.base_schema import BaseSchema
class GenTableOptionModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
parent_menu_id: Optional[int] = Field(default=None, description='所属父级分类')
tree_code: Optional[str] = Field(default=None, description='tree_code')
tree_name: Optional[str] = Field(default=None, description='tree_name')
tree_parent_code: Optional[str] = Field(default=None, description='tree_parent_code')
class GenDBTableSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
database_name: Optional[str] = Field(default=None, description='数据库名称')
table_name: Optional[str] = Field(default=None, description='表名称')
table_type: Optional[str] = Field(default=None, description='表类型')
table_comment: Optional[str] = Field(default=None, description='表描述')
class GenTableCreateSchema(BaseModel):
"""
代码生成业务表创建模型
"""
model_config = ConfigDict(from_attributes=True)
table_name: str = Field(..., description='表名称')
table_comment: str = Field(..., description='表描述')
table_name: Optional[str] = Field(default=None, description='表名称')
table_comment: Optional[str] = Field(default=None, description='表描述')
sub_table_name: Optional[str] = Field(default=None, description='关联子表的表名')
sub_table_fk_name: str = Field(..., description='子表关联的外键名')
class_name: str = Field(..., description='实体类名称')
tpl_category: Optional[str] = Field(default=None, description='使用的模板(crud单表操作 tree树表操作)')
sub_table_fk_name: Optional[str] = Field(default=None, description='子表关联的外键名')
class_name: Optional[str] = Field(default=None, description='实体类名称')
tpl_category: Optional[Literal['crud', 'tree']] = Field(default=None, description='使用的模板(crud单表操作 tree树表操作)')
tpl_web_type: Optional[str] = Field(default=None, description='前端模板类型(element-ui模版 element-plus模版)')
package_name: str = Field(..., description='生成包路径')
module_name: str = Field(..., description='生成模块名')
business_name: str = Field(..., description='生成业务名')
function_name: str = Field(..., 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='生成功能名')
function_author: Optional[str] = Field(default=None, description='生成功能作者')
gen_type: Optional[Literal['0', '1']] = Field(default=None, description='生成代码方式(0zip压缩包 1自定义路径)')
gen_path: Optional[str] = Field(default=None, description='生成路径(不填默认项目路径)')
options: Optional[str] = Field(default=None, description='其它生成选项')
description: Optional[str] = Field(default=None, description='功能描述')
class GenTableUpdateSchema(GenTableCreateSchema):
@@ -38,10 +58,10 @@ class GenTableUpdateSchema(GenTableCreateSchema):
"""
pk_column: Optional['GenTableColumnUpdateSchema'] = Field(default=None, description='主键信息')
sub_table: Optional['GenTableUpdateSchema'] = Field(default=None, description='子表信息')
columns: List['GenTableColumnUpdateSchema'] = Field(..., description='表列信息')
tree_code: Optional[str] = Field(default=None, description='树编码字段')
columns: Optional[List['GenTableColumnUpdateSchema']] = Field(default=None, description='表列信息')
tree_code: Optional[str] = Field(default=None, description='树编码字段tree_code')
tree_parent_code: Optional[str] = Field(default=None, description='树父编码字段')
tree_name: Optional[str] = Field(default=None, description='树名称字段')
tree_name: Optional[str] = Field(default=None, description='树名称字段ree_name')
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='是否为子表')
@@ -79,12 +99,11 @@ class GenTableColumnCreateSchema(BaseModel):
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
table_id: Optional[int] = Field(default=None, description='归属表编号')
column_name: str = Field(..., description='列名称')
column_name: Optional[str] = Field(default=None, description='列名称')
column_comment: Optional[str] = Field(default=None, description='列描述')
column_type: str = Field(..., description='列类型')
column_type: Optional[str] = Field(default=None, description='列类型')
python_type: Optional[str] = Field(default=None, description='PYTHON类型')
python_field: str = Field(..., 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是)')
@@ -94,48 +113,17 @@ class GenTableColumnCreateSchema(BaseModel):
is_list: Optional[str] = Field(default=None, description='是否列表字段(1是)')
is_query: Optional[str] = Field(default=None, description='是否查询字段(1是)')
query_type: Optional[str] = Field(default=None, description='查询方式(等于、不等于、大于、小于、范围)')
html_type: str = Field(..., description='显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)')
dict_type: str = Field(..., 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='功能描述')
class GenTableColumnUpdateSchema(GenTableColumnCreateSchema):
"""
代码生成业务表字段更新模型
"""
cap_python_field: Optional[str] = Field(default=None, description='字段大写形式')
pk: Optional[bool] = Field(default=None, description='是否主键')
increment: Optional[bool] = Field(default=None, description='是否自增')
required: Optional[bool] = Field(default=None, description='是否必填')
unique: Optional[bool] = Field(default=None, description='是否唯一')
insert: Optional[bool] = Field(default=None, description='是否为插入字段')
edit: Optional[bool] = Field(default=None, description='是否编辑字段')
list: Optional[bool] = Field(default=None, description='是否列表字段')
query: Optional[bool] = Field(default=None, description='是否查询字段')
super_column: Optional[bool] = Field(default=None, description='是否为基类字段')
usable_column: Optional[bool] = Field(default=None, description='是否为基类字段白名单')
@model_validator(mode='after')
def check_some_is(self) -> 'GenTableColumnUpdateSchema':
self.cap_python_field = self.python_field[0].upper() + self.python_field[1:] if self.python_field else None
self.pk = True if self.is_pk and self.is_pk == '1' else False
self.increment = True if self.is_increment and self.is_increment == '1' else False
self.required = True if self.is_required and self.is_required == '1' else False
self.unique = True if self.is_unique and self.is_unique == '1' else False
self.insert = True if self.is_insert and self.is_insert == '1' else False
self.edit = True if self.is_edit and self.is_edit == '1' else False
self.list = True if self.is_list and self.is_list == '1' else False
self.query = True if self.is_query and self.is_query == '1' else False
self.super_column = (
True
if StringUtil.equals_any_ignore_case(self.python_field, GenConstant.TREE_ENTITY + GenConstant.BASE_ENTITY)
else False
)
self.usable_column = (
True if StringUtil.equals_any_ignore_case(self.python_field, ['parentId', 'orderNum', 'remark']) else False
)
return self
...
class GenTableColumnOutSchema(GenTableColumnUpdateSchema, BaseSchema):
@@ -151,4 +139,4 @@ class GenTableColumnDeleteSchema(BaseModel):
"""
model_config = ConfigDict(from_attributes=True)
column_ids: str = Field(..., description='需要删除的代码生成业务表字段ID')
column_ids: List[int] = Field(..., description='需要删除的代码生成业务表字段ID')
@@ -156,7 +156,7 @@ class UserService:
raise CustomException(msg="超级管理员不能删除")
if user.status:
raise CustomException(msg="用户已启用,不能删除")
if auth.user.id == id:
if auth.user and auth.user.id == id:
raise CustomException(msg="不能删除当前登陆用户")
# 删除用户角色关联数据
await UserCRUD(auth).set_user_roles_crud(user_ids=ids, role_ids=[])
@@ -171,35 +171,35 @@ class UserService:
async def get_current_user_info_service(cls, auth: AuthSchema) -> Dict:
"""获取当前用户信息"""
# 获取用户基本信息
user = await UserCRUD(auth).get_by_id_crud(id=auth.user.id)
if not user:
if not auth.user:
raise CustomException(msg="用户不存在")
user = await UserCRUD(auth).get_by_id_crud(id=auth.user.id)
# 获取部门名称
if user.dept_id:
dept = await DeptCRUD(auth).get_by_id_crud(id=auth.user.dept_id)
user.dept_name = dept.name if dept else None
if user and user.dept_id:
dept = await DeptCRUD(auth).get_by_id_crud(id=user.dept_id)
UserOutSchema.dept_name = dept.name if dept else None
user_dict = UserOutSchema.model_validate(user).model_dump()
# 获取菜单权限
if auth.user.is_superuser:
if auth.user and auth.user.is_superuser:
# 使用树形结构查询,预加载children关系
menu_all = await MenuCRUD(auth).get_tree_list_crud(search={'type': ('in', [1, 2, 4]), 'status': True})
menus = [MenuOutSchema.model_validate(menu).model_dump() for menu in menu_all]
else:
# 收集用户所有角色的菜单ID
menu_ids = []
for role in auth.user.roles:
for menu in role.menus:
if menu.status and menu.type in [1, 2, 4]:
menu_ids.append(menu.id)
# 收集用户所有角色的菜单ID,使用列表推导式优化代码
menu_ids = {
menu.id
for role in auth.user.roles or []
for menu in role.menus
if menu.status and menu.type in [1, 2, 4]
}
# 使用树形结构查询,预加载children关系
if menu_ids:
menu_all = await MenuCRUD(auth).get_tree_list_crud(search={'id': ('in', menu_ids)})
menus = [MenuOutSchema.model_validate(menu).model_dump() for menu in menu_all]
else:
menus = []
menus = [
MenuOutSchema.model_validate(menu).model_dump()
for menu in await MenuCRUD(auth).get_tree_list_crud(search={'id': ('in', list(menu_ids))})
] if menu_ids else []
user_dict["menus"] = traversal_to_tree(menus)
return user_dict