## 背景 当前 `contract-template` 模块前端页面已经迁移到 Next.js,但核心数据仍依赖旧的 PostgREST 直查表能力: - `contract_categories` - `contract_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` 模块保持一致 ## 现有前端依赖梳理 当前前端依赖的旧查询行为包括: 1. 分类列表 - 首页展示合同分类 - 需要附带每个分类下的模板数量 2. 模板列表 - 支持分页 - 支持按分类筛选 - 支持排序 3. 模板搜索 - 支持关键词查询 - 支持分类过滤 - 支持结果分页 - 搜索结果页还需要分类统计 4. 模板详情 - 查看模板元数据 - 下载模板 - 预览模板 ## 推荐后端目录结构 建议新增以下文件: - `fastapi_modules/fastapi_leaudit/controllers/contractTemplateController.py` - `fastapi_modules/fastapi_leaudit/services/contractTemplateService.py` - `fastapi_modules/fastapi_leaudit/services/impl/contractTemplateServiceImpl.py` - `fastapi_modules/fastapi_leaudit/domian/Dto/contractTemplateDto.py` - `fastapi_modules/fastapi_leaudit/domian/vo/contractTemplateVo.py` 原因: - `contract-template` 是独立业务域,不适合继续塞入 `homeService` 或 `documentService` - 当前仓库主要采用“每个业务一个 controller/service/dto/vo”的组织方式 - 前端菜单和 RBAC 已经把 `contract-template` 视作独立模块,后端也应保持同样边界 ## 路由设计建议 建议统一挂在: - `/api/v3/contract-templates` 建议提供以下接口: 1. `GET /api/v3/contract-templates/categories` 2. `GET /api/v3/contract-templates` 3. `GET /api/v3/contract-templates/search` 4. `GET /api/v3/contract-templates/{TemplateId}` 以上 4 个接口用于替代当前页面的 PostgREST 查询。 ## VO 设计草案 建议新增文件: - `fastapi_modules/fastapi_leaudit/domian/vo/contractTemplateVo.py` 草稿如下: ```python 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` 草稿如下: ```python 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` 方法签名草案: ```python 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` 接口草案: ```python 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` 职责边界建议: 1. `ListCategories` - 直接查 `contract_categories` - 如 `with_template_count=true`,通过聚合一次性返回每类模板数 - 不要像前端当前逻辑那样逐分类循环查询 2. `ListTemplates` - 负责分页、筛选、排序 - 支持 `category_id` 和 `category_name` - 统一返回 `ContractTemplatePageVO` 3. `SearchTemplates` - 可复用 `ListTemplates` 的底层查询逻辑 - 额外补 `categoryStats` - 搜索条件建议覆盖: - `title` - `description` - `template_code` - 分类名称 4. `GetTemplateDetail` - 查单个模板及所属分类信息 - 必要时回填 `placeholderSchema` ## 权限 key 建议 当前仓库权限格式见: - `module:resource:action` 参考现有: - `entry_module:list:read` - `evaluation_point:detail:read` - `rules:create:write` 因此建议新增以下权限: 1. `contract_template:list:read` - 查看模板列表 - 对应: - `GET /api/v3/contract-templates` - `GET /api/v3/contract-templates/categories` 2. `contract_template:search:read` - 使用模板搜索 - 对应: - `GET /api/v3/contract-templates/search` 3. `contract_template:detail:read` - 查看模板详情 - 对应: - `GET /api/v3/contract-templates/{id}` 如果权限体系希望更简化,也可以把 `categories` 和 `search` 并入 `contract_template:list:read`。但从产品语义上,保留 `search` 独立权限更清晰,方便以后做入口控制和审计。 ## Controller 权限校验建议 建议: 1. 分类接口 - 允许以下任一权限: - `contract_template:list:read` - `contract_template:search:read` 2. 列表接口 - `contract_template:list:read` 3. 搜索接口 - `contract_template:search:read` 4. 详情接口 - `contract_template:detail:read` - 或兼容放宽为: - `contract_template:detail:read` - `contract_template:list:read` 建议控制器内沿用现有 `_check_permission` 风格,允许多个 key 中任一通过。 ## RBAC 权限蓝图补充建议 建议在: - `fastapi_modules/fastapi_leaudit/services/impl/rbacAdminServiceImpl.py` 的 `_MANAGEABLE_PERMISSION_BLUEPRINTS` 中补充: ```python {"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_code` - `category_id` - `file_path` - `pdf_file_path` - `updated_at` 建议保持: - DB / SQL 层继续使用 snake_case - 对外 VO 统一转为 camelCase 这样可以和当前 `documentVo.py`、`evaluationPointGroupVo.py` 等风格保持一致,减少前端做字段转换的成本。 ## SQL / 查询实现建议 ### 1. 分类数量统计 不要逐分类循环查询模板数量,建议使用聚合: - `contract_categories` 左连接 `contract_templates` - 按分类分组统计 ### 2. 搜索匹配 建议搜索条件覆盖: - 模板标题 - 模板描述 - 模板编码 - 分类名称 ### 3. 排序白名单 建议只允许以下排序字段: - `id` - `title` - `updated_at` - `created_at` 避免任意字段透传造成 SQL 注入或不可控查询。 ## 推荐实施顺序 1. 新增 `VO/DTO` 2. 新增 `Service` 接口与 `ServiceImpl` 空实现 3. 新增 `Controller` 4. 在 RBAC 权限蓝图中补充权限 key 5. 前端 `contract-template/templates.ts` 从 PostgREST 切换到新后端接口 ## 最小落地范围 如果本轮只做最小可用闭环,建议先补齐: 1. `GET /api/v3/contract-templates/categories` 2. `GET /api/v3/contract-templates` 3. `GET /api/v3/contract-templates/search` 4. `GET /api/v3/contract-templates/{id}` 这样可以先解决: - 搜索首页 - 搜索结果页 - 列表页 - 详情页 ## 结论 `contract-template` 模块当前缺的不是前端页面,而是新的业务后端接口层。推荐按现有仓库习惯新增独立的: - `contractTemplateController` - `contractTemplateService` - `contractTemplateServiceImpl` - `contractTemplateDto` - `contractTemplateVo` 并优先落地 4 个只读接口,把 `search / list / detail` 从 PostgREST 迁出。