From 980996d93332457479d40329bcc89e296675890c Mon Sep 17 00:00:00 2001 From: wren <“porlong@qq.com”> Date: Tue, 19 May 2026 20:50:41 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E5=90=88=E5=90=8C?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E4=B8=8E=E6=96=87=E6=A1=A3=E8=B4=A8=E9=87=8F?= =?UTF-8?q?=E6=96=B9=E6=A1=88=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../合同模板前端切换新接口改造清单.md | 253 ++++ .../合同模板搜索功能后端接口补充设计.md | 519 +++++++ .../合同模板权限蓝图补充建议.md | 86 ++ .../文档图片质量校验模块第3版接口与落地清单.md | 1227 +++++++++++++++++ ...前端分支提交与合并防覆盖操作规范-2026-05-19.md | 456 ++++++ 5 files changed, 2541 insertions(+) create mode 100644 docs/合同模板搜索合同起草/合同模板前端切换新接口改造清单.md create mode 100644 docs/合同模板搜索合同起草/合同模板搜索功能后端接口补充设计.md create mode 100644 docs/合同模板搜索合同起草/合同模板权限蓝图补充建议.md create mode 100644 docs/文档图片质量校验模块/文档图片质量校验模块第3版接口与落地清单.md create mode 100644 docs/项目总览/前端分支提交与合并防覆盖操作规范-2026-05-19.md diff --git a/docs/合同模板搜索合同起草/合同模板前端切换新接口改造清单.md b/docs/合同模板搜索合同起草/合同模板前端切换新接口改造清单.md new file mode 100644 index 0000000..1de46a9 --- /dev/null +++ b/docs/合同模板搜索合同起草/合同模板前端切换新接口改造清单.md @@ -0,0 +1,253 @@ +## 目标 + +将 `contract-template` 相关页面从旧的 PostgREST 直连方式切换到新的 FastAPI 业务接口,范围仅覆盖: + +- 模板分类 +- 模板列表 +- 模板搜索 +- 模板详情 + +本清单不包含“起草合同”能力,后续会作为独立模块重新开发。 + + +## 当前前端依赖点 + +当前前端核心依赖集中在: + +- `legal-platform-frontend/lib/api/legacy/contract-template/templates.ts` + +这个文件现在直接调用: + +- `/api/postgrest/proxy/contract_categories` +- `/api/postgrest/proxy/contract_templates` + +受影响页面: + +- `app/(audit)/contract-template/search/page.tsx` +- `app/(audit)/contract-template/search/results/page.tsx` +- `app/(audit)/contract-template/list/page.tsx` +- `app/(audit)/contract-template/detail/[id]/page.tsx` + + +## 推荐新接口 + +建议前端最终切换为: + +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}` + + +## 改造步骤 + +### 第 1 步:新增新接口客户端文件 + +建议新增: + +- `legal-platform-frontend/lib/api/contract-template/index.ts` + +职责: + +- 仅封装新的业务后端接口 +- 不再依赖 `postgrest-client.ts` +- 返回字段尽量与页面现有消费结构兼容 + +建议方法: + +- `getContractTemplateCategories` +- `getContractTemplateList` +- `searchContractTemplateList` +- `getContractTemplateDetail` + + +### 第 2 步:定义前端接口类型 + +建议在新客户端文件中定义或复用以下类型: + +- `ContractTemplateCategory` +- `ContractTemplateListItem` +- `ContractTemplatePage` +- `ContractTemplateDetail` +- `ContractTemplateSearchResult` + +字段建议优先与新后端对齐,然后在页面边界做一次轻量转换,避免业务层长期保留 snake_case 和 camelCase 混用。 + + +### 第 3 步:改造搜索首页 + +文件: + +- `app/(audit)/contract-template/search/page.tsx` + +当前行为: + +- 调 `getContractCategoriesWithCount` + +改造后: + +- 改为调用 `getContractTemplateCategories` +- 直接消费后端返回的 `templateCount` + +页面影响: + +- `transformCategory` 中的 `template_count` 改为新接口字段 + + +### 第 4 步:改造列表页 + +文件: + +- `app/(audit)/contract-template/list/page.tsx` + +当前行为: + +- 调 `getContractTemplates` +- 同时调 `getContractCategoriesWithCount` + +改造后: + +- 列表数据改为 `getContractTemplateList` +- 分类数据改为 `getContractTemplateCategories` + +注意事项: + +- 当前页面把 `sortBy=relevance` 映射成 `id.asc`,这是旧 PostgREST 兼容逻辑 +- 切换新接口后应明确排序语义: + - `relevance` 仅搜索场景有效 + - 列表页默认建议改为 `updated_at desc` + + +### 第 5 步:改造搜索结果页 + +文件: + +- `app/(audit)/contract-template/search/results/page.tsx` + +当前行为: + +- 调 `searchContractTemplates` +- 额外循环所有分类,再逐类搜索统计命中数 + +改造后: + +- 改为一次调用 `searchContractTemplateList` +- 直接使用后端返回的 `categoryStats` + +收益: + +- 避免当前每次搜索都发起多次分类二次查询 +- 页面搜索耗时会明显降低 + + +### 第 6 步:改造详情页 + +文件: + +- `app/(audit)/contract-template/detail/[id]/page.tsx` + +当前行为: + +- 调 `getContractTemplate` + +改造后: + +- 改为调用 `getContractTemplateDetail` + +注意事项: + +- 页面中依赖: + - `template_code` + - `file_path` + - `pdf_file_path` + - `placeholder_schema` + - `category.name` + - `category.description` +- 新接口最好直接扁平返回,前端减少再拼装 + + +### 第 7 步:保留旧文件作为过渡,避免一次性大改 + +建议不要立刻删除: + +- `lib/api/legacy/contract-template/templates.ts` + +更稳妥的做法: + +1. 新建新接口客户端文件 +2. 页面逐个切换 +3. 确认没有页面再引用旧文件 +4. 再删除旧实现 + + +## 字段映射建议 + +建议后端返回使用 camelCase 还是 snake_case,前后端尽量统一一次定死。 + +如果后端沿用当前 Python VO 风格的 snake_case,那么前端建议统一在 API client 中做转换: + +- `template_code -> templateCode` +- `category_id -> categoryId` +- `file_path -> filePath` +- `pdf_file_path -> pdfFilePath` +- `placeholder_schema -> placeholderSchema` +- `updated_at -> updatedAt` + +不要把这类转换分散在页面组件里。 + + +## 具体函数替换建议 + +当前旧函数: + +- `getContractCategories` +- `getContractCategoriesWithCount` +- `getContractTemplates` +- `getContractTemplate` +- `searchContractTemplates` + +建议替换为新函数: + +- `getContractTemplateCategories` +- `getContractTemplateList` +- `getContractTemplateDetail` +- `searchContractTemplateList` + + +## 风险点 + +1. 排序语义变化 + - 旧逻辑混入了 PostgREST 风格排序拼接 + - 新接口需要明确合法排序字段白名单 + +2. 搜索分类统计变化 + - 旧页面自己循环查询分类统计 + - 新接口如果不返回 `categoryStats`,页面逻辑还要保留一部分旧实现 + +3. 详情页字段结构变化 + - 旧页面默认拿 `template.category?.name` + - 新接口若改成扁平字段,需要同步调整页面 transform + +4. token 传递方式变化 + - 旧实现依赖 `postgrest-client.ts` + - 新接口应统一走 `axios-client.ts` 或新业务客户端 + + +## 建议落地顺序 + +1. 新增 `lib/api/contract-template/index.ts` +2. 先切 `search/page.tsx` +3. 再切 `list/page.tsx` +4. 再切 `search/results/page.tsx` +5. 最后切 `detail/[id]/page.tsx` +6. 全部验证通过后删除旧 `legacy/contract-template/templates.ts` + + +## 验收标准 + +1. `contract-template/search` 能正常展示分类和数量 +2. `contract-template/list` 能分页、筛选、排序 +3. `contract-template/search/results` 能搜索并展示分类统计 +4. `contract-template/detail/[id]` 能正常查看详情与预览 +5. 页面不再直接请求 `/api/postgrest/proxy/contract_categories` +6. 页面不再直接请求 `/api/postgrest/proxy/contract_templates` diff --git a/docs/合同模板搜索合同起草/合同模板搜索功能后端接口补充设计.md b/docs/合同模板搜索合同起草/合同模板搜索功能后端接口补充设计.md new file mode 100644 index 0000000..32985eb --- /dev/null +++ b/docs/合同模板搜索合同起草/合同模板搜索功能后端接口补充设计.md @@ -0,0 +1,519 @@ +## 背景 + +当前 `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 迁出。 diff --git a/docs/合同模板搜索合同起草/合同模板权限蓝图补充建议.md b/docs/合同模板搜索合同起草/合同模板权限蓝图补充建议.md new file mode 100644 index 0000000..5de88c3 --- /dev/null +++ b/docs/合同模板搜索合同起草/合同模板权限蓝图补充建议.md @@ -0,0 +1,86 @@ +## 目标 + +补齐 `contract-template` 当前阶段只读能力所需的权限蓝图,仅覆盖: + +- 分类 +- 列表 +- 搜索 +- 详情 + +明确不包含: + +- 起草合同 +- 草稿管理 +- 合同编辑 + + +## 建议新增权限 key + +建议在 `fastapi_modules/fastapi_leaudit/services/impl/rbacAdminServiceImpl.py` 的 `_MANAGEABLE_PERMISSION_BLUEPRINTS` 中补充以下 3 个权限: + +```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"}, +``` + + +## 分类接口权限建议 + +接口: + +- `GET /api/v3/contract-templates/categories` + +建议权限策略: + +- 允许 `contract_template:list:read` +- 或 `contract_template:search:read` + +原因: + +- 分类数据同时服务于列表页和搜索页 +- 不建议单独再拆一个 `category:read` 权限,当前阶段收益不高 + + +## Controller 校验建议 + +### 分类 + +允许任一权限: + +- `contract_template:list:read` +- `contract_template:search:read` + +### 列表 + +- `contract_template:list:read` + +### 搜索 + +- `contract_template:search:read` + +### 详情 + +建议放宽为任一权限: + +- `contract_template:detail:read` +- `contract_template:list:read` + +原因: + +- 详情通常从列表页进入 +- 允许列表权限兼容详情访问,可以减少菜单和权限配置初期的阻塞 + + +## 当前阶段不应新增的权限 + +以下权限本轮不要进入蓝图: + +- `contract_draft:create:write` +- `contract_draft:update:write` +- `contract_draft:delete:delete` + +原因: + +- 你已经明确起草能力将重做为独立模块 +- 当前阶段只解决 `contract-template` 的只读接口迁移 diff --git a/docs/文档图片质量校验模块/文档图片质量校验模块第3版接口与落地清单.md b/docs/文档图片质量校验模块/文档图片质量校验模块第3版接口与落地清单.md new file mode 100644 index 0000000..cd66994 --- /dev/null +++ b/docs/文档图片质量校验模块/文档图片质量校验模块第3版接口与落地清单.md @@ -0,0 +1,1227 @@ +# 文档图片质量校验模块第3版接口与落地清单 + +## 1. 目标边界 + +本模块用于对案卷文档中的拍照图片、扫描图片、附件图片做“图片清晰度预警检测”。 + +本模块的边界固定如下: + +- 与现有 OCR 抽取、评查主流程完全独立。 +- 即使检测出图片模糊,也不能阻断上传、不能阻断 OCR、不能阻断评查。 +- 结果只做预警展示和留痕,分三档: + - `pass`:通过 + - `review`:疑似模糊待人工确认 + - `reject`:不通过需重拍 +- 第一版优先支持: + - 图片文件 + - PDF / 扫描 PDF + - 图片附件 + - PDF 附件 +- `doc/docx/wps` 第一版允许降级为“正文第 N 张图”定位,不强承诺精确页码。 + +## 2. 建议代码落点 + +### 2.1 后端目录 + +建议新增如下目录与文件: + +- `fastapi_modules/fastapi_leaudit/domian/vo/imageQualityVo.py` +- `fastapi_modules/fastapi_leaudit/services/imageQualityService.py` +- `fastapi_modules/fastapi_leaudit/services/impl/imageQualityServiceImpl.py` +- `fastapi_modules/fastapi_leaudit/controllers/imageQualityController.py` +- `fastapi_modules/fastapi_leaudit/image_quality/tasks.py` +- `fastapi_modules/fastapi_leaudit/image_quality/runner.py` +- `fastapi_modules/fastapi_leaudit/image_quality/storage_adapter.py` +- `fastapi_modules/fastapi_leaudit/image_quality/input_resolver.py` +- `fastapi_modules/fastapi_leaudit/image_quality/extractors.py` +- `fastapi_modules/fastapi_leaudit/image_quality/detector.py` +- `fastapi_modules/fastapi_leaudit/image_quality/config_resolver.py` + +### 2.2 现有文件改动点 + +- `fastapi_modules/fastapi_leaudit/controllers/documentController.py` +- `fastapi_modules/fastapi_leaudit/services/documentService.py` +- `fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py` +- `fastapi_modules/fastapi_leaudit/domian/vo/documentVo.py` +- `fastapi_admin/config/_settings.py` +- `fastapi_admin/config/__init__.pyi` + +## 3. 新增 VO 建议 + +按当前仓库 `documentVo.py / auditVo.py / reviewPointVo.py` 风格,建议单独新增 `imageQualityVo.py`。 + +### 3.1 配置相关 VO + +```python +from pydantic import BaseModel, Field + + +class ImageQualityConfigItemVO(BaseModel): + """图片质量校验配置项。""" + + id: int = Field(..., description="配置ID") + scopeType: str = Field(..., description="作用域类型:global/doc_type/region/doc_type_region") + documentTypeId: int | None = Field(None, description="文档类型ID") + region: str | None = Field(None, description="地区") + enabled: bool = Field(..., description="是否启用") + warnThreshold: float | None = Field(None, description="疑似模糊阈值") + rejectThreshold: float | None = Field(None, description="不通过阈值") + maxImagesPerDoc: int | None = Field(None, description="单文档最大检测图片数") + maxConcurrency: int | None = Field(None, description="单任务并发数") + updatedAt: str | None = Field(None, description="更新时间") + + +class ImageQualityConfigUpsertDTO(BaseModel): + """图片质量校验配置新增/更新请求。""" + + scopeType: str = Field(..., description="作用域类型") + documentTypeId: int | None = Field(None, description="文档类型ID") + region: str | None = Field(None, description="地区") + enabled: bool = Field(..., description="是否启用") + warnThreshold: float | None = Field(None, description="疑似模糊阈值") + rejectThreshold: float | None = Field(None, description="不通过阈值") + maxImagesPerDoc: int | None = Field(None, description="单文档最大检测图片数") + maxConcurrency: int | None = Field(None, description="单任务并发数") +``` + +### 3.2 明细与摘要相关 VO + +```python +class ImageQualityItemVO(BaseModel): + """单张图片质检明细。""" + + itemId: int = Field(..., description="明细ID") + runId: int = Field(..., description="质检运行ID") + documentId: int = Field(..., description="文档ID") + documentFileId: int | None = Field(None, description="文档文件ID") + sourceKind: str = Field(..., description="来源类型") + sourceFileName: str | None = Field(None, description="来源文件名") + sourcePageNum: int | None = Field(None, description="来源页码") + imageIndexInPage: int | None = Field(None, description="页内图片序号") + imageIndexInFile: int | None = Field(None, description="文件内图片序号") + bbox: dict | list | None = Field(None, description="图片定位框") + qualityStatus: str = Field(..., description="pass/review/reject") + qualityScore: float | None = Field(None, description="清晰度分值") + reasonCode: str | None = Field(None, description="原因编码") + reasonText: str | None = Field(None, description="原因说明") + cropOssUrl: str | None = Field(None, description="裁剪图OSS地址") + displayText: str | None = Field(None, description="展示文案,例如第12页第1张图片模糊") + + +class ImageQualitySummaryVO(BaseModel): + """文档图片质检摘要。""" + + runId: int | None = Field(None, description="最新运行ID") + runStatus: str | None = Field(None, description="queued/running/completed/failed/skipped") + summaryStatus: str | None = Field(None, description="pass/review/reject") + skipReason: str | None = Field(None, description="跳过原因") + totalImages: int = Field(0, description="总图片数") + passCount: int = Field(0, description="通过数") + reviewCount: int = Field(0, description="待人工确认数") + rejectCount: int = Field(0, description="需重拍数") + warningText: str | None = Field(None, description="摘要提示文案") + finishedAt: str | None = Field(None, description="完成时间") + + +class ImageQualityDetailVO(BaseModel): + """文档图片质检详情。""" + + summary: ImageQualitySummaryVO = Field(..., description="质检摘要") + items: list[ImageQualityItemVO] = Field(default_factory=list, description="问题图片列表") +``` + +### 3.3 批量状态与重检相关 VO + +```python +class ImageQualityStatusItemVO(BaseModel): + """文档图片质检状态项。""" + + documentId: int = Field(..., description="文档ID") + runId: int | None = Field(None, description="最新运行ID") + runStatus: str | None = Field(None, description="运行状态") + summaryStatus: str | None = Field(None, description="摘要状态") + rejectCount: int = Field(0, description="需重拍数") + reviewCount: int = Field(0, description="待确认数") + updatedAt: str | None = Field(None, description="更新时间") + + +class ImageQualityRecheckVO(BaseModel): + """手工重检响应。""" + + runId: int = Field(..., description="新质检运行ID") + documentId: int = Field(..., description="文档ID") + status: str = Field(..., description="queued") + message: str = Field("", description="提示信息") +``` + +## 4. 对现有 Document VO 的扩展建议 + +建议直接扩充 `documentVo.py`,这样上传页、列表页、详情页可以少开新接口或少拼数据。 + +### 4.1 `DocumentUploadVO` 增加字段 + +```python +imageQualityEnabled: bool = Field(False, description="当前文档是否启用图片质量校验") +imageQualityRunId: int | None = Field(None, description="图片质量校验运行ID") +imageQualityRunStatus: str | None = Field(None, description="图片质量校验运行状态") +imageQualitySummaryStatus: str | None = Field(None, description="图片质量校验摘要状态") +``` + +### 4.2 `DocumentStatusItemVO` 增加字段 + +```python +imageQualityRunId: int | None = Field(None, description="图片质量校验运行ID") +imageQualityRunStatus: str | None = Field(None, description="图片质量校验运行状态") +imageQualitySummaryStatus: str | None = Field(None, description="图片质量校验摘要状态") +imageQualityRejectCount: int = Field(0, description="需重拍图片数") +imageQualityReviewCount: int = Field(0, description="待人工确认图片数") +``` + +### 4.3 `DocumentListItemVO` 增加字段 + +```python +imageQualityRunId: int | None = Field(None, description="图片质量校验运行ID") +imageQualityRunStatus: str | None = Field(None, description="图片质量校验运行状态") +imageQualitySummaryStatus: str | None = Field(None, description="图片质量校验摘要状态") +imageQualityIssueCount: int = Field(0, description="问题图片数") +imageQualityWarningText: str | None = Field(None, description="图片质量提示文案") +``` + +### 4.4 `DocumentDetailVO` 增加字段 + +```python +imageQualitySummary: ImageQualitySummaryVO | None = Field(None, description="图片质量摘要") +``` + +## 5. 新增 Service 接口签名 + +建议新建 `services/imageQualityService.py`,不要污染 `IDocumentService` 主职责。 + +```python +from abc import ABC, abstractmethod + +from fastapi_modules.fastapi_leaudit.domian.vo.imageQualityVo import ( + ImageQualityConfigItemVO, + ImageQualityConfigUpsertDTO, + ImageQualityDetailVO, + ImageQualityRecheckVO, + ImageQualityStatusItemVO, + ImageQualitySummaryVO, +) + + +class IImageQualityService(ABC): + """图片质量校验服务接口。""" + + @abstractmethod + async def DispatchForDocument( + self, + DocumentId: int, + TriggerUserId: int | None = None, + Force: bool = False, + Speed: str = "normal", + ) -> ImageQualityRecheckVO | None: + """按文档触发图片质量校验任务。""" + ... + + @abstractmethod + async def GetDocumentSummary( + self, + CurrentUserId: int, + DocumentId: int, + ) -> ImageQualitySummaryVO: + """获取文档图片质检摘要。""" + ... + + @abstractmethod + async def GetDocumentDetail( + self, + CurrentUserId: int, + DocumentId: int, + ) -> ImageQualityDetailVO: + """获取文档图片质检详情。""" + ... + + @abstractmethod + async def GetDocumentsStatus( + self, + CurrentUserId: int, + Ids: list[int], + ) -> list[ImageQualityStatusItemVO]: + """批量获取文档图片质检状态。""" + ... + + @abstractmethod + async def RecheckDocument( + self, + CurrentUserId: int, + DocumentId: int, + Speed: str = "normal", + ) -> ImageQualityRecheckVO: + """手工重跑文档图片质量校验。""" + ... + + @abstractmethod + async def ListConfigs(self) -> list[ImageQualityConfigItemVO]: + """获取图片质量校验配置列表。""" + ... + + @abstractmethod + async def UpsertConfig(self, Body: ImageQualityConfigUpsertDTO) -> ImageQualityConfigItemVO: + """新增或更新图片质量校验配置。""" + ... + + @abstractmethod + async def DeleteConfig(self, Id: int) -> None: + """删除图片质量校验配置。""" + ... +``` + +## 6. 对现有 DocumentService 的接入点建议 + +建议只在现有 `IDocumentService / DocumentServiceImpl` 中补“触发点”,不要把查询逻辑也塞进去。 + +### 6.1 `DocumentServiceImpl.Upload` + +在上传主文件和附件落库完成后,补一段: + +```python +await self.ImageQualityService.DispatchForDocument( + DocumentId=document.Id, + TriggerUserId=CreatedBy, + Force=False, + Speed=Speed, +) +``` + +要求: + +- try/except 包裹 +- 失败只记 warning log +- 不影响现有上传成功返回 + +### 6.2 `DocumentServiceImpl.AppendAttachments` + +在追加附件成功后,补一段: + +```python +await self.ImageQualityService.DispatchForDocument( + DocumentId=Id, + TriggerUserId=CurrentUserId, + Force=True, + Speed="normal", +) +``` + +原因: + +- 附件变化后,图片质检结果已过期 +- 需要按最新附件重新跑 + +## 7. Controller 路由建议 + +建议新建 `controllers/imageQualityController.py`,风格保持与 `documentController.py / auditController.py` 一致。 + +### 7.1 文档级路由 + +```python +@self.router.get("/documents/{DocumentId}/image-quality/summary", response_model=Result[ImageQualitySummaryVO]) +async def GetDocumentImageQualitySummary( + DocumentId: int, + payload: dict[str, Any] = Depends(verify_access_token), +): + """获取单个文档图片质量校验摘要。""" + + +@self.router.get("/documents/{DocumentId}/image-quality", response_model=Result[ImageQualityDetailVO]) +async def GetDocumentImageQualityDetail( + DocumentId: int, + payload: dict[str, Any] = Depends(verify_access_token), +): + """获取单个文档图片质量校验详情。""" + + +@self.router.post("/documents/{DocumentId}/image-quality/recheck", response_model=Result[ImageQualityRecheckVO]) +async def RecheckDocumentImageQuality( + DocumentId: int, + speed: str = Form("normal", description="执行速度档位:urgent/normal"), + payload: dict[str, Any] = Depends(verify_access_token), +): + """手工重跑单个文档图片质量校验。""" +``` + +### 7.2 批量状态路由 + +```python +@self.router.get("/documents/image-quality/status", response_model=Result[list[ImageQualityStatusItemVO]]) +async def GetDocumentsImageQualityStatus( + ids: str = Query(..., description="逗号分隔的文档ID列表"), + payload: dict[str, Any] = Depends(verify_access_token), +): + """批量获取文档图片质量校验状态。""" +``` + +### 7.3 配置管理路由 + +如果第一期不做后台配置页,可以先只保留 service,不先开放 controller。 + +如果要开接口,建议: + +```python +@self.router.get("/v3/image-quality/configs", response_model=Result[list[ImageQualityConfigItemVO]]) +async def ListImageQualityConfigs(): + """获取图片质量校验配置。""" + + +@self.router.post("/v3/image-quality/configs", response_model=Result[ImageQualityConfigItemVO]) +async def UpsertImageQualityConfig(Body: ImageQualityConfigUpsertDTO): + """新增或更新图片质量校验配置。""" + + +@self.router.delete("/v3/image-quality/configs/{ConfigId}", response_model=Result[None]) +async def DeleteImageQualityConfig(ConfigId: int): + """删除图片质量校验配置。""" +``` + +## 8. Celery 任务与函数签名建议 + +新增文件:`fastapi_modules/fastapi_leaudit/image_quality/tasks.py` + +```python +def resolve_image_quality_queue(speed: str = "normal") -> str: + """根据优先级返回图片质量校验队列名。""" + + +def dispatch_image_quality_task( + run_id: int, + *, + speed: str = "normal", + trigger_user_id: int | None = None, +) -> Any: + """投递图片质量校验任务。""" + + +@celery_app.task( + bind=True, + name="leaudit.image_quality.process_document", + acks_late=True, +) +def image_quality_process_document_task( + self, + run_id: int, + trigger_user_id: int | None = None, +) -> dict[str, Any]: + """Celery worker 入口。""" +``` + +新增文件:`fastapi_modules/fastapi_leaudit/image_quality/runner.py` + +```python +class ImageQualityRunner: + """图片质量校验执行器。""" + + async def Execute( + self, + RunId: int, + TriggerUserId: int | None = None, + ) -> dict[str, Any]: + """执行一次完整的图片质量校验。""" +``` + +## 9. SQL 草案文件命名建议 + +按你当前仓库习惯,建议直接放到: + +- `scripts/创建sql/` + +建议拆 3 个 SQL 草案文件。 + +### 9.1 表结构 SQL + +文件名: + +- `scripts/创建sql/schema_add_image_quality_module.sql` + +内容范围: + +- `leaudit_image_quality_configs` +- `leaudit_image_quality_runs` +- `leaudit_image_quality_items` +- 索引 +- 唯一约束 +- 软删除字段 + +### 9.2 权限点 SQL + +文件名: + +- `scripts/创建sql/seed_image_quality_permissions.sql` + +内容范围: + +- 图片质量校验查询权限 +- 图片质量校验重检权限 +- 图片质量配置管理权限 + +### 9.3 路由/入口 SQL + +文件名: + +- `scripts/创建sql/seed_image_quality_routes.sql` + +内容范围: + +- 新增接口路由资源 +- 如需配置后台菜单,再补菜单项 + +## 10. 建议表结构草案 + +### 10.1 `leaudit_image_quality_configs` + +建议字段: + +- `id BIGSERIAL PRIMARY KEY` +- `scope_type VARCHAR(32) NOT NULL` +- `document_type_id BIGINT NULL` +- `region VARCHAR(64) NULL` +- `enabled BOOLEAN NOT NULL DEFAULT TRUE` +- `warn_threshold NUMERIC(10,4) NULL` +- `reject_threshold NUMERIC(10,4) NULL` +- `max_images_per_doc INTEGER NULL` +- `max_concurrency INTEGER NULL` +- `created_at TIMESTAMP NOT NULL DEFAULT NOW()` +- `updated_at TIMESTAMP NOT NULL DEFAULT NOW()` +- `deleted_at TIMESTAMP NULL` + +建议索引: + +- `idx_leaudit_image_quality_configs_scope` +- `idx_leaudit_image_quality_configs_doc_type` +- `idx_leaudit_image_quality_configs_region` + +### 10.2 `leaudit_image_quality_runs` + +建议字段: + +- `id BIGSERIAL PRIMARY KEY` +- `document_id BIGINT NOT NULL` +- `document_file_id BIGINT NULL` +- `status VARCHAR(32) NOT NULL DEFAULT 'queued'` +- `skip_reason VARCHAR(64) NULL` +- `summary_status VARCHAR(32) NULL` +- `total_images INTEGER NOT NULL DEFAULT 0` +- `pass_count INTEGER NOT NULL DEFAULT 0` +- `review_count INTEGER NOT NULL DEFAULT 0` +- `reject_count INTEGER NOT NULL DEFAULT 0` +- `task_id VARCHAR(255) NULL` +- `error_message TEXT NULL` +- `started_at TIMESTAMP NULL` +- `finished_at TIMESTAMP NULL` +- `created_by BIGINT NULL` +- `created_at TIMESTAMP NOT NULL DEFAULT NOW()` +- `updated_at TIMESTAMP NOT NULL DEFAULT NOW()` +- `deleted_at TIMESTAMP NULL` + +建议索引: + +- `idx_leaudit_image_quality_runs_document_id` +- `idx_leaudit_image_quality_runs_status` +- `idx_leaudit_image_quality_runs_document_created_at` + +### 10.3 `leaudit_image_quality_items` + +建议字段: + +- `id BIGSERIAL PRIMARY KEY` +- `run_id BIGINT NOT NULL` +- `document_id BIGINT NOT NULL` +- `document_file_id BIGINT NULL` +- `source_kind VARCHAR(64) NOT NULL` +- `source_file_name VARCHAR(255) NULL` +- `source_page_num INTEGER NULL` +- `image_index_in_page INTEGER NULL` +- `image_index_in_file INTEGER NULL` +- `bbox_json JSONB NULL` +- `image_key VARCHAR(255) NULL` +- `parent_image_key VARCHAR(255) NULL` +- `crop_oss_url TEXT NULL` +- `quality_status VARCHAR(32) NOT NULL` +- `quality_score NUMERIC(10,4) NULL` +- `reason_code VARCHAR(64) NULL` +- `reason_text TEXT NULL` +- `extra_json JSONB NULL` +- `created_at TIMESTAMP NOT NULL DEFAULT NOW()` +- `updated_at TIMESTAMP NOT NULL DEFAULT NOW()` +- `deleted_at TIMESTAMP NULL` + +建议索引: + +- `idx_leaudit_image_quality_items_run_id` +- `idx_leaudit_image_quality_items_document_id` +- `idx_leaudit_image_quality_items_quality_status` +- `idx_leaudit_image_quality_items_source_page_num` + +## 11. 配置项建议 + +在 `fastapi_admin/config/_settings.py` 的 `LeauditSettings` 增加: + +```python +LEAUDIT_IMAGE_QUALITY_ENABLED: bool = False +LEAUDIT_IMAGE_QUALITY_WARN_THRESHOLD: float = 0.45 +LEAUDIT_IMAGE_QUALITY_REJECT_THRESHOLD: float = 0.30 +LEAUDIT_IMAGE_QUALITY_MAX_IMAGES_PER_DOC: int = 80 +LEAUDIT_IMAGE_QUALITY_MAX_CONCURRENCY: int = 4 +LEAUDIT_IMAGE_QUALITY_QUEUE_NORMAL: str = "leaudit.image_quality.normal" +LEAUDIT_IMAGE_QUALITY_QUEUE_URGENT: str = "leaudit.image_quality.urgent" +LEAUDIT_IMAGE_QUALITY_TIMEOUT: int = 120 +``` + +并同步补到 `fastapi_admin/config/__init__.pyi`。 + +## 12. 前端接口命名建议 + +建议前端 API 文件新增: + +- `legal-platform-frontend/lib/api/legacy/files/image-quality.ts` + +建议函数: + +```ts +export async function getDocumentImageQualitySummary(documentId: number) +export async function getDocumentImageQualityDetail(documentId: number) +export async function getDocumentsImageQualityStatus(ids: number[]) +export async function recheckDocumentImageQuality(documentId: number, speed?: string) +``` + +## 13. 第一版实施顺序 + +建议按下面顺序做: + +1. 先补 SQL 与后端表模型 +2. 再补 `imageQualityVo.py` +3. 再补 `IImageQualityService / ImageQualityServiceImpl` +4. 再补 `tasks.py / runner.py` +5. 再在 `DocumentServiceImpl.Upload / AppendAttachments` 挂触发 +6. 再补 `imageQualityController.py` +7. 最后接前端上传页、列表页、详情页 + +## 14. 结论 + +按当前仓库风格,这个模块最稳妥的落地方式是: + +- 新开独立 `VO + Service + Controller + Celery task + SQL` +- 只在 `DocumentServiceImpl` 做触发,不侵入现有 `AuditServiceImpl` +- 对外暴露文档级摘要、明细、批量状态、手工重检四类接口 +- SQL 先拆成结构、权限、路由三个脚本,方便分阶段上线 + +这个拆法能保证图片质量校验模块后续可单独演进,不会把现有 OCR / 评查主链路拖乱。 + +## 15. 页码定位策略修正版 + +这一节用于修正第一版里对 `doc/docx/wps` 页码定位的表述,避免研发误以为可以直接靠当前 `python-docx` 兜底逻辑拿到真实页码。 + +### 15.1 现有 OCR / 评查链路里的页码定位是怎么来的 + +当前系统里,评查详情页能展示字段页码、部分情况下还能做定位高亮,主来源并不是“原始 Word 文档页码”,而是下面两层能力: + +- 第一层:OCR/归一化结果自带页结构 + - `ocr_result.pages[].page_num` + - OCR chunk 的 `bbox` + - `field_positions` 里的 `pageNum / bbox / matchPosition` +- 第二层:详情页兜底文本匹配 + - 对 PDF 重新逐页抽文本,再把字段文本匹配回页码 + - 这是兜底逻辑,不是主定位来源 + +也就是说,当前评查模块“真正稳定可复用”的页码语义,主要来自 OCR / normalized document,而不是详情页自己的回推逻辑。 + +### 15.2 当前项目里 `docx` 的兜底页码能力并不是真分页 + +当前 `documentServiceImpl.py` 里的 `_extract_page_texts_from_docx()` 实现,实际是: + +- 读取段落和表格文本 +- 拼成整篇文档文本 +- 最终只返回 `[(1, text)]` + +这意味着: + +- 当前详情页兜底逻辑对 `docx` 并没有真实分页能力 +- 它只是把整篇文档退化成“第 1 页” +- 因此不能把这条逻辑当成 `docx` 图片定位的基础 + +结论要固定为: + +- 当前系统里 `docx` 不是完全不能定位 +- 但不能依赖现有 `python-docx` 兜底逻辑做精确页码 +- 如果要把 `docx` 图片定位做准,必须优先复用 OCR / 归一化后的页结构 + +### 15.3 图片质量模块对 `docx` 的正确做法 + +图片质量模块在处理 `doc/docx/wps` 时,页码定位策略建议按优先级分层: + +1. 优先复用 OCR 结果中的页结构和视觉对象定位 +2. 其次复用 `visual_manifest` 中已有的视觉对象元数据 +3. 如果 OCR 已能给出该图片或父图像所在页,则直接采用该页码 +4. 如果拿不到稳定页码,则降级为“正文第 N 张图”或“附件第 N 张图” + +推荐优先使用的字段包括: + +- `ocr_result.pages[].page_num` +- `ocr_result.visual_manifest` +- `visual_manifest` 中对象的 `page_num` +- `visual_manifest` 中对象的 `bbox` +- `image_key / parent_image_key` + +这样做的本质是: + +- 不自己重新发明 Word 分页 +- 而是复用现有 OCR pipeline 已经归一化出来的页概念 + +### 15.4 为什么图片质量模块不能直接复用详情页 `docx` 兜底逻辑 + +因为那条逻辑只适合“文本字段没有页码时,尽量补一个最小可用页码”,不适合图片质量场景。 + +图片质量场景要解决的是: + +- 哪一张图片模糊 +- 它在第几页 +- 它在页内第几张 +- 最好还能给出 bbox 或裁剪图 + +而当前 `docx` 兜底逻辑没有: + +- 真分页 +- 图片级索引 +- 页内图片序号 +- 图片 bbox + +所以如果直接复用它,只会得到一个看似有页码、其实不可用的假定位。 + +### 15.5 图片质量模块的页码定位分级策略 + +建议在方案里明确写死: + +- `PDF / 扫描PDF / 图片文件 / 图片附件 / PDF附件` + - 第一优先级支持精确定位 + - 目标输出:`source_page_num + image_index_in_page + bbox` +- `doc/docx/wps` + - 第一优先级复用 OCR / visual_manifest 的页码与 bbox + - 第二优先级降级为 `image_index_in_file` + - 第一版不承诺所有 Office 文档都能稳定精确到预览页码 + +也就是说,`docx` 可以做定位,但实现前提不是当前 `python-docx` 的退化逻辑,而是 OCR 归一化之后的页结构。 + +### 15.6 对图片质量模块的实现修正 + +基于上面的分析,图片质量模块实现时建议补一个约束: + +- 对 `doc/docx/wps`,图片抽取和质量判断可以独立做 +- 但页码回指优先放在“抽图后结合 OCR visual metadata 回填”这一层完成 +- 不允许直接调用当前 `_extract_page_texts_from_docx()` 来生成图片页码 + +更具体地说: + +- `extractors.py` 负责抽图和生成图片级唯一索引 +- `detector.py` 负责清晰度三档判断 +- `runner.py` 在汇总结果前,优先尝试将图片索引和 OCR/visual_manifest 做关联 +- 关联成功则写入 `source_page_num / bbox` +- 关联失败则写入 `image_index_in_file`,前端展示“正文第 N 张图” + +### 15.7 最终结论 + +本项目当前“评查页码定位”对 `docx` 的可用性,主要来自 OCR 归一化页结构,而不是 `python-docx` 的原生分页能力。 + +因此文档图片质量校验模块如果要把 `docx` 也做准,正确方向是: + +- 参考现有 OCR / 评查流程的页结构设计 +- 复用 `ocr_result.pages / visual_manifest / bbox / image_key` +- 避免把当前 `docx` 文本兜底逻辑误当成图片级定位能力 + +这一点在后续实施时应作为明确技术约束,不建议再走“直接解析 Word 文本推页码”的路线。 + +## 16. 图片索引表与 OCR visual_manifest 关联策略 + +这一节用于把图片质量模块里最关键的“抽图记录如何回绑到 OCR 页码与 bbox”讲清楚,尤其是 `doc/docx/wps` 场景。 + +### 16.1 为什么必须做关联层 + +图片质量模块自己的 `extractors.py` 会先把图片从原始文档里抽出来,并落到 `leaudit_image_quality_items`。 + +但抽图本身只能保证下面这些信息: + +- 这张图片来自哪个文档文件 +- 它是主文件还是附件 +- 它是文件内第几张图 +- 它在抽图阶段的原始宽高、字节、hash + +仅靠这些信息,还不够支持前端稳定回指到: + +- 第几页 +- 页内第几张 +- 具体 bbox + +所以必须增加一层“图片索引表与 OCR visual metadata 的关联层”,把抽图记录和 OCR/visual_manifest 里的页码、坐标、父图像标识打通。 + +### 16.2 建议增加的落库字段 + +在 `leaudit_image_quality_items` 里,建议补充或明确以下字段: + +- `image_sha256` + - 抽图后二进制 hash +- `image_width` + - 抽图宽度 +- `image_height` + - 抽图高度 +- `source_ext` + - 来源文件扩展名 +- `ocr_page_num` + - 关联到 OCR 后得到的页码 +- `ocr_bbox_json` + - 关联到 OCR 后得到的 bbox +- `ocr_image_key` + - 关联到 OCR visual object 的 image_key +- `ocr_parent_image_key` + - 关联到 OCR visual object 的 parent_image_key +- `ocr_match_mode` + - 关联命中方式 +- `ocr_match_score` + - 关联命中置信度 + +说明: + +- `source_page_num` 表示模块自己理解的来源页码 +- `ocr_page_num` 表示从 OCR / visual_manifest 里回填的页码 +- 第一版前端展示时,优先取 `ocr_page_num`,取不到再回退到 `source_page_num` + +### 16.3 关联优先级建议 + +建议在 `runner.py` 里做一段统一的关联流程,优先级从高到低如下: + +1. `image_key / parent_image_key` 直接命中 +2. 图片二进制 hash 命中 +3. 图片尺寸 + 裁剪相似度命中 +4. 页级候选范围内按 bbox / area 比例匹配 +5. 全部失败则降级为文件内序号定位 + +这套顺序的原因是: + +- `image_key / parent_image_key` 一旦能命中,最稳定 +- hash 命中次稳,但前提是 OCR side 和抽图 side 用的是同一图像字节 +- 尺寸和相似度可以做弱匹配 +- bbox/area 更适合已经知道页候选时做局部定位 + +### 16.4 各类文件的推荐关联策略 + +#### 16.4.1 PDF / 扫描PDF / PDF附件 + +推荐策略: + +- 抽图阶段直接记录 `source_page_num` +- OCR 若返回同页 visual object,则用页内 bbox 做二次确认 +- 最终定位以 `source_page_num + image_index_in_page + bbox` 为准 + +这一类文件通常不需要过度依赖 `visual_manifest`,因为自身抽图时就有稳定页概念。 + +#### 16.4.2 图片文件 / 图片附件 + +推荐策略: + +- `source_page_num` 固定记为 `1` +- `image_index_in_page = 1` +- 若 OCR 输出 visual object,则仅作为 bbox 或 `image_key` 补充 + +这类文件定位最简单,本身就是单页单图。 + +#### 16.4.3 `doc/docx/wps` + +推荐策略: + +- 抽图阶段只保证: + - `image_index_in_file` + - `image_sha256` + - `image_width / image_height` + - `source_file_name` +- 页码不要在抽图阶段硬算 +- 等 OCR/visual_manifest 结果出来后,再做回填 + +这一类文件的关联优先级建议是: + +1. `parent_image_key` 命中 +2. `image_key` 命中 +3. hash 命中 +4. 尺寸与内容相似度命中 +5. 命不中则只展示“正文第 N 张图” + +### 16.5 建议的 `ocr_match_mode` 枚举 + +建议在 `ocr_match_mode` 里固定以下值,方便后续排查: + +- `parent_image_key_exact` +- `image_key_exact` +- `image_sha256_exact` +- `size_similarity_match` +- `bbox_overlap_match` +- `page_candidate_match` +- `file_index_fallback` +- `no_match` + +这样后面如果业务反馈“页码不准”,可以直接按命中模式排查是哪一层出了问题。 + +### 16.6 `visual_manifest` 关联的实现建议 + +建议把这块逻辑放在 `image_quality/runner.py` 或单独的 `image_quality/locator.py` 中,职责清晰一点: + +- `extractors.py` + - 只负责抽图 + - 不负责猜页码 +- `detector.py` + - 只负责判断图片清晰度 +- `locator.py` 或 `runner.py` + - 负责把图片索引记录与 OCR `visual_manifest` 做关联 + - 回填 `ocr_page_num / ocr_bbox_json / ocr_match_mode` + +也就是说,页码定位不是抽图逻辑的一部分,而是“抽图后 + OCR结果可用后”的补全逻辑。 + +### 16.7 为什么建议先落图片索引,再做关联 + +因为如果把“抽图、关联、检测”混在一起,会有两个问题: + +- 当 OCR 结果未就绪时,图片质量模块会被迫等待主流程 +- 当 OCR side 结构调整时,图片质量模块会被连带影响 + +正确拆法是: + +1. 先独立抽图并持久化图片索引 +2. 独立跑清晰度检测 +3. 如果 OCR 结果已就绪,则补做 visual_manifest 关联 +4. 如果 OCR 结果尚未就绪,则先以弱定位展示,后续可异步补齐 + +这样既保持了两条 pipeline 独立,又能最大化复用 OCR 页结构。 + +### 16.8 页面展示的取值优先级 + +前端展示“第几页/第几张图”时,建议按下面顺序取值: + +1. `ocr_page_num + image_index_in_page` +2. `source_page_num + image_index_in_page` +3. `image_index_in_file` +4. 仅展示来源文件名 + +示例文案: + +- `主文件第 12 页第 1 张图片模糊` +- `附件《现场照片2.pdf》第 3 页第 2 张图片疑似模糊` +- `正文第 5 张内嵌图片模糊` +- `附件《取证照片.jpg》图片模糊` + +### 16.9 对第一版实施的建议收口 + +为了控制复杂度,第一版建议这样收: + +- `PDF / 扫描PDF / 图片文件 / 图片附件 / PDF附件` + - 做强定位 +- `doc/docx/wps` + - 先把 OCR `visual_manifest` 关联能力做出来 + - 关联成功就展示页码 + - 关联失败就降级到“正文第 N 张图” + +不要第一版就要求所有 Word/WPS 内嵌图都 100% 精确到预览页码,否则开发成本和误判风险都会明显上升。 + +### 16.10 最终结论 + +图片质量模块能不能把 `docx` 场景做准,关键不在“能不能把图抽出来”,而在“抽出来以后,能不能与 OCR visual metadata 建立稳定关联”。 + +所以第一版技术路线应明确为: + +- 先做图片索引表 +- 再做 visual_manifest 关联层 +- 最后才是页码/坐标展示 + +这比直接依赖 `python-docx` 或手工猜 Word 分页要稳得多,也更贴合当前项目已有的 OCR / 评查体系。 + +## 17. `extractors.py` 职责拆分与 `runner.py` 执行时序 + +这一节用于把模块内部分层再落细一点,方便研发直接按文件拆任务。 + +### 17.1 模块职责分层建议 + +建议图片质量模块按下面几层拆,不要把所有逻辑堆在一个 service 或一个 task 文件里。 + +#### 17.1.1 `input_resolver.py` + +职责: + +- 读取文档主文件与附件 +- 统一返回原始文件输入列表 +- 不做抽图 +- 不做检测 +- 不做 OCR 关联 + +建议输出结构: + +- `document_id` +- `document_file_id` +- `file_role` +- `file_name` +- `file_ext` +- `mime_type` +- `source_type` +- `source_path` +- `file_bytes` + +#### 17.1.2 `extractors.py` + +职责: + +- 针对不同文件类型抽图 +- 生成图片级索引记录 +- 记录图片最基础元数据 +- 不负责模糊判断 +- 不负责最终页码回指策略 + +建议内部再拆为: + +- `extract_images_from_pdf()` +- `extract_images_from_image_file()` +- `extract_images_from_docx()` +- `extract_images_from_attachment()` +- `persist_image_index_items()` + +建议统一输出的数据结构: + +- `documentId` +- `documentFileId` +- `sourceKind` +- `sourceFileName` +- `sourcePageNum` +- `imageIndexInPage` +- `imageIndexInFile` +- `imageSha256` +- `imageWidth` +- `imageHeight` +- `imageBytes` +- `extraJson` + +其中: + +- `PDF / 扫描PDF / 图片文件 / 图片附件` 可以在这里直接给出 `sourcePageNum` +- `doc/docx/wps` 这里不强行给精确页码,允许先留空 + +#### 17.1.3 `detector.py` + +职责: + +- 对抽出的图片执行清晰度检测 +- 返回三档结果 +- 输出原因码和说明 +- 不负责页码定位 + +建议统一返回: + +- `qualityStatus` +- `qualityScore` +- `reasonCode` +- `reasonText` + +建议原因码先收敛为有限集合: + +- `blur_detected` +- `low_resolution` +- `over_exposure` +- `under_exposure` +- `motion_blur` +- `text_unreadable` +- `detector_timeout` +- `detector_failed` + +#### 17.1.4 `locator.py` + +职责: + +- 读取 OCR / visual_manifest +- 把图片索引记录与 visual object 做关联 +- 回填 `ocr_page_num / ocr_bbox_json / ocr_match_mode` +- 不负责清晰度打分 + +如果第一期不想单独开 `locator.py`,也可以先内聚到 `runner.py`,但职责概念上仍然建议独立。 + +#### 17.1.5 `storage_adapter.py` + +职责: + +- 创建 run +- 批量写入 image items +- 更新图片检测结果 +- 更新关联结果 +- 汇总 run 主状态 + +不要把业务判断写进这里,这里只做存取。 + +### 17.2 `extractors.py` 建议的实现规则 + +#### 17.2.1 PDF 抽图规则 + +建议: + +- 按页遍历 +- 同页内按出现顺序编号 `image_index_in_page` +- 全文件维度编号 `image_index_in_file` +- 能拿到 bbox 就记录 bbox +- 原始图片字节计算 `sha256` + +最终目标: + +- PDF 在抽图阶段就拿到强定位能力 + +#### 17.2.2 图片文件抽图规则 + +建议: + +- 单文件直接视为 1 页 1 图 +- `sourcePageNum = 1` +- `imageIndexInPage = 1` +- `imageIndexInFile = 1` + +#### 17.2.3 `doc/docx/wps` 抽图规则 + +建议: + +- 只抽出内嵌图片 +- 保证文件内顺序稳定 +- 第一版不在这里做页码推断 +- 重点保留: + - `image_sha256` + - `image_width` + - `image_height` + - `image_index_in_file` + +原因: + +- 这类文件的难点不在抽图,而在“抽图后如何挂回 OCR 页结构” + +#### 17.2.4 附件抽图规则 + +建议: + +- 每个附件独立视为一个来源文件 +- 不要沿用主文件页码空间 +- `source_file_name` 必填 +- 前端展示时允许出现: + - `附件《xxx.pdf》第 2 页第 1 张图片模糊` + - `附件《xxx.jpg》图片模糊` + +### 17.3 `runner.py` 建议执行时序 + +建议整条图片质量任务按下面时序执行: + +1. 创建或确认 `image_quality_run` +2. 更新 run 状态为 `running` +3. 调用 `input_resolver.py` 读取主文件与附件 +4. 调用 `extractors.py` 抽图 +5. 将抽图结果落表 `leaudit_image_quality_items` +6. 判断是否无图 +7. 若无图则 run 标记 `skipped/no_images` +8. 若有图则进入并发检测 +9. 调用 `detector.py` 批量检测,回写每条 item 的质量结果 +10. 尝试读取 OCR / visual_manifest +11. 若可读,则做 `locator` 关联并回填页码/bbox +12. 汇总 `pass/review/reject` 计数 +13. 更新 run 的 `summary_status` +14. 标记 run `completed` 或 `partial_failed` + +### 17.4 推荐时序图口径 + +可以把执行理解成下面这条线: + +1. 上传成功 +2. 文档主流程正常投递 OCR/评查 +3. 图片质量任务独立投递 +4. 图片质量任务先抽图、先检测 +5. OCR 若先完成,则图片质量任务顺手做 visual_manifest 关联 +6. OCR 若未完成,则图片质量任务先给弱定位结果 +7. 后续可通过补偿任务把弱定位升级成强定位 + +这意味着: + +- 图片质量任务不依赖 OCR 主任务完成后才能启动 +- 但它可以在 OCR 结果可用时变得更准 + +### 17.5 是否需要补偿任务 + +建议保留“可选补偿任务”设计,但第一版不强求一定实现。 + +补偿任务适合处理: + +- 图片质检先完成,但 OCR 尚未完成 +- 首次只拿到了 `image_index_in_file` +- OCR 完成后再补齐 `ocr_page_num / bbox` + +如果要做,建议命名: + +- `leaudit.image_quality.relocate_by_ocr` + +但第一版也可以先不单独开 task,而是在详情查询时做一次懒补偿,或者在 OCR 主流程结束后顺手触发一次回填。 + +### 17.6 `runner.py` 的失败处理原则 + +建议明确: + +- 抽图失败: + - 当前 run 标记 `failed` + - 不影响文档主流程 +- 单张图片检测失败: + - 当前 item 标记失败原因 + - run 最终允许为 `partial_failed` +- OCR 关联失败: + - 不算质检失败 + - 只算定位降级 +- 全部图片检测完成但都无法定位页码: + - run 仍可 `completed` + - 只是展示层回退到“第 N 张图” + +也就是说,质量检测失败和定位失败要分开统计,不能混成一类。 + +### 17.7 推荐的汇总规则 + +建议 `runner.py` 最终汇总时按下面规则判断: + +- 只要存在至少 1 张 `reject`,`summary_status = reject` +- 否则只要存在至少 1 张 `review`,`summary_status = review` +- 否则若全部 `pass`,`summary_status = pass` +- 若任务本身没跑起来,则 `status = failed` +- 若任务跑完但部分图片检测失败,则 `status = partial_failed` +- 若命中无图、纯文本、开关关闭,则 `status = skipped` + +### 17.8 第一版最小可交付实现 + +为了避免第一版过重,建议把可交付标准压到下面这一级: + +- `extractors.py` + - 先支持 PDF、图片、docx 内嵌图、附件 +- `detector.py` + - 先给出稳定三档判断 +- `runner.py` + - 先能完成抽图、检测、汇总 +- `locator` + - 先支持 OCR `visual_manifest` 存在时的强关联 + - 不存在时允许降级 + +只要能做到: + +- 不影响主流程 +- 能把问题图片列出来 +- 大多数 PDF/图片类文档能精确到页 +- `docx` 至少能稳定定位到“正文第 N 张图” + +第一版就已经是可上线状态。 + +### 17.9 最终建议 + +研发拆任务时,建议直接按下面方式分: + +- A 同学:`SQL + VO + Controller + Service` +- B 同学:`input_resolver + extractors` +- C 同学:`detector + runner` +- D 同学:`locator / OCR visual_manifest 关联` +- 前端同学:`上传页 + 列表页 + 详情页展示` + +这样拆分写入面冲突最小,也最贴合当前仓库结构。 diff --git a/docs/项目总览/前端分支提交与合并防覆盖操作规范-2026-05-19.md b/docs/项目总览/前端分支提交与合并防覆盖操作规范-2026-05-19.md new file mode 100644 index 0000000..35909ca --- /dev/null +++ b/docs/项目总览/前端分支提交与合并防覆盖操作规范-2026-05-19.md @@ -0,0 +1,456 @@ +# 前端分支提交与合并防覆盖操作规范 + +## 适用范围 + +本文档适用于 `legal-platform-frontend` 前端仓库,重点解决以下高频问题: + +- 本地有未提交改动时,如何安全合并别人的分支 +- `main`、`wren-dev`、`shiy-dev` 等并行开发分支之间,如何避免代码被覆盖 +- 为什么“提交历史已经包含某个 commit”,但代码内容实际丢了 +- 如何做正确的提交、推送、PR 和合并后校验 + +--- + +## 一、核心原则 + +### 1. 不要在脏工作区直接合并 + +合并前必须先执行: + +```bash +git status +``` + +如果存在未提交改动,必须先处理: + +- 能独立提交的,先提交 +- 暂时不想提交的,先 `stash` + +例如: + +```bash +git stash -u +``` + +否则很容易出现: + +- 合并过程把本地改动混进别人的改动 +- 冲突解决时误选本地旧代码 +- 最终提交混杂多个需求,后续难以回溯 + +### 2. 不要把“历史包含”误判成“功能保留” + +以下命令只能说明“某个提交进入过历史”: + +```bash +git branch --contains +``` + +但它**不能说明这次提交改过的内容现在还保留在代码树里**。 + +实际开发中常见情况是: + +- A 分支的提交先合入 `main` +- 后续 `main` 再合到 B 分支 +- 冲突时错误选择了旧版本 +- 结果:`commit` 在历史里,但 `patch` 被覆盖没了 + +所以合并后必须额外做“内容保留校验”。 + +### 3. 冲突处理不能图快全选一边 + +遇到冲突时,不能默认: + +- 全部选 `ours` +- 全部选 `theirs` + +必须逐文件判断: + +- 哪些是对方新增能力 +- 哪些是我方已有修复 +- 哪些要手工拼接 + +尤其是以下类型文件最容易被误覆盖: + +- 页面组件 +- API 路由 +- 配置文件 +- 公共组件 +- 菜单/路由白名单 + +### 4. 合并完成后必须做“保留性校验” + +至少要检查三件事: + +1. 提交历史是否进入 +2. 关键文件是否仍保留目标改动 +3. 关键标识是否还能在代码中搜到 + +--- + +## 二、标准操作流程 + +## 1. 合并别人的分支到自己分支 + +假设目标是:把 `origin/shiy-dev` 合到当前 `wren-dev` + +### 第一步:同步远程 + +```bash +git fetch origin +``` + +### 第二步:检查本地工作区 + +```bash +git status +``` + +如果有未提交改动: + +```bash +git stash -u +``` + +或者先拆分提交。 + +### 第三步:确认自己当前所在分支 + +```bash +git branch --show-current +``` + +必须确认当前就在 `wren-dev`,不要站错分支。 + +### 第四步:执行合并 + +```bash +git merge --no-ff origin/shiy-dev +``` + +如果要明确记录来源,建议使用: + +```bash +git merge --no-ff origin/shiy-dev -m "merge: sync origin/shiy-dev into wren-dev" +``` + +### 第五步:如果冲突,逐文件处理 + +处理完冲突后: + +```bash +git add <冲突文件> +git commit +``` + +### 第六步:恢复之前的 stash + +```bash +git stash pop +``` + +如果 `stash pop` 冲突,不要慌,继续按文件处理。 + +--- + +## 2. 合并 `main` 到自己分支 + +目标:保持 `wren-dev` 跟上主线进度 + +标准步骤: + +```bash +git fetch origin +git status +git stash -u # 如果有本地未提交改动 +git merge --no-ff origin/main -m "merge: sync origin/main into wren-dev" +git stash pop # 如果前面 stash 了 +``` + +说明: + +- 不要直接把 `main` 切到当前脏工作区里操作 +- 如果本地 `main` 有 worktree,优先在独立 worktree 同步 +- 当前主开发工作区建议长期停留在 `wren-dev` + +--- + +## 3. 合并后做“内容保留校验” + +这是最关键的一步。 + +### 先检查目标提交是否进入历史 + +```bash +git branch --contains +``` + +### 再检查关键文件内容是否保住 + +```bash +git diff ..HEAD -- <关键文件> +``` + +如果看到的是“反向撤销”差异,说明: + +- 历史里有这个提交 +- 但代码内容被后续 merge 覆盖掉了 + +### 再搜关键符号 + +例如某次改动新增了: + +- `ENTRY_MODULE_ROUTE_OPTIONS` +- `FormSelect` +- `isAllowedEntryModuleRoutePath` + +就应该执行: + +```bash +rg "ENTRY_MODULE_ROUTE_OPTIONS|FormSelect|isAllowedEntryModuleRoutePath" . +``` + +如果关键标识不在,说明功能实际上没有保住。 + +--- + +## 三、发现“历史包含但代码没了”怎么办 + +这是本项目已经真实发生过的情况。 + +## 处理原则 + +不要重新大范围 merge。 + +正确做法是: + +- 精确定位丢失的是哪些文件 +- 只恢复这些文件 +- 单独提交 + +### 推荐命令 + +```bash +git restore --source=<目标提交> -- <文件1> <文件2> ... +``` + +例如: + +```bash +git restore --source=2b912ca -- \ + app/(audit)/documents/list/DocumentsListClient.tsx \ + app/(audit)/entry-modules/new/EntryModuleNewClient.tsx \ + app/api/pdf-proxy/route.ts \ + components/layout/Sidebar.tsx \ + lib/config/entry-module-route-options.ts +``` + +然后单独提交: + +```bash +git add <这些文件> +git commit -m "fix: restore shiy-dev entry route whitelist changes" +git push origin wren-dev +``` + +这种方式最安全,且不会影响你当前其他本地需求改动。 + +--- + +## 四、提交规范 + +## 1. 一个提交只解决一类问题 + +不要把以下内容混在一个 commit: + +- 聊天功能修复 +- 公文审查 UI 收敛 +- 合同模板页面开发 +- 分支恢复补丁 + +应该拆成: + +- `fix: remove govdoc inspector file info tab` +- `fix: restore shiy-dev entry route whitelist changes` +- `feat: add contract template search results page` + +## 2. 提交前先看范围 + +提交前务必执行: + +```bash +git status +git diff --stat +``` + +如果只想提交部分文件: + +```bash +git add <目标文件> +git commit -m "" +``` + +不要图省事直接: + +```bash +git add . +``` + +除非你确认工作区所有改动都属于同一件事。 + +## 3. 提交信息规范 + +推荐格式: + +```text +feat: 新增功能 +fix: 修复问题 +refactor: 重构实现 +merge: 分支合并 +docs: 文档更新 +``` + +示例: + +- `feat: stabilize rag chat conversation lifecycle` +- `fix: remove govdoc inspector file info tab` +- `fix: restore shiy-dev entry route whitelist changes` +- `merge: sync origin/main into wren-dev` + +--- + +## 五、推送规范 + +推送前先确认: + +```bash +git status +git log --oneline --max-count=5 +``` + +再执行: + +```bash +git push origin wren-dev +``` + +如果本地还有未提交改动,不影响推送已提交的 commit,但要明确知道: + +- 已提交内容会推上去 +- 未提交内容不会推上去 + +不要误以为“工作区里看到的所有代码”都已经在远程。 + +--- + +## 六、PR 规范 + +PR 标题必须说明“做了什么”,不要只写模块名。 + +推荐示例: + +- `恢复入口模块跳转路径白名单与 FormSelect 收敛改动` +- `修复 RAG 对话会话生命周期、自动重命名刷新与列表状态问题` + +PR 描述建议固定包含: + +### 1. 背景 + +为什么要改。 + +### 2. 本次改动 + +具体改了哪些点。 + +### 3. 影响范围 + +改到了哪些页面、接口、组件、模块。 + +### 4. 验证建议 + +告诉审核人怎么测。 + +### 5. 特别说明 + +如果是“恢复被覆盖改动”,要明确写出来,避免评审人误解成重复开发。 + +--- + +## 七、推荐命令清单 + +### 检查工作区 + +```bash +git status +git diff --stat +``` + +### 同步远程 + +```bash +git fetch origin +``` + +### 暂存本地未提交改动 + +```bash +git stash -u +git stash pop +``` + +### 合并主线或他人分支 + +```bash +git merge --no-ff origin/main +git merge --no-ff origin/shiy-dev +``` + +### 检查某个提交是否进入历史 + +```bash +git branch --contains +``` + +### 检查关键 patch 是否仍保留 + +```bash +git diff ..HEAD -- <关键文件> +``` + +### 精确恢复某次提交改动 + +```bash +git restore --source= -- <文件列表> +``` + +### 只提交指定文件 + +```bash +git add <文件列表> +git commit -m "" +``` + +### 推送当前分支 + +```bash +git push origin wren-dev +``` + +--- + +## 八、结论 + +以后判断一次合并是否真正成功,不能只看: + +- 分支图里有没有那个 commit + +还必须同时看: + +- 关键文件内容还在不在 +- 关键标识还能不能搜到 +- 最终页面行为是不是正确 + +一句话总结: + +> 合并成功的标准,不是“历史里有 commit”,而是“当前代码树里还保留对应 patch,并且功能行为正确”。 +