docs: add fix-double-finalize-and-bindings-api implementation plan

This commit is contained in:
wren
2026-04-28 11:44:31 +08:00
parent 1b4e0ec00a
commit be9fc4856b
15 changed files with 5733 additions and 0 deletions
+459
View File
@@ -0,0 +1,459 @@
# LeAudit Platform — 基础设施深度重设计方案
> 基于老项目 docauditai 深度逆向分析,对标新平台 leaudit-platform 重新规划。
---
## 一、文件存储 OSS 路径设计
### 1.1 老项目路径模式
```
documents/{instance_name}/{doc_type_name}/{year}/{中文日期}/{doc_dir}/{filename}
实例: documents/mz/行政许可卷宗/2026/04月27日/采购合同_14时30分25秒/采购合同.pdf
```
**老项目核心问题:**
- 中文路径(区域名、文档类型名、日期)— URL 编码后不可读,程序解析困难
- `instance_name` 用区域缩写(mz/yf/jy/cz/sj),耦合 `INSTANCE_NAME` 环境变量
- 纯时间戳区分版本,无语义化版本号,查找历史版本全靠 DB 反查
- 业务文档和评查产物混在一个路径空间,无类型区分
- 无文件级权限元数据,拿到 presigned URL 即可访问
### 1.2 新平台路径设计
#### 两级路径体系
```
┌── 业务文档 (Business Documents) ── 用户上传的原始文件
│ bdocs/{region}/{type_code}/{doc_id}/{version}/{file_role}.{ext}
│ bdocs/gd-mz/contract.entrust/10042/v1/primary.pdf
│ bdocs/gd-mz/contract.entrust/10042/v1/attachment_a.docx
└── 评查产物 (Audit Artifacts) ── 引擎产出的中间/最终文件
artifacts/{region}/{run_id}/{artifact_type}/{detail}.{ext}
artifacts/gd-mz/5801/ocr_result/ocr.json
artifacts/gd-mz/5801/render_png/page_003.png
artifacts/gd-mz/5801/final_report/report.pdf
```
#### 路径段规范
| 段 | 含义 | 格式 | 示例 |
|---|---|---|---|
| `bdocs` / `artifacts` | 顶层命名空间 | 固定 | `bdocs` = 业务文档, `artifacts` = 评查产物 |
| `{region}` | 区域代码 | `{province}-{city}` | `gd-mz` (广东-梅州), `gd-yf` (云浮), `gd-jy` (揭阳), `gd-cz` (潮州), `gd-sj` (省级) |
| `{type_code}` | 文档类型编码 | DSL type_id | `contract.entrust`, `admin_license.new` |
| `{doc_id}` | 文档 ID | DB 主键 | `10042` |
| `{version}` | 版本号 | `v{n}` | `v1`, `v2`, `v3` |
| `{file_role}` | 文件角色 | 枚举 | `primary` / `attachment_a` / `scan` / `ocr_text` |
| `{run_id}` | 评查运行 ID | DB 主键 | `5801` |
| `{artifact_type}` | 产物类型 | 枚举(20种) | `ocr_result`, `extract_json`, `evaluate_json`, `final_report` |
| `{detail}` | 产物详情 | 自由格式 | `page_003.png`, `rule_R001.json` |
#### 关键设计决策
1. **全英文路径** — 无 URL 编码问题,日志/调试可直接阅读
2. **区域用 `{province}-{city}` 代码** — 比旧系统 `mz`/`yf` 更明确,未来跨省扩展无歧义
3. **`doc_id` 入路径** — 路径即自描述,无需查 DB 即可知道文件归属
4. **显式 `{version}` 段** — 版本号在路径中可见,支持 v1/v2/v3 并存
5. **产物按 `run_id` 组织** — 一次评查的所有产物在同一目录下,清理/归档方便
---
## 二、同文件多版本机制
### 2.1 老项目做法
- 每次上传用时间戳生成新目录 → 物理隔离
- DB 中按 `(name, type_id)` 分组,`create_time DESC` 取最新
- 版本号在查询时动态计算,不存储
- 旧版本永久保留,无清理策略
### 2.2 新平台版本设计
#### 版本存储模型
```
bdocs/gd-mz/contract.entrust/10042/
├── v1/primary.pdf ← 首次上传
├── v2/primary.pdf ← 第二次上传(修正版)
└── v3/primary.pdf ← 第三次上传(最终版)
```
#### 版本元数据
`leaudit_document_files` 表中记录:
```python
class LeauditDocumentFile(Base):
document_id: int # 文档 ID
version_no: int # 版本号 (1, 2, 3...)
version_seq: str # 语义版本 "v1", "v2"
file_role: str # primary / attachment / ...
oss_url: str # 完整 OSS 路径
sha256: str # 文件哈希
is_current: bool # 是否当前活跃版本
replaced_by_id: int # 被哪个新版本取代(版本链)
upload_user_id: int # 上传者
change_note: str # 变更说明
```
#### 版本生命周期
```
upload v1 → v1.is_current = True
upload v2 → v1.is_current = False, v1.replaced_by_id = v2.id
v2.is_current = True
upload v3 → v2.is_current = False, v2.replaced_by_id = v3.id
v3.is_current = True
```
- 所有旧版本文件保留在 OSS(不物理删除)
- 版本链可在前端展示为"历史版本"列表
- 回滚 = 将指定旧版本标记为 `is_current = True`(无需复制文件)
#### 与评查运行的关联
每个 `leaudit_audit_runs` 记录锁定使用的版本:
```
audit_runs.document_file_id → 指向具体版本的 leaudit_document_files.id
```
这样即使文档后来更新到 v3,历史评查记录仍然精确指向当时的 v1 文件。
---
## 三、多地区文件查看权限 & 区域隔离
### 3.1 老项目做法
- 单一 bucket,路径前缀 `{instance_name}` 区分区域
- 文件访问无权限校验(拿到 presigned URL 即访问)
- 隔离依赖 `INSTANCE_NAME` 环境变量 → 只在 API 层有效
### 3.2 新平台权限模型
#### 三层权限控制
```
Layer 1: 区域隔离 (Region Isolation)
└── 用户所属区域决定可见文档范围
省级 (gd-sj) 用户可看所有区域
地市级 (gd-mz) 用户只能看本区域
Layer 2: 文件级权限 (Document-Level)
└── 基于 RBAC 的文档访问控制
document:read:own → 本人上传的
document:read:all → 本区域全部的
document:read:cross → 跨区域查看
Layer 3: 产物级权限 (Artifact-Level)
└── 评查产物按 run_id 隔离
产物继承其文档的权限策略
临时产物 (rescue debug log) 仅内部系统可读
```
#### 权限检查流程
```
请求: GET /api/v2/documents/10042/files/v1/primary.pdf
1. JWT 解析 → 获取 user_id, user_role, user_region
2. 区域检查: user_region == 'gd-sj' OR user_region == 文档的区域
3. 权限检查: CheckPermission(user_id, "document:read:own")
或通过 GRANT/DENY 通配符匹配
4. 通过 → 生成 presigned URL (TTL 10分钟)
5. 拒绝 → 返回 403
```
#### 跨区域访问
```python
# 省级用户发起跨区域评查
POST /api/v2/documents/cross-review
{
"document_id": 10042, # gd-mz 的文档
"reviewer_region": "gd-yf", # 让云浮审核员查看
"permission": "document:read:cross"
}
系统为 gd-yf 区域的审核员创建临时访问授权
记录到 leaudit_cross_access_logs
临时授权在评查完成后自动过期
```
---
## 四、队列机制重设计
### 4.1 老项目分析
**架构:**
```
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ API Server │────▶│ Redis Queue │────▶│ Celery Worker│
│ (8000-8873) │ │ (单队列) │ │ (8线程, 4并发)│
└─────────────┘ └──────────────┘ └──────────────┘
│ │
└── source_port ────────────────────────▶│ os.environ 切换
│ 线程级隔离
```
**关键机制:**
- 所有区域共享一个 Redis 队列
- `source_port` 参数 → worker 在任务执行时切换环境变量
- Redis 信号量限制全局并发为 4
- Thread pool (8 线程) → 4 个实际并发 + 4 个 I/O 等待
**问题:**
- 环境变量切换是脆弱的状态管理(线程安全问题,需 threading.local 补偿)
- 单一队列无优先级区分(紧急任务和批处理同队列)
- 信号量修复依赖定时任务(有窗口期泄漏风险)
### 4.2 新平台队列设计
#### 多队列架构
```
┌──────────────────────────────────────────────┐
│ Redis │
│ │
│ leaudit:queue:high (优先级高) │
│ leaudit:queue:default (普通) │
│ leaudit:queue:batch (批量/低优先级) │
│ leaudit:queue:system (系统维护) │
│ │
│ leaudit:semaphore:global (并发控制) │
│ leaudit:semaphore:vlm (VLM并发) │
└──────────────────────────────────────────────┘
```
#### 任务路由(不再用 source_port
```python
# 新方案:在任务提交时直接带上下文,而非运行时切换环境变量
@celery_app.task(
bind=True,
queue="leaudit:queue:default",
time_limit=1800,
soft_time_limit=1500,
max_retries=3,
default_retry_delay=60,
)
async def leaudit_process_document(
self,
document_id: int,
run_id: int,
region: str, # gd-mz, gd-yf... (替代 source_port)
config: dict, # 运行时配置快照
user_id: int | None = None,
):
"""文档评查任务 - 上下文通过参数传递,不依赖环境变量"""
...
```
**改进点:**
1. **显式参数替代环境变量**`region` + `config` 直接传参,线程安全,可测试
2. **优先级队列** — 用户手动触发的走 high,API 自动触发的走 default,批量导入走 batch
3. **去 source_port** — 不再需要 `set_instance_environment` / `restore_instance_environment` 这种脆弱模式
4. **配置快照** — 任务创建时拍下完整配置(LLM model、OCR endpoint 等),即使配置后续变更也不影响已提交任务
#### 任务类型与路由
| 任务 | 队列 | 优先级 | 并发限制 | 超时 |
|---|---|---|---|---|
| 用户手动评查 | `high` | 8 | 全局 4 | 30min |
| API 自动评查 | `default` | 5 | 全局 4 | 30min |
| 批量导入 | `batch` | 3 | 全局 2 | 60min |
| 交叉评查 | `default` | 5 | 全局 4 | 30min |
| 信号量修复 | `system` | 10 | 无限制 | 10s |
| 健康检查 | `system` | 10 | 无限制 | 5s |
| 统计更新 | `batch` | 1 | 全局 1 | 5min |
#### 并发控制改进
```python
# 新方案:上下文管理器 + 自动释放
class TaskConcurrencyLimiter:
"""基于 Redis 的并发限制器,使用 Lua 脚本保证原子性"""
async def acquire(self, semaphore_key: str, max_concurrency: int, timeout: float) -> str:
"""原子获取许可 → 返回 permit_id"""
...
async def release(self, semaphore_key: str, permit_id: str):
"""原子释放许可"""
...
@asynccontextmanager
async def limit(self, semaphore_key: str, max_concurrency: int):
permit_id = await self.acquire(semaphore_key, max_concurrency, timeout=300)
try:
yield
finally:
await self.release(semaphore_key, permit_id)
```
- Redis Lua 脚本替代 WATCH/MULTI/EXEC(减少乐观锁冲突)
- 上下文管理器替代手动 acquire/release(防泄漏)
- 无定时修复任务(Lua 原子操作 → 不会出现不一致状态)
---
## 五、缓存机制重设计
### 5.1 老项目分析
**三层 Redis 使用:**
| 层 | 用途 | 数据 | TTL |
|---|---|---|---|
| 权限缓存 | RBAC 鉴权加速 | user:permissions:*, rbac:routes:* | 5-30 min |
| Token 黑名单 | JWT 吊销 | token:revoked:{jti} | 最长 24h |
| 并发控制 | 信号量 | semaphore:* | 1800s 许可 TTL |
**问题:**
- 5+ 个独立 Redis 连接(无统一连接池)
- `fastapi_cache2` 已初始化但从未使用
- 区域隔离依赖 Redis DB 号切换(配置复杂,易出错)
- 无 API 响应缓存(每次请求都查 DB)
- 统计缓存 TTL=20min,与权限 TTL 不一致
### 5.2 新平台缓存架构
#### 统一连接池
```python
# fastapi_common/fastapi_common_cache/redis_pool.py
class RedisPool:
"""全局 Redis 连接池 — 所有模块共用"""
_instance: 'RedisPool' = None
def __init__(self, config: dict):
# 异步连接池 (主)
self.async_pool = aioredis.ConnectionPool(
max_connections=50,
socket_connect_timeout=5,
socket_keepalive=True,
retry_on_timeout=True,
)
# 同步客户端 (Celery worker 用)
self.sync_client = redis.Redis(...)
```
**关键决策:不再用 Redis DB 号做区域隔离** — 改用 key prefix
```
# 旧: REDIS_DB=12 (gd-mz), REDIS_DB=13 (gd-yf)
# 新: 所有区域共享 DB 0, 用 prefix 区分
leaudit:gd-mz:permission:user:12345
leaudit:gd-yf:permission:user:12345
leaudit:gd-sj:cache:stats:homepage
```
好处:统一监控、可在单次 SCAN 中查询跨区域数据、配置简单(只有一个 DB 号)。
#### 缓存分层策略
```
L1: 进程内存缓存 (Python dict / lru_cache)
├── 配置数据 (规则集列表、引擎版本) — 启动加载,手动刷新
└── DSL 规则文件内容 — 本地文件缓存 + mtime 检测
L2: Redis 缓存 (TTL 驱动)
├── 权限数据: 5 min TTL (变化频率低)
├── Token 黑名单: 按 JWT exp 计算,最长 24h
├── 统计聚合: 10-30 min TTL (计算开销大)
└── API 响应: 1-5 min TTL (热点接口)
L3: 分布式锁
└── 并发控制信号量 (Lua 原子操作)
```
#### Cache Key 命名规范
```
leaudit:{region}:{domain}:{entity_type}:{entity_id}
└── 空 = 全局共享
# 权限
leaudit:gd-mz:perm:user:12345 # 用户权限集
leaudit:gd-mz:perm:user:12345:doc:read # 单条权限检查
# Token
leaudit:global:token:revoked:{jti} # 全局共享
# 统计
leaudit:gd-mz:stats:homepage:{user_id} # 首页统计
leaudit:gd-sj:stats:daily:2026-04-27 # 日报
# 并发
leaudit:global:sem:task:permits # 任务并发信号量
leaudit:global:sem:vlm:permits # VLM 并发信号量
# API 响应缓存
leaudit:gd-mz:api:documents:list:{hash} # 文档列表
```
#### 缓存失效策略
| 触发器 | 清理动作 | 粒度 |
|---|---|---|
| 用户角色变更 | `SCAN leaudit:*:perm:user:{uid}` → DEL | 精确 |
| 规则集发布新版本 | `DEL leaudit:*:perm:role:*` (所有角色) | 全局 |
| Token 吊销 | `SETEX token:revoked:{jti} TTL 1` | 精确 |
| 文档状态变更 | `DEL leaudit:{region}:stats:*` | 区域 |
| 配置变更 | `DEL leaudit:global:config:*` | 全局 |
#### 不缓存的数据
- **评查结果详情** — 每次评查结果不同,缓存无意义
- **文档文件内容** — 在 OSS,不在缓存
- **实时队列状态** — 直接读 Redis List,不做额外缓存
- **DSL 规则执行中间结果** — 一次性的,不需要缓存
---
## 六、整体基础设施拓扑
```
┌──────────────────────────┐
│ PostgreSQL │
│ nas.7bm.co:54302 │
│ leaudit_platform │
│ (17 tables) │
└──────────┬───────────────┘
┌──────────────┐ ┌──────────┴───────────────┐ ┌──────────────┐
│ MinIO OSS │ │ FastAPI App │ │ Redis │
│ │◀───│ (port 8000-8873) │───▶│ │
│ bdocs/ │ │ │ │ Queue │
│ artifacts/ │ │ Controller→Service→Model │ │ Cache │
│ │ │ Bridge→LeAudit Engine │ │ Semaphore │
└──────────────┘ └──────────────────────────┘ └──────────────┘
┌──────────┴───────────────┐
│ Celery Worker(s) │
│ (thread pool) │
│ Pipeline run │
└──────────────────────────┘
```
---
## 七、实施优先级
| 优先级 | 任务 | 依赖 |
|---|---|---|
| **P0** | 配置文件补充:Redis/OSS/LLM/VLM/OCR 真实值 | app.toml |
| **P0** | OSS 路径工具类 `oss_path_utils.py` | 上述路径规范 |
| **P0** | Redis 统一连接池 `redis_pool.py` | Redis 配置 |
| **P1** | 文件上传流程适配新 OSS 路径 | oss_path_utils |
| **P1** | Celery 任务路由改造(参数化替代 source_port | Redis 连接池 |
| **P1** | 并发控制器 Lua 脚本实现 | Redis |
| **P1** | 权限缓存对接 PermissionService | Redis 缓存层 |
| **P2** | API 响应缓存(热点接口) | Redis 缓存层 |
| **P2** | 多版本文件管理 | 文件上传流程 |
| **P3** | 跨区域访问审计日志 | 权限系统 |