Files
leaudit-platform-backend/docs/leaudit/infrastructure_redesign.md
T

460 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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** | 跨区域访问审计日志 | 权限系统 |