diff --git a/auth_doc/API_RESPONSE_EXAMPLES_V3.2.md b/auth_doc/API_RESPONSE_EXAMPLES_V3.2.md deleted file mode 100644 index 810c1b9..0000000 --- a/auth_doc/API_RESPONSE_EXAMPLES_V3.2.md +++ /dev/null @@ -1,529 +0,0 @@ -# RBAC 路由权限 API 响应示例 v3.2 - -## 📋 文档说明 - -本文档展示 v3.2 版本 RBAC 路由权限接口的完整响应格式。 - -**v3.2 核心变更**: -- ✅ 使用 `status` 字段管理路由启用/禁用状态 -- ✅ 取消勾选后路由不删除,只是设置为 status=0 -- ✅ 接口返回 `enabled` 字段标识路由是否启用 -- ✅ 仅省级管理员可修改路由权限 - ---- - -## 1️⃣ GET /rbac/roles/{role_id}/routes - -### 功能说明 -获取角色关联的所有路由(包括禁用的),每个路由带 `enabled` 字段 - -### 请求示例 -```bash -GET /rbac/roles/1/routes -Authorization: Bearer -``` - -### 响应示例 -```json -{ - "code": 200, - "msg": "success", - "data": { - "role_id": 1, - "routes": [ - { - "id": 31, - "route_path": "/home", - "route_name": "Home", - "route_title": "系统概览", - "parent_id": null, - "icon": "ri-dashboard-line", - "sort_order": 1, - "is_hidden": false, - "component": "views/Home.vue", - "enabled": true, - "permissions": [ - { - "id": 10, - "permission_key": "dashboard:stats:view", - "display_name": "查看统计数据", - "api_method": "GET", - "api_path": "/api/v3/dashboard/stats" - } - ] - }, - { - "id": 2, - "route_path": "/documents", - "route_name": "Documents", - "route_title": "文件管理", - "parent_id": null, - "icon": "ri-folder-3-line", - "sort_order": 2, - "is_hidden": false, - "component": "views/documents/Index.vue", - "enabled": false, - "permissions": [ - { - "id": 20, - "permission_key": "document:list:read", - "display_name": "查看文档列表", - "api_method": "GET", - "api_path": "/api/v2/documents" - } - ], - "children": [ - { - "id": 59, - "route_path": "/files/upload", - "route_name": "FilesUpload", - "route_title": "文件上传", - "parent_id": 2, - "enabled": false, - "permissions": [] - } - ] - }, - { - "id": 41, - "route_path": "/rules", - "route_name": "Rules", - "route_title": "评查规则库", - "parent_id": null, - "icon": "ri-book-3-line", - "sort_order": 3, - "is_hidden": false, - "component": "views/rules/Index.vue", - "enabled": true, - "permissions": [ - { - "id": 28, - "permission_key": "evaluation_group:list:read", - "display_name": "查看评查点分组列表", - "api_method": "GET", - "api_path": "/api/v3/evaluation-point-groups" - } - ] - } - ] - } -} -``` - -### 字段说明 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `enabled` | boolean | **true=启用(前端显示为勾选)**, **false=禁用(前端显示为未勾选)** | -| `permissions` | array | 路由关联的API权限列表 | - -### 前端使用示例 - -```javascript -// 获取角色路由 -const response = await fetch('/rbac/roles/1/routes') -const { routes } = response.data - -// 提取启用的路由ID(用于Tree组件初始勾选) -const checkedKeys = [] -const extractEnabled = (routeList) => { - routeList.forEach(route => { - if (route.enabled) { - checkedKeys.push(route.id) - } - if (route.children) { - extractEnabled(route.children) - } - }) -} -extractEnabled(routes) - -// 设置Tree组件默认勾选 -treeRef.value.setCheckedKeys(checkedKeys) -``` - ---- - -## 2️⃣ PUT /rbac/roles/{role_id}/routes - -### 功能说明 -批量更新角色路由权限(状态管理模式) -- route_ids 中的路由设置为启用(status=1) -- 不在 route_ids 中的路由设置为禁用(status=0) -- **仅省级管理员可调用** - -### 请求示例 -```bash -PUT /rbac/roles/1/routes -Authorization: Bearer -Content-Type: application/json - -{ - "route_ids": [31, 41], - "permission": "RW" -} -``` - -### 响应示例(成功) -```json -{ - "code": 200, - "msg": "success", - "data": { - "role_id": 1, - "enabled_count": 2, - "disabled_count": 1, - "inserted_count": 0, - "route_ids": [31, 41] - } -} -``` - -### 响应字段说明 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `enabled_count` | int | 本次启用的路由数量 | -| `disabled_count` | int | 本次禁用的路由数量 | -| `inserted_count` | int | 本次新插入的路由数量 | -| `route_ids` | array | 启用的路由ID列表 | - -### 响应示例(权限不足) -```json -{ - "code": 4003, - "msg": "权限不足:仅省级管理员可以修改角色路由权限", - "data": null -} -``` - -### 前端使用示例 - -```javascript -// 保存路由权限 -const saveRoutePermissions = async (roleId) => { - const checkedKeys = treeRef.value.getCheckedKeys() - - try { - const response = await fetch(`/rbac/roles/${roleId}/routes`, { - method: 'PUT', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - route_ids: checkedKeys, - permission: 'RW' - }) - }) - - const result = await response.json() - - if (result.code === 200) { - ElMessage.success( - `成功启用 ${result.data.enabled_count} 个路由,` + - `禁用 ${result.data.disabled_count} 个路由` - ) - - // 刷新路由列表以更新enabled状态 - await loadRoutes() - } else if (result.code === 4003) { - ElMessage.error('权限不足:仅省级管理员可以修改路由权限') - } - } catch (error) { - ElMessage.error('保存失败:' + error.message) - } -} -``` - ---- - -## 3️⃣ 完整工作流程示例 - -### Vue 3 + Element Plus 完整组件 - -```vue - - - - - -``` - ---- - -## 4️⃣ 数据库表结构 - -### role_route 表(v3.2更新) - -```sql -CREATE TABLE role_route ( - id SERIAL PRIMARY KEY, - role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE, - route_id INTEGER NOT NULL REFERENCES sys_routes(id) ON DELETE CASCADE, - permission VARCHAR(10) DEFAULT 'RW', - status SMALLINT DEFAULT 1 NOT NULL, -- ⭐新增:0=禁用,1=启用 - created_at TIMESTAMP(6) WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP(6) WITH TIME ZONE DEFAULT NOW(), - - CONSTRAINT role_route_role_id_route_id_key UNIQUE (role_id, route_id), - CONSTRAINT chk_role_route_status CHECK (status IN (0, 1)) -); - --- 索引 -CREATE INDEX idx_role_route_status ON role_route(status); -CREATE INDEX idx_role_route_role_status ON role_route(role_id, status); - --- 注释 -COMMENT ON COLUMN role_route.status IS '状态:0=禁用,1=启用'; -``` - ---- - -## 5️⃣ 核心逻辑说明 - -### 状态管理逻辑 - -```python -# 批量更新时的逻辑: -# 1. 先将所有路由设置为禁用(status=0) -UPDATE role_route SET status = 0 WHERE role_id = ? - -# 2. 将勾选的路由设置为启用(status=1) -UPDATE role_route SET status = 1 WHERE role_id = ? AND route_id IN (?) - -# 3. 如果记录不存在,插入新记录(status默认为1) -INSERT INTO role_route (role_id, route_id, permission, status) -VALUES (?, ?, 'RW', 1) -``` - -### 查询逻辑 - -```python -# 查询时返回所有路由(包括禁用的) -SELECT - sr.*, - rr.status as route_status -FROM role_route rr -JOIN sys_routes sr ON rr.route_id = sr.id -WHERE rr.role_id = ? -- 不限制status,返回所有记录 - -# 将 status 字段映射为 enabled -route['enabled'] = route['route_status'] == 1 -``` - ---- - -## 📝 版本对比 - -| 特性 | v3.1 | v3.2 | -|------|------|------| -| 取消勾选行为 | 删除记录 | 设置 status=0 | -| 路由显示 | 取消勾选后消失 | 取消勾选后仍显示 | -| 数据库操作 | DELETE | UPDATE status | -| 权限检查 | 无 | 仅省级管理员 | -| 返回字段 | assigned_count, removed_count | enabled_count, disabled_count, inserted_count | - ---- - -## 💡 常见问题 - -### Q1: 为什么取消勾选后路由还显示? -**A**: v3.2 采用状态管理模式,取消勾选只是将 status 设置为 0(禁用),不会删除记录。这样可以保留历史配置,方便恢复。 - -### Q2: 非省级管理员调用修改接口会怎样? -**A**: 返回 403 错误,提示"权限不足:仅省级管理员可以修改角色路由权限" - -### Q3: enabled 和 assigned 有什么区别? -**A**: -- `enabled` (v3.2): 基于 role_route.status 字段,表示路由是否启用 -- `assigned` (v3.2旧版): 基于记录是否存在,已废弃 - -### Q4: 如何查看只启用的路由? -**A**: 调用接口时传参 `include_disabled=false`(默认为 true,返回所有) - ---- - -**文档版本**: v3.2 -**最后更新**: 2025-11-28 -**维护者**: Backend Team diff --git a/auth_doc/API_RESPONSE_EXAMPLES_V3.3.md b/auth_doc/API_RESPONSE_EXAMPLES_V3.3.md index 7a89c7f..adb770f 100644 --- a/auth_doc/API_RESPONSE_EXAMPLES_V3.3.md +++ b/auth_doc/API_RESPONSE_EXAMPLES_V3.3.md @@ -89,7 +89,7 @@ Authorization: Bearer "id": 41, "route_path": "/rules", "route_name": "Rules", - "route_title": "评查规则库", + "route_title": "规则管理", "parent_id": null, "icon": "ri-book-3-line", "sort_order": 3, diff --git a/auth_doc/entry_modules_api(1).md b/auth_doc/entry_modules_api(1).md deleted file mode 100644 index a2b654d..0000000 --- a/auth_doc/entry_modules_api(1).md +++ /dev/null @@ -1,1931 +0,0 @@ -# 入口模块管理 API 完整技术文档 - -> **供前端对接使用** | 更新时间:2025-01-29 | 版本:v2.1 - -## 📋 目录 - -1. [概述](#概述) -2. [数据库设计](#数据库设计) -3. [技术架构](#技术架构) -4. [API接口详细说明](#api接口详细说明) -5. [业务逻辑详解](#业务逻辑详解) -6. [错误处理机制](#错误处理机制) -7. [前端对接指南](#前端对接指南) - ---- - -## 概述 - -**入口模块管理系统**负责管理系统首页的功能入口模块,支持多地区独立配置、图标管理和动态展示控制。 - -### 基本信息 - -| 项目 | 说明 | -|------|------| -| **基础路径** | `/api/v3/entry-modules` | -| **认证方式** | JWT Bearer Token | -| **后端框架** | FastAPI + asyncpg + PostgreSQL | -| **存储服务** | MinIO (对象存储) | -| **权限系统** | RBAC v2 (基于装饰器) | - -### 核心功能 - -- ✅ 入口模块的增删改查(CRUD) -- ✅ 分页查询 + 模糊搜索 + 地区筛选 -- ✅ 多地区独立配置(启用/禁用、排序) -- ✅ 图标上传到MinIO(自动覆盖策略) -- ✅ RBAC权限控制(基于 `entry_module:*:*` 权限代码) - ---- - -## 数据库设计 - -### entry_modules 表结构 - -```sql -CREATE TABLE entry_modules ( - -- 主键,自增 - id SERIAL PRIMARY KEY, - - -- 入口模块名称,唯一且非空 - name VARCHAR(255) NOT NULL UNIQUE, - - -- 类型描述,可为空 - description TEXT, - - -- 更新时间(自动维护) - updated_at TIMESTAMP(6) WITH TIME ZONE DEFAULT NOW(), - - -- 创建时间(自动维护) - created_at TIMESTAMP(6) WITH TIME ZONE DEFAULT NOW(), - - -- 入口菜单的图片存储路径(MinIO路径) - path VARCHAR(255), - - -- 地区配置 JSONB数组 - -- 格式: [{"area": "地区名", "enabled": true/false, "sort_order": 排序号}] - areas JSONB -); -``` - -### 索引设计 - -```sql --- 主键索引(自动创建) -CREATE UNIQUE INDEX entry_modules_pkey ON entry_modules USING btree (id); - --- 名称唯一索引(保证模块名称不重复) -CREATE UNIQUE INDEX entry_modules_name_key ON entry_modules USING btree (name); - --- JSONB GIN索引(加速 areas 字段的 JSONB 查询) -CREATE INDEX idx_entry_modules_areas ON entry_modules USING GIN (areas); -``` - -**GIN索引作用**: -- 支持 `@>` 操作符进行高效的 JSONB 包含查询 -- 加速地区筛选查询(`WHERE areas @> '[{"area": "梅州"}]'::jsonb`) - -### 触发器 - -```sql --- 自动更新 updated_at 字段的触发器 -CREATE TRIGGER update_entry_modules_updated_at - BEFORE UPDATE ON entry_modules - FOR EACH ROW - EXECUTE FUNCTION update_updated_at(); -``` - -**触发器逻辑**: -```sql -CREATE OR REPLACE FUNCTION update_updated_at() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; -``` - -**说明**:每次 `UPDATE` 操作时,自动将 `updated_at` 字段更新为当前时间戳,无需在应用层手动设置。 - -### areas 字段 JSONB 格式 - -```json -[ - { - "area": "梅州", // 地区名称(字符串) - "enabled": true, // 是否启用(布尔值) - "sort_order": 1 // 排序号(整数,越小越靠前) - }, - { - "area": "云浮", - "enabled": true, - "sort_order": 2 - }, - { - "area": "揭阳", - "enabled": false, // 禁用状态 - "sort_order": 3 - } -] -``` - -### 实际数据示例 - -```sql --- 实际数据查询结果 -id | name | description | path | areas | created_at | updated_at -----+--------------+-------------------------+---------------------------------------------------+---------------------------------------------------------------------------+--------------------------------+-------------------------------- -1 | 合同管理 | 合同管理入口模块 | documents/mz/static/img/entry_module_1.png | [{"area":"梅州","enabled":true,"sort_order":1},{"area":"云浮","enabled":true,"sort_order":2}] | 2025-11-18 21:33:33.857417+08 | 2025-11-28 11:05:38.421705+08 -2 | 案卷智能评查 | 案卷类型的入口模块 | entryModule/aj | [{"area":"梅州","enabled":true,"sort_order":1},{"area":"揭阳","enabled":true,"sort_order":2}] | 2025-11-18 21:34:48.467438+08 | 2025-11-26 20:44:31.438596+08 -3 | 内部公文 | 内部公文的入口模块 | entryModule/nw | [{"area":"云浮","enabled":true,"sort_order":1},{"area":"梅州","enabled":true,"sort_order":2}] | 2025-11-18 21:35:09.977711+08 | 2025-11-28 09:44:18.20009+08 -``` - ---- - -## 技术架构 - -### 三层架构设计 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 路由层 (app/routes/entry_modules.py) │ -│ - FastAPI路由注册 │ -│ - 请求参数验证(Pydantic) │ -│ - JWT权限校验(verify_token + require_permission_v2) │ -│ - 统一响应封装(@unified_resp) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 服务层 (app/services/entry_module_service.py) │ -│ - 业务逻辑处理 │ -│ - 数据库操作(asyncpg直连) │ -│ - MinIO文件操作(core/storage/minio_client.py) │ -│ - 数据转换(JSONB ↔ Python对象) │ -│ - 异常处理和日志记录 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 数据层 │ -│ ├─ PostgreSQL (entry_modules表) │ -│ └─ MinIO (图片文件存储) │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 数据库连接管理 - -**连接方式**:asyncpg(异步PostgreSQL驱动) - -```python -@staticmethod -async def _get_db_connection(): - """ - 获取数据库连接 - - 环境变量: - - DB_HOST: 数据库主机 - - DB_PORT: 数据库端口 - - DB_NAME: 数据库名称 - - DB_USER: 数据库用户 - - DB_PASSWORD: 数据库密码 - """ - conn = await asyncpg.connect( - host=os.getenv("DB_HOST"), - port=int(os.getenv("DB_PORT")), - user=os.getenv("DB_USER"), - password=os.getenv("DB_PASSWORD"), - database=os.getenv("DB_NAME") - ) - return conn -``` - -**连接生命周期**: -1. 每个服务方法独立获取连接 -2. 使用 `try...finally` 确保连接关闭 -3. 不使用连接池(使用asyncpg自身的连接管理) - -### MinIO存储配置 - -**配置来源**:`core/config.py` 的 `MINIO_CONFIG` - -```python -MINIO_CONFIG = { - "endpoint": "172.16.0.81:9000", # MinIO服务地址 - "access_key": "minioadmin", # 访问密钥 - "secret_key": "minioadmin", # 秘密密钥 - "bucket_name": "docauditai", # 存储桶名称 - "secure": False, # 是否使用HTTPS - "base_url": "http://172.16.0.81:9000/docauditai/" # 访问URL前缀 -} -``` - -**客户端初始化**: -```python -def _get_minio_client() -> MinioStorage: - return MinioStorage( - endpoint=MINIO_CONFIG["endpoint"], - access_key=MINIO_CONFIG["access_key"], - secret_key=MINIO_CONFIG["secret_key"], - bucket_name=MINIO_CONFIG["bucket_name"], - secure=MINIO_CONFIG.get("secure", False), - base_url=MINIO_CONFIG.get("base_url", ""), - ) -``` - ---- - -## API接口详细说明 - -### 1. 获取入口模块列表 - -#### 接口定义 - -```http -GET /api/v3/entry-modules?page=1&page_size=20&name=合同&area=梅州 -Authorization: Bearer {JWT_TOKEN} -``` - -#### 权限要求 - -| 权限代码 | 说明 | -|---------|------| -| `entry_module:list:read` | 查看入口模块列表 | - -#### 请求参数 - -| 参数 | 类型 | 必填 | 默认值 | 校验规则 | 说明 | -|------|------|------|--------|---------|------| -| page | int | 否 | 1 | `>= 1` | 页码,从1开始 | -| page_size | int | 否 | 20 | `>= 1`, `<= 100` | 每页数量,最大100条 | -| name | string | 否 | - | - | 模块名称模糊搜索(ILIKE) | -| area | string | 否 | - | - | 地区筛选(精确匹配) | - -#### 业务逻辑流程 - -``` -1. 参数验证 - ├─ page: 自动校正为 >=1 - ├─ page_size: 限制在 1~100 之间 - ├─ name: 可选字符串(支持中文) - └─ area: 可选字符串(地区名称) - -2. 数据库连接 - └─ 调用 _get_db_connection() 获取 asyncpg 连接 - -3. 构建查询条件 - ├─ name 参数 → "name ILIKE $1" with "%{name}%" - ├─ area 参数 → "areas @> $2::jsonb" with '[{"area": "{area}"}]' - └─ 无参数时 → WHERE 1=1 - -4. 执行查询 - ├─ 步骤1: SELECT COUNT(*) 获取总数 - └─ 步骤2: SELECT * LIMIT/OFFSET 获取分页数据 - -5. 数据转换 - └─ JSONB字段 → Python字典(如果是字符串则JSON解析) - -6. 返回响应 - └─ { total, page, page_size, items } -``` - -#### SQL执行示例 - -**场景1:无筛选条件** -```sql --- 查询总数 -SELECT COUNT(*) FROM entry_modules WHERE 1=1; - --- 查询数据(第1页,每页20条) -SELECT id, name, description, path, areas, created_at, updated_at -FROM entry_modules -WHERE 1=1 -ORDER BY id ASC -LIMIT 20 OFFSET 0; -``` - -**场景2:名称模糊搜索** -```sql --- 查询总数 -SELECT COUNT(*) FROM entry_modules WHERE name ILIKE '%合同%'; - --- 查询数据 -SELECT id, name, description, path, areas, created_at, updated_at -FROM entry_modules -WHERE name ILIKE '%合同%' -ORDER BY id ASC -LIMIT 20 OFFSET 0; -``` - -**场景3:地区筛选** -```sql --- 查询总数 -SELECT COUNT(*) FROM entry_modules -WHERE areas @> '[{"area": "梅州"}]'::jsonb; - --- 查询数据 -SELECT id, name, description, path, areas, created_at, updated_at -FROM entry_modules -WHERE areas @> '[{"area": "梅州"}]'::jsonb -ORDER BY id ASC -LIMIT 20 OFFSET 0; -``` - -**场景4:复合筛选** -```sql --- 查询总数 -SELECT COUNT(*) FROM entry_modules -WHERE name ILIKE '%合同%' - AND areas @> '[{"area": "梅州"}]'::jsonb; - --- 查询数据 -SELECT id, name, description, path, areas, created_at, updated_at -FROM entry_modules -WHERE name ILIKE '%合同%' - AND areas @> '[{"area": "梅州"}]'::jsonb -ORDER BY id ASC -LIMIT 20 OFFSET 0; -``` - -#### 响应示例 - -**成功响应(200)** -```json -{ - "code": 0, - "msg": "成功", - "data": { - "total": 3, - "page": 1, - "page_size": 20, - "items": [ - { - "id": 1, - "name": "合同管理", - "description": "合同管理入口模块", - "path": "documents/mz/static/img/entry_module_1.png", - "areas": [ - {"area": "梅州", "enabled": true, "sort_order": 1}, - {"area": "云浮", "enabled": true, "sort_order": 2} - ], - "created_at": "2025-11-18T21:33:33.857417+08:00", - "updated_at": "2025-11-28T11:05:38.421705+08:00" - }, - { - "id": 2, - "name": "案卷智能评查", - "description": "案卷类型的入口模块", - "path": "entryModule/aj", - "areas": [ - {"area": "梅州", "enabled": true, "sort_order": 1}, - {"area": "揭阳", "enabled": true, "sort_order": 2} - ], - "created_at": "2025-11-18T21:34:48.467438+08:00", - "updated_at": "2025-11-26T20:44:31.438596+08:00" - } - ] - } -} -``` - -**错误响应(500)** -```json -{ - "code": 500, - "msg": "查询入口模块列表失败: connection to server failed", - "data": [] -} -``` - ---- - -### 2. 获取入口模块详情 - -#### 接口定义 - -```http -GET /api/v3/entry-modules/{module_id} -Authorization: Bearer {JWT_TOKEN} -``` - -#### 权限要求 - -| 权限代码 | 说明 | -|---------|------| -| `entry_module:detail:read` | 查看入口模块详情 | - -#### 路径参数 - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| module_id | int | 是 | 模块ID(必须为正整数) | - -#### 业务逻辑流程 - -``` -1. 参数验证 - └─ module_id: FastAPI自动校验为int类型 - -2. 数据库查询 - └─ SELECT * FROM entry_modules WHERE id = $1 - -3. 存在性检查 - ├─ 查询结果为空 → 抛出 HTTPException(404) - └─ 查询结果存在 → 继续处理 - -4. 数据转换 - └─ JSONB字段 → Python字典 - -5. 返回响应 - └─ 模块详情字典 -``` - -#### SQL执行 - -```sql -SELECT id, name, description, path, areas, created_at, updated_at -FROM entry_modules -WHERE id = 1; -- $1 参数化查询 -``` - -#### 响应示例 - -**成功响应(200)** -```json -{ - "code": 0, - "msg": "成功", - "data": { - "id": 1, - "name": "合同管理", - "description": "合同管理入口模块", - "path": "documents/mz/static/img/entry_module_1.png", - "areas": [ - {"area": "梅州", "enabled": true, "sort_order": 1}, - {"area": "云浮", "enabled": true, "sort_order": 2} - ], - "created_at": "2025-11-18T21:33:33.857417+08:00", - "updated_at": "2025-11-28T11:05:38.421705+08:00" - } -} -``` - -**错误响应(404)** -```json -{ - "code": 404, - "msg": "入口模块不存在: id=999", - "data": [] -} -``` - ---- - -### 3. 创建入口模块 - -#### 接口定义 - -```http -POST /api/v3/entry-modules -Content-Type: application/json -Authorization: Bearer {JWT_TOKEN} - -{ - "name": "合同管理", - "description": "合同管理入口模块", - "path": null, - "areas": [ - {"area": "梅州", "enabled": true, "sort_order": 1}, - {"area": "云浮", "enabled": true, "sort_order": 2} - ] -} -``` - -#### 权限要求 - -| 权限代码 | 说明 | -|---------|------| -| `entry_module:create:write` | 创建入口模块 | - -#### 请求体字段 - -| 字段 | 类型 | 必填 | 校验规则 | 说明 | -|------|------|------|---------|------| -| name | string | 是 | `max_length=255`, 不能重复 | 模块名称 | -| description | string | 否 | - | 模块描述 | -| path | string | 否 | `max_length=255` | 图标路径(通常为null,后续通过上传接口设置) | -| areas | AreaConfig[] | 否 | 每项需包含 area, enabled, sort_order | 地区配置数组 | - -**AreaConfig 结构:** -```typescript -interface AreaConfig { - area: string; // 地区名称(必填) - enabled: boolean; // 是否启用(必填) - sort_order: number; // 排序号(必填) -} -``` - -#### 业务逻辑流程 - -``` -1. 请求体验证 - ├─ Pydantic Schema 自动验证 - ├─ name: 非空,最大255字符 - ├─ description: 可选字符串 - ├─ path: 可选字符串,最大255字符 - └─ areas: 可选数组,每项包含 area/enabled/sort_order - -2. 数据库连接 - └─ 调用 _get_db_connection() - -3. 唯一性检查 - ├─ SQL: SELECT id FROM entry_modules WHERE name = $1 - ├─ 结果存在 → 抛出 HTTPException(400, "模块名称已存在") - └─ 结果不存在 → 继续处理 - -4. 数据转换 - └─ areas 数组 → JSON字符串(json.dumps) - -5. 插入数据 - └─ SQL: INSERT INTO entry_modules (name, description, path, areas) - VALUES ($1, $2, $3, $4) - RETURNING * - -6. 数据转换 - └─ JSONB字段 → Python字典 - -7. 日志记录 - └─ logger.info(f"入口模块创建成功: id={item['id']}, name={name}") - -8. 返回响应 - └─ 新创建的模块详情 -``` - -#### SQL执行示例 - -**唯一性检查:** -```sql -SELECT id FROM entry_modules WHERE name = '合同管理'; --- 结果:如果存在返回id,不存在返回空 -``` - -**插入数据:** -```sql -INSERT INTO entry_modules (name, description, path, areas) -VALUES ( - '合同管理', - '合同管理入口模块', - NULL, - '[{"area":"梅州","enabled":true,"sort_order":1},{"area":"云浮","enabled":true,"sort_order":2}]'::jsonb -) -RETURNING id, name, description, path, areas, created_at, updated_at; -``` - -**插入后触发器自动设置:** -- `created_at`: NOW()(由DEFAULT值设置) -- `updated_at`: NOW()(由DEFAULT值设置) -- `id`: SERIAL自增 - -#### 响应示例 - -**成功响应(200)** -```json -{ - "code": 0, - "msg": "成功", - "data": { - "id": 4, - "name": "合同管理", - "description": "合同管理入口模块", - "path": null, - "areas": [ - {"area": "梅州", "enabled": true, "sort_order": 1}, - {"area": "云浮", "enabled": true, "sort_order": 2} - ], - "created_at": "2025-11-28T16:30:00.123456+08:00", - "updated_at": "2025-11-28T16:30:00.123456+08:00" - } -} -``` - -**错误响应(400)- 名称重复** -```json -{ - "code": 400, - "msg": "模块名称已存在: name=合同管理", - "data": [] -} -``` - ---- - -### 4. 更新入口模块 - -#### 接口定义 - -```http -PUT /api/v3/entry-modules/{module_id} -Content-Type: application/json -Authorization: Bearer {JWT_TOKEN} - -{ - "name": "合同管理v2", - "description": "更新后的描述", - "areas": [ - {"area": "梅州", "enabled": true, "sort_order": 1} - ] -} -``` - -#### 权限要求 - -| 权限代码 | 说明 | -|---------|------| -| `entry_module:update:write` | 更新入口模块 | - -#### 请求参数 - -**路径参数:** -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| module_id | int | 是 | 模块ID | - -**请求体字段(所有可选):** -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| name | string | 否 | 模块名称(如修改需验证唯一性) | -| description | string | 否 | 模块描述 | -| path | string | 否 | 图标路径 | -| areas | AreaConfig[] | 否 | 地区配置(**完全替换**) | - -#### 业务逻辑流程 - -``` -1. 参数验证 - ├─ module_id: FastAPI自动验证 - └─ 请求体: Pydantic Schema 验证(所有字段可选) - -2. 数据库连接 - └─ 调用 _get_db_connection() - -3. 存在性检查 - ├─ SQL: SELECT id, name FROM entry_modules WHERE id = $1 - ├─ 结果为空 → 抛出 HTTPException(404) - └─ 结果存在 → 继续处理 - -4. 名称唯一性检查(如果更新name) - ├─ 判断: name != existing['name'] - ├─ SQL: SELECT id FROM entry_modules WHERE name = $1 AND id != $2 - ├─ 结果存在 → 抛出 HTTPException(400, "模块名称已存在") - └─ 结果不存在 → 继续处理 - -5. 动态构建UPDATE语句 - ├─ 收集传入的字段(非None) - ├─ 构建 SET 子句: "name = $1, description = $2, ..." - ├─ 没有字段传入 → 抛出 HTTPException(400, "没有要更新的字段") - └─ 自动添加: updated_at = CURRENT_TIMESTAMP - -6. 执行更新 - └─ SQL: UPDATE entry_modules SET ... WHERE id = $n RETURNING * - -7. 数据转换 - └─ JSONB字段 → Python字典 - -8. 日志记录 - └─ logger.info(f"入口模块更新成功: id={module_id}") - -9. 返回响应 - └─ 更新后的模块详情 -``` - -#### SQL执行示例 - -**场景1:只更新 name** -```sql --- 存在性检查 -SELECT id, name FROM entry_modules WHERE id = 1; --- 返回: {"id": 1, "name": "合同管理"} - --- 名称唯一性检查 -SELECT id FROM entry_modules WHERE name = '合同管理v2' AND id != 1; --- 返回: 空(不存在重复) - --- 执行更新 -UPDATE entry_modules -SET name = '合同管理v2', updated_at = CURRENT_TIMESTAMP -WHERE id = 1 -RETURNING id, name, description, path, areas, created_at, updated_at; -``` - -**场景2:更新多个字段** -```sql -UPDATE entry_modules -SET - name = '合同管理v2', - description = '更新后的描述', - areas = '[{"area":"梅州","enabled":true,"sort_order":1}]'::jsonb, - updated_at = CURRENT_TIMESTAMP -WHERE id = 1 -RETURNING id, name, description, path, areas, created_at, updated_at; -``` - -**场景3:只更新 areas(完全替换)** -```sql -UPDATE entry_modules -SET - areas = '[{"area":"云浮","enabled":false,"sort_order":1}]'::jsonb, - updated_at = CURRENT_TIMESTAMP -WHERE id = 1 -RETURNING id, name, description, path, areas, created_at, updated_at; -``` - -#### 响应示例 - -**成功响应(200)** -```json -{ - "code": 0, - "msg": "成功", - "data": { - "id": 1, - "name": "合同管理v2", - "description": "更新后的描述", - "path": "documents/mz/static/img/entry_module_1.png", - "areas": [ - {"area": "梅州", "enabled": true, "sort_order": 1} - ], - "created_at": "2025-11-18T21:33:33.857417+08:00", - "updated_at": "2025-11-28T16:45:00.123456+08:00" - } -} -``` - -**错误响应(404)- 模块不存在** -```json -{ - "code": 404, - "msg": "入口模块不存在: id=999", - "data": [] -} -``` - -**错误响应(400)- 名称已存在** -```json -{ - "code": 400, - "msg": "模块名称已存在: name=案卷智能评查", - "data": [] -} -``` - -**错误响应(400)- 没有更新字段** -```json -{ - "code": 400, - "msg": "没有要更新的字段", - "data": [] -} -``` - ---- - -### 5. 删除入口模块 - -#### 接口定义 - -```http -DELETE /api/v3/entry-modules/{module_id} -Authorization: Bearer {JWT_TOKEN} -``` - -#### 权限要求 - -| 权限代码 | 说明 | -|---------|------| -| `entry_module:delete:delete` | 删除入口模块 | - -#### 路径参数 - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| module_id | int | 是 | 模块ID | - -#### 业务逻辑流程 - -``` -1. 参数验证 - └─ module_id: FastAPI自动验证 - -2. 数据库连接 - └─ 调用 _get_db_connection() - -3. 存在性检查 - ├─ SQL: SELECT id, name FROM entry_modules WHERE id = $1 - ├─ 结果为空 → 抛出 HTTPException(404) - └─ 结果存在 → 记录模块名称,继续处理 - -4. 执行删除 - └─ SQL: DELETE FROM entry_modules WHERE id = $1 - -5. 日志记录 - └─ logger.info(f"入口模块删除成功: id={module_id}, name={module_name}") - -6. 返回响应 - └─ {"message": "入口模块删除成功: {module_name}"} -``` - -#### SQL执行 - -```sql --- 存在性检查 -SELECT id, name FROM entry_modules WHERE id = 1; --- 返回: {"id": 1, "name": "合同管理"} - --- 执行删除 -DELETE FROM entry_modules WHERE id = 1; --- 影响行数: 1 -``` - -#### 响应示例 - -**成功响应(200)** -```json -{ - "code": 0, - "msg": "成功", - "data": { - "message": "入口模块删除成功: 合同管理" - } -} -``` - -**错误响应(404)** -```json -{ - "code": 404, - "msg": "入口模块不存在: id=999", - "data": [] -} -``` - -**注意事项:** -- ⚠️ 删除操作是**物理删除**,数据库记录直接删除,不可恢复 -- ⚠️ MinIO中的图标文件**不会自动删除**,需要手动清理或通过定时任务清理孤立文件 -- ⚠️ 如果其他表有外键引用,可能会触发级联删除或报错(取决于外键约束设置) - ---- - -### 6. 上传入口模块图标 - -> ⚠️ **重要更新 (v2.1)**:图片上传功能已修复JWT拦截问题,静态资源现在可以无需Token访问。 - -#### 🔥 前端常见问题及解决方案 - -**问题1:上传时报错 "Field required - body.file"** - -```json -{ - "code": 3000, - "msg": "参数校验错误", - "data": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required" - } - ] -} -``` - -**原因**:字段名错误或没有使用FormData上传 - -**正确做法**: -```javascript -// ✅ 正确 -const formData = new FormData(); -formData.append('file', imageFile); // 字段名必须是 "file" - -await fetch(`/api/v3/entry-modules/${moduleId}/image`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}` - // ✅ 不要设置 Content-Type,让浏览器自动添加boundary - }, - body: formData -}); - -// ❌ 错误示例 -formData.append('image', imageFile); // 字段名错误 -formData.append('picture', imageFile); // 字段名错误 -body: JSON.stringify({file: base64}); // 不能用JSON -headers: {'Content-Type': 'multipart/form-data'} // 不要手动设置 -``` - -**问题2:图片显示401 Unauthorized** - -这是因为旧版本JWT中间件拦截了MinIO静态资源。 - -**已修复** ✅: -- MinIO路径(`/docauditai/`)已加入JWT白名单 -- 图片现在可以**无需Token**直接访问 -- 测试:`curl http://your-server/docauditai/documents/mz/static/img/entry_module_1.png` - -**问题3:错误的图片访问路径** - -```javascript -// ❌ 错误 - 硬编码路径 -const imageUrl = `/docauditai/entryModule/${module.code}`; - -// ✅ 正确 - 使用数据库path字段 -const imageUrl = `http://172.16.0.81:9000/docauditai/${module.path}`; - -// ✅ 或者使用上传接口返回的url字段(推荐) -const result = await uploadImage(moduleId, file); -const imageUrl = result.data.url; -``` - -#### 接口定义 - -```http -POST /api/v3/entry-modules/{module_id}/image -Content-Type: multipart/form-data -Authorization: Bearer {JWT_TOKEN} - -------WebKitFormBoundary -Content-Disposition: form-data; name="file"; filename="icon.png" -Content-Type: image/png - - -------WebKitFormBoundary-- -``` - -#### 权限要求 - -| 权限代码 | 说明 | -|---------|------| -| `entry_module:update:write` | 上传图标(复用更新权限) | - -#### 路径参数 - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| module_id | int | 是 | 模块ID | - -#### 表单字段 - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| file | File | 是 | 图片文件 | - -**支持的文件格式:** -- 扩展名:`.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`, `.svg` -- Content-Type:`image/jpeg`, `image/png`, `image/gif`, `image/webp`, `image/svg+xml` - -#### 业务逻辑流程 - -``` -1. 文件格式验证 - ├─ 检查文件名非空 - ├─ 获取扩展名: os.path.splitext(file.filename)[1].lower() - ├─ 扩展名不在允许列表 → 抛出 HTTPException(400, "不支持的图片格式") - ├─ 检查 Content-Type - └─ 不匹配时根据扩展名推断 - -2. 数据库连接 - └─ 调用 _get_db_connection() - -3. 模块存在性检查 - ├─ SQL: SELECT id, name, path FROM entry_modules WHERE id = $1 - ├─ 结果为空 → 抛出 HTTPException(404) - └─ 结果存在 → 记录旧路径 old_path - -4. 构建存储路径 - ├─ 获取实例名称: os.environ.get("INSTANCE_NAME", "default") - ├─ 固定文件名: entry_module_{module_id}{ext} - └─ 完整路径: documents/{instance_name}/static/img/{filename} - 示例: documents/mz/static/img/entry_module_1.png - -5. 读取文件内容 - ├─ file_content = await file.read() - └─ 内容为空 → 抛出 HTTPException(400, "文件内容为空") - -6. MinIO上传 - ├─ 获取客户端: _get_minio_client() - ├─ 旧文件清理(如果路径不同): - │ ├─ 检查: old_path and old_path != storage_path - │ ├─ 存在性检查: minio_client.file_exists(old_path) - │ └─ 删除: minio_client.delete_file(old_path) - ├─ 上传新文件: - │ └─ file_url = minio_client.upload_bytes( - │ data=file_content, - │ destination_path=storage_path, - │ content_type=content_type - │ ) - └─ 返回访问URL - -7. 更新数据库 - └─ SQL: UPDATE entry_modules - SET path = $1, updated_at = CURRENT_TIMESTAMP - WHERE id = $2 - -8. 日志记录 - └─ logger.info(f"入口模块图片上传成功: module_id={module_id}, path={storage_path}") - -9. 返回响应 - └─ {module_id, path, url, message} -``` - -#### 文件路径规则 - -**格式**:`documents/{instance_name}/static/img/entry_module_{module_id}.{ext}` - -**示例**: -``` -梅州实例(INSTANCE_NAME=mz): - documents/mz/static/img/entry_module_1.png - documents/mz/static/img/entry_module_2.jpg - -云浮实例(INSTANCE_NAME=yf): - documents/yf/static/img/entry_module_1.png - documents/yf/static/img/entry_module_3.gif -``` - -**覆盖策略**: -- 使用**固定文件名** `entry_module_{module_id}` -- 每次上传**覆盖**同一模块的旧图标 -- 如果扩展名改变(如 `.png` → `.jpg`),旧文件会被**删除** - -#### MinIO 上传详细流程 - -**upload_bytes 方法实现:** -```python -def upload_bytes( - self, - data: bytes, - destination_path: str, - content_type: Optional[str] = None -) -> str: - """ - 上传字节数据到MinIO - - 内部流程: - 1. 规范化路径(移除前导斜杠,统一使用正斜杠) - 2. 创建BytesIO对象包装字节数据 - 3. 调用MinIO客户端 put_object() 方法 - 4. 自动设置Content-Type头 - 5. 返回完整访问URL - """ - object_name = self._normalize_path(destination_path) - - file_data = BytesIO(data) - file_size = len(data) - - self.client.put_object( - bucket_name=self.bucket_name, - object_name=object_name, - data=file_data, - length=file_size, - content_type=content_type or "application/octet-stream" - ) - - return self.get_file_url(destination_path) -``` - -**get_file_url 方法实现:** -```python -def get_file_url(self, file_path: str) -> str: - """ - 生成文件访问URL - - 逻辑: - 1. 如果配置了 base_url,直接拼接 - http://172.16.0.81:9000/docauditai/ + file_path - 2. 否则使用MinIO SDK生成预签名URL(默认7天有效期) - """ - if self.base_url: - object_name = self._normalize_path(file_path) - return urljoin(self.base_url, object_name) - else: - return self.client.presigned_get_object( - bucket_name=self.bucket_name, - object_name=self._normalize_path(file_path), - expires=timedelta(days=7) - ) -``` - -#### SQL执行示例 - -**存在性检查:** -```sql -SELECT id, name, path FROM entry_modules WHERE id = 1; --- 返回: {"id": 1, "name": "合同管理", "path": "entryModule/old.png"} -``` - -**更新路径:** -```sql -UPDATE entry_modules -SET path = 'documents/mz/static/img/entry_module_1.png', - updated_at = CURRENT_TIMESTAMP -WHERE id = 1; --- 影响行数: 1 -``` - -#### 响应示例 - -**成功响应(200)** -```json -{ - "code": 0, - "msg": "成功", - "data": { - "module_id": 1, - "path": "documents/mz/static/img/entry_module_1.png", - "url": "http://172.16.0.81:9000/docauditai/documents/mz/static/img/entry_module_1.png", - "message": "图片上传成功" - } -} -``` - -**错误响应(400)- 不支持的格式** -```json -{ - "code": 400, - "msg": "不支持的图片格式: .pdf,支持的格式: .jpg, .jpeg, .png, .gif, .webp, .svg", - "data": [] -} -``` - -**错误响应(400)- 文件为空** -```json -{ - "code": 400, - "msg": "文件内容为空", - "data": [] -} -``` - -**错误响应(404)- 模块不存在** -```json -{ - "code": 404, - "msg": "入口模块不存在: id=999", - "data": [] -} -``` - -**错误响应(422)- 缺少file字段** ⚠️ **常见错误** -```json -{ - "code": 3000, - "msg": "参数校验错误", - "data": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": null - } - ] -} -``` - -**错误响应(500)- MinIO上传失败** -```json -{ - "code": 500, - "msg": "上传入口模块图片失败: S3 error: Access Denied", - "data": [] -} -``` - ---- - -## 业务逻辑详解 - -### 唯一性约束处理 - -**数据库层面**: -- `name` 字段有 `UNIQUE` 约束 -- 插入重复值时PostgreSQL会抛出异常:`duplicate key value violates unique constraint "entry_modules_name_key"` - -**应用层面**: -- 创建前主动查询检查 -- 更新前检查名称是否与其他模块冲突(排除自身) - -```python -# 创建时的检查 -existing = await conn.fetchval( - "SELECT id FROM entry_modules WHERE name = $1", - name -) -if existing: - raise HTTPException(status_code=400, detail=f"模块名称已存在: name={name}") - -# 更新时的检查(排除自身) -if name and name != existing['name']: - duplicate = await conn.fetchval( - "SELECT id FROM entry_modules WHERE name = $1 AND id != $2", - name, module_id - ) - if duplicate: - raise HTTPException(status_code=400, detail=f"模块名称已存在: name={name}") -``` - -### JSONB 数据处理 - -**存储时(Python → PostgreSQL)**: -```python -# 将Python列表转换为JSON字符串 -areas_list = [ - {"area": "梅州", "enabled": True, "sort_order": 1}, - {"area": "云浮", "enabled": True, "sort_order": 2} -] -areas_json = json.dumps(areas_list) # 转为字符串 - -# 插入时PostgreSQL自动转为JSONB类型 -INSERT INTO entry_modules (..., areas) VALUES (..., $1) -- $1 = areas_json -``` - -**读取时(PostgreSQL → Python)**: -```python -# asyncpg返回的JSONB已自动解析为Python对象 -row = await conn.fetchrow("SELECT areas FROM entry_modules WHERE id = 1") -areas = row['areas'] # 已经是 list[dict] - -# 兼容处理(如果是字符串则手动解析) -if isinstance(areas, str): - areas = json.loads(areas) -``` - -### 地区筛选查询原理 - -**JSONB `@>` 操作符**: -- 含义:左侧JSONB**包含**右侧JSONB -- 用法:检查数组中是否存在某个元素 - -**查询示例**: -```sql --- 查找包含"梅州"地区配置的模块 -SELECT * FROM entry_modules -WHERE areas @> '[{"area": "梅州"}]'::jsonb; - --- 等价于:areas数组中存在至少一个对象,其 area 字段为 "梅州" -``` - -**实际执行计划**: -```sql -EXPLAIN ANALYZE -SELECT * FROM entry_modules -WHERE areas @> '[{"area": "梅州"}]'::jsonb; - --- 输出(使用GIN索引): --- Bitmap Index Scan on idx_entry_modules_areas (cost=0.00..4.50 rows=1) --- Index Cond: (areas @> '[{"area":"梅州"}]'::jsonb) -``` - -### 触发器自动维护时间戳 - -**创建时**: -- `created_at`: 由 `DEFAULT NOW()` 自动设置 -- `updated_at`: 由 `DEFAULT NOW()` 自动设置 - -**更新时**: -- `updated_at`: 由触发器 `update_entry_modules_updated_at` 自动更新 -- 应用层无需手动设置 `updated_at = CURRENT_TIMESTAMP`(但加上也无害) - -**触发器代码**: -```sql -CREATE OR REPLACE FUNCTION update_updated_at() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = NOW(); -- 自动将新记录的 updated_at 设置为当前时间 - RETURN NEW; -END; -$$ LANGUAGE plpgsql; -``` - -### MinIO文件覆盖策略 - -**场景1:首次上传** -``` -数据库 path: NULL -MinIO文件: 不存在 -操作: - 1. 上传文件到 documents/mz/static/img/entry_module_1.png - 2. 更新数据库 path = 'documents/mz/static/img/entry_module_1.png' -结果: ✅ 文件上传成功 -``` - -**场景2:覆盖相同格式** -``` -数据库 path: documents/mz/static/img/entry_module_1.png -MinIO文件: entry_module_1.png 存在 -新文件: icon.png (将重命名为 entry_module_1.png) -操作: - 1. old_path == storage_path → 不删除旧文件 - 2. 直接上传,MinIO自动覆盖 - 3. 数据库 path 不变 -结果: ✅ 文件覆盖成功 -``` - -**场景3:更换文件格式** -``` -数据库 path: entryModule/aj (旧路径,无扩展名或其他格式) -MinIO文件: entryModule/aj 存在 -新文件: icon.png (将保存为 entry_module_2.png) -操作: - 1. old_path != storage_path → 删除旧文件 entryModule/aj - 2. 上传新文件到 documents/mz/static/img/entry_module_2.png - 3. 更新数据库 path = 'documents/mz/static/img/entry_module_2.png' -结果: ✅ 旧文件删除,新文件上传成功 -``` - -### 动态SQL构建 - -**更新接口的动态字段处理**: -```python -update_fields = [] -params = [] -param_index = 1 - -if name is not None: - update_fields.append(f"name = ${param_index}") - params.append(name) - param_index += 1 - -if description is not None: - update_fields.append(f"description = ${param_index}") - params.append(description) - param_index += 1 - -# ...其他字段同理 - -if not update_fields: - raise HTTPException(status_code=400, detail="没有要更新的字段") - -# 构建最终SQL -update_query = f""" - UPDATE entry_modules - SET {', '.join(update_fields)}, updated_at = CURRENT_TIMESTAMP - WHERE id = ${param_index} - RETURNING * -""" -params.append(module_id) - -# 执行 -result = await conn.fetchrow(update_query, *params) -``` - -**生成的SQL示例**: -```sql --- 只更新 name -UPDATE entry_modules -SET name = $1, updated_at = CURRENT_TIMESTAMP -WHERE id = $2 -RETURNING *; --- 参数: ['新名称', 1] - --- 更新 name 和 areas -UPDATE entry_modules -SET name = $1, areas = $2, updated_at = CURRENT_TIMESTAMP -WHERE id = $3 -RETURNING *; --- 参数: ['新名称', '[...]'::jsonb, 1] -``` - ---- - -## 错误处理机制 - -### 异常层级 - -``` -1. HTTPException(FastAPI标准异常) - ├─ 400 Bad Request: 参数错误、业务规则错误 - ├─ 404 Not Found: 资源不存在 - └─ 500 Internal Server Error: 服务器内部错误 - -2. 数据库异常(asyncpg) - └─ 捕获后转换为 HTTPException(500) - -3. MinIO异常(S3Error) - └─ 捕获后转换为 HTTPException(500) -``` - -### 服务层异常处理模式 - -```python -try: - conn = await EntryModuleService._get_db_connection() - try: - # 业务逻辑... - result = await conn.fetchrow(query, *params) - - if not result: - raise HTTPException(status_code=404, detail="资源不存在") - - return result - - finally: - await conn.close() # 确保连接关闭 - -except HTTPException: - raise # 直接抛出HTTP异常 -except Exception as e: - logger.error(f"操作失败: {e}") - raise HTTPException(status_code=500, detail=f"操作失败: {str(e)}") -``` - -### 常见错误及处理 - -| HTTP状态码 | 错误原因 | 错误消息示例 | 处理建议 | -|-----------|---------|-------------|---------| -| 400 | 模块名称已存在 | `模块名称已存在: name=合同管理` | 修改模块名称 | -| 400 | 没有要更新的字段 | `没有要更新的字段` | 至少传入一个字段 | -| 400 | 不支持的图片格式 | `不支持的图片格式: .pdf` | 使用支持的格式 | -| 400 | 文件内容为空 | `文件内容为空` | 检查文件是否损坏 | -| 404 | 模块不存在 | `入口模块不存在: id=999` | 检查模块ID | -| 500 | 数据库连接失败 | `connection to server failed` | 检查数据库服务 | -| 500 | MinIO上传失败 | `上传入口模块图片失败: S3 error` | 检查MinIO服务 | - -### 日志记录 - -**日志级别**: -```python -logger.info() # 成功操作 -logger.warning() # 警告(如删除旧文件失败,但可忽略) -logger.error() # 错误操作 -``` - -**日志示例**: -``` -2025-11-28 16:30:00 INFO 查询入口模块列表成功: total=3, page=1, page_size=20 -2025-11-28 16:31:00 INFO 入口模块创建成功: id=4, name=合同管理 -2025-11-28 16:32:00 INFO 删除旧图片: entryModule/aj -2025-11-28 16:32:00 WARN 删除旧图片失败(可忽略): file not found -2025-11-28 16:33:00 INFO 入口模块图片上传成功: module_id=1, path=documents/mz/static/img/entry_module_1.png -2025-11-28 16:34:00 ERROR 查询入口模块列表失败: connection to server failed -``` - ---- - -## 前端对接指南 - -### TypeScript 类型定义 - -```typescript -/** - * 地区配置项 - */ -interface AreaConfig { - area: string; // 地区名称 - enabled: boolean; // 是否启用 - sort_order: number; // 排序号 -} - -/** - * 入口模块 - */ -interface EntryModule { - id: number; - name: string; - description: string | null; - path: string | null; - areas: AreaConfig[] | null; - created_at: string; // ISO 8601格式 - updated_at: string; -} - -/** - * 列表响应 - */ -interface EntryModuleListResponse { - total: number; - page: number; - page_size: number; - items: EntryModule[]; -} - -/** - * 创建请求 - */ -interface EntryModuleCreateRequest { - name: string; - description?: string; - path?: string; - areas?: AreaConfig[]; -} - -/** - * 更新请求(所有字段可选) - */ -interface EntryModuleUpdateRequest { - name?: string; - description?: string; - path?: string; - areas?: AreaConfig[]; -} - -/** - * 图片上传响应 - */ -interface ImageUploadResponse { - module_id: number; - path: string; - url: string; - message: string; -} -``` - -### API 封装示例 - -```typescript -import axios, { AxiosInstance } from 'axios'; - -// 创建axios实例 -const api: AxiosInstance = axios.create({ - baseURL: '/api/v3', - timeout: 30000, - headers: { - 'Content-Type': 'application/json' - } -}); - -// 请求拦截器 - 添加Token -api.interceptors.request.use( - (config) => { - const token = localStorage.getItem('jwt_token'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; - }, - (error) => { - return Promise.reject(error); - } -); - -// 响应拦截器 - 统一错误处理 -api.interceptors.response.use( - (response) => response, - (error) => { - if (error.response) { - const { status, data } = error.response; - // ⭐ v2.1更新:错误响应统一格式 {code, msg, data} - const errorMsg = data.msg || data.detail || '请求失败'; - - switch (status) { - case 401: - // Token过期,跳转登录 - console.error('未授权:', errorMsg); - window.location.href = '/login'; - break; - case 403: - // 无权限 - console.error('权限不足:', errorMsg); - break; - case 404: - // 资源不存在 - console.error('资源不存在:', errorMsg); - break; - case 422: - // 参数校验错误 - console.error('参数错误:', errorMsg, data.data); - break; - case 500: - // 服务器错误 - console.error('服务器错误:', errorMsg); - break; - } - } - return Promise.reject(error); - } -); - -/** - * 入口模块 API - */ -export const entryModuleApi = { - /** - * 获取列表 - */ - list: (params?: { - page?: number; - page_size?: number; - name?: string; - area?: string; - }) => { - return api.get<{ - code: number; - msg: string; - data: EntryModuleListResponse; - }>('/entry-modules', { params }); - }, - - /** - * 获取详情 - */ - get: (id: number) => { - return api.get<{ - code: number; - msg: string; - data: EntryModule; - }>(`/entry-modules/${id}`); - }, - - /** - * 创建模块 - */ - create: (data: EntryModuleCreateRequest) => { - return api.post<{ - code: number; - msg: string; - data: EntryModule; - }>('/entry-modules', data); - }, - - /** - * 更新模块 - */ - update: (id: number, data: EntryModuleUpdateRequest) => { - return api.put<{ - code: number; - msg: string; - data: EntryModule; - }>(`/entry-modules/${id}`, data); - }, - - /** - * 删除模块 - */ - delete: (id: number) => { - return api.delete<{ - code: number; - msg: string; - data: { message: string }; - }>(`/entry-modules/${id}`); - }, - - /** - * 上传图标 - * ⚠️ 注意:字段名必须是 "file",不要手动设置Content-Type - */ - uploadImage: (id: number, file: File) => { - const formData = new FormData(); - formData.append('file', file); // ✅ 字段名必须是 "file" - - return api.post<{ - code: number; - msg: string; - data: ImageUploadResponse; - }>(`/entry-modules/${id}/image`, formData - // ✅ 不要设置Content-Type,axios会自动设置 - // 如果手动设置会缺少boundary参数导致上传失败 - ); - } -}; -``` - -### 使用示例 - -```typescript -// 1. 获取列表(带筛选) -const fetchModules = async () => { - try { - const { data } = await entryModuleApi.list({ - page: 1, - page_size: 20, - area: '梅州' - }); - - console.log('总数:', data.data.total); - console.log('模块列表:', data.data.items); - } catch (error) { - console.error('获取列表失败:', error); - } -}; - -// 2. 创建模块 -const createModule = async () => { - try { - const { data } = await entryModuleApi.create({ - name: '合同管理', - description: '合同管理入口模块', - areas: [ - { area: '梅州', enabled: true, sort_order: 1 }, - { area: '云浮', enabled: true, sort_order: 2 } - ] - }); - - console.log('创建成功:', data.data); - return data.data.id; - } catch (error) { - console.error('创建失败:', error); - } -}; - -// 3. 上传图标 -const uploadIcon = async (moduleId: number, file: File) => { - try { - const { data } = await entryModuleApi.uploadImage(moduleId, file); - - console.log('上传成功,访问URL:', data.data.url); - return data.data.url; - } catch (error) { - console.error('上传失败:', error); - } -}; - -// 4. 更新地区配置(完全替换) -const updateAreas = async (moduleId: number) => { - try { - const { data } = await entryModuleApi.update(moduleId, { - areas: [ - { area: '梅州', enabled: false, sort_order: 1 } // 只保留梅州,且禁用 - ] - }); - - console.log('更新成功:', data.data); - } catch (error) { - console.error('更新失败:', error); - } -}; - -// 5. 删除模块 -const deleteModule = async (moduleId: number) => { - if (!confirm('确定要删除此模块吗?')) return; - - try { - const { data } = await entryModuleApi.delete(moduleId); - - console.log(data.data.message); - } catch (error) { - console.error('删除失败:', error); - } -}; -``` - -### 完整创建流程示例 - -```typescript -/** - * 创建入口模块的完整流程 - */ -const createModuleWithIcon = async ( - moduleData: EntryModuleCreateRequest, - iconFile: File -) => { - try { - // 步骤1: 创建模块(path为null) - const { data: createResult } = await entryModuleApi.create({ - ...moduleData, - path: null - }); - - const moduleId = createResult.data.id; - console.log(`模块创建成功,ID: ${moduleId}`); - - // 步骤2: 上传图标 - const { data: uploadResult } = await entryModuleApi.uploadImage( - moduleId, - iconFile - ); - - console.log(`图标上传成功,URL: ${uploadResult.data.url}`); - - // 步骤3: 获取完整信息(包含最新的path) - const { data: detailResult } = await entryModuleApi.get(moduleId); - - return detailResult.data; - - } catch (error) { - console.error('创建流程失败:', error); - throw error; - } -}; - -// 使用 -const iconFile = document.querySelector('#icon-upload')!.files![0]; -const newModule = await createModuleWithIcon( - { - name: '合同管理', - description: '合同管理入口模块', - areas: [ - { area: '梅州', enabled: true, sort_order: 1 } - ] - }, - iconFile -); - -console.log('最终模块信息:', newModule); -``` - ---- - -## 附录 - -### 常见问题 FAQ - -**Q1: areas 配置会增量更新吗?** -A: 不会。`areas` 字段是**完全替换**。如果只想修改某个地区的 `enabled` 状态,需要先获取当前完整的 `areas` 数组,在前端修改后,再将完整数组传给更新接口。 - -**Q2: 删除模块后图标会自动删除吗?** -A: 不会。删除模块只删除数据库记录,MinIO中的图标文件不会自动删除。建议定期清理孤立文件。 - -**Q3: 如何拼接图标的完整访问URL?** -A: 上传接口返回的 `url` 字段已经是完整URL,直接使用即可。格式:`http://172.16.0.81:9000/docauditai/{path}` - -**Q4: 支持批量上传吗?** -A: 当前不支持。每次只能上传一个模块的图标。 - -**Q5: 图标文件大小有限制吗?** -A: 代码中未设置文件大小限制,但建议控制在 5MB 以内以保证上传速度。 - -**Q6: 如何获取某个地区启用的所有模块?** -A: 调用列表接口 `?area=梅州` 筛选,前端再过滤 `enabled=true` 的记录。 - -**Q7: 为什么模块名称不能重复?** -A: 数据库表有 `UNIQUE` 约束,保证名称唯一性。这是业务规则,避免前端展示混淆。 - -### 性能优化建议 - -**前端**: -1. 列表页使用虚拟滚动,减少DOM渲染压力 -2. 图标使用懒加载,按需加载图片 -3. 使用防抖优化搜索输入 -4. 缓存列表数据,避免频繁请求 - -**后端**: -1. 数据库已建立 GIN 索引,地区筛选性能良好 -2. asyncpg 异步驱动,支持高并发 -3. MinIO 分布式存储,读写性能优秀 -4. 建议启用 Redis 缓存热门数据 - -### 权限配置参考 - -**数据库权限表**: -```sql -SELECT permission_key, display_name -FROM permissions -WHERE module = 'entry_module'; -``` - -**结果**: -| permission_key | display_name | -|----------------|--------------| -| entry_module:list:read | 查看入口模块列表 | -| entry_module:detail:read | 查看入口模块详情 | -| entry_module:create:write | 创建入口模块 | -| entry_module:update:write | 更新入口模块 | -| entry_module:delete:delete | 删除入口模块 | - -**角色分配示例**: -```sql --- 给管理员角色分配所有权限 -INSERT INTO role_permissions (role_id, permission_id, grant_type, data_scope) -SELECT 1, id, 'GRANT', NULL -FROM permissions -WHERE module = 'entry_module'; -``` - ---- - -## 📝 版本更新记录 - -### v2.1 (2025-01-29) - -**重要更新**: -1. ✅ 修复JWT拦截MinIO静态资源问题 - - MinIO路径(`/docauditai/`)已加入JWT白名单 - - 图片现在可以无需Token访问 - -2. ✅ 统一错误响应格式 - - 旧格式:`{"detail": "错误信息"}` - - 新格式:`{"code": xxx, "msg": "错误信息", "data": []}` - -3. ✅ 新增图片上传常见问题解决方案 - - 详细说明了"Field required - body.file"错误的解决方法 - - 提供了正确的FormData使用示例 - - 强调字段名必须是 "file" - -4. ✅ 创建测试工具 - - 新增HTML测试工具:`scripts/test_entry_module_image_upload.html` - - 可视化测试界面,帮助调试图片上传功能 - -**前端重要修改**: -- ⚠️ 删除手动设置 `Content-Type: multipart/form-data` -- ⚠️ 使用 `data.msg` 替代 `data.detail` 获取错误信息 -- ⚠️ 图片访问不要硬编码路径,使用数据库path字段 - -### v2.0 (2025-11-28) - -- 完整的API文档 -- 数据库设计详解 -- 前端对接指南 -- TypeScript类型定义 - ---- - -**当前版本**:v2.1 -**最后更新**:2025-01-29 -**维护者**:Backend Team -**反馈渠道**:[提交Issue](http://git.7bm.co:1024/leke/docauditai/-/issues) diff --git a/auth_doc/entry_modules_api.md b/auth_doc/entry_modules_api.md index f1145f1..a2b654d 100644 --- a/auth_doc/entry_modules_api.md +++ b/auth_doc/entry_modules_api.md @@ -1,6 +1,6 @@ # 入口模块管理 API 完整技术文档 -> **供前端对接使用** | 更新时间:2025-11-28 | 版本:v2.0 +> **供前端对接使用** | 更新时间:2025-01-29 | 版本:v2.1 ## 📋 目录 @@ -390,7 +390,9 @@ LIMIT 20 OFFSET 0; **错误响应(500)** ```json { - "detail": "查询入口模块列表失败: connection to server failed" + "code": 500, + "msg": "查询入口模块列表失败: connection to server failed", + "data": [] } ``` @@ -470,7 +472,9 @@ WHERE id = 1; -- $1 参数化查询 **错误响应(404)** ```json { - "detail": "入口模块不存在: id=999" + "code": 404, + "msg": "入口模块不存在: id=999", + "data": [] } ``` @@ -606,7 +610,9 @@ RETURNING id, name, description, path, areas, created_at, updated_at; **错误响应(400)- 名称重复** ```json { - "detail": "模块名称已存在: name=合同管理" + "code": 400, + "msg": "模块名称已存在: name=合同管理", + "data": [] } ``` @@ -756,21 +762,27 @@ RETURNING id, name, description, path, areas, created_at, updated_at; **错误响应(404)- 模块不存在** ```json { - "detail": "入口模块不存在: id=999" + "code": 404, + "msg": "入口模块不存在: id=999", + "data": [] } ``` **错误响应(400)- 名称已存在** ```json { - "detail": "模块名称已存在: name=案卷智能评查" + "code": 400, + "msg": "模块名称已存在: name=案卷智能评查", + "data": [] } ``` **错误响应(400)- 没有更新字段** ```json { - "detail": "没有要更新的字段" + "code": 400, + "msg": "没有要更新的字段", + "data": [] } ``` @@ -849,7 +861,9 @@ DELETE FROM entry_modules WHERE id = 1; **错误响应(404)** ```json { - "detail": "入口模块不存在: id=999" + "code": 404, + "msg": "入口模块不存在: id=999", + "data": [] } ``` @@ -862,6 +876,73 @@ DELETE FROM entry_modules WHERE id = 1; ### 6. 上传入口模块图标 +> ⚠️ **重要更新 (v2.1)**:图片上传功能已修复JWT拦截问题,静态资源现在可以无需Token访问。 + +#### 🔥 前端常见问题及解决方案 + +**问题1:上传时报错 "Field required - body.file"** + +```json +{ + "code": 3000, + "msg": "参数校验错误", + "data": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required" + } + ] +} +``` + +**原因**:字段名错误或没有使用FormData上传 + +**正确做法**: +```javascript +// ✅ 正确 +const formData = new FormData(); +formData.append('file', imageFile); // 字段名必须是 "file" + +await fetch(`/api/v3/entry-modules/${moduleId}/image`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}` + // ✅ 不要设置 Content-Type,让浏览器自动添加boundary + }, + body: formData +}); + +// ❌ 错误示例 +formData.append('image', imageFile); // 字段名错误 +formData.append('picture', imageFile); // 字段名错误 +body: JSON.stringify({file: base64}); // 不能用JSON +headers: {'Content-Type': 'multipart/form-data'} // 不要手动设置 +``` + +**问题2:图片显示401 Unauthorized** + +这是因为旧版本JWT中间件拦截了MinIO静态资源。 + +**已修复** ✅: +- MinIO路径(`/docauditai/`)已加入JWT白名单 +- 图片现在可以**无需Token**直接访问 +- 测试:`curl http://your-server/docauditai/documents/mz/static/img/entry_module_1.png` + +**问题3:错误的图片访问路径** + +```javascript +// ❌ 错误 - 硬编码路径 +const imageUrl = `/docauditai/entryModule/${module.code}`; + +// ✅ 正确 - 使用数据库path字段 +const imageUrl = `http://172.16.0.81:9000/docauditai/${module.path}`; + +// ✅ 或者使用上传接口返回的url字段(推荐) +const result = await uploadImage(moduleId, file); +const imageUrl = result.data.url; +``` + #### 接口定义 ```http @@ -1067,28 +1148,52 @@ WHERE id = 1; **错误响应(400)- 不支持的格式** ```json { - "detail": "不支持的图片格式: .pdf,支持的格式: .jpg, .jpeg, .png, .gif, .webp, .svg" + "code": 400, + "msg": "不支持的图片格式: .pdf,支持的格式: .jpg, .jpeg, .png, .gif, .webp, .svg", + "data": [] } ``` **错误响应(400)- 文件为空** ```json { - "detail": "文件内容为空" + "code": 400, + "msg": "文件内容为空", + "data": [] } ``` **错误响应(404)- 模块不存在** ```json { - "detail": "入口模块不存在: id=999" + "code": 404, + "msg": "入口模块不存在: id=999", + "data": [] +} +``` + +**错误响应(422)- 缺少file字段** ⚠️ **常见错误** +```json +{ + "code": 3000, + "msg": "参数校验错误", + "data": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": null + } + ] } ``` **错误响应(500)- MinIO上传失败** ```json { - "detail": "上传入口模块图片失败: S3 error: Access Denied" + "code": 500, + "msg": "上传入口模块图片失败: S3 error: Access Denied", + "data": [] } ``` @@ -1465,22 +1570,30 @@ api.interceptors.response.use( (error) => { if (error.response) { const { status, data } = error.response; + // ⭐ v2.1更新:错误响应统一格式 {code, msg, data} + const errorMsg = data.msg || data.detail || '请求失败'; + switch (status) { case 401: // Token过期,跳转登录 + console.error('未授权:', errorMsg); window.location.href = '/login'; break; case 403: // 无权限 - console.error('权限不足'); + console.error('权限不足:', errorMsg); break; case 404: // 资源不存在 - console.error(data.detail || '资源不存在'); + console.error('资源不存在:', errorMsg); + break; + case 422: + // 参数校验错误 + console.error('参数错误:', errorMsg, data.data); break; case 500: // 服务器错误 - console.error(data.detail || '服务器错误'); + console.error('服务器错误:', errorMsg); break; } } @@ -1554,20 +1667,20 @@ export const entryModuleApi = { /** * 上传图标 + * ⚠️ 注意:字段名必须是 "file",不要手动设置Content-Type */ uploadImage: (id: number, file: File) => { const formData = new FormData(); - formData.append('file', file); + formData.append('file', file); // ✅ 字段名必须是 "file" return api.post<{ code: number; msg: string; data: ImageUploadResponse; - }>(`/entry-modules/${id}/image`, formData, { - headers: { - 'Content-Type': 'multipart/form-data' - } - }); + }>(`/entry-modules/${id}/image`, formData + // ✅ 不要设置Content-Type,axios会自动设置 + // 如果手动设置会缺少boundary参数导致上传失败 + ); } }; ``` @@ -1776,7 +1889,43 @@ WHERE module = 'entry_module'; --- -**文档版本**:v2.0 -**最后更新**:2025-11-28 +## 📝 版本更新记录 + +### v2.1 (2025-01-29) + +**重要更新**: +1. ✅ 修复JWT拦截MinIO静态资源问题 + - MinIO路径(`/docauditai/`)已加入JWT白名单 + - 图片现在可以无需Token访问 + +2. ✅ 统一错误响应格式 + - 旧格式:`{"detail": "错误信息"}` + - 新格式:`{"code": xxx, "msg": "错误信息", "data": []}` + +3. ✅ 新增图片上传常见问题解决方案 + - 详细说明了"Field required - body.file"错误的解决方法 + - 提供了正确的FormData使用示例 + - 强调字段名必须是 "file" + +4. ✅ 创建测试工具 + - 新增HTML测试工具:`scripts/test_entry_module_image_upload.html` + - 可视化测试界面,帮助调试图片上传功能 + +**前端重要修改**: +- ⚠️ 删除手动设置 `Content-Type: multipart/form-data` +- ⚠️ 使用 `data.msg` 替代 `data.detail` 获取错误信息 +- ⚠️ 图片访问不要硬编码路径,使用数据库path字段 + +### v2.0 (2025-11-28) + +- 完整的API文档 +- 数据库设计详解 +- 前端对接指南 +- TypeScript类型定义 + +--- + +**当前版本**:v2.1 +**最后更新**:2025-01-29 **维护者**:Backend Team **反馈渠道**:[提交Issue](http://git.7bm.co:1024/leke/docauditai/-/issues) diff --git a/auth_doc/交叉评查接口文档(1).md b/auth_doc/交叉评查接口文档(1).md deleted file mode 100644 index c1febef..0000000 --- a/auth_doc/交叉评查接口文档(1).md +++ /dev/null @@ -1,1080 +0,0 @@ -# 交叉评查接口文档 - -> 本文档描述交叉评查模块的所有API接口,供前端直接对接使用。 - -## 基础信息 - -- **基础路径**: `/api/v2/cross_review` (推荐) -- **备用路径**: `/admin/v2/cross_review` (兼容,功能相同) -- **认证方式**: JWT Token (Header: `Authorization: Bearer `) -- **Content-Type**: `application/json` - -> **说明**: `/api/v2` 和 `/admin/v2` 两个前缀指向同一套接口,功能完全相同。建议前端统一使用 `/api/v2/cross_review`。 - ---- - -## 接口总览 - -### 提案管理 - -| 方法 | 路径 | 接口名称 | 权限 | -|------|------|----------|------| -| `POST` | `/proposals` | 发起评分提案 | `cross_review:proposal:create` | -| `DELETE` | `/proposals/{proposal_id}` | 撤销评分提案 | `cross_review:proposal:delete` | -| `POST` | `/proposals/{proposal_id}/votes` | 对提案投票 | `cross_review:proposal:vote` | -| `POST` | `/proposals/details` | 获取提案列表及详情 | `cross_review:proposal:read` | -| `POST` | `/proposals/document` | 获取指定文档的提案列表 | `cross_review:proposal:read` | -| `POST` | `/proposals/document/check_pending_votes` | 检查未投票用户 | `cross_review:task:read` | - -### 任务管理 - -| 方法 | 路径 | 接口名称 | 权限 | -|------|------|----------|------| -| `POST` | `/tasks/user_tasks` | 获取用户参与的任务列表 | `cross_review:task:read` | -| `GET` | `/tasks/{task_id}/progress` | 获取评查任务进度 | `cross_review:progress:view` | -| `POST` | `/tasks/{task_id}/documents` | 获取任务下文档列表 | `cross_review:task:read` | -| `POST` | `/tasks/{task_id}/documents/{document_id}/complete` | 确认完成文档评查 | `cross_review:document:complete` | - ---- - -## 接口详细说明 - ---- - -### 1. 发起评分提案 - -**POST** `/api/v2/cross_review/proposals` - -为某个评查结果创建一个新的评分提案(加分或扣分)。 - -**权限**: `cross_review:proposal:create` - -#### 请求参数 - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `document_id` | int | 是 | 文档ID | -| `evaluation_point_id` | int | 是 | 评查点ID | -| `proposed_score` | float | 是 | 建议加/减分数(正数加分,负数扣分,不能为0) | -| `reason` | string | 是 | 理由说明(不能为空) | -| `proposer_id` | int | 是 | 提案人ID | -| `evaluation_result_id` | int | 是 | 评查结果ID(必填!) | - -#### 请求示例 - -```json -{ - "document_id": 123, - "evaluation_point_id": 456, - "proposed_score": -5, - "reason": "该评查点存在明显问题,应扣5分", - "proposer_id": 1, - "evaluation_result_id": 789 -} -``` - -#### 业务逻辑 - -1. **权限验证** - - 验证用户是否有权限访问该文档 - - 验证 `evaluation_result_id` 是否真正属于 `document_id`(防止IDOR攻击) - -2. **任务参与者验证** - - 查找文档关联的最新交叉评查任务 - - 验证提案人是否是任务的参与者(在 `user_ids` 中) - -3. **重复提案检查** - - 同一用户不能对同一评查点重复创建提案 - -4. **分数校验** - - 不能创建 0 分的提案 - - 当前分数为 0 时,不能发起扣分提案(`proposed_score < 0`) - - 当前分数已满分时,不能发起加分提案(`proposed_score > 0`) - -5. **自动投票** - - 创建提案后,系统自动为提案人创建一条"同意"的投票记录 - -6. **状态检查** - - 检查是否达到通过/否决条件,自动更新提案状态 - -#### 响应示例 - -**成功 (201)**: -```json -{ - "code": 0, - "success": true, - "message": "评分提案创建成功", - "proposal": { - "id": 1, - "document_id": 123, - "evaluation_point_id": 456, - "proposed_score": -5, - "reason": "该评查点存在明显问题,应扣5分", - "proposer_id": 1, - "status": "pending", - "evaluation_result_id": 789, - "created_at": "2024-01-01T10:00:00" - } -} -``` - -#### 错误码 - -| 状态码 | 错误信息 | 说明 | -|--------|----------|------| -| `400` | 评查结果ID不能为空 | `evaluation_result_id` 未提供 | -| `400` | 不能创建0分的提案 | `proposed_score` 为 0 | -| `400` | 当前分数为0,不能再发起扣分提案 | 当前评查结果分数已经是0 | -| `400` | 当前已满分,不能再加分 | 当前评查结果已达到满分 | -| `400` | 您已经为该评查点创建过提案 | 重复创建提案 | -| `400` | 文档未分配任何评查任务 | 文档不在交叉评查任务中 | -| `400` | 用户无权为该文档创建提案 | 用户不是任务参与者 | -| `403` | 无权访问此文档 | 文档权限验证失败 | -| `403` | 评查结果与文档不匹配 | IDOR攻击防护触发 | -| `404` | 评查结果不存在 | `evaluation_result_id` 无效 | - ---- - -### 2. 对提案投票 - -**POST** `/api/v2/cross_review/proposals/{proposal_id}/votes` - -对指定的评分提案进行投票(同意/反对/取消)。 - -**权限**: `cross_review:proposal:vote` - -#### 路径参数 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `proposal_id` | int | 提案ID(必须大于0) | - -#### 请求参数 - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `vote_type` | string | 是 | 投票类型:`agree`(同意) / `disagree`(反对) / `cancel`(取消投票) | -| `voter_id` | int | 是 | 投票人的用户ID | - -#### 请求示例 - -```json -{ - "vote_type": "agree", - "voter_id": 2 -} -``` - -#### 业务逻辑 - -1. **提案状态检查** - - 提案必须存在且未被删除 - - 只能对 `pending` 状态的提案投票 - - 已 `approved` 或 `rejected` 的提案无法投票 - -2. **任务参与者验证** - - 根据提案关联的文档找到对应任务 - - 验证投票人是否是任务参与者 - -3. **分数校验**(投票前再次校验) - - 不能投 0 分的提案 - - 当前分数为 0 时,不能投赞成扣分提案 - - 当前已满分时,不能投赞成加分提案 - -4. **投票处理** - - **新投票**: 创建新的投票记录 - - **更新投票**: 如果已投票,更新投票类型 - - **恢复投票**: 如果之前取消过投票,恢复并更新 - - **取消投票**: 软删除投票记录(设置 `deleted_at`) - -5. **自动状态更新** - - 投票后自动检查提案是否达到通过/否决条件 - - **通过条件**: 同意票数 >= (参与人数/2 + 1) - - **否决条件**: 反对票数 >= (参与人数/2 + 1) 或 剩余票数不足以达到通过条件 - - 提案通过后自动更新评查结果的 `final_score` - -6. **文档完成检查** - - 提案状态变更后,检查文档下所有提案是否都已完成 - - 如果都完成,自动将文档标记为已完成(`audit_status=1`) - -#### 响应示例 - -**成功 (201)**: -```json -{ - "code": 0, - "success": true, - "message": "投票成功", - "proposal_status": "approved" -} -``` - -**取消投票成功**: -```json -{ - "code": 0, - "success": true, - "message": "投票已撤销", - "proposal_status": "pending" -} -``` - -#### 错误码 - -| 状态码 | 错误信息 | 说明 | -|--------|----------|------| -| `400` | 提案不存在或已被删除 | 无效的提案ID | -| `400` | 提案状态为 approved/rejected,无法投票 | 提案已结束 | -| `400` | 不能投0分的提案 | 提案分数为0 | -| `400` | 当前分数为0,不能再扣分 | 评查结果分数已是0 | -| `400` | 当前已满分,不能再加分 | 评查结果已满分 | -| `400` | 用户无权对该提案投票 | 用户不是任务参与者 | - ---- - -### 3. 撤销评分提案 - -**DELETE** `/api/v2/cross_review/proposals/{proposal_id}` - -撤销一个评分提案。仅提案人本人可以撤销。 - -**权限**: `cross_review:proposal:delete` - -#### 路径参数 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `proposal_id` | int | 提案ID(必须大于0) | - -#### 请求体 - -无需请求体。 - -#### 业务逻辑 - -1. **提案存在性验证** - - 验证提案存在且未被删除 - -2. **权限验证** - - 只有提案人本人才能撤销自己的提案 - -3. **状态验证** - - 只能撤销 `pending` 状态的提案 - - 已 `approved` 或 `rejected` 的提案无法撤销 - -4. **软删除** - - 将提案的 `deleted_at` 设置为当前时间 - - 同时软删除所有关联的投票记录 - -#### 响应示例 - -**成功 (200)**: -```json -{ - "code": 0, - "success": true, - "message": "提案已成功撤销" -} -``` - -#### 错误码 - -| 状态码 | 错误信息 | 说明 | -|--------|----------|------| -| `400` | 提案不存在 | 无效的提案ID | -| `400` | 提案状态为 approved/rejected,无法撤销 | 提案已结束 | -| `403` | 只有提案人才能撤销自己的提案 | 权限不足 | - ---- - -### 4. 获取提案列表及详情 - -**POST** `/api/v2/cross_review/proposals/details` - -获取当前用户需要处理的所有待投票提案列表(排除自己创建的提案和已投票的提案)。 - -**权限**: `cross_review:proposal:read` - -#### 请求参数 - -| 字段 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| `document_id` | int | 否 | null | 按文档ID过滤(不传则查所有) | -| `page` | int | 否 | 1 | 页码(从1开始) | -| `page_size` | int | 否 | 20 | 每页数量 | - -#### 请求示例 - -```json -{ - "document_id": 123, - "page": 1, - "page_size": 20 -} -``` - -#### 业务逻辑 - -1. **任务范围确定** - - 查找当前用户参与的所有交叉评查任务 - - 获取这些任务下的所有文档ID - -2. **提案过滤** - - 只返回 `pending` 状态的提案 - - 排除当前用户自己创建的提案 - - 如果指定了 `document_id`,只返回该文档的提案 - -3. **关联信息查询** - - 评查点名称 - - 提案人昵称 - - 所有投票人及投票类型 - - 同意者列表、反对者列表 - - 待投票者列表 - - 发现问题(evaluation_result 的 message) - -4. **投票状态判断** - - 判断当前用户是否已对该提案投票 - - 返回 `can_vote` 字段 - -#### 响应示例 - -```json -{ - "data": [ - { - "proposal_id": 1, - "evaluation_point_name": "当事人签名检查", - "proposed_score": -5, - "reason": "缺少当事人签名", - "proposer": "张三", - "votes": [ - {"voter": "张三", "vote_type": "agree"}, - {"voter": "李四", "vote_type": "disagree"} - ], - "agree_voters": ["张三"], - "disagree_voters": ["李四"], - "can_vote": true, - "problem_message": "文档中未找到当事人签名", - "pending_voters": ["王五", "赵六"], - "status": "pending" - } - ], - "pagination": { - "page": 1, - "page_size": 20, - "total": 5, - "total_pages": 1 - } -} -``` - -#### 返回字段说明 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `proposal_id` | int | 提案ID | -| `evaluation_point_name` | string | 评查点名称 | -| `proposed_score` | float | 提议的加/减分数 | -| `reason` | string | 提案理由 | -| `proposer` | string | 提案人昵称 | -| `votes` | array | 所有投票记录(含投票人和投票类型) | -| `agree_voters` | array | 同意者昵称列表 | -| `disagree_voters` | array | 反对者昵称列表 | -| `can_vote` | boolean | 当前用户是否可以投票 | -| `problem_message` | string | 评查结果发现的问题描述 | -| `pending_voters` | array | 待投票者昵称列表 | -| `status` | string | 提案状态(pending/approved/rejected) | - ---- - -### 5. 获取指定文档的提案列表 - -**POST** `/api/v2/cross_review/proposals/document` - -获取指定文档下的所有评分提案及其详细信息(包含所有状态的提案)。 - -**权限**: `cross_review:proposal:read` - -#### 请求参数 - -| 字段 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| `document_id` | int | 是 | - | 文档ID | -| `page` | int | 否 | 1 | 页码(从1开始) | -| `page_size` | int | 否 | 10 | 每页数量 | - -#### 请求示例 - -```json -{ - "document_id": 123, - "page": 1, - "page_size": 10 -} -``` - -#### 业务逻辑 - -1. **权限验证** - - 验证当前用户是否有权限访问该文档 - -2. **提案查询** - - 获取该文档下所有未删除的提案(包括 pending/approved/rejected) - -3. **关联信息查询** - - 评查点名称 - - 提案人昵称和ID - - 所有投票人及投票类型 - - 同意者列表、反对者列表 - - 待投票者列表 - - 发现问题 - - 创建时间 - -4. **投票状态判断** - - 判断当前用户是否可以投票 - - 条件:未投票 + 不是提案人 + 是任务参与者 - -#### 响应示例 - -```json -{ - "data": [ - { - "proposal_id": 1, - "evaluation_point_name": "当事人签名检查", - "proposed_score": -5, - "reason": "缺少当事人签名", - "proposer": "张三", - "proposer_id": 1, - "votes": [ - {"voter": "张三", "vote_type": "agree"}, - {"voter": "李四", "vote_type": "agree"}, - {"voter": "王五", "vote_type": "agree"} - ], - "agree_voters": ["张三", "李四", "王五"], - "disagree_voters": [], - "problem_message": "文档中未找到当事人签名", - "pending_voters": [], - "created_at": "2024-01-01 10:00:00", - "can_vote": false, - "status": "approved" - } - ], - "pagination": { - "page": 1, - "page_size": 10, - "total": 3, - "total_pages": 1 - } -} -``` - -#### 返回字段说明 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `proposal_id` | int | 提案ID | -| `evaluation_point_name` | string | 评查点名称 | -| `proposed_score` | float | 提议的加/减分数 | -| `reason` | string | 提案理由 | -| `proposer` | string | 提案人昵称 | -| `proposer_id` | int | 提案人ID | -| `votes` | array | 所有投票记录 | -| `agree_voters` | array | 同意者昵称列表 | -| `disagree_voters` | array | 反对者昵称列表 | -| `problem_message` | string | 评查结果发现的问题描述 | -| `pending_voters` | array | 待投票者昵称列表 | -| `created_at` | string | 创建时间(格式:yyyy-MM-dd HH:mm:ss) | -| `can_vote` | boolean | 当前用户是否可以投票 | -| `status` | string | 提案状态 | - -#### 错误码 - -| 状态码 | 错误信息 | 说明 | -|--------|----------|------| -| `403` | 无权访问此文档 | 用户无权限访问该文档 | - ---- - -### 6. 检查未投票用户 - -**POST** `/api/v2/cross_review/proposals/document/check_pending_votes` - -检查指定文档下所有提案是否存在未投票的用户。**仅任务创建人可调用**。 - -**权限**: `cross_review:task:read` - -#### 请求参数 - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `document_id` | int | 是 | 文档ID | - -#### 请求示例 - -```json -{ - "document_id": 123 -} -``` - -#### 业务逻辑 - -1. **任务查找** - - 根据文档ID查找关联的最新任务 - -2. **权限验证** - - 验证当前用户是否是任务的创建人(`assigner_id`) - - 只有任务创建人才能调用此接口 - -3. **投票统计** - - 查找文档下所有提案 - - 统计每个提案的已投票用户 - - 计算待投票用户(任务参与者 - 已投票用户 - 提案人) - -#### 响应示例 - -**有未投票用户**: -```json -{ - "has_pending_votes": true, - "pending_proposals": [ - { - "proposal_id": 1, - "pending_voters_num": 2, - "pending_voters": ["王五", "赵六"], - "evaluation_point_name": "当事人签名检查" - }, - { - "proposal_id": 2, - "pending_voters_num": 1, - "pending_voters": ["赵六"], - "evaluation_point_name": "日期格式检查" - } - ] -} -``` - -**无未投票用户**: -```json -{ - "has_pending_votes": false, - "pending_proposals": [] -} -``` - -#### 错误码 - -| 状态码 | 错误信息 | 说明 | -|--------|----------|------| -| `400` | 未找到该文档的任务 | 文档不在任何任务中 | -| `403` | 只有任务创建人可以执行此操作 | 当前用户不是任务创建人 | - ---- - -### 7. 获取用户参与的任务列表 - -**POST** `/api/v2/cross_review/tasks/user_tasks` - -获取当前用户参与的所有交叉评查任务列表(分页)。 - -**权限**: `cross_review:task:read` - -#### 请求参数 - -| 字段 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| `page` | int | 否 | 1 | 页码(从1开始) | -| `page_size` | int | 否 | 10 | 每页数量 | - -#### 请求示例 - -```json -{ - "page": 1, - "page_size": 10 -} -``` - -#### 业务逻辑 - -1. **任务查询** - - 查询当前用户参与的所有任务(用户ID在任务的 `user_ids` 数组中) - -2. **进度计算** - - 统计每个任务下的文档总数 - - 统计已完成文档数(`cross_task_document_mapping.audit_status = 1`) - - 计算完成百分比 - -3. **文档类型解析** - - 将 `doc_type` 代码转换为可读名称 - -4. **地区信息聚合** - - 获取任务参与者的地区列表 - -#### 响应示例 - -```json -{ - "total": 5, - "page": 1, - "page_size": 10, - "items": [ - { - "task_id": 1, - "task_name": "2024年度交叉评查任务", - "task_status": "in_progress", - "doc_type": "行政处罚", - "task_type": "CITY", - "task_created_at": "2024-01-01T10:00:00", - "progress": 75, - "total_documents": 20, - "evaluation_region": ["梅州", "云浮"] - }, - { - "task_id": 2, - "task_name": "2024年Q1评查", - "task_status": "completed", - "doc_type": "行政许可", - "task_type": "DISTRICT", - "task_created_at": "2024-02-01T10:00:00", - "progress": 100, - "total_documents": 15, - "evaluation_region": ["揭阳"] - } - ] -} -``` - -#### 返回字段说明 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `task_id` | int | 任务ID | -| `task_name` | string | 任务名称 | -| `task_status` | string | 任务状态(in_progress/completed) | -| `doc_type` | string | 文档类型名称 | -| `task_type` | string | 任务类型(CITY/DISTRICT等) | -| `task_created_at` | datetime | 任务创建时间 | -| `progress` | int | 完成进度百分比 (0-100) | -| `total_documents` | int | 任务包含的文档总数 | -| `evaluation_region` | array | 参与评查的地区列表 | - ---- - -### 8. 获取评查任务进度 - -**GET** `/api/v2/cross_review/tasks/{task_id}/progress` - -根据任务ID获取评查进度详情。**仅任务参与者可访问**。 - -**权限**: `cross_review:progress:view` - -#### 路径参数 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `task_id` | int | 任务ID(必须大于0) | - -#### 请求体 - -无需请求体。 - -#### 业务逻辑 - -1. **权限验证** - - 验证当前用户是否是任务的参与者(在 `user_ids` 中) - -2. **进度计算** - - 获取任务关联的所有文档映射(排除已删除的) - - 统计已完成文档数(`audit_status = 1`) - - 计算完成百分比 - -#### 响应示例 - -```json -{ - "code": 0, - "task_id": 1, - "total_documents": 20, - "completed_documents": 15, - "progress": 75.0 -} -``` - -#### 返回字段说明 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `code` | int | 状态码(0表示成功) | -| `task_id` | int | 任务ID | -| `total_documents` | int | 文档总数 | -| `completed_documents` | int | 已完成文档数 | -| `progress` | float | 完成百分比(保留2位小数) | - -#### 错误码 - -| 状态码 | 错误信息 | 说明 | -|--------|----------|------| -| `403` | 无权访问任务:您不是该任务的参与者 | 用户不是任务参与者 | -| `404` | 任务不存在 | 无效的任务ID | - ---- - -### 9. 获取任务下文档列表 - -**POST** `/api/v2/cross_review/tasks/{task_id}/documents` - -获取指定任务下的文档列表,支持多种筛选条件和排序。**仅任务参与者可访问**。 - -**权限**: `cross_review:task:read` - -#### 路径参数 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `task_id` | int | 任务ID(必须大于0) | - -#### 请求参数 - -| 字段 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| `page` | int | 否 | 1 | 页码(从1开始) | -| `page_size` | int | 否 | 10 | 每页数量 | -| `file_type_ids` | array[int] | 否 | null | 文件类型ID列表(不传查所有类型) | -| `date_from` | string | 否 | null | 起始日期 (yyyy-MM-dd) | -| `date_to` | string | 否 | null | 结束日期 (yyyy-MM-dd) | -| `keyword` | string | 否 | null | 搜索关键字(匹配文件名或文书号) | -| `order` | string | 否 | `upload_time_desc` | 排序方式 | - -#### 排序方式 - -| 值 | 说明 | -|----|------| -| `upload_time_desc` | 上传时间降序(默认,最新的在前) | -| `upload_time_asc` | 上传时间升序(最早的在前) | - -#### 请求示例 - -```json -{ - "page": 1, - "page_size": 10, - "file_type_ids": [1, 2], - "date_from": "2024-01-01", - "date_to": "2024-12-31", - "keyword": "处罚", - "order": "upload_time_desc" -} -``` - -#### 业务逻辑 - -1. **权限验证** - - 验证当前用户是否是任务的参与者 - -2. **文档查询** - - 从 `cross_task_document_mapping` 获取任务关联的文档 - - 应用各种筛选条件 - - 关联 `documents` 和 `document_types` 表 - -3. **评查统计** - - 统计每个文档的评查结果 - - 计算通过/警告/失败/人工审核数量 - - 计算最终得分和满分 - - 获取问题摘要(最多5条) - -#### 响应示例 - -```json -{ - "total": 100, - "page": 1, - "page_size": 10, - "items": [ - { - "document_id": 123, - "file_name": "行政处罚决定书_001.pdf", - "status": "completed", - "path": "/documents/2024/01/123.pdf", - "file_code": "穗市监罚字[2024]001号", - "file_type_name": "行政处罚", - "file_type_id": 1, - "file_size": 1024000, - "upload_time": "2024-01-01T10:00:00", - "created_at": "2024-01-01T10:00:00", - "evaluations_status": "completed", - "audit_status": 1, - "created_by_user_id": 1, - "final_score": 85.5, - "full_score": 100, - "score_summary": "85.5/100", - "score_percent": 85.5, - "pass_count": 18, - "warning_count": 2, - "fail_count": 1, - "manual_count": 3, - "issues": [ - { - "severity": "error", - "message": "缺少当事人签名" - }, - { - "severity": "warning", - "message": "日期格式不规范" - } - ] - } - ] -} -``` - -#### 返回字段说明 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `document_id` | int | 文档ID | -| `file_name` | string | 文件名 | -| `status` | string | 文档处理状态 | -| `path` | string | 文件存储路径 | -| `file_code` | string | 文书号 | -| `file_type_name` | string | 文件类型名称 | -| `file_type_id` | int | 文件类型ID | -| `file_size` | int | 文件大小(字节) | -| `upload_time` | datetime | 上传时间 | -| `created_at` | datetime | 创建时间 | -| `evaluations_status` | string | 评查状态 | -| `audit_status` | int | 审核状态 (0:待审核, 1:已完成) | -| `created_by_user_id` | int | 创建用户ID | -| `final_score` | float | 最终得分 | -| `full_score` | float | 满分 | -| `score_summary` | string | 得分摘要(如 "85.5/100") | -| `score_percent` | float | 得分百分比 | -| `pass_count` | int | 通过的评查点数量 | -| `warning_count` | int | 警告的评查点数量(severity=warning或info) | -| `fail_count` | int | 失败的评查点数量(severity=error) | -| `manual_count` | int | 需人工审核的评查点数量(post_action=manual) | -| `issues` | array | 问题列表(最多5条,包含severity和message) | - -#### 错误码 - -| 状态码 | 错误信息 | 说明 | -|--------|----------|------| -| `403` | 无权访问任务:您不是该任务的参与者 | 用户不是任务参与者 | - ---- - -### 10. 确认完成文档评查 - -**POST** `/api/v2/cross_review/tasks/{task_id}/documents/{document_id}/complete` - -确认完成对指定文档的评查,更新任务进度。 - -**权限**: `cross_review:document:complete` - -#### 路径参数 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `task_id` | int | 任务ID(必须大于0) | -| `document_id` | int | 文档ID(必须大于0) | - -#### 请求体 - -无需请求体。 - -#### 业务逻辑 - -1. **任务存在性验证** - - 验证任务存在 - -2. **权限验证** - - 验证当前用户是否是任务的参与者 - -3. **文档归属验证** - - 验证文档确实属于该任务(在 `cross_task_document_mapping` 中) - -4. **状态更新** - - 将 `cross_task_document_mapping.audit_status` 设置为 `1`(已完成) - -#### 响应示例 - -```json -{ - "code": 0, - "success": true, - "message": "文档评查已完成", - "task_id": 1, - "document_id": 123 -} -``` - -#### 错误码 - -| 状态码 | 错误信息 | 说明 | -|--------|----------|------| -| `400` | 任务不存在 | 无效的任务ID | -| `400` | 文档不在任务中 | 文档不属于该任务 | -| `403` | 用户不是任务的参与者 | 权限不足 | - ---- - -## 核心业务规则 - -### 提案投票机制 - -1. **投票通过条件**: 同意票数 >= ⌊参与人数/2⌋ + 1 -2. **投票否决条件**: - - 反对票数 >= ⌊参与人数/2⌋ + 1 - - 或剩余可投票数不足以达到通过条件 - -3. **提案人自动投票**: 创建提案时,系统自动为提案人投一票"同意" - -4. **分数自动更新**: 提案通过后,自动将 `proposed_score` 累加到评查结果的 `final_score` - -### 分数校验规则 - -| 场景 | 限制 | -|------|------| -| 创建0分提案 | 禁止 | -| 当前分数为0时扣分 | 禁止 | -| 当前分数已满分时加分 | 禁止 | -| 分数计算后超过满分 | 自动截断为满分 | -| 分数计算后小于0 | 自动截断为0 | - -### 文档完成自动标记 - -当文档下所有提案都达到终态(approved 或 rejected)时,系统自动将该文档在任务中的状态标记为已完成(`audit_status=1`)。 - ---- - -## 权限列表 - -| 权限 Key | 说明 | -|----------|------| -| `cross_review:proposal:create` | 创建提案 | -| `cross_review:proposal:read` | 查看提案 | -| `cross_review:proposal:delete` | 撤销提案 | -| `cross_review:proposal:vote` | 提案投票 | -| `cross_review:task:read` | 查看任务 | -| `cross_review:progress:view` | 查看进度 | -| `cross_review:document:complete` | 完成文档评查 | - ---- - -## 枚举值说明 - -### VoteType 投票类型 - -| 值 | 说明 | -|----|------| -| `agree` | 同意 | -| `disagree` | 反对 | -| `cancel` | 取消投票 | - -### ProposalStatus 提案状态 - -| 值 | 说明 | -|----|------| -| `pending` | 待处理(投票中) | -| `approved` | 已通过(分数已更新) | -| `rejected` | 已否决 | - -### DocType 文档类型 - -| 值 | 说明 | -|----|------| -| `XZCF` | 行政处罚 | -| `XZXK` | 行政许可 | - -### audit_status 任务内文档审核状态 - -| 值 | 说明 | -|----|------| -| `0` | 待审核/进行中 | -| `1` | 已完成 | - -### task_status 任务状态 - -| 值 | 说明 | -|----|------| -| `in_progress` | 进行中 | -| `completed` | 已完成 | - ---- - -## 数据库表关系 - -``` -cross_examination_tasks (交叉评查任务) -├── id: 任务ID -├── assigner_id: 创建人ID -├── user_ids: 参与者ID数组 -├── task_name: 任务名称 -├── task_status: 任务状态 -├── doc_type: 文档类型 -└── task_type: 任务类型 - -cross_task_document_mapping (任务-文档映射) -├── task_id: 任务ID -├── document_id: 文档ID -├── audit_status: 文档审核状态 -└── deleted_at: 软删除标记 - -cross_scoring_proposals (评分提案) -├── id: 提案ID -├── document_id: 文档ID -├── evaluation_point_id: 评查点ID -├── evaluation_result_id: 评查结果ID -├── proposed_score: 提议分数 -├── reason: 理由 -├── proposer_id: 提案人ID -├── status: 提案状态 -└── deleted_at: 软删除标记 - -cross_opinion_votes (投票记录) -├── proposal_id: 提案ID -├── voter_id: 投票人ID -├── vote_type: 投票类型 -└── deleted_at: 软删除标记 -``` - ---- - -## 通用错误响应格式 - -```json -{ - "detail": "错误信息描述" -} -``` - -**HTTP 状态码**: -- `400 Bad Request`: 请求参数错误/业务规则校验失败 -- `403 Forbidden`: 权限不足 -- `404 Not Found`: 资源不存在 -- `500 Internal Server Error`: 服务器内部错误 - ---- - -## 前端对接注意事项 - -1. **所有接口都需要认证**: 必须在 Header 中携带有效的 JWT Token - -2. **evaluation_result_id 必填**: 创建提案时必须提供评查结果ID - -3. **任务权限检查**: 大部分任务相关接口会校验当前用户是否为任务参与者 - -4. **分页参数**: 分页从第1页开始,不是第0页 - -5. **日期格式**: 使用 `yyyy-MM-dd` 格式(如 `2024-01-01`) - -6. **投票后刷新**: 投票后应刷新提案列表,因为提案状态可能已自动更新 - -7. **proposed_score 含义**: - - 正数表示加分 - - 负数表示扣分 - - 0 不允许 - -8. **can_vote 字段**: 前端应根据此字段决定是否显示投票按钮 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..0e8e9ea --- /dev/null +++ b/docs/README.md @@ -0,0 +1,34 @@ +# 前端工程文档导航 + +> 最后整理:2026-05-04 +> 说明:本目录主要存放 `new_doc_review` 前端实现、联调、部署、能力评估类文档。 + +## 当前优先阅读 + +- 权限子页映射规范:`new_doc_review/docs/route-alias-guidelines.md` +- PostgREST 迁移盘点: + - `new_doc_review/docs/PostgREST使用情况-后端API替代建议.md` + - `new_doc_review/docs/PostgREST请求模块清单.md` + - `new_doc_review/docs/PostgREST实际使用清单.md` +- 文档类型页面改造背景:`new_doc_review/docs/文档类型管理CRUD详细分析.md` +- 评查点域联调资料:`new_doc_review/docs/evaluation/` +- 部署与运行:`new_doc_review/docs/deployment-config.md`、`new_doc_review/docs/docker-deployment.md` + +## 按主题分类 + +| 主题 | 重点文档 | 说明 | +|------|----------|------| +| 权限 / 会话 / 登录 | `RBAC路由权限集成说明.md`、`RBAC路由权限集成_修复说明.md`、`JWT_IMPLEMENTATION.md`、`统一登录流程_实施记录.md` | 登录、JWT、路由权限接入 | +| PostgREST 迁移 | `PostgREST使用情况-后端API替代建议.md`、`PostgREST请求模块清单.md`、`PostgREST实际使用清单.md`、`PostgREST未使用函数清单.md` | 哪些页面仍依赖 PostgREST、替代优先级 | +| 首页 / 数据首页 | `getHomeData_完整逻辑文档.md`、`home-api-analysis.md` | 首页数据获取与结构说明 | +| 文档类型 / 规则页 | `文档类型管理CRUD详细分析.md`、`attribute_type_frontend_integration.md`、`evaluation/` | 文档类型、评查点、属性类型联调 | +| 合同模板 / 起草 | `contract-drafting-*.md`、`contract-draft-*.md` | 合同模板搜索、列表、起草与历史重构记录 | +| Dify / 智能助手 | `dify-proxy-backend-integration.md`、`new-dify/`、`Token管理架构优化总结.md` | 智能助手、代理接入、Token 管理 | +| 文件 / 文档能力 | `minio-docx-extraction-fix.md`、`docxtemplater-placeholder-extraction.md`、`react-pdf功能总结.md` | 文件处理、占位符提取、PDF 能力 | +| 部署 / 运行维护 | `deployment-config.md`、`deployment-checklist.md`、`docker-deployment.md`、`postgresql-backup-guide.md` | 环境部署与备份 | + +## 使用约束 + +- 这里偏前端实现细节,不替代 `docs/` 下的项目主文档 +- 涉及当前主链路的事实判断,优先对照 `docs/HANDOFF.md` 与 `docs/接口/README.md` +- 临时排障文档若已失效,应合并进正式文档后删除,不再继续堆积“紧急说明”类文件 diff --git a/docs/URGENT-前端必须修改.md b/docs/URGENT-前端必须修改.md deleted file mode 100644 index 5153e3e..0000000 --- a/docs/URGENT-前端必须修改.md +++ /dev/null @@ -1,398 +0,0 @@ -# 🚨 紧急:前端Dify集成问题修复指南 - -## 问题现状 - -**当前错误**: -```javascript -// ❌ 错误的Authorization头 -Authorization: Bearer app-lHn5EmeACIaLjG9yz0rYIFfM // 这是Dify API Key,不是JWT! - -// ❌ 错误的user字段 -user: "user_34a1d450-6a24-4db7-b1e7-8dd7c0876f14:sess_3z84y7dtuhr" // UUID格式 -``` - -**导致的问题**: -1. 无法加载历史对话记录(user不匹配) -2. 对话没有按用户隔离(所有人共享对话) -3. 安全问题(API Key暴露在前端) - ---- - -## 🎯 必须修改的3个地方 - -### 修改1: Authorization头 - 使用JWT替代Dify API Key - -#### ❌ 旧代码(错误) -```typescript -// app/services/dify-client.server.ts 或类似文件 -const DIFY_API_KEY = "app-lHn5EmeACIaLjG9yz0rYIFfM"; - -async function callDifyAPI(endpoint: string, data: any) { - const response = await fetch(`${DIFY_BASE_URL}${endpoint}`, { - headers: { - 'Authorization': `Bearer ${DIFY_API_KEY}`, // ❌ 错误!暴露API Key - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }); - return response.json(); -} -``` - -#### ✅ 新代码(正确) -```typescript -// app/services/dify-client.server.ts -// 1. 移除DIFY_API_KEY配置 -// const DIFY_API_KEY = "..."; // 删除这行 - -// 2. 从用户session获取JWT -import { getUserSession } from "~/api/login/auth.server"; - -async function callDifyAPI(request: Request, endpoint: string, data: any) { - // 获取JWT - const { frontendJWT } = await getUserSession(request); - - if (!frontendJWT) { - throw new Error("未登录,请先登录"); - } - - // 使用JWT作为Authorization头 - const response = await fetch(`http://172.16.0.55:8000/dify${endpoint}`, { - headers: { - 'Authorization': `Bearer ${frontendJWT}`, // ✅ 正确!使用JWT - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }); - return response.json(); -} -``` - -### 修改2: 请求URL - 指向FastAPI代理而非直连Dify - -#### ❌ 旧代码(错误) -```typescript -// 直连Dify API -const DIFY_BASE_URL = "http://nas.7bm.co:12980/v1"; - -// 示例:获取会话列表 -const response = await fetch( - `${DIFY_BASE_URL}/conversations?user=${user}&limit=100`, - // ... -); -``` - -#### ✅ 新代码(正确) -```typescript -// 通过FastAPI代理访问Dify -const API_BASE_URL = "http://172.16.0.55:8000"; // FastAPI地址 - -// 示例:获取会话列表 -const response = await fetch( - `${API_BASE_URL}/dify/conversations?limit=100`, // ✅ 走/dify代理,不传user - { - headers: { - 'Authorization': `Bearer ${frontendJWT}` // ✅ 使用JWT - } - } -); -``` - -### 修改3: user字段 - 完全移除,让后端自动添加 - -#### ❌ 旧代码(错误) -```typescript -// 生成UUID格式的user -function generateUserId() { - return `user_${crypto.randomUUID()}:sess_${randomString()}`; -} - -// 发送消息 -const data = { - query: "你好", - user: generateUserId(), // ❌ 错误!UUID格式 - conversation_id: null, - response_mode: "streaming", - inputs: {} -}; -``` - -#### ✅ 新代码(正确) -```typescript -// 完全不传user字段,后端会自动添加JWT的username -const data = { - query: "你好", - // user字段完全不传! ✅ 正确 - conversation_id: null, - response_mode: "streaming", - inputs: {} -}; -``` - ---- - -## 📝 完整的修改示例 - -### 示例1: 获取会话列表 - -#### ❌ 旧代码 -```typescript -// app/routes/api.conversations.tsx -export async function loader({ request }: LoaderFunctionArgs) { - const url = new URL(request.url); - const user = url.searchParams.get("user") || generateUserId(); - const limit = url.searchParams.get("limit") || "100"; - - const response = await fetch( - `http://nas.7bm.co:12980/v1/conversations?user=${user}&limit=${limit}`, - { - headers: { - 'Authorization': `Bearer app-lHn5EmeACIaLjG9yz0rYIFfM`, // ❌ Dify API Key - 'Content-Type': 'application/json' - } - } - ); - - return json(await response.json()); -} -``` - -#### ✅ 新代码 -```typescript -// app/routes/api.conversations.tsx -import { getUserSession } from "~/api/login/auth.server"; - -export async function loader({ request }: LoaderFunctionArgs) { - // 1. 获取JWT - const { frontendJWT } = await getUserSession(request); - - if (!frontendJWT) { - return json({ error: "未登录" }, { status: 401 }); - } - - // 2. 调用FastAPI代理(不传user,后端自动添加) - const url = new URL(request.url); - const limit = url.searchParams.get("limit") || "100"; - - const response = await fetch( - `http://172.16.0.55:8000/dify/conversations?limit=${limit}`, // ✅ 走代理 - { - headers: { - 'Authorization': `Bearer ${frontendJWT}`, // ✅ 使用JWT - 'Content-Type': 'application/json' - } - } - ); - - return json(await response.json()); -} -``` - -### 示例2: 发送消息 - -#### ❌ 旧代码 -```typescript -// app/routes/api.chat-messages.tsx -export async function action({ request }: ActionFunctionArgs) { - const body = await request.json(); - - const response = await fetch( - `http://nas.7bm.co:12980/v1/chat-messages`, - { - method: 'POST', - headers: { - 'Authorization': `Bearer app-lHn5EmeACIaLjG9yz0rYIFfM`, // ❌ Dify API Key - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - query: body.query, - user: body.user || generateUserId(), // ❌ UUID格式 - conversation_id: body.conversation_id, - response_mode: "streaming", - inputs: body.inputs || {} - }) - } - ); - - return new Response(response.body, { - headers: { 'Content-Type': 'text/event-stream' } - }); -} -``` - -#### ✅ 新代码 -```typescript -// app/routes/api.chat-messages.tsx -import { getUserSession } from "~/api/login/auth.server"; - -export async function action({ request }: ActionFunctionArgs) { - // 1. 获取JWT - const { frontendJWT } = await getUserSession(request); - - if (!frontendJWT) { - return json({ error: "未登录" }, { status: 401 }); - } - - // 2. 解析请求体 - const body = await request.json(); - - // 3. 调用FastAPI代理(不传user,后端自动添加) - const response = await fetch( - `http://172.16.0.55:8000/dify/chat-messages`, // ✅ 走代理 - { - method: 'POST', - headers: { - 'Authorization': `Bearer ${frontendJWT}`, // ✅ 使用JWT - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - query: body.query, - // user字段完全不传 ✅ 后端自动添加 - conversation_id: body.conversation_id, - response_mode: "streaming", - inputs: body.inputs || {} - }) - } - ); - - return new Response(response.body, { - headers: { 'Content-Type': 'text/event-stream' } - }); -} -``` - ---- - -## 🔍 检查清单 - -### 前端开发者必须检查的文件 - -1. **Dify客户端配置文件** - - 位置:`app/services/dify-client.server.ts` 或类似 - - 检查:是否还有`DIFY_API_KEY`定义?(应该删除) - - 检查:是否还有`DIFY_BASE_URL = "http://nas.7bm.co:12980"`?(应改为FastAPI地址) - -2. **API路由文件** - - 位置:`app/routes/api.*.tsx` 中所有与Dify相关的 - - 检查:所有`fetch`调用是否指向`http://172.16.0.55:8000/dify/*`? - - 检查:所有`Authorization`头是否使用`frontendJWT`? - -3. **user字段生成逻辑** - - 搜索关键字:`generateUserId`, `user_`, `crypto.randomUUID` - - 检查:是否还在生成UUID格式的user?(应该完全移除) - -4. **环境变量配置** - - 位置:`.env` 或 `.env.local` - - 检查:是否还有`DIFY_API_KEY`或`NEXT_PUBLIC_APP_KEY`?(应该删除) - ---- - -## 🧪 验证方法 - -修改完成后,使用浏览器开发者工具验证: - -### 1. 检查Authorization头 -打开 Network 标签,找到Dify相关请求: - -**✅ 正确的请求头**: -``` -Request URL: http://172.16.0.55:8000/dify/conversations?limit=100 -Request Headers: - Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... (JWT格式,很长) - Content-Type: application/json -``` - -**❌ 错误的请求头**: -``` -Request URL: http://nas.7bm.co:12980/v1/conversations?user=xxx -Request Headers: - Authorization: Bearer app-lHn5EmeACIaLjG9yz0rYIFfM (Dify API Key,很短) -``` - -### 2. 检查请求体 -对于POST请求(如发送消息): - -**✅ 正确的请求体**: -```json -{ - "query": "你好", - "conversation_id": null, - "response_mode": "streaming", - "inputs": {} -} -``` -注意:**没有user字段** - -**❌ 错误的请求体**: -```json -{ - "query": "你好", - "user": "user_34a1d450-6a24-4db7-b1e7-8dd7c0876f14:sess_xxx", - "conversation_id": null, - "response_mode": "streaming", - "inputs": {} -} -``` - -### 3. 检查后端日志 -修改后,后端日志应该显示: - -```log -[INFO] Dify请求用户: admin (ID: 5, 路径: conversations) -[INFO] 自动添加user参数: admin -[INFO] 查询参数: {'user': 'admin', 'limit': '100'} -``` - -### 4. 检查Dify后台 -在Dify管理界面的对话列表中,应该看到: -- 新对话的用户显示为:`admin`(或实际的username) -- 不再是:`user_34a1d450...` - ---- - -## ⚠️ 常见错误 - -### 错误1: 前端仍使用DIFY_API_KEY -**症状**:Authorization头是`Bearer app-xxx`(短字符串) -**解决**:删除所有`DIFY_API_KEY`定义,改用`frontendJWT` - -### 错误2: 前端直连Dify -**症状**:请求URL是`http://nas.7bm.co:12980/v1/...` -**解决**:所有URL改为`http://172.16.0.55:8000/dify/...` - -### 错误3: 仍在传递UUID格式的user -**症状**:请求体包含`"user": "user_34a1d450..."` -**解决**:完全删除user字段,不要传递 - -### 错误4: JWT获取失败 -**症状**:`frontendJWT`为`null`或`undefined` -**解决**:检查`getUserSession`导入路径和session配置 - ---- - -## 📋 修改总结 - -| 项目 | 旧值 | 新值 | -|------|------|------| -| Authorization头 | `Bearer app-lHn5EmeACIaLjG9yz0rYIFfM` | `Bearer {frontendJWT}` | -| 请求URL | `http://nas.7bm.co:12980/v1/*` | `http://172.16.0.55:8000/dify/*` | -| user字段 | `user_34a1d450-...` | 完全不传(后端自动添加) | - ---- - -## 🎯 期望结果 - -修改完成后: -- ✅ 每个用户只能看到自己的对话记录 -- ✅ Dify后台显示真实的username(如admin) -- ✅ API Key不再暴露在前端 -- ✅ 所有对话按用户隔离 -- ✅ 历史对话记录正常加载 - ---- - -**紧急程度**: 🔴 高优先级 -**预计修改时间**: 30-60分钟 -**影响范围**: 所有Dify相关功能 - -**联系后端**: 如有疑问,请查看`docs/dify-frontend-user-field-guide.md`获取更多细节 diff --git a/docs/role-permissions/FRONTEND_API_GUIDE.md b/docs/role-permissions/FRONTEND_API_GUIDE.md index eb5133f..641f702 100644 --- a/docs/role-permissions/FRONTEND_API_GUIDE.md +++ b/docs/role-permissions/FRONTEND_API_GUIDE.md @@ -558,7 +558,7 @@ const wrongUsers = await getRoleUsers(3); // 数据库中不存在 "id": 41, "route_path": "/rules", "route_name": "Rules", - "route_title": "评查规则库", + "route_title": "规则管理", "parent_id": null, "icon": "ri-book-3-line", "sort_order": 3, @@ -833,7 +833,7 @@ sys_routes 表:仅存储页面路由(侧边栏菜单) permissions 表:存储API操作权限(通过 route_id 关联到页面) 页面路由 -├── 评查规则库 (/rules) +├── 规则管理 (/rules) │ ├── [权限] 查看评查点分组列表 (GET /api/v3/evaluation-point-groups) │ ├── [权限] 创建评查点分组 (POST /api/v3/evaluation-point-groups) │ ├── [权限] 查看评查点规则列表 (GET /api/v3/evaluation-points) @@ -880,7 +880,7 @@ interface Permission { │ │ │ ☑ 系统概览 (/home) │ │ │ -│ ☑ 评查规则库 (/rules) │ +│ ☑ 规则管理 (/rules) │ │ ├─ ☑ 查看评查点分组列表 [GET] │ │ ├─ ☑ 创建评查点分组 [POST] │ │ ├─ ☑ 更新评查点分组 [PUT] │ diff --git a/docs/route-alias-guidelines.md b/docs/route-alias-guidelines.md index 3cb6be7..514c3ec 100644 --- a/docs/route-alias-guidelines.md +++ b/docs/route-alias-guidelines.md @@ -68,18 +68,21 @@ - `/config-lists/new` -> `/config-lists` - `/prompts/new` -> `/prompts` - `/rule-groups/new` -> `/rule-groups` - - `/rules/new` -> `/rules/list` + - `/rules/new` -> `/rules` + - `/documents/list` -> `/documents` - `/documents/edit` -> `/documents` - 模块内子页 - `/contract-template/search/results` -> `/contract-template/search` - - `/contract-template/detail/:id` -> `/contract-template` - - `/contract-draft/:id` -> `/contract-template` + - `/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` - 历史兼容页 - `/reviewsTest` -> `/reviews` - - `/rulesTest/list` -> `/rules/list` - - `/rulesTest/detail` -> `/rules/list` + - `/rulesTest/list` -> `/rules` + - `/rulesTest/detail` -> `/rules` ---