From 0d8f13ab3dd013c5e2b0a35751adbc88bf66b10b Mon Sep 17 00:00:00 2001 From: wren <“porlong@qq.com”> Date: Wed, 6 May 2026 09:42:29 +0800 Subject: [PATCH] docs: reorganize backend project documentation --- docs/HANDOFF.md | 500 +++++--- docs/README.md | 99 ++ docs/leaudit/README.md | 33 + docs/leaudit/SYSTEM_OVERVIEW.md | 497 -------- docs/leaudit/并发与重试参数说明.md | 149 --- docs/routes/http_routes_dedup_2026-04-27.md | 344 ------ ...28-fix-double-finalize-and-bindings-api.md | 683 ----------- docs/历史API与权限文档索引.md | 30 + docs/接口/README.md | 57 +- docs/接口/入口模块绑定最终设计方案.md | 762 ++++++++++++ docs/接口/文档上传与列表接口分析.md | 114 +- docs/接口/文档上传与评查接口.md | 692 ----------- docs/接口/文档类型与评查组关联方案.md | 127 -- docs/接口/新系统版_documents_list接口.md | 307 ----- docs/接口/用户权限与权限点清单.md | 2 +- docs/接口/用户权限初始化SQL.sql | 2 +- docs/接口/系统设置入口恢复说明.md | 133 --- docs/接口/老用户迁移脚本说明.md | 190 +++ docs/接口/评查点分组目标结构与迁移方案.md | 336 ++++++ docs/接口/评查点分组迁移执行前检查清单.md | 379 ++++++ docs/接口/首页入口接口落地说明.md | 188 --- docs/权限与地区隔离文档导航.md | 33 + docs/用户与地区权限完整设计方案.md | 35 + docs/用户权限开发TaskList.md | 878 -------------- .../老系统_docauditai_用户权限架构深度分析.md | 1047 ----------------- docs/规则编辑/README.md | 39 + docs/规则编辑/worker并发执行改造方案.md | 336 ------ docs/规则编辑/yaml规则在线编辑设计.md | 306 ----- docs/规则编辑/为什么仍然需要Bridge适配层.md | 296 ----- docs/规则编辑/原生AuditCtx接入重构方案.md | 44 +- docs/规则编辑/开发任务拆解清单.md | 711 ----------- docs/规则编辑/统一OSS与规则管理实施计划.md | 7 + docs/规则编辑/跑通全流程所需准备项.md | 520 -------- 33 files changed, 2469 insertions(+), 7407 deletions(-) create mode 100644 docs/README.md delete mode 100644 docs/leaudit/SYSTEM_OVERVIEW.md delete mode 100644 docs/leaudit/并发与重试参数说明.md delete mode 100644 docs/routes/http_routes_dedup_2026-04-27.md delete mode 100644 docs/superpowers/plans/2026-04-28-fix-double-finalize-and-bindings-api.md create mode 100644 docs/历史API与权限文档索引.md create mode 100644 docs/接口/入口模块绑定最终设计方案.md delete mode 100644 docs/接口/文档上传与评查接口.md delete mode 100644 docs/接口/文档类型与评查组关联方案.md delete mode 100644 docs/接口/新系统版_documents_list接口.md delete mode 100644 docs/接口/系统设置入口恢复说明.md create mode 100644 docs/接口/老用户迁移脚本说明.md create mode 100644 docs/接口/评查点分组目标结构与迁移方案.md create mode 100644 docs/接口/评查点分组迁移执行前检查清单.md delete mode 100644 docs/接口/首页入口接口落地说明.md create mode 100644 docs/权限与地区隔离文档导航.md delete mode 100644 docs/用户权限开发TaskList.md delete mode 100644 docs/老系统_docauditai_用户权限架构深度分析.md create mode 100644 docs/规则编辑/README.md delete mode 100644 docs/规则编辑/worker并发执行改造方案.md delete mode 100644 docs/规则编辑/yaml规则在线编辑设计.md delete mode 100644 docs/规则编辑/为什么仍然需要Bridge适配层.md delete mode 100644 docs/规则编辑/开发任务拆解清单.md delete mode 100644 docs/规则编辑/跑通全流程所需准备项.md diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index a1a7fcc..e5799e9 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1,7 +1,8 @@ -# 开发交接文档 — leaudit-platform 前后端 +# 开发交接文档 — leaudit-platform -> 本会话时间:2026-04-29 ~ 2026-04-30 -> 工作目录:`/home/wren-dev/Porject/leaudit-platform/` +> 本轮整理时间:2026-04-29 ~ 2026-05-04 +> 工作目录:`/home/wren-dev/Porject/leaudit-platform` +> 当前主线:前端 `new_doc_review` + 后端 `fastapi_modules/fastapi_leaudit` --- @@ -9,223 +10,404 @@ | 项目 | 路径 | 分支 | Remote | |------|------|------|--------| -| **后端** | `/home/wren-dev/Porject/leaudit-platform/` | `main` | **无**(需配置) | -| **前端** | `/home/wren-dev/Porject/leaudit-platform/new_doc_review/` | `wren` | `http://git.7bm.co:1024/jande/new_doc_review.git` | +| 后端 | `/home/wren-dev/Porject/leaudit-platform` | `main` | 当前未配置 remote | +| 前端 | `/home/wren-dev/Porject/leaudit-platform/new_doc_review` | `wren` | `http://git.7bm.co:1024/jande/new_doc_review.git` | -后端未推送(无 remote),前端 `wren` 分支领先 origin 7 个提交。 +## 数据库 / 环境 + +| 项目 | 当前值 | +|------|--------| +| 后端 API | `http://nas.7bm.co:8096` | +| 前端开发地址 | `http://nas.7bm.co:5173` / 本地 `http://localhost:5173` | +| PostgreSQL Host | `nas.7bm.co` | +| PostgreSQL Port | `54302` | +| Database | `leaudit_platform` | +| DB User | `docauditai_admin` | + +说明: +- 仓库内不记录数据库密码,仍需通过安全渠道获取 +- 当前用户已明确允许按现在配置的真实库继续联调 +- 文档里的“旧系统 / 老库”通常指 `docauditai` 侧历史表,不是让前端继续直接走旧接口 --- -## 数据库 +## 一、先看结论 -| 项 | 值 | -|------|-----| -| Host | `nas.7bm.co:54302` | -| 用户 | `docauditai_admin` | -| 密码 | `zhfw*123*` | -| 数据库 | `leaudit_platform` | +当前项目状态可以直接概括成 4 句话: + +1. 首页入口、菜单、RBAC、文档类型、规则组、文档上传这条主链路已经基本串起来了。 +2. 当前主要问题不是“完全不能用”,而是“新旧模型并存,部分语义还没完全收口”。 +3. 当前正确方向很明确:前端尽量统一走新后端接口,PostgREST 只保留存量过渡,不再新增依赖。 +4. 还要继续收口的核心是:上传后评查链路、文档类型到规则集的关系表达、以及旧字段旧文案清理。 + +### 当前正式阅读顺序 + +1. `docs/HANDOFF.md` +2. `docs/接口/README.md` +3. `docs/权限与地区隔离文档导航.md` +4. `docs/leaudit/README.md` +5. `docs/规则编辑/README.md` --- -## 一、已完成的工作 +## 二、本轮已完成的关键工作 -### 1. 首页入口模块 RBAC 过滤修复 +### 1. 文档列表详情 / 更新 / 删除已从 PostgREST 迁到新后端接口 -**问题**:`内部公文` 入口模块配置存在(id=3, path=/home),但首页不显示。 -**根因**:`HomeServiceImpl` 做二次过滤——入口模块的 targetPath 必须匹配用户有路由权限的路径。`provincial_admin` 的 `role_route` 里没有 `/home`。 -**修复**:插入 `role_route` — role_id=2, route_id=10 (/home), permission='RW', status=1。 -**后端提交**:`19c70d2` +后端已补接口: +- `GET /api/documents/{id}` +- `PUT /api/documents/{id}` +- `DELETE /api/documents/{id}` -### 2. 首页地区过滤修正 +实现口径: +- 文档列表、详情、更新、删除都围绕 `leaudit_documents` / `leaudit_document_files` +- 删除为软删除,并自动维护最新版本标记 +- 更新允许维护 `documentNumber`、`remark`、`isTestDocument`、`auditStatus` 等元数据 -**问题**:`provincial_admin` 跳过了入口模块的 areas 地区过滤,导致取消地区后仍可见。 -**修复**:`homeServiceImpl.py` 的 `bypass_area` 逻辑从包含 `provincial_admin` 改为只包含 `super_admin`。 -**后端提交**:`c16bb84` +对应文档: +- `docs/接口/文档上传与列表接口分析.md` -### 3. 细粒度权限执行 +### 2. 文档数据隔离已补到后端强约束 -**问题**:后端 `_assertManagePermission` 只检查用户是否属于 super_admin/provincial_admin/admin(粗粒度),不检查具体的 `rbac:roles:create` 等细粒度权限。 -**修复**: -- 新增 `_assertPermission(CurrentUserId, PermissionKey)` 方法 -- 所有 RBAC 管理端点补全细粒度权限检查: - - `CreateRole` → `rbac:roles:create` - - `UpdateRole` → `rbac:roles:update` - - `DeleteRole` → `rbac:roles:delete` - - `ListRoles/GetRoleRoutes/GetRolePermissions` → `rbac:roles:read` - - `ListUsers/ListRoleUsers/GetUserRoles` → `rbac:users:read` - - `AssignUserRoles/RevokeUserRole` → `rbac:user_roles:write` - - `UpdateRoleRoutes` → `rbac:role_routes:write` - - `SaveRolePermissions` → `rbac:role_permissions:write` - - `GetRoutePermissions` → `rbac:permissions:read` - - `super_admin` 自动跳过细粒度权限检查 -- 数据库给 `provincial_admin` (role_id=2) 和 `admin` (role_id=3) 补充了缺失的权限点 -**后端提交**:`33255e8`, `b6d7f15` +当前规则: +- `super_admin` / `provincial_admin`:看全量 +- `admin`:只能看本地市 `region = user.area` +- `common`:只能看自己上传的文档 -### 4. 统一异常处理 +已覆盖范围: +- 文档列表 +- 文档详情 +- 文档更新 +- 文档删除 -**问题**:`LeauditException` 没有注册 FastAPI exception handler,所有业务异常都变成 500。 -**修复**:`app.py` 新增 `@app.exception_handler(BusinessException)`,返回带状态码的 JSON。 -**后端提交**:`292b187` +这意味着“每个地市管理员只能改各自数据”的要求,已经不只是前端过滤,而是后端接口层强制执行。 -### 5. 403 错误信息中文化 +### 3. 上传页主依赖已改为新接口 -**问题**:403 返回 `"缺少权限: rbac:roles:delete"` 显示的是 permission key。 -**修复**:`_assertPermission` 查 `permissions` 表的 `display_name`,改为显示 `"缺少「删除角色」权限"`。 -**后端提交**:`ab31c80` +已完成: +- `GET /api/document-types` +- `GET /api/documents/list` 支持 `userId`、日期范围等过滤 +- `GET /api/v2/system/queue/status` +- `POST /api/upload` 前端已改为扁平 `multipart/form-data` -**前端**:axios 拦截器不再硬编码 `"无权限"`,改为透传后端的具体消息。`frontendJWT` 改为 `canEdit`,包含 `provincial_admin` + `admin` 两个角色。 -**前端提交**:`185f55c`, `e2ae791` +当前口径: +- 主文件上传已经走新后端 +- 合同附件上传仍然是要继续收口的重点场景,因为它与规则抽取、评查触发紧密耦合 -### 6. 上传页 PostgREST 依赖替换 +### 4. 文档类型 CRUD 与绑定能力已切到新后端 -**问题**:上传页 loader 里 `getTodayDocuments`、`getDocumentTypes`、`getQueueStatus` 调 PostgREST 返回 404。 -**后端新增**: -- `GET /api/document-types` — 文档类型列表 -- `GET /api/documents/list` 新增 `userId`、`dateFrom`、`dateTo` 过滤参数 -- `GET /api/v2/system/queue/status` — 队列状态(查询 `leaudit_documents` 的 `processing_status` 统计) -**后端提交**:`8f307ae`, `9e1b7a6`, `e6e129c` +后端已支持: +- `GET /api/document-types` +- `GET /api/document-types/{id}` +- `POST /api/document-types` +- `PUT /api/document-types/{id}` +- `DELETE /api/document-types/{id}` -**前端**: -- `getDocumentTypes()` → `GET /api/document-types` -- `getTodayDocuments()` → `GET /api/documents/list?userId=&dateFrom=` -- `getQueueStatus()` → 404 时返回空状态不报错 -**前端提交**:`dd249cc` +当前文档类型支持维护: +- 编码 / 名称 / 描述 +- 入口模块绑定 +- 启停状态 +- 规则集绑定(`ruleSetIds`) -### 7. 上传接口对齐 +说明: +- 这部分是“文档类型绑定入口模块”和“文档类型绑定规则集”的基础设施 +- 目前页面 UI 仍在继续按老系统风格收口,但主接口已具备 -**问题**:前端上传 `POST ${UPLOAD_URL}/upload` 用的是嵌套 JSON 的 `upload_info` 格式,与新后端 `POST /api/upload` 的扁平 `multipart/form-data` 不兼容。 -**修复**: -- `uploadDocumentToServer()` 重写,用扁平 FormData(typeId, region, fileRole, createdBy, autoRun, speed) -- `handleFileUpload()` 适配:从 `userInfo.area` 取 `region`,从 `Priority` 枚举映射 `speed` -- 响应类型从 `FileUploadResponse` 改为 `UploadResult`(`documentId` 替代 `result.id`) -**前端提交**:`73fd861` +### 5. 文档类型子页权限问题已收口一轮 -### 8. 文档类型 CRUD 后端 +已补的关键修复: +- `route-alias` 从零散判断升级为配置表 +- 给新建/编辑/详情/结果页补齐父列表权限映射 +- 新增约束文档,明确:什么时候该加 alias,什么时候该补真实菜单路由 +- 已增加独立测试脚本,`npm run test:route-aliases` 可直接回归 -**新增端点**: -``` -POST /api/document-types 创建(含 rule_set_ids 自动绑定) -PUT /api/document-types/{id} 更新(rule_set_ids 全量替换) -DELETE /api/document-types/{id} 软删除(级联解绑 rule_type_bindings) -GET /api/document-types/{id} 详情 -GET /api/document-types 列表(扩展字段:description, entryModuleId, isEnabled, ruleSetIds) -``` +关键文件: +- `new_doc_review/app/utils/route-alias.shared.js` +- `new_doc_review/docs/route-alias-guidelines.md` -**关键实现**: -- `DocumentTypeItemVO` 新增 `description`, `entryModuleId`, `isEnabled`, `ruleSetIds` -- `DocumentTypeCreateDTO` / `DocumentTypeUpdateDTO` 支持 `ruleSetIds` 传入 -- 创建/更新时自动调用 `_syncRuleBindings()` 全量替换 `leaudit_rule_type_bindings` -- 软删除级联清理 bindings -**后端提交**:`52c2bed` +已覆盖的典型路径: +- `/documents/list -> /documents` +- `/document-types/new -> /document-types` +- `/contract-template/detail/:id -> /contract-template/list` +- `/contract-draft/:id -> /contract-template/list` +- `/chat-with-llm/chat -> /chat-with-llm` +- `/chat-with-llm/dataset-manager -> /chat-with-llm` -### 9. 文档类型管理前端页面 +### 6. 首页、菜单、最小可见路由与真实 RBAC 路由已补齐 -**文件**: -- `app/api/document-types/document-types.ts` — 重写,调用新后端 `/api/document-types` + `/api/rule-sets` -- `app/routes/document-types._index.tsx` — 列表页(编码、名称、入口模块、规则集数量、状态) -- `app/routes/document-types.new.tsx` — 新建/编辑页(编码、名称、描述、入口模块下拉、规则集多选) +这轮不只是 alias 修了一下,而是把“用户能否看到模块、能否从侧边栏稳定进入”的缺口补了: +- `minimal-scope.ts` 已补 `/contract-template`、`/cross-checking`、`/document-types` +- 后端 `rbacServiceImpl.py` 同步补最小可见路由逻辑 +- 已新增并执行首页 / 前端路由范围 seed 脚本 -**前端提交**:`81c5e98`, `477bcaf` +结果: +- `chat-with-llm` +- `contract-template` +- `cross-checking` +- `document-types` -### 10. 文档类型路由注册 +这些模块不再只靠历史残留路由“碰运气出现”,而是进入了真实的 RBAC 可见范围。 -**修复**: -- `_MANAGEABLE_ROUTE_BLUEPRINTS` 添加 `/document-types` 路由 -- `_MANAGEABLE_PERMISSION_BLUEPRINTS` 添加 `doc_type:*` 权限定义 -- `_MINIMAL_VISIBLE_ROUTE_PREFIXES` 添加 `/document-types`(关键!) -- `_PERMISSION_PREFIXES_BY_PATH` 添加 `/document-types` → `["doc_type:"]` -- 数据库 `sys_routes` 和 `role_route` 已补 `/document-types` -**后端提交**:`283c822`, `acdcfeb` +### 7. Sidebar 已补路径兜底,不再只依赖 sessionStorage + +之前问题: +- 直接刷新子页面或从地址栏直开,侧边栏上下文容易丢失 + +已完成: +- `new_doc_review/app/components/layout/Sidebar.tsx` 增加基于路径的 fallback 识别 +- 已覆盖 settings / cross-checking / chat / contract-template / contract-draft 等模块 + +结果: +- 刷新后仍能显示正确模块上下文 +- 用户不会因为 sessionStorage 丢失而看到错误的侧栏状态 + +### 8. 合同管理模块命名已统一一轮 + +统一方向: +- 模块层叫:`合同管理` +- 子功能叫:`模板搜索`、`模板列表` + +已覆盖: +- 路由 seed +- 最小路由 scope +- 页面标题与描述 +- 侧边栏模块名 + +说明: +- 这次统一的是“首页入口 / 侧边栏 / 页面标题”的基础口径 +- 仍需继续检查细碎页面文案,避免出现“合同模板 / 合同管理 / 模板列表”混用过多 + +### 9. 规则集可用性问题已定位到真实规则文件并修复 + +发现的问题: +- `借款合同` 的规则集显示“可用规则数 0” +- 根因不是前端,而是 MinIO 上真实规则文件 DSL 非法 + +已修复: +- `rules/contract_loan/rules.yaml` +- `new_doc_review/mock-data/leaudit-rules/packs/yc/contract_loan/rules.yaml` +- 同步替换了 MinIO 对象并保留备份 + +结果: +- `contract.loan.general` 的 `usableRuleCount` 已恢复为 `10` + +### 10. 评查点分组与评查点主数据已进入“新接口包旧数据”的过渡形态 + +这是当前系统最容易混乱的部分,结论要写清楚: + +- 前端页面与后端接口层,正在往新系统接口收口 +- 但评查点分组 / 评查点主数据的正式新库模型还没有完全替代旧数据 +- 所以这部分当前采用的是:**新接口 + 老库存量数据** 的过渡方案 + +已补接口: +- `GET /api/v3/evaluation-point-groups/by-document-types` +- `GET /api/v3/evaluation-point-groups/{id}/children` +- `GET /api/v3/evaluation-points` +- `GET /api/v3/evaluation-points/{id}` +- `POST /api/v3/evaluation-points` +- `PUT /api/v3/evaluation-points/{id}` +- `DELETE /api/v3/evaluation-points/{id}` +- `GET /api/v3/evaluation-points/attribute-types` + +当前业务解释: +- 一级分组:业务大类根节点 +- 二级分组:实际挂评查点、挂规则集的子类型节点 +- 文档类型绑定的是一级分组范围 +- 业务页面实际使用时,再展开到对应二级分组与规则 + +### 11. 规则组迁移方案文档与脚本已经补齐 + +已补充: +- `scripts/precheck_rule_group_migration.sql` +- `scripts/migrate_rule_groups_to_business_roots.sql` +- `scripts/migrate_rule_groups_to_doc_type_roots.sql` +- `docs/接口/评查点分组迁移执行前检查清单.md` + +说明: +- 这部分还属于“迁移方案与执行准备已就绪” +- 是否在正式库落地,需要按检查清单逐步执行 + +### 12. 合同上传失败原因已开始朝前端可读化方向收口 + +当前已明确的真实报错语义包括: +- 当前文档类型未绑定可用规则数 +- 入口模块配置异常 +- 某规则集不可运行 / 规则文件异常 + +说明: +- 这块产品表达要统一成“可用规则数”,不要再出现“可用版本”旧说法 +- 页面上还需要继续把这些错误稳定映射成用户可理解提示 --- -## 二、关键代码位置速查 +## 三、当前业务逻辑口径 + +### 1. 入口模块、文档类型、规则组、规则集之间的关系 + +当前建议口径: + +1. 入口模块:业务入口容器,例如合同、卷宗、后续新增模块 +2. 文档类型:属于某个入口模块下的具体业务类型 +3. 一级分组:该文档类型所属的评查业务大类 +4. 二级分组:该业务大类下的实际子类型 +5. 规则集:挂在二级分组下的具体可执行规则包 + +也就是: + +`入口模块 -> 文档类型 -> 一级分组 -> 二级分组 -> 规则集 -> 上传抽取评查` + +### 2. 为什么规则集挂在二级分组下 + +原因不是硬编码,而是为了和老系统评查实际执行逻辑对齐: +- 一级分组负责定义“这类文档能进入哪棵规则树” +- 二级分组才是实际选择具体规则配置的位置 +- 上传或评查运行时,最终命中的也是子类型级别的规则集 + +### 3. 为什么不能继续新增 PostgREST 依赖 + +因为现在最容易出问题的地方,几乎都和它有关: +- 路径找不到 +- 结构和现有后端不一致 +- 权限口径分裂 +- 老字段命名污染当前页面文案 + +所以现在的原则已经很明确: +- 已经有新后端接口的页面,不再回退到 PostgREST +- 还没替换完的地方,只做过渡,不扩散 + +--- + +## 四、关键代码位置速查 ### 后端 -| 功能 | 文件 | +| 位置 | 作用 | |------|------| -| 异常处理 | `fastapi_admin/app.py:67-76` | -| 权限检查 | `rbacAdminServiceImpl.py:571-606` (`_assertPermission`) | -| 用户上下文 | `rbacAdminServiceImpl.py:608-620` (`_getCurrentUserContext`) | -| 文档上传 | `documentServiceImpl.py:44-246` (`Upload`) | -| 文档列表 | `documentServiceImpl.py:248-443` (`ListDocuments`) | -| 文档类型 CRUD | `documentServiceImpl.py:477-560` | -| 文档类型列表 | `documentController.py:87-130` | -| 队列状态 | `documentController.py:135-180` | -| 路由可见白名单 | `rbacServiceImpl.py:21-29` (`_MINIMAL_VISIBLE_ROUTE_PREFIXES`) | -| 权限前缀映射 | `rbacServiceImpl.py:539-550` (`_PERMISSION_PREFIXES_BY_PATH`) | -| 首页地区过滤 | `homeServiceImpl.py:39` (`bypass_area`) | -| 首页路由过滤 | `homeServiceImpl.py:91-108` | +| `fastapi_modules/fastapi_leaudit/controllers/documentController.py` | 文档上传、列表、详情、更新、删除 | +| `fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py` | 文档主服务实现、数据隔离、版本链处理 | +| `fastapi_modules/fastapi_leaudit/services/impl/auditServiceImpl.py` | 上传后触发评查 | +| `fastapi_modules/fastapi_leaudit/controllers/rbacAdminController.py` | RBAC 管理接口 | +| `fastapi_modules/fastapi_leaudit/services/impl/rbacAdminServiceImpl.py` | 角色、用户、权限管理逻辑 | +| `fastapi_modules/fastapi_leaudit/services/impl/rbacServiceImpl.py` | 路由权限、最小可见路由、菜单可见性 | +| `fastapi_modules/fastapi_leaudit/controllers/evaluationPointGroupController.py` | 评查点分组 v3 接口 | +| `fastapi_modules/fastapi_leaudit/services/impl/evaluationPointGroupServiceImpl.py` | 分组树、按文档类型反查分组 | +| `fastapi_modules/fastapi_leaudit/controllers/evaluationPointController.py` | 评查点主数据 v3 接口 | +| `fastapi_modules/fastapi_leaudit/services/impl/evaluationPointServiceImpl.py` | 评查点列表/详情/增删改 | +| `fastapi_modules/fastapi_leaudit/services/impl/ruleServiceImpl.py` | 规则集列表、可用规则数、规则文件校验 | ### 前端 -| 功能 | 文件 | +| 位置 | 作用 | |------|------| -| 上传 API | `app/api/files/files-upload.ts` | -| 文档类型 API | `app/api/document-types/document-types.ts` | -| 上传页面 | `app/routes/files.upload.tsx` | -| 文档类型列表 | `app/routes/document-types._index.tsx` | -| 文档类型编辑 | `app/routes/document-types.new.tsx` | -| 403 拦截器 | `app/api/axios-client.ts:183-190` | +| `new_doc_review/app/api/files/files-upload.ts` | 上传主文件 / 附件相关前端调用 | +| `new_doc_review/app/api/files/documents.ts` | 文档列表、详情、更新、删除 API | +| `new_doc_review/app/api/document-types/document-types.ts` | 文档类型 CRUD 与绑定接口 | +| `new_doc_review/app/api/evaluation_points/rules.ts` | 评查点列表、详情、按文档类型分组查询 | +| `new_doc_review/app/api/evaluation_points/rule-groups.ts` | 评查点分组页 API | +| `new_doc_review/app/components/layout/Sidebar.tsx` | 侧边栏模块识别与路径 fallback | +| `new_doc_review/app/hooks/usePermission.tsx` | 路由权限判断与 alias 应用 | +| `new_doc_review/app/utils/route-alias.shared.js` | 子页权限映射配置表 | +| `new_doc_review/app/routes/document-types.*` | 文档类型列表 / 新建编辑页 | +| `new_doc_review/app/routes/documents.*` | 文档列表 / 编辑页 | +| `new_doc_review/app/routes/rule-groups*` | 评查点分组页 | +| `new_doc_review/app/routes/rules.*` | 评查点规则列表 / 新建编辑页 | +| `new_doc_review/app/routes/files.upload.tsx` | 上传页 | -### 数据库关键表 +### 规则 / 脚本 / 文档 -| 表 | 用途 | -|----|------| -| `leaudit_document_types` | 文档类型(20 条种子数据) | -| `leaudit_rule_type_bindings` | 文档类型→规则集绑定 | -| `leaudit_rule_sets` | 规则集 | -| `leaudit_documents` | 文档(含版本管理) | -| `leaudit_document_files` | 文档文件 | -| `role_permissions` | 角色细粒度权限 | -| `role_route` | 角色路由授权 | -| `sys_routes` | 系统路由定义 | +| 位置 | 作用 | +|------|------| +| `rules/contract_loan/rules.yaml` | 借款合同规则集修复点 | +| `scripts/seed_frontend_route_scope.sql` | 前端真实路由范围 seed | +| `scripts/user_rbac_seed.sql` | RBAC 基础 seed | +| `scripts/seed_home_entry_modules.sql` | 首页入口模块 seed | +| `scripts/precheck_rule_group_migration.sql` | 分组迁移前检查 | +| `scripts/migrate_rule_groups_to_business_roots.sql` | 按业务根迁移分组 | +| `scripts/migrate_rule_groups_to_doc_type_roots.sql` | 按文档类型根迁移分组 | +| `docs/接口/` | 当前有效接口主文档 | --- -## 三、未完成 / 待做 +## 五、下一步最该做什么 -### 高优先级 +### P0:必须继续做 -1. **上传端到端验证** — 上传接口已重写但未实际测试。需在前端上传一份文件,确认 `POST /api/upload` 返回正确、文件入 MinIO、`leaudit_documents` 有记录。 +1. 合同附件上传与抽取评查链路继续收口 + - 当前用户强调:合同附件上传是正式业务链的一部分 + - 需要继续明确:附件追加 -> 合并上传 -> 抽取 -> 评查 的最终接口与页面反馈 -2. **附件上传** — `appendContractAttachments` 和 `uploadContractTemplate` 仍调用旧 API,后端无对应接口。需新增: - - `POST /api/documents/{id}/attachments`(fileRole=attachment) - - `POST /api/documents/{id}/template`(fileRole=template) +2. 文档类型绑定页与规则组页的语义继续收口 + - 用户已经多次反馈“看不直观” + - 核心不是再堆字段,而是要把“入口模块 / 文档类型 / 一级分组 / 二级分组 / 规则集”的层级展示清楚 -3. **文档列表字段精简** — `documents.ts` 里 `LeauditListItem → DocumentUI` 有 80 行映射代码(`mapLeauditDocToAuditStatus`、`mapProcessingStatusToFileStatus` 等)。建议逐步直接消费新后端字段名。 +3. 上传失败提示继续前端友好化 + - 明确提示“哪个规则集不可用 / 哪个入口模块配置异常 / 当前文档类型没有可用规则数” + - 避免只弹统一 400/500 -### 中优先级 +4. 继续清理文档与代码中的旧术语 + - `可用版本` 统一改为 `可用规则数` + - 避免“规则组 / 分组 / 规则集 / 评查点”混用造成误解 -4. **文档 CRUD 补全** — 后端缺: - - `DELETE /api/documents/{id}` — 软删除 - - `PUT /api/documents/{id}` — 更新元数据(备注、测试标记) - - `GET /api/documents/{id}` — 文档详情 +### P1:建议继续做 -5. **文档类型管理页 CSS** — `document-types._index.tsx` 用了 `.data-table`、`.tag`、`.status-badge` 等类名,需要检查 `document-types_index.css` 是否包含这些样式,或改用现有组件(如 `Table`)。 +1. 评查点分组正式迁移是否执行 + - 现在脚本和方案已具备 + - 但正式执行前要先完成 precheck 并确认现网数据 -6. **前端 role-permissions 页面权限 UI** — 规则集绑定已改为 `ruleSetIds`(替代老系统的 `group_ids`),但 role-permissions 页面显示的权限分组文案可能需要更新。 +2. 文档列表页与上传页剩余 PostgREST 旁路彻底清理 + - 已经不是主依赖,但要继续排查存量调用 -### 低优先级 +3. 规则组页 UI 再向老系统样式收口 + - 用户多次强调不要大改配色 + - 要沿用现有系统保守风格,不做“新设计感”重构 -7. **规则集选择器优化** — 当前新建/编辑文档类型页以 checkbox 列表展示所有规则集。规则集数量增长后需要加搜索/分组。 +### P2:后续再做 -8. **队列状态接口完善** — 当前 `/api/v2/system/queue/status` 只查了数据库的 `processing_status`,未接入 Celery 实时队列深度。后续可以调 Celery inspect API 补充 `pending_tasks` / `processing_tasks`。 - -9. **后端推送** — 后端仓库无 git remote,需要配置后推送。 - -10. **旧 PostgREST 完全清理** — 确认无其他 PostgREST 调用后移除旧 RPC 函数。 +1. 历史文档与临时脚本继续归档清理 +2. 合同管理与 AI 对话模块命名细节再统一 +3. 少量无数据导致的详情 404 页面做更友好空态提示 --- -## 四、当前用户/角色状态 +## 六、当前用户 / 角色 / 权限状态 -| 角色 | role_key | ID | 用户数 | -|------|----------|-----|--------| -| 系统超级管理员 | super_admin | 1 | 0 | -| 省级管理员 | provincial_admin | 2 | 1 (id=5, username=admin, area=梅州) | -| 地区管理员 | admin | 3 | 4 (揭阳惠来烟草, 梅州烟草, 云浮烟草, 潮州烟草) | -| 普通用户 | common | 4 | ~200 (000... 用户) | +当前已知关注角色: +- `super_admin` +- `provincial_admin` +- `admin` +- `common` -省级管理员 (id=5) 拥有所有权限点,包括新增的 `doc_type:*`(通过 `_ensureAdminSeeds` 自动补种)。 +当前已经明确生效的权限方向: +- RBAC 管理接口已补细粒度权限检查 +- 中文化 403 提示已打通 +- 文档相关接口已带地区 / 用户隔离 +- 前端子页权限映射已补一轮,但后续新页面仍要按 `route-alias-guidelines.md` 判断是否该进 alias + +--- + +## 七、接手时不要再踩的坑 + +1. 不要再把“评查点分组页看到的树”理解成纯前端写死结构 + - 现在目标是后端接口给树,前端只负责展示与管理 + +2. 不要遇到 403 就直接继续堆 alias + - 先判断:这是子页派生权限问题,还是后端根本没注册真实菜单路由 + +3. 不要新增 PostgREST 依赖 + - 当前主线是迁出,不是再混回去 + +4. 不要把“文档类型绑定规则集”和“规则组页绑定规则集”理解成互斥逻辑 + - 它们是不同层级的配置入口,最终都要服务同一条上传评查链路 + +5. 不要在文档里继续留下带密码的连接信息 + +--- + +## 八、配套主文档 + +- `docs/README.md`:后端 `/docs` 总导航 +- `docs/接口/README.md`:接口文档导航 +- `docs/权限与地区隔离文档导航.md`:权限主线导航 +- `docs/leaudit/README.md`:后端架构主线 +- `docs/规则编辑/README.md`:规则链路主线 +- `new_doc_review/docs/route-alias-guidelines.md`:前端子页权限映射规范 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..2b0ee66 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,99 @@ +# leaudit-platform 文档地图 + +> 最后整理:2026-05-04 +> 当前主线:以后端 `fastapi_leaudit` 的 `/docs` 为主整理入口,前端文档仅保留必要索引引用。 + +## 先看什么 + +- 给老板汇报当前状态:先看 `docs/HANDOFF.md` +- 接手继续开发:先看 `docs/HANDOFF.md`,再看 `docs/接口/README.md` +- 查具体模块:按下面“功能模块索引”找对应文档 +- 查规则引擎与后端处理链路:看 `docs/leaudit/README.md` 与 `docs/规则编辑/README.md` +- 查前端历史对接资料:看 `new_doc_review/docs/README.md` +- 查旧版 RBAC / API 存量资料:看 `docs/历史API与权限文档索引.md` + +## 当前后端主文档 + +- `docs/HANDOFF.md`:当前状态、已完成事项、下一步、关键代码位置 +- `docs/接口/README.md`:后端接口文档导航 +- `docs/权限与地区隔离文档导航.md`:权限、路由、地区隔离主线 +- `docs/leaudit/README.md`:稳定架构、表结构、处理流水线 +- `docs/规则编辑/README.md`:规则管理、Bridge 接入、落地实施顺序 + +## 功能模块索引 + +| 模块 | 重点文档 | 说明 | +|------|----------|------| +| 全局交接 / 当前状态 | `docs/HANDOFF.md` | 当前主线、已完成事项、剩余阻塞、关键代码位置 | +| 后端接口总览 | `docs/接口/README.md` | 当前有效接口文档的阅读入口 | +| 首页入口模块 / 菜单 | `docs/接口/入口模块绑定最终设计方案.md` | 首页入口展示、绑定、RBAC 最小可用链路 | +| 文档上传 / 文档列表 / 文档详情 | `docs/接口/文档上传与列表接口分析.md` | 上传、列表、详情/更新/删除、评查触发、数据隔离 | +| 文档类型 / 规则绑定 | `docs/接口/评查点分组目标结构与迁移方案.md` | 文档类型、入口模块、规则组、规则集关系 | +| 评查点分组迁移 | `docs/接口/评查点分组目标结构与迁移方案.md`、`docs/接口/评查点分组迁移执行前检查清单.md` | 新老分组模型梳理与迁移脚本执行建议 | +| 权限 / 路由 / 地区隔离 | `docs/权限与地区隔离文档导航.md`、`docs/接口/用户权限与权限点清单.md`、`docs/用户与地区权限完整设计方案.md` | RBAC、路由、用户与地区隔离设计 | +| 规则引擎 / OSS / Bridge | `docs/规则编辑/README.md`、`docs/leaudit/README.md` | 规则 DSL、执行链路、对象存储、Bridge 设计 | +| 历史资料索引 | `docs/历史API与权限文档索引.md` | 老阶段 RBAC / API 快照的使用入口 | +| 前端权限别名 | `new_doc_review/docs/route-alias-guidelines.md` | 什么时候加 alias,什么时候补真实菜单路由 | + +## 文档目录约束 + +### `docs/` + +- 这里是当前项目的主文档目录 +- 只保留当前主线设计、接口、交接、迁移、规则架构文档 +- 新增业务说明优先写到这里,不要再散落到临时目录 + +### `new_doc_review/docs/` + +- 这里保留前端工程实现文档 +- 偏实现、联调、部署、组件能力说明 +- 如果某项内容已经上升为“全项目共识”,应同步沉淀回 `docs/` + +### `new_doc_review/auth_doc/` + +- 这里以历史 API / RBAC / 前端对接资料为主 +- 多数是老阶段快照,不应直接当成当前系统唯一事实来源 +- 使用前先对照 `docs/HANDOFF.md` 与 `docs/接口/README.md` + +## 本次已清理 + +- 删除重复或已被新版本覆盖的 Markdown: + - `docs/routes/http_routes_dedup_2026-04-27.md` + - `docs/superpowers/plans/2026-04-28-fix-double-finalize-and-bindings-api.md` + - `docs/用户权限开发TaskList.md` + - `docs/leaudit/SYSTEM_OVERVIEW.md` + - `docs/接口/新系统版_documents_list接口.md` + - `docs/接口/首页菜单最小可用收口说明.md` + - `docs/接口/系统设置入口恢复说明.md` + - `docs/规则编辑/开发任务拆解清单.md` + - `docs/规则编辑/跑通全流程所需准备项.md` + - `docs/规则编辑/为什么仍然需要Bridge适配层.md` + - `docs/规则编辑/yaml规则在线编辑设计.md` + - `docs/规则编辑/worker并发执行改造方案.md` + - `docs/leaudit/并发与重试参数说明.md` + - `docs/接口/文档上传与评查接口.md` + - `docs/接口/首页入口接口落地说明.md` + - `docs/接口/评查点分组正式迁移执行建议.md` + - `docs/接口/文档类型与评查组关联方案.md` + - `docs/老系统_docauditai_用户权限架构深度分析.md` + - `docs/接口/前端联调404与资源缺口收口清单.md` + - `new_doc_review/auth_doc/entry_modules_api(1).md` + - `new_doc_review/auth_doc/交叉评查接口文档(1).md` + - `new_doc_review/auth_doc/API_RESPONSE_EXAMPLES_V3.2.md` + - `new_doc_review/docs/URGENT-前端必须修改.md` +- 补齐文档导航: + - `docs/README.md` + - `docs/接口/README.md` + - `docs/权限与地区隔离文档导航.md` + - `docs/leaudit/README.md` + - `docs/规则编辑/README.md` + - `docs/历史API与权限文档索引.md` + - `new_doc_review/docs/README.md` +- 统一现状入口:`docs/HANDOFF.md` + +## 后续维护规则 + +- 新功能如果已经进入当前主线,优先补 `docs/HANDOFF.md` 的“已完成 / 待做” +- 接口发生变化时,优先更新 `docs/接口/` 下对应文档 +- 只用于阶段排障的“临时说明”在问题关闭后要么合并进正式文档,要么删除 +- 带版本号或带 `(1)` 的副本文件,原则上不继续新增 diff --git a/docs/leaudit/README.md b/docs/leaudit/README.md index e6aefdb..104afba 100644 --- a/docs/leaudit/README.md +++ b/docs/leaudit/README.md @@ -2,6 +2,15 @@ 本目录包含 `leaudit-platform` 项目的核心架构设计文档。 +## 阅读顺序 + +1. `docs/HANDOFF.md` +2. `document_schema_design.md` +3. `dsl_rule_schema_design.md` +4. `bridge_directory_design.md` +5. `processing_logic.md` +6. `docs/规则编辑/README.md` + ## 文档索引 | 文档 | 内容 | 状态 | @@ -12,6 +21,30 @@ | [bridge_directory_design.md](bridge_directory_design.md) | Bridge 桥接层目录与职责设计 | ✅ 已落地 | | [infrastructure_redesign.md](infrastructure_redesign.md) | 基础设施重设计 — OSS/队列/缓存/区域隔离 | 📋 设计蓝图 | +## 当前运行参数口径 + +- 并发与重试参数不再单独保留为独立说明文档 +- 当前约定统一如下: + - 重试参数使用 `*_RETRY_MAX_ATTEMPTS` + - 退避参数使用 `*_RETRY_BACKOFF_BASE_SECONDS` + - 统一按指数退避计算等待时间 +- 当前主要链路包括: + - LLM + - VLM + - OCR + - signature probe +- 相关代码位置: + - `fastapi_admin/config/_settings.py` + - `fastapi_modules/fastapi_leaudit/leaudit_bridge/client_factory.py` + - `fastapi_modules/fastapi_leaudit/leaudit_bridge/resilient_clients.py` + - `fastapi_modules/fastapi_leaudit/leaudit_bridge/ocr_bridge.py` + +## 当前边界 + +- 当前“系统现状、已完成事项、剩余阻塞”统一看 `docs/HANDOFF.md` +- 本目录只保留相对稳定的后端架构、表结构、处理链路设计 +- 原 `SYSTEM_OVERVIEW.md` 的阶段性现状内容已收口到 `docs/HANDOFF.md` 与本文件 + ## 快览 ``` diff --git a/docs/leaudit/SYSTEM_OVERVIEW.md b/docs/leaudit/SYSTEM_OVERVIEW.md deleted file mode 100644 index 0cbc043..0000000 --- a/docs/leaudit/SYSTEM_OVERVIEW.md +++ /dev/null @@ -1,497 +0,0 @@ -# LeAudit Platform — 系统现状总览 - -> 最后更新:2026-04-28 - -## 一、目标架构 - -补充文档: - -- 并发与重试参数:`docs/leaudit/并发与重试参数说明.md` - -``` -┌─ API ───────────────────────────────────────────────────────────┐ -│ AuditController (/audit) RuleController (/rule-sets) │ -│ DocumentController (/upload) │ -│ POST /run 触发评查 GET / 规则集列表 │ -│ POST /upload 上传建档/可选自动评查 │ -│ GET /run/:id 查询状态 GET /{type}/versions 版本列表 │ -│ GET /result/:id 查询结果 GET /versions/:id/content 正文│ -│ POST /{type}/validate 校验 │ -│ POST /{type}/versions 创建 │ -│ POST /{type}/publish 发布 │ -│ POST /{type}/rollback 回滚 │ -│ GET /bindings 绑定列表 │ -│ POST /{type}/bindings 创建绑定│ -│ PUT /bindings/{id} 更新绑定│ -│ DELETE /bindings/{id} 删除绑定│ -├─ Service ───────────────────────────────────────────────────────┤ -│ AuditServiceImpl RuleServiceImpl + OssServiceImpl│ -├─ Bridge ────────────────────────────────────────────────────────┤ -│ FileSourceResolver RuleVersionResolver RulesLoader │ -│ AuditCtxBuilder AuditServiceFactory NativeRunner │ -│ StorageAdapter ResultAdapter RuleValidator │ -│ tasks.py (dispatch_leaudit_task) │ -├─ leaudit (不改) ────────────────────────────────────────────────│ -│ AuditCtx → AuditService.audit() → OCR/Normalize/Extract/ │ -│ Evaluate/Rescue → 填充 ctx.normalized_doc/extraction/ │ -│ evaluation/fallback_tasks/timing/extraction_errors │ -├─ 存储 ──────────────────────────────────────────────────────────┤ -│ OSS (MinIO) PostgreSQL │ -│ bdocs/{region}/{type}/{doc_id}/ leaudit_documents │ -│ rules/{rule_type}/{version_no}/ leaudit_document_files │ -│ artifacts/{region}/{run_id}/ leaudit_audit_runs │ -│ leaudit_rule_sets │ -│ leaudit_rule_versions │ -│ leaudit_rule_type_bindings │ -│ leaudit_rule_results │ -│ leaudit_field_results │ -│ leaudit_run_metrics │ -│ leaudit_run_errors │ -│ leaudit_rescue_outcomes │ -│ leaudit_artifacts │ -└─────────────────────────────────────────────────────────────────┘ -``` - -## 二、两条核心数据流 - -### 流 A:规则生命周期 - -``` -编辑 YAML - → POST /rule-sets/{type}/validate → RuleValidator (YAML语法 + DSL语义) - → POST /rule-sets/{type}/versions → 上传 OSS + INSERT leaudit_rule_versions - → POST /rule-sets/{type}/publish → UPDATE leaudit_rule_sets.current_version_id - → POST /rule-sets/{type}/bindings → INSERT leaudit_rule_type_bindings - → 新 run 自动绑定新版本到对应文档类型 -``` - -### 流 B:评查执行 - -``` -POST /audit/run { documentId } - → AuditServiceImpl.Run() - 1. 查 leaudit_documents + leaudit_document_files - 2. 查 leaudit_rule_type_bindings → rule_set_id + rule_version_id - 3. INSERT leaudit_audit_runs (rule_version_id, rule_source_oss_url, sha256) - 4. FileSourceResolver → 下载文档 bytes - 5. dispatch_leaudit_task() - ┌─ RuleVersionResolver → OSS 下载规则 YAML → SHA256 校验 - ├─ RulesLoader → RulesFile - ├─ NativeRunner.run() → AuditCtx → AuditService.audit() - └─ NativeRunner.persist_result() - ├─ save_ocr_result() → leaudit_artifacts - ├─ save_extraction_result() → leaudit_field_results - ├─ save_evaluation_results() → leaudit_rule_results + 分数 - ├─ save_run_errors() → leaudit_run_errors - ├─ save_rescue_outcomes() → leaudit_rescue_outcomes - ├─ save_run_metrics() → leaudit_run_metrics - └─ finalize_run() → 终态 (result_status/finished_at) -``` - -## 三、模块完成度 - -### M1:OSS 基础设施 — 100% - -| 文件 | 状态 | 说明 | -|---|---|---| -| `fastapi_common/fastapi_common_storage/oss_client.py` | 已完成 | 上传/下载/Presign/ObjectExists | -| `fastapi_common/fastapi_common_storage/oss_path_utils.py` | 已完成 | BuildBusinessDocKey/ArtifactKey/RuleYamlKey | -| `fastapi_admin/config/_settings.py` | 已完成 | OSS_ENDPOINT/ACCESS_KEY/SECRET_KEY/BUCKET/REGION/USE_SSL/PRESIGN_EXPIRE | -| `fastapi_admin/config/__init__.pyi` | 已完成 | 类型声明 | - -### M2:规则管理后端 — 100% - -| 文件 | 状态 | 说明 | -|---|---|---| -| `controllers/ruleController.py` | 已完成 | 11 个端点(含绑定管理) | -| `services/ruleService.py` | 已完成 | IRuleService 接口 | -| `services/impl/ruleServiceImpl.py` | 已完成 | 规则 CRUD + 绑定 CRUD | -| `leaudit_bridge/ruleValidator.py` | 已完成 | YAML 语法 + DSL 语义校验 | -| `domian/Dto/ruleVersionCreateDto.py` | 已完成 | 创建版本 DTO | -| `domian/Dto/ruleValidateDto.py` | 已完成 | 校验 DTO | -| `domian/Dto/rulePublishDto.py` | 已完成 | 发布/回滚 DTO | -| `domian/Dto/ruleBindingDto.py` | 已完成 | 绑定 DTO | -| `domian/vo/ruleVo.py` | 已完成 | RuleSetVO/VersionVO/ContentVO/ValidationVO/BindingVO | - -### M3:执行链与持久化 — ~95% - -| 文件 | 状态 | 说明 | -|---|---|---| -| `leaudit_bridge/auditCtxBuilder.py` | 已完成 | 构建原生 AuditCtx | -| `leaudit_bridge/auditServiceFactory.py` | 已完成 | 创建 AuditService | -| `leaudit_bridge/nativeRunner.py` | 已完成 | 原生执行入口 + persist_result | -| `leaudit_bridge/fileSourceResolver.py` | 已完成 | 文档来源解析(接 OSS) | -| `leaudit_bridge/ruleVersionResolver.py` | 已完成 | 规则版本解析(含 SHA256) | -| `leaudit_bridge/storage_adapter.py` | 已完成 | 全部持久化方法(已修双重 finalize) | -| `leaudit_bridge/tasks.py` | 已完成 | 任务入口 + 失败处理 | -| `leaudit_bridge/pipeline.py` | 待退役 | 旧管线(已被 nativeRunner 替代) | -| `leaudit_bridge/client_factory.py` | 90% | OCR/LLM/VLM 客户端工厂 | - -### M4:全流程联调 — ~25% - -| 项目 | 状态 | -|---|---| -| 上传入口梳理 | 已完成 | -| 上传后自动触发评查 | 已完成最小链路(`POST /upload` + `autoRun`) | -| 结果查询展示 | 已补运行/规则/字段/errors/rescue/metrics/artifacts 查询 | -| 联调样例准备 | 待做 | -| E2E 验证 | 待做 | - -## 四、持久化执行顺序与终态 - -`persist_result()` 严格按以下顺序执行,`finalize_run` 在最后是唯一的终态写入者: - -``` -save_ocr_result() -save_extraction_result() -save_evaluation_results() ← 只写分数/计数,不写 finished_at -save_run_errors() -save_rescue_outcomes() -save_run_metrics() -finalize_run() ← 唯一写 result_status / finished_at / rescue_applied / phase -``` - -**终态来源(直接从 AuditCtx 读取,不另存平行状态):** - -| 平台表 | 来源 | -|---|---| -| `leaudit_run_metrics` | `ctx.timing` | -| `leaudit_run_errors` | `ctx.extraction_errors` + `ctx.extraction.all_errors` | -| `leaudit_rescue_outcomes` | `ctx.fallback_tasks` | -| `leaudit_audit_runs.result_status` | 综合 `ctx.fallback_tasks` + `ctx.evaluation` 推导 | -| `leaudit_audit_runs.finished_at` | `finalize_run` 写入 now() | -| `leaudit_audit_runs.rescue_applied` | `bool(ctx.fallback_tasks)` | - -## 四点五、最新补充:上传建档入口 - -当前仓库已补最小上传入口: - -```text -POST /upload (multipart/form-data) - file - typeId / typeCode - region=default - fileRole=primary - createdBy? - autoRun=false - speed=normal|urgent -``` - -执行链: - -```text -Upload - -> DocumentServiceImpl.Upload() - -> create leaudit_documents - -> 旧 active 文件失效 - -> 上传原始文件到 OSS: - bdocs/{region}/{type_code}/{document_id}/v{n}/{file_role}.{ext} - -> INSERT leaudit_document_files - -> autoRun=true 时直接调用 AuditServiceImpl.Run() -``` - -说明: - -- `leaudit_documents` 现阶段是平台内部文档主表,不再依赖旧系统 `documents.id` -- 每次前端上传都会新建一条 `leaudit_documents` -- `speed` - - `normal` -> `leaudit.normal` - - `urgent` -> `leaudit.urgent` - -## 四点六、最新补充:结果查询视图 - -当前 `GetRunStatus()` / `GetResult()` 已不再只返回 run 主表摘要。 - -- `GetRunStatus()` 当前已返回: - - `documentFileId` - - `resultStatus` - - `ruleSetId` / `ruleVersionId` / `ruleTypeId` - - `rescueApplied` - - `skippedCount` -- `GetResult()` 当前已可聚合查询: - - `leaudit_rule_results` - - `leaudit_field_results` - - `leaudit_run_errors` - - `leaudit_rescue_outcomes` - - `leaudit_run_metrics` - - `leaudit_artifacts` - -建议联调方式: - -- 上传后从 `run.runId` 轮询 `GET /audit/run/{runId}` -- 完成后调用 `GET /audit/result/{runId}` -- worker 日志里会明确打印: - - 已投递到哪个队列 - - worker 实际消费的是 `urgent` 还是 `normal` - -## 四点七、关键关系图:DocumentType -> Binding -> RuleSet -> RuleVersion - -这一段是当前规则执行链里最关键的一层路由关系。 - -很多时候容易把这四张表混在一起,但它们职责其实完全不同: - -- `leaudit_document_types`:定义“系统里有哪些文档类型” -- `leaudit_rule_type_bindings`:定义“某个文档类型该用哪套规则集” -- `leaudit_rule_sets`:定义“某类规则集合本身” -- `leaudit_rule_versions`:定义“某套规则的具体版本快照” - -### 关系图 - -```text -┌────────────────────────────┐ -│ leaudit_document_types │ -│----------------------------│ -│ id │ -│ code │ <- 文档类型编码,如 contract.entrust -│ name │ -│ extraction_mode │ -│ is_enabled │ -└──────────────┬─────────────┘ - │ 1:N - │ - ▼ -┌────────────────────────────┐ -│ leaudit_rule_type_bindings │ -│----------------------------│ -│ id │ -│ doc_type_id │ -> leaudit_document_types.id -│ doc_type_code │ -> 冗余编码,便于快速排查 -│ rule_set_id │ -> leaudit_rule_sets.id -│ binding_mode │ -> explicit / wildcard / fallback -│ priority │ -> 多条命中时按优先级选 -│ is_active │ -│ region │ -> default / mz / yf / ... -└──────────────┬─────────────┘ - │ N:1 - │ - ▼ -┌────────────────────────────┐ -│ leaudit_rule_sets │ -│----------------------------│ -│ id │ -│ rule_type │ <- 与 DSL metadata.type_id 对齐 -│ rule_name │ -│ current_version_id │ -> 当前生效版本 -│ status │ -└──────────────┬─────────────┘ - │ 1:N - │ - ▼ -┌────────────────────────────┐ -│ leaudit_rule_versions │ -│----------------------------│ -│ id │ -│ rule_set_id │ -> leaudit_rule_sets.id -│ version_no │ -│ status │ -│ oss_url │ -> rules/.../rules.yaml -│ file_sha256 │ -│ metadata_type_id │ -│ published_at │ -└────────────────────────────┘ -``` - -### 执行时怎么走 - -真正运行评查时,不是直接“拿一个 rule_type 去找 YAML”,而是按下面这条链走: - -```text -leaudit_documents.type_id - -> leaudit_document_types.id - -> leaudit_rule_type_bindings.doc_type_id - -> leaudit_rule_type_bindings.rule_set_id - -> leaudit_rule_sets.current_version_id - -> leaudit_rule_versions.id - -> leaudit_rule_versions.oss_url - -> 下载对应 rules.yaml - -> NativeRunner / AuditService.audit(ctx) -``` - -### 每张表到底解决什么问题 - -#### 1. `leaudit_document_types` - -它解决的是: - -```text -“这份文档属于什么类型?” -``` - -例如: - -```text -id = 2 -code = contract.entrust -name = 通用委托合同 -``` - -那一条 `leaudit_documents` 记录里如果: - -```text -type_id = 2 -``` - -就表示这份文档被平台认定为“通用委托合同”。 - -它本质上是平台的“文档分类字典表”。 - -#### 2. `leaudit_rule_type_bindings` - -它解决的是: - -```text -“这种文档类型,应该使用哪套规则集?” -``` - -例如: - -```text -doc_type_id = 2 -rule_set_id = 22 -region = default -binding_mode = explicit -priority = 100 -``` - -意思就是: - -- 文档类型 `2`(通用委托合同) -- 在 `default` 区域 -- 显式绑定到规则集 `22` - -这张表本质上是“规则路由表”。 - -它不能省略,因为后续扩展会依赖它支持: - -- 同一种文档类型在不同地区走不同规则 -- 同一种文档类型绑定多套规则并通过优先级决策 -- fallback / wildcard 兜底规则 - -#### 3. `leaudit_rule_sets` - -它解决的是: - -```text -“系统里存在什么规则集合?” -``` - -例如: - -```text -id = 22 -rule_type = contract.entrust -rule_name = 通用委托合同 -current_version_id = 2 -``` - -这表示: - -- 系统里有一套“通用委托合同”规则集 -- 当前生效版本是 `2` - -注意: - -- `rule_set` 表示“规则集合”的稳定身份 -- 它不是某个具体 YAML 版本文件 - -#### 4. `leaudit_rule_versions` - -它解决的是: - -```text -“这套规则当前/历史具体长什么样?” -``` - -例如: - -```text -id = 2 -rule_set_id = 22 -version_no = 2.0 -oss_url = rules/contract.entrust/2.0/rules.yaml -status = published -``` - -这表示: - -- 规则集 `22` -- 的 `2.0` 版本 -- 对应 OSS 上某个 `rules.yaml` - -这层是规则版本治理的核心,支持: - -- 发布 -- 回滚 -- 历史追溯 -- 运行时精确复盘 - -### 为什么一定要拆成四层 - -如果不拆,后面会遇到很多问题: - -- 文档类型和规则类型耦合死,无法独立演进 -- 没法优雅支持一类文档多套规则 -- 没法支持地区差异 -- 没法做发布 / 回滚 / 历史 run 溯源 - -拆成四层以后,结构会更稳定: - -- `document_type` 负责“分类” -- `binding` 负责“路由” -- `rule_set` 负责“规则身份” -- `rule_version` 负责“规则版本快照” - -### 当前初始化策略 - -基于当前数据库里已有的 `leaudit_rule_sets`,当前最合理的初始化策略是: - -```text -rule_set.rule_type -> document_type.code -rule_set.rule_name -> document_type.name -document_type.id -> binding.doc_type_id -rule_set.id -> binding.rule_set_id -rule_set.current_version_id -> 当前执行版本 -``` - -也就是说,先用现有 `rule_set` 反向补齐: - -- `leaudit_document_types` -- `leaudit_rule_type_bindings` - -这是当前最稳、也最方便后续扩展的做法。 - -## 五、数据库表关系 - -``` -leaudit_document_types - └── leaudit_rule_type_bindings (doc_type_id → rule_set_id) - └── leaudit_rule_sets (current_version_id) - └── leaudit_rule_versions (oss_url, sha256) - └── leaudit_audit_runs (rule_version_id, rule_source_oss_url) - ├── leaudit_rule_results - ├── leaudit_field_results - ├── leaudit_run_metrics - ├── leaudit_run_errors - ├── leaudit_rescue_outcomes - └── leaudit_artifacts -``` - -## 六、关键设计原则 - -1. **leaudit 核心不改** — 所有定制在 bridge 层,leaudit 保持纯 Python 包 -2. **只读 AuditCtx** — 执行完只从 ctx 上读取,不自己模拟 stage -3. **规则版本溯源** — 每个 run 绑定具体 `rule_version_id`,老 run 不受新发布影响 -4. **OSS 真源 + DB 索引** — YAML 正文存 OSS,元数据索引存 PostgreSQL -5. **终态单点写入** — `finalize_run` 是 run 主表终态的唯一写入者 -6. **独立 session 提交** — 每个 `save_*` 独立会话,按"先子表后主表"顺序保证最终一致性 - -## 七、已知待补项 - -| 缺口 | 优先级 | -|---|---| -| `run_metrics.llm_call_count` / `vlm_call_count` 仍为空 | P1(可从 RescueTask 汇总) | -| `ctx.timing` 缺 normalize/rescue 独立 key | P2(等 leaudit 原生补) | -| 文档状态更新逻辑分散在两处 | P1(应收敛到 StorageAdapter) | -| Celery 异步化 | P2(当前同步可跑通,生产需异步) | -| `_TYPE_ID_RULES_MAP` 硬编码兜底 | P2(等 bindings 全覆盖后移除) | -| M4 E2E 联调 | P0(下个里程碑) | diff --git a/docs/leaudit/并发与重试参数说明.md b/docs/leaudit/并发与重试参数说明.md deleted file mode 100644 index d2bc62e..0000000 --- a/docs/leaudit/并发与重试参数说明.md +++ /dev/null @@ -1,149 +0,0 @@ -# LeAudit 并发与重试参数说明 - -这份文档记录当前 `leaudit-platform` 已经落地的并发与重试参数,避免后面再忘。 - ---- - -## 1. 为什么没有用 `RETRY_BACKOFF_SECONDS = [1, 2, 4]` - -这次我没有把退避时间设计成数组,而是改成了: - -- `*_RETRY_MAX_ATTEMPTS` -- `*_RETRY_BACKOFF_BASE_SECONDS` - -原因很简单: - -- 配置更短,更容易看懂 -- 不用每次都手写 `[1, 2, 4]` -- 代码统一按“指数退避”算等待时间 - -计算方式: - -```text -第 1 次重试等待 = base -第 2 次重试等待 = base * 2 -第 3 次重试等待 = base * 4 -... -``` - -例如: - -```toml -LLM_RETRY_MAX_ATTEMPTS = 3 -LLM_RETRY_BACKOFF_BASE_SECONDS = 1 -``` - -表示: - -- 第 1 次请求失败后,等 `1s` -- 第 2 次再失败后,等 `2s` -- 总共最多尝试 `3` 次 - ---- - -## 2. 当前已正式化的参数 - -当前 `app.toml` 中已启用: - -```toml -[LEAUDIT] -LLM_MAX_CONCURRENCY = 5 -VLM_MAX_CONCURRENCY = 3 - -LLM_REQUEST_TIMEOUT = 120 -LLM_RETRY_MAX_ATTEMPTS = 3 -LLM_RETRY_BACKOFF_BASE_SECONDS = 1 - -VLM_REQUEST_TIMEOUT = 45 -VLM_RETRY_MAX_ATTEMPTS = 2 -VLM_RETRY_BACKOFF_BASE_SECONDS = 1 - -OCR_VLM_CONCURRENCY = 32 -OCR_RETRY_MAX_ATTEMPTS = 3 -OCR_RETRY_BACKOFF_BASE_SECONDS = 1 - -SIGNATURE_PROBE_CONCURRENCY = 2 -SIGNATURE_PROBE_TIMEOUT = 20 -SIGNATURE_PROBE_RETRY_MAX_ATTEMPTS = 2 -SIGNATURE_PROBE_RETRY_BACKOFF_BASE_SECONDS = 0.5 -``` - ---- - -## 3. 各链路当前策略 - -### 3.1 LLM - -- 客户端:`ResilientOpenAICompatibleClient` -- 超时:`LLM_REQUEST_TIMEOUT` -- 最大尝试次数:`LLM_RETRY_MAX_ATTEMPTS` -- 退避基数:`LLM_RETRY_BACKOFF_BASE_SECONDS` -- 重试条件: - - 超时 - - 连接错误 - - `5xx` - - `408` - - `429` - -### 3.2 VLM - -- 客户端:`ResilientQwenVLMClient` -- 超时:`VLM_REQUEST_TIMEOUT` -- 最大尝试次数:`VLM_RETRY_MAX_ATTEMPTS` -- 退避基数:`VLM_RETRY_BACKOFF_BASE_SECONDS` -- 重试条件: - - 超时 - - 连接错误 - - `5xx` - - `408` - - `429` - -### 3.3 OCR - -- 客户端:`ResilientChandraOCRClient` -- OCR HTTP 超时:沿用 `[OCR].TIMEOUT` -- 最大尝试次数:`OCR_RETRY_MAX_ATTEMPTS` -- 退避基数:`OCR_RETRY_BACKOFF_BASE_SECONDS` -- 重试条件: - - 超时 - - 连接错误 - - `5xx` - - `408` - - `429` - -### 3.4 Signature Probe - -这是 DOCX 签名候选补识别,不是主 OCR。 - -- 并发:`SIGNATURE_PROBE_CONCURRENCY` -- 单次超时:`SIGNATURE_PROBE_TIMEOUT` -- 最大尝试次数:`SIGNATURE_PROBE_RETRY_MAX_ATTEMPTS` -- 退避基数:`SIGNATURE_PROBE_RETRY_BACKOFF_BASE_SECONDS` -- 第 1 次失败后,会用 fresh VLM client 再试 - ---- - -## 4. 为什么把 signature probe 调小 - -之前这里容易长时间卡住,根因是: - -- 它是补探测 -- 但单次超时太长 -- 且失败后还会再试一次 - -所以现在把它收成: - -- `SIGNATURE_PROBE_CONCURRENCY = 2` -- `SIGNATURE_PROBE_TIMEOUT = 20` - -这样它仍然能补识别,但不会把整条 OCR 长尾拖得太夸张。 - ---- - -## 5. 当前代码位置 - -- 配置定义:`fastapi_admin/config/_settings.py` -- 客户端工厂:`fastapi_modules/fastapi_leaudit/leaudit_bridge/client_factory.py` -- 重试封装:`fastapi_modules/fastapi_leaudit/leaudit_bridge/resilient_clients.py` -- signature probe:`fastapi_modules/fastapi_leaudit/leaudit_bridge/ocr_bridge.py` - diff --git a/docs/routes/http_routes_dedup_2026-04-27.md b/docs/routes/http_routes_dedup_2026-04-27.md deleted file mode 100644 index db0a9c0..0000000 --- a/docs/routes/http_routes_dedup_2026-04-27.md +++ /dev/null @@ -1,344 +0,0 @@ -# 去重后的完整接口清单 - -- 统计时间:2026-04-27 -- 当前分支:`Wren-Development-V5` -- 去重口径:按“HTTP 方法 + 归一化路径 + endpoint”去重 -- 去重结果:`201` 个 HTTP 接口 -- 原始运行时路由对象:`347` 个 -- 归一化合并前缀:`/admin`、`/api/v2`、`/api/v3`、`/v3` - -## [ai-suggestions] 4 - -```text -POST /ai-suggestions/batch-generate -POST /ai-suggestions/generate -GET /ai-suggestions/task/{task_id} -GET /ai-suggestions/{evaluation_result_id} -``` - -## [api] 4 - -```text -DELETE,GET,PATCH,POST,PUT /api/dataset/{path:path} -POST /api/postgrest/proxy -DELETE,GET,PATCH,POST,PUT /api/postgrest/proxy/{table_path:path} -DELETE,GET,PATCH,POST,PUT /api/{path:path} -``` - -## [areas] 2 - -```text -GET /areas -POST /areas/reload -``` - -## [auth] 6 - -```text -GET /auth/admin-only -GET /auth/check-permission -POST /auth/login -GET /auth/me -POST /auth/password_login -DELETE,GET,PATCH,POST /auth/sso_users -``` - -## [awareness-configs] 5 - -```text -GET /awareness-configs -POST /awareness-configs -DELETE /awareness-configs/{doc_type_code} -GET /awareness-configs/{doc_type_code} -PUT /awareness-configs/{doc_type_code} -``` - -## [awareness-templates] 8 - -```text -GET /awareness-templates -POST /awareness-templates -GET /awareness-templates/code/{template_code} -GET /awareness-templates/types -DELETE /awareness-templates/{template_id} -GET /awareness-templates/{template_id} -PUT /awareness-templates/{template_id} -POST /awareness-templates/{template_id}/duplicate -``` - -## [cross_review] 14 - -```text -POST /cross_review/proposals -POST /cross_review/proposals/details -POST /cross_review/proposals/document -POST /cross_review/proposals/document/check_pending_votes -DELETE /cross_review/proposals/{proposal_id} -POST /cross_review/proposals/{proposal_id}/votes -POST /cross_review/tasks/user_tasks -GET /cross_review/tasks/{task_id}/can-confirm -GET /cross_review/tasks/{task_id}/documents -POST /cross_review/tasks/{task_id}/documents -POST /cross_review/tasks/{task_id}/documents/{document_id}/append_attachments -POST /cross_review/tasks/{task_id}/documents/{document_id}/complete -GET /cross_review/tasks/{task_id}/progress -POST /cross_review/tasks/{task_id}/upload_documents -``` - -## [debug] 2 - -```text -GET /debug/dify-config -GET /debug/test-dify -``` - -## [dify] 10 - -```text -GET /dify/area-datasets -POST /dify/area-datasets -GET /dify/area-datasets/areas -GET /dify/area-datasets/check/{dataset_id} -GET /dify/area-datasets/my -DELETE /dify/area-datasets/{dataset_bind_id} -GET /dify/area-datasets/{dataset_bind_id} -PUT /dify/area-datasets/{dataset_bind_id} -GET /dify/chat-apps -GET /dify/chat-apps/default -``` - -## [dify_chat] 1 - -```text -DELETE,GET,PATCH,POST,PUT /dify_chat/{path:path} -``` - -## [dify_dataset] 1 - -```text -DELETE,GET,PATCH,POST,PUT /dify_dataset/{path:path} -``` - -## [document-types] 7 - -```text -GET /document-types -POST /document-types -GET /document-types/options/entry-modules -GET /document-types/options/prompt-templates -DELETE /document-types/{type_id} -GET /document-types/{type_id} -PUT /document-types/{type_id} -``` - -## [documents] 7 - -```text -GET /documents/check-duplicate -POST /documents/contract_templates/{comparison_id}/append_attachments -POST /documents/contracts/{document_id}/append_attachments -POST /documents/cross_review/documents/upload_and_assign -GET /documents/list -POST /documents/upload -POST /documents/upload_contract_template -``` - -## [entry-modules] 6 - -```text -GET /entry-modules -POST /entry-modules -DELETE /entry-modules/{module_id} -GET /entry-modules/{module_id} -PUT /entry-modules/{module_id} -POST /entry-modules/{module_id}/image -``` - -## [leaudit-review-points] 1 - -```text -GET /leaudit-review-points/{document_id} -``` - -## [prompt-templates] 8 - -```text -GET /prompt-templates -POST /prompt-templates -GET /prompt-templates/code/{template_code} -GET /prompt-templates/types -DELETE /prompt-templates/{template_id} -GET /prompt-templates/{template_id} -PUT /prompt-templates/{template_id} -POST /prompt-templates/{template_id}/duplicate -``` - -## [qichacha] 5 - -```text -POST /qichacha/batch -POST /qichacha/company -POST /qichacha/dishonesty -POST /qichacha/enterprise -GET /qichacha/status -``` - -## [rbac] 25 - -```text -GET /rbac/check-route -POST /rbac/clear-routes-cache -GET /rbac/permissions -POST /rbac/permissions -DELETE /rbac/permissions/{permission_id} -GET /rbac/permissions/{permission_id} -PUT /rbac/permissions/{permission_id} -DELETE /rbac/role-permissions -GET /rbac/role-permissions -POST /rbac/role-permissions -PUT /rbac/role-permissions -GET /rbac/roles -POST /rbac/roles -DELETE /rbac/roles/{role_id} -GET /rbac/roles/{role_id} -PUT /rbac/roles/{role_id} -GET /rbac/roles/{role_id}/all-routes -GET /rbac/roles/{role_id}/routes -PUT /rbac/roles/{role_id}/routes -GET /rbac/roles/{role_id}/users -GET /rbac/user/routes -GET /rbac/users -GET /rbac/users/{user_id}/roles -POST /rbac/users/{user_id}/roles -DELETE /rbac/users/{user_id}/roles/{role_id} -``` - -## [routes] 3 - -```text -GET /routes -GET /routes/{route_id} -GET /routes/{route_id}/permissions -``` - -## [rpc] 1 - -```text -DELETE,GET,PATCH,POST,PUT /rpc/{rpc_function:path} -``` - -## [statistics] 3 - -```text -GET /statistics/home-data -GET /statistics/top-error-points -GET /statistics/top-risk-users -``` - -## [storage] 11 - -```text -DELETE /storage/buckets -GET /storage/buckets -POST /storage/buckets -DELETE /storage/files -GET /storage/files -POST /storage/files/batch-delete -POST /storage/files/copy -GET /storage/files/download -GET /storage/files/metadata -POST /storage/files/move -GET /storage/files/presigned-url -``` - -## [system] 5 - -```text -GET /system/ai-cloud/status -POST /system/ai-cloud/switch -GET /system/queue/details -GET /system/queue/position/{document_id} -GET /system/queue/status -``` - -## [user] 2 - -```text -GET /user/routes -GET /user/routes/flat -``` - -## [users] 5 - -```text -GET /users -GET /users/organizations -GET /users/organizations/flat -GET /users/organizations/tree -GET /users/organizations/{ou_id}/users -``` - -## [v2 admin alias] 51 - -```text -POST /v2/ai-suggestions/batch-generate -POST /v2/ai-suggestions/generate -GET /v2/ai-suggestions/task/{task_id} -GET /v2/ai-suggestions/{evaluation_result_id} -POST /v2/cross_review/proposals -POST /v2/cross_review/proposals/details -POST /v2/cross_review/proposals/document -POST /v2/cross_review/proposals/document/check_pending_votes -DELETE /v2/cross_review/proposals/{proposal_id} -POST /v2/cross_review/proposals/{proposal_id}/votes -POST /v2/cross_review/tasks/user_tasks -GET /v2/cross_review/tasks/{task_id}/can-confirm -GET /v2/cross_review/tasks/{task_id}/documents -POST /v2/cross_review/tasks/{task_id}/documents -POST /v2/cross_review/tasks/{task_id}/documents/{document_id}/append_attachments -POST /v2/cross_review/tasks/{task_id}/documents/{document_id}/complete -GET /v2/cross_review/tasks/{task_id}/progress -POST /v2/cross_review/tasks/{task_id}/upload_documents -GET /v2/documents/check-duplicate -POST /v2/documents/contract_templates/{comparison_id}/append_attachments -POST /v2/documents/contracts/{document_id}/append_attachments -POST /v2/documents/cross_review/documents/upload_and_assign -GET /v2/documents/list -POST /v2/documents/upload -POST /v2/documents/upload_contract_template -POST /v2/qichacha/batch -POST /v2/qichacha/company -POST /v2/qichacha/dishonesty -POST /v2/qichacha/enterprise -GET /v2/qichacha/status -DELETE /v2/storage/buckets -GET /v2/storage/buckets -POST /v2/storage/buckets -DELETE /v2/storage/files -GET /v2/storage/files -POST /v2/storage/files/batch-delete -POST /v2/storage/files/copy -GET /v2/storage/files/download -GET /v2/storage/files/metadata -POST /v2/storage/files/move -GET /v2/storage/files/presigned-url -GET /v2/system/ai-cloud/status -POST /v2/system/ai-cloud/switch -GET /v2/system/queue/details -GET /v2/system/queue/position/{document_id} -GET /v2/system/queue/status -GET /v2/users -GET /v2/users/organizations -GET /v2/users/organizations/flat -GET /v2/users/organizations/tree -GET /v2/users/organizations/{ou_id}/users -``` - -## [versions] 4 - -```text -POST /versions/compare -GET /versions/documents-list -GET /versions/statistics -GET /versions/{entity_id} -``` diff --git a/docs/superpowers/plans/2026-04-28-fix-double-finalize-and-bindings-api.md b/docs/superpowers/plans/2026-04-28-fix-double-finalize-and-bindings-api.md deleted file mode 100644 index a67cc79..0000000 --- a/docs/superpowers/plans/2026-04-28-fix-double-finalize-and-bindings-api.md +++ /dev/null @@ -1,683 +0,0 @@ -# Fix Double Finalize + Rule Type Bindings API Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Fix two blocking issues: (1) eliminate the duplicate `result_status` / `finished_at` write in `save_evaluation_results`, and (2) add full CRUD API for the `leaudit_rule_type_bindings` table. - -**Architecture:** Fix 1 is a one-line removal in `storage_adapter.py` — strip the premature run summary UPDATE from `save_evaluation_results` so `finalize_run` is the single source of truth for terminal state. Fix 2 follows the existing RuleController → IRuleService → RuleServiceImpl layered pattern, adding DTO/VO types and 4 endpoints for binding management. - -**Tech Stack:** Python, FastAPI, SQLAlchemy async, PostgreSQL - ---- - -## File Map - -| File | Action | Responsibility | -|---|---|---| -| `fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py` | Modify | Remove premature UPDATE from `save_evaluation_results` | -| `fastapi_modules/fastapi_leaudit/domian/Dto/ruleBindingDto.py` | Create | `RuleBindingCreateDTO`, `RuleBindingUpdateDTO` | -| `fastapi_modules/fastapi_leaudit/domian/vo/ruleVo.py` | Modify | Add `RuleBindingVO` | -| `fastapi_modules/fastapi_leaudit/services/ruleService.py` | Modify | Add 4 abstract methods | -| `fastapi_modules/fastapi_leaudit/services/impl/ruleServiceImpl.py` | Modify | Add 4 method implementations | -| `fastapi_modules/fastapi_leaudit/controllers/ruleController.py` | Modify | Add 4 endpoints | - ---- - -### Task 1: Fix Double Finalize — Strip Premature UPDATE from save_evaluation_results - -**Files:** -- Modify: `fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py:167-181` - -- [ ] **Step 1: Remove result_status and finished_at from the UPDATE clause** - -Replace lines 167-181 of `storage_adapter.py`: - -```python - # Update audit_runs summary - await session.execute( - text("""UPDATE leaudit_audit_runs SET - total_score = :ts, passed_count = :pc, failed_count = :fc, - skipped_count = :sc, result_status = :rs, finished_at = now(), update_time = now() - WHERE id = :rid"""), - { - "ts": evaluation.total_score, - "pc": evaluation.passed_count, - "fc": evaluation.failed_count, - "sc": evaluation.skipped_count, - "rs": "pass" if evaluation.failed_count == 0 else "fail", - "rid": resolved_run_id, - }, - ) -``` - -With: - -```python - # Update audit_runs summary (scores only — terminal state set by finalize_run) - await session.execute( - text("""UPDATE leaudit_audit_runs SET - total_score = :ts, passed_count = :pc, failed_count = :fc, - skipped_count = :sc, update_time = now() - WHERE id = :rid"""), - { - "ts": evaluation.total_score, - "pc": evaluation.passed_count, - "fc": evaluation.failed_count, - "sc": evaluation.skipped_count, - "rid": resolved_run_id, - }, - ) -``` - -- [ ] **Step 2: Verify finalize_run is still the last writer in persist_result** - -Read `nativeRunner.py:149-157` to confirm `finalize_run` runs after all other persist steps, including `save_evaluation_results`. The order is: - -``` -save_ocr_result → save_extraction_result → save_evaluation_results → save_run_errors → save_rescue_outcomes → save_run_metrics → finalize_run -``` - -Confirmed: `finalize_run` is the LAST call in `persist_result()`, so it will always set the definitive terminal state. - -- [ ] **Step 3: Syntax check** - -```bash -cd /home/wren-dev/Porject/leaudit-platform && python -m compileall fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py -``` -Expected: Compile successful, no errors. - -- [ ] **Step 4: Commit** - -```bash -cd /home/wren-dev/Porject/leaudit-platform -git add fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py -git commit -m "fix: remove premature result_status/finished_at from save_evaluation_results - -finalize_run() is the single source of truth for terminal run state. -Previously save_evaluation_results wrote a binary pass/fail status and -finished_at BEFORE rescue outcomes/metrics were saved, then finalize_run -overwrote it. Now scores only are written here; terminal state is set -once by finalize_run after all sub-results are persisted." -``` - ---- - -### Task 2: Create RuleBinding DTOs - -**Files:** -- Create: `fastapi_modules/fastapi_leaudit/domian/Dto/ruleBindingDto.py` - -- [ ] **Step 1: Create the DTO file** - -```python -"""规则类型绑定 DTO。""" - -from pydantic import BaseModel, Field - - -class RuleBindingCreateDTO(BaseModel): - """创建规则类型绑定请求。""" - - docTypeId: int = Field(..., description="文档类型ID → leaudit_document_types.id") - docTypeCode: str | None = Field(None, description="文档类型编码(冗余快速匹配)") - ruleSetId: int = Field(..., description="规则集ID → leaudit_rule_sets.id") - bindingMode: str = Field("explicit", description="绑定模式: explicit / wildcard / fallback") - priority: int = Field(0, description="优先级(数值越大优先级越高)") - note: str | None = Field(None, description="备注说明") - - -class RuleBindingUpdateDTO(BaseModel): - """更新规则类型绑定请求。""" - - isActive: bool | None = Field(None, description="是否激活") - priority: int | None = Field(None, description="优先级") - bindingMode: str | None = Field(None, description="绑定模式") - note: str | None = Field(None, description="备注说明") -``` - -- [ ] **Step 2: Syntax check** - -```bash -cd /home/wren-dev/Porject/leaudit-platform && python -m compileall fastapi_modules/fastapi_leaudit/domian/Dto/ruleBindingDto.py -``` -Expected: Compile successful. - -- [ ] **Step 3: Commit** - -```bash -cd /home/wren-dev/Porject/leaudit-platform -git add fastapi_modules/fastapi_leaudit/domian/Dto/ruleBindingDto.py -git commit -m "feat: add RuleBindingCreateDTO and RuleBindingUpdateDTO" -``` - ---- - -### Task 3: Add RuleBindingVO to ruleVo.py - -**Files:** -- Modify: `fastapi_modules/fastapi_leaudit/domian/vo/ruleVo.py` - -- [ ] **Step 1: Append RuleBindingVO class at end of file** - -Add after line 49 (after the `RuleValidationVO` class): - -```python - - -class RuleBindingVO(BaseModel): - """规则类型绑定响应。""" - - id: int = Field(..., description="绑定ID") - docTypeId: int = Field(..., description="文档类型ID") - docTypeCode: str | None = Field(None, description="文档类型编码") - ruleSetId: int = Field(..., description="规则集ID") - ruleType: str | None = Field(None, description="规则类型编码(来自关联查询)") - ruleName: str | None = Field(None, description="规则集名称(来自关联查询)") - bindingMode: str = Field(..., description="绑定模式: explicit / wildcard / fallback") - priority: int = Field(0, description="优先级") - isActive: bool = Field(True, description="是否激活") - note: str | None = Field(None, description="备注说明") -``` - -- [ ] **Step 2: Syntax check** - -```bash -cd /home/wren-dev/Porject/leaudit-platform && python -m compileall fastapi_modules/fastapi_leaudit/domian/vo/ruleVo.py -``` -Expected: Compile successful. - -- [ ] **Step 3: Commit** - -```bash -cd /home/wren-dev/Porject/leaudit-platform -git add fastapi_modules/fastapi_leaudit/domian/vo/ruleVo.py -git commit -m "feat: add RuleBindingVO for rule type bindings response" -``` - ---- - -### Task 4: Add Binding Methods to IRuleService Interface - -**Files:** -- Modify: `fastapi_modules/fastapi_leaudit/services/ruleService.py` - -- [ ] **Step 1: Add import for RuleBindingVO at top of file** - -Add `RuleBindingVO` to the existing import block (line 5-10): - -```python -from fastapi_modules.fastapi_leaudit.domian.vo.ruleVo import ( - RuleBindingVO, - RuleContentVO, - RuleSetVO, - RuleValidationVO, - RuleVersionVO, -) -``` - -- [ ] **Step 2: Add 4 abstract methods before the closing of the class** - -Add after the `Rollback` method (before the last blank line of the class): - -```python - @abstractmethod - async def ListBindings(self, RuleType: str | None = None) -> list[RuleBindingVO]: - """列出规则类型绑定。可按规则类型过滤。""" - ... - - @abstractmethod - async def CreateBinding( - self, - DocTypeId: int, - RuleSetId: int, - BindingMode: str = "explicit", - Priority: int = 0, - DocTypeCode: str | None = None, - Note: str | None = None, - ) -> RuleBindingVO: - """创建规则类型绑定。""" - ... - - @abstractmethod - async def UpdateBinding( - self, - BindingId: int, - IsActive: bool | None = None, - Priority: int | None = None, - BindingMode: str | None = None, - Note: str | None = None, - ) -> RuleBindingVO: - """更新规则类型绑定。""" - ... - - @abstractmethod - async def DeleteBinding(self, BindingId: int) -> None: - """删除规则类型绑定。""" - ... -``` - -- [ ] **Step 3: Syntax check** - -```bash -cd /home/wren-dev/Porject/leaudit-platform && python -m compileall fastapi_modules/fastapi_leaudit/services/ruleService.py -``` -Expected: Compile successful. - -- [ ] **Step 4: Commit** - -```bash -cd /home/wren-dev/Porject/leaudit-platform -git add fastapi_modules/fastapi_leaudit/services/ruleService.py -git commit -m "feat: add binding CRUD methods to IRuleService interface" -``` - ---- - -### Task 5: Implement Binding Methods in RuleServiceImpl - -**Files:** -- Modify: `fastapi_modules/fastapi_leaudit/services/impl/ruleServiceImpl.py` - -- [ ] **Step 1: Add RuleBindingVO import** - -Add `RuleBindingVO` to the import from `ruleVo` (line 12-17): - -```python -from fastapi_modules.fastapi_leaudit.domian.vo.ruleVo import ( - RuleBindingVO, - RuleContentVO, - RuleSetVO, - RuleValidationVO, - RuleVersionVO, -) -``` - -- [ ] **Step 2: Add 4 method implementations after Rollback method (before _SwitchVersion)** - -Insert before the `_SwitchVersion` method (before line 342): - -```python - async def ListBindings(self, RuleType: str | None = None) -> list[RuleBindingVO]: - """列出规则类型绑定,可按规则类型过滤。""" - async with GetAsyncSession() as Session: - if RuleType: - Result = await Session.execute( - text( - """ - SELECT - b.id, - b.doc_type_id, - b.doc_type_code, - b.rule_set_id, - b.binding_mode, - b.priority, - b.is_active, - b.note, - rs.rule_type, - rs.rule_name - FROM leaudit_rule_type_bindings b - JOIN leaudit_rule_sets rs ON rs.id = b.rule_set_id - WHERE rs.rule_type = :rule_type - AND rs.delete_time IS NULL - ORDER BY b.priority DESC, b.id DESC - """ - ), - {"rule_type": RuleType}, - ) - else: - Result = await Session.execute( - text( - """ - SELECT - b.id, - b.doc_type_id, - b.doc_type_code, - b.rule_set_id, - b.binding_mode, - b.priority, - b.is_active, - b.note, - rs.rule_type, - rs.rule_name - FROM leaudit_rule_type_bindings b - JOIN leaudit_rule_sets rs ON rs.id = b.rule_set_id - WHERE rs.delete_time IS NULL - ORDER BY rs.rule_type, b.priority DESC, b.id DESC - """ - ), - ) - return [ - RuleBindingVO( - id=int(Row["id"]), - docTypeId=int(Row["doc_type_id"]), - docTypeCode=Row["doc_type_code"], - ruleSetId=int(Row["rule_set_id"]), - ruleType=Row["rule_type"], - ruleName=Row["rule_name"], - bindingMode=Row["binding_mode"], - priority=int(Row["priority"]), - isActive=bool(Row["is_active"]), - note=Row["note"], - ) - for Row in Result.mappings().all() - ] - - async def CreateBinding( - self, - DocTypeId: int, - RuleSetId: int, - BindingMode: str = "explicit", - Priority: int = 0, - DocTypeCode: str | None = None, - Note: str | None = None, - ) -> RuleBindingVO: - """创建规则类型绑定。""" - async with GetAsyncSession() as Session: - RuleSet = await Session.execute( - text("SELECT id, rule_type, rule_name FROM leaudit_rule_sets WHERE id = :rid AND delete_time IS NULL LIMIT 1"), - {"rid": RuleSetId}, - ) - if not RuleSet.mappings().first(): - raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "规则集不存在") - - Existing = await Session.execute( - text( - """ - SELECT id FROM leaudit_rule_type_bindings - WHERE doc_type_id = :dtid AND rule_set_id = :rsid - LIMIT 1 - """ - ), - {"dtid": DocTypeId, "rsid": RuleSetId}, - ) - if Existing.mappings().first(): - raise LeauditException(StatusCodeEnum.HTTP_409_CONFLICT, "该文档类型已绑定此规则集") - - Result = await Session.execute( - text( - """ - INSERT INTO leaudit_rule_type_bindings ( - doc_type_id, - doc_type_code, - rule_set_id, - binding_mode, - priority, - is_active, - note - ) VALUES ( - :doc_type_id, - :doc_type_code, - :rule_set_id, - :binding_mode, - :priority, - true, - :note - ) - RETURNING id, doc_type_id, doc_type_code, rule_set_id, - binding_mode, priority, is_active, note - """ - ), - { - "doc_type_id": DocTypeId, - "doc_type_code": DocTypeCode, - "rule_set_id": RuleSetId, - "binding_mode": BindingMode, - "priority": Priority, - "note": Note, - }, - ) - await Session.commit() - Row = Result.mappings().first() - RsRow = RuleSet.mappings().first() - return RuleBindingVO( - id=int(Row["id"]), - docTypeId=int(Row["doc_type_id"]), - docTypeCode=Row["doc_type_code"], - ruleSetId=int(Row["rule_set_id"]), - ruleType=RsRow["rule_type"], - ruleName=RsRow["rule_name"], - bindingMode=Row["binding_mode"], - priority=int(Row["priority"]), - isActive=bool(Row["is_active"]), - note=Row["note"], - ) - - async def UpdateBinding( - self, - BindingId: int, - IsActive: bool | None = None, - Priority: int | None = None, - BindingMode: str | None = None, - Note: str | None = None, - ) -> RuleBindingVO: - """更新规则类型绑定。""" - async with GetAsyncSession() as Session: - Existing = await Session.execute( - text( - """ - SELECT - b.id, b.doc_type_id, b.doc_type_code, b.rule_set_id, - b.binding_mode, b.priority, b.is_active, b.note, - rs.rule_type, rs.rule_name - FROM leaudit_rule_type_bindings b - JOIN leaudit_rule_sets rs ON rs.id = b.rule_set_id - WHERE b.id = :bid - LIMIT 1 - """ - ), - {"bid": BindingId}, - ) - Row = Existing.mappings().first() - if not Row: - raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "绑定记录不存在") - - SetClauses: list[str] = [] - Params: dict[str, object] = {"bid": BindingId} - - if IsActive is not None: - SetClauses.append("is_active = :is_active") - Params["is_active"] = IsActive - if Priority is not None: - SetClauses.append("priority = :priority") - Params["priority"] = Priority - if BindingMode is not None: - SetClauses.append("binding_mode = :binding_mode") - Params["binding_mode"] = BindingMode - if Note is not None: - SetClauses.append("note = :note") - Params["note"] = Note - - if SetClauses: - SetClauses.append("update_time = now()") - await Session.execute( - text(f"UPDATE leaudit_rule_type_bindings SET {', '.join(SetClauses)} WHERE id = :bid"), - Params, - ) - await Session.commit() - - Result = await Session.execute( - text( - """ - SELECT - b.id, b.doc_type_id, b.doc_type_code, b.rule_set_id, - b.binding_mode, b.priority, b.is_active, b.note, - rs.rule_type, rs.rule_name - FROM leaudit_rule_type_bindings b - JOIN leaudit_rule_sets rs ON rs.id = b.rule_set_id - WHERE b.id = :bid - LIMIT 1 - """ - ), - {"bid": BindingId}, - ) - Row = Result.mappings().first() - return RuleBindingVO( - id=int(Row["id"]), - docTypeId=int(Row["doc_type_id"]), - docTypeCode=Row["doc_type_code"], - ruleSetId=int(Row["rule_set_id"]), - ruleType=Row["rule_type"], - ruleName=Row["rule_name"], - bindingMode=Row["binding_mode"], - priority=int(Row["priority"]), - isActive=bool(Row["is_active"]), - note=Row["note"], - ) - - async def DeleteBinding(self, BindingId: int) -> None: - """删除规则类型绑定。""" - async with GetAsyncSession() as Session: - Result = await Session.execute( - text("DELETE FROM leaudit_rule_type_bindings WHERE id = :bid"), - {"bid": BindingId}, - ) - await Session.commit() - if Result.rowcount == 0: - raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "绑定记录不存在") -``` - -- [ ] **Step 2: Syntax check** - -```bash -cd /home/wren-dev/Porject/leaudit-platform && python -m compileall fastapi_modules/fastapi_leaudit/services/impl/ruleServiceImpl.py -``` -Expected: Compile successful. - -- [ ] **Step 3: Commit** - -```bash -cd /home/wren-dev/Porject/leaudit-platform -git add fastapi_modules/fastapi_leaudit/services/impl/ruleServiceImpl.py -git commit -m "feat: implement binding CRUD in RuleServiceImpl" -``` - ---- - -### Task 6: Add Binding Endpoints to RuleController - -**Files:** -- Modify: `fastapi_modules/fastapi_leaudit/controllers/ruleController.py` - -- [ ] **Step 1: Add imports for new types** - -Update the imports (lines 3-16) to include binding DTOs and VO: - -```python -"""规则管理控制器。""" - -from fastapi_common.fastapi_common_web.controller import BaseController -from fastapi_common.fastapi_common_web.domain.responses import Result - -from fastapi_modules.fastapi_leaudit.domian.Dto.ruleBindingDto import ( - RuleBindingCreateDTO, - RuleBindingUpdateDTO, -) -from fastapi_modules.fastapi_leaudit.domian.Dto.rulePublishDto import RulePublishDTO -from fastapi_modules.fastapi_leaudit.domian.Dto.ruleValidateDto import RuleValidateDTO -from fastapi_modules.fastapi_leaudit.domian.Dto.ruleVersionCreateDto import RuleVersionCreateDTO -from fastapi_modules.fastapi_leaudit.domian.vo.ruleVo import ( - RuleBindingVO, - RuleContentVO, - RuleSetVO, - RuleValidationVO, - RuleVersionVO, -) -from fastapi_modules.fastapi_leaudit.services import IRuleService -from fastapi_modules.fastapi_leaudit.services.impl.ruleServiceImpl import RuleServiceImpl -``` - -- [ ] **Step 2: Add 4 endpoint definitions inside __init__** - -Add after the `RollbackRuleVersion` endpoint (after line 82, before the closing of `__init__`): - -```python - # ── Rule Type Bindings ────────────────────────────────────── - - @self.router.get("/bindings", response_model=Result[list[RuleBindingVO]]) - async def ListBindings(ruleType: str | None = None): - """列出规则类型绑定。可按规则类型过滤。""" - Data = await self.RuleService.ListBindings(RuleType=ruleType) - return Result.success(data=Data) - - @self.router.post("/{RuleType}/bindings", response_model=Result[RuleBindingVO]) - async def CreateBinding(RuleType: str, body: RuleBindingCreateDTO): - """创建规则类型绑定。""" - Data = await self.RuleService.CreateBinding( - DocTypeId=body.docTypeId, - RuleSetId=body.ruleSetId, - BindingMode=body.bindingMode, - Priority=body.priority, - DocTypeCode=body.docTypeCode, - Note=body.note, - ) - return Result.success(data=Data) - - @self.router.put("/bindings/{BindingId}", response_model=Result[RuleBindingVO]) - async def UpdateBinding(BindingId: int, body: RuleBindingUpdateDTO): - """更新规则类型绑定。""" - Data = await self.RuleService.UpdateBinding( - BindingId=BindingId, - IsActive=body.isActive, - Priority=body.priority, - BindingMode=body.bindingMode, - Note=body.note, - ) - return Result.success(data=Data) - - @self.router.delete("/bindings/{BindingId}", response_model=Result[None]) - async def DeleteBinding(BindingId: int): - """删除规则类型绑定。""" - await self.RuleService.DeleteBinding(BindingId=BindingId) - return Result.success() -``` - -- [ ] **Step 3: Syntax check** - -```bash -cd /home/wren-dev/Porject/leaudit-platform && python -m compileall fastapi_modules/fastapi_leaudit/controllers/ruleController.py -``` -Expected: Compile successful. - -- [ ] **Step 4: Commit** - -```bash -cd /home/wren-dev/Porject/leaudit-platform -git add fastapi_modules/fastapi_leaudit/controllers/ruleController.py -git commit -m "feat: add rule type binding CRUD endpoints to RuleController" -``` - ---- - -### Task 7: Verification — Cross-Module Import Check - -**Files:** None (verification only) - -- [ ] **Step 1: Verify all modified modules compile together** - -```bash -cd /home/wren-dev/Porject/leaudit-platform && python -c " -from fastapi_modules.fastapi_leaudit.domian.Dto.ruleBindingDto import RuleBindingCreateDTO, RuleBindingUpdateDTO -from fastapi_modules.fastapi_leaudit.domian.vo.ruleVo import RuleBindingVO -from fastapi_modules.fastapi_leaudit.services.ruleService import IRuleService -from fastapi_modules.fastapi_leaudit.services.impl.ruleServiceImpl import RuleServiceImpl -from fastapi_modules.fastapi_leaudit.leaudit_bridge.storage_adapter import StorageAdapter -print('All imports OK') -" -``` -Expected: `All imports OK` - -- [ ] **Step 2: Verify the double finalize fix — confirm finalize_run is the only terminal state writer** - -```bash -cd /home/wren-dev/Porject/leaudit-platform && grep -n "result_status\|finished_at" fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py -``` -Expected output should show `result_status` and `finished_at` ONLY in `finalize_run` and `fail_run`, NOT in `save_evaluation_results`. - -- [ ] **Step 3: Commit final verification** - -```bash -cd /home/wren-dev/Porject/leaudit-platform -git add -A -git diff --cached --stat -git commit -m "chore: verify cross-module imports and finalize consistency" -``` diff --git a/docs/历史API与权限文档索引.md b/docs/历史API与权限文档索引.md new file mode 100644 index 0000000..612caeb --- /dev/null +++ b/docs/历史API与权限文档索引.md @@ -0,0 +1,30 @@ +# 历史 API 与权限文档索引 + +> 最后整理:2026-05-04 +> 说明:`new_doc_review/auth_doc/` 目录以历史阶段的 API、RBAC、前端对接快照为主,适合补背景,不适合作为当前系统唯一事实来源。 + +## 使用方式 + +- 当前主链路以 `docs/HANDOFF.md` 与 `docs/接口/README.md` 为准 +- `new_doc_review/auth_doc/` 里的文档,多数写于旧接口或 PostgREST 仍是主依赖的时期 +- 若历史文档与现网代码冲突,以现网代码和主文档为准 + +## 建议优先查阅的历史文档 + +| 主题 | 文档 | 说明 | +|------|------|------| +| RBAC 总说明 | `new_doc_review/auth_doc/RBAC_API_GUIDE_V3.4.md` | 当前最接近可用的 RBAC API 总览 | +| RBAC 历史全量设计 | `new_doc_review/auth_doc/RBAC_COMPLETE_GUIDE_V3.3.md` | 适合追溯设计演进 | +| 入口模块 API | `new_doc_review/auth_doc/ENTRY_MODULE_API.md`、`new_doc_review/auth_doc/entry_modules_api.md` | 入口模块旧接口资料 | +| 文档类型 API | `new_doc_review/auth_doc/document_types_api.md` | 文档类型早期接口说明 | +| 评查点前端接口 | `new_doc_review/auth_doc/review_points_api_frontend.md` | 评查点老链路前端接口说明 | +| 交叉评查 | `new_doc_review/auth_doc/交叉评查接口文档.md`、`new_doc_review/auth_doc/交叉评查接口对接状态报告.md` | 交叉评查历史对接资料 | +| 路由 / 共享权限 | `new_doc_review/auth_doc/通用权限前端对接文档.md`、`new_doc_review/auth_doc/通用权限前端对接文档(1).md`、`new_doc_review/auth_doc/角色-路由权限实现方案.md` | 一个是 RBAC 总对接文档,一个是共享权限专项说明,一个是后端设计说明 | + +## 本次清理 + +- 已删除被新版覆盖或明显重复的副本: + - `new_doc_review/auth_doc/entry_modules_api(1).md` + - `new_doc_review/auth_doc/交叉评查接口文档(1).md` + - `new_doc_review/auth_doc/API_RESPONSE_EXAMPLES_V3.2.md` +- 保留 `new_doc_review/auth_doc/API_RESPONSE_EXAMPLES_V3.3.md` 作为现存最新响应示例 diff --git a/docs/接口/README.md b/docs/接口/README.md index 619436e..36d5792 100644 --- a/docs/接口/README.md +++ b/docs/接口/README.md @@ -1,29 +1,40 @@ -# 接口文档目录 +# 接口文档导航 -这个目录专门放当前 `leaudit-platform` 已落地接口的使用说明,重点记录: +> 最后整理:2026-05-04 +> 说明:这里记录的是 `leaudit-platform` 当前主线已落地、正在联调、或明确作为迁移目标的接口文档。 -- 接口用途 -- 参数说明 -- 业务逻辑 -- 请求示例 -- 返回示例 +## 阅读顺序 -当前已整理: +1. `docs/HANDOFF.md` +2. `文档上传与列表接口分析.md` +3. `入口模块绑定最终设计方案.md` +4. `用户权限与权限点清单.md` +5. 根据具体模块继续往下看 -- `文档上传与评查接口.md` -- `新系统版_documents_list接口.md` -- `用户权限与权限点清单.md` -- `用户权限初始化SQL.sql` -- `老用户迁移脚本说明.md` -- `首页入口接口落地说明.md` -- `系统设置入口恢复说明.md` +## 按模块查找 -建议阅读顺序: +| 模块 | 文档 | 说明 | +|------|------|------| +| 首页入口 / 菜单 | `入口模块绑定最终设计方案.md` | 入口模块、文档类型、规则链路绑定模型 | +| 文档上传 / 列表 / 评查 | `文档上传与列表接口分析.md` | 上传、列表、详情、更新、删除、评查触发、数据隔离 | +| 文档类型 / 评查组 | `评查点分组目标结构与迁移方案.md` | 文档类型、一级分组、二级分组、规则集与迁移口径 | +| 评查点分组迁移 | `评查点分组目标结构与迁移方案.md` | 新老分组结构对齐方案 | +| 评查点分组迁移 | `评查点分组迁移执行前检查清单.md` | 正式迁移前检查项 | +| 权限 / 路由 | `用户权限与权限点清单.md` | RBAC、路由、权限点清单 | +| 用户迁移 | `老用户迁移脚本说明.md` | 老用户迁移脚本与校验说明 | -1. 先看 `文档上传与评查接口.md` -2. 再看 `新系统版_documents_list接口.md` -3. 再看 `用户权限与权限点清单.md` -4. 如果要初始化角色与权限数据,再执行 `用户权限初始化SQL.sql` -5. 如果要迁移老系统用户,再看 `老用户迁移脚本说明.md` -6. 如果要理解 worker 并发执行,再结合 `docs/规则编辑/worker并发执行改造方案.md` -7. 如果要理解底层数据结构,再看 `docs/leaudit/document_schema_design.md` +## 本次归并 + +- `新系统版_documents_list接口.md` 已并入 `文档上传与列表接口分析.md` +- `文档上传与评查接口.md` 已并入 `文档上传与列表接口分析.md` +- `首页入口接口落地说明.md` 已并入 `入口模块绑定最终设计方案.md` +- `评查点分组正式迁移执行建议.md` 已并入 `评查点分组迁移执行前检查清单.md` +- `文档类型与评查组关联方案.md` 已并入 `评查点分组目标结构与迁移方案.md` +- `首页菜单最小可用收口说明.md`、`系统设置入口恢复说明.md` 的阶段性收口内容已并入 `docs/HANDOFF.md` +- `前端联调404与资源缺口收口清单.md` 属于前端联调阶段记录,已从后端主文档目录移除 + +## 使用约束 + +- 这里的文档优先级高于 `new_doc_review/auth_doc/` 的历史快照资料 +- 如果某份文档描述和当前代码不一致,以当前代码与 `docs/HANDOFF.md` 为准,并尽快回补文档 +- 新接口如果已经用于当前主链路,必须同步补到本目录 diff --git a/docs/接口/入口模块绑定最终设计方案.md b/docs/接口/入口模块绑定最终设计方案.md new file mode 100644 index 0000000..7ff3ca4 --- /dev/null +++ b/docs/接口/入口模块绑定最终设计方案.md @@ -0,0 +1,762 @@ +# 入口模块绑定最终设计方案 + +> 适用项目:`leaudit-platform` +> +> 参考来源: +> - 老系统:`/home/wren-dev/Porject/docauditai` +> - 新前端:`/home/wren-dev/Porject/leaudit-platform/new_doc_review` +> - 新后端:`/home/wren-dev/Porject/leaudit-platform/fastapi_modules` + +--- + +## 1. 这份文档解决什么问题 + +本方案要解决的是: + +1. 老系统“入口模块”到底是什么语义 +2. 新系统里 `entry_modules / document_types / sys_routes / role_route` 各自该负责什么 +3. 首页入口模块应该由谁来组装 +4. 现有逻辑有哪些遗漏和隐患 +5. 下一步如何把它收口成一套稳定方案 + +这份文档的目标不是直接讲实现细节,而是先把**边界、语义、数据流、遗漏点**固定下来,避免后续继续前后端双写规则。 + +--- + +## 2. 结论先行 + +### 2.1 核心结论 + +老系统里的“入口模块”**不是权限系统**,而是**首页业务入口编排系统**。 + +它的职责是: + +- 决定首页显示哪些入口卡片 +- 给每个入口卡片绑定一组文档类型 +- 按地区启用/禁用入口 + +它**不负责**: + +- 控制用户是否可以访问某个页面 +- 控制用户是否可以调用某个 API +- 替代 RBAC 路由菜单系统 + +真正的权限系统始终是: + +- `sys_routes` +- `role_route` +- `permissions` +- `role_permissions` + +### 2.2 新系统必须收口的总原则 + +新系统应该明确分成三层: + +1. **首页入口层** + - 数据源:`leaudit_entry_modules` + - 作用:决定首页显示什么入口 + +2. **业务归类层** + - 数据源:`leaudit_document_types.entry_module_id` + - 作用:决定某入口下挂哪些文档类型 + +3. **权限控制层** + - 数据源:`sys_routes / role_route / permissions / role_permissions` + - 作用:决定用户是否真的能访问页面和动作 + +### 2.3 当前已落地接口口径 + +当前首页入口已经统一走: + +- `GET /api/home/entry-modules` + +当前返回结构核心字段包括: + +- `id` +- `name` +- `description` +- `targetPath` +- `routePath` +- `iconPath` +- `sortOrder` +- `requiresDocumentTypes` +- `areas` +- `documentTypes` + +当前过滤顺序为: + +1. `leaudit_entry_modules.is_enabled = true` +2. `leaudit_entry_modules.deleted_at is null` +3. 地区过滤 +4. 若目标页面已存在于 `sys_routes`,则继续做 `role_route` 权限过滤 + +这意味着首页入口已经不是前端自己拼装,而是后端统一组装后返回。 + +--- + +## 3. 老系统深度分析结论 + +## 3.1 首页入口不是从 sys_routes 直接来 + +老系统首页入口不是直接根据 RBAC 路由树生成,而是单独查询入口模块配置。 + +对应证据: + +- 老系统入口模块服务: + - `/home/wren-dev/Porject/docauditai/app/services/entry_module_service.py` +- 老系统入口模块路由: + - `/home/wren-dev/Porject/docauditai/app/routes/entry_modules.py` +- 老系统文档类型服务: + - `/home/wren-dev/Porject/docauditai/app/services/document_type_service.py` + +### 实际链路 + +老系统首页逻辑本质是: + +1. 查 `entry_modules` +2. 按 `areas` 做地区过滤 +3. 对每个入口模块,再查 `document_types where entry_module_id = module.id` +4. 拼成首页卡片 +5. 点击卡片后跳转目标页面 +6. 目标页面再走 RBAC 校验 + +所以首页入口和页面权限本来就是两套体系。 + +--- + +## 3.2 老系统中 entry_modules 的真实职责 + +老系统 `entry_modules` 本质上存的是“首页业务入口卡片配置”。 + +典型字段语义: + +- `id`:入口模块主键 +- `name`:入口名称 +- `description`:说明 +- `path`:旧系统里语义不稳定,常被当成资源/图片路径使用 +- `areas`:地区配置 +- `created_at / updated_at` + +### 关键观察 + +老系统里 `path` 不是一个稳定可靠的“点击后跳转路由字段”。 + +也就是说: + +- 它并不总是纯前端路由 +- 它常常承担资源路径、展示配置、模块图片等混合语义 + +这也是老系统最容易让后续维护者误解的地方之一。 + +--- + +## 3.3 文档类型和入口模块的关系 + +老系统文档类型是通过 `entry_module_id` 绑定到入口模块的。 + +这说明: + +- 一个入口模块下面挂很多文档类型 +- 一个文档类型归属于一个入口模块 + +这个设计本身是合理的,属于“业务归类关系”,不属于“权限关系”。 + +### 当前判断 + +除非未来明确出现“一个文档类型必须出现在多个入口模块”的业务需求,否则: + +- 新系统应继续保留 `entry_module_id` +- 没必要提前拆成中间表 + +--- + +## 3.4 真正的页面权限来自 RBAC + +老系统页面权限来源一直是: + +- `sys_routes` +- `role_route` + +动作/API 权限来源是: + +- `permissions` +- `role_permissions` + +这意味着在老系统中可能出现: + +1. 首页能看到某个入口模块 +2. 点击后跳到某个页面 +3. 页面又因为 RBAC 不通过而拒绝访问 + +这是老系统长期存在的结构性问题,不是 Bug,而是设计分层没有被清楚说明。 + +--- + +## 4. 新系统最终语义边界 + +## 4.1 `leaudit_entry_modules` + +### 负责 + +- 首页显示哪些业务入口 +- 每个入口的名称、图标、排序、地区启用规则 +- 每个入口点击后去哪 + +### 不负责 + +- 页面访问权限 +- API 权限 +- 替代路由表 + +--- + +## 4.2 `leaudit_document_types` + +### 负责 + +- 定义系统支持的文档类型 +- 通过 `entry_module_id` 归属到某个入口模块 + +### 不负责 + +- 决定首页是否展示入口模块 +- 决定菜单权限 + +--- + +## 4.3 `sys_routes / role_route` + +### 负责 + +- 页面路由菜单权限 +- 用户可访问哪些页面 + +### 不负责 + +- 首页模块展示编排 +- 文档类型归类 + +--- + +## 4.4 `permissions / role_permissions` + +### 负责 + +- API 权限 +- 动作按钮权限 +- 数据范围权限 + +### 不负责 + +- 首页入口展示 +- 文档类型归属 + +--- + +## 5. 新系统最终数据模型建议 + +## 5.1 `leaudit_entry_modules` 建议字段 + +当前表建议正式化为以下语义: + +- `id` +- `module_key` +- `name` +- `description` +- `icon_path` +- `target_path` +- `route_path` +- `module_type` +- `allow_empty_types` +- `areas` +- `sort_order` +- `is_enabled` +- `create_time` +- `update_time` + +### 字段解释 + +#### `module_key` + +稳定的业务标识,用于程序判断,不可依赖 `name`。 + +示例: + +- `contract` +- `case_review` +- `assistant` +- `cross_review` + +#### `icon_path` + +首页卡片图标/图片路径。 + +应从旧的 `path` 语义中剥离出来,单独承担展示职责。 + +#### `target_path` + +用户点击首页卡片后实际跳转的前端路径。 + +示例: + +- `/contract-template/search` +- `/home` +- `/chat-with-llm/chat` +- `/cross-checking` + +#### `route_path` + +与 RBAC 路由树关联的目标页面路径,用于做“当前用户是否真的有访问权限”的判断。 + +通常可与 `target_path` 相同,但不强制必须相同。 + +#### `module_type` + +建议枚举: + +- `document` +- `assistant` +- `cross_review` +- `settings_link` + +#### `allow_empty_types` + +控制“没有绑定文档类型时是否仍然允许显示”。 + +建议: + +- 文档类模块:`false` +- 助手类模块:`true` +- 交叉评查类模块:`true` + +--- + +## 5.2 `leaudit_document_types` + +继续保留: + +- `entry_module_id` + +表示“该文档类型归属哪个入口模块”。 + +### 当前不建议做的事 + +暂不建议改成: + +- `leaudit_entry_module_document_type_bindings` + +因为当前没有明确“一种文档类型要同时挂多个入口”的需求。 + +--- + +## 6. 首页统一接口最终设计 + +建议新增: + +- `GET /api/home/entry-modules` + +由后端直接根据当前登录用户返回首页入口列表。 + +前端不再自己拼 `entry_modules + document_types + area + 跳转规则`。 + +--- + +## 6.1 接口职责 + +后端统一完成: + +1. 识别当前用户 +2. 读取用户角色 +3. 读取用户地区 +4. 查询可用入口模块 +5. 按地区过滤 +6. 按页面权限做一次 RBAC 二次过滤 +7. 挂载该入口下的文档类型 +8. 返回最终首页可见入口列表 + +--- + +## 6.2 建议返回结构 + +```json +{ + "code": 200, + "message": "ok", + "data": { + "items": [ + { + "id": 1, + "moduleKey": "contract", + "name": "合同管理", + "description": "合同智能审核与模板管理", + "iconPath": "static/modules/contract.png", + "targetPath": "/contract-template/search", + "routePath": "/contract-template/search", + "moduleType": "document", + "allowEmptyTypes": false, + "sortOrder": 10, + "documentTypes": [ + { + "id": 9, + "code": "contract.sale", + "name": "买卖合同" + } + ] + } + ] + } +} +``` + +--- + +## 7. 首页统一接口的推荐过滤规则 + +## 7.1 基础过滤 + +必须满足: + +- `is_enabled = true` + +--- + +## 7.2 地区过滤 + +### 省级管理员 + +- `provincial_admin` 可查看所有启用入口模块 + +### 地市管理员 / 普通用户 + +- `admin` +- `common` + +仅可看到 `areas` 中当前 `user.area` 且 `enabled = true` 的模块。 + +### 重要约束 + +地区过滤必须在后端完成,不能信前端传 area。 + +--- + +## 7.3 RBAC 二次过滤 + +如果入口模块配置了 `route_path`,则需要再检查当前用户是否有该页面的访问权限。 + +### 目的 + +避免出现: + +- 首页看得到入口 +- 点进去却被 RBAC 拒绝 + +### 推荐做法 + +基于当前用户角色集合,查询: + +- `sys_routes.route_path = route_path` +- 是否被 `role_route` 授权 + +若未授权,则该入口模块不返回。 + +--- + +## 7.4 文档类型过滤 + +### 对 `module_type = document` + +必须至少有一个启用中的文档类型,否则不返回。 + +### 对 `module_type = assistant / cross_review` + +允许没有文档类型,只要模块本身启用并通过权限检查即可返回。 + +--- + +## 7.5 排序规则 + +建议优先级: + +1. `areas[].sort_order`(如果当前地区有配置) +2. `leaudit_entry_modules.sort_order` +3. `id` + +--- + +## 8. 前端必须删掉的旧逻辑 + +## 8.1 不能再由前端自己查 `entry_modules` + +当前旧逻辑: + +- 前端查入口模块 +- 前端按地区过滤 +- 前端再查文档类型 + +这会导致规则散落在前端,不利于后端统一治理。 + +应改为: + +- 前端只调用 `GET /api/home/entry-modules` + +--- + +## 8.2 不能再按模块名硬编码跳转 + +当前风险逻辑: + +- 模块名包含“合同” -> `/contract-template/search` +- 模块名是“智慧法务助手” -> `/chat-with-llm/chat` + +这类逻辑必须移除。 + +应改为直接使用后端返回的: + +- `targetPath` + +--- + +## 8.3 不能再在前端默认硬塞特殊模块 + +例如: + +- “智慧法务助手” + +不应再由前端强行 `push()` 到入口列表里。 + +必须和普通模块一样进入 `leaudit_entry_modules` 正式配置。 + +这样做的好处: + +- 权限统一 +- 排序统一 +- 是否启用统一 +- 图标统一 +- 后台管理统一 + +--- + +## 9. 当前逻辑遗漏点深度检查 + +下面是本次分析中确认存在或高概率会出现的遗漏点。 + +## 9.1 首页可见 ≠ 页面可访问 + +这是老系统就存在的问题。 + +如果首页只按 `entry_modules` 过滤,不按 RBAC 再过滤,就会出现: + +- 首页看到入口 +- 点进去又被页面权限拦住 + +### 结论 + +首页接口必须做 RBAC 二次过滤。 + +--- + +## 9.2 `path` 字段语义不稳定 + +老系统里 `entry_modules.path` 混杂: + +- 图片路径 +- 资源路径 +- 可能被误当成前端路由 + +### 结论 + +新系统不能继续复用一个 `path` 字段承担两种含义。 + +必须拆成: + +- `icon_path` +- `target_path` + +--- + +## 9.3 特殊模块没有文档类型时会被误过滤 + +如果首页统一规则是“模块下必须有文档类型”,那么: + +- 智慧法务助手 +- 交叉评查 + +这类模块会被错误隐藏。 + +### 结论 + +必须引入: + +- `module_type` +- `allow_empty_types` + +--- + +## 9.4 省级管理员是否应该看到所有模块 + +这个规则必须固定,不然以后省级账号行为会不一致。 + +### 建议 + +- `provincial_admin` 看全部已启用入口 +- 但仍需受目标页面 RBAC 权限约束 + +--- + +## 9.5 文档类型全部停用后,文档类入口是否显示 + +如果一个“合同管理”模块下面一个启用文档类型都没有,继续显示没有意义。 + +### 建议 + +- `module_type = document` 且无可用文档类型 -> 不显示 +- `assistant / cross_review` 不受此规则影响 + +--- + +## 9.6 入口模块与左侧菜单不是同一概念 + +首页入口模块是“业务入口卡片”,左侧菜单是“页面路由树”。 + +如果不写清楚,后面会经常出现误解: + +- 为什么首页有入口,但侧边栏没有? +- 为什么有路由权限,但首页不显示? + +### 结论 + +文档中必须明确: + +- 首页入口 = 业务编排 +- 左侧菜单 = RBAC 路由 + +--- + +## 9.7 交叉评查专属模式不应混入入口模块业务规则 + +当前前端已有: + +- `CROSS_CHECKING_ONLY_MODE` + +它本质上是部署态开关,不是入口模块自身规则。 + +### 结论 + +该逻辑应作为部署模式单独处理,不要污染 `entry_modules` 基础语义。 + +--- + +## 9.8 首页入口上下文仍然需要保留 + +即使首页入口最终完全由后端返回,前端仍然建议保留: + +- `selectedModuleId` +- `selectedModuleName` +- `documentTypeIds` + +写入 `sessionStorage` + +### 原因 + +后续页面仍然需要知道: + +- 当前用户是从哪个入口进入的 +- 当前模块下允许哪些文档类型 + +--- + +## 10. 新系统最终推荐方案 + +## 10.1 强约束 + +新系统正式规则建议固定如下: + +1. 首页入口只认 `leaudit_entry_modules` +2. 文档类型归属只认 `leaudit_document_types.entry_module_id` +3. 页面访问权限只认 `sys_routes + role_route` +4. API/动作权限只认 `permissions + role_permissions` +5. 首页入口列表必须由后端统一接口生成 +6. 前端只能消费最终结果,不再自己拼规则 + +--- + +## 10.2 最终推荐模型 + +### 模块主数据 + +- `leaudit_entry_modules` + +### 文档类型归属 + +- `leaudit_document_types.entry_module_id` + +### 页面访问权限 + +- `sys_routes` +- `role_route` + +### 动作/API 权限 + +- `permissions` +- `role_permissions` + +--- + +## 10.3 首页统一接口 + +- `GET /api/home/entry-modules` + +由后端返回: + +- 当前用户可见的入口模块 +- 每个入口模块的目标跳转路径 +- 每个入口模块挂载的文档类型列表 + +--- + +## 11. 后续代码改造建议 + +本次先定方案,代码实现建议按以下顺序推进。 + +### 阶段 1:后端收口 + +1. 新增首页入口接口 +2. 后端实现地区过滤 +3. 后端实现 RBAC 二次过滤 +4. 后端挂载 `document_types` + +### 阶段 2:前端收口 + +1. 首页改为只调用后端统一接口 +2. 删除前端拼 `entry_modules + document_types` 的逻辑 +3. 删除按模块名硬编码跳转 +4. 删除前端默认注入“智慧法务助手” + +### 阶段 3:表结构正式化 + +1. 给 `leaudit_entry_modules` 增加: + - `module_key` + - `target_path` + - `route_path` + - `module_type` + - `allow_empty_types` +2. 若历史字段 `path` 仍在使用,则明确其迁移方向: + - 老 `path` -> `icon_path` + +--- + +## 12. 当前审阅建议 + +本方案建议你重点审阅以下 5 个点: + +1. 是否同意“入口模块不是权限系统”的边界 +2. 是否同意首页入口改为后端统一生成 +3. 是否同意给 `entry_modules` 新增 `module_key / target_path / route_path / module_type / allow_empty_types` +4. 是否同意“智慧法务助手”这类特殊入口也必须正式落表 +5. 是否同意“首页可见”和“页面可访问”保留两层校验,但由后端首页接口统一兜一次 + +--- + +## 13. 一句话版最终结论 + +新系统应把“首页入口模块”正式定义为**首页业务入口编排层**,把“文档类型绑定”定义为**业务归类层**,把“页面/API权限”继续留在 **RBAC 层**,并由后端统一输出首页入口列表,彻底结束前端自己拼入口、自己判地区、自己硬编码跳转的旧模式。 diff --git a/docs/接口/文档上传与列表接口分析.md b/docs/接口/文档上传与列表接口分析.md index c8bf291..92d856d 100644 --- a/docs/接口/文档上传与列表接口分析.md +++ b/docs/接口/文档上传与列表接口分析.md @@ -1,6 +1,8 @@ # 文档上传与列表 — 接口分析 & 接入方案 > 所有信息已逐文件验证。源代码位置均已标注。 +> +> 说明:这份文档已经吸收旧的 `新系统版_documents_list接口.md` 与 `文档上传与评查接口.md`,后续以本文件作为文档上传、列表、详情、更新、删除、评查触发与状态查询的统一说明。 --- @@ -23,7 +25,7 @@ URL: POST /api/upload | `typeCode` | str | typeId/typeCode 二选一 | None | 文档类型编码,如 `contract.construction` | | `region` | str | 否 | `"default"` | 地区标识 | | `fileRole` | str | 否 | `"primary"` | 文件角色:primary/attachment/template | -| `createdBy` | int | 否 | None | 上传用户 ID | +| `createdBy` | int | 否 | None | 前端可传,但后端当前以 JWT 中的 `user_id` 为准 | | `autoRun` | bool | 否 | `false` | 是否上传后自动触发评查 | | `speed` | str | 否 | `"normal"` | urgent / normal | @@ -124,17 +126,119 @@ URL: GET /api/documents/list --- -### 1.3 缺失的后端接口 +### 1.3 文档详情 / 更新 / 删除(现已实现) + +#### GET /api/documents/{id} + +- 用途:获取单个文档详情 +- 返回:`Result[DocumentDetailVO]` +- 关键字段: + - 继承列表接口的版本链、文件、run、统计字段 + - 新增 `documentNumber`、`remark`、`isTestDocument`、`auditStatus`、`pageCount` +- 历史版本:`historyVersions` 返回同一 `version_group_key` 下除当前文档外的其它未删除版本 + +#### PUT /api/documents/{id} + +- 用途:更新文档元数据 +- 请求体:`DocumentUpdateDTO` + +```json +{ + "documentNumber": "粤烟合同〔2026〕001号", + "remark": "地市管理员修订备注", + "isTestDocument": false, + "auditStatus": 1 +} +``` + +- 字段说明: + - `documentNumber`:若 `leaudit_documents.document_number` 列存在则写入 + - `remark`:若 `leaudit_documents.remark` 列存在则写入 + - `isTestDocument`:若 `leaudit_documents.is_test_document` 列存在则写入 + - `auditStatus`:若 `leaudit_documents.audit_status` 列存在则写入;若当前环境未加该列,则接口忽略该字段但不报错 + +#### DELETE /api/documents/{id} + +- 用途:软删除文档 +- 行为: + - `leaudit_documents.deleted_at = now()` + - 当前文档的 active file 置为 `is_active = false` + - 若删除的是当前最新版本,则自动把同版本链中剩余的最高版本提升为 `is_latest_version = true` + +#### 数据隔离规则(已在后端强制执行) + +| 角色 | 范围 | +|------|------| +| `super_admin` / `provincial_admin` | 全量文档 | +| `admin` | **仅本地市 `region = user.area`** | +| `common` | **仅自己上传的文档 `created_by = current_user_id`** | + +额外约束: +- `admin` 若在列表接口传入其他地市 `region`,后端直接返回空结果 +- `common` 若试图访问、修改、删除他人文档,后端返回“文档不存在或无权访问” + +#### 尚未完成 | 操作 | 当前 | 需要 | |------|------|------| -| 删除文档 | ❌ | `DELETE /api/documents/{id}` — 软删除 | -| 编辑元数据 | ❌ | `PUT /api/documents/{id}` — 更新备注、测试标记等 | -| 文档详情 | ❌ | `GET /api/documents/{id}` — 单文档查询 | | 附件追加 | ❌ | `POST /api/documents/{id}/attachments` — fileRole=attachment | --- +### 1.4 自动评查 / 手动评查 / 状态结果查询 + +#### POST /api/upload + +- 上传成功且 `autoRun=true` 时,后端会直接调用 `AuditService.Run()` 创建 run +- 队列档位当前只保留: + - `urgent` + - `normal` + +#### POST /api/audit/run + +- 用途:对已存在文档手动触发评查 +- 典型参数: + - `documentId` + - `speed=normal|urgent` +- 当前收口: + - 若同一文档已有活动 run,后端优先复用,不重复创建 + +#### GET /api/audit/run/{runId} + +- 用途:查询评查任务状态 +- 关键状态: + - `queued` + - `running` + - `completed` + - `failed` +- 关键阶段: + - `dispatch` + - `prepare` + - `ocr / extraction / evaluation / rescue / persist` + +#### GET /api/audit/result/{runId} + +- 用途:查询评查结果 +- 返回来源: + - `leaudit_rule_results` + - `leaudit_field_results` + - `leaudit_run_metrics` + - `leaudit_run_errors` + +#### 当前评查链路口径 + +1. 上传文档 +2. 建立 `leaudit_documents / leaudit_document_files` +3. 创建 `leaudit_audit_runs` +4. 按 `speed` 投递 `leaudit.urgent` 或 `leaudit.normal` +5. worker 按 `runId` 查库 +6. bridge 下载文档与规则 YAML 到本地临时文件 +7. `NativeRunner` 构建原生 `AuditCtx` +8. `AuditService.audit(ctx)` 执行 +9. `StorageAdapter` 结果写回 `leaudit_*` + +--- + ## 二、前端调用现状(已验证) ### 2.1 上传 — `uploadDocumentToServer()` diff --git a/docs/接口/文档上传与评查接口.md b/docs/接口/文档上传与评查接口.md deleted file mode 100644 index 4034348..0000000 --- a/docs/接口/文档上传与评查接口.md +++ /dev/null @@ -1,692 +0,0 @@ -# 文档上传与评查接口 - -这份文档描述当前已经落地的文档上传、文档列表、自动评查、手动评查、状态查询、结果查询接口。 - -当前接口围绕以下业务语义设计: - -- 每次前端上传都会形成一个平台内部文档实例 -- 同名文档会尝试归入同一个版本组 -- 同名且内容相同: - - 不新建版本 - - `duplicateUpload=true` - - 如果 `autoRun=true`,仍然可以重新走一次评查流程 -- 同名但内容变化: - - 新建版本 - - 形成 `v2 / v3 / ...` -- 评查任务走 worker 异步执行 -- 队列只有两档: - - `urgent` - - `normal` - ---- - -## 1. 上传接口 - -### 路径 - -```http -POST /upload -``` - -### Content-Type - -```http -multipart/form-data -``` - -### 用途 - -- 上传文档 -- 创建或命中文档版本 -- 建立 `leaudit_documents / leaudit_document_files` -- 可选自动触发评查 - -### 请求参数 - -| 参数 | 类型 | 必填 | 说明 | -|---|---|---:|---| -| `file` | file | 是 | 上传文件 | -| `typeId` | int | 否 | 文档类型 ID,和 `typeCode` 二选一至少传一个 | -| `typeCode` | string | 否 | 文档类型编码,例如 `contract.sale` | -| `region` | string | 否 | 区域,默认 `default` | -| `fileRole` | string | 否 | 文件角色,默认 `primary` | -| `createdBy` | int | 否 | 上传用户 ID | -| `autoRun` | bool | 否 | 是否上传后自动触发评查,默认 `false` | -| `speed` | string | 否 | 执行速度档位:`normal` / `urgent`,默认 `normal` | - -### 版本匹配逻辑 - -上传时会先做版本候选匹配: - -1. 归一化文件名,得到 `normalized_name` -2. 按以下条件查找最新版本候选: - - `type_id` 相同 - - `region` 相同 - - `normalized_name` 相同 - - `is_latest_version = true` - - 主文件 `file_role = 'primary'` -3. 比较最新版本主文件的 `sha256` - -结果分三种: - -- 找不到候选 - - 新建版本组 - - 当前版本为 `v1` -- 找到候选且 `sha256` 相同 - - 视为重复上传 - - 不新建版本 - - `duplicateUpload=true` -- 找到候选但 `sha256` 不同 - - 新建版本 - - 当前版本为 `v2 / v3 / ...` - - 旧版本 `is_latest_version=false` - - 新版本 `is_latest_version=true` - -### 队列路由逻辑 - -- `speed=urgent` -> 投递 `leaudit.urgent` -- `speed=normal` -> 投递 `leaudit.normal` - -### 请求示例:普通上传,不自动评查 - -```bash -curl -X POST 'http://127.0.0.1:8096/api/upload' \ - -F 'file=@/path/to/合同.docx' \ - -F 'typeCode=contract.sale' \ - -F 'region=default' \ - -F 'fileRole=primary' \ - -F 'autoRun=false' \ - -F 'speed=normal' -``` - -### 返回示例:首次上传,命中 `v1` - -```json -{ - "code": 200, - "message": "ok", - "data": { - "documentId": 11, - "internalDocumentNo": 1777426812904262854, - "versionGroupKey": "4e02e455aa504cb9b75a254727f1bb4c", - "versionNo": 1, - "previousVersionId": null, - "rootVersionId": 11, - "duplicateUpload": false, - "fileId": 12, - "typeId": 9, - "typeCode": "contract.sale", - "region": "default", - "fileName": "版本归档验证合同.docx", - "ossUrl": "bdocs/default/contract.sale/2026/04/11/v1/primary__版本归档验证合同.docx", - "speed": "normal", - "processingStatus": "waiting", - "autoRunTriggered": false, - "run": null - } -} -``` - -### 返回示例:重复上传,不升版 - -```json -{ - "code": 200, - "message": "ok", - "data": { - "documentId": 11, - "internalDocumentNo": 1777426812904262854, - "versionGroupKey": "4e02e455aa504cb9b75a254727f1bb4c", - "versionNo": 1, - "previousVersionId": null, - "rootVersionId": 11, - "duplicateUpload": true, - "fileId": 12, - "typeId": 9, - "typeCode": "contract.sale", - "region": "default", - "fileName": "版本归档验证合同.docx", - "ossUrl": "bdocs/default/contract.sale/2026/04/11/v1/primary__版本归档验证合同.docx", - "speed": "normal", - "processingStatus": "waiting", - "autoRunTriggered": false, - "run": null - } -} -``` - -### 返回示例:同名但内容变化,自动形成 `v2` - -```json -{ - "code": 200, - "message": "ok", - "data": { - "documentId": 12, - "internalDocumentNo": 1777426813574315361, - "versionGroupKey": "4e02e455aa504cb9b75a254727f1bb4c", - "versionNo": 2, - "previousVersionId": 11, - "rootVersionId": 11, - "duplicateUpload": false, - "fileId": 13, - "typeId": 9, - "typeCode": "contract.sale", - "region": "default", - "fileName": "版本归档验证合同.docx", - "ossUrl": "bdocs/default/contract.sale/2026/04/12/v2/primary__版本归档验证合同.docx", - "speed": "normal", - "processingStatus": "waiting", - "autoRunTriggered": false, - "run": null - } -} -``` - -### 返回示例:重复上传但自动重新评查 - -```json -{ - "code": 200, - "message": "ok", - "data": { - "documentId": 13, - "internalDocumentNo": 1777427235286905027, - "versionGroupKey": "4e02e455aa504cb9b75a254727f1bb4c", - "versionNo": 3, - "previousVersionId": 12, - "rootVersionId": 11, - "duplicateUpload": true, - "fileId": 14, - "typeId": 9, - "typeCode": "contract.sale", - "region": "default", - "fileName": "版本归档验证合同.docx", - "ossUrl": "bdocs/default/contract.sale/2026/04/13/v3/primary__版本归档验证合同.docx", - "speed": "normal", - "processingStatus": "queued", - "autoRunTriggered": true, - "run": { - "runId": 13, - "documentId": 13, - "runNo": 2, - "documentFileId": 14, - "status": "queued", - "phase": "dispatch", - "resultStatus": null, - "ruleSetId": 29, - "ruleVersionId": 9, - "ruleTypeId": "contract.sale", - "rescueApplied": false, - "totalScore": null, - "passedCount": null, - "failedCount": null, - "skippedCount": null, - "startedAt": null, - "finishedAt": null - } - } -} -``` - ---- - -## 2. 文档列表接口 - -### 路径 - -```http -GET /documents/list -``` - -### 用途 - -- 返回文档主列表 -- 只返回每个版本组的最新版本 -- 每条记录附带历史版本摘要,前端可以直接做“展开历史版本” - -### 查询参数 - -| 参数 | 类型 | 必填 | 说明 | -|---|---|---:|---| -| `page` | int | 否 | 页码,从 `1` 开始,默认 `1` | -| `pageSize` | int | 否 | 每页数量,默认 `20`,最大 `100` | -| `keyword` | string | 否 | 文件名 / 归一化名称模糊搜索 | -| `typeCode` | string | 否 | 文档类型编码,例如 `contract.sale` | -| `region` | string | 否 | 区域过滤 | -| `processingStatus` | string | 否 | 文档处理状态过滤 | -| `resultStatus` | string | 否 | 最新 run 的结果状态过滤 | - -### 查询逻辑 - -- 主查询只看 `leaudit_documents.is_latest_version = true` -- 只关联主文件: - - `leaudit_document_files.is_active = true` - - `leaudit_document_files.file_role = 'primary'` -- 当前评查状态来自 `leaudit_audit_runs` -- 历史版本按 `version_group_key` 再查一次并挂到 `historyVersions` - -### 请求示例 - -```bash -curl 'http://127.0.0.1:8096/api/documents/list?page=1&pageSize=5' -``` - -### 带筛选请求示例 - -```bash -curl 'http://127.0.0.1:8096/api/documents/list?page=1&pageSize=2&keyword=版本归档&typeCode=contract.sale®ion=default' -``` - -### 返回示例 - -```json -{ - "code": 200, - "message": "ok", - "data": { - "total": 1, - "page": 1, - "pageSize": 2, - "totalPages": 1, - "documents": [ - { - "documentId": 13, - "internalDocumentNo": 1777427235286905027, - "versionGroupKey": "4e02e455aa504cb9b75a254727f1bb4c", - "versionNo": 3, - "rootVersionId": 11, - "previousVersionId": 12, - "typeId": 9, - "typeCode": "contract.sale", - "region": "default", - "normalizedName": "版本归档验证合同", - "fileId": 14, - "fileName": "版本归档验证合同.docx", - "fileExt": "docx", - "mimeType": "application/octet-stream", - "fileSize": 587279, - "ossUrl": "bdocs/default/contract.sale/2026/04/13/v3/primary__版本归档验证合同.docx", - "processingStatus": "completed", - "currentRunId": 13, - "runStatus": "completed", - "resultStatus": "review", - "totalScore": 92.0, - "passedCount": 25, - "failedCount": 3, - "skippedCount": 0, - "updatedAt": "2026-04-29T01:50:05.241397+00:00", - "hasHistory": true, - "totalVersions": 3, - "historyVersions": [ - { - "documentId": 12, - "fileId": 13, - "versionNo": 2, - "fileName": "版本归档验证合同.docx", - "fileExt": "docx", - "processingStatus": "waiting", - "runStatus": null, - "resultStatus": null, - "updatedAt": "2026-04-29T01:47:15.250697+00:00" - }, - { - "documentId": 11, - "fileId": 12, - "versionNo": 1, - "fileName": "版本归档验证合同.docx", - "fileExt": "docx", - "processingStatus": "waiting", - "runStatus": null, - "resultStatus": null, - "updatedAt": "2026-04-29T01:40:13.538839+00:00" - } - ] - } - ] - } -} -``` - -### 返回字段说明 - -| 字段 | 说明 | -|---|---| -| `documents[]` | 主列表,仅最新版本 | -| `versionGroupKey` | 同一版本链的归档组键 | -| `versionNo` | 当前版本号 | -| `rootVersionId` | 版本链根文档 ID | -| `previousVersionId` | 上一版本文档 ID | -| `hasHistory` | 是否存在历史版本 | -| `totalVersions` | 该版本组的总版本数 | -| `historyVersions[]` | 历史版本摘要,按 `versionNo DESC` 排序 | - ---- - -## 3. 手动触发评查 - -### 路径 - -```http -POST /audit/run -``` - -### 用途 - -- 对指定 `documentId` 手动触发一次新的评查 run -- 不改变文档版本 -- 只新增 `leaudit_audit_runs` - -### 请求体 - -```json -{ - "documentId": 13, - "ruleType": null, - "force": false, - "speed": "normal" -} -``` - -### 参数说明 - -| 字段 | 类型 | 必填 | 说明 | -|---|---|---:|---| -| `documentId` | int | 是 | 文档 ID | -| `ruleType` | string/null | 否 | 指定规则类型编码 | -| `force` | bool | 否 | 是否强制重跑 | -| `speed` | string | 否 | `normal` / `urgent` | - -### 请求示例 - -```bash -curl -X POST 'http://127.0.0.1:8096/api/audit/run' \ - -H 'Content-Type: application/json' \ - -d '{ - "documentId": 13, - "force": false, - "speed": "urgent" - }' -``` - -### 返回示例 - -```json -{ - "code": 200, - "message": "ok", - "data": { - "runId": 15, - "documentId": 13, - "runNo": 3, - "documentFileId": 14, - "status": "queued", - "phase": "dispatch", - "resultStatus": null, - "ruleSetId": 29, - "ruleVersionId": 9, - "ruleTypeId": "contract.sale", - "rescueApplied": false, - "totalScore": null, - "passedCount": null, - "failedCount": null, - "skippedCount": null, - "startedAt": null, - "finishedAt": null - } -} -``` - ---- - -## 4. 查询运行状态 - -### 路径 - -```http -GET /audit/run/{runId} -``` - -### 用途 - -- 查询 run 当前状态 -- 适合前端轮询 - -### 状态说明 - -常见状态: - -- `queued` -- `running` -- `completed` -- `failed` - -常见阶段: - -- `dispatch` -- `prepare` -- `ocr` -- `extract` -- `evaluate` -- `rescue` -- `persist` -- `executed` - -### 请求示例 - -```bash -curl 'http://127.0.0.1:8096/api/audit/run/11' -``` - -### 返回示例 - -```json -{ - "code": 200, - "message": "ok", - "data": { - "runId": 11, - "documentId": 10, - "runNo": 1, - "documentFileId": 11, - "status": "completed", - "phase": "executed", - "resultStatus": "review", - "ruleSetId": 29, - "ruleVersionId": 9, - "ruleTypeId": "contract.sale", - "rescueApplied": true, - "totalScore": 89.0, - "passedCount": 24, - "failedCount": 4, - "skippedCount": 0, - "startedAt": "2026-04-28T19:01:01.766352+08:00", - "finishedAt": "2026-04-28T19:03:11.044894+08:00" - } -} -``` - ---- - -## 5. 查询评查结果 - -### 路径 - -```http -GET /audit/result/{runId} -``` - -### 用途 - -- 查询本次 run 的完整结果 -- 包括: - - 规则结果 - - 抽取字段 - - 运行错误 - - rescue 结果 - - metrics - - artifacts - -### 请求示例 - -```bash -curl 'http://127.0.0.1:8096/api/audit/result/11' -``` - -### 返回结构说明 - -顶层字段: - -| 字段 | 说明 | -|---|---| -| `runId` | 运行 ID | -| `documentId` | 文档 ID | -| `documentFileId` | 本次锁定的文件 ID | -| `status` | 运行状态 | -| `totalScore` | 总分 | -| `passedCount` | 通过数 | -| `failedCount` | 失败数 | -| `skippedCount` | 跳过数 | -| `phase` | 当前阶段 | -| `resultStatus` | 总体结果 | -| `rescueApplied` | 是否执行 rescue | -| `ruleSetId` | 规则集 ID | -| `ruleVersionId` | 规则版本 ID | -| `startedAt` / `finishedAt` | 起止时间 | -| `rules` | 规则结果列表 | -| `fields` | 抽取字段列表 | -| `errors` | 错误列表 | -| `rescueOutcomes` | 补救结果列表 | -| `metrics` | 阶段指标 | -| `artifacts` | 产物列表 | - -### 返回示例(节选) - -```json -{ - "code": 200, - "message": "ok", - "data": { - "runId": 11, - "documentId": 10, - "documentFileId": 11, - "status": "completed", - "totalScore": 89.0, - "passedCount": 24, - "failedCount": 4, - "skippedCount": 0, - "phase": "executed", - "resultStatus": "review", - "rescueApplied": true, - "ruleSetId": 29, - "ruleVersionId": 9, - "startedAt": "2026-04-28T19:01:01.766352+08:00", - "finishedAt": "2026-04-28T19:03:11.044894+08:00", - "rules": [ - { - "ruleId": "MM-SALE-012", - "ruleName": "甲方信用代码校验", - "passed": false, - "status": "executed", - "risk": "medium", - "score": 3.0, - "failMessage": "甲方统一社会信用代码校验位错误" - } - ], - "fields": [ - { - "fieldName": "合同名称", - "valueText": "智慧法务平台建设采购项目合同", - "confidence": 0.9991 - } - ], - "rescueOutcomes": [ - { - "ruleId": "MM-SALE-012", - "status": "final_fail", - "finalStatus": "review", - "requiresHumanReview": true, - "failureReason": "Agent (4 iter, requires_human): token_budget_exhausted" - } - ], - "metrics": { - "ocrSeconds": 79.06, - "extractSeconds": 11.87, - "evaluateSeconds": 9.9, - "totalSeconds": 100.83, - "pageCount": 2, - "fieldCount": 35, - "ruleCount": 28, - "rescueRuleCount": 5, - "artifactCount": 8 - }, - "artifacts": [ - { - "artifactType": "ocr_json", - "fileName": "ocr_result.json", - "fileExt": "json", - "mimeType": "application/json" - } - ] - } -} -``` - ---- - -## 6. worker 日志怎么看 - -worker 关键日志已经做了可读化,重点看这两类: - -### 投递日志 - -```text -run_id=13 已投递到 worker 队列: queue=leaudit.normal, speed=normal, task_id=... -``` - -### 执行日志 - -```text -run_id=13 worker开始执行: queue=leaudit.normal, speed=normal, filename=版本归档验证合同.docx -``` - -结合状态查询接口可以快速判断: - -- 是否已经成功投递 -- 是否已被 worker 消费 -- 跑的是 `urgent` 还是 `normal` - ---- - -## 7. 前端建议接法 - -### 文档上传页 - -1. 调 `POST /upload` -2. 读取返回: - - `documentId` - - `versionGroupKey` - - `versionNo` - - `duplicateUpload` - - `run` - -### 自动评查场景 - -如果 `autoRun=true` 且返回里 `run != null`: - -1. 取 `run.runId` -2. 轮询 `GET /audit/run/{runId}` -3. `status=completed/failed` 后停止轮询 -4. 再调 `GET /audit/result/{runId}` - -### 列表页 - -列表页建议默认只展示: - -- `is_latest_version = true` 的 document - -点击某条后,再按: - -- `versionGroupKey` - -展开其历史版本。 diff --git a/docs/接口/文档类型与评查组关联方案.md b/docs/接口/文档类型与评查组关联方案.md deleted file mode 100644 index fbf1456..0000000 --- a/docs/接口/文档类型与评查组关联方案.md +++ /dev/null @@ -1,127 +0,0 @@ -# 文档类型与评查组关联 — 老项目分析 & 新方案 - -## 一、老项目模型(docauditai) - -### 三层间接绑定 - -``` -DocumentType → TopLevelGroup(s) → ChildGroups → EvaluationPoints - │ │ │ │ - │ evaluation_ │ pid=0 │ pid={top} │ evaluation_ - │ point_groups │ │ │ point_groups_id - │ _ids (JSONB) │ │ │ -``` - -- `document_types.evaluation_point_groups_ids` — JSONB 数组,存顶层组 ID,如 `[3]` 或 `[1,5]` -- `evaluation_point_groups` — 两级树:`pid=0` 是顶层组,"pid>0" 是子组 -- `evaluation_points` — 属于子组,含 `extraction_config`(提取规则)和 `evaluation_config`(评查规则),按 `area` + `document_attribute_type` 过滤 - -### 运行时解析流程 - -``` -doc_type_code - → 查 document_types.evaluation_point_groups_ids[0] - → 查 evaluation_point_groups WHERE pid = {top_group_id} - → 查 evaluation_points WHERE evaluation_point_groups_id = {child_id} - → 按 area + attribute_type 过滤 - → 返回最终评查点列表 -``` - ---- - -## 二、新平台模型(leaudit-platform) - -### 直接绑定 - -``` -DocumentType ──→ RuleTypeBinding ──→ RuleSet ──→ RuleVersions -``` - -**`leaudit_rule_type_bindings`**: -| 字段 | 说明 | -|------|------| -| `doc_type_id` | FK → leaudit_document_types.id | -| `rule_set_id` | FK → leaudit_rule_sets.id | -| `binding_mode` | "explicit"(一对一)/ "inherit"(继承) | -| `region` | 地区隔离 | -| `priority` | 同一 doc_type 多个 rule_set 时的优先级 | - -**运行时**(已在 auditServiceImpl.py 实现): -```sql -SELECT b.rule_set_id, rs.current_version_id -FROM leaudit_rule_type_bindings b -JOIN leaudit_rule_sets rs ON rs.id = b.rule_set_id -WHERE b.doc_type_id = :doc_type_id - AND b.region = :region - AND b.is_active = true -ORDER BY b.priority DESC -``` - -### 与老模型的关键差异 - -| | 老 | 新 | -|---|---|---| -| 绑定粒度 | 文档类型 → 顶层组(间接) | 文档类型 → 规则集(直接) | -| 评查点层级 | 三级(类型→组→子组→点) | 两级(类型→规则集→版本) | -| 地区隔离 | evaluation_points.area | rule_type_bindings.region | -| 规则版本 | 无版本概念 | 规则集 + 版本管理 | - -实际上新模型更简洁:老系统里的"顶层组"≈新系统的"规则集",老系统里的"子组+评查点"≈新系统的"规则版本内容"。新系统去掉了中间的子组层级。 - ---- - -## 三、已落地 vs 待落地 - -### ✅ 已落地 - -| 项 | 说明 | -|----|------| -| `leaudit_document_types` 表 | 有 20 条种子数据,含 code/name/entry_module_id/prompt_config | -| `leaudit_rule_sets` 表 | 规则集 + 版本管理 | -| `leaudit_rule_type_bindings` 表 | 20 条绑定记录,已全部配对 | -| 运行时绑定解析 | auditServiceImpl.Run() 中查 bindings → 找 rule_set + version | -| `GET /api/document-types` | 刚加的,返回 id/name/code | - -### ❌ 待落地 - -| 优先 | 项 | 说明 | -|------|----|------| -| 高 | 文档类型 CRUD 后端 | POST/PUT/DELETE /api/document-types | -| 高 | 规则绑定 CRUD 后端 | 在文档类型编辑中配 rule_set_id(s) | -| 高 | 前端文档类型管理页 | 对应旧 `document-types._index.tsx` | -| 中 | 规则绑定前端 UI | 文档类型编辑页中选规则集的控件 | -| 中 | 前端规则集选择器 | 对接 `/api/rule-sets` 或现有规则接口 | -| 低 | prompt_config 编辑 | 每个文档类型的提示词模板配置 | - ---- - -## 四、推荐接入方案 - -### 第 1 步:文档类型 CRUD 后端 - -``` -GET /api/document-types ← 已实现 -POST /api/document-types ← 新增 -PUT /api/document-types/{id} ← 新增 -DELETE /api/document-types/{id} ← 新增 -``` - -字段:`code`, `name`, `description`, `entry_module_id`, `prompt_config`, `rule_set_ids`(一次性处理绑定) - -### 第 2 步:规则绑定 CRUD - -两种方案: -- **方案 A**(推荐):绑定内嵌到文档类型接口。创建/更新文档类型时传 `rule_set_ids: [21, 31]`,后端自动维护 `leaudit_rule_type_bindings`。 -- **方案 B**:独立绑定接口 `POST/DELETE /api/rule-type-bindings`。 - -推荐方案 A,因为绑定是文档类型的属性,一起管理更自然。 - -### 第 3 步:前端文档类型管理页 - -参考旧前端 `document-types._index.tsx` / `document-types.new.tsx`,用新 API 重写: -- 列表页:表格展示 code/name/entry_module/规则集数量 -- 新建/编辑页:表单填 code/name/entry_module,多选规则集 - -### 第 4 步:上传页关联 - -上传时将 `typeCode` 写入 `leaudit_documents.type_id` → 运行时自动找绑定 → 加载规则集 → 执行评查。整个链路已通,只需确保文档类型数据存在。 diff --git a/docs/接口/新系统版_documents_list接口.md b/docs/接口/新系统版_documents_list接口.md deleted file mode 100644 index a24c1d0..0000000 --- a/docs/接口/新系统版_documents_list接口.md +++ /dev/null @@ -1,307 +0,0 @@ -# 新系统版 `documents/list` 接口 - -这份文档专门说明当前 `leaudit-platform` 里已经落地的“新系统版文档列表接口”。 - -目标很明确: - -- 前端文档列表只看“最新版本” -- 同名文档的历史版本直接归到同一个版本链 -- 列表接口直接返回历史版本摘要,前端不用自己再拼版本关系 - ---- - -## 1. 接口路径 - -```http -GET /api/documents/list -``` - ---- - -## 2. 当前接口语义 - -这个接口不是“把所有上传记录平铺出来”。 - -它的语义是: - -- 每个 `version_group_key` 只返回一条“最新版本文档” -- 这条最新版本文档下面附带 `historyVersions` -- `historyVersions` 里放的是同组下更老的版本摘要 - -也就是说,前端主列表看到的是: - -- 当前版本 -- 是否有历史版本 -- 一共有多少版本 -- 历史版本有哪些 - ---- - -## 3. 请求参数 - -| 参数 | 类型 | 必填 | 说明 | -|---|---|---:|---| -| `page` | int | 否 | 页码,从 `1` 开始,默认 `1` | -| `pageSize` | int | 否 | 每页数量,默认 `20`,最大 `100` | -| `keyword` | string | 否 | 按文件名或归一化名称模糊搜索 | -| `typeCode` | string | 否 | 文档类型编码,例如 `contract.sale` | -| `region` | string | 否 | 区域 | -| `processingStatus` | string | 否 | 文档处理状态 | -| `resultStatus` | string | 否 | 最新 run 的结果状态 | - -请求示例: - -```bash -curl 'http://127.0.0.1:8096/api/documents/list?page=1&pageSize=5' -``` - -带筛选示例: - -```bash -curl 'http://127.0.0.1:8096/api/documents/list?page=1&pageSize=2&keyword=版本归档&typeCode=contract.sale®ion=default' -``` - ---- - -## 4. 返回结构 - -返回模型是分页结构: - -```json -{ - "code": 200, - "message": "ok", - "data": { - "total": 1, - "page": 1, - "pageSize": 20, - "totalPages": 1, - "documents": [] - } -} -``` - -其中 `documents[]` 的单条结构核心字段如下: - -| 字段 | 说明 | -|---|---| -| `documentId` | 当前最新版本文档 ID | -| `internalDocumentNo` | 平台内部追踪号 | -| `versionGroupKey` | 版本归档组键 | -| `versionNo` | 当前版本号 | -| `rootVersionId` | 版本链根文档 ID | -| `previousVersionId` | 上一版本文档 ID | -| `typeId` | 文档类型 ID | -| `typeCode` | 文档类型编码 | -| `region` | 区域 | -| `normalizedName` | 归一化后的名称 | -| `fileId` | 当前主文件 ID | -| `fileName` | 文件名 | -| `fileExt` | 文件扩展名 | -| `mimeType` | MIME 类型 | -| `fileSize` | 文件大小 | -| `ossUrl` | 对象存储路径 | -| `processingStatus` | 文档处理状态 | -| `currentRunId` | 当前 run ID | -| `runStatus` | 当前 run 状态 | -| `resultStatus` | 当前 run 结果状态 | -| `totalScore` | 总分 | -| `passedCount` | 通过数 | -| `failedCount` | 失败数 | -| `skippedCount` | 跳过数 | -| `updatedAt` | 更新时间 | -| `hasHistory` | 是否有历史版本 | -| `totalVersions` | 总版本数 | -| `historyVersions` | 历史版本摘要列表 | - -`historyVersions[]` 结构: - -| 字段 | 说明 | -|---|---| -| `documentId` | 历史版本文档 ID | -| `fileId` | 历史版本文件 ID | -| `versionNo` | 历史版本号 | -| `fileName` | 文件名 | -| `fileExt` | 文件扩展名 | -| `processingStatus` | 处理状态 | -| `runStatus` | 运行状态 | -| `resultStatus` | 结果状态 | -| `updatedAt` | 更新时间 | - ---- - -## 5. SQL 逻辑 - -### 5.1 主列表计数 SQL - -```sql -SELECT COUNT(*) -FROM leaudit_documents d -JOIN leaudit_document_files f - ON f.document_id = d.id -LEFT JOIN leaudit_document_types dt - ON dt.id = d.type_id -LEFT JOIN leaudit_audit_runs ar - ON ar.id = d.current_run_id -WHERE d.is_latest_version = true - AND d.deleted_at IS NULL - AND f.is_active = true - AND f.file_role = 'primary' - -- 可选过滤: - -- AND (f.file_name ILIKE :keyword OR d.normalized_name ILIKE :keyword) - -- AND dt.code = :type_code - -- AND d.region = :region - -- AND d.processing_status = :processing_status - -- AND ar.result_status = :result_status -``` - -### 5.2 主列表分页 SQL - -```sql -SELECT - d.id AS document_id, - d.biz_document_id AS internal_document_no, - d.version_group_key, - d.version_no, - d.root_version_id, - d.previous_version_id, - d.type_id, - dt.code AS type_code, - d.region, - d.normalized_name, - d.processing_status, - d.current_run_id, - d.updated_at, - f.id AS file_id, - f.file_name, - f.file_ext, - f.mime_type, - f.file_size, - f.oss_url, - ar.status AS run_status, - ar.result_status, - ar.total_score, - ar.passed_count, - ar.failed_count, - ar.skipped_count, - vc.total_versions, - COALESCE(vc.total_versions, 1) > 1 AS has_history -FROM leaudit_documents d -JOIN leaudit_document_files f - ON f.document_id = d.id -LEFT JOIN leaudit_document_types dt - ON dt.id = d.type_id -LEFT JOIN leaudit_audit_runs ar - ON ar.id = d.current_run_id -LEFT JOIN ( - SELECT version_group_key, COUNT(*) AS total_versions - FROM leaudit_documents - WHERE deleted_at IS NULL - GROUP BY version_group_key -) vc - ON vc.version_group_key = d.version_group_key -WHERE d.is_latest_version = true - AND d.deleted_at IS NULL - AND f.is_active = true - AND f.file_role = 'primary' -ORDER BY d.updated_at DESC, d.id DESC -LIMIT :limit OFFSET :offset -``` - -### 5.3 历史版本摘要 SQL - -```sql -SELECT - d.version_group_key, - d.id AS document_id, - d.version_no, - d.processing_status, - d.updated_at, - f.id AS file_id, - f.file_name, - f.file_ext, - ar.status AS run_status, - ar.result_status -FROM leaudit_documents d -JOIN leaudit_document_files f - ON f.document_id = d.id - AND f.is_active = true - AND f.file_role = 'primary' -LEFT JOIN leaudit_audit_runs ar - ON ar.id = d.current_run_id -WHERE d.version_group_key = ANY(:group_keys) - AND d.is_latest_version = false - AND d.deleted_at IS NULL -ORDER BY d.version_group_key, d.version_no DESC, d.id DESC -``` - ---- - -## 6. 为什么新系统要这么做 - -因为现在我们已经不是老系统那种“文档记录 + 旁路版本信息”的模式了。 - -当前新系统已经有真正的版本链字段: - -- `version_group_key` -- `version_no` -- `previous_version_id` -- `root_version_id` -- `is_latest_version` -- `normalized_name` - -所以列表天然应该是: - -- 主列表 = 最新版本 -- 展开项 = 历史版本 - -而不是把所有版本平铺在一个列表里。 - ---- - -## 7. 当前代码落点 - -实现代码在这些文件: - -- 路由:`fastapi_modules/fastapi_leaudit/controllers/documentController.py` -- 服务接口:`fastapi_modules/fastapi_leaudit/services/documentService.py` -- VO:`fastapi_modules/fastapi_leaudit/domian/vo/documentVo.py` -- 具体实现:`fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py` - ---- - -## 8. 当前验证结果 - -已经实际验证通过: - -```bash -curl 'http://127.0.0.1:8096/api/documents/list?page=1&pageSize=5' -``` - -以及: - -```bash -curl 'http://127.0.0.1:8096/api/documents/list?page=1&pageSize=2&keyword=版本归档&typeCode=contract.sale®ion=default' -``` - -验证结论: - -- 接口正常返回 -- 主列表只返回最新版本 -- `historyVersions` 能正确带出历史版本摘要 -- 已验证版本组 `4e02e455aa504cb9b75a254727f1bb4c` - - `documentId=13` 是当前最新 `v3` - - `documentId=12` 是 `v2` - - `documentId=11` 是 `v1` - ---- - -## 9. 下一步建议 - -如果后面前端需要更完整的“版本展开页”,再补一个: - -```http -GET /api/documents/{documentId}/versions -``` - -但当前列表页场景下,`/documents/list` 已经够用了。 diff --git a/docs/接口/用户权限与权限点清单.md b/docs/接口/用户权限与权限点清单.md index 8952ad1..48c728e 100644 --- a/docs/接口/用户权限与权限点清单.md +++ b/docs/接口/用户权限与权限点清单.md @@ -412,7 +412,7 @@ SQL 里初始化了以下核心菜单: 如果你后面忘了这套体系总原则,看这里: - 总设计:`docs/用户与地区权限完整设计方案.md` -- 老系统分析:`docs/老系统_docauditai_用户权限架构深度分析.md` +- 导航说明:`docs/权限与地区隔离文档导航.md` - 本文:`docs/接口/用户权限与权限点清单.md` - SQL:`docs/接口/用户权限初始化SQL.sql` diff --git a/docs/接口/用户权限初始化SQL.sql b/docs/接口/用户权限初始化SQL.sql index 80196c9..cc61c29 100644 --- a/docs/接口/用户权限初始化SQL.sql +++ b/docs/接口/用户权限初始化SQL.sql @@ -33,7 +33,7 @@ WITH upsert_routes AS ( ('/documents', '文档管理', 'Layout', NULL, 10, TRUE, TRUE, '{"icon":"files"}'::jsonb, NOW(), NOW()), ('/documents/list', '文档列表', 'documents/list', NULL, 11, TRUE, TRUE, '{"icon":"table"}'::jsonb, NOW(), NOW()), ('/rules', '规则管理', 'Layout', NULL, 20, TRUE, TRUE, '{"icon":"rule"}'::jsonb, NOW(), NOW()), - ('/rules/sets', '规则集管理', 'rules/sets', NULL, 21, TRUE, TRUE, '{"icon":"yaml"}'::jsonb, NOW(), NOW()), + ('/rules/sets', '规则管理', 'rules/sets', NULL, 21, TRUE, TRUE, '{"icon":"yaml"}'::jsonb, NOW(), NOW()), ('/audit', '评查任务', 'Layout', NULL, 30, TRUE, TRUE, '{"icon":"audit"}'::jsonb, NOW(), NOW()), ('/audit/runs', '评查运行', 'audit/runs', NULL, 31, TRUE, TRUE, '{"icon":"history"}'::jsonb, NOW(), NOW()), ('/system', '系统管理', 'Layout', NULL, 90, TRUE, TRUE, '{"icon":"setting"}'::jsonb, NOW(), NOW()), diff --git a/docs/接口/系统设置入口恢复说明.md b/docs/接口/系统设置入口恢复说明.md deleted file mode 100644 index 9cc9ec7..0000000 --- a/docs/接口/系统设置入口恢复说明.md +++ /dev/null @@ -1,133 +0,0 @@ -# 系统设置入口恢复说明 - -## 本次恢复范围 - -本轮不是把整个旧“系统设置域”一次性全部接回,而是先恢复最关键的两块: - -- `入口模块管理`:`/entry-modules` -- `角色权限管理`:`/role-permissions` - -同时恢复首页右上角 `系统设置` 入口,但当前只会进入上述两个已经承接的新能力。 - -## 当前策略 - -### 1. 首页与菜单 - -- 后端 `/api/rbac/user/routes` 已重新返回 `/settings` -- `/settings` 当前子路由只保留: - - `/entry-modules` - - `/role-permissions` -- `config-lists / document-types / prompts` 仍未重新放开 - -### 2. 入口模块管理 - -已承接后端接口: - -- `GET /api/v3/entry-modules` -- `GET /api/v3/entry-modules/{id}` -- `POST /api/v3/entry-modules` -- `PUT /api/v3/entry-modules/{id}` -- `DELETE /api/v3/entry-modules/{id}` -- `POST /api/v3/entry-modules/{id}/image` - -数据源: - -- 表:`leaudit_entry_modules` -- 图标:OSS `leaudit` 桶 - -说明: - -- 前端展示字段 `path` 现在对应数据库 `icon_path` -- 前端编辑页面中的跳转路径仍走数据库 `path` - -### 3. 角色权限管理 - -已承接后端接口: - -- `GET /api/v3/rbac/roles` -- `POST /api/v3/rbac/roles` -- `PUT /api/v3/rbac/roles/{roleId}` -- `DELETE /api/v3/rbac/roles/{roleId}` -- `GET /api/v3/rbac/users` -- `POST /api/v3/rbac/users/{userId}/roles` -- `DELETE /api/v3/rbac/users/{userId}/roles/{roleId}` -- `GET /api/v3/rbac/users/{userId}/roles` -- `GET /api/rbac/roles/{roleId}/routes` -- `PUT /api/rbac/roles/{roleId}/routes` -- `GET /api/v3/rbac/role-permissions` -- `POST /api/v3/rbac/role-permissions` -- `GET /api/v3/routes` -- `GET /api/v3/routes/{routeId}/permissions` - -数据源: - -- `roles` -- `sso_users` -- `user_role` -- `sys_routes` -- `role_route` -- `permissions` -- `role_permissions` - -## 自动补种策略 - -为避免当前数据库里的 `sys_routes / permissions` 与现前端路由结构不一致,这次后端在 RBAC 管理接口里增加了“按需补种”: - -- 自动补齐最小可用路由: - - `/home` - - `/chat-with-llm` - - `/files` - - `/files/upload` - - `/documents` - - `/settings` - - `/entry-modules` - - `/role-permissions` -- 自动补齐入口模块管理、角色权限管理所需的权限定义 - -这样做的目的,是先把“入口模块 + 权限架构”真正跑通,不再依赖旧系统那套不匹配的 `sys_routes` 种子。 - -## 首页入口联动提醒 - -入口模块管理恢复后,首页入口是否显示,不只取决于 `leaudit_entry_modules` 本身,还取决于: - -- 入口模块 `path` -- `sys_routes` -- `role_route` - -例如这次的真实案例: - -- `内部公文` 入口模块配置存在 -- 其跳转路径是 `/home` -- 但 `provincial_admin` 之前没有 `/home` 的 `role_route` 授权 -- 所以首页接口不会返回该入口 - -已处理动作: - -- 2026-04-29 已补充 `provincial_admin -> /home` 路由授权 - -后续如果首页入口“配置了却不显示”,先不要只盯 `leaudit_entry_modules`,要连同下面三层一起查: - -1. `leaudit_entry_modules.path` -2. `sys_routes.route_path` -3. `role_route(role_id, route_id, status)` - -## 暂未恢复项 - -以下仍未在本轮开放: - -- `config-lists` -- `document-types` -- `prompts` - -原因: - -- 当前数据库里没有旧 `configurations` 表 -- `document-types / prompts` 仍依赖另一批尚未承接完的后台能力 - -## 下一步建议 - -后续按这个顺序继续最稳: - -1. 先联调验证 `入口模块管理` 与 `角色权限管理` -2. 再补 `config-lists` 新表与新接口 -3. 最后恢复 `document-types / prompts` diff --git a/docs/接口/老用户迁移脚本说明.md b/docs/接口/老用户迁移脚本说明.md new file mode 100644 index 0000000..3cbdb51 --- /dev/null +++ b/docs/接口/老用户迁移脚本说明.md @@ -0,0 +1,190 @@ +# 老用户迁移脚本说明 + +这份说明对应两个文件: + +- `scripts/migrate_legacy_users.py` +- `scripts/user_rbac_migration_audit.sql` + +目标很明确: + +- 把老系统 `docauditai` 的用户主数据迁到新系统 `leaudit_platform` +- 当前先迁:`sso_users` + `user_role` +- 新系统角色基础数据使用当前新库已经初始化好的 `roles` + +--- + +## 1. 为什么现在先这样迁 + +当前真实情况是: + +- 老库 `sso_users = 4106` +- 老库 `user_role = 11` +- 也就是大多数老用户没有显式角色记录 + +所以这次迁移不能只“复制 user_role”,还必须带一条默认规则: + +- 老用户没有角色时,自动落到 `common` + +这正是 `scripts/migrate_legacy_users.py` 已经处理好的逻辑。 + +--- + +## 2. 脚本会做什么 + +脚本默认只做 dry-run,不写入。 + +它会: + +1. 连接老库 `docauditai` +2. 连接新库 `leaudit_platform` +3. 读取老库 `sso_users` +4. 读取老库 `user_role + roles` +5. 按新系统允许的角色集合收口: + - `provincial_admin` + - `admin` + - `common` + - `super_admin` +6. 如果老用户没有角色,则自动指定 `common` +7. 迁移 `sso_users` +8. 迁移 `user_role` +9. 重置新库 `sso_users.id` 序列 + +--- + +## 3. 脚本的关键规则 + +## 3.1 保留老用户 ID + +脚本会尽量保留老库 `sso_users.id` 写入新库。 + +这样做的好处是: + +- 后续如果业务表开始引用用户 ID,语义更稳定 +- 后台排查时新旧库用户编号一致,方便核对 + +## 3.2 地区字段只认 `area` + +迁移时: + +- 会把老库 `sso_users.area` 带到新库 +- 并做基础 trim / 别名归一化 + +## 3.3 无角色用户自动补 `common` + +这是当前最重要的迁移策略。 + +因为老库 4106 个用户里,4098 个没有显式 `user_role`。 + +如果不补: + +- 新系统大量用户会迁过去但无法使用 + +## 3.4 幂等设计 + +脚本不是只适合跑一次。 + +它支持: + +- `sub` 已存在时更新用户资料 +- `user_role` 已存在时跳过重复插入 + +所以可以重复执行。 + +--- + +## 4. 使用方式 + +## 4.1 先做 dry-run + +```bash +python3 scripts/migrate_legacy_users.py +``` + +我已经实际跑过 dry-run,当前输出结果是: + +- `legacy_users_total: 4106` +- `default_common_role: 4098` +- `insert_user: 4106` +- `admin: 4` +- `common: 4101` +- `provincial_admin: 1` +- `id_conflicts: 0` + +这说明: + +- 迁移逻辑当前是通的 +- 角色映射结果和老库真实分布一致 +- 当前没有发现 ID 冲突 + +## 4.2 正式执行迁移 + +```bash +python3 scripts/migrate_legacy_users.py --apply +``` + +--- + +## 5. 迁移前审计 SQL + +如果你想先单独跑审计,用这个: + +```bash +psql -h 172.16.0.81 -p 54302 -U docauditai_admin -d docauditai -f scripts/user_rbac_migration_audit.sql +``` + +它会检查: + +- 用户总数 +- 地区分布 +- 重复 `sub` +- 重复 `username` +- 空地区用户 +- 无角色用户 +- 脏 `user_role` +- 脏 `role_permissions` +- 脏 `role_route` + +--- + +## 6. 当前不迁什么 + +这版脚本当前不迁: + +- `permissions` +- `role_permissions` +- `sys_routes` +- `role_route` + +原因不是不能迁,而是: + +- 新系统已经有自己当前阶段的 seed 权限集 +- 当前最急的是先把“用户能登录、能识别角色、能拿到地区”打通 + +也就是说,这一版脚本优先解决: + +- 用户主数据迁入 +- 默认角色补齐 +- 登录可用 + +--- + +## 7. 推荐执行顺序 + +建议严格按这个顺序: + +1. 确认 `scripts/user_rbac_schema_patch.sql` 已执行 +2. 确认 `scripts/user_rbac_seed.sql` 已执行 +3. 先跑 `scripts/user_rbac_migration_audit.sql` +4. 再跑 `python3 scripts/migrate_legacy_users.py` 做 dry-run +5. 最后跑 `python3 scripts/migrate_legacy_users.py --apply` + +--- + +## 8. 下一步衔接 + +老用户迁进去之后,下一步应该继续做: + +1. 用新迁入用户实际验证 `/auth/login` 和 `/auth/me` +2. 把文档列表、评查结果接口接上 `ALL / DEPT / SELF` 数据范围控制 +3. 再决定是否迁移老库的完整 `permissions / role_permissions / sys_routes / role_route` + diff --git a/docs/接口/评查点分组目标结构与迁移方案.md b/docs/接口/评查点分组目标结构与迁移方案.md new file mode 100644 index 0000000..3f7f00e --- /dev/null +++ b/docs/接口/评查点分组目标结构与迁移方案.md @@ -0,0 +1,336 @@ +# 评查点分组、文档类型与规则集目标结构与迁移方案 + +更新时间:2026-05-03 + +## 1. 产品确认后的最终口径 + +最终目标结构统一为: + +- 一级分组 = 业务大类 + - 例如:`合同`、`卷宗` + - 后续若出现新的入口业务,也允许继续新增一级分组 +- 二级分组 = 该业务大类下的具体业务类型 + - 例如合同下:`建设工程合同`、`买卖合同` + - 例如卷宗下:`处罚-一般程序`、`许可-停业办理` +- 规则集 = 挂在二级分组下 +- 入口模块 = 绑定一级分组 + +补充关系口径: + +- 文档类型 = 具体业务类型主数据 +- 文档类型页 = 主数据维护与汇总展示 +- 评查点分组页 = 实际运行绑定维护页 + +这套结构对应的运行链路是: + +- 入口模块 +- 一级分组(业务大类) +- 二级分组(具体业务类型) +- 规则集 +- 规则版本 + +其中: + +- 入口模块:决定当前页面进入的是哪条业务线 +- 一级分组:承接该业务线的一级分类容器 +- 二级分组:决定这次上传/评查具体命中的业务类型 +- 规则集:决定跑哪组规则 +- 规则版本:决定跑规则集的哪一版 + +也就是说,当前正式口径是: + +`入口模块 -> 一级分组(业务大类) -> 二级分组(具体业务类型) -> 规则集 -> 规则版本` + +文档类型在这条链路中的位置是: + +- 作为“具体业务类型主数据” +- 通过 `document_type_id` 与二级分组形成稳定对应关系 +- 用于上传页、规则页、文档列表等业务页面识别当前业务类型 + +## 1.1 老系统与新系统的关系 + +### 老系统(docauditai) + +老系统更接近: + +```text +DocumentType -> TopLevelGroup(s) -> ChildGroups -> EvaluationPoints +``` + +特点: + +- `document_types.evaluation_point_groups_ids` 绑定顶层组 +- `evaluation_point_groups` 是两级树 +- `evaluation_points` 实际挂在子组下 +- 运行时还会按 `area + document_attribute_type` 过滤 + +### 当前新平台(leaudit-platform) + +当前新平台已经同时存在两套表达: + +1. 规则执行主链 + - `DocumentType -> RuleTypeBinding -> RuleSet -> RuleVersion` +2. 分组管理主链 + - `EntryModule -> Level1Group -> Level2Group -> RuleSet` + +这就是大家容易混淆的根源。 + +### 当前统一理解 + +应统一理解为: + +- 文档类型是业务类型主数据 +- 二级分组是运行时具体命中的业务类型节点 +- 规则集挂在二级分组下 +- `leaudit_rule_type_bindings` 继续承担执行链快速查找规则集的职责 + +也就是说: + +- 文档类型页负责“主数据” +- 分组页负责“运行绑定” +- `rule_type_bindings` 负责“执行收口” + +## 2. 现在系统为什么会让人混淆 + +当前系统的过渡态更接近: + +- 一级分组 = 具体文档类型 + - 如:`建设工程合同`、`停业办理` +- 二级分组 = 该文档类型下的默认子类型 + - 如:`通用` + +这与产品最新口径不一致,主要有三个问题: + +1. 一级分组被错误地做成了“具体文档类型”,而不是“业务大类容器”。 +2. 上传页看到的“子类型”其实只是默认二级分组,业务人员会误以为配置还没做好。 +3. 入口模块与一级分组之间缺少明确绑定语义,后续无法优雅支持新入口。 + +## 3. 目标树形样例 + +### 3.1 合同类 + +```text +合同(一级 = 业务大类) +├── 建设工程合同(二级 = 具体业务类型) +│ ├── 规则集:建设工程合同-正文审查 +│ └── 规则集:建设工程合同-签署审查 +├── 买卖合同(二级 = 具体业务类型) +│ └── 规则集:买卖合同-通用审查 +└── 委托合同(二级 = 具体业务类型) + └── 规则集:委托合同-通用审查 +``` + +### 3.2 卷宗类 + +```text +卷宗(一级 = 业务大类) +├── 处罚-一般程序(二级 = 具体业务类型) +│ └── 规则集:行政处罚-一般程序审查 +├── 处罚-简易程序(二级 = 具体业务类型) +│ └── 规则集:行政处罚-简易程序审查 +├── 许可-停业办理(二级 = 具体业务类型) +│ └── 规则集:行政许可-停业办理审查 +└── 许可-新办办理(二级 = 具体业务类型) + └── 规则集:行政许可-新办办理审查 +``` + +### 3.3 将来新增入口 + +```text +内部公文(一级 = 业务大类) +├── 请示类公文(二级) +├── 通知类公文(二级) +└── 纪要类公文(二级) +``` + +即: + +- 不需要预先写死“只有合同、卷宗” +- 后续新增业务时,直接新建一级、新建二级、挂规则集、再把一级绑定给入口模块即可 + +## 4. 页面职责重定义 + +### 4.1 评查点分组页 `/rule-groups` + +这是唯一的运行绑定页,负责: + +- 维护一级分组(业务大类) +- 维护二级分组(具体业务类型) +- 维护二级分组绑定的规则集 +- 维护一级分组与入口模块的绑定关系 + +统一文案: + +- 一级:业务大类 +- 二级:具体业务类型 +- 规则集:实际运行绑定 +- 入口模块:一级分组归属入口 + +### 4.2 文档类型页 `/document-types` + +文档类型页保留为“类型主数据页”,职责调整为: + +- 维护具体文档类型名称、编码、描述 +- 展示该类型的汇总规则集 +- 不再承担一级/二级分组结构维护职责 + +统一文案: + +- “汇总规则集” +- “仅用于总览,实际运行绑定请在评查点分组页维护” + +补充说明: + +- 当前文档类型与规则集之间仍可保留“汇总绑定 / 快速绑定”能力 +- 但最终运行时应以“二级分组绑定规则集”为准 +- 因此文档类型页更适合做: + - 主数据编辑 + - 入口模块归属展示 + - 汇总规则集总览 + +### 4.3 上传页 `/files/upload` + +上传页统一交互为: + +1. 先根据入口模块确定可用的一级分组 +2. 再根据一级分组或文档类型加载二级分组 +3. 用户选择本次上传实际命中的二级分组 +4. 最终上传时携带 `groupId` + +## 5. 数据模型建议 + +## 5.1 继续复用 `leaudit_evaluation_point_groups` + +当前表不必推倒重建,建议继续复用,并明确字段语义: + +- `pid = 0` + - 表示一级分组(业务大类) +- `pid != 0` + - 表示二级分组(具体业务类型) + +字段语义建议: + +- 一级分组: + - `document_type_id` 可为空 + - `entry_module_id` 可为空,但新模型建议填写 + - `name` = 业务大类名称,如 `合同` / `卷宗` +- 二级分组: + - `document_type_id` 建议必填,对应实际具体文档类型 + - `pid` 指向对应一级分组 + - `entry_module_id` 通常从一级继承,不需要单独维护 + +## 5.2 新增或正式启用 `entry_module_id` + +为支撑“一级分组绑定入口模块”,分组表需要显式支持: + +- `entry_module_id` + +用途: + +- 绑定一级分组与入口模块的归属关系 +- 后续上传页、首页导航、入口权限控制都可以围绕该关系展开 + +## 5.3 与 `leaudit_rule_type_bindings` 的关系 + +当前不建议粗暴删除 `leaudit_rule_type_bindings`。 + +更合理的口径是: + +- 分组树负责表达“业务结构”和“规则集挂载关系” +- `leaudit_rule_type_bindings` 负责表达“执行时文档类型如何快速命中规则集” + +可以把它理解为: + +- 分组树 = 业务真相源 +- `rule_type_bindings` = 执行加速索引 / 收口层 + +## 6. 迁移原则 + +### 6.1 旧“一级=具体文档类型”是过渡态 + +当前数据库里已经有一批: + +- 一级 = `建设工程合同` +- 二级 = `通用` + +这些不是最终结构,只是为了先把运行链路跑通。 + +### 6.2 未来迁移目标 + +应逐步迁成: + +- 一级 = `合同` +- 二级 = `建设工程合同` +- 规则集继续挂二级 + +也就是说,未来需要把“当前一级里的具体文档类型”下沉成真正的二级。 + +## 7. 推荐迁移步骤 + +### 步骤 1:冻结语义 + +先统一团队口径: + +- 一级 = 业务大类 +- 二级 = 具体业务类型 +- 规则集 = 只挂二级 +- 一级 = 入口模块绑定对象 + +### 步骤 2:后端先支持双语义兼容 + +后端接口先支持: + +- 一级可绑定 `entry_module_id` +- 二级继续绑定 `document_type_id` +- `/by-document-types` 允许从“二级文档类型”反查所属一级 + +这样即使数据库还没完全迁,也能先把接口语义准备好。 + +### 步骤 3:前端页面改口径 + +`/rule-groups` 需逐步改为: + +- 一级显示业务大类 +- 二级显示具体业务类型 +- 页面突出一级与入口模块关系 + +### 步骤 4:测试库迁移旧树 + +建议先在测试库按以下思路演练: + +1. 建立一级:`合同` +2. 建立一级:`卷宗` +3. 将现有“建设工程合同 / 买卖合同 / 停业办理 ...”迁成各自二级 +4. 把原二级下规则集保留在新二级 +5. 清理旧默认 `通用` 子类型,仅在确实没有继续细分的情况下保留 + +### 步骤 5:再落正式库 + +正式库迁移必须在测试通过后执行,避免规则命中错位。 + +## 8. 运行时唯一正确规则 + +迁移完成后,唯一正确的运行规则应为: + +1. 用户进入某入口模块 +2. 系统确定该入口可见的一级分组 +3. 用户选择具体二级分组 +4. 后端按 `groupId` 查询 `leaudit_rule_group_bindings` +5. 找到对应规则集 +6. 取规则集可执行版本执行评查 + +即: + +- 文档类型页:总览 +- 分组页:运行绑定 +- 上传页:按二级分组命中 + +## 9. 当前开发阶段结论 + +截至当前: + +- “规则集挂二级”这部分方向是对的 +- “一级=具体文档类型”这部分已被产品新口径推翻 +- 后续开发应转向“一级=业务大类、一级绑定入口、二级=具体业务类型” + +因此接下来所有实现都应围绕这条新口径继续推进,而不是再沿旧过渡模型加深。 diff --git a/docs/接口/评查点分组迁移执行前检查清单.md b/docs/接口/评查点分组迁移执行前检查清单.md new file mode 100644 index 0000000..2578d71 --- /dev/null +++ b/docs/接口/评查点分组迁移执行前检查清单.md @@ -0,0 +1,379 @@ +# 评查点分组迁移执行前检查清单 + +更新时间:2026-05-03 + +## 1. 目标 + +本清单用于在正式执行 `scripts/migrate_rule_groups_to_doc_type_roots.sql` 之前,先把当前库里的旧数据、兼容态数据和潜在冲突点查清楚。 + +如果希望直接执行一份只读巡检 SQL,可使用: + +- `scripts/precheck_rule_group_migration.sql` + +本次迁移的目标仍然是: + +- 一级分组 = 业务大类 +- 二级分组 = 具体业务类型 +- 规则集 = 挂在二级分组下 +- 入口模块 = 绑定一级分组 + +## 2. 当前风险结论 + +正式迁移前,必须先确认下面 4 类风险: + +1. 是否仍存在“一级直接挂具体文档类型”的旧根数据。 +2. 是否存在同一个 `document_type_id` 被多个二级分组同时承接的情况。 +3. 是否存在规则集同时挂在旧一级根和旧默认子级上,导致迁移后重复。 +4. `document_types.entry_module_id` 是否足够可靠,能支撑“先按入口模块粗分到一级业务大类”。 + +如果这 4 类风险没有检查清楚,不应直接跑迁移脚本。 + +## 2.1 当前已知巡检结论(2026-05-03) + +基于本次实际巡检结果: + +- `leaudit_document_types` 共 20 条,入口归属完整 + - 合同管理:10 条 + - 案卷智能评查:10 条 +- 当前仍有 20 个旧一级根 + - 即:`一级 = 具体文档类型` +- 当前已经存在 2 个新一级业务大类根 + - `root.contract -> 合同` + - `root.casefile -> 行政卷宗` +- 当前有 20 个默认二级分组 + - 即:`二级 = 通用 / *.default` +- 当前没有发现明显冲突: + - 没有“同一文档类型被多个二级分组重复承接” + - 没有“同一规则集同时挂在旧一级根和默认子级” + - 没有“二级分组未绑定规则集” + +结论: + +- 当前库已经具备正式迁移的基础条件 +- 但还没有完成最后的结构补齐 +- 正式迁移前仍要先补 `entry_module_id`,再重跑巡检 + +## 3. 必查项 + +### 3.1 一级旧根检查 + +确认当前是否还有“一级 = 具体文档类型”的历史根: + +```sql +SELECT + id, + code, + name, + document_type_id, + entry_module_id, + sort_order, + is_enabled +FROM leaudit_evaluation_point_groups +WHERE deleted_at IS NULL + AND COALESCE(pid, 0) = 0 + AND document_type_id IS NOT NULL +ORDER BY id; +``` + +判定原则: + +- 查出来的这些记录,都属于兼容态旧根。 +- 正式迁移后,它们不应再继续作为最终一级分组存在。 + +### 3.2 一级业务大类根检查 + +确认当前是否已经存在真正的业务大类根: + +```sql +SELECT + id, + code, + name, + entry_module_id, + sort_order, + is_enabled +FROM leaudit_evaluation_point_groups +WHERE deleted_at IS NULL + AND COALESCE(pid, 0) = 0 + AND document_type_id IS NULL +ORDER BY sort_order, id; +``` + +判定原则: + +- 正常目标结构下,这里应该看到如 `合同`、`卷宗` 这类一级业务大类。 +- 如果这里为空,说明当前库还没有真正切到目标结构。 + +### 3.3 二级分组唯一性检查 + +确认同一个文档类型是否被多个二级分组重复承接: + +```sql +SELECT + document_type_id, + COUNT(*) AS child_count, + STRING_AGG(name, ' / ' ORDER BY id) AS child_names +FROM leaudit_evaluation_point_groups +WHERE deleted_at IS NULL + AND COALESCE(pid, 0) <> 0 + AND document_type_id IS NOT NULL +GROUP BY document_type_id +HAVING COUNT(*) > 1 +ORDER BY document_type_id; +``` + +判定原则: + +- 如果结果为空,说明每个文档类型目前最多只挂到一个二级分组,迁移最安全。 +- 如果结果不为空,要逐条判断: + - 是合理的“一个文档类型拆成多个业务子类型” + - 还是历史脏数据 + +### 3.4 旧默认子级检查 + +确认当前是否存在旧默认子级: + +```sql +SELECT + child.id, + child.pid, + parent.name AS parent_name, + child.name, + child.code, + child.document_type_id +FROM leaudit_evaluation_point_groups child +JOIN leaudit_evaluation_point_groups parent + ON parent.id = child.pid + AND parent.deleted_at IS NULL +WHERE child.deleted_at IS NULL + AND COALESCE(child.pid, 0) <> 0 + AND ( + child.name = '通用' + OR child.code LIKE '%.default' + ) +ORDER BY child.id; +``` + +判定原则: + +- 这些记录多数是旧链路下的“默认子类型”兼容数据。 +- 迁移时如果它们已经被真实二级业务类型替代,应考虑把规则集迁走后再停用或清理。 + +### 3.5 规则集重复挂载检查 + +确认同一个文档类型下,旧一级根和默认子级是否同时挂了相同规则集: + +```sql +WITH target_groups AS ( + SELECT + g.id, + g.document_type_id, + g.name, + g.code, + CASE + WHEN COALESCE(g.pid, 0) = 0 THEN 'old_root' + WHEN g.name = '通用' OR g.code LIKE '%.default' THEN 'default_child' + ELSE 'other_child' + END AS group_kind + FROM leaudit_evaluation_point_groups g + WHERE g.deleted_at IS NULL + AND g.document_type_id IS NOT NULL +) +SELECT + tg.document_type_id, + rgb.rule_set_id, + COUNT(*) AS binding_count, + STRING_AGG(tg.group_kind || ':' || tg.name, ' / ' ORDER BY tg.id) AS group_sources +FROM leaudit_rule_group_bindings rgb +JOIN target_groups tg + ON tg.id = rgb.group_id +WHERE rgb.deleted_at IS NULL +GROUP BY tg.document_type_id, rgb.rule_set_id +HAVING COUNT(*) > 1 +ORDER BY tg.document_type_id, rgb.rule_set_id; +``` + +判定原则: + +- 如果结果不为空,迁移脚本执行前要先确认是否允许去重。 +- 否则迁移后可能出现: + - 同一规则集被重复搬到新二级分组 + - 或者旧运行汇总结果不稳定 + +### 3.6 文档类型与入口模块映射检查 + +确认 `document_types` 是否都有稳定入口归属: + +```sql +SELECT + dt.id, + dt.code, + dt.name, + dt.entry_module_id, + em.name AS entry_module_name +FROM leaudit_document_types dt +LEFT JOIN leaudit_entry_modules em + ON em.id = dt.entry_module_id +WHERE dt.deleted_at IS NULL +ORDER BY dt.id; +``` + +重点看: + +- 是否存在 `entry_module_id IS NULL` +- 是否存在入口模块已被删除、停用或名称异常 +- 是否存在“明明是合同类型却挂在卷宗入口”这类错误归属 + +### 3.7 规则集可运行性检查 + +迁移前需要确认二级分组绑定过去的规则集不是空壳: + +```sql +SELECT + rs.id, + rs.rule_name, + rs.rule_type, + rs.current_version_id, + rs.fallback_version_id +FROM leaudit_rule_sets rs +WHERE rs.deleted_at IS NULL +ORDER BY rs.id; +``` + +这一步建议再配合业务接口实际看: + +- 是否有可用版本 + +## 4. 正式迁移推荐执行顺序 + +### 步骤 1:先备份 + +必须先备份这几张表: + +- `leaudit_evaluation_point_groups` +- `leaudit_rule_group_bindings` +- `leaudit_rule_type_bindings` +- `leaudit_document_types` + +### 步骤 2:先补字段,不迁数据 + +先补结构层缺口: + +1. 给 `leaudit_evaluation_point_groups` 增加 `entry_module_id` +2. 建索引 +3. 给已有一级业务大类根补入口绑定 + +建议值: + +- `root.contract -> entry_module_id = 1` +- `root.casefile -> entry_module_id = 2` + +### 步骤 3:重新跑巡检 + +字段补完后,重新执行: + +- `scripts/precheck_rule_group_migration.sql` + +确认: + +- 一级业务大类根能正确看到入口模块字段 +- 巡检结果仍无重复承接、重复绑定、空壳规则集等问题 + +### 步骤 4:测试库执行正式迁移 + +再执行: + +- `scripts/migrate_rule_groups_to_business_roots.sql` + +说明: + +- 旧脚本 `scripts/migrate_rule_groups_to_doc_type_roots.sql` 仅保留兼容历史引用 +- 不建议继续作为正式迁移脚本使用 + +### 步骤 5:迁后验证 4 个页面 + +必须验证: + +1. `/rule-groups` +2. `/files/upload` +3. `/document-types/new?id=...` +4. `/documents/list` + +## 5. 迁移脚本调整口径 + +基于当前巡检结果,正式脚本建议继续遵守: + +1. 不要再硬编码“只有合同和卷宗” +2. 优先复用已存在业务大类根,不重复新造 +3. 一级根编码必须统一,例如继续沿用: + - `root.contract` + - `root.casefile` +- 可用规则数是否大于 0 + +否则迁完结构后,上传仍然会失败,只是失败原因从“结构问题”变成“规则集不可运行”。 + +## 4. 执行前必须人工确认的业务映射 + +SQL 只能粗分,业务归属必须人工确认: + +### 4.1 一级业务大类清单 + +至少要先列清: + +- 哪些文档类型归到 `合同` +- 哪些文档类型归到 `卷宗` +- 是否已经存在第 3 个业务大类,如 `内部公文` + +### 4.2 二级业务类型命名规范 + +要确认: + +- 二级显示名称是否直接用 `document_types.name` +- 还是需要改成更贴近业务的名称 + - 例如:`处罚-一般程序` + - 而不是数据库里原始的技术名 + +### 4.3 默认子类型保留策略 + +需要人工确认: + +- 哪些文档类型仍保留 `默认子类型(通用)` +- 哪些必须拆成多个真实二级业务类型 + +## 5. 执行顺序建议 + +推荐按下面顺序推进: + +1. 跑本清单中的查询 SQL,导出当前状态。 +2. 形成一份 `document_type -> 一级业务大类 -> 目标二级名称` 的确认表。 +3. 在测试库执行迁移脚本。 +4. 验证以下页面: + - `/rule-groups` + - `/files/upload` + - `/document-types/new?id=...` + - `/documents/list` +5. 验证上传链路: + - 能否正确选择子类型 + - 上传后是否命中预期规则集 + - 合同附件追加链路是否仍正常 +6. 测试通过后,再评估正式库执行窗口。 + +## 6. 当前不建议直接执行的情况 + +只要出现以下任一情况,都不建议直接执行迁移脚本: + +- `document_types.entry_module_id` 还有大量空值 +- 同一文档类型被多个旧组重复承接,但业务还没定论 +- 规则集存在大量“可用规则数 = 0” +- 一级业务大类并不只有 `合同 / 卷宗`,但映射方案还没补 +- 业务方还没确认二级命名与保留策略 + +## 7. 当前结论 + +截至当前,这套系统已经具备: + +- 前端按新链路展示和解释的能力 +- 后端兼容新旧结构的能力 +- 新配置回写旧运行绑定表的能力 + +但数据库正式迁移这一步,仍然必须先做“数据盘点 + 业务确认 + 测试库演练”三件事,不能直接在正式库硬切。 diff --git a/docs/接口/首页入口接口落地说明.md b/docs/接口/首页入口接口落地说明.md deleted file mode 100644 index e245e6d..0000000 --- a/docs/接口/首页入口接口落地说明.md +++ /dev/null @@ -1,188 +0,0 @@ -# 首页入口接口落地说明 - -## 1. 已落地内容 - -- 后端新增统一接口:`GET /api/home/entry-modules` -- 后端来源统一收口到: - - `leaudit_entry_modules` - - `leaudit_document_types` - - `sys_routes + role_route`(仅做页面可见性补充校验) -- 前端首页改为只调用后端统一接口,不再直接查 PostgREST 的 `entry_modules + document_types` -- 前端首页不再手工注入“智慧法务助手”模块 - -## 2. 当前接口返回语义 - -返回结构: - -```json -{ - "code": 200, - "message": "ok", - "data": [ - { - "id": 1, - "name": "合同智能审查", - "description": "合同类文档入口,进入合同检索与评查主页", - "targetPath": "/contract-template/search", - "routePath": "/contract-template/search", - "iconPath": "entryModule/contract.png", - "sortOrder": 10, - "requiresDocumentTypes": true, - "areas": [], - "documentTypes": [ - { - "id": 1, - "name": "建设工程合同", - "code": "contract.construction.general" - } - ] - } - ] -} -``` - -字段说明: - -- `targetPath` - - 首页点击后真正跳转的前端路由 -- `routePath` - - 当前阶段与 `targetPath` 同值 - - 预留给后续把“展示路径”和“权限判断路径”拆开 -- `iconPath` - - 模块图标路径 - - 若以 `/images/` 开头,前端按静态资源处理 - - 否则前端按 `DOCUMENT_URL + iconPath` 处理 -- `requiresDocumentTypes` - - `true`:模块必须挂文档类型才能进入 - - `false`:允许零文档类型直接进入 - - 当前兼容规则: - - `/chat-with-llm/chat` → `false` - - `/cross-checking` → `false` - - 其他路径 → `true` - -## 3. 当前过滤逻辑 - -后端按以下顺序过滤首页入口: - -1. `leaudit_entry_modules.is_enabled = true` -2. `leaudit_entry_modules.deleted_at is null` -3. 地区过滤 - - `super_admin / provincial_admin` 跳过地区过滤 - - 其他用户按 `sso_users.area` 匹配 `leaudit_entry_modules.areas` - - 若 `areas` 为空,则视为全地区可见 -4. 页面权限过滤 - - 若 `sys_routes` 里存在与 `path` 相同的页面路由,则要求当前用户角色在 `role_route` 中已授权 - - 若 `sys_routes` 尚未配置该路由,则当前阶段放行,不阻塞首页展示 - -### 3.1 `/home` 路由补充说明(2026-04-29) - -这次联调里出现过一个容易忘的问题: - -- `leaudit_entry_modules` 里的 `内部公文` 入口配置本身是存在的 -- 它当前配置的跳转路径是 `path = /home` -- 但如果当前角色在 `role_route` 里没有 `/home` 的启用记录,首页接口会把这个入口过滤掉 - -实际排查结论: - -- 不是“内部公文入口模块丢了” -- 也不是“地区过滤没命中” -- 而是“入口模块目标路径存在,但角色没有对应页面路由授权” - -本次已补充: - -- 给 `provincial_admin` 角色补上 `/home` 的 `role_route` - -因此后续如果再次出现“首页某个入口配置明明在库里,但前端没显示”,优先按下面顺序排查: - -1. `leaudit_entry_modules` 里该入口是否存在且 `is_enabled = true` -2. 该入口 `areas` 是否覆盖当前用户地区 -3. 该入口 `path` 对应的页面路由是否存在于 `sys_routes` -4. 当前角色是否在 `role_route` 中对该 `path` 有 `status = 1` - -特别注意: - -- `内部公文 -> /home` 这类首页型入口,对 `role_route` 很敏感 -- 只配入口模块、不配页面路由授权,首页接口不会放出来 - -## 4. 当前表语义 - -### `leaudit_entry_modules` - -当前阶段它只负责“首页业务入口配置”,核心字段语义如下: - -- `name`:首页模块名称 -- `description`:首页模块描述 -- `path`:当前阶段同时承担 `targetPath / routePath` -- `icon_path`:首页卡片图标 -- `areas`:地区可见范围 -- `sort_order`:首页排序 -- `is_enabled`:是否启用 - -### `leaudit_document_types` - -- `entry_module_id`:文档类型归属的首页入口模块 -- 首页进入某模块后,前端把该模块下的 `documentTypes[].id` 写入 `sessionStorage.documentTypeIds` -- 后续列表、首页统计、规则页面仍复用现有 `documentTypeIds` 逻辑 - -## 5. 前端改造点 - -已改造: - -- `new_doc_review/app/api/home/home.ts` - - 改为请求 `/home/entry-modules` -- `new_doc_review/app/routes/_index.tsx` - - 直接使用后端返回的 `targetPath` - - 直接使用后端返回的 `iconPath` - - 按 `requiresDocumentTypes` 判断是否允许进入 - -未改造但仍兼容: - -- 系统设置入口仍沿用现有 `getUserRoutesByRole()` 逻辑 -- 交叉评查专属端口模式仍保留 - -## 6. 初始化数据 - -已补脚本: - -- `scripts/seed_home_entry_modules.sql` - -脚本内容: - -- 按老系统真实数据初始化 5 个入口模块: - - `合同管理` - - `案卷智能评查` - - `内部公文` - - `智慧法务助手` - - `交叉评查` -- 其中: - - `path` 存前端跳转路由 - - `icon_path` 存老系统入口图片路径 -- 自动把: - - `contract.%` 绑定到 `合同管理` - - `行政卷宗.%` 绑定到 `案卷智能评查` - - `NBGW / internal.document` 绑定到 `内部公文` - -## 7. 当前兼容性取舍 - -这次落地刻意没有直接改库结构新增: - -- `module_key` -- `module_type` -- `allow_empty_types` -- `target_path` -- `route_path` - -而是先用现有字段完成可运行版本: - -- `path` 兼容成 `targetPath + routePath` -- 特殊模块是否允许空文档类型,先按路径规则判断 - -同时和老系统保持语义对齐: - -- `icon_path` 承接老库 `entry_modules.path` 的图片路径语义 - -这样做的目的: - -- 先把首页入口能力真正跑起来 -- 不阻塞当前前后端联调 -- 后续若需要长期治理,再补结构性字段升级 diff --git a/docs/权限与地区隔离文档导航.md b/docs/权限与地区隔离文档导航.md new file mode 100644 index 0000000..cb909e1 --- /dev/null +++ b/docs/权限与地区隔离文档导航.md @@ -0,0 +1,33 @@ +# 权限与地区隔离文档导航 + +> 最后整理:2026-05-04 +> 说明:后端权限文档现在按“现行设计 / 接口与权限点 / 老系统背景”三层来读,避免再被历史 TaskList 干扰。 + +## 建议阅读顺序 + +1. `docs/HANDOFF.md` +2. `docs/接口/用户权限与权限点清单.md` +3. `docs/用户与地区权限完整设计方案.md` + +## 各文档角色 + +| 文档 | 作用 | 建议 | +|------|------|------| +| `docs/接口/用户权限与权限点清单.md` | 当前新系统权限点、路由、初始化说明 | 日常开发先看这个 | +| `docs/用户与地区权限完整设计方案.md` | 当前正式权限设计稿 | 作为现行设计主文档 | + +## 本次收口 + +- `docs/用户权限开发TaskList.md` 已删除 +- `docs/老系统_docauditai_用户权限架构深度分析.md` 已并入 `docs/用户与地区权限完整设计方案.md` +- 其“执行清单”价值已经被下面几类文档覆盖: + - `docs/HANDOFF.md`:当前真实状态与下一步 + - `docs/接口/用户权限与权限点清单.md`:当前可执行接口与权限点 + - `docs/用户与地区权限完整设计方案.md`:正式设计边界 + +## 当前口径 + +- 只做 `RBAC + 单地区数据隔离` +- 用户地区只认 `sso_users.area` +- 角色主线只保留:`provincial_admin`、`admin`、`common` +- `super_admin` 仅作为系统维护角色保留 diff --git a/docs/用户与地区权限完整设计方案.md b/docs/用户与地区权限完整设计方案.md index cbe8008..d88c13f 100644 --- a/docs/用户与地区权限完整设计方案.md +++ b/docs/用户与地区权限完整设计方案.md @@ -95,6 +95,41 @@ - 去掉老系统里不够清晰、靠约定隐式生效的部分 - 在新系统里把“地区”“角色”“权限点”“数据范围”写清楚 +### 3.1 老系统真正值得继承的 4 个点 + +1. 登录入口虽然支持 OAuth 和账密两种模式,但最终都统一落到: + - `sso_users` + - `user_role` + - `roles` + - JWT +2. 菜单权限和动作权限始终分层: + - 菜单 / 页面:`sys_routes + role_route` + - 接口 / 动作:`permissions + role_permissions` +3. 数据范围虽然分散,但真实主轴一直是: + - `sso_users.area` + - 业务表 `area/region` + - `data_scope` +4. 老系统本质上就是: + - `RBAC + area-based data scope` + +也就是说,新系统不需要重新发明一个“全新权限平台”,而是要把这条老主线收口得更清楚。 + +### 3.2 老系统不再继续继承的部分 + +这些老逻辑在新系统里不建议继续保留: + +- 靠前端端口、部署环境、隐式约定推断地区 +- 多套登录入口分别维护不同用户模型 +- 模糊的多地区授权集合 +- 没写清楚边界的隐藏 data scope 规则 + +新系统应改成: + +- 一个用户一个主地区 +- 一个统一用户主表 +- 一套明确 RBAC 结构 +- 一套明确的数据范围规则 + --- ## 4. 新系统最终权限模型 diff --git a/docs/用户权限开发TaskList.md b/docs/用户权限开发TaskList.md deleted file mode 100644 index a4cac35..0000000 --- a/docs/用户权限开发TaskList.md +++ /dev/null @@ -1,878 +0,0 @@ -# 用户权限开发 TaskList - -> 目标:在 `leaudit-platform` 里,基于当前真实业务落地一套 **单地区隔离 + 角色权限 + 数据范围控制** 的用户体系,并能把老系统用户数据平滑迁移过来。 - -这份文档不是泛泛设计稿,而是后续开发的执行清单。 - ---- - -## 1. 当前结论先说清楚 - -当前这条线,应该按下面原则做,不要再发散: - -- 用户地区只认 `sso_users.area` -- 数据隔离只做单地区,不做多地区授权 -- 当前业务角色只保留: - - `provincial_admin` - - `admin` - - `common` -- `super_admin` 只作为可选系统维护角色 -- 数据范围只保留: - - `ALL` - - `DEPT` - - `SELF` -- 新系统要兼容老系统已有数据结构,不要为了“理论优雅”把老数据迁移难度抬高 - ---- - -## 2. 当前新系统现状盘点 - -## 2.0 本次实际检查结果(2026-04-29) - -我已经实际检查了当前 `leaudit_platform` 新库和老库 `docauditai`,结论如下: - -- 新库 `leaudit_platform` 原先 **完全没有** 这 7 张核心 RBAC 表: - - `sso_users` - - `roles` - - `user_role` - - `permissions` - - `role_permissions` - - `sys_routes` - - `role_route` -- 新库现阶段只有 `leaudit_*` 业务表,所以原有认证/权限代码其实处于“代码已写、库表未落地”的状态 -- 我已经补了可执行 SQL: - - `scripts/user_rbac_schema_patch.sql` - - `scripts/user_rbac_seed.sql` - - `scripts/user_rbac_migration_audit.sql` -- 我已经把 `schema_patch + seed` 执行进当前新库 -- 当前新库 RBAC 初始化结果: - - `roles = 4` - - `permissions = 30` - - `role_permissions = 90` - - `sys_routes = 9` - - `role_route = 30` - - `sso_users = 0` - - `user_role = 0` - -老库 `docauditai` 的迁移审计关键结果: - -- `sso_users = 4106` -- `roles = 3` -- `user_role = 11` -- `permissions = 133` -- `role_permissions = 222` -- `sys_routes = 34` -- `role_route = 95` -- 老库 **没有空地区、没有重复 sub、没有重复 username** -- 老库 **4106 个用户里有 4098 个没有角色** -- 老库真实角色分布非常集中: - - `common = 6` - - `admin = 4` - - `provincial_admin = 1` - -这说明: - -- 老系统用户登录主数据是可迁的 -- 但角色并不是“每个用户都显式分配了 user_role” -- 新系统迁移时必须补一轮“默认角色落地策略”,不能只机械复制 `user_role` - -## 2.1 已有能力 - -当前新系统已经有这些基础: - -- 已有 `sso_users` 登录查询逻辑 -- 已有 JWT 签发与鉴权 -- 已有 `user_role / roles / role_permissions / permissions` 的查询代码雏形 -- 已有受保护路由统一挂载 JWT 依赖:`fastapi_modules/fastapi_leaudit/controllers/__init__.py` -- 已有认证入口:`/auth/login`、`/auth/password_login` - -## 2.2 当前实际代码路径 - -当前这条线的核心代码在: - -- `fastapi_modules/fastapi_leaudit/services/impl/authServiceImpl.py` -- `fastapi_modules/fastapi_leaudit/services/impl/permissionServiceImpl.py` -- `fastapi_common/fastapi_common_security/jwtService.py` -- `fastapi_common/fastapi_common_security/security.py` -- `fastapi_modules/fastapi_leaudit/controllers/auth/authController.py` - -## 2.3 当前最关键的问题 - -### 问题 1:权限设计文档和实际代码字段名不一致 - -当前代码查的是: - -- `roles.role_key` -- `permissions.permission_key` -- `role_permissions.grant_type` - -但我前面新写的初始化 SQL 用的是: - -- `roles.role_name` -- `permissions.permission_code` - -这说明: - -- **现阶段不能直接执行那份初始化 SQL 到现有库** -- 必须先按当前实际数据库字段重新收一版正式建表/初始化方案 - -### 问题 2:登录后只取“一个角色” - -当前 `authServiceImpl.py` 里: - -```python -SELECT r.role_key ... LIMIT 1 -``` - -这意味着: - -- 多角色用户会被截断成单角色 -- JWT 中的 `roles` 没真正使用起来 -- 后续 `ALL > DEPT > SELF` 的数据范围收敛逻辑无法成立 - -### 问题 3:JWT 里没有真正放入完整权限集 - -当前 JWT 虽然支持 `roles` 字段,但登录时只传了: - -- `userRole` -- `area` - -没有把: - -- 全量角色列表 -- 权限点列表 -- 最终数据范围摘要 - -稳定打进去。 - -这会导致: - -- `/me` 接口还不完整 -- 前端拿不到统一用户权限视图 -- 后端每次都要重新查库,且现有逻辑并未闭环 - -### 问题 4:当前没有统一的数据范围注入层 - -现在只是“能查角色 / 权限”,但还没有真正统一实现: - -- 文档列表自动按 `area` 过滤 -- 用户列表自动按 `area` 过滤 -- 评查 run / result 自动按文档归属校验 - -也就是说: - -- **功能权限雏形有了,数据权限还没有真正落地** - -### 问题 5:OAuth 登录仍然可能把 area 当外部输入覆盖 - -当前 OAuth 登录里: - -- `Area=requestData.get("area")` -- 然后直接 `UPDATE sso_users SET ... area = :area` - -这是很危险的。 - -因为这意味着: - -- 前端或调用方传什么 area,就可能把用户地区改掉 -- 数据隔离基础字段可能被外部请求污染 - -后续必须改成: - -- area 只能来自后台可信源 -- 登录请求不能直接覆盖 area - -### 问题 6:新用户自动创建后没有默认角色闭环 - -当前 OAuth 首登自动插入 `sso_users`,但代码里没有看到紧接着分配默认 `common` 角色的闭环。 - -风险: - -- 新用户建出来了,但没有角色 -- 没角色则权限查询为空 -- 前端表现为“登录成功但啥也看不到” - -### 问题 7:密码仍然是明文比对 - -当前 `PasswordLogin()` 里: - -- 直接 `user.get("password") != Password` - -这是明显安全风险。 - -即使当前先沿用旧系统,也至少要在 tasklist 里明确: - -- 第一阶段兼容旧明文 -- 第二阶段升级为哈希存储与校验 - -### 问题 8:缺少统一的 `/api/auth/me` - -当前文档里已经定义了 `/api/auth/me`,但现有 controller 里还没有真正落地。 - -这会影响: - -- 前端初始化用户态 -- 角色/权限/地区统一回显 -- 登录后刷新恢复 - ---- - -## 3. 老系统可迁移数据结论 - -基于老项目 `docauditai` 的数据库快照和代码分析,当前可直接继承的核心表是: - -- `sso_users` -- `roles` -- `user_role` -- `permissions` -- `role_permissions` -- `sys_routes` -- `role_route` - -## 3.1 老系统 `sso_users` 关键字段 - -老系统 `sso_users` 目前至少包含: - -- `id` -- `sub` -- `username` -- `nick_name` -- `phone_number` -- `email` -- `ou_id` -- `ou_name` -- `status` -- `is_leader` -- `password` -- `try_count` -- `try_login_time` -- `area` -- `tenant_name` -- `dep_short_name` -- `dep_name` -- `mq_person_uuid` -- `mq_account_uuid` -- `mq_synced_at` -- `created_at` -- `updated_at` -- `deleted_at` - -### 迁移判断 - -这说明: - -- 新系统**完全没必要重新发明用户主表** -- 最优方案是: - - 继续沿用 `sso_users` - - 补必要索引/约束/注释 - - 修正其业务语义 - -## 3.2 老系统 `roles` 关键字段 - -老系统 `roles` 目前至少包含: - -- `id` -- `role_key` -- `role_name` -- `data_scope` -- `description` -- `parent_role_id` -- `priority` -- `is_system_role` -- `permissions_cache` -- `metadata` -- `created_at` -- `updated_at` - -### 迁移判断 - -建议: - -- 保留 `role_key` 作为机器标识 -- 保留 `role_name` 作为中文/展示名 -- 继续使用 `data_scope` -- `parent_role_id / permissions_cache / metadata` 可以先保留但不作为第一阶段核心依赖 - -## 3.3 老系统 `permissions` 关键字段 - -老系统 `permissions` 目前至少包含: - -- `id` -- `permission_key` -- `module` -- `resource` -- `action` -- `description` -- `display_name` -- `permission_type` -- `is_system` -- `metadata` -- `parent_id` -- `sort_order` -- `route_id` -- `api_path` -- `api_method` -- `created_at` -- `updated_at` - -### 迁移判断 - -建议: - -- 不要再新造 `permission_code` -- 直接沿用 `permission_key` -- 后续新系统全部按 `permission_key` 统一 - -## 3.4 老系统 `role_permissions` 关键字段 - -老系统 `role_permissions` 目前至少包含: - -- `role_id` -- `permission_id` -- `grant_type` -- `data_scope` -- `condition_filter` -- `metadata` -- `created_at` -- `updated_at` - -### 迁移判断 - -建议: - -- 继续保留 `grant_type` -- 第一阶段只用 `GRANT` -- `DENY` 能力先兼容不扩展 -- `condition_filter` 暂不作为第一阶段核心能力 - ---- - -## 4. 最终推荐表结构策略 - -这里不是从零建库,而是: - -- **以老系统实际表结构为准** -- 对新系统补齐缺失字段、索引、语义约束 -- 避免再造第二套平行用户权限模型 - -## 4.1 `sso_users` 目标策略 - -### 保留字段 - -- 保留老系统现有字段不动 -- 核心业务字段继续认:`area` - -### 必须加的约束 / 索引 - -建议检查并补: - -- `UNIQUE(sub)` -- `INDEX(area)` -- `INDEX(status)` -- `INDEX(deleted_at)` -- `INDEX(username)` - -### 字段语义正式化 - -- `sub`:统一身份唯一标识 -- `username`:登录名 / 工号 / 展示账号 -- `nick_name`:真实姓名 -- `area`:用户主地区,也是默认数据隔离依据 -- `password`:短期兼容旧值,后续迁移为哈希 - -## 4.2 `roles` 目标策略 - -继续沿用: - -- `role_key`:机器标识,例如 `provincial_admin` -- `role_name`:中文展示名,例如“省级管理员” -- `data_scope`:默认数据范围 - -建议正式保留的角色: - -- `provincial_admin` -- `admin` -- `common` -- `super_admin`(可选) - -## 4.3 `permissions` 目标策略 - -继续沿用: - -- `permission_key` -- `api_path` -- `api_method` -- `route_id` - -建议后续所有新权限都按下面格式命名: - -- `auth:me:read` -- `documents:list:read` -- `documents:upload:write` -- `audit:run:execute` -- `rules:binding:update` - -不要混用: - -- 一部分 `documents.list` -- 一部分 `document:read:all` - -必须统一成一种风格。 - -### 我建议 - -直接收口成老系统已有风格: - -- `module:resource:action` - -这样能和老数据完全对齐。 - -## 4.4 `role_permissions` 目标策略 - -继续沿用: - -- `grant_type` -- `data_scope` - -建议唯一约束明确为: - -- `(role_id, permission_id, grant_type, data_scope)` - -但在第一阶段实际配置时: - -- 尽量只放 `GRANT` -- `data_scope` 按权限点是否涉及数据决定 - -## 4.5 `user_role` 目标策略 - -继续沿用现有结构即可。 - -关键点不是表结构,而是后端逻辑必须支持: - -- 一个用户可以挂多个角色 -- 登录时不能 `LIMIT 1` -- 权限汇总和数据范围要支持多角色收敛 - -## 4.6 `sys_routes / role_route` 目标策略 - -继续沿用旧系统结构即可。 - -重点是: - -- 前端菜单权限可以继续从这里出 -- 但 **接口权限不能只依赖菜单权限** -- API 权限必须还是走 `permissions / role_permissions` - ---- - -## 5. 当前逻辑漏洞清单(必须按优先级修) - -## P0:必须先修 - -### 5.1 角色/权限字段命名统一 - -要统一到底使用: - -- `role_key` 还是 `role_name` -- `permission_key` 还是 `permission_code` - -当前结论: - -- 统一用 `role_key` -- 统一用 `permission_key` - -### 5.2 登录角色查询改成多角色 - -当前: - -- `LIMIT 1` - -必须改成: - -- 查询全部角色 -- JWT 带 `roles` -- 兼容保留一个 `user_role` 主角色字段给旧前端 - -### 5.3 OAuth 登录禁止直接覆盖 `area` - -必须改成: - -- 登录入参中的 `area` 不再直接写库 -- area 只能来自: - - 老库已存在值 - - 后台管理维护 - - 后续可信组织同步 - -### 5.4 新用户自动分配默认角色 - -OAuth 首登新用户创建后,必须补: - -- 自动赋 `common` - -### 5.5 `/api/auth/me` 落地 - -要把当前用户的: - -- 基本信息 -- `area` -- `roles` -- `permissions` - -统一返回。 - -## P1:第二阶段紧跟着修 - -### 5.6 数据范围注入 - -至少要先覆盖: - -- 文档列表 -- 文档详情 -- 用户列表 -- 评查状态 / 结果 - -### 5.7 密码校验升级 - -短期兼容旧明文,长期迁移到哈希: - -- 登录时支持“旧明文兼容 + 新哈希优先” -- 用户下次修改密码时写入哈希 - -### 5.8 权限缓存 / 权限聚合 - -后续可以考虑: - -- JWT 轻载 -- 服务端缓存权限聚合结果 - -但这不是 P0。 - ---- - -## 6. 老系统用户数据迁移策略 - -## 6.1 迁移原则 - -不是“把老用户复制一份到新表”,而是: - -- **尽可能复用原表结构** -- 用增量清洗 + 角色校正 + 地区校正的方式迁移 - -## 6.2 迁移对象 - -第一阶段只迁这几类数据: - -1. `sso_users` -2. `roles` -3. `user_role` -4. `permissions` -5. `role_permissions` -6. `sys_routes` -7. `role_route` - -## 6.3 `sso_users` 迁移规则 - -### 必迁字段 - -- `id` -- `sub` -- `username` -- `nick_name` -- `phone_number` -- `email` -- `ou_id` -- `ou_name` -- `status` -- `is_leader` -- `password` -- `area` -- `tenant_name` -- `dep_name` -- `dep_short_name` -- `created_at` -- `updated_at` -- `deleted_at` - -### 可选迁字段 - -- `mq_person_uuid` -- `mq_account_uuid` -- `mq_synced_at` -- `try_count` -- `try_login_time` - -### 迁移前校验 - -要先出统计: - -- `sub` 是否有重复 -- `username` 是否有重复 -- `area` 是否为空 -- `status != 0` 的禁用用户数量 -- `deleted_at is not null` 的软删除用户数量 - -## 6.4 角色映射规则 - -要先梳理老库中真实 `role_key` 分布。 - -预期映射: - -- `provincial_admin` → 保留 -- `admin` → 保留 -- `common` → 保留 -- 其他历史角色: - - 先做映射表 - - 无法归类的先落到 `common` - - 再人工复核 - -### 明确不再延续的角色 - -- `city_admin` -- `review_manager` -- `review_user` -- `rule_admin` - -如果老库真的还残留这些历史角色,不建议直接带入新系统业务角色定义,应该做映射清洗。 - -## 6.5 area 清洗规则 - -迁移前要先把 `sso_users.area` 跑一遍质量检查: - -- 是否为空 -- 是否存在历史别名 -- 是否存在前后空格 -- 是否存在同义不同写法 - -最后要统一成一套稳定值,例如: - -- `省局` -- `梅州` -- `云浮` -- `揭阳` -- `潮州` - ---- - -## 7. 开发分阶段 TaskList - -下面是正式开发顺序。 - -## 阶段 A:数据库基线收口 - -### A1. 先核对当前库真实表结构 - -目标: - -- 以当前数据库真实字段为准 -- 不再拿“理想 SQL”直接执行 - -动作: - -- 检查 `sso_users` -- 检查 `roles` -- 检查 `user_role` -- 检查 `permissions` -- 检查 `role_permissions` -- 检查 `sys_routes` -- 检查 `role_route` - -产出: - -- 一份“现网真实表结构快照” -- 一份“目标结构差异清单” - -### A2. 出正式迁移 SQL - -目标: - -- 不是重建表 -- 是补字段 / 补索引 / 补约束 / 补初始化数据 - -动作: - -- 修正角色初始化 SQL -- 修正权限初始化 SQL -- 修正菜单初始化 SQL -- 统一全部字段名到现有真实库 - -产出: - -- `用户权限_schema_patch.sql` -- `用户权限_seed.sql` - -## 阶段 B:登录与 JWT 收口 - -### B1. 修 `AuthServiceImpl` - -必须改: - -- 多角色查询 -- 新用户默认角色分配 -- OAuth 登录禁止外部覆盖 `area` -- 登录响应统一返回 `roles` -- 兼容返回单个 `user_role` - -### B2. 修 `JwtService` - -必须改: - -- token 中写入 `roles` -- 保留 `user_role` -- 写入 `area` -- 视情况写入精简 `permissions` - -### B3. 新增 `/api/auth/me` - -返回: - -- 用户基本信息 -- area -- roles -- permissions - -## 阶段 C:权限聚合与数据范围引擎 - -### C1. 做统一权限聚合服务 - -目标: - -- 给定 `user_id` -- 返回: - - 角色列表 - - 权限列表 - - 每个权限点最终 data_scope - -### C2. 做统一数据范围判定器 - -目标: - -- 输入:用户 + 权限点 + 业务资源 -- 输出: - - `ALL` - - `DEPT` - - `SELF` - -### C3. 先接到核心接口 - -优先改: - -- `/api/documents/list` -- `/api/audit/run/{RunId}` -- `/api/audit/result/{RunId}` -- `/api/users/list` - -## 阶段 D:老用户数据迁移 - -### D1. 出迁移前检查 SQL - -统计: - -- 用户数 -- 角色分布 -- area 分布 -- 无角色用户 -- 空 area 用户 -- 重复 sub / username - -### D2. 出迁移脚本 - -做法: - -- 先备份 -- 再清洗 area -- 再导入 / 映射角色 -- 再补默认角色 -- 再跑校验 - -### D3. 迁移后验收 - -验收项: - -- 登录正常 -- `/api/auth/me` 正常 -- 文档列表按地区隔离正常 -- admin 不能跨区 -- provincial_admin 能看全局 -- common 只能看自己 - -## 阶段 E:安全与运维收口 - -### E1. 密码升级方案 - -- 兼容旧明文 -- 新密码写哈希 -- 后续批量升级 - -### E2. 审计日志 - -至少记录: - -- 登录成功/失败 -- 角色变更 -- 用户地区变更 -- 权限分配变更 - -### E3. 文档留档 - -要把以下文档保持最新: - -- `docs/用户与地区权限完整设计方案.md` -- `docs/接口/用户权限与权限点清单.md` -- `docs/用户权限开发TaskList.md` - ---- - -## 8. 推荐的实际开发顺序 - -如果按“马上开始干”的节奏,建议严格按下面顺序: - -1. 先检查当前库真实 RBAC 表结构 -2. 再把初始化 SQL 改成真实可执行版 -3. 再改登录与 JWT -4. 再补 `/api/auth/me` -5. 再做权限聚合与数据范围注入 -6. 再做老用户迁移脚本 -7. 最后再做密码升级和审计收口 - -这个顺序的原因很简单: - -- 不先确认表结构,后面代码全会写偏 -- 不先收口登录态,前端无法稳定接入 -- 不先做数据范围,地区隔离只是纸面方案 -- 不最后做迁移,容易把脏数据带着跑 - ---- - -## 9. 我建议下一步立刻做什么 - -下一步最应该做的不是直接改 controller,而是: - -### 第一步 - -先把**当前数据库里 7 张核心表的真实结构**完整导出来: - -- `sso_users` -- `roles` -- `user_role` -- `permissions` -- `role_permissions` -- `sys_routes` -- `role_route` - -### 第二步 - -基于真实结构,产出两份正式 SQL: - -- schema patch SQL -- seed SQL - -### 第三步 - -再开始改: - -- `authServiceImpl.py` -- `jwtService.py` -- `/api/auth/me` - -也就是说,**下一阶段开发入口就是“数据库真相收口”**。 - diff --git a/docs/老系统_docauditai_用户权限架构深度分析.md b/docs/老系统_docauditai_用户权限架构深度分析.md deleted file mode 100644 index f5aa4a0..0000000 --- a/docs/老系统_docauditai_用户权限架构深度分析.md +++ /dev/null @@ -1,1047 +0,0 @@ -# 老系统 `docauditai` 用户权限架构深度分析 - -本文档用于系统性梳理老项目: - -- `/home/wren-dev/Porject/docauditai` - -中的用户、认证、角色、路由、权限、地区、数据范围逻辑。 - -目标不是简单列表,而是明确回答: - -- 老系统真实的用户权限架构是什么 -- 它依赖哪些表和哪些业务规则 -- 新系统应该继承什么、舍弃什么、升级什么 - ---- - -## 1. 结论先行 - -老系统并不是一个“只有 6 张 RBAC 表”的简单权限系统。 - -它真实的架构是: - -- 统一登录入口:OAuth + 账密登录 -- 用户主表:`sso_users` -- 角色体系:`roles + user_role` -- 菜单路由:`sys_routes + role_route` -- 权限点:`permissions + role_permissions` -- 数据权限:`role_permissions.data_scope` -- 地区隔离:大量业务依赖 `sso_users.area` -- 组织同步:登录时通过 MQ / 组织表同步 `area / tenant / dep` - -也就是说,老系统本质上是: - -- `RBAC + area-based data scope` - -而不是纯粹的: - -- `用户 - 角色 - 菜单` - ---- - -## 2. 核心表与模块 - -从代码实际使用看,老系统核心不只是以下 6 张表: - -- `sys_routes` -- `sso_users` -- `roles` -- `role_route` -- `role_permissions` -- `user_role` - -还必须加上: - -- `permissions` -- `route_permission` -- 业务表中的 `area` 字段 -- `roles.data_scope` -- `role_permissions.data_scope` - -相关代码位置: - -- 认证:`app/routes/auth.py` -- JWT:`app/auth/auth.py` -- 路由权限:`app/rbac/route_permission.py` -- 权限检查:`app/rbac/permission_checker_v2.py` -- 数据范围:`app/rbac/data_scope_injector_v2.py` -- RBAC API:`app/routes/v3/rbac.py` - ---- - -## 3. 登录架构 - -## 3.1 统一登录入口 - -老系统登录主入口是: - -- `POST /auth/login` - -代码: - -- `app/routes/auth.py` - -统一登录入口自动识别两种模式: - -- OAuth 登录 -- 账号密码登录 - -识别规则: - -- 请求体包含 `userInfo.sub` → OAuth 登录 -- 请求体包含 `username + password` → 账密登录 - -对应代码: - -- `app/routes/auth.py:111` -- `app/routes/auth.py:154` -- `app/routes/auth.py:158` - -这说明老系统在登录入口设计上已经做到了统一。 - ---- - -## 3.2 OAuth 登录逻辑 - -OAuth 登录处理函数: - -- `app/routes/auth.py:_handle_oauth_login` - -主要流程: - -1. 读取前端传入的 `userInfo` -2. 通过 `sub` 查询 `sso_users` -3. 如果用户不存在,则自动创建用户 -4. 如果用户已存在,则更新用户基本资料 -5. 查询用户角色 -6. 生成 JWT -7. 返回统一登录响应 - -对应代码位置: - -- `app/routes/auth.py:412` -- `app/routes/auth.py:451` -- `app/routes/auth.py:518` -- `app/routes/auth.py:563` -- `app/routes/auth.py:601` -- `app/routes/auth.py:611` - -### OAuth 用户创建规则 - -当用户不存在时: - -- 自动插入 `sso_users` -- 自动分配默认角色 `common` - -对应代码: - -- `app/routes/auth.py:471` -- `app/routes/auth.py:512` - -这意味着: - -- 老系统支持 OAuth 首登自动建号 -- 并且会自动授予最低默认权限 - ---- - -## 3.3 账号密码登录逻辑 - -密码登录处理函数: - -- `app/routes/auth.py:_handle_password_login` - -主要流程: - -1. 根据 `username/sub` 查询 `sso_users` -2. 校验密码 -3. 校验状态 / 删除标记 -4. 同步组织信息(如果有) -5. 查询角色 -6. 生成 JWT -7. 返回统一响应 - -对应代码: - -- `app/routes/auth.py:655` -- `app/routes/auth.py:689` -- `app/routes/auth.py:723` -- `app/routes/auth.py:745` -- `app/routes/auth.py:756` -- `app/routes/auth.py:798` - -可以看出,老系统并没有把账密登录和 OAuth 登录做成两套完全不同的人群模型。 - -它们最终都归并到: - -- `sso_users` -- `user_role` -- `roles` -- JWT - -这是一个非常重要的架构特征。 - ---- - -## 4. JWT 模型 - -JWT 逻辑在: - -- `app/auth/auth.py` - -JWT Payload 中实际携带的关键字段有: - -- `user_id` -- `username` -- `user_role` -- `permissions`(可选) -- `sub` -- `nick_name` -- `email` -- `phone_number` -- `ou_id` -- `ou_name` -- `is_leader` -- `area` - -对应代码: - -- `app/auth/auth.py:61` -- `app/auth/auth.py:94` -- `app/auth/auth.py:101` -- `app/auth/auth.py:106` - -JWT 解码后,同样会恢复成: - -- `User` -- `TokenData` - -对应: - -- `app/auth/auth.py:24` -- `app/auth/auth.py:46` -- `app/auth/auth.py:119` -- `app/auth/auth.py:171` - -### 结论 - -老系统 JWT 不只是认证 token,它还承担了: - -- 用户身份 -- 用户角色 -- 用户地区 -- 组织基础信息 - -其中 `area` 是最关键的业务字段之一。 - ---- - -## 5. 用户地区到底怎么来的 - -这是老系统最关键的逻辑之一。 - -### 5.1 不是靠前端端口 - -老系统的地区不是通过前端访问端口推断。 - -它主要通过组织主数据反查得到。 - -### 5.2 OAuth 登录时的地区来源 - -OAuth 登录会调用: - -- `_get_user_org_info_from_mq` - -位置: - -- `app/routes/auth.py:280` - -查询流程: - -1. 用 `ou_id + nickname` 查询 `um_personinfo` -2. 获取: - - `tenant_uuid` - - `dep_uuid` - - `org_uuid` -3. 再查: - - `um_tenant` - - `um_department` -4. 最终组装: - - `area` - - `tenant_name` - - `dep_name` - - `dep_short_name` - - `ou_name` - -其中 `area` 的来源是: - -- `tenant_short_name` -- 或 `tenant_name` - -对应代码: - -- `app/routes/auth.py:206` -- `app/routes/auth.py:263` -- `app/routes/auth.py:280` -- `app/routes/auth.py:380` -- `app/routes/auth.py:435` - -### 5.3 用户首次创建时 - -新用户创建时,组织信息会直接写入 `sso_users`: - -- `area` -- `tenant_name` -- `dep_name` -- `dep_short_name` -- `ou_name` - -对应: - -- `app/routes/auth.py:471` -- `app/routes/auth.py:480` -- `app/routes/auth.py:512` - -### 5.4 老用户每次登录时 - -如果用户已存在,每次 OAuth 登录仍会同步这些字段: - -- `area` -- `tenant_name` -- `dep_name` -- `dep_short_name` -- `ou_name` - -对应: - -- `app/routes/auth.py:518` -- `app/routes/auth.py:543` -- `app/routes/auth.py:552` -- `app/routes/auth.py:593` - -### 结论 - -老系统对地区的真实定义是: - -- 地区是组织主数据的一部分 -- 地区由后端通过 MQ/组织系统同步 -- 登录时会把地区同步进 `sso_users.area` -- 之后大量业务依赖这个 `area` - -这说明: - -- 老系统是“后端主数据决定地区” -- 不是“前端端口决定地区” - ---- - -## 6. 用户主表 `sso_users` 的真实角色 - -从代码实际用法来看,`sso_users` 不只是登录表。 - -它承担了: - -- 统一身份映射表 -- 用户基础档案表 -- 地区信息存储表 -- 组织信息缓存表 -- 登录失败次数 / 锁定状态记录表 - -常见使用字段: - -- `id` -- `sub` -- `username` -- `nick_name` -- `phone_number` -- `email` -- `ou_id` -- `ou_name` -- `is_leader` -- `status` -- `deleted_at` -- `password` -- `try_count` -- `try_login_time` -- `area` -- `tenant_name` -- `dep_name` -- `dep_short_name` - -对应代码位置: - -- `app/routes/auth.py:451` -- `app/routes/auth.py:689` -- `app/routes/auth.py:914` - -### 结论 - -老系统把 `sso_users` 作为了: - -- 用户唯一主表 -- 认证与组织信息的汇聚中心 - -这一点新系统应该继承。 - ---- - -## 7. 角色体系是什么 - -角色相关表: - -- `roles` -- `user_role` - -### 7.1 用户和角色关系 - -一个用户可以拥有多个角色。 - -角色查询通常通过: - -- `user_role -> roles` - -对应代码: - -- `app/routes/auth.py:840` -- `app/routes/auth.py:852` -- `app/routes/auth.py:862` - -### 7.2 OAuth 新用户默认角色 - -新用户默认自动分配: - -- `common` - -对应代码: - -- `app/routes/auth.py:512` - -### 7.3 角色本身带数据范围 - -老系统 `roles` 表本身带: - -- `data_scope` - -在 RBAC API 中大量出现: - -- `RoleService.list_roles` -- `RoleService.get_role` - -对应代码: - -- `app/services/rbac/role_service.py:98` -- `app/services/rbac/role_service.py:171` -- `app/services/rbac/user_role_service.py:217` - -### 结论 - -老系统角色不只是页面访问角色,它还是: - -- 权限角色 -- 数据范围角色 - -也就是说: - -- 一个角色同时决定功能权限和数据权限 - ---- - -## 8. 菜单 / 路由权限模型 - -### 8.1 路由表和角色路由表 - -老系统页面菜单权限主要基于: - -- `sys_routes` -- `role_route` - -主逻辑在: - -- `app/rbac/route_permission.py` - -### 8.2 `get_user_routes(user_id)` 的真实行为 - -流程: - -1. 查用户所有角色 -2. 用角色查 `role_route` -3. join `sys_routes` -4. 只返回启用路由 -5. 组装成树形结构 -6. 还会把该路由相关的权限点附在 `permissions` 字段里 - -对应: - -- `app/rbac/route_permission.py:22` -- `app/rbac/route_permission.py:47` -- `app/rbac/route_permission.py:63` -- `app/rbac/route_permission.py:100` -- `app/rbac/route_permission.py:127` - -### 8.3 路由不仅是菜单,还带权限上下文 - -老系统会在返回路由树时,把页面对应的权限点也挂上去。 - -这意味着前端不是只拿“菜单”,而是拿: - -- 菜单结构 -- 页面权限上下文 - -这是很成熟的一种设计。 - ---- - -## 9. 功能权限模型 - -### 9.1 老系统不是只有 `role_permissions` - -真正的功能权限核心是: - -- `permissions` -- `role_permissions` - -其中: - -- `permissions`:权限定义表 -- `role_permissions`:角色授权表 - -### 9.2 权限检查器 V2 - -代码: - -- `app/rbac/permission_checker_v2.py` - -它的逻辑是: - -1. 查用户所有角色 -2. 通过 `role_permissions` 找到权限 -3. join `permissions` -4. 读 `permission_key` -5. 支持: - - 精确匹配 - - 通配符权限 - - `GRANT` - - `DENY` - -对应: - -- `app/rbac/permission_checker_v2.py:61` -- `app/rbac/permission_checker_v2.py:147` -- `app/rbac/permission_checker_v2.py:199` - -### 9.3 权限键格式 - -权限键采用: - -- `module:resource:action` - -例如: - -- `document:list:read` -- `document:delete:delete` -- `dify:dataset:read` -- `system:rbac:manage` - -这套结构非常清晰,推荐新系统继续保留。 - -### 结论 - -老系统功能权限的真实设计是: - -- `permissions.permission_key` 做标准权限定义 -- `role_permissions` 决定角色拥有哪些权限 -- 路由权限只是页面层,不能替代 `permissions` - ---- - -## 10. 数据权限模型 - -这是老系统最关键但最容易被忽略的一层。 - -### 10.1 数据权限来源 - -数据权限核心在: - -- `role_permissions.data_scope` -- `roles.data_scope` -- 用户 `area` - -### 10.2 数据范围注入器 - -代码: - -- `app/rbac/data_scope_injector_v2.py` - -定义了三种数据范围: - -- `ALL` -- `DEPT` -- `SELF` - -对应: - -- `app/rbac/data_scope_injector_v2.py:24` - -### 10.3 三种范围含义 - -#### `ALL` - -- 查看全部数据 -- 不加过滤条件 - -#### `DEPT` - -- 查看本地区数据 -- 实现方式是:按 `area` 过滤 - -#### `SELF` - -- 只能查看本人数据 -- 实现方式是:按 `user_id` 过滤 - -对应: - -- `app/rbac/data_scope_injector_v2.py:170` -- `app/rbac/data_scope_injector_v2.py:228` - -### 10.4 本地区的真实含义 - -老系统里 `DEPT` 虽然名称叫部门范围,但实际很多地方是: - -- 按地区 `area` 过滤 - -也就是说: - -- 它更像“本地市范围” -- 而不是严格的部门树范围 - -这说明老系统的数据权限其实是: - -- `ALL / 地区 / 本人` - -而不是严格组织树。 - ---- - -## 11. `area` 在老系统中的真实地位 - -### 11.1 `area` 是核心业务字段 - -老系统中,`area` 被用于: - -- 用户归属地区 -- 数据权限过滤条件 -- 业务记录写入默认地区 -- Dify 知识库选择 -- Dify 对话应用过滤 -- 评查点地区隔离 - -### 11.2 Dify 知识库访问 - -代码: - -- `app/routes/v3/dify_area_dataset.py` - -逻辑: - -- 普通用户:按 `current_user.area` 查本地区知识库 -- 省级管理员:可看全部 - -对应: - -- `app/routes/v3/dify_area_dataset.py:39` -- `app/routes/v3/dify_area_dataset.py:57` -- `app/routes/v3/dify_area_dataset.py:67` - -### 11.3 Dify 对话应用过滤 - -代码: - -- `app/routes/v3/dify_chat_apps.py` - -逻辑: - -- 根据 `current_user.area` 返回本地区应用 -- 同时可以返回省级应用 - -对应: - -- `app/routes/v3/dify_chat_apps.py:29` -- `app/routes/v3/dify_chat_apps.py:60` - -### 11.4 PostgREST 转发层的 area 注入 - -代码: - -- `app/exceptions/global_exc.py` - -老系统在某些写操作中,甚至会自动把用户 `area` 写进业务数据。 - -典型例子: - -- `evaluation_points` 表写入时自动填充 `area` - -并且: - -- `provincial_admin` 会被硬编码成 `省级` - -对应代码: - -- `app/exceptions/global_exc.py:250` -- `app/exceptions/global_exc.py:292` -- `app/exceptions/global_exc.py:307` - -### 结论 - -在老系统中: - -- `area` 不是附属字段 -- 而是整套业务隔离体系的核心字段之一 - ---- - -## 12. 中间件与鉴权行为 - -### 12.1 JWT 中间件 - -代码: - -- `app/middleware/jwt_auth.py` - -行为: - -- 白名单路径跳过 -- 其他请求必须有 Bearer Token -- 中间件先做基础校验 -- 然后将当前用户信息塞进: - - `request.state.current_user` - -塞入字段包括: - -- `user_id` -- `username` -- `nick_name` -- `email` -- `phone_number` -- `ou_id` -- `ou_name` -- `is_leader` -- `user_role` -- `area` - -对应: - -- `app/middleware/jwt_auth.py:49` -- `app/middleware/jwt_auth.py:75` -- `app/middleware/jwt_auth.py:88` -- `app/middleware/jwt_auth.py:89` - -### 12.2 路由层再做细粒度权限校验 - -真正功能权限由: - -- `require_permission_v2` - -处理,位置: - -- `app/rbac/decorators_v2.py` - -它会: - -- 从 `request.state.current_user` 取 `user_id` -- 调 `PermissionCheckerV2.check_permission` -- 无权限则 403 - -对应: - -- `app/rbac/decorators_v2.py:56` -- `app/rbac/decorators_v2.py:92` - -### 结论 - -老系统是两层鉴权: - -- 中间件:验 token -- 装饰器/依赖:验功能权限 - -这个分层设计是合理的。 - ---- - -## 13. 老系统角色体系的实际业务语义 - -根据代码表现,老系统大致存在以下角色层次: - -- `provincial_admin` -- `admin` -- `common` -- 以及其他业务角色 - -### 13.1 `provincial_admin` - -特点: - -- 高权限角色 -- 数据范围通常为 `ALL` -- 某些业务场景会被特殊处理为“省级” -- 可访问全部地区的数据 / 配置 - -体现位置: - -- `app/exceptions/global_exc.py:292` -- `app/routes/v3/dify_area_dataset.py:67` - -### 13.2 `admin` - -特点: - -- 更像市级管理员 -- 数据范围通常为本地区 -- 很多场景按 `area` 做限制 - -体现位置: - -- `app/services/rbac/user_role_service.py:91` -- `app/exceptions/global_exc.py:229` - -### 13.3 `common` - -特点: - -- 默认普通用户 -- 新用户自动分配 -- 权限最低 -- 数据范围通常为 `SELF` 或较弱的本地区权限 - -体现位置: - -- `app/routes/auth.py:512` -- `app/routes/auth.py:601` - ---- - -## 14. 老系统真实的架构图 - -如果按实际行为抽象,老系统可以概括为: - -### 14.1 用户身份层 - -- `sso_users` -- `sub` -- OAuth / 本地账密登录 - -### 14.2 角色层 - -- `roles` -- `user_role` - -### 14.3 页面菜单层 - -- `sys_routes` -- `role_route` - -### 14.4 功能权限层 - -- `permissions` -- `role_permissions` -- `permission_key` -- `grant_type` - -### 14.5 数据权限层 - -- `role_permissions.data_scope` -- `roles.data_scope` -- 用户 `area` -- 业务表 `area` - -### 14.6 组织归属层 - -- `ou_id` -- `ou_name` -- `tenant_name` -- `dep_name` -- `dep_short_name` -- 通过 MQ / 组织表同步 - -### 结论 - -老系统不是简单 RBAC,而是: - -- `RBAC + 地区数据隔离 + 组织同步` - ---- - -## 15. 老系统优点 - -### 15.1 登录入口统一 - -OAuth 和账密最终走同一条认证主链。 - -### 15.2 用户主数据集中 - -所有用户最终都汇总到: - -- `sso_users` - -### 15.3 路由权限设计成熟 - -- `sys_routes + role_route` -- 支持树形菜单 -- 支持隐藏路由 -- 支持角色路由缓存 - -### 15.4 功能权限设计成熟 - -- `permissions` -- `role_permissions` -- `grant / deny` -- 通配符匹配 - -### 15.5 数据范围是正式模型,不是临时过滤 - -虽然比较粗糙,但已经有: - -- `ALL` -- `DEPT` -- `SELF` - -### 15.6 地区来自后端主数据 - -老系统地区来源比“前端端口推断”可靠得多。 - ---- - -## 16. 老系统缺点 - -### 16.1 `area` 过载过重 - -`area` 同时承担: - -- 用户地区 -- 数据权限过滤条件 -- 业务默认地区 -- 规则隔离条件 -- 知识库分配条件 - -语义过重。 - -### 16.2 数据范围表达力不足 - -只有三档: - -- `ALL` -- `DEPT` -- `SELF` - -无法优雅表达: - -- 多地区访问 -- 指定地区集合 -- 复杂跨区权限 - -### 16.3 大量业务写死 `area` 逻辑 - -导致: - -- 架构耦合重 -- 后续改动成本高 -- 组织逻辑与业务逻辑混杂 - -### 16.4 存在角色硬编码 - -例如: - -- `provincial_admin -> 省级` - -这类逻辑写死在业务层,不够优雅。 - -### 16.5 `DEPT` 实际上更像地区范围,不是部门范围 - -命名容易误导。 - ---- - -## 17. 对新系统的启示 - -## 17.1 必须继承的部分 - -新系统应继承老系统的这些优点: - -- 统一登录入口 -- OAuth / 账密统一落主用户表 -- `sys_routes + role_route` -- `permissions + role_permissions` -- 标准 `permission_key` -- `grant / deny` -- 数据范围模型概念 -- 组织同步决定地区 - -## 17.2 必须升级的部分 - -新系统不能原样照搬老系统的缺点。 - -必须升级为: - -- 不再只有一个 `area` 字段承载所有数据权限语义 -- 把用户默认地区和可访问地区分开 -- 把数据权限从 `DEPT` 升级为正式 `region scope` -- 把组织同步保留,但不要在各业务层到处散写地区逻辑 - ---- - -## 18. 新系统设计应如何继承老系统 - -从老系统出发,新系统最合理的方向不是推翻,而是升级: - -### 18.1 保留骨架 - -- `sso_users` -- `roles` -- `user_role` -- `sys_routes` -- `role_route` -- `permissions` -- `role_permissions` - -### 18.2 替换数据权限实现 - -将老系统: - -- `ALL / DEPT / SELF` - -升级为: - -- `ALL` -- `SELF` -- `HOME_REGION` -- `CUSTOM_REGIONS` -- `PROVINCIAL` - -### 18.3 组织同步仍保留 - -地区来源仍然应该以后端组织主数据为准,而不是前端推断。 - -### 18.4 将 `area` 升级为正式 `region_code` - -业务表中的地区字段建议统一收口成稳定 code,而不是直接到处使用中文字符串。 - ---- - -## 19. 一句话总结 - -老系统 `docauditai` 的真实用户权限架构不是“6 张表的简单角色系统”,而是: - -- 以 `sso_users` 为用户主数据中心 -- 通过 OAuth / 账密统一登录 -- 通过组织系统同步 `area / tenant / dep` -- 用 `roles + user_role + sys_routes + role_route + permissions + role_permissions` - 管功能权限 -- 用 `data_scope + area` - 管数据权限 - -本质是: - -- `RBAC + 地区数据权限 + 组织同步` - -这就是新系统设计时真正应该继承的老逻辑骨架。 - diff --git a/docs/规则编辑/README.md b/docs/规则编辑/README.md new file mode 100644 index 0000000..57a2ca8 --- /dev/null +++ b/docs/规则编辑/README.md @@ -0,0 +1,39 @@ +# 规则编辑与规则链路文档导航 + +> 最后整理:2026-05-04 +> 说明:本目录关注后端规则引擎、规则存储、Bridge 接入、YAML 编辑与执行链路。 + +## 阅读顺序 + +1. `docs/leaudit/README.md` +2. `统一OSS与规则管理实施计划.md` +3. `原生AuditCtx接入重构方案.md` +4. 按需查看其余专项文档 + +## 文档分组 + +| 文档 | 作用 | 当前建议 | +|------|------|----------| +| `统一OSS与规则管理实施计划.md` | 规则链路主实施方案,含 OSS、规则管理、执行链、验收顺序 | 主参考 | +| `原生AuditCtx接入重构方案.md` | 新平台如何接入原生 `AuditCtx` 流程 | 主参考 | + +## 本次归并 + +- `开发任务拆解清单.md` 的执行清单已并入 `统一OSS与规则管理实施计划.md` +- `跑通全流程所需准备项.md` 的能力清单已并入 `统一OSS与规则管理实施计划.md` +- `为什么仍然需要Bridge适配层.md` 的边界说明已并入 `原生AuditCtx接入重构方案.md` +- `yaml规则在线编辑设计.md` 的真相源设计已并入 `统一OSS与规则管理实施计划.md` +- `worker并发执行改造方案.md` 的任务编排与队列口径已并入 `统一OSS与规则管理实施计划.md` +- 当前如果只想知道“先做什么、后做什么、验收到哪里”,优先看 `统一OSS与规则管理实施计划.md` + +## 和 `docs/leaudit/` 的分工 + +- `docs/leaudit/`:稳定架构、表结构、处理流水线 +- `docs/规则编辑/`:规则管理、Bridge 接入、落地实施顺序 +- 当前阶段状态与真实阻塞统一回写 `docs/HANDOFF.md` + +## 维护规则 + +- 这里的文档以“规则链路后端实现”视角为主 +- 如果内容已经变成当前正式现状,应同步回写 `docs/HANDOFF.md` +- 临时推演稿如果已经完成使命,应合并进正式文档后删除 diff --git a/docs/规则编辑/worker并发执行改造方案.md b/docs/规则编辑/worker并发执行改造方案.md deleted file mode 100644 index 79402ce..0000000 --- a/docs/规则编辑/worker并发执行改造方案.md +++ /dev/null @@ -1,336 +0,0 @@ -原生 LeAudit Worker 并发执行改造方案 - -这份文档不是纯方案稿,而是“已经落地到哪一步 + 当前系统为什么这么设计 + 后续接手时要看什么”的实施记录。后面如果忘了这套东西是干什么的,先看这份。 - -目标 - -- 把上传触发评查从 HTTP 同步执行切到 `Run -> dispatch -> worker` -- 平台侧只做: - - 上传入库 - - OSS / 规则文件解析 - - run 状态编排 - - 结果持久化 -- 原生 `/home/wren-dev/Porject/leaudit/src/leaudit` 继续负责: - - `AuditCtx` - - `AuditService.audit(ctx)` - - OCR / extract / evaluate / rescue 主流程 - -为什么要改成 worker - -- HTTP 请求里同步跑全链路会把 OCR、LLM、VLM、补救流程都塞进 Web 线程 -- 后续存在并发上传、多文档批量处理、多 worker 扩容场景 -- 同步模式下容易遇到: - - 请求超时 - - 事件循环混用 - - 多线程/多请求互相阻塞 - - 状态不可追踪 -- 改造成 worker 后,API 只负责“受理 + 排队”,重任务交给 Celery worker 进程 - -当前已落地 - -- `fastapi_admin/celery_app.py` - - 新增 Celery app,broker/backend 复用当前 Redis -- `fastapi_admin/config/_settings.py` - - 新增 worker 队列、并发、超时、OCR 视觉并发、signature probe 限时等配置 -- `fastapi_admin/config/__init__.pyi` - - 暴露新的 LEAUDIT 配置项 -- `app.toml` - - 正式加入 `[LEAUDIT]` worker/并发相关配置 - - 补齐 `[OCR].BASE_URL` -- `fastapi_modules/fastapi_leaudit/services/impl/auditServiceImpl.py` - - `Run()` 改为只创建 `leaudit_audit_runs` 并投递任务 - - 相同文档已有活动 run 时直接复用,避免重复创建 -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/tasks.py` - - 改为 `runId` 驱动,而不是把整份文件内容直接塞到同步调用链 - - 增加 Celery task 入口 - - 增加 run 抢占,避免重复消费 - - 增加队列路由 -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/client_factory.py` - - OCR client 读取平台配置,避免 `base_url is required` -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/auditServiceFactory.py` - - 原生服务装配读取平台并发配置 -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py` - - 修复原生 `FieldValue` 新旧结构兼容 - - 修复 JSONB 字段写库序列化 -- `scripts/start_worker.sh` - - worker 启动脚本改为动态读取配置 - - worker nodename 改成唯一,避免 Celery `DuplicateNodenameWarning` -- `docs/规则编辑/worker并发执行改造方案.md` - - 记录当前这套机制,避免后续自己忘记 - -执行链路 - -1. `POST /api/upload` -2. `DocumentServiceImpl.Upload()` 写 `leaudit_documents / leaudit_document_files` -3. 原始文件上传 OSS -4. `AuditServiceImpl.Run()` 创建 `leaudit_audit_runs` -5. `dispatch_leaudit_task(run_id)` 投递 Celery -6. worker 消费 `leaudit.process_document` -7. worker 按 `runId` 查库,取: - - 文档 - - 活跃文件版本 - - 当前规则版本 - - 规则 OSS 地址 -8. bridge 下载 OSS 文件 / 规则文件到本地临时文件 -9. `NativeRunner` 构建原生 `AuditCtx` 并调用原生 audit -10. `StorageAdapter` 把结果写回 `leaudit_*` - -当前实现涉及的关键文件 - -- 上传入口 - - `fastapi_modules/fastapi_leaudit/controllers/documentController.py` - - `fastapi_modules/fastapi_leaudit/services/documentService.py` - - `fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py` -- bridge 层 - - `fastapi_modules/fastapi_leaudit/leaudit_bridge/fileSourceResolver.py` - - `fastapi_modules/fastapi_leaudit/leaudit_bridge/ruleVersionResolver.py` - - `fastapi_modules/fastapi_leaudit/leaudit_bridge/auditCtxBuilder.py` - - `fastapi_modules/fastapi_leaudit/leaudit_bridge/auditServiceFactory.py` - - `fastapi_modules/fastapi_leaudit/leaudit_bridge/nativeRunner.py` - - `fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py` - - `fastapi_modules/fastapi_leaudit/leaudit_bridge/tasks.py` -- worker 与配置 - - `fastapi_admin/celery_app.py` - - `fastapi_admin/config/_settings.py` - - `app.toml` - - `scripts/start_worker.sh` - -并发配置正式化 - -- `LEAUDIT_LLM_MAX_CONCURRENCY` - - 单文档内 LLM 全局信号量上限 -- `LEAUDIT_VLM_MAX_CONCURRENCY` - - 单文档内 VLM 主流程上限 -- `LEAUDIT_OCR_VLM_CONCURRENCY` - - OCR 视觉增强 / 印章分类阶段的 VLM 并发上限 -- `LEAUDIT_SIGNATURE_PROBE_CONCURRENCY` - - DOCX 签名候选区域补探测并发上限 -- `LEAUDIT_SIGNATURE_PROBE_TIMEOUT` - - 单次签名 probe 的 VLM 超时,避免长时间卡住 OCR 尾部 -- `LEAUDIT_WORKER_CONCURRENCY` - - 单 worker 进程可并发执行的任务数 -- `LEAUDIT_WORKER_QUEUE_URGENT` - - 前端上传中的“紧急”队列 -- `LEAUDIT_WORKER_QUEUE_NORMAL` - - 前端上传中的“普通”队列 -- `LEAUDIT_TASK_SOFT_TIME_LIMIT` - - Celery 软超时 -- `LEAUDIT_TASK_TIME_LIMIT` - - Celery 硬超时 -- `LEAUDIT_RUN_LOCK_SECONDS` - - 为后续升级“租约锁”预留的锁超时配置 - -当前 `app.toml` 基线 - -```toml -[OCR] -BASE_URL = "http://i-2.gpushare.com:44112/" - -[LEAUDIT] -LLM_MAX_CONCURRENCY = 5 -VLM_MAX_CONCURRENCY = 3 -OCR_VLM_CONCURRENCY = 3 -SIGNATURE_PROBE_CONCURRENCY = 2 -SIGNATURE_PROBE_TIMEOUT = 20 -WORKER_QUEUE_URGENT = "leaudit.urgent" -WORKER_QUEUE_NORMAL = "leaudit.normal" -WORKER_CONCURRENCY = 2 -RUN_LOCK_SECONDS = 1800 -TASK_SOFT_TIME_LIMIT = 3300 -TASK_TIME_LIMIT = 3600 -``` - -当前队列路由策略 - -- 前端上传 `speed=urgent` -> `leaudit.urgent` -- 前端上传 `speed=normal` -> `leaudit.normal` -- `/audit/run` 也支持 `speed` -- 未识别值默认归到 `normal` - -接口收口 - -- 上传接口:`POST /upload` - - 入参: - - `file` - - `typeId` / `typeCode` - - `region` - - `fileRole` - - `createdBy` - - `autoRun` - - `speed=normal|urgent` - - 已去掉旧系统 `bizDocumentId` 输入 -- 上传返回: - - `documentId` - - `internalDocumentNo` - - `fileId` - - `typeId` - - `typeCode` - - `region` - - `fileName` - - `ossUrl` - - `speed` - - `processingStatus` - - `autoRunTriggered` - - `run` -- 手动触发:`POST /audit/run` - - 支持 `speed=normal|urgent` -- 状态查询: - - `GET /audit/run/{runId}` -- 结果查询: - - `GET /audit/result/{runId}` - -当前业务收敛结论 - -- 不再继续扩 `high/default/batch` 三层方案 -- 当前真实业务只有前端上传,并且只有“紧急 / 普通”两档速度需求 -- 所以 worker 体系正式收敛成两级模型: - - `urgent` - - `normal` -- 这样既满足优先级需求,也避免过度设计 - -状态流转 - -- 新建 run - - `status=queued` - - `phase=dispatch` -- worker 抢占成功 - - `status=running` - - `phase=prepare` -- 原生流程推进中 - - `phase` 会继续流转为 OCR / extraction / evaluation / rescue / persist 等阶段 -- 执行成功 - - `status=completed` - - `result_status` 由 bridge 汇总写回 -- 执行失败 - - `status=failed` - - `phase` 停在失败发生阶段 - - `leaudit_run_errors` 记录错误明细 - -避免重复消费 - -- API 触发 `Run()` 时: - - 若同一文档已有 `queued/running/retrying` 的 run,且 `Force=False` - - 直接返回当前活动 run,不重复创建 -- worker 开始执行前: - - 先按 run 状态执行原子抢占 - - 仅允许 `status in ('queued', 'pending', 'retrying')` 的 run 进入执行 -- 抢占失败时: - - 当前 worker 直接跳过 - - 返回 `already_claimed` - -当前表语义 - -- `leaudit_documents` - - 平台内部文档主表 - - 一次前端上传会创建一条新的 document - - 不再依赖旧系统 `documents.id` - - `biz_document_id` 仅作为内部追踪号保留,避免立刻改库 -- `leaudit_document_files` - - 具体上传文件记录 - - 保存文件名、后缀、MIME、SHA256、OSS 地址等文件级信息 - - 当前仍保留单独文件表,为后续是否合表留余地 - -已踩过并修掉的坑 - -- 事件循环混用 - - 同步 HTTP 链路里直接执行原生异步流程,容易出现 `Future attached to a different loop` - - 现在改成 worker 独立进程执行,HTTP 只排队 -- OCR base_url 缺失 - - 原因是平台 bridge 没把 `[OCR].BASE_URL` 正确喂给原生 client - - 现已在 `app.toml` 和 `client_factory.py` 补齐 -- 原生 `FieldValue` 结构变化 - - 新版本 `FieldValue` 不一定有 `.position` - - `storage_adapter.py` 已改为兼容: - - 优先读 `position` - - 没有时回退到 `metadata.match_position / bbox / page_num` -- asyncpg + JSONB 序列化 - - 不能把 `list/dict` 直接按字符串列方式塞给 `CAST(:x AS JSONB)` - - 需要先 `json.dumps(..., ensure_ascii=False)` 再入库 - - 当前已在这些路径处理: - - `leaudit_rule_results.stages` - - `leaudit_rule_results.extracted_fields` - - `leaudit_rule_results.field_positions` - - `leaudit_rule_results.remediation` - - `leaudit_rule_results.rule_meta` - - `leaudit_rescue_outcomes.payload` - -当前运行现状 - -- `POST /api/upload` 已经可以快速返回 `queued` -- worker 已能真正执行到: - - OCR - - extraction - - evaluation - - rescue -- 已验证真实样本端到端成功: - - `run_id=10` - - `document_id=9` - - `phase=executed` - - `24 passed / 4 failed / 0 skipped` -- 当前主要耗时瓶颈明确在 OCR 阶段 - - 一次验证中: - - `ocr=277.79s` - - `extraction=13.65s` - - `evaluation=11.81s` - - `total=303.25s` -- 已针对 OCR 长尾补第一轮治理: - - DOCX `signature probe` 增加单独超时 - - DOCX `signature probe` 增加并发限制 - - 避免某个候选框失败时串行拖住整个尾段 -- 如果代码刚修过但日志仍报旧错,第一怀疑项不是代码没改,而是 worker 进程还没重启,仍在跑旧代码 -- worker 日志会明确打印: - - `run_id=xx 已投递到 worker 队列: queue=..., speed=...` - - `run_id=xx worker开始执行: queue=..., speed=..., filename=...` - -启动方式 - -后端 API: - -```bash -.venv/bin/python run.py -``` - -worker: - -```bash -bash scripts/start_worker.sh -``` - -脚本会自动读取: - -- `LEAUDIT_WORKER_CONCURRENCY` -- `LEAUDIT_WORKER_QUEUE_URGENT` -- `LEAUDIT_WORKER_QUEUE_NORMAL` - -并拼成: - -```bash -celery -A fastapi_admin.celery_app:celery_app worker \ - -Q leaudit.urgent,leaudit.normal \ - --concurrency=2 -``` - -同时 worker nodename 会带主机名和进程号,避免多个 worker 使用同名节点。 - -联调时的最小操作顺序 - -1. 启动 API:`.venv/bin/python run.py` -2. 启动 worker:`bash scripts/start_worker.sh` -3. 调用上传接口,确认返回 `processingStatus=queued` -4. 看 worker 日志是否出现: - - 抢占 run - - OCR result saved - - Extraction result saved - - Evaluation results saved - - finalize run -5. 如果日志还是旧报错,先重启 worker 再测一次 - -后续明确待办 - -- 给 run 抢占补上真正的租约锁字段: - - `worker_id` - - `lock_expires_at` - - `attempt_count` -- 如果后续紧急任务量上来,再考虑把 `urgent` 单独拆成专属 worker -- 补 `GET /run/{id}/status` 或前端轮询说明 -- 评估 OCR 阶段是否继续拆更细粒度并发,但前提是先确认外部 OCR / VLM 服务的限流上限 diff --git a/docs/规则编辑/yaml规则在线编辑设计.md b/docs/规则编辑/yaml规则在线编辑设计.md deleted file mode 100644 index 2f8362b..0000000 --- a/docs/规则编辑/yaml规则在线编辑设计.md +++ /dev/null @@ -1,306 +0,0 @@ -# LeAudit YAML 规则在线编辑设计 - -## 1. 背景 - -`LeAudit` 内核当前以本地 YAML/DSL 文件作为规则输入载体执行评查流程。现阶段 **不修改 `leaudit` 核心**,但平台侧需要逐步具备以下能力: - -- 在线查看 YAML 规则内容 -- 在线编辑 YAML 规则 -- 校验规则语法与 DSL 语义 -- 规则版本化保存 -- 规则发布、回滚、审计 - -因此需要在 `leaudit-platform` 中补充一套“**规则管理真相源**”方案,使平台能够支持规则后台,而运行时仍然兼容 `leaudit` 只读取本地 YAML 的既有行为。 - ---- - -## 2. 结论 - -**可以不改 `leaudit` 核心,同时把 DSL YAML 存储到 OSS,并把路径、版本、哈希等元数据存入数据库。** - -运行时通过 bridge 层完成一次“远端规则 → 本地临时文件”的转换: - -```text -数据库读取规则版本信息 - → 获取 rules.yaml 的 oss_url - → 从 OSS 下载到本地临时文件 - → 调用 leaudit.dsl.loader.load_rules_file(local_tmp_path) - → 交给 leaudit 原生执行链继续处理 -``` - -也就是说: - -- **规则真相源**:OSS + 数据库 -- **执行载体**:本地临时 YAML 文件 -- **LeAudit 输入接口**:保持不变 - -这是当前约束下最稳妥、最容易演进的方案。 - ---- - -## 3. 为什么不能继续把本地 `rules/` 目录作为正式真相源 - -如果未来要开放 YAML 在线编辑界面,本地目录方案会迅速暴露问题: - -### 3.1 不利于在线编辑 - -- 前端编辑后的内容最终仍要人工写回服务器目录 -- 多实例部署时,需要同步到多台机器 -- 容器化部署时,本地文件可能不是稳定持久层 - -### 3.2 不利于版本管理 - -- 难以明确记录“这次评查到底使用了哪一版规则” -- 覆盖同一路径的 `rules.yaml` 后,历史执行很难追溯 -- 回滚通常会退化为“手工替换文件” - -### 3.3 不利于审计与权限 - -- 谁改的、何时改的、为什么发布,很难形成正式审计链 -- 无法自然承载“编辑 / 审核 / 发布 / 回滚”权限流程 - -### 3.4 不利于多实例一致性 - -- API 节点 A 和 Worker 节点 B 可能读取到不同版本本地文件 -- 扩容后所有节点都要同步规则目录,运维成本高 - -因此,本地 `rules/` 目录更适合保留为: - -- 种子规则导入源 -- 紧急回退备份 -- 开发环境本地调试资源 - -而不应该继续承担正式规则真相源角色。 - ---- - -## 4. 为什么采用“OSS + DB + 本地临时文件”模式 - -该方案同时兼顾了 **不改核心** 和 **平台化管理** 两个目标。 - -### 4.1 对 `leaudit` 零侵入 - -`leaudit` 仍然读取本地 YAML 文件,无需改造其解析器、执行器或 DSL 加载逻辑。 - -### 4.2 支持在线编辑界面 - -前端提交 YAML 文本后,平台可以执行标准流程: - -```text -编辑 - → 保存草稿 - → 语法校验 - → DSL 语义校验 - → 上传 OSS - → 写入规则版本表 - → 发布 / 回滚 -``` - -这让规则成为“平台可管理资产”,而不是“服务器磁盘文件”。 - -### 4.3 规则版本可追溯 - -每次评查运行都可以记录: - -- `rule_set_id` -- `rule_version_id` -- `rule_source_oss_url` -- `rule_source_sha256` - -这样可以准确回答: - -- 这个结果用的是哪一版规则? -- 规则文件是否被篡改? -- 是否可以按历史版本回放? - -### 4.4 发布与回滚简单 - -- 发布:切换 `leaudit_rule_sets.current_version_id` -- 回滚:切回旧版本 ID - -无需登录服务器替换目录文件,也不要求应用重新发版。 - -### 4.5 多实例一致 - -所有 API / Worker 都从同一份 DB + OSS 真相源取规则,不再依赖本地目录是否同步。 - ---- - -## 5. 建议的系统分层 - -### 5.1 真相源 - -- **OSS**:存储 `rules.yaml` 正文文件 -- **数据库**:存储规则集、规则版本、绑定关系、发布状态、哈希、编辑人、发布时间等元数据 - -### 5.2 执行层 - -- bridge 层负责把 OSS 文件下载到本地临时路径 -- 临时路径交给 `leaudit.dsl.loader.load_rules_file()` 使用 - -### 5.3 回退层 - -- 本地 `rules/` 目录保留为 fallback 或 emergency backup -- 当 OSS 不可用或某些历史规则尚未迁移时可临时使用 - ---- - -## 6. 在线编辑功能设计 - -### 6.1 目标能力 - -平台应逐步具备以下功能: - -- 查看规则集列表 -- 查看规则版本历史 -- 查看某版本 YAML 内容 -- 在线编辑 YAML -- 保存草稿版本 -- 校验 YAML 语法 -- 校验 LeAudit DSL 语义 -- 发布指定版本 -- 回滚到历史版本 -- 查看发布日志与校验日志 - -### 6.2 推荐流程 - -#### 编辑保存 - -```text -前端提交 YAML 文本 - → 后端做 YAML 语法校验 - → 后端做 LeAudit DSL 语义校验 - → 生成新版本号 / version_seq - → 上传 rules.yaml 到 OSS - → 写 leaudit_rule_versions - → 返回版本信息 -``` - -#### 发布生效 - -```text -选择版本发布 - → 更新 leaudit_rule_sets.current_version_id - → 记录发布日志 - → 清理规则缓存 - → 后续新 run 自动使用新版本 -``` - -#### 回滚 - -```text -选择旧版本 - → 切换 current_version_id 到旧版本 - → 写回滚日志 - → 清理规则缓存 -``` - ---- - -## 7. 运行时加载设计 - -### 7.1 核心原则 - -运行时不直接让 `leaudit` 读取 OSS,也不直接读取数据库文本;而是通过 bridge 统一适配。 - -### 7.2 推荐加载链路 - -```text -document/type 确定 - → leaudit_rule_type_bindings 查规则集 - → leaudit_rule_sets.current_version_id - → leaudit_rule_versions.oss_url - → 下载 OSS 文件到本地临时目录 - → 校验 sha256 - → load_rules_file(local_path) - → 执行 leaudit pipeline -``` - -### 7.2.1 当前项目已落地状态 - -当前 bridge 已按这条路线开始落地,分成两条对称链路: - -- 文档文件链: - - `leaudit_document_files.local_path / oss_url` - - 下载或读取后落本地临时文件 - - 再交给原生 `AuditCtx.file_path` -- 规则文件链: - - `leaudit_rule_type_bindings` - - `leaudit_rule_sets.current_version_id` - - `leaudit_rule_versions.oss_url` - - 下载到本地临时 `rules.yaml` - - `RulesLoader.load(local_path)` - - `NativeRunner -> AuditService.audit(ctx)` - -这意味着后续开放 YAML 在线编辑界面时,不需要改 `leaudit` 核心,只要继续维护 “OSS + DB + 本地临时文件” 这条桥接链即可。 - -### 7.3 为什么必须保留“本地临时文件” - -因为当前约束是: - -- 不修改 `leaudit` 核心 -- `leaudit` 仍以本地路径作为 DSL 加载输入 - -所以本地临时文件不是“倒退”,而是一个必要的兼容层。 - ---- - -## 8. 与现有文档的一致性 - -该方案与当前 `docs/leaudit` 目录中的设计方向保持一致: - -- `docs/leaudit/dsl_rule_schema_design.md` - - 已提出“规则真相源 = OSS 文件 + 数据库索引” - - 已提出“运行时 DB → OSS → 本地临时 YAML → LeAudit loader” -- `docs/leaudit/bridge_directory_design.md` - - 已明确 bridge 负责规则加载与缓存 -- `docs/leaudit/processing_logic.md` - - 已明确 rules resolve 属于桥接层职责 - -本文件的重点是把“**为了未来开放 YAML 编辑界面,为什么必须这样设计**”单独说明清楚。 - ---- - -## 9. 当前项目建议 - -### 9.1 短期 - -- 保持本地 `rules/` 目录可用,确保现有流程可运行 -- 将其视为 fallback,而非长期正式真相源 - -### 9.2 中期 - -- 增加规则内容查看 / 编辑 / 保存 / 发布接口 -- 补齐 `leaudit_rule_versions` 的 OSS 文件上传和版本切换能力 -- 补统一 OSS 客户端与 presign / upload / version publish 能力 - -### 9.3 长期 - -- 后台提供完整 YAML 在线编辑器 -- 支持草稿、发布、回滚、审计 -- 清理本地硬编码规则映射,统一走规则绑定表 - ---- - -## 10. 最终结论 - -如果未来要开放 YAML 编辑界面,那么当前项目最合适的规则架构不是“继续依赖本地目录”,而是: - -- **OSS 存规则文件正文** -- **数据库存路径、版本、哈希、状态、发布信息** -- **运行时下载到本地临时文件后交给 `leaudit` 执行** - -这样既能保证: - -- 不修改 `leaudit` -- 兼容现有 DSL 加载方式 - -又能保证: - -- 在线编辑方便 -- 版本管理清晰 -- 发布回滚简单 -- 多实例一致 -- 运行结果可审计可追溯 - -这是当前项目向“规则可运营平台”演进时最合理的方案。 diff --git a/docs/规则编辑/为什么仍然需要Bridge适配层.md b/docs/规则编辑/为什么仍然需要Bridge适配层.md deleted file mode 100644 index 02df06b..0000000 --- a/docs/规则编辑/为什么仍然需要Bridge适配层.md +++ /dev/null @@ -1,296 +0,0 @@ -# 为什么仍然需要 Bridge 适配层 - -## 1. 结论先行 - -即使当前项目已经确认: - -- 后续应使用 `leaudit` 原生 `AuditCtx` -- 后续不应继续由平台自己手写主流程编排 -- 正式执行入口应收敛到 `AuditService.audit(ctx)` - -**也仍然需要保留 Bridge / 适配层。** - -原因不是因为不用原生 CTX,而恰恰是因为: - -> **要正确使用原生 CTX,就更应该把它封装在 Bridge 里。** - -也就是说,正确架构不是: - -```text -平台层 -> 直接调用 leaudit AuditCtx / AuditService -``` - -而是: - -```text -平台层 -> Bridge 适配层 -> leaudit AuditCtx / AuditService -``` - ---- - -## 2. 误区澄清 - -一个很容易出现的误区是: - -- 既然 `leaudit` 已经有原生 `AuditCtx` -- 那平台直接调用它就好了 -- Bridge 似乎没有必要 - -这个判断看起来简化了结构,但实际上会把平台和 `leaudit` 深度耦合起来,后续维护成本更高。 - -**真正应该取消的是:** - -- 平台自己重写 7 阶段编排 - -**不应该取消的是:** - -- 平台和 `leaudit` 之间的正式边界层 - -所以: - -- **不要自己编排** -- **但要保留适配层** - ---- - -## 3. 适配层到底在适配什么 - -Bridge 的本质,是把“平台世界”翻译成“引擎世界”,再把“引擎结果”翻译回“平台世界”。 - -## 3.1 平台世界 - -平台里实际关心的是这些对象: - -- `document_id` -- `document_file_id` -- `rule_set_id` -- `rule_version_id` -- `oss_url` -- `run_id` -- 用户触发信息 -- 权限信息 -- 数据库记录 -- 前端 DTO / VO - -## 3.2 引擎世界 - -`leaudit` 原生执行关心的是这些对象: - -- `file_path` -- `rules_file` -- `AuditServices` -- `AuditConfig` -- `AuditCtx` -- `AuditService.audit(ctx)` - -这两套概念体系并不相同,因此天然需要一层转换。 - ---- - -## 4. 为什么不能让平台层直接碰 leaudit 原生对象 - -## 4.1 会导致架构边界失守 - -如果 Controller / Service / Model 层直接构造 `AuditCtx`,那就意味着: - -- 平台业务代码开始直接依赖 `leaudit` -- `leaudit` 的内部概念会渗透到整个项目 -- 后面任何原生字段调整都会扩散到平台层 - -这会破坏当前项目一直强调的边界原则: - -- 平台层不直接感知 `leaudit` 内核细节 -- `leaudit_bridge/` 是唯一正式桥接层 - ---- - -## 4.2 会让平台逻辑和引擎逻辑搅在一起 - -平台侧还必须处理这些事情: - -- 文档文件从哪里取 -- OSS 文件如何下载 -- 规则版本从哪里查 -- run 如何创建 -- run 如何更新状态 -- 结果如何写回 `leaudit_*` 表 -- 前端如何查询结果 - -这些都不是 `leaudit` 原生 CTX 的职责。 - -如果平台层直接碰 `AuditCtx`,这些平台职责和引擎职责就会混在同一个 service 里,结构会越来越乱。 - ---- - -## 4.3 会让未来升级风险更大 - -如果以后 `leaudit` 升级: - -- `AuditCtx` 字段变更 -- `AuditService` 签名调整 -- `AuditServices` 装配方式变化 -- `AuditConfig` 配置项增加 - -那么: - -- 如果只有 bridge 感知这些对象,改动范围很小 -- 如果平台层很多地方直接依赖这些对象,改动会扩散全项目 - -因此,适配层的价值就在于: - -> 把 `leaudit` 变化锁死在边界层里。 - ---- - -## 5. Bridge 的正确职责 - -确认使用原生 CTX 后,Bridge 的职责应该重新定义为: - -## 5.1 输入适配 - -- 根据 `document_id` 找到待执行文档 -- 根据 `document_file_id` 找到文件真源 -- 如有需要,从 OSS 下载文档到本地临时路径 -- 根据 `type_id` / `rule_type_binding` 找到本次评查规则版本 -- 从 OSS 下载规则 YAML -- 解析出 `RulesFile` - -## 5.2 运行装配 - -- 创建 `AuditServices` -- 创建 `AuditConfig` -- 创建原生 `AuditCtx` -- 调用 `AuditService.audit(ctx)` - -## 5.3 输出适配 - -- 从最终 `ctx` 读取: - - `normalized_doc` - - `extraction` - - `phase` - - `evaluation` - - `fallback_tasks` - - `timing` -- 写入: - - `leaudit_audit_runs` - - `leaudit_rule_results` - - `leaudit_field_results` - - `leaudit_artifacts` - - `leaudit_run_metrics` - - `leaudit_run_errors` - -## 5.4 边界保护 - -- 平台 Controller / Service 不直接 import `leaudit.services.*` -- 只有 `leaudit_bridge/` 感知原生 `AuditCtx`、`AuditService`、`AuditServices` - ---- - -## 6. 三层结构图 - -推荐的结构应该是三层,而不是两层: - -```text -┌────────────────────────────────────────────┐ -│ 平台层 │ -│ Controller / Service / Model / API / DB │ -│ 文档、规则、权限、任务、结果查询 │ -└──────────────────┬─────────────────────────┘ - │ - ▼ -┌────────────────────────────────────────────┐ -│ Bridge 适配层 │ -│ file resolve / rules resolve / ctx build │ -│ AuditServices / AuditConfig / AuditCtx │ -│ persist ctx outputs to leaudit_* │ -└──────────────────┬─────────────────────────┘ - │ - ▼ -┌────────────────────────────────────────────┐ -│ leaudit 原生内核层 │ -│ AuditCtx / AuditService / Evaluation / │ -│ Extraction / Rescue / DSL loader │ -└────────────────────────────────────────────┘ -``` - -这个结构的关键点是: - -- 平台层不直接碰 `leaudit` 细节 -- `leaudit` 不直接感知平台数据库和 OSS -- 所有翻译工作都集中在 Bridge - ---- - -## 7. 为什么这比“平台直接调原生 CTX”更稳 - -### 优势 1:边界清晰 - -- 平台只管业务 -- `leaudit` 只管评查 -- Bridge 只管适配 - -### 优势 2:变化可控 - -- `leaudit` 升级时改 Bridge -- 平台结构基本不动 - -### 优势 3:便于替换 - -将来如果评查引擎变化: - -- 平台不需要全面改造 -- 只需替换适配层实现 - -### 优势 4:测试更容易 - -Bridge 可以单独测试: - -- 是否正确组装 `AuditCtx` -- 是否正确调用 `AuditService.audit(ctx)` -- 是否正确写回平台结果表 - ---- - -## 8. 对当前项目的直接要求 - -既然已经确认: - -- 必须使用原生 `AuditCtx` -- 不能继续自己编排主流程 - -那么当前项目应该同步修正为: - -### 不再推荐的方向 - -- 在 `fastapi_modules/fastapi_leaudit/leaudit_bridge/pipeline.py` 中继续手写 7 阶段流程 -- 在平台侧继续直接串联: - - OCR - - Extract - - Phase - - Evaluate - - Rescue - -### 推荐的方向 - -- `pipeline.py` 改造成薄包装器 -- 新增 `audit_ctx_builder.py` -- 新增 `audit_service_factory.py` -- 由 bridge 统一: - - build ctx - - run audit - - persist result - ---- - -## 9. 最终结论 - -确认使用原生 `AuditCtx` 之后,不是 Bridge 就没用了,反而说明: - -> **Bridge 不该负责重写编排,但必须负责原生编排的接入适配。** - -所以最终应该坚持这条原则: - -- **主流程执行:交给 `leaudit` 原生 `AuditService.audit(ctx)`** -- **平台边界控制:交给 `leaudit_bridge/`** - -这才是当前项目最稳妥、最可维护的长期方案。 diff --git a/docs/规则编辑/原生AuditCtx接入重构方案.md b/docs/规则编辑/原生AuditCtx接入重构方案.md index c270161..b30b1db 100644 --- a/docs/规则编辑/原生AuditCtx接入重构方案.md +++ b/docs/规则编辑/原生AuditCtx接入重构方案.md @@ -579,14 +579,50 @@ AuditServiceImpl.Run() --- +## 10.5 为什么仍然需要 Bridge 适配层 + +即使当前方向已经明确为: + +- 使用原生 `AuditCtx` +- 使用原生 `AuditService.audit(ctx)` +- 不再由平台自己手写 7 阶段主流程编排 + +也仍然必须保留 Bridge,原因是: + +1. 平台世界和引擎世界不是同一套对象 + - 平台关心:`document_id`、`rule_set_id`、`rule_version_id`、`oss_url`、`run_id` + - 引擎关心:`file_path`、`RulesFile`、`AuditServices`、`AuditConfig`、`AuditCtx` +2. 平台层还要处理 OSS 下载、run 状态、结果落库、前端查询 +3. 如果 Controller / Service 直接 import `leaudit.services.*`,边界会失守,后续升级会把改动扩散到整个平台 + +因此正确结构不是: + +```text +平台层 -> 直接调用 leaudit AuditCtx / AuditService +``` + +而是: + +```text +平台层 -> Bridge 适配层 -> leaudit AuditCtx / AuditService +``` + +Bridge 的固定职责应保持为: + +- 输入适配:文档真源、规则版本、OSS 下载、本地临时路径 +- 运行装配:`AuditServices`、`AuditConfig`、`AuditCtx` +- 输出适配:把 `ctx` 结果写回 `leaudit_*` 表 +- 边界保护:只有 `leaudit_bridge/` 感知原生 `leaudit` 类型 + +所以这次重构真正要取消的,是“平台自己编排评查主流程”,不是 Bridge 本身。 + +--- + ## 11. 与现有文档的关系 本方案是以下文档的进一步收敛: -- `docs/规则编辑/yaml规则在线编辑设计.md` -- `docs/规则编辑/跑通全流程所需准备项.md` -- `docs/规则编辑/开发任务拆解清单.md` -- `docs/规则编辑/为什么仍然需要Bridge适配层.md` +- `docs/规则编辑/统一OSS与规则管理实施计划.md` 它的核心新增点是: diff --git a/docs/规则编辑/开发任务拆解清单.md b/docs/规则编辑/开发任务拆解清单.md deleted file mode 100644 index fe55f44..0000000 --- a/docs/规则编辑/开发任务拆解清单.md +++ /dev/null @@ -1,711 +0,0 @@ -# LeAudit 开发任务拆解清单 - -## 1. 目标 - -基于以下两份文档,进一步拆出一份可执行的开发任务清单,并尽量精确到建议修改文件: - -- `docs/规则编辑/yaml规则在线编辑设计.md` -- `docs/规则编辑/跑通全流程所需准备项.md` - -本清单覆盖的目标不是单点“规则编辑”,而是完整业务链路: - -```text -上传文档 - → 获取文件真源 - → OCR - → 抽取 - → 评查 - → 结果落库 - → 查询运行状态 / 结果 - → 再扩展到 YAML 在线编辑 / 发布 / 回滚 -``` - ---- - -## 2. 当前代码现状摘要 - -在开始任务前,先明确当前代码的真实状态。 - -> 2026-04-27 补充结论:结合 `/home/wren-dev/Porject/leaudit/src` 源码确认, -> 当前 `leaudit` 的正式执行入口应视为: -> `AuditCtx` + `AuditService.audit(ctx)`。 -> 因此本清单中的后续任务,默认都以“保留 Bridge,但禁止平台自己重写主流程编排”为前提。 - -### 2.1 已有骨架 - -- 评查控制器: - - `fastapi_modules/fastapi_leaudit/controllers/auditController.py` -- 评查服务接口/实现: - - `fastapi_modules/fastapi_leaudit/services/auditService.py` - - `fastapi_modules/fastapi_leaudit/services/impl/auditServiceImpl.py` -- bridge 层: - - `fastapi_modules/fastapi_leaudit/leaudit_bridge/pipeline.py` - - `fastapi_modules/fastapi_leaudit/leaudit_bridge/tasks.py` - - `fastapi_modules/fastapi_leaudit/leaudit_bridge/rules_loader.py` - - `fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py` - - `fastapi_modules/fastapi_leaudit/leaudit_bridge/ctx_builder.py` -- 模型: - - `fastapi_modules/fastapi_leaudit/models/leauditDocument.py` - - `fastapi_modules/fastapi_leaudit/models/leauditDocumentFile.py` - - `fastapi_modules/fastapi_leaudit/models/leauditAuditRun.py` -- 规则服务接口骨架: - - `fastapi_modules/fastapi_leaudit/services/ruleService.py` - - `fastapi_modules/fastapi_leaudit/domian/vo/ruleVo.py` - -### 2.2 当前主要缺口 - -- `AuditServiceImpl.Run()` 已可创建 run 并触发 NativeRunner 任务 -- `GetResult()` 已可查询 `leaudit_rule_results` -- 规则文件主链已开始支持 `run -> rule_version -> oss_url -> 本地临时 YAML` -- `tasks.py` 仍保留 `LEAUDIT_RULES_DIR` 和 `_TYPE_ID_RULES_MAP` 作为 fallback -- 尚未看到规则编辑控制器与 `RuleServiceImpl` -- 尚未形成统一 OSS 文件服务 -- 结果写入仍有“按 document_id 找最新 run”的简化逻辑 - -因此,任务拆解应该分两层: - -- **P0:先把上传 → OCR → 抽取 → 评查 → 查询打通** -- **P1/P2:再把规则 OSS 化、版本化、在线编辑化** - ---- - -## 3. 分阶段开发任务清单 - -## P0:先打通最小评查闭环 - -目标: - -```text -上传文档 - → 创建 document / file / run - → bridge 执行 OCR / 抽取 / 评查 - → 落库 - → 查到 run 状态与结果 -``` - ---- - -### P0-1:补齐评查服务入口 - -#### 任务说明 - -把 `POST /api/audit/run` 从“只抛异常”改成真正可执行的评查入口。 - -#### 需要修改 - -- `fastapi_modules/fastapi_leaudit/services/impl/auditServiceImpl.py` -- `fastapi_modules/fastapi_leaudit/services/auditService.py` -- `fastapi_modules/fastapi_leaudit/domian/Dto/auditDto.py` -- `fastapi_modules/fastapi_leaudit/domian/vo/auditVo.py` - -#### 具体工作 - -- 在 `Run()` 中完成: - - 校验文档是否存在 - - 查当前可执行文件版本 - - 计算 `run_no` - - 创建 `leaudit_audit_runs` - - 调用 `dispatch_leaudit_task()` - - 返回 `AuditRunVO` -- 调整 `IAuditService.Run()` 的接口定义,使其与实现参数一致 -- 如有必要,为 `AuditRunDTO` 增加可选字段: - - `documentFileId` - - `force` - - `ruleType` - - `ruleVersionId`(可选,便于指定版本重跑) - -#### 产出目标 - -- 调用 `/api/audit/run` 不再报 “Celery 任务集成待实现” -- 至少能创建 run 并触发 bridge 层执行 - ---- - -### P0-2:补齐 run 创建与状态更新逻辑 - -#### 任务说明 - -把 run 作为整条链的中心对象,保证每次执行都能明确追踪。 - -#### 需要修改 - -- `fastapi_modules/fastapi_leaudit/models/leauditAuditRun.py` -- `fastapi_modules/fastapi_leaudit/services/impl/auditServiceImpl.py` -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/tasks.py` -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py` - -#### 具体工作 - -- 在触发评查前创建 `leaudit_audit_runs` -- 明确 run 初始字段: - - `status=pending` - - `phase=normalize` 或空 - - `startedAt` - - `documentFileId` - - `ruleSetId` - - `ruleVersionId` -- 执行链中显式传递 `run_id` -- `storage_adapter.py` 所有落库方法改为: - - 不再“按 `document_id` 查最新 run” - - 统一显式使用 `run_id` - -#### 产出目标 - -- 所有结果写入能严格绑定到唯一 `run_id` -- 避免多次重跑 / 并发时结果串写 - ---- - -### P0-3:补齐文件输入链 - -#### 任务说明 - -在执行 OCR 前,明确“这次评查使用哪一个文件”。 - -#### 需要修改 - -- `fastapi_modules/fastapi_leaudit/models/leauditDocument.py` -- `fastapi_modules/fastapi_leaudit/models/leauditDocumentFile.py` -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/ctx_builder.py` -- `fastapi_modules/fastapi_leaudit/services/impl/auditServiceImpl.py` - -#### 建议新增 - -- `fastapi_modules/fastapi_leaudit/services/documentService.py` -- `fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py` - 或 -- `fastapi_modules/fastapi_leaudit/services/fileService.py` -- `fastapi_modules/fastapi_leaudit/services/impl/fileServiceImpl.py` - -#### 具体工作 - -- 增加“获取当前有效文件”的服务方法 -- 根据 `document_id` 找到当前激活的 `leaudit_document_files` -- 如果文件在 OSS,先下载到本地临时路径 -- 给 pipeline 提供稳定的 `file_path` - -#### 产出目标 - -- 评查入口不依赖调用方直接传原始字节 -- 可以从数据库+文件真源独立还原执行输入 - ---- - -### P0-4:打通 bridge 任务入口 - -#### 任务说明 - -让 `dispatch_leaudit_task()` 真正成为评查执行入口,而不是演示性同步封装。 -但注意:这里的“执行入口”不是继续扩写平台自编排 pipeline,而是逐步过渡到: - -```text -build AuditCtx - → call AuditService.audit(ctx) - → persist ctx outputs -``` - -#### 需要修改 - -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/tasks.py` -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/__init__.py` -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/pipeline.py` - -#### 建议新增 - -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/audit_ctx_builder.py` -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/audit_service_factory.py` - -#### 具体工作 - -- 统一 `dispatch_leaudit_task()` 的入参: - - `run_id` - - `document_id` - - `document_file_id` - - `rules_path` 或 `rule_version_id` - - 可选 `trigger_user_id` -- 逐步去掉 `source_port` 作为主上下文依赖 -- 允许先同步执行,后续再切 Celery -- 逐步让 `pipeline.py` 退化为薄包装层,而不是 7 阶段自编排器 -- 在 bridge 内部统一完成: - - `AuditServices` 构造 - - `AuditConfig` 构造 - - 原生 `AuditCtx` 构造 - - `AuditService.audit(ctx)` 调用 - -#### 产出目标 - -- 业务层只调一个稳定入口 -- bridge 层掌控实际执行上下文 -- 主流程编排回归 `leaudit` 原生服务层 - ---- - -### P0-5:补齐结果查询接口 - -#### 任务说明 - -不仅要能跑,还要能看到结果。 - -#### 需要修改 - -- `fastapi_modules/fastapi_leaudit/services/impl/auditServiceImpl.py` -- `fastapi_modules/fastapi_leaudit/controllers/auditController.py` -- `fastapi_modules/fastapi_leaudit/domian/vo/auditVo.py` - -#### 具体工作 - -- `GetRunStatus()` 查询真实 run 状态 -- `GetResult()` 从以下表联合查询: - - `leaudit_audit_runs` - - `leaudit_rule_results` - - 可选 `leaudit_field_results` -- 把 `rules=[]` 改成真实规则级返回 -- 如有必要,为 `AuditResultVO` 增加: - - `timing` - - `fields` - - `errors` - -#### 产出目标 - -- `/api/audit/result/{RunId}` 能返回真实评查结果 - ---- - -### P0-6:补齐结果落库结构 - -#### 任务说明 - -当前 `StorageAdapter` 已有雏形,但还需要工程化加固。 - -#### 需要修改 - -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py` -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/result_adapter.py` - -#### 具体工作 - -- 所有保存方法显式接收 `run_id` -- 补 `run_metrics` 写入 -- 补 `run_errors` 写入 -- 梳理 `artifacts` 与 `field_results` 的最小必要字段 -- 保持 `rule_results` 与 `AuditResultVO` 的结构一致 - -#### 产出目标 - -- 结果写入不再依赖“最新 run”猜测 -- 后续前端查询更稳定 - ---- - -## P1:把规则执行链切到 OSS + DB - -目标: - -```text -文档类型 - → 绑定表 - → 规则集 - → 当前版本 - → OSS YAML - → 下载本地临时文件 - → leaudit 执行 -``` - ---- - -### P1-1:补规则读取服务 - -#### 任务说明 - -把规则加载从“本地目录路径”升级成“DB + OSS + 临时文件”。 - -#### 当前状态 - -这一项已经完成第一阶段落地: - -- `auditServiceImpl.py` 会在建 run 时锁定 `ruleVersionId` 与 `ruleSourceOssUrl` -- `tasks.py` 会优先按 `run_id` 解析规则来源 -- `ruleVersionResolver.py` 会把 OSS YAML 下载到本地临时文件 -- `RulesLoader.load(local_path)` 已接入执行链 - -当前剩余工作已经从“能不能执行”变成“如何把上传 / 发布 / 缓存 / 回收做完整”。 - -#### 需要修改 - -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/tasks.py` -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/nativeRunner.py` -- `fastapi_modules/fastapi_leaudit/services/impl/auditServiceImpl.py` - -#### 建议新增 - -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/ruleVersionResolver.py` - -#### 具体工作 - -- 根据 run 锁定的 `rule_version_id` / `rule_source_oss_url` 解析规则来源 -- 下载 YAML 到本地临时文件 -- 校验 `rule_source_sha256` -- 调 `load_rules_file(local_path)` -- 继续保留本地 `rules/` 作为 fallback - -#### 产出目标 - -- 执行时优先走 run 绑定规则版本,而不是 `_TYPE_ID_RULES_MAP` -- 原生 `AuditCtx.rules_file` 由 bridge 正式注入,而不是平台手工绕过服务编排层 - ---- - -### P1-2:补统一 OSS 文件服务 - -#### 任务说明 - -项目当前有 OSS 配置,但缺少统一文件服务。 - -#### 需要修改 - -- `fastapi_admin/config/_settings.py`(仅在配置不够时) - -#### 建议新增 - -- `fastapi_common/fastapi_common_storage/__init__.py` -- `fastapi_common/fastapi_common_storage/oss_client.py` -- `fastapi_common/fastapi_common_storage/oss_path_utils.py` - -如果不想放到 `fastapi_common/`,也可以先放: - -- `fastapi_modules/fastapi_leaudit/services/ossService.py` -- `fastapi_modules/fastapi_leaudit/services/impl/ossServiceImpl.py` - -#### 具体工作 - -- 提供统一方法: - - 上传文件到 OSS - - 下载到本地临时文件 - - 计算 / 校验 sha256 - - 删除临时文件 -- 同时兼容: - - 规则文件 - - 原始文档 - - 评查产物 - -#### 产出目标 - -- 文档文件和规则文件都可以共用一套对象存储服务 - ---- - -### P1-3:补规则版本与绑定的查询模型 - -#### 任务说明 - -当前代码里还没有看到对应规则表的 ORM / 查询对象,后续查询会比较痛苦。 - -#### 建议新增 - -- `fastapi_modules/fastapi_leaudit/models/leauditRuleSet.py` -- `fastapi_modules/fastapi_leaudit/models/leauditRuleVersion.py` -- `fastapi_modules/fastapi_leaudit/models/leauditRuleTypeBinding.py` -- 更新 `fastapi_modules/fastapi_leaudit/models/__init__.py` - -#### 具体工作 - -- 为规则集、规则版本、绑定表建立 ORM -- 后续服务层不必到处手写 SQL - -#### 产出目标 - -- 规则管理服务、规则解析服务都能清晰建模 - ---- - -## P2:开放 YAML 在线编辑 / 发布 / 回滚 - -目标: - -让规则成为后台可管理资产,而不是服务器上的裸文件。 - ---- - -### P2-1:补规则控制器与服务实现 - -#### 任务说明 - -当前只有 `IRuleService` 接口,需要真正落地规则后台。 - -#### 需要修改 - -- `fastapi_modules/fastapi_leaudit/services/ruleService.py` -- `fastapi_modules/fastapi_leaudit/domian/vo/ruleVo.py` - -#### 建议新增 - -- `fastapi_modules/fastapi_leaudit/controllers/ruleController.py` -- `fastapi_modules/fastapi_leaudit/services/impl/ruleServiceImpl.py` -- `fastapi_modules/fastapi_leaudit/domian/Dto/ruleDto.py` - -#### 具体工作 - -- 完成接口: - - 列表 - - 版本历史 - - 查看内容 - - 保存版本 - - 校验 - - 发布 - - 回滚 -- 为 controller 增加路由 - -#### 产出目标 - -- 后台可以真正管理规则 - ---- - -### P2-2:补规则内容查看/保存接口 - -#### 建议新增 - -- `fastapi_modules/fastapi_leaudit/domian/Dto/ruleContentDto.py` - -#### 具体工作 - -- 定义: - - 保存 YAML 文本请求 DTO - - 规则校验响应 VO - - 规则内容响应 VO -- 从 OSS 读回内容展示给前端 -- 新版本保存时先写 OSS,再写 DB - -#### 产出目标 - -- 前端可以拿到 YAML 文本并保存新版本 - ---- - -### P2-3:补 YAML 语法校验 + DSL 语义校验 - -#### 建议新增 - -- `fastapi_modules/fastapi_leaudit/services/ruleValidationService.py` -- `fastapi_modules/fastapi_leaudit/services/impl/ruleValidationServiceImpl.py` - -#### 具体工作 - -- YAML 解析校验 -- `leaudit` DSL schema 校验 -- 提取 metadata 快照 -- 形成标准错误列表 - -#### 产出目标 - -- 发布前可拦截坏规则 - ---- - -### P2-4:补发布、回滚、审计 - -#### 需要修改 - -- `fastapi_modules/fastapi_leaudit/services/impl/ruleServiceImpl.py` - -#### 建议新增 - -- `fastapi_modules/fastapi_leaudit/models/leauditRulePublishLog.py` -- `fastapi_modules/fastapi_leaudit/models/leauditRuleValidationLog.py` - -如果暂时不建 ORM,也至少需要: - -- 对应 SQL migration / 建表脚本 - -#### 具体工作 - -- 发布时更新 `current_version_id` -- 写发布日志 -- 回滚时写回滚日志 -- 记录操作者与时间 - -#### 产出目标 - -- 规则变更具备可审计性 - ---- - -## P3:补平台级工程能力 - -目标: - -让系统从“能跑”升级到“可持续运行”。 - ---- - -### P3-1:Celery / Redis 正式接入 - -#### 需要修改 - -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/tasks.py` -- `fastapi_modules/fastapi_leaudit/tasks/__init__.py` -- `fastapi_admin/config/_settings.py` - -#### 建议新增 - -- `fastapi_common/fastapi_common_cache/redis_pool.py` -- `fastapi_modules/fastapi_leaudit/tasks/celery_app.py` - -#### 具体工作 - -- 同步任务改为异步分发 -- 配置任务超时 / 重试 / 队列 - ---- - -### P3-2:规则缓存与发布失效 - -#### 需要修改 - -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/rules_loader.py` -- `fastapi_modules/fastapi_leaudit/services/impl/ruleServiceImpl.py` - -#### 具体工作 - -- 规则缓存 key 改为 `rule_version_id` 或 `oss_url + sha256` -- 发布后清缓存 -- 多 worker 时设计统一失效策略 - ---- - -### P3-3:结果增强与诊断能力 - -#### 需要修改 - -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py` -- `fastapi_modules/fastapi_leaudit/services/impl/auditServiceImpl.py` - -#### 具体工作 - -- 完整落 `run_metrics` -- 完整落 `run_errors` -- 补 `rescue_outcomes` -- 前端可查看错误详情、阶段耗时 - ---- - -## 4. 建议新增文件总表 - -下面是我建议优先考虑新增的文件,便于你按模块建任务: - -### 规则管理 - -- `fastapi_modules/fastapi_leaudit/controllers/ruleController.py` -- `fastapi_modules/fastapi_leaudit/services/impl/ruleServiceImpl.py` -- `fastapi_modules/fastapi_leaudit/services/ruleValidationService.py` -- `fastapi_modules/fastapi_leaudit/services/impl/ruleValidationServiceImpl.py` -- `fastapi_modules/fastapi_leaudit/services/ruleResolverService.py` -- `fastapi_modules/fastapi_leaudit/services/impl/ruleResolverServiceImpl.py` -- `fastapi_modules/fastapi_leaudit/domian/Dto/ruleDto.py` -- `fastapi_modules/fastapi_leaudit/domian/Dto/ruleContentDto.py` - -### 规则 ORM - -- `fastapi_modules/fastapi_leaudit/models/leauditRuleSet.py` -- `fastapi_modules/fastapi_leaudit/models/leauditRuleVersion.py` -- `fastapi_modules/fastapi_leaudit/models/leauditRuleTypeBinding.py` -- `fastapi_modules/fastapi_leaudit/models/leauditRulePublishLog.py` -- `fastapi_modules/fastapi_leaudit/models/leauditRuleValidationLog.py` - -### 文件与存储 - -- `fastapi_common/fastapi_common_storage/oss_client.py` -- `fastapi_common/fastapi_common_storage/oss_path_utils.py` - -### 原生 AuditCtx 接入 - -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/audit_ctx_builder.py` -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/audit_service_factory.py` - -### 文档执行输入 - -- `fastapi_modules/fastapi_leaudit/services/documentService.py` -- `fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py` - -### 异步任务 / 缓存 - -- `fastapi_modules/fastapi_leaudit/tasks/celery_app.py` -- `fastapi_common/fastapi_common_cache/redis_pool.py` - ---- - -## 5. 建议优先修改文件总表 - -如果按“先把主链跑通”的角度,最优先改的文件是: - -1. `fastapi_modules/fastapi_leaudit/services/impl/auditServiceImpl.py` -2. `fastapi_modules/fastapi_leaudit/leaudit_bridge/tasks.py` -3. `fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py` -4. `fastapi_modules/fastapi_leaudit/leaudit_bridge/rules_loader.py` -5. `fastapi_modules/fastapi_leaudit/leaudit_bridge/ctx_builder.py` -6. `fastapi_modules/fastapi_leaudit/domian/vo/auditVo.py` -7. `fastapi_modules/fastapi_leaudit/services/ruleService.py` -8. `fastapi_modules/fastapi_leaudit/domian/vo/ruleVo.py` -9. `fastapi_modules/fastapi_leaudit/models/__init__.py` -10. `fastapi_admin/config/_settings.py` - ---- - -## 6. 推荐执行顺序 - -如果按最稳妥的方式推进,我建议这样做: - -### 第一阶段:只做主链闭环 - -- 改 `AuditServiceImpl` -- 改 `tasks.py` -- 新增 `audit_ctx_builder.py` -- 新增 `audit_service_factory.py` -- 改 `storage_adapter.py` -- 改 `GetResult()` - -目标:先能跑通上传后评查与结果查询。 - -### 第二阶段:切规则到 OSS + DB - -- 补规则 ORM -- 改 `rules_loader.py` -- 加 OSS 文件服务 - -目标:评查执行真正使用数据库发布的规则版本。 - -### 第三阶段:开放规则后台 - -- 加 `ruleController.py` -- 加 `RuleServiceImpl` -- 加校验服务 -- 加发布/回滚日志 - -目标:前端可编辑、发布、回滚 YAML。 - -### 第四阶段:工程化增强 - -- Celery / Redis -- 缓存失效 -- 审计 -- metrics / errors / rescue - -目标:从“能跑”变成“可运营”。 - ---- - -## 7. 一句话结论 - -如果你要的是“基于当前代码库,把功能拆成能开发的任务”,那么真正的主线不是先做编辑器,而是: - -1. 先把 `auditServiceImpl + bridge + storageAdapter` 打通 -2. 再把 `rules_loader` 从本地目录切到 `OSS + DB` -3. 最后再做 `ruleController + RuleServiceImpl + 校验/发布/回滚` - -也就是说: - -- **第一优先级是评查主链** -- **第二优先级是规则执行链** -- **第三优先级才是规则编辑后台** - -这样开发成本最低,验证路径也最清晰。 diff --git a/docs/规则编辑/统一OSS与规则管理实施计划.md b/docs/规则编辑/统一OSS与规则管理实施计划.md index a19e14c..aa43e99 100644 --- a/docs/规则编辑/统一OSS与规则管理实施计划.md +++ b/docs/规则编辑/统一OSS与规则管理实施计划.md @@ -1,3 +1,10 @@ +# LeAudit 规则链路实施总方案 + +> 说明:这份文档是当前 `docs/规则编辑/` 目录的主实施文档。 +> 原 `开发任务拆解清单.md` 的任务顺序、文件清单与验收口径,后续统一以本文件为准。 +> 原 `跑通全流程所需准备项.md` 的能力清单,也统一归并到本文件维护。 +> 原 `yaml规则在线编辑设计.md` 与 `worker并发执行改造方案.md` 的核心口径,也统一归并到本文件维护。 + ## 背景 当前 `leaudit-platform` 已经完成第一阶段原生执行链接入: diff --git a/docs/规则编辑/跑通全流程所需准备项.md b/docs/规则编辑/跑通全流程所需准备项.md deleted file mode 100644 index 458d2ef..0000000 --- a/docs/规则编辑/跑通全流程所需准备项.md +++ /dev/null @@ -1,520 +0,0 @@ -# LeAudit 跑通全流程所需准备项 - -## 1. 范围说明 - -本文记录的不是“仅把 YAML 规则搬到 OSS”这一件事,而是 **跑通 LeAudit 整个业务链路** 所需要补齐的能力。 - -这里的“跑通全流程”明确指: - -```text -上传文档 - → 获取文件真源 - → OCR - → 规则解析 - → 字段抽取 - → 评查 - →(可选)Rescue - → 结果落库 - → 前端查询运行状态和结果 -``` - -也就是说,目标不是只做“规则编辑”,而是要让下面这条链条在当前项目内真实可执行: - -- 用户上传文档 -- 平台找到该文档对应的规则版本 -- Bridge 调用 `leaudit` 引擎执行 OCR / Extract / Evaluate -- 结果写入 `leaudit_*` 表 -- 前端可以查询 run 状态和评查结果 - ---- - -## 2. 当前状态判断 - -当前项目的设计方向是对的,但距离“整条链真正可跑通”还有明显缺口。 - -> 2026-04-27 补充结论:结合 `/home/wren-dev/Porject/leaudit/src` 源码确认,`leaudit` -> 当前已经形成正式的服务编排层:`AuditCtx` + -> `AuditService.audit(ctx)`。因此本项目后续不应长期维持“平台自己手搓 -> OCR / Extract / Evaluate / Rescue 编排”的模式,而应保留 bridge, -> 但把 bridge 改造成“平台对象 -> 原生 AuditCtx -> 原生 AuditService -> 平台持久化”的适配层。 - -### 2.1 已具备的基础 - -- 已有 `docs/leaudit/` 一整套设计文档 -- 已有 `leaudit_*` 相关表设计与部分模型 -- 已有 bridge 目录骨架: - - `pipeline.py` - - `tasks.py` - - `rules_loader.py` - - `storage_adapter.py` -- 已有规则集 / 规则版本 / 绑定表设计 -- 已有 YAML 在线编辑设计文档:`docs/规则编辑/yaml规则在线编辑设计.md` - -### 2.2 当前仍未闭环的关键问题 - -- 评查服务入口还没有真正触发可执行任务 -- 规则加载仍以本地目录 / 硬编码过渡方案为主 -- OSS 规则文件上传 / 下载 / 校验链未补齐 -- 规则后台控制面未落地 -- 运行结果与 `run_id` 的强绑定还不够严格 -- 上传文件 → 文件真源 → 本地临时文件 → pipeline 的输入链还未完全收口 - -所以现在更准确的说法是: - -- **架构蓝图已成型** -- **部分代码骨架已存在** -- **但全流程尚未真正打通** - ---- - -## 3. 跑通整个流程,必须补齐的 8 大能力块 - -## 3.1 上传链路与文档真源 - -要跑通 OCR / 抽取 / 评查,首先必须保证上传文档在系统里成为一个稳定的“可执行输入”。 - -### 需要准备的能力 - -- 上传接口能够接收主文档 / 附件 -- 上传后写入 `leaudit_documents` -- 文件元数据写入 `leaudit_document_files` -- 原始文件上传到 OSS 或稳定本地真源 -- 为每个运行锁定 `document_file_id` -- 需要时可把文档从 OSS 下载到本地临时路径供 `leaudit` 读取 - -### 为什么这是前提 - -`leaudit` 执行时依赖本地文件路径,因此即便业务真源在 OSS,执行阶段仍要有: - -```text -document_file.oss_url - → 下载到本地临时文件 - → pipeline.run(file_path=local_tmp_path) -``` - -如果这一层不稳定,后面 OCR、抽取、评查都无从谈起。 - ---- - -## 3.2 评查运行主线(Run)管理 - -整条链必须围绕 `leaudit_audit_runs` 来组织,否则运行结果会失去可追踪性。 - -### 需要准备的能力 - -- 触发评查时先创建一条 `leaudit_audit_runs` -- 记录: - - `document_id` - - `document_file_id` - - `run_no` - - `trigger_source` - - `status` - - `rule_set_id` - - `rule_version_id` - - `rule_source_oss_url` - - `rule_source_sha256` -- 运行中逐步更新: - - `status` - - `phase` - - `started_at` - - `finished_at` - - 汇总统计字段 - -### 当前项目缺口 - -当前 `AuditServiceImpl.Run()` 还没有真正创建和分发 run,只是直接抛出“Celery 任务集成待实现”: - -- `fastapi_modules/fastapi_leaudit/services/impl/auditServiceImpl.py` - -因此,当前“触发评查”这一步实际上还没有闭环。 - ---- - -## 3.3 规则管理控制面 - -如果未来要做 YAML 在线编辑,那么规则一定不能只是本地 `rules/` 目录,而必须成为平台管理对象。 - -### 需要准备的能力 - -- 规则集列表 -- 规则版本列表 -- 查看某个版本 YAML 内容 -- 保存草稿版本 -- 发布指定版本 -- 回滚到历史版本 -- 规则编辑 / 发布权限控制 -- 发布 / 回滚审计 - -### 建议最少接口 - -- `GET /rule-sets` -- `GET /rule-sets/{rule_type}/versions` -- `GET /rule-versions/{id}/content` -- `POST /rule-sets/{rule_type}/versions` -- `POST /rule-sets/{rule_type}/validate` -- `POST /rule-sets/{rule_type}/publish` -- `POST /rule-sets/{rule_type}/rollback` - -### 当前项目缺口 - -当前只有规则服务接口定义的一小部分骨架: - -- `fastapi_modules/fastapi_leaudit/services/ruleService.py` - -尚未形成完整规则后台能力。 - ---- - -## 3.4 规则文件存储链(OSS + DB) - -这部分是“在线编辑”和“运行执行”之间的核心桥梁。 - -### 需要准备的能力 - -- YAML 文本上传到 OSS -- OSS 路径写入 `leaudit_rule_versions.oss_url` -- 同步保存: - - `file_sha256` - - `file_size` - - `metadata_type_id` - - `metadata_name` - - `metadata_version` -- 下载规则文件到本地临时目录 -- 下载后校验 sha256 -- 发布后能根据 `current_version_id` 找到正在生效的版本 - -### 运行时目标链路 - -```text -leaudit_rule_type_bindings - → leaudit_rule_sets.current_version_id - → leaudit_rule_versions.oss_url - → 下载到本地临时文件 - → load_rules_file(local_path) - → 执行 pipeline -``` - -### 当前项目状态 - -当前项目已经开始按“原生 AuditCtx + Bridge 适配”方向落地两条来源链: - -- 文档文件: - - 已支持 `leaudit_document_files.local_path` - - 也已支持 `leaudit_document_files.oss_url` - - Worker 执行前会统一落为本地临时文件 -- 规则文件: - - 已支持 `leaudit_audit_runs.rule_source_oss_url` - - 运行时按 `run -> rule_version -> oss_url` 下载 YAML 到本地临时文件 - - 再交给 `RulesLoader.load(local_path)` 与 `NativeRunner` 执行 - -当前仍保留 fallback: - -- `LEAUDIT_RULES_DIR` -- `_TYPE_ID_RULES_MAP` -- 本地 `rules/` 目录 - -也就是说,“DB + OSS -> 本地临时 YAML”的正式主链已经接入,旧本地目录逻辑仅作为兼容回退。 - ---- - -## 3.5 规则校验链 - -规则编辑能力一旦开放,就必须要有保存前 / 发布前校验,否则很容易把错误 YAML 发到线上。 - -### 至少需要两层校验 - -#### 1)YAML 语法校验 - -- 缩进是否正确 -- 结构是否可解析 -- 基本字段是否存在 - -#### 2)LeAudit DSL 语义校验 - -- `metadata` 是否完整 -- `type_id` / `version` / `name` 是否可识别 -- rule / stage / extract 结构是否符合 `leaudit` 的 DSL 约束 -- 规则中引用的字段是否存在 -- phase / activate_if / score / risk 等配置是否合理 - -### 需要准备的结果形式 - -- 校验是否通过 -- 错误列表 -- 警告列表 -- 可选:抽取出的 metadata 快照 - ---- - -## 3.6 执行链:OCR → 抽取 → 评查 → Rescue - -这是整个系统真正的“核心业务流水线”。 - -### 需要准备的能力 - -- OCR 客户端可正常调用 -- 文档分类 / rules resolve 可正常执行 -- `dispatch_extract()` 能跑通字段抽取 -- `evaluate_extraction()` 能完成规则评查 -- 如果平台定义最终结果包含 Rescue,则补齐 rescue 阶段 -- 执行链中每个阶段都要能记录错误与耗时 - -### 当前项目情况 - -`pipeline.py` 已有主链骨架: - -- OCR -- 抽取 -- 坐标解析 -- phase detection -- evaluate - -见: - -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/pipeline.py` - -但当前还存在这些问题: - -- 结果存储依赖“按 document_id 查最新 run”这种简化逻辑 -- rescue 尚未形成完整闭环 -- 任务上下文仍残留 `source_port` 过渡参数 -- 当前 `pipeline.py` 是平台侧自编排器,而不是调用 `leaudit` 原生 - `AuditService.audit(ctx)` 的适配包装器 - -因此还不能认为“执行链已经完全生产可用”。 - -### 最新架构修正建议 - -基于 `leaudit` 源码核对,正式建议改成: - -```text -平台文档/规则/配置 - → bridge 解析输入 - → 构造 AuditServices - → 构造 AuditConfig - → 构造原生 AuditCtx - → 调用 AuditService.audit(ctx) - → 从最终 ctx 提取产物落库 -``` - -也就是说: - -- bridge 继续保留 -- 但 bridge 不再负责自己重写 7 阶段编排 -- bridge 负责“适配”和“持久化” -- `leaudit` 原生 `AuditService.audit(ctx)` 负责“执行” - ---- - -## 3.7 结果落库与查询链 - -跑通全流程不只是引擎执行成功,还包括结果能写进去、查出来。 - -### 需要准备的能力 - -- OCR 产物写入 `leaudit_artifacts` -- 字段抽取结果写入 `leaudit_field_results` -- 规则评查结果写入 `leaudit_rule_results` -- 运行指标写入 `leaudit_run_metrics` -- 错误信息写入 `leaudit_run_errors` -- 补救结果写入 `leaudit_rescue_outcomes` -- 更新 `leaudit_audit_runs` 汇总字段 -- 前端可查询: - - run 状态 - - 规则级结果 - - 抽取字段 - - 汇总统计 - -### 当前项目缺口 - -当前 `StorageAdapter` 与 `AuditServiceImpl.GetResult()` 已明显向前推进,但还有工程缺口: - -- 仍保留少量“按 document_id 找最新 run”的兼容路径 -- 结果查询虽已能查规则/字段/errors/rescue/metrics/artifacts,但还缺前端最终展示口径梳理 - -见: - -- `fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py` -- `fastapi_modules/fastapi_leaudit/services/impl/auditServiceImpl.py` - -这说明“结果查询主链已基本打通,但前端展示闭环仍待联调确认”。 - ---- - -## 3.8 异步任务、缓存、幂等与审计 - -如果只在本地同步跑 Demo,可以先简化;但如果要真的作为平台运行,就必须补齐基础工程能力。 - -### 需要准备的能力 - -#### 任务调度 - -- Celery / Redis 异步任务 -- 任务超时 -- 失败重试 -- 队列优先级 - -#### 幂等与并发 - -- 同一个文档重复点击“评查”如何处理 -- 同一 `run_id` 重试还是新建 run -- 避免结果串写到错误 run - -#### 缓存失效 - -- 新规则发布后,旧缓存如何失效 -- 多 worker 下规则缓存如何同步更新 - -#### 审计 - -- 谁上传了规则 -- 谁发布了规则 -- 谁触发了评查 -- 该评查具体用了哪版规则 - -### 当前项目缺口 - -- Celery 仍未真正接入业务主链 -- 发布后的规则缓存失效机制未明确实现 -- 审计日志表和日志落库链未形成闭环 - ---- - -## 4. 从“规则编辑”到“全流程可跑”的完整依赖链 - -如果要把功能讲清楚,可以把整个系统拆成下面这条依赖链: - -```text -【A】上传文档 - → 写 leaudit_documents / leaudit_document_files - → 文件进 OSS - -【B】编辑规则 - → YAML 文本保存 - → 语法/语义校验 - → 上传 rules.yaml 到 OSS - → 写 leaudit_rule_versions - → 发布切换 current_version_id - -【C】触发评查 - → 创建 leaudit_audit_runs - → 锁定 document_file_id + rule_version_id - → 分发任务 - -【D】bridge 执行 - → 下载文档到本地临时文件 - → 下载规则到本地临时 YAML - → OCR - → Extract - → Evaluate - → Rescue(如启用) - -【E】结果写回 - → artifacts / field_results / rule_results / metrics / errors - → 更新 audit_runs 汇总 - -【F】前端查询 - → 查 run 状态 - → 查规则结果 - → 查字段结果 - → 展示最终评查报告 -``` - -只有 A~F 都闭环,才能说“跑通整个流程”。 - ---- - -## 5. 建议的实施优先级 - -## P0:先跑通最小闭环 - -目标:先让“上传文档 -> 触发评查 -> OCR/抽取/评查 -> 结果查询”最小可用。 - -### P0 需要完成 - -- 上传文件能落真源 -- `AuditServiceImpl.Run()` 真正可触发 -- 创建 `leaudit_audit_runs` -- `pipeline.run()` 真正执行 -- `StorageAdapter` 明确按 `run_id` 写结果 -- `GetRunStatus()` / `GetResult()` 能查到真实数据 - -> 注意:这一阶段甚至可以暂时继续兼容本地 `rules/`,重点先是把业务主链打通。 - ---- - -## P1:切换规则真相源到 OSS + DB - -目标:让规则不再依赖本地目录作为正式来源。 - -### P1 需要完成 - -- 规则版本上传到 OSS -- `leaudit_rule_versions` 完整入库 -- `leaudit_rule_type_bindings` 真正生效 -- `tasks.py` / `rules_loader.py` 走 `DB -> OSS -> 本地临时 YAML` -- `_TYPE_ID_RULES_MAP` 降级为 fallback - ---- - -## P2:开放 YAML 在线编辑能力 - -目标:让规则成为后台可管理资产。 - -### P2 需要完成 - -- 规则列表 / 版本历史 / YAML 内容查看 -- 编辑保存 -- YAML 语法校验 -- DSL 语义校验 -- 发布 / 回滚 -- 权限与审计 - ---- - -## P3:补齐平台级工程能力 - -目标:让系统从“能跑”升级到“稳定可运营”。 - -### P3 需要完成 - -- Celery 多队列 -- Redis 缓存与缓存失效 -- 幂等控制 -- 失败重试 -- 规则缓存刷新 -- 跨区域权限 / 审计 -- run_metrics / run_errors / rescue_outcomes 全量落库 - ---- - -## 6. 一句话结论 - -如果目标是“跑通整个流程:上传、OCR、抽取、评查”,那么除了“把规则 YAML 放 OSS、路径放数据库”之外,还必须同时补齐: - -- 上传文件真源链 -- 评查 run 主线 -- 规则控制面 -- 规则文件上传/下载/校验链 -- 正式执行链 -- 结果落库与结果查询链 -- 任务、缓存、幂等、审计等基础设施能力 - -所以这不是一个单点功能,而是一条完整平台链路的闭环建设。 - ---- - -## 7. 当前项目的核心推进建议 - -按实际落地顺序,建议当前项目这样推进: - -1. 先打通“上传 -> 触发评查 -> run -> 结果查询”最小链路 -2. 再把规则解析从本地目录切到 `OSS + DB` -3. 然后再做 YAML 在线编辑、发布、回滚 -4. 最后补缓存、审计、并发、重试等平台级能力 - -这样做的好处是: - -- 可以尽快验证主业务链是否真实可用 -- 不会在规则后台还没落地时就把复杂度全部堆上来 -- 能明确区分“能跑通”和“能运营”的阶段目标