16 KiB
背景
当前 contract-template 模块前端页面已经迁移到 Next.js,但核心数据仍依赖旧的 PostgREST 直查表能力:
contract_categoriescontract_templates
受影响页面包括:
/contract-template/search/contract-template/search/results/contract-template/list/contract-template/detail/[id]
现状问题不是“前端页面不存在”,而是“新后端业务接口尚未补齐”。本文补充一版适合当前仓库风格的后端接口设计,覆盖:
- VO/DTO 草稿
- Controller 方法签名建议
- 权限 key 建议
- 代码目录落点建议
目标
为合同模板搜索、列表、详情链路补齐新的 FastAPI 业务接口,替代前端对 PostgREST 的直接依赖。
原则:
- 统一走
fastapi_modules/fastapi_leaudit业务后端 - 不继续扩散 PostgREST 依赖
- 接口命名、分层、权限风格与现有
v3模块保持一致
现有前端依赖梳理
当前前端依赖的旧查询行为包括:
-
分类列表
- 首页展示合同分类
- 需要附带每个分类下的模板数量
-
模板列表
- 支持分页
- 支持按分类筛选
- 支持排序
-
模板搜索
- 支持关键词查询
- 支持分类过滤
- 支持结果分页
- 搜索结果页还需要分类统计
-
模板详情
- 查看模板元数据
- 下载模板
- 预览模板
推荐后端目录结构
建议新增以下文件:
fastapi_modules/fastapi_leaudit/controllers/contractTemplateController.pyfastapi_modules/fastapi_leaudit/services/contractTemplateService.pyfastapi_modules/fastapi_leaudit/services/impl/contractTemplateServiceImpl.pyfastapi_modules/fastapi_leaudit/domian/Dto/contractTemplateDto.pyfastapi_modules/fastapi_leaudit/domian/vo/contractTemplateVo.py
原因:
contract-template是独立业务域,不适合继续塞入homeService或documentService- 当前仓库主要采用“每个业务一个 controller/service/dto/vo”的组织方式
- 前端菜单和 RBAC 已经把
contract-template视作独立模块,后端也应保持同样边界
路由设计建议
建议统一挂在:
/api/v3/contract-templates
建议提供以下接口:
GET /api/v3/contract-templates/categoriesGET /api/v3/contract-templatesGET /api/v3/contract-templates/searchGET /api/v3/contract-templates/{TemplateId}
以上 4 个接口用于替代当前页面的 PostgREST 查询。
VO 设计草案
建议新增文件:
fastapi_modules/fastapi_leaudit/domian/vo/contractTemplateVo.py
草稿如下:
from pydantic import BaseModel, Field
class ContractTemplateCategoryVO(BaseModel):
"""合同模板分类。"""
id: int = Field(..., description="分类ID")
name: str = Field(..., description="分类名称")
icon: str | None = Field(None, description="分类图标")
description: str | None = Field(None, description="分类描述")
sortOrder: int = Field(0, description="排序")
templateCount: int = Field(0, description="分类下模板数量")
isEnabled: bool = Field(True, description="是否启用")
class ContractTemplateListItemVO(BaseModel):
"""合同模板列表项。"""
id: int = Field(..., description="模板ID")
templateCode: str = Field(..., description="模板编码")
title: str = Field(..., description="模板标题")
categoryId: int = Field(..., description="分类ID")
categoryName: str | None = Field(None, description="分类名称")
categoryIcon: str | None = Field(None, description="分类图标")
description: str | None = Field(None, description="模板简介")
filePath: str | None = Field(None, description="原始模板文件路径")
pdfFilePath: str | None = Field(None, description="PDF 预览文件路径")
fileFormat: str = Field(..., description="文件格式")
isFeatured: bool = Field(False, description="是否推荐")
createdAt: str | None = Field(None, description="创建时间")
updatedAt: str | None = Field(None, description="更新时间")
class ContractTemplatePageVO(BaseModel):
"""合同模板分页结果。"""
total: int = Field(..., description="总数")
page: int = Field(..., description="当前页")
pageSize: int = Field(..., description="分页大小")
totalPages: int = Field(..., description="总页数")
templates: list[ContractTemplateListItemVO] = Field(default_factory=list, description="模板列表")
class ContractTemplateDetailVO(ContractTemplateListItemVO):
"""合同模板详情。"""
categoryDescription: str | None = Field(None, description="分类描述")
placeholderSchema: dict | None = Field(None, description="模板占位符结构")
class ContractTemplateSearchCategoryVO(BaseModel):
"""搜索结果分类统计。"""
id: int = Field(..., description="分类ID")
name: str = Field(..., description="分类名称")
searchCount: int = Field(0, description="当前关键词命中的模板数")
class ContractTemplateSearchResultVO(BaseModel):
"""合同模板搜索结果。"""
total: int = Field(..., description="总数")
page: int = Field(..., description="当前页")
pageSize: int = Field(..., description="分页大小")
totalPages: int = Field(..., description="总页数")
templates: list[ContractTemplateListItemVO] = Field(default_factory=list, description="模板列表")
categoryStats: list[ContractTemplateSearchCategoryVO] = Field(default_factory=list, description="分类统计")
DTO 设计草案
建议新增文件:
fastapi_modules/fastapi_leaudit/domian/Dto/contractTemplateDto.py
草稿如下:
from pydantic import BaseModel, Field
class ContractTemplateListQueryDTO(BaseModel):
"""合同模板列表查询参数。"""
keyword: str | None = Field(None, description="关键词")
category_id: int | None = Field(None, description="分类ID")
category_name: str | None = Field(None, description="分类名称")
file_format: str | None = Field(None, description="文件格式")
is_featured: bool | None = Field(None, description="是否推荐")
page: int = Field(1, ge=1, description="页码")
page_size: int = Field(12, ge=1, le=200, description="分页大小")
sort_by: str = Field("updated_at", description="排序字段")
sort_order: str = Field("desc", description="排序方向")
class ContractTemplateSearchQueryDTO(BaseModel):
"""合同模板搜索参数。"""
q: str = Field(..., min_length=1, description="搜索关键词")
category_id: int | None = Field(None, description="分类ID")
page: int = Field(1, ge=1, description="页码")
page_size: int = Field(12, ge=1, le=200, description="分页大小")
sort_by: str = Field("updated_at", description="排序字段")
sort_order: str = Field("desc", description="排序方向")
说明:
- 列表查询和搜索查询可以拆开,保持语义清晰
- 如果后端最终希望统一实现,也可以在 service 层共用同一套内部查询对象
Controller 设计建议
建议新增文件:
fastapi_modules/fastapi_leaudit/controllers/contractTemplateController.py
建议风格对齐现有 BaseController 用法,路由前缀采用:
/v3/contract-templates
方法签名草案:
from fastapi import Depends, Query
from fastapi.responses import JSONResponse
from fastapi_common.fastapi_common_security.security import verify_access_token
from fastapi_common.fastapi_common_web.controller import BaseController
from fastapi_modules.fastapi_leaudit.services.contractTemplateService import IContractTemplateService
from fastapi_modules.fastapi_leaudit.services.impl.contractTemplateServiceImpl import ContractTemplateServiceImpl
from fastapi_modules.fastapi_leaudit.services.impl.permissionServiceImpl import PermissionServiceImpl
from fastapi_modules.fastapi_leaudit.services.permissionService import IPermissionService
class ContractTemplateController(BaseController):
def __init__(self):
super().__init__(prefix="/v3/contract-templates", tags=["合同模板"])
self.ContractTemplateService: IContractTemplateService = ContractTemplateServiceImpl()
self.PermissionService: IPermissionService = PermissionServiceImpl()
@self.router.get("/categories")
async def ListContractTemplateCategories(
include_disabled: bool = Query(False, description="是否包含禁用分类"),
with_template_count: bool = Query(True, description="是否附带模板数量"),
payload: dict = Depends(verify_access_token),
):
...
@self.router.get("")
async def ListContractTemplates(
keyword: str | None = Query(None, description="关键词"),
category_id: int | None = Query(None, description="分类ID"),
category_name: str | None = Query(None, description="分类名称"),
file_format: str | None = Query(None, description="文件格式"),
is_featured: bool | None = Query(None, description="是否推荐"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(12, ge=1, le=200, description="分页大小"),
sort_by: str = Query("updated_at", description="排序字段"),
sort_order: str = Query("desc", description="排序方向"),
payload: dict = Depends(verify_access_token),
):
...
@self.router.get("/search")
async def SearchContractTemplates(
q: str = Query(..., min_length=1, description="搜索关键词"),
category_id: int | None = Query(None, description="分类ID"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(12, ge=1, le=200, description="分页大小"),
sort_by: str = Query("updated_at", description="排序字段"),
sort_order: str = Query("desc", description="排序方向"),
payload: dict = Depends(verify_access_token),
):
...
@self.router.get("/{TemplateId}")
async def GetContractTemplateDetail(
TemplateId: int,
payload: dict = Depends(verify_access_token),
):
...
Service 接口建议
建议新增文件:
fastapi_modules/fastapi_leaudit/services/contractTemplateService.py
接口草案:
from abc import ABC, abstractmethod
from fastapi_modules.fastapi_leaudit.domian.Dto.contractTemplateDto import (
ContractTemplateListQueryDTO,
ContractTemplateSearchQueryDTO,
)
from fastapi_modules.fastapi_leaudit.domian.vo.contractTemplateVo import (
ContractTemplateCategoryVO,
ContractTemplateDetailVO,
ContractTemplatePageVO,
ContractTemplateSearchResultVO,
)
class IContractTemplateService(ABC):
@abstractmethod
async def ListCategories(self, IncludeDisabled: bool, WithTemplateCount: bool) -> list[ContractTemplateCategoryVO]:
raise NotImplementedError
@abstractmethod
async def ListTemplates(self, Query: ContractTemplateListQueryDTO) -> ContractTemplatePageVO:
raise NotImplementedError
@abstractmethod
async def SearchTemplates(self, Query: ContractTemplateSearchQueryDTO) -> ContractTemplateSearchResultVO:
raise NotImplementedError
@abstractmethod
async def GetTemplateDetail(self, TemplateId: int) -> ContractTemplateDetailVO | None:
raise NotImplementedError
ServiceImpl 实现建议
建议新增文件:
fastapi_modules/fastapi_leaudit/services/impl/contractTemplateServiceImpl.py
职责边界建议:
-
ListCategories- 直接查
contract_categories - 如
with_template_count=true,通过聚合一次性返回每类模板数 - 不要像前端当前逻辑那样逐分类循环查询
- 直接查
-
ListTemplates- 负责分页、筛选、排序
- 支持
category_id和category_name - 统一返回
ContractTemplatePageVO
-
SearchTemplates- 可复用
ListTemplates的底层查询逻辑 - 额外补
categoryStats - 搜索条件建议覆盖:
titledescriptiontemplate_code- 分类名称
- 可复用
-
GetTemplateDetail- 查单个模板及所属分类信息
- 必要时回填
placeholderSchema
权限 key 建议
当前仓库权限格式见:
module:resource:action
参考现有:
entry_module:list:readevaluation_point:detail:readrules:create:write
因此建议新增以下权限:
-
contract_template:list:read- 查看模板列表
- 对应:
GET /api/v3/contract-templatesGET /api/v3/contract-templates/categories
-
contract_template:search:read- 使用模板搜索
- 对应:
GET /api/v3/contract-templates/search
-
contract_template:detail:read- 查看模板详情
- 对应:
GET /api/v3/contract-templates/{id}
如果权限体系希望更简化,也可以把 categories 和 search 并入 contract_template:list:read。但从产品语义上,保留 search 独立权限更清晰,方便以后做入口控制和审计。
Controller 权限校验建议
建议:
-
分类接口
- 允许以下任一权限:
contract_template:list:readcontract_template:search:read
-
列表接口
contract_template:list:read
-
搜索接口
contract_template:search:read
-
详情接口
contract_template:detail:read- 或兼容放宽为:
contract_template:detail:readcontract_template:list:read
建议控制器内沿用现有 _check_permission 风格,允许多个 key 中任一通过。
RBAC 权限蓝图补充建议
建议在:
fastapi_modules/fastapi_leaudit/services/impl/rbacAdminServiceImpl.py
的 _MANAGEABLE_PERMISSION_BLUEPRINTS 中补充:
{"permission_key": "contract_template:list:read", "display_name": "查看合同模板列表", "module": "contract_template", "resource": "list", "action": "read", "api_method": "GET", "api_path": "/api/v3/contract-templates", "route_path": "/contract-template/list"},
{"permission_key": "contract_template:search:read", "display_name": "搜索合同模板", "module": "contract_template", "resource": "search", "action": "read", "api_method": "GET", "api_path": "/api/v3/contract-templates/search", "route_path": "/contract-template/search"},
{"permission_key": "contract_template:detail:read", "display_name": "查看合同模板详情", "module": "contract_template", "resource": "detail", "action": "read", "api_method": "GET", "api_path": "/api/v3/contract-templates/{id}", "route_path": "/contract-template/list"},
命名与字段映射建议
数据库字段当前大概率仍是 snake_case,例如:
template_codecategory_idfile_pathpdf_file_pathupdated_at
建议保持:
- DB / SQL 层继续使用 snake_case
- 对外 VO 统一转为 camelCase
这样可以和当前 documentVo.py、evaluationPointGroupVo.py 等风格保持一致,减少前端做字段转换的成本。
SQL / 查询实现建议
1. 分类数量统计
不要逐分类循环查询模板数量,建议使用聚合:
contract_categories左连接contract_templates- 按分类分组统计
2. 搜索匹配
建议搜索条件覆盖:
- 模板标题
- 模板描述
- 模板编码
- 分类名称
3. 排序白名单
建议只允许以下排序字段:
idtitleupdated_atcreated_at
避免任意字段透传造成 SQL 注入或不可控查询。
推荐实施顺序
- 新增
VO/DTO - 新增
Service接口与ServiceImpl空实现 - 新增
Controller - 在 RBAC 权限蓝图中补充权限 key
- 前端
contract-template/templates.ts从 PostgREST 切换到新后端接口
最小落地范围
如果本轮只做最小可用闭环,建议先补齐:
GET /api/v3/contract-templates/categoriesGET /api/v3/contract-templatesGET /api/v3/contract-templates/searchGET /api/v3/contract-templates/{id}
这样可以先解决:
- 搜索首页
- 搜索结果页
- 列表页
- 详情页
结论
contract-template 模块当前缺的不是前端页面,而是新的业务后端接口层。推荐按现有仓库习惯新增独立的:
contractTemplateControllercontractTemplateServicecontractTemplateServiceImplcontractTemplateDtocontractTemplateVo
并优先落地 4 个只读接口,把 search / list / detail 从 PostgREST 迁出。