460 lines
17 KiB
Markdown
460 lines
17 KiB
Markdown
# 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** | 跨区域访问审计日志 | 权限系统 |
|