refactor: 重构代码结构并优化命名一致性

feat(backend): 新增模块示例和路径配置
fix(backend): 修复菜单模型名称唯一性约束
refactor(backend): 重构日志模块和路径配置
style(backend): 统一API方法命名规范
refactor(frontend): 重构API方法命名规范
fix(frontend): 修复权限校验问题
refactor: 移动定时任务测试文件到正确模块
docs: 更新模板文件路径
refactor: 优化日志记录和错误处理
fix: 修复排序默认值问题
This commit is contained in:
zhangtao
2025-11-19 02:24:16 +08:00
parent fce6333722
commit 279f547f84
132 changed files with 1670 additions and 1106 deletions
@@ -10,7 +10,7 @@ from app.core.router_class import OperationLogRoute
from app.core.base_params import PaginationQueryParam
from app.common.request import PaginationService
from app.utils.common_util import bytes2file_response
from app.core.logger import logger
from app.core.logger import log
from app.api.v1.module_system.auth.schema import AuthSchema
from .param import GenTableQueryParam
@@ -40,7 +40,7 @@ async def gen_table_list_controller(
"""
result_dict_list = await GenTableService.get_gen_table_list_service(auth=auth, search=search)
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
logger.info('获取代码生成业务表列表成功')
log.info('获取代码生成业务表列表成功')
return SuccessResponse(data=result_dict, msg="获取代码生成业务表列表成功")
@@ -63,7 +63,7 @@ async def get_gen_db_table_list_controller(
"""
result_dict_list = await GenTableService.get_gen_db_table_list_service(auth=auth, search=search)
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
logger.info('获取数据库表列表成功')
log.info('获取数据库表列表成功')
return SuccessResponse(data=result_dict, msg="获取数据库表列表成功")
@@ -84,7 +84,7 @@ async def import_gen_table_controller(
"""
add_gen_table_list = await GenTableService.get_gen_db_table_list_by_name_service(auth, table_names)
result = await GenTableService.import_gen_table_service(auth, add_gen_table_list)
logger.info('导入表结构成功')
log.info('导入表结构成功')
return SuccessResponse(msg="导入表结构成功", data=result)
@@ -104,7 +104,7 @@ async def gen_table_detail_controller(
- JSONResponse: 包含业务表详细信息的JSON响应
"""
gen_table_detail_result = await GenTableService.get_gen_table_detail_service(auth, table_id)
logger.info(f'获取table_id为{table_id}的信息成功')
log.info(f'获取table_id为{table_id}的信息成功')
return SuccessResponse(data=gen_table_detail_result, msg="获取业务表详细信息成功")
@@ -124,7 +124,7 @@ async def create_table_controller(
- JSONResponse: 包含创建结果的JSON响应
"""
result = await GenTableService.create_table_service(auth, sql)
logger.info('创建表结构成功')
log.info('创建表结构成功')
return SuccessResponse(msg="创建表结构成功", data=result)
@@ -146,7 +146,7 @@ async def update_gen_table_controller(
- JSONResponse: 包含编辑结果的JSON响应
"""
result_dict = await GenTableService.update_gen_table_service(auth, data, table_id)
logger.info('编辑业务表信息成功')
log.info('编辑业务表信息成功')
return SuccessResponse(data=result_dict, msg="编辑业务表信息成功")
@@ -166,14 +166,14 @@ async def delete_gen_table_controller(
- JSONResponse: 包含删除结果的JSON响应
"""
result = await GenTableService.delete_gen_table_service(auth, ids)
logger.info('删除业务表信息成功')
log.info('删除业务表信息成功')
return SuccessResponse(msg="删除业务表信息成功", data=result)
@GenRouter.patch("/batch/output", summary="批量生成代码", description="批量生成代码")
async def batch_gen_code_controller(
table_names: List[str] = Body(..., description="表名列表"),
auth: AuthSchema = Depends(AuthPermission(["module_generator:gencode:operate"]))
auth: AuthSchema = Depends(AuthPermission(["module_generator:gencode:patch"]))
) -> StreamResponse:
"""
批量生成代码
@@ -186,7 +186,7 @@ async def batch_gen_code_controller(
- StreamResponse: 包含批量生成代码的ZIP文件流响应
"""
batch_gen_code_result = await GenTableService.batch_gen_code_service(auth, table_names)
logger.info(f'批量生成代码成功,表名列表:{table_names}')
log.info(f'批量生成代码成功,表名列表:{table_names}')
return StreamResponse(
data=bytes2file_response(batch_gen_code_result),
media_type='application/zip',
@@ -210,7 +210,7 @@ async def gen_code_local_controller(
- JSONResponse: 包含生成结果的JSON响应
"""
result = await GenTableService.generate_code_service(auth, table_name)
logger.info(f'生成代码,表名:{table_name},到指定路径成功')
log.info(f'生成代码,表名:{table_name},到指定路径成功')
return SuccessResponse(msg="生成代码到指定路径成功", data=result)
@@ -230,7 +230,7 @@ async def preview_code_controller(
- JSONResponse: 包含预览代码的JSON响应
"""
preview_code_result = await GenTableService.preview_code_service(auth, table_id)
logger.info(f'预览代码,表id{table_id},成功')
log.info(f'预览代码,表id{table_id},成功')
return SuccessResponse(data=preview_code_result, msg="预览代码成功")
@@ -250,5 +250,5 @@ async def sync_db_controller(
- JSONResponse: 包含同步数据库结果的JSON响应
"""
result = await GenTableService.sync_db_service(auth, table_name)
logger.info(f'同步数据库,表名:{table_name},成功')
log.info(f'同步数据库,表名:{table_name},成功')
return SuccessResponse(msg="同步数据库成功", data=result)
@@ -5,7 +5,7 @@ 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.core.logger import log
from app.config.setting import settings
from app.core.base_crud import CRUDBase
@@ -262,7 +262,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
else:
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}")
log.error(f"查询表信息时发生错误: {e}")
# 查询错误时直接抛出,不需要事务处理
raise
@@ -299,7 +299,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
result = await self.db.execute(query, {"table_name": table_name})
return result.scalar() is not None
except Exception as e:
logger.error(f"检查表格存在性时发生错误: {e}")
log.error(f"检查表格存在性时发生错误: {e}")
# 出错时返回False,避免误报表已存在
return False
@@ -324,7 +324,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
await self.db.execute(text(sql))
return True
except Exception as e:
logger.error(f"创建表时发生错误: {e}")
log.error(f"创建表时发生错误: {e}")
return False
@@ -487,7 +487,7 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
)
return columns_list
except Exception as e:
logger.error(f"获取表{table_name}的字段列表时出错: {str(e)}")
log.error(f"获取表{table_name}的字段列表时出错: {str(e)}")
# 确保即使出错也返回空列表而不是None
raise
@@ -7,13 +7,14 @@ from typing import Any, List, Dict, Literal, Optional
from sqlglot.expressions import Add, Alter, Create, Delete, Drop, Expression, Insert, Table, TruncateTable, Update
from sqlglot import parse as sqlglot_parse
from app.config.path_conf import BASE_DIR
from app.config.setting import settings
from app.core.logger import logger
from app.core.logger import log
from app.core.exceptions import CustomException
from app.utils.gen_util import GenUtils
from app.utils.jinja2_template_util import Jinja2TemplateUtil
from app.api.v1.module_system.auth.schema import AuthSchema
from .tools.jinja2_template_util import Jinja2TemplateUtil
from .tools.gen_util import GenUtils
from .schema import GenTableSchema, GenTableOutSchema, GenTableColumnSchema, GenTableColumnOutSchema
from .param import GenTableQueryParam
from .crud import GenTableColumnCRUD, GenTableCRUD
@@ -294,7 +295,7 @@ class GenTableService:
table_out = GenTableOutSchema.model_validate(gen_table)
result.append(table_out)
except Exception as e:
logger.warning(f"转换业务表时出错: {str(e)}")
log.error(f"转换业务表时出错: {str(e)}")
continue
return result
@@ -318,7 +319,7 @@ class GenTableService:
render_content = await env.get_template(template).render_async(**context)
preview_code_result[template] = render_content
except Exception as e:
logger.error(f"渲染模板 {template} 时出错: {str(e)}")
log.error(f"渲染模板 {template} 时出错: {str(e)}")
# 即使某个模板渲染失败,也继续处理其他模板
preview_code_result[template] = f"渲染错误: {str(e)}"
return preview_code_result
@@ -376,7 +377,7 @@ class GenTableService:
render_content = await env.get_template(template_file).render_async(**render_info[2])
zip_file.writestr(output_file, render_content)
except Exception as e:
logger.error(f"批量生成代码时处理表 {table_name} 出错: {str(e)}")
log.error(f"批量生成代码时处理表 {table_name} 出错: {str(e)}")
# 继续处理其他表,不中断整个过程
continue
@@ -507,18 +508,18 @@ class GenTableService:
try:
file_name = Jinja2TemplateUtil.get_file_name(template, gen_table)
# 默认写入到项目根目录(backend的上一级)
project_root = str(settings.BASE_DIR.parent)
project_root = str(BASE_DIR.parent)
full_path = os.path.join(project_root, file_name)
# 确保路径在项目根目录内,防止路径遍历攻击
if not os.path.abspath(full_path).startswith(os.path.abspath(project_root)):
logger.warning(f"路径越界,回退到项目根目录: {file_name}")
log.error(f"路径越界,回退到项目根目录: {file_name}")
# 回退到项目根目录下的generated文件夹
full_path = os.path.join(project_root, "generated", os.path.basename(file_name))
return full_path
except Exception as e:
logger.error(f"生成路径时出错: {str(e)}")
log.error(f"生成路径时出错: {str(e)}")
return None
@@ -0,0 +1,129 @@
# -*- coding:utf-8 -*-
from fastapi import APIRouter, Depends, UploadFile, Body, Path
from fastapi.responses import StreamingResponse, JSONResponse
from app.common.response import SuccessResponse, StreamResponse
from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute
from app.api.v1.module_system.auth.schema import AuthSchema
from app.core.base_params import PaginationQueryParam
from app.utils.common_util import bytes2file_response
from app.core.logger import log
from app.core.base_schema import BatchSetAvailable
from .service import {{ class_name }}Service
from .schema import {{ class_name }}CreateSchema, {{ class_name }}UpdateSchema
from .param import {{ class_name }}QueryParam
{{ class_name }}Router = APIRouter(route_class=OperationLogRoute, prefix='/{{ business_name }}', tags=["{{ function_name }}模块"])
@{{ class_name }}Router.get("/detail/{id}", summary="获取{{ function_name }}详情", description="获取{{ function_name }}详情")
async def get_{{ business_name }}_detail_controller(
id: int = Path(..., description="ID"),
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:query"]))
) -> JSONResponse:
"""获取{{ function_name }}详情接口"""
result_dict = await {{ class_name }}Service.detail_{{ business_name }}_service(auth=auth, id=id)
log.info(f"获取{{ function_name }}详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取{{ function_name }}详情成功")
@{{ class_name }}Router.get("/list", summary="查询{{ function_name }}列表", description="查询{{ function_name }}列表")
async def get_{{ business_name }}_list_controller(
page: PaginationQueryParam = Depends(),
search: {{ class_name }}QueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:query"]))
) -> JSONResponse:
"""查询{{ function_name }}列表接口(数据库分页)"""
result_dict = await {{ class_name }}Service.page_service(
auth=auth,
page_no=page.page_no if page.page_no is not None else 1,
page_size=page.page_size if page.page_size is not None else 10,
search=search,
order_by=page.order_by
)
log.info("查询{{ function_name }}列表成功")
return SuccessResponse(data=result_dict, msg="查询{{ function_name }}列表成功")
@{{ class_name }}Router.post("/create", summary="创建{{ function_name }}", description="创建{{ function_name }}")
async def create_{{ business_name }}_controller(
data: {{ class_name }}CreateSchema,
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:create"]))
) -> JSONResponse:
"""创建{{ function_name }}接口"""
result_dict = await {{ class_name }}Service.create_{{ business_name }}_service(auth=auth, data=data)
log.info("创建{{ function_name }}成功")
return SuccessResponse(data=result_dict, msg="创建{{ function_name }}成功")
@{{ class_name }}Router.put("/update/{id}", summary="修改{{ function_name }}", description="修改{{ function_name }}")
async def update_{{ business_name }}_controller(
data: {{ class_name }}UpdateSchema,
id: int = Path(..., description="ID"),
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:update"]))
) -> JSONResponse:
"""修改{{ function_name }}接口"""
result_dict = await {{ class_name }}Service.update_{{ business_name }}_service(auth=auth, id=id, data=data)
log.info("修改{{ function_name }}成功")
return SuccessResponse(data=result_dict, msg="修改{{ function_name }}成功")
@{{ class_name }}Router.delete("/delete", summary="删除{{ function_name }}", description="删除{{ function_name }}")
async def delete_{{ business_name }}_controller(
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:delete"]))
) -> JSONResponse:
"""删除{{ function_name }}接口"""
await {{ class_name }}Service.delete_{{ business_name }}_service(auth=auth, ids=ids)
log.info(f"删除{{ function_name }}成功: {ids}")
return SuccessResponse(msg="删除{{ function_name }}成功")
@{{ class_name }}Router.patch("/available/setting", summary="批量修改{{ function_name }}状态", description="批量修改{{ function_name }}状态")
async def batch_set_available_{{ business_name }}_controller(
data: BatchSetAvailable,
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:patch"]))
) -> JSONResponse:
"""批量修改{{ function_name }}状态接口"""
await {{ class_name }}Service.set_available_{{ business_name }}_service(auth=auth, data=data)
log.info(f"批量修改{{ function_name }}状态成功: {data.ids}")
return SuccessResponse(msg="批量修改{{ function_name }}状态成功")
@{{ class_name }}Router.post('/export', summary="导出{{ function_name }}", description="导出{{ function_name }}")
async def export_{{ business_name }}_list_controller(
search: {{ class_name }}QueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:export"]))
) -> StreamingResponse:
"""导出{{ function_name }}接口"""
result_dict_list = await {{ class_name }}Service.list_{{ business_name }}_service(search=search, auth=auth)
export_result = await {{ class_name }}Service.batch_export_service(obj_list=result_dict_list)
log.info('导出{{ function_name }}成功')
return StreamResponse(
data=bytes2file_response(export_result),
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers={
'Content-Disposition': 'attachment; filename={{ table_name }}.xlsx'
}
)
@{{ class_name }}Router.post('/import', summary="导入{{ function_name }}", description="导入{{ function_name }}")
async def import_{{ business_name }}_list_controller(
file: UploadFile,
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:import"]))
) -> JSONResponse:
"""导入{{ function_name }}接口"""
batch_import_result = await {{ class_name }}Service.batch_import_{{ business_name }}_service(file=file, auth=auth, update_support=True)
log.info("导入{{ function_name }}成功")
return SuccessResponse(data=batch_import_result, msg="导入{{ function_name }}成功")
@{{ class_name }}Router.post('/download/template', summary="获取{{ function_name }}导入模板", description="获取{{ function_name }}导入模板", dependencies=[Depends(AuthPermission(["{{ permission_prefix }}:download"]))])
async def export_{{ business_name }}_template_controller() -> StreamingResponse:
"""获取{{ function_name }}导入模板接口"""
example_import_template_result = await {{ class_name }}Service.import_template_download_{{ business_name }}_service()
log.info('获取{{ function_name }}导入模板成功')
return StreamResponse(
data=bytes2file_response(example_import_template_result),
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers={
'Content-Disposition': 'attachment; filename={{ table_name }}_template.xlsx'
}
)
@@ -0,0 +1,123 @@
# -*- coding:utf-8 -*-
from typing import Dict, List, Optional, Sequence, Union, Any
from app.core.base_crud import CRUDBase
from app.api.v1.module_system.auth.schema import AuthSchema
from .model import {{ class_name }}Model
from .schema import {{ class_name }}CreateSchema, {{ class_name }}UpdateSchema, {{ class_name }}OutSchema
class {{ class_name }}CRUD(CRUDBase[{{ class_name }}Model, {{ class_name }}CreateSchema, {{ class_name }}UpdateSchema]):
"""{{ function_name }}数据层"""
def __init__(self, auth: AuthSchema) -> None:
"""
初始化CRUD数据层
参数:
- auth (AuthSchema): 认证信息模型
"""
super().__init__(model={{ class_name }}Model, auth=auth)
async def get_by_id_{{ business_name }}_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[{{ class_name }}Model]:
"""
详情
参数:
- id (int): 对象ID
- preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项
返回:
- Optional[{{ class_name }}Model]: 模型实例或None
"""
return await self.get(id=id, preload=preload)
async def list_{{ business_name }}_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[{{ class_name }}Model]:
"""
列表查询
参数:
- search (Optional[Dict]): 查询参数
- order_by (Optional[List[Dict[str, str]]]): 排序参数
- preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项
返回:
- Sequence[{{ class_name }}Model]: 模型实例序列
"""
return await self.list(search=search, order_by=order_by, preload=preload)
async def create_{{ business_name }}_crud(self, data: {{ class_name }}CreateSchema) -> Optional[{{ class_name }}Model]:
"""
创建
参数:
- data ({{ class_name }}CreateSchema): 创建模型
返回:
- Optional[{{ class_name }}Model]: 模型实例或None
"""
return await self.create(data=data)
async def update_{{ business_name }}_crud(self, id: int, data: {{ class_name }}UpdateSchema) -> Optional[{{ class_name }}Model]:
"""
更新
参数:
- id (int): 对象ID
- data ({{ class_name }}UpdateSchema): 更新模型
返回:
- Optional[{{ class_name }}Model]: 模型实例或None
"""
return await self.update(id=id, data=data)
async def delete_{{ business_name }}_crud(self, ids: List[int]) -> None:
"""
批量删除
参数:
- ids (List[int]): 对象ID列表
返回:
- None
"""
return await self.delete(ids=ids)
async def set_available_{{ business_name }}_crud(self, ids: List[int], status: bool) -> None:
"""
批量设置可用状态
参数:
- ids (List[int]): 对象ID列表
- status (bool): 可用状态
返回:
- None
"""
return await self.set(ids=ids, status=status)
async def page_{{ business_name }}_crud(self, offset: int, limit: int, order_by: Optional[List[Dict[str, str]]] = None, search: Optional[Dict] = None, preload: Optional[List[Union[str, Any]]] = None) -> Dict:
"""
分页查询
参数:
- offset (int): 偏移量
- limit (int): 每页数量
- order_by (Optional[List[Dict[str, str]]]): 排序参数
- search (Optional[Dict]): 查询参数
- preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项
返回:
- Dict: 分页数据
"""
order_by_list = order_by or [{'id': 'asc'}]
search_dict = search or {}
return await self.page(
offset=offset,
limit=limit,
order_by=order_by_list,
search=search_dict,
out_schema={{ class_name }}OutSchema,
preload=preload
)
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
{% for model_import in model_import_list %}
{{ model_import }}
{% endfor %}
{% if table.sub %}
from sqlalchemy.orm import relationship
{% endif %}
from typing import Optional
from sqlalchemy.orm import Mapped, mapped_column
from app.core.base_model import CreatorMixin
class {{ class_name }}Model(CreatorMixin):
"""
{{ function_name }}表
"""
__tablename__ = '{{ table_name }}'
__table_args__ = {'comment': '{{ function_name }}'}
__loader_options__ = ["creator"]
{% for column in columns %}
{% if column.column_name not in ['id', 'creator_id', 'description', 'created_at', 'updated_at'] %}
{{ column.column_name }}: Mapped[Optional[{{ column.python_type }}]] = mapped_column({{ column.column_type|get_sqlalchemy_type }}, {% if column.pk %}primary_key=True, {% endif %}{% if column.increment %}autoincrement=True, {% endif %}{% if column.required or column.pk %}nullable=False{% else %}nullable=True{% endif %}, comment='{{ column.column_comment }}')
{% endif %}
{% endfor %}
{% if table.sub %}
{{ sub_class_name }}_list = relationship('{{ sub_class_name }}', back_populates='{{ business_name }}')
{% endif %}
@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
from typing import Optional
from fastapi import Query
from app.core.validator import DateTimeStr
class {{ class_name }}QueryParam:
"""{{ function_name }}查询参数"""
def __init__(
self,
{% for column in columns %}
{% if column.query_type == 'LIKE' %}
{{ column.column_name }}: Optional[{{ column.python_type }}] = Query(None, description="{{ column.column_comment }}"),
{% endif %}
{% endfor %}
{% for column in columns %}
{% if column.column_name == 'EQ' %}
{{ column.column_name }}: Optional[{{ column.python_type }}] = Query(None, description="{{ column.column_comment }}"),
{% endif %}
{% endfor %}
creator: Optional[int] = Query(None, description="创建人"),
start_time: Optional[DateTimeStr] = Query(None, description="开始时间", example="2025-01-01 00:00:00"),
end_time: Optional[DateTimeStr] = Query(None, description="结束时间", example="2025-12-31 23:59:59"),
) -> None:
# 模糊查询字段
{% for column in columns %}
{% if column.query_type == 'LIKE' %}
self.{{ column.column_name }} = ("like", {{ column.column_name }})
{% endif %}
{% endfor %}
# 精确查询字段
{% for column in columns %}
{% if column.query_type == 'EQ' %}
self.{{ column.column_name }} = {{ column.column_name }}
{% endif %}
{% endfor %}
self.creator_id = creator
# 时间范围查询
if start_time and end_time:
self.created_at = ("between", (start_time, end_time))
@@ -0,0 +1,35 @@
# -*- coding:utf-8 -*-
{% if table.sub %}
from typing import List, Optional
{% else %}
from typing import Optional
{% endif %}
from pydantic import BaseModel, ConfigDict, Field
from app.core.base_schema import BaseSchema
class {{ class_name }}CreateSchema(BaseModel):
"""
{{ function_name }}新增模型
"""
{% for column in columns %}
{% if column.column_name not in ['id', 'creator_id', 'created_at', 'updated_at'] %}
{{ column.column_name }}: Optional[{{ column.python_type }}] = Field(default=None, description='{{ column.column_comment }}')
{% endif %}
{% endfor %}
class {{ class_name }}UpdateSchema({{ class_name }}CreateSchema):
"""
{{ function_name }}更新模型
"""
...
class {{ class_name }}OutSchema({{ class_name }}CreateSchema, BaseSchema):
"""
{{ function_name }}响应模型
"""
model_config = ConfigDict(from_attributes=True)
@@ -0,0 +1,225 @@
# -*- coding:utf-8 -*-
import io
from typing import Any, List, Dict, Optional
from fastapi import UploadFile
import pandas as pd
from app.core.base_schema import BatchSetAvailable
from app.core.exceptions import CustomException
from app.utils.excel_util import ExcelUtil
from app.core.logger import log
from app.api.v1.module_system.auth.schema import AuthSchema
from .schema import {{ class_name }}CreateSchema, {{ class_name }}UpdateSchema, {{ class_name }}OutSchema
from .param import {{ class_name }}QueryParam
from .crud import {{ class_name }}CRUD
class {{ class_name }}Service:
"""
{{ function_name }}服务层
"""
@classmethod
async def detail_{{ business_name }}_service(cls, auth: AuthSchema, id: int) -> Dict:
"""详情"""
obj = await {{ class_name }}CRUD(auth).get_by_id_{{ business_name }}_crud(id=id)
if not obj:
raise CustomException(msg="该数据不存在")
return {{ class_name }}OutSchema.model_validate(obj).model_dump()
@classmethod
async def list_{{ business_name }}_service(cls, auth: AuthSchema, search: Optional[{{ class_name }}QueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict]:
"""列表查询"""
search_dict = search.__dict__ if search else None
obj_list = await {{ class_name }}CRUD(auth).list_{{ business_name }}_crud(search=search_dict, order_by=order_by)
return [{{ class_name }}OutSchema.model_validate(obj).model_dump() for obj in obj_list]
@classmethod
async def page_{{ business_name }}_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: Optional[{{ class_name }}QueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Dict:
"""分页查询(数据库分页)"""
search_dict = search.__dict__ if search else {}
order_by_list = order_by or [{'id': 'asc'}]
offset = (page_no - 1) * page_size
result = await {{ class_name }}CRUD(auth).page_{{ business_name }}_crud(
offset=offset,
limit=page_size,
order_by=order_by_list,
search=search_dict
)
return result
@classmethod
async def create_{{ business_name }}_service(cls, auth: AuthSchema, data: {{ class_name }}CreateSchema) -> Dict:
"""创建"""
# 检查唯一性约束
{% for column in columns %}
{% if column.is_unique == '1' %}
obj = await {{ class_name }}CRUD(auth).get({{ column.column_name }}=data.{{ column.column_name }})
if obj:
raise CustomException(msg='创建失败,{{ column.column_comment }}已存在')
{% endif %}
{% endfor %}
obj = await {{ class_name }}CRUD(auth).create_{{ business_name }}_crud(data=data)
return {{ class_name }}OutSchema.model_validate(obj).model_dump()
@classmethod
async def update_{{ business_name }}_service(cls, auth: AuthSchema, id: int, data: {{ class_name }}UpdateSchema) -> Dict:
"""更新"""
# 检查数据是否存在
obj = await {{ class_name }}CRUD(auth).get_by_id_{{ business_name }}_crud(id=id)
if not obj:
raise CustomException(msg='更新失败,该数据不存在')
# 检查唯一性约束
{% for column in columns %}
{% if column.is_unique == '1' %}
exist_obj = await {{ class_name }}CRUD(auth).get({{ column.column_name }}=data.{{ column.column_name }})
if exist_obj and exist_obj.id != id:
raise CustomException(msg='更新失败,{{ column.column_comment }}重复')
{% endif %}
{% endfor %}
obj = await {{ class_name }}CRUD(auth).update_{{ business_name }}_crud(id=id, data=data)
return {{ class_name }}OutSchema.model_validate(obj).model_dump()
@classmethod
async def delete_{{ business_name }}_service(cls, auth: AuthSchema, ids: List[int]) -> None:
"""删除"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
for id in ids:
obj = await {{ class_name }}CRUD(auth).get_by_id_{{ business_name }}_crud(id=id)
if not obj:
raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
await {{ class_name }}CRUD(auth).delete_{{ business_name }}_crud(ids=ids)
@classmethod
async def set_available_{{ business_name }}_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
"""批量设置状态"""
await {{ class_name }}CRUD(auth).set_available_{{ business_name }}_crud(ids=data.ids, status=data.status)
@classmethod
async def batch_export_{{ business_name }}_service(cls, obj_list: List[Dict[str, Any]]) -> bytes:
"""批量导出"""
mapping_dict = {
{% for column in columns %}
'{{ column.column_name }}': '{{ column.column_comment }}',
{% endfor %}
'creator': '创建者',
}
data = obj_list.copy()
for item in data:
# 状态转换
if 'status' in item:
item['status'] = '正常' if item.get('status') else '停用'
# 创建者转换
creator_info = item.get('creator')
if isinstance(creator_info, dict):
item['creator'] = creator_info.get('name', '未知')
elif creator_info is None:
item['creator'] = '未知'
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
@classmethod
async def batch_import_{{ business_name }}_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
"""批量导入"""
header_dict = {
{% for column in columns %}
'{{ column.column_comment }}': '{{ column.column_name }}',
{% endfor %}
}
try:
contents = await file.read()
df = pd.read_excel(io.BytesIO(contents))
await file.close()
if df.empty:
raise CustomException(msg="导入文件为空")
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
if missing_headers:
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
df.rename(columns=header_dict, inplace=True)
# 验证必填字段
{% for column in columns %}
{% if column.required == '1' %}
missing_rows = df[df['{{ column.column_name }}'].isnull()].index.tolist()
if missing_rows:
raise CustomException(msg="{{ column.column_comment }}不能为空,第{0}行".format([i+1 for i in missing_rows]))
{% endif %}
{% endfor %}
error_msgs = []
success_count = 0
count = 0
for index, row in df.iterrows():
count += 1
try:
data = {
{% for column in columns %}
"{{ column.column_name }}": row['{{ column.column_name }}'],
{% endfor %}
}
# 使用CreateSchema做校验后入库
create_schema = {{ class_name }}CreateSchema.model_validate(data)
# 检查唯一性约束
{% for column in columns %}
{% if column.is_unique == '1' %}
exists_obj = await {{ class_name }}CRUD(auth).get({{ column.column_name }}=create_schema.{{ column.column_name }})
if exists_obj:
if update_support:
await {{ class_name }}CRUD(auth).update(id=exists_obj.id, data=create_schema)
success_count += 1
else:
error_msgs.append(f"第{count}行: {{ column.column_comment }} {create_schema.{{ column.column_name }}} 已存在")
continue
{% endif %}
{% endfor %}
await {{ class_name }}CRUD(auth).create_crud(data=create_schema)
success_count += 1
except Exception as e:
error_msgs.append(f"第{count}行: {str(e)}")
continue
result = f"成功导入 {success_count} 条数据"
if error_msgs:
result += "\n错误信息:\n" + "\n".join(error_msgs)
return result
except Exception as e:
log.error(f"批量导入失败: {str(e)}")
raise CustomException(msg=f"导入失败: {str(e)}")
@classmethod
async def import_template_download_{{ business_name }}_service(cls) -> bytes:
"""下载导入模板"""
header_list = [
{% for column in columns %}
'{{ column.column_comment }}',
{% endfor %}
]
selector_header_list = []
option_list = []
# 添加下拉选项
{% for column in columns %}
{% if column.html_type == 'select' and column.dict_type %}
selector_header_list.append('{{ column.column_comment }}')
option_list.append({'{{ column.column_comment }}': []})
{% endif %}
{% endfor %}
return ExcelUtil.get_excel_template(
header_list=header_list,
selector_header_list=selector_header_list,
option_list=option_list
)
@@ -0,0 +1,133 @@
-- 统一的菜单 SQL(兼容 MySQL / PostgreSQL),对齐到 system_menu 表结构
{# 布尔值与保留字列名处理 #}
{% set b_true = 1 if db_type == 'mysql' else true %}
{% set b_false = 0 if db_type == 'mysql' else false %}
{% set order_col = '`order`' if db_type == 'mysql' else '"order"' %}
{# 公共字段列表(按实际库字段) #}
{# name, type, order, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at #}
{% if db_type == 'mysql' %}
-- 父菜单(类型=2:菜单)
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
VALUES (
'{{ function_name }}',
2,
1,
{{ b_true }},
'{{ permission_prefix }}:query',
NULL,
'{{ business_name|snake_to_camel }}',
'/{{ module_name }}/{{ business_name }}',
'{{ module_name }}/{{ business_name }}/index',
NULL,
{{ b_false }},
{{ b_true }},
{{ b_false }},
'{{ function_name }}',
NULL,
{{ b_false }},
{{ parent_menu_id }},
'{{ function_name }}菜单',
now(),
now()
);
-- 获取父菜单IDMySQL
SELECT @parentId := LAST_INSERT_ID();
-- 按钮权限(类型=3:按钮/权限)
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
VALUES ('{{ function_name }}查询', 3, 1, {{ b_true }}, '{{ permission_prefix }}:query', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}查询', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
VALUES ('{{ function_name }}新增', 3, 2, {{ b_true }}, '{{ permission_prefix }}:create', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}新增', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
VALUES ('{{ function_name }}修改', 3, 3, {{ b_true }}, '{{ permission_prefix }}:update', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}修改', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
VALUES ('{{ function_name }}删除', 3, 4, {{ b_true }}, '{{ permission_prefix }}:delete', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}删除', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
VALUES ('{{ function_name }}导出', 3, 5, {{ b_true }}, '{{ permission_prefix }}:export', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}导出', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
VALUES ('{{ function_name }}导入', 3, 6, {{ b_true }}, '{{ permission_prefix }}:import', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}导入', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
VALUES ('{{ function_name }}批量状态修改', 3, 7, {{ b_true }}, '{{ permission_prefix }}:patch', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}批量状态修改', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
VALUES ('{{ function_name }}下载导入模板', 3, 8, {{ b_true }}, '{{ permission_prefix }}:download', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}下载导入模板', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
{% elif db_type == 'postgres' %}
-- 菜单 SQLPostgreSQL DO 块方案)
DO $$
DECLARE
parent_id INTEGER;
BEGIN
-- 插入父菜单并获取ID
INSERT INTO public.system_menu (
name, type, {{ order_col }}, status, permission, icon, route_name, route_path,
component_path, redirect, hidden, keep_alive, always_show, title,
params, affix, parent_id, description, created_at, updated_at
)
VALUES (
'{{ function_name }}',
2,
1,
{{ b_true }},
'{{ permission_prefix }}:query',
NULL,
'{{ business_name|snake_to_camel }}',
'/{{ module_name }}/{{ business_name }}',
'{{ module_name }}/{{ business_name }}/index',
NULL,
{{ b_false }},
{{ b_true }},
{{ b_false }},
'{{ function_name }}',
NULL,
{{ b_false }},
{{ parent_menu_id }},
'{{ function_name }}菜单',
NOW(),
NOW()
) RETURNING id INTO parent_id;
-- 插入所有子菜单按钮(单条 INSERT 语句,性能更好)
INSERT INTO public.system_menu (
name, type, {{ order_col }}, status, permission, icon, route_name, route_path,
component_path, redirect, hidden, keep_alive, always_show, title,
params, affix, parent_id, description, created_at, updated_at
) VALUES
('{{ function_name }}查询', 3, 1, {{ b_true }}, '{{ permission_prefix }}:query', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}查询', NULL, {{ b_false }}, parent_id, NULL, NOW(), NOW()),
('{{ function_name }}新增', 3, 2, {{ b_true }}, '{{ permission_prefix }}:create', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}新增', NULL, {{ b_false }}, parent_id, NULL, NOW(), NOW()),
('{{ function_name }}修改', 3, 3, {{ b_true }}, '{{ permission_prefix }}:update', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}修改', NULL, {{ b_false }}, parent_id, NULL, NOW(), NOW()),
('{{ function_name }}删除', 3, 4, {{ b_true }}, '{{ permission_prefix }}:delete', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}删除', NULL, {{ b_false }}, parent_id, NULL, NOW(), NOW()),
('{{ function_name }}导出', 3, 5, {{ b_true }}, '{{ permission_prefix }}:export', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}导出', NULL, {{ b_false }}, parent_id, NULL, NOW(), NOW()),
('{{ function_name }}导入', 3, 6, {{ b_true }}, '{{ permission_prefix }}:import', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}导入', NULL, {{ b_false }}, parent_id, NULL, NOW(), NOW()),
('{{ function_name }}批量状态修改', 3, 7, {{ b_true }}, '{{ permission_prefix }}:patch', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}批量状态修改', NULL, {{ b_false }}, parent_id, NULL, NOW(), NOW()),
('{{ function_name }}下载导入模板', 3, 8, {{ b_true }}, '{{ permission_prefix }}:download', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}下载导入模板', NULL, {{ b_false }}, parent_id, NULL, NOW(), NOW());
-- 可选:输出插入的父菜单ID(调试用)
RAISE NOTICE '{{ function_name }}菜单创建完成,父菜单ID: %', parent_id;
END $$;
{% else %}
-- 未识别的数据库类型,默认按 MySQL 处理
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
VALUES ('{{ function_name }}', 2, 1, {{ b_true }}, '{{ permission_prefix }}:query', NULL, '{{ business_name|snake_to_camel }}', '/{{ module_name }}/{{ business_name }}', '{{ module_name }}/{{ business_name }}/index', NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}', NULL, {{ b_false }}, {{ parent_menu_id }}, '{{ function_name }}菜单', now(), now());
SELECT @parentId := LAST_INSERT_ID();
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
VALUES ('{{ function_name }}查询', 3, 1, {{ b_true }}, '{{ permission_prefix }}:query', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}查询', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
VALUES ('{{ function_name }}新增', 3, 2, {{ b_true }}, '{{ permission_prefix }}:create', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}新增', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
VALUES ('{{ function_name }}修改', 3, 3, {{ b_true }}, '{{ permission_prefix }}:update', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}修改', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
VALUES ('{{ function_name }}删除', 3, 4, {{ b_true }}, '{{ permission_prefix }}:delete', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}删除', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
VALUES ('{{ function_name }}导出', 3, 5, {{ b_true }}, '{{ permission_prefix }}:export', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}导出', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
VALUES ('{{ function_name }}导入', 3, 6, {{ b_true }}, '{{ permission_prefix }}:import', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}导入', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
VALUES ('{{ function_name }}批量状态修改', 3, 7, {{ b_true }}, '{{ permission_prefix }}:patch', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}批量状态修改', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
VALUES ('{{ function_name }}下载导入模板', 3, 8, {{ b_true }}, '{{ permission_prefix }}:download', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}下载导入模板', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
{% endif %}
@@ -0,0 +1,132 @@
import request from "@/utils/request";
const API_PATH = "/{{ module_name }}/{{ business_name|lower }}";
// 参考 demo.ts 的风格,提供标准的 CRUD 与导入/导出 APITypeScript
const {{ class_name }}API = {
// 列表查询
list{{ class_name }}(query: {{ class_name }}PageQuery) {
return request<ApiResponse<PageResult<{{ class_name }}Table[]>>>({
url: `${API_PATH}/list`,
method: "get",
params: query,
});
},
// 详情查询
detail{{ class_name }}(id: number) {
return request<ApiResponse<{{ class_name }}Table>>({
url: `${API_PATH}/detail/${id}`,
method: "get",
});
},
// 新增
create{{ class_name }}(data: {{ class_name }}Form) {
return request<ApiResponse>({
url: `${API_PATH}/create`,
method: "post",
data,
});
},
// 修改(带主键)
update{{ class_name }}(id: number, data: {{ class_name }}Form) {
return request<ApiResponse>({
url: `${API_PATH}/update/${id}`,
method: "put",
data,
});
},
// 删除(支持批量)
delete{{ class_name }}(ids: number[]) {
return request<ApiResponse>({
url: `${API_PATH}/delete`,
method: "delete",
data: ids,
});
},
// 导出
export{{ class_name }}(query: {{ class_name }}PageQuery) {
return request<Blob>({
url: `${API_PATH}/export`,
method: "post",
data: query,
responseType: "blob",
});
},
// 下载导入模板
downloadTemplate{{ class_name }}() {
return request<Blob>({
url: `${API_PATH}/download/template`,
method: "post",
responseType: "blob",
});
},
// 导入
import{{ class_name }}(data: FormData) {
return request<ApiResponse>({
url: `${API_PATH}/import`,
method: "post",
data,
headers: { "Content-Type": "multipart/form-data" },
});
},
// 批量启用/停用
batchAvailable{{ class_name }}(body: { ids: number[]; status: boolean }) {
return request<ApiResponse>({
url: `${API_PATH}/available/setting`,
method: "patch",
data: body,
});
},
};
export default {{ class_name }}API;
// ------------------------------
// TS 类型声明
// ------------------------------
export interface {{ class_name }}PageQuery extends PageQuery {
{% for column in columns %}
{% if column.is_query == "1" and column.query_type != "BETWEEN" %}
{{ column.python_field }}?: {{
'boolean' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio')
else 'number' if column.is_pk == '1'
else 'string'
}};
{% endif %}
{% endfor %}
// 时间范围查询(按示例统一字段名,如需按字段拆分可在页面层处理)
start_time?: string;
end_time?: string;
}
export interface {{ class_name }}Table {
{% for column in columns %}
{{ column.python_field }}?: {{
'boolean' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio')
else 'number' if column.is_pk == '1'
else 'string'
}};
{% endfor %}
creator?: creatorType;
}
export interface {{ class_name }}Form {
id?: number;
{% for column in columns %}
{% if column.is_insert == "1" or column.is_edit == "1" %}
{{ column.python_field }}?: {{
'boolean' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio')
else 'number' if column.is_pk == '1'
else 'string'
}};
{% endif %}
{% endfor %}
}
@@ -0,0 +1,646 @@
<template>
<div class="app-container">
<!-- 搜索区域 -->
<div v-show="visible" class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" label-suffix=":" :inline="true" @submit.prevent="handleQuery">
{% for column in columns %}
{% if column.is_query == "1" %}
{% set dict_type = column.dict_type %}
{% set column_comment = column.column_comment if column.column_comment else '' %}
{% set parentheseIndex = column_comment.find("") %}
{% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %}
{% if column.html_type == "input" %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
<el-input v-model="queryFormData.{{ column.python_field }}" placeholder="请输入{{ comment }}" clearable />
</el-form-item>
{% elif (column.html_type == "select" or column.html_type == "radio") and dict_type != "" %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
<el-select v-model="queryFormData.{{ column.python_field }}" placeholder="请选择{{ comment }}" style="width: 180px" clearable>
<el-option v-for="dict in dictStore.getDictArray('{{ dict_type }}')" :key="dict.dict_value" :label="dict.dict_label" :value="dict.dict_value" />
</el-select>
</el-form-item>
{% elif (column.html_type == "select" or column.html_type == "radio") and dict_type %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
<el-select v-model="queryFormData.{{ column.python_field }}" placeholder="请选择{{ comment }}" clearable>
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
{% elif column.html_type == "datetime" and column.query_type != "BETWEEN" %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
<el-date-picker v-model="queryFormData.{{ column.python_field }}" type="date" value-format="YYYY-MM-DD" clearable placeholder="请选择{{ comment }}" />
</el-form-item>
{% endif %}
{% endif %}
{% endfor %}
<!-- 可选:创建人选择与统一日期范围(展开后显示) -->
<el-form-item v-if="isExpand" prop="creator" label="创建人">
<UserTableSelect v-model="queryFormData.creator" @confirm-click="handleConfirm" @clear-click="handleQuery" />
</el-form-item>
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
<DatePicker v-model="dateRange" @update:model-value="handleDateRangeChange" />
</el-form-item>
<el-form-item>
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:query']" type="primary" icon="search" @click="handleQuery">查询</el-button>
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
<template v-if="isExpandable">
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
{{ '{{' }} isExpand ? "收起" : "展开" {{ '}}' }}
<el-icon>
<template v-if="isExpand">
<ArrowUp />
</template>
<template v-else>
<ArrowDown />
</template>
</el-icon>
</el-link>
</template>
</el-form-item>
</el-form>
</div>
<!-- 内容区域 -->
<el-card shadow="hover" class="data-table">
<template #header>
<div class="card-header">
<span>
<el-tooltip content="{{ function_name }}列表">
<QuestionFilled class="w-4 h-4 mx-1" />
</el-tooltip>
{{ function_name }}列表
</span>
</div>
</template>
<!-- 功能区域 -->
<div class="data-table__toolbar">
<div class="data-table__toolbar--left">
<el-row :gutter="10">
<el-col :span="1.5">
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:create']" type="success" icon="plus" @click="handleOpenDialog('create')">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:delete']" type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
</el-col>
<el-col :span="1.5">
<el-dropdown v-hasPerm="['{{ module_name }}:{{ business_name }}:batch']" trigger="click">
<el-button type="default" :disabled="selectIds.length === 0" icon="ArrowDown">更多</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item icon="Check" @click="handleMoreClick(true)">批量启用</el-dropdown-item>
<el-dropdown-item icon="CircleClose" @click="handleMoreClick(false)">批量停用</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-col>
</el-row>
</div>
<div class="data-table__toolbar--right">
<el-row :gutter="10">
<el-col :span="1.5">
<el-tooltip content="导入">
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:import']" type="success" icon="upload" circle @click="handleOpenImportDialog" />
</el-tooltip>
</el-col>
<el-col :span="1.5">
<el-tooltip content="导出">
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:export']" type="warning" icon="download" circle @click="handleOpenExportsModal" />
</el-tooltip>
</el-col>
<el-col :span="1.5">
<el-tooltip content="搜索显示/隐藏">
<el-button v-hasPerm="['*:*:*']" type="info" icon="search" circle @click="visible = !visible" />
</el-tooltip>
</el-col>
<el-col :span="1.5">
<el-tooltip content="刷新">
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:refresh']" type="primary" icon="refresh" circle @click="handleRefresh" />
</el-tooltip>
</el-col>
<el-col :span="1.5">
<el-popover placement="bottom" trigger="click">
<template #reference>
<el-button type="danger" icon="operation" circle></el-button>
</template>
<el-scrollbar max-height="350px">
<template v-for="column in tableColumns" :key="column.prop">
<el-checkbox v-if="column.prop" v-model="column.show" :label="column.label" />
</template>
</el-scrollbar>
</el-popover>
</el-col>
</el-row>
</div>
</div>
<!-- 表格区域 -->
<el-table
ref="dataTableRef"
v-loading="loading"
:data="pageTableData"
highlight-current-row
class="data-table__content"
:height="450"
border
stripe
@selection-change="handleSelectionChange"
>
<template #empty>
<el-empty :image-size="80" description="暂无数据" />
</template>
<el-table-column v-if="tableColumns.find((col) => col.prop === 'selection')?.show" type="selection" min-width="55" align="center" />
<el-table-column v-if="tableColumns.find((col) => col.prop === 'index')?.show" fixed label="序号" min-width="60">
<template #default="scope">
{{ '{{' }} (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 {{ '}}' }}
</template>
</el-table-column>
{% for column in columns %}
{% set python_field = column.python_field %}
{% set column_comment = column.column_comment if column.column_comment else '' %}
{% set parentheseIndex = column_comment.find("") %}
{% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %}
{% if column.is_list == "1" %}
<el-table-column v-if="tableColumns.find((col) => col.prop === '{{ python_field }}')?.show" label="{{ comment }}" prop="{{ python_field }}" min-width="140">
{% if python_field == "status" %}
<template #default="scope">
<el-tag :type="scope.row.status ? 'success' : 'info'">{{ '{{' }} scope.row.status ? '启用' : '停用' {{ '}}' }}</el-tag>
</template>
{% elif python_field == "creator" %}
<template #default="scope">
<el-tag>{{ '{{' }} scope.row.creator?.name {{ '}}' }}</el-tag>
</template>
{% endif %}
</el-table-column>
{% endif %}
{% endfor %}
<el-table-column v-if="tableColumns.find(col => col.prop === 'operation')?.show" fixed="right" label="操作" align="center" min-width="180">
<template #default="scope">
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:detail']" type="info" size="small" link icon="document" @click="handleOpenDialog('detail', scope.row.id)">详情</el-button>
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:update']" type="primary" size="small" link icon="edit" @click="handleOpenDialog('update', scope.row.id)">编辑</el-button>
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:delete']" type="danger" size="small" link icon="delete" @click="handleDelete([scope.row.id])">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页区域 -->
<template #footer>
<pagination v-model:total="total" v-model:page="queryFormData.page_no" v-model:limit="queryFormData.page_size" @pagination="loadingData" />
</template>
</el-card>
<!-- 弹窗区域 -->
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">
<!-- 详情 -->
<template v-if="dialogVisible.type === 'detail'">
<el-descriptions :column="4" border>
{% for column in columns %}
{% set python_field = column.python_field %}
{% set column_comment = column.column_comment if column.column_comment else '' %}
{% set parentheseIndex = column_comment.find("") %}
{% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %}
<el-descriptions-item label="{{ comment }}" :span="2">
{{ '{' }}{{ '{' }} detailFormData.{{ python_field }} {{ '}' }}{{ '}' }}
</el-descriptions-item>
{% endfor %}
</el-descriptions>
</template>
<!-- 新增、编辑表单 -->
<template v-else>
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-suffix=":" label-width="auto" label-position="right">
{% for column in columns %}
{% if column.is_insert == "1" or column.is_edit == "1" %}
{% set dict_type = column.dict_type %}
{% set column_comment = column.column_comment if column.column_comment else '' %}
{% set parentheseIndex = column_comment.find("") %}
{% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %}
{% set required = 'true' if column.is_nullable == '1' else 'false' %}
{% if column.python_field == "status" %}
<el-form-item label="状态" prop="status" :required="true">
<el-radio-group v-model="formData.status">
<el-radio :value="true">启用</el-radio>
<el-radio :value="false">停用</el-radio>
</el-radio-group>
</el-form-item>
{% elif column.html_type == "input" %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}" :required="{{ required }}">
<el-input v-model="formData.{{ column.python_field }}" placeholder="请输入{{ comment }}" />
</el-form-item>
{% elif column.html_type == "textarea" %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}" :required="{{ required }}">
<el-input v-model="formData.{{ column.python_field }}" type="textarea" placeholder="请输入{{ comment }}" rows="4" />
</el-form-item>
{% elif (column.html_type == "select" or column.html_type == "radio") and dict_type != "" %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}" :required="{{ required }}">
<el-select v-model="formData.{{ column.python_field }}" placeholder="请选择{{ comment }}">
<el-option v-for="dict in dictStore.getDictArray('{{ dict_type }}')" :key="dict.dict_value" :label="dict.dict_label" :value="dict.dict_value" />
</el-select>
</el-form-item>
{% elif (column.html_type == "select" or column.html_type == "radio") and dict_type %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}" :required="{{ required }}">
<el-select v-model="formData.{{ column.python_field }}" placeholder="请选择{{ comment }}">
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
{% elif column.html_type == "date" %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}" :required="{{ required }}">
<el-date-picker v-model="formData.{{ column.python_field }}" type="date" value-format="YYYY-MM-DD" placeholder="请选择{{ comment }}" />
</el-form-item>
{% elif column.html_type == "datetime" %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}" :required="{{ required }}">
<el-date-picker v-model="formData.{{ column.python_field }}" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择{{ comment }}" />
</el-form-item>
{% elif column.html_type == "checkbox" %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
<el-checkbox v-model="formData.{{ column.python_field }}">{{ comment }}</el-checkbox>
</el-form-item>
{% elif column.html_type == "imageUpload" %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
<SingleImageUpload v-model="formData.{{ column.python_field }}" />
</el-form-item>
{% endif %}
{% endif %}
{% endfor %}
</el-form>
</template>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCloseDialog">取消</el-button>
<el-button v-if="dialogVisible.type !== 'detail'" v-hasPerm="['{{ module_name }}:{{ business_name }}:submit']" type="primary" @click="handleSubmit">确定</el-button>
<el-button v-else v-hasPerm="['{{ module_name }}:{{ business_name }}:detail']" type="primary" @click="handleCloseDialog">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 导入弹窗 -->
<ImportModal
v-model="importDialogVisible"
:content-config="curdContentConfig"
@upload="handleUpload"
/>
<!-- 导出弹窗 -->
<ExportModal
v-model="exportsDialogVisible"
:content-config="curdContentConfig"
:query-params="queryFormData"
:page-data="pageTableData"
:selection-data="selectionRows"
/>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: "{{ class_name }}",
inheritAttrs: false,
});
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ResultEnum } from '@/enums/api/result.enum'
import { QuestionFilled, ArrowUp, ArrowDown, Check, CircleClose } from '@element-plus/icons-vue'
import { formatToDateTime } from '@/utils/dateUtil'
import {{ class_name }}API, { {{ class_name }}PageQuery, {{ class_name }}Table, {{ class_name }}Form } from '@/api/{{ module_name }}/{{ business_name }}'
import { useDictStore } from '@/store/index'
import SingleImageUpload from '@/components/Upload/SingleImageUpload.vue'
import ImportModal from '@/components/CURD/ImportModal.vue'
import ExportModal from '@/components/CURD/ExportModal.vue'
import DatePicker from '@/components/DatePicker/index.vue'
import type { IContentConfig } from '@/components/CURD/types'
const visible = ref(true)
const isExpand = ref(false)
const isExpandable = ref(true)
const queryFormRef = ref()
const dataFormRef = ref()
const total = ref(0)
const selectIds = ref<number[]>([])
const selectionRows = ref<{{ class_name }}Table[]>([]);
const loading = ref(false)
// 字典仓库与需要加载的字典类型
const dictStore = useDictStore()
const dictTypes = [
{% for column in columns %}
{% if column.dict_type %}
'{{ column.dict_type }}',
{% endif %}
{% endfor %}
]
// 分页表单
const pageTableData = ref<{{ class_name }}Table[]>([]);
// 表格列配置(根据列生成,可显隐)
const tableColumns = ref([
{ prop: 'selection', label: '选择框', show: true },
{ prop: 'index', label: '序号', show: true },
{% for column in columns %}
{% if column.is_list == "1" %}
{ prop: '{{ column.python_field }}', label: '{{ column.column_comment or column.python_field }}', show: true },
{% endif %}
{% endfor %}
{ prop: 'operation', label: '操作', show: true }
])
// 导出列(不含选择/序号/操作)
const exportColumns = [
{% for column in columns %}
{% if column.is_list == "1" %}
{ prop: '{{ column.python_field }}', label: '{{ column.column_comment or column.python_field }}' },
{% endif %}
{% endfor %}
]
// 导入/导出配置
const curdContentConfig = {
permPrefix: '{{ module_name }}:{{ business_name }}',
cols: exportColumns as any,
importTemplate: () => {{ class_name }}API.downloadTemplate{{ class_name }}(),
exportsAction: async (params: any) => {
const query: any = { ...params };
if (typeof query.status === 'string') {
query.status = query.status === 'true';
}
query.page_no = 1;
query.page_size = 9999;
const all: any[] = [];
while (true) {
const res = await {{ class_name }}API.list{{ class_name }}(query)
const items = res.data?.data?.items || []
const total = res.data?.data?.total || 0
all.push(...items)
if (all.length >= total || items.length === 0) break
query.page_no += 1
}
return all;
},
} as unknown as IContentConfig
// 弹窗状态
const dialogVisible = reactive({
title: '',
visible: false,
type: 'create', // 'create' | 'update' | 'detail'
})
// 编辑表单
const formData = reactive<{{ class_name }}Form>({
id: undefined,
{% for column in columns %}
{% if column.is_insert == "1" or column.is_edit == "1" %}
{{ column.python_field }}: undefined,
{% endif %}
{% endfor %}
})
// 定义初始表单数据常量
const initialFormData: {{ class_name }}Form = {
id: undefined,
{% for column in columns %}
{% if column.is_insert == "1" or column.is_edit == "1" %}
{{ column.python_field }}: {{ 'true' if column.python_field == 'status' else ('' if column.html_type == 'textarea' else 'undefined') }},
{% endif %}
{% endfor %}
}
// 重置表单
async function resetForm() {
if (dataFormRef.value) {
dataFormRef.value.resetFields();
dataFormRef.value.clearValidate();
}
// 完全重置 formData 为初始状态
Object.assign(formData, initialFormData);
}
// 表单验证规则(必填项按 is_nullable 生成)
const rules = reactive({
{% for column in columns %}
{% if column.is_insert == "1" or column.is_edit == "1" %}
{% set required = 'true' if column.is_nullable == '1' else 'false' %}
{{ column.python_field }}: [
{ required: {{ required }}, message: '请输入{{ column.column_comment or column.python_field }}', trigger: 'blur' },
],
{% endif %}
{% endfor %}
})
// 详情表单
const detailFormData = ref<{{ class_name }}Table>({});
// 统一日期范围
const dateRange = ref<[Date, Date] | []>([]);
function handleDateRangeChange(range: [Date, Date]) {
dateRange.value = range;
if (range && range.length === 2) {
queryFormData.start_time = formatToDateTime(range[0]);
queryFormData.end_time = formatToDateTime(range[1]);
} else {
queryFormData.start_time = undefined;
queryFormData.end_time = undefined;
}
}
// 查询参数
const queryFormData = reactive<{{ class_name }}PageQuery>({
page_no: 1,
page_size: 10,
{% for column in columns %}
{% if column.is_query == "1" and column.query_type != "BETWEEN" %}
{{ column.python_field }}: undefined,
{% endif %}
{% endfor %}
start_time: undefined,
end_time: undefined,
creator: undefined,
})
// 加载表格数据
async function loadingData() {
loading.value = true;
try {
const response = await {{ class_name }}API.list{{ class_name }}(queryFormData);
pageTableData.value = response.data.data.items;
total.value = response.data.data.total;
} catch (error) {
console.error(error);
} finally {
loading.value = false;
}
}
// 查询(重置页码后获取数据)
async function handleQuery() {
queryFormData.page_no = 1;
loadingData();
}
// 选择创建人后触发查询
function handleConfirm() {
handleQuery()
}
// 重置查询
async function handleResetQuery() {
queryFormRef.value.resetFields();
queryFormData.page_no = 1;
dateRange.value = [];
queryFormData.start_time = undefined;
queryFormData.end_time = undefined;
loadingData();
}
// 行复选框选中项变化
function handleSelectionChange(selection: any[]) {
selectIds.value = selection.map((item: any) => item.id)
selectionRows.value = selection
}
// 关闭弹窗
async function handleCloseDialog() {
dialogVisible.visible = false;
resetForm();
}
// 打开弹窗
async function handleOpenDialog(type: 'create' | 'update' | 'detail', id?: number) {
dialogVisible.type = type
if (id) {
const response = await {{ class_name }}API.detail{{ class_name }}(id);
if (type === 'detail') {
dialogVisible.title = '详情';
Object.assign(detailFormData.value, response.data.data);
} else if (type === 'update') {
dialogVisible.title = '修改';
Object.assign(formData, response.data.data);
}
} else {
dialogVisible.title = '新增{{ function_name }}';
formData.id = undefined;
}
dialogVisible.visible = true;
}
// 提交表单
async function handleSubmit() {
dataFormRef.value.validate(async (valid: any) => {
if (valid) {
loading.value = true
try {
const id = formData.id
if (id) {
await {{ class_name }}API.update{{ class_name }}(id, { id, ...formData });
dialogVisible.visible = false;
resetForm();
handleCloseDialog();
handleResetQuery();
} else {
await {{ class_name }}API.create{{ class_name }}(formData);
dialogVisible.visible = false;
resetForm();
handleCloseDialog();
handleResetQuery();
}
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
})
}
// 删除、批量删除
async function handleDelete(ids: number[]) {
ElMessageBox.confirm('确认删除该项数据?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(async () => {
try {
loading.value = true;
await {{ class_name }}API.delete{{ class_name }}(ids);
handleResetQuery()
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
})
.catch(() => {
ElMessageBox.close()
})
}
// 批量启用/停用
async function handleMoreClick(status: boolean) {
if (selectIds.value.length) {
ElMessageBox.confirm(`确认${status ? '启用' : '停用'}该项数据?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
try {
loading.value = true
await {{ class_name }}API.batchAvailable{{ class_name }}({ ids: selectIds.value, status });
handleResetQuery()
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}).catch(() => {
ElMessageBox.close()
})
}
}
// 导入弹窗显示状态
const importDialogVisible = ref(false)
// 导出弹窗显示状态
const exportsDialogVisible = ref(false)
// 打开导入弹窗
function handleOpenImportDialog() {
importDialogVisible.value = true
}
// 打开导出弹窗
function handleOpenExportsModal() {
exportsDialogVisible.value = true
}
// 处理上传
const handleUpload = async (formData: FormData) => {
try {
const response = await {{ class_name }}API.import{{ class_name }}(formData);
if (response.data.code === ResultEnum.SUCCESS) {
ElMessage.success(`${response.data.msg}${response.data.data}`)
importDialogVisible.value = false
await handleQuery()
}
} catch (error) {
console.error(error)
}
}
// 列表刷新
async function handleRefresh() {
await loadingData()
}
onMounted(async () => {
// 预加载字典数据
if (dictTypes.length > 0) {
await dictStore.getDict(dictTypes)
}
loadingData()
})
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,228 @@
# -*- coding: utf-8 -*-
import re
from typing import List
from app.common.constant import GenConstant
from app.utils.string_util import StringUtil
from app.api.v1.module_generator.gencode.schema import GenTableOutSchema, GenTableSchema, GenTableColumnSchema
class GenUtils:
"""代码生成器工具类"""
@classmethod
def init_table(cls, gen_table: GenTableSchema) -> None:
"""
初始化表信息
参数:
- gen_table (GenTableSchema): 业务表对象。
返回:
- None
"""
# 只有当字段为None时才设置默认值
gen_table.class_name = cls.convert_class_name(gen_table.table_name or "")
gen_table.package_name = 'module_gencode'
gen_table.module_name = gen_table.package_name.split('.')[-1]
gen_table.business_name = gen_table.table_name
gen_table.function_name = re.sub(r'(?:表|测试)', '', gen_table.table_comment or "")
@classmethod
def init_column_field(cls, column: GenTableColumnSchema, table: GenTableOutSchema) -> None:
"""
初始化列属性字段
参数:
- column (GenTableColumnSchema): 业务表字段对象。
- table (GenTableOutSchema): 业务表对象。
返回:
- None
"""
data_type = cls.get_db_type(column.column_type or "")
column_name = column.column_name or ""
if not table.id:
raise ValueError("业务表ID不能为空")
column.table_id = table.id
column.python_field = cls.to_camel_case(column_name)
# 只有当python_type为None时才设置默认类型
column.python_type = StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_PYTHON, data_type)
if column.column_length is None:
column.column_length = ''
if column.column_default is None:
column.column_default = ''
if column.html_type is None:
if cls.arrays_contains(GenConstant.COLUMNTYPE_STR, data_type) or cls.arrays_contains(
GenConstant.COLUMNTYPE_TEXT, data_type
):
# 字符串长度超过500设置为文本域
column_length = cls.get_column_length(column.column_type or "")
html_type = (
GenConstant.HTML_TEXTAREA
if column_length >= 500 or cls.arrays_contains(GenConstant.COLUMNTYPE_TEXT, data_type)
else GenConstant.HTML_INPUT
)
column.html_type = html_type
elif cls.arrays_contains(GenConstant.COLUMNTYPE_TIME, data_type):
column.html_type = GenConstant.HTML_DATETIME
elif cls.arrays_contains(GenConstant.COLUMNTYPE_NUMBER, data_type):
column.html_type = GenConstant.HTML_INPUT
elif column_name.lower().endswith("status"):
column.html_type = GenConstant.HTML_RADIO
elif column_name.lower().endswith("type") or column_name.lower().endswith("sex"):
column.html_type = GenConstant.HTML_SELECT
elif column_name.lower().endswith("image"):
column.html_type = GenConstant.HTML_IMAGE_UPLOAD
elif column_name.lower().endswith("file"):
column.html_type = GenConstant.HTML_FILE_UPLOAD
elif column_name.lower().endswith("content"):
column.html_type = GenConstant.HTML_EDITOR
else:
column.html_type = GenConstant.HTML_INPUT
# 只有当is_insert为None时才设置插入字段(默认所有字段都需要插入)
if column.is_insert:
column.is_insert = GenConstant.REQUIRE
else:
column.is_insert = False
# 只有当is_edit为None时才设置编辑字段
if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_EDIT, column_name) and not column.is_pk:
column.is_edit = GenConstant.REQUIRE
else:
column.is_edit = False
# 只有当is_list为None时才设置列表字段
if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_LIST, column_name) and not column.is_pk:
column.is_list = GenConstant.REQUIRE
else:
column.is_list = False
# 只有当is_query为None时才设置查询字段
if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_QUERY, column_name) and not column.is_pk:
column.is_query = GenConstant.REQUIRE
# 直接设置查询类型,因为我们已经确定这是一个查询字段
if column_name.lower().endswith('name') or data_type in ['varchar', 'char', 'text']:
column.query_type = GenConstant.QUERY_LIKE
else:
column.query_type = GenConstant.QUERY_EQ
else:
column.is_query = False
column.query_type = None
@classmethod
def arrays_contains(cls, arr, target_value) -> bool:
"""
检查目标值是否在数组中
注意:从根本上解决问题,现在确保传入的参数都是正确的类型:
- arr 是列表类型,且在GenConstant中定义
- target_value 不会是None
参数:
- arr: 数组类型
- target_value: 目标值
返回:
- bool: 如果目标值在数组中,返回True;否则返回False
"""
# 从根本上解决问题,不再需要复杂的防御性检查
# 因为现在我们确保传入的arr是GenConstant中定义的列表常量
# 并且target_value在调用前已经被处理过不会是None
# 简单直接地执行包含检查
target_str = str(target_value).lower()
return any(str(item).lower() == target_str for item in arr)
@classmethod
def convert_class_name(cls, table_name: str) -> str:
"""
表名转换成 Python 类名
参数:
- table_name (str): 业务表名。
返回:
- str: Python 类名。
"""
return StringUtil.convert_to_camel_case(table_name)
@classmethod
def replace_first(cls, input_string: str, search_list: List[str]) -> str:
"""
批量替换前缀
参数:
- input_string (str): 需要被替换的字符串。
- search_list (List[str]): 可替换的字符串列表。
返回:
- str: 替换后的字符串。
"""
for search_string in search_list:
if input_string.startswith(search_string):
return input_string.replace(search_string, '', 1)
return input_string
@classmethod
def get_db_type(cls, column_type: str) -> str:
"""
获取数据库类型字段
参数:
- column_type (str): 字段类型。
返回:
- str: 数据库类型。
"""
if '(' in column_type:
return column_type.split('(')[0]
return column_type
@classmethod
def get_column_length(cls, column_type: str) -> int:
"""
获取字段长度
参数:
- column_type (str): 字段类型,例如 'varchar(255)''decimal(10,2)'
返回:
- int: 字段长度(优先取第一个长度值,无法解析时返回0)。
"""
if '(' in column_type:
length = len(column_type.split('(')[1].split(')')[0])
return length
return 0
@classmethod
def split_column_type(cls, column_type: str) -> List[str]:
"""
拆分列类型
参数:
- column_type (str): 字段类型。
返回:
- List[str]: 拆分结果。
"""
if '(' in column_type and ')' in column_type:
return column_type.split('(')[1].split(')')[0].split(',')
return []
@classmethod
def to_camel_case(cls, text: str) -> str:
"""
将字符串转换为驼峰命名
param text: 需要转换的字符串
:return: 驼峰命名
"""
parts = text.split('_')
return parts[0] + ''.join(word.capitalize() for word in parts[1:])
@@ -0,0 +1,395 @@
# -*- coding:utf-8 -*-
from datetime import datetime
from jinja2.environment import Environment
from jinja2 import Environment, FileSystemLoader, select_autoescape, Template
from typing import List, Any, Set
from app.common.constant import GenConstant
from app.config.path_conf import TEMPLATE_DIR
from app.config.setting import settings
from app.utils.common_util import CamelCaseUtil, SnakeCaseUtil
from app.utils.string_util import StringUtil
from app.api.v1.module_generator.gencode.schema import GenTableOutSchema, GenTableColumnOutSchema
class Jinja2TemplateUtil:
"""
模板处理工具类
"""
# 项目路径
FRONTEND_PROJECT_PATH = 'frontend'
BACKEND_PROJECT_PATH = 'backend'
# 默认上级菜单,系统工具
DEFAULT_PARENT_MENU_ID = "3"
# 环境对象
_env = None
@classmethod
def get_env(cls):
"""
获取模板环境对象。
参数:
- 无
返回:
- Environment: Jinja2 环境对象。
"""
try:
if cls._env is None:
cls._env = Environment(
loader=FileSystemLoader(TEMPLATE_DIR),
autoescape=False, # 自动转义HTML
trim_blocks=True, # 删除多余的空行
lstrip_blocks=True, # 删除行首空格
keep_trailing_newline=True, # 保留行尾换行符
enable_async=True, # 开启异步支持
)
cls._env.filters.update(
{
'camel_to_snake': SnakeCaseUtil.camel_to_snake,
'snake_to_camel': CamelCaseUtil.snake_to_camel,
'get_sqlalchemy_type': cls.get_sqlalchemy_type,
}
)
return cls._env
except Exception as e:
raise RuntimeError(f'初始化Jinja2模板引擎失败: {e}')
@classmethod
def get_template(cls, template_path: str) -> Template:
"""
获取模板。
参数:
- template_path (str): 模板路径。
返回:
- Template: Jinja2 模板对象。
异常:
- TemplateNotFound: 模板未找到时抛出。
"""
return cls.get_env().get_template(template_path)
@classmethod
def prepare_context(cls, gen_table: GenTableOutSchema) -> dict[str, Any]:
"""
准备模板变量。
参数:
- gen_table (GenTableOutSchema): 生成表的配置信息。
返回:
- Dict[str, Any]: 模板上下文字典。
"""
# 处理options为None的情况
# if not gen_table.options:
# raise ValueError('请先完善生成配置信息')
class_name = gen_table.class_name or ''
module_name = gen_table.module_name or ''
business_name = gen_table.business_name or ''
package_name = gen_table.package_name or ''
function_name = gen_table.function_name or ''
context = {
'table_name': gen_table.table_name or '',
'table_comment': gen_table.table_comment or '',
'function_name': function_name if StringUtil.is_not_empty(function_name) else '【请填写功能名称】',
'class_name': class_name,
'module_name': module_name,
'business_name': business_name,
'base_package': cls.get_package_prefix(package_name),
'package_name': package_name,
'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'pk_column': gen_table.pk_column,
'model_import_list': cls.get_model_import_list(gen_table),
'schema_import_list': cls.get_schema_import_list(gen_table),
'permission_prefix': cls.get_permission_prefix(module_name, business_name),
'columns': gen_table.columns or [],
'table': gen_table,
'dicts': cls.get_dicts(gen_table),
'db_type': settings.DATABASE_TYPE,
'column_not_add_show': GenConstant.COLUMNNAME_NOT_ADD_SHOW,
'column_not_edit_show': GenConstant.COLUMNNAME_NOT_EDIT_SHOW,
'parent_menu_id': int(gen_table.parent_menu_id) if gen_table.parent_menu_id is not None else int(cls.DEFAULT_PARENT_MENU_ID),
}
return context
@classmethod
def get_template_list(cls):
"""
获取模板列表。
参数:
- 无
返回:
- List[str]: 模板路径列表。
"""
templates = [
'python/controller.py.j2',
'python/service.py.j2',
'python/crud.py.j2',
'python/schema.py.j2',
'python/param.py.j2',
'python/model.py.j2',
'sql/sql.sql.j2',
'ts/api.ts.j2',
'vue/index.vue.j2',
]
return templates
@classmethod
def get_file_name(cls, template: str, gen_table: GenTableOutSchema):
"""
根据模板生成文件名。
参数:
- template (str): 模板路径字符串。
- gen_table (GenTableOutSchema): 生成表的配置信息。
返回:
- str: 模板生成的文件名。
异常:
- ValueError: 当无法生成有效文件名时抛出。
"""
module_name = gen_table.module_name or ''
business_name = gen_table.business_name or ''
# 验证必要的参数
if not module_name or not business_name:
raise ValueError(f"无法为模板 {template} 生成文件名:模块名或业务名未设置")
# 映射表方式简化
template_mapping = {
'controller.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/controller.py',
'service.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/service.py',
'crud.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/crud.py',
'model.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/model.py',
'param.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/param.py',
'schema.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/schema.py',
'sql.sql.j2': f'{cls.BACKEND_PROJECT_PATH}/sql/menu/{module_name}/{business_name}.sql',
'api.ts.j2': f'{cls.FRONTEND_PROJECT_PATH}/src/api/{module_name}/{business_name}.ts',
'index.vue.j2': f'{cls.FRONTEND_PROJECT_PATH}/src/views/{module_name}/{business_name}/index.vue'
}
# 查找匹配的模板路径
for key, path in template_mapping.items():
if key in template:
return path
# 默认处理
template_name = template.split('/')[-1].replace('.j2', '')
return f'{cls.BACKEND_PROJECT_PATH}/generated/{template_name}'
@classmethod
def get_package_prefix(cls, package_name: str) -> str:
"""
获取包前缀。
参数:
- package_name (str): 包名。
返回:
- str: 包前缀。
"""
# 修复:当包名中不存在'.'时,直接返回原包名
return package_name[: package_name.rfind('.')] if '.' in package_name else package_name
@classmethod
def get_schema_import_list(cls, gen_table: GenTableOutSchema):
"""
获取schema模板导入包列表
:param gen_table: 生成表的配置信息
:return: 导入包列表
"""
columns = gen_table.columns or []
import_list = set()
for column in columns:
if column.python_type in GenConstant.TYPE_DATE:
import_list.add(f'from datetime import {column.python_type}')
elif column.python_type == GenConstant.TYPE_DECIMAL:
import_list.add('from decimal import Decimal')
if gen_table.sub:
if gen_table.sub_table and gen_table.sub_table.columns:
sub_columns = gen_table.sub_table.columns or []
for sub_column in sub_columns:
if sub_column.python_type in GenConstant.TYPE_DATE:
import_list.add(f'from datetime import {sub_column.python_type}')
elif sub_column.python_type == GenConstant.TYPE_DECIMAL:
import_list.add('from decimal import Decimal')
return cls.merge_same_imports(list(import_list), 'from datetime import')
@classmethod
def get_model_import_list(cls, gen_table: GenTableOutSchema):
"""
获取do模板导入包列表
:param gen_table: 生成表的配置信息
:return: 导入包列表
"""
columns = gen_table.columns or []
import_list = set()
for column in columns:
if column.column_type:
data_type = cls.get_db_type(column.column_type)
if data_type in GenConstant.COLUMNTYPE_GEOMETRY:
import_list.add('from geoalchemy2 import Geometry')
import_list.add(
f'from sqlalchemy import {StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY, data_type)}'
)
if gen_table.sub:
import_list.add('from sqlalchemy import ForeignKey')
if gen_table.sub_table and gen_table.sub_table.columns:
sub_columns = gen_table.sub_table.columns or []
for sub_column in sub_columns:
if sub_column.column_type:
data_type = cls.get_db_type(sub_column.column_type)
import_list.add(
f'from sqlalchemy import {StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY, data_type)}'
)
return cls.merge_same_imports(list(import_list), 'from sqlalchemy import')
@classmethod
def get_db_type(cls, column_type: str) -> str:
"""
获取数据库字段类型。
参数:
- column_type (str): 字段类型字符串。
返回:
- str: 数据库类型(去除长度等修饰)。
"""
if '(' in column_type:
return column_type.split('(')[0]
return column_type
@classmethod
def merge_same_imports(cls, imports: List[str], import_start: str) -> List[str]:
"""
合并相同的导入语句。
参数:
- imports (List[str]): 导入语句列表。
- import_start (str): 导入语句的起始字符串。
返回:
- List[str]: 合并后的导入语句列表。
"""
merged_imports = []
_imports = []
for import_stmt in imports:
if import_stmt.startswith(import_start):
imported_items = import_stmt.split('import')[1].strip()
_imports.extend(imported_items.split(', '))
else:
merged_imports.append(import_stmt)
if _imports:
merged_datetime_import = f'{import_start} {", ".join(_imports)}'
merged_imports.append(merged_datetime_import)
return merged_imports
@classmethod
def get_dicts(cls, gen_table: GenTableOutSchema):
"""
获取字典列表。
参数:
- gen_table (GenTableOutSchema): 生成表的配置信息。
返回:
- str: 以逗号分隔的字典类型字符串。
"""
columns = gen_table.columns or []
dicts = set()
cls.add_dicts(dicts, columns)
# 处理sub_table为None的情况
if gen_table.sub_table is not None:
# 处理sub_table.columns为None的情况
sub_columns = gen_table.sub_table.columns or []
cls.add_dicts(dicts, sub_columns)
return ', '.join(dicts)
@classmethod
def add_dicts(cls, dicts: Set[str], columns: List[GenTableColumnOutSchema]):
"""
添加字典类型到集合。
参数:
- dicts (Set[str]): 字典类型集合。
- columns (List[GenTableColumnOutSchema]): 字段列表。
返回:
- Set[str]: 更新后的字典类型集合。
"""
for column in columns:
super_column = column.super_column if column.super_column is not None else '0'
dict_type = column.dict_type or ''
html_type = column.html_type or ''
if (
not super_column
and StringUtil.is_not_empty(dict_type)
and StringUtil.equals_any_ignore_case(
html_type, [GenConstant.HTML_SELECT, GenConstant.HTML_RADIO, GenConstant.HTML_CHECKBOX]
)
):
dicts.add(f"'{dict_type}'")
@classmethod
def get_permission_prefix(cls, module_name: str | None, business_name: str | None) -> str:
"""
获取权限前缀。
参数:
- module_name (str | None): 模块名。
- business_name (str | None): 业务名。
返回:
- str: 权限前缀字符串。
"""
return f'{module_name}:{business_name}'
@classmethod
def get_sqlalchemy_type(cls, column):
"""
获取 SQLAlchemy 类型。
参数:
- column_type (Any): 列类型或包含 `column_type` 属性的对象。
返回:
- str: SQLAlchemy 类型字符串。
"""
if '(' in column:
column_type_list = column.split('(')
if column_type_list[0] in GenConstant.COLUMNTYPE_STR:
sqlalchemy_type = (
StringUtil.get_mapping_value_by_key_ignore_case(
GenConstant.DB_TO_SQLALCHEMY, column_type_list[0]
)
+ '('
+ column_type_list[1]
)
else:
sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case(
GenConstant.DB_TO_SQLALCHEMY, column_type_list[0]
)
else:
sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case(
GenConstant.DB_TO_SQLALCHEMY, column
)
return sqlalchemy_type