Files
lingxi/docs/技术架构/灵犀_信息流素材生产平台_技术架构_V0.1.md
T
Stev_Wang f23ed1e6f8 docs(arch): add technical architecture V0.4
代码开建前缺少技术事实来源。补入技术架构 V0.4:全栈 TypeScript 选型、
模块化单体与 8 个业务模块、数据模型与隔离策略、接口与异步任务协议、
11 条 ADR,需求基线对齐 PRD V1.6。

影响面:仅文档,无代码影响。回滚方式:git revert 本提交。
2026-11-02 15:09:51 +08:00

2019 lines
147 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
# 灵犀 · 信息流素材生产平台 技术架构设计
| **项** | **内容** |
| --- | --- |
| 文档版本 | V0.4(图片线按 PSD 套版 + 抠图合成落地) |
| 文档状态 | 待评审 |
| 编写日期 | 2026-11-02 |
| 需求基线 | `docs/PRD/灵犀-信息流广告素材一站式生产平台PRD.md` V1.6 |
| 关联文档 | `docs/立项建议/灵犀_信息流素材生产平台_立项建议书_V0.1.md`(论证)、`docs/PRD/灵犀_信息流素材生产平台_PRD_V0.3.md`(同事基线) |
| 适用对象 | 研发(前端 / 后端 / 测试 / 运维)、AI 工程、技术负责人、产品(用于确认技术边界) |
| 文档范围 | 技术栈选型、分层与模块、部署拓扑、数据模型、接口设计、关键链路、关键决策、非功能落地、交付路径、风险与未决 |
| 不包含 | 排期承诺与人力预算的最终审批、AI 模型的训练与微调方案、投放平台对接、代码实现细节 |
> **一句话**:这不是"用什么框架"的清单,而是**用一套可维护的模块化单体 + 异步任务管线,把 PRD 里的 15 类资产、两条素材生产线、行业包与 schema 动态表单、客户级数据隔离、全量审计与成本可见性,落在公有云上、让 2–4 个研发能在 3 个月内跑通 M0–M1**。
---
## 0 阅读说明
### 0.1 这份文档回答什么
1. **选什么**:前端、后端、数据库、队列、对象存储、媒体处理、AI 接入、观测、部署方式,以及为什么。
2. **怎么排**:进程与模块怎么划分、一次图片生产与一次视频生产在系统里怎么流动、数据写在哪。
3. **怎么兜**:PRD 的性能、隔离、审计、成本、合规要求,分别由哪个技术机制承接。
4. **不做什么**:哪些架构花样(微服务、K8s、自建推理、通用低代码)本期明确不做,以及什么条件下再谈。
### 0.2 结论摘要(先看这张表)
| **层** | **选型** | **一句话理由** |
| --- | --- | --- |
| 前端 | React 19 + TypeScript 5 + Vite 6 + Semi Design 2.x(`@douyinfe/semi-ui-19`) | 后台型产品,表单/表格/权限组件密度高;Semi 中后台组件齐全、设计语言贴合运营后台 |
| 图标 | Remix Icon 4.x(`@remixicon/react`) | 唯一图标库,线性风格统一;禁止其他图标源(见 2.2.1) |
| 后端 | NestJS 11(Fastify 适配器)+ TypeScript | 模块化边界、DI、守卫/拦截器天然对应"权限点 + 审计 + 数据范围"三件套 |
| 数据库 | PostgreSQL 18(**自建**,容器镜像 / 云主机,开发机 Docker) | JSONB 承载动态表单值、强事务、行级审计;自建可完全掌控版本、参数、扩展与备份策略;18 的异步 I/O 与虚拟生成列降低大表扫描与冗余列维护成本 |
| 缓存 / 队列 | Redis 7.4 + BullMQ 5 | 一套 Redis 同时做缓存、分布式锁、限流与任务队列,M0–M2 足够 |
| 对象存储 | S3 兼容(M0 自建 MinIO,可切云 OSS/COS) | 成片与中间产物按"对象 + 签名 URL"治理;S3 接口避免绑死厂商 |
| 媒体处理 | Node 24(24.15.0)+ ffmpeg 7 + sharp(CPU worker) | 日产 < 500 条时,拼接/转码/缩略图用 CPU 足够;不引入 Python 服务 |
| AI 接入 | 自建 Provider 适配层(HTTP/异步任务 + Webhook 轮询) | 多供应商并存、按用途路由、故障切换、逐次记账,都在这一层收口 |
| 部署 | 单地域公有云,Docker Compose 起步(预留 K8s 触发条件) | 日均 < 500 条不需要 K8s;先把可观测与备份做对 |
| 观测 | OpenTelemetry + Prometheus + Grafana + Loki + Sentry | 任务链路跨 API/Worker,必须能从"一条任务"追到"一次模型调用" |
| 文档与接口 | OpenAPI 3.1 + Zod 单一真源 | 前后端共享类型,减少"字段对不上"的返工 |
### 0.3 一页架构图
```mermaid
flowchart TB
subgraph Client["浏览器(内部用户,30–100 人)"]
WEB["React SPA<br/>运营 / 设计 / 审核 / 管理员"]
end
subgraph Edge["接入层"]
NGX["Nginx / Caddy<br/>TLS · 静态资源 · 反向代理"]
end
subgraph App["应用层(无状态,可水平扩)"]
API["NestJS API<br/>鉴权 · 权限点 · 数据范围 · 审计埋点"]
WS["进度通道<br/>SSE / WebSocket"]
end
subgraph Async["异步任务层(CPU worker,可水平扩)"]
Q["BullMQ 队列组<br/>script / image / video / export / verify"]
W1["脚本 Worker<br/>LLM 调用 · 质量校核"]
W2["图片 Worker<br/>模板合成 · 规格矩阵"]
W3["视频 Worker<br/>分镜 · 关键帧 · 图生视频 · 拼接"]
W4["导出 Worker<br/>打包 · 命名 · 清单 · 归档"]
end
subgraph Data["数据层"]
PG[("PostgreSQL 18<br/>业务数据 · 审计 · 成本账")]
RD[("Redis 7<br/>缓存 · 队列 · 限流 · 锁")]
S3[("对象存储 S3 兼容<br/>MinIO / OSS<br/>物料 · 成片 · 中间产物")]
end
subgraph Ext["外部能力(全部走适配层)"]
LLM["文本模型 API"]
IMG["图生图 / 图生视频 API"]
TTS["TTS / Lip Sync API"]
OBS["观测与告警"]
end
WEB --> NGX --> API
WEB -.->|订阅进度| WS
API --> PG
API --> RD
API --> S3
API -->|投递任务| Q
Q --> W1 & W2 & W3 & W4
W1 & W2 & W3 -->|出网调用| LLM & IMG & TTS
W1 & W2 & W3 & W4 --> PG
W2 & W3 & W4 --> S3
API & W1 & W2 & W3 & W4 --> OBS
```
### 0.4 阅读路径
- **只想看结论**:读 0.2 与第 2 章(选型)→ 第 8 章(关键决策)。
- **要评审技术方案**:第 1 章(目标与约束)→ 3 章(架构)→ 5 章(数据模型)→ 7 章(关键链路)→ 8 章(决策取舍)。
- **要排期与开工**:第 4 章(部署)→ 10 章(交付路径与里程碑)→ 附录 B(PRD 追溯矩阵)。
- **要评估风险**:第 11 章(技术风险与未决)。
### 0.5 与 PRD 的关系
| PRD 章节 | 本文承接方式 |
| --- | --- |
| 5.1 脚本模块 | 5.4 数据模型(要素卡 / 模板 / 脚本版本)+ 6.4 接口 + 7.2 时序 |
| 5.2 素材模块 | 5.4(任务 / 产物 / 分镜)+ 5.3 队列组 + 7.3 / 7.4 时序 |
| 5.3 资产模块 | 5.4(资产 / 版本 / 引用关系)+ 5.6 动态字段 + 7.5 归档 |
| 5.4 平台管理 | 5.4(权限 / 行业包 / 表单 schema)+ 6.2 鉴权 + 7.1 / 7.6 时序 |
| 5.5 导出与交付 | 6.7 导出接口 + 7.5 时序 |
| 6 关键规则 R-01~R-20 | 8 关键决策 + 5.4 约束(如资产删除保护用外键 + 应用校验双层) |
| 9 非功能与合规 | 9 非功能落地(逐条给指标拆解与实现机制) |
| 10 待确认 Q-01~Q-21 | 11.2 技术侧待确认 QT-01~QT-14(只列技术相关的) |
---
## 1 架构目标与约束
### 1.1 必须被架构承接的 14 条硬需求
下面每一条都不是"功能",而是**会改变架构形状的约束**。评审时逐条对齐,避免"PRD 写了、技术没落"。
| **#** | **PRD 出处** | **硬要求** | **架构应对** |
| --- | --- | --- | --- |
| 1 | 5.4.2、R-20 | 行业 → 客户两级目录是全部资产的根节点,客户不可跨行业移动 | 所有业务表强制带 `customer_id`,`industry_id` 由客户派生并冗余;外键与唯一约束在库层兜底 |
| 2 | R-04、9.2 | 客户级数据隔离:任何查询必须带客户维度,禁止"先查全量再过滤" | Repository 层强制注入数据范围片段;CI 静态检查禁止裸查询;P1 叠加 PG 行级安全(RLS) |
| 3 | 5.4.3 | 仅内置超管,其余角色可配置(权限点 + 数据范围 + 特殊权限 + SoD) | 权限点在代码中声明式注册(装饰器 + 常量表),角色为数据;鉴权 = 守卫(功能)+ 范围解析器(数据)+ 特殊权限校验 |
| 4 | 5.4.7 / 5.4.8 | 行业包 9 槽位 + 要素卡 schema 动态渲染,换行业不发版 | schema 以 JSON Schema 形式版本化存储;后端 Ajv 校验;前端轻量渲染器;可检索字段物化到索引表 |
| 5 | 5.1.1、R-11 / R-14 | 已发布字段只可弃用不可改名;历史要素卡按当时 schema 解释 | 字段 key 不可变、版本快照不可变;要素卡存 `schema_version_id`,读时按该版本解释 |
| 6 | 5.2.2 | 视频逐镜生成、单镜重试、拼接包装 | 任务拆到"镜头(shot)"粒度,每镜独立子任务与幂等键;拼接是独立任务,可重入 |
| 7 | 5.2.2、5.2.3 | 异步、进度可见、失败重试、中断可恢复(100 张到 87 张不必从头) | 任务头 + 任务项两级表;每项独立状态机;队列重入 + 幂等键 + 断点续跑 |
| 8 | 5.2.2、5.5 | 在线预览(首帧 ≤ 2s、断点续播)、命名下载、导出即归档 | 成片转 H.264 faststart MP4 + 签名 URL + Range 请求;缩略图预生成;导出为服务端打包 zip |
| 9 | 5.4.4、R-06 | 多供应商、按用途路由、额度与预算、失败切换、成本看板 | AI 适配层统一收口;模型/路由/额度全部数据化;每次调用落 `ai_usage` 账本 |
| 10 | 5.4.6、9.2 | 审计只读、不可修改、≥ 12 个月、可检索 | 审计表 append-only(禁 UPDATE/DELETE 权限)+ 按月分区 + 归档到对象存储 |
| 11 | 5.4.5、R-10 | 字典变更只影响新数据,历史保留快照 | 字典项带版本;业务记录存字典快照值而非仅外键 |
| 12 | 5.3.3、R-02 | 被引用资产禁止删除、版本可回溯、引用关系双向可查 | 资产表 + 版本表 + 引用关系表;删除前外键检查 + 应用层提示;软删除 |
| 13 | 9.1 | 列表 ≤ 2s、脚本 ≤ 30s、图片 ≤ 10s、50 张预览不卡 | 查询走覆盖索引 + 分页游标;生成走异步;缩略图独立小图;前端虚拟滚动 |
| 14 | 9.3、R-09 | AI 生成内容显式 + 隐式标识、肖像授权、分行业合规 | 合成阶段写入可见角标与 MP4 元数据;人物池存授权材料与到期时间;合规规则集数据化 |
### 1.2 规模假设与容量口径
用户给定:**日均成片 < 500 条、全部走外部 AI API、无自建 GPU、公有云部署**。以下推算是选型与规格的依据,评审时如有出入按比例调整。
| **维度** | **假设** | **推算** | **影响** |
| --- | --- | --- | --- |
| 用户规模 | 30–100 人,峰值在线 ≤ 60 | API 2 实例足够(QPS < 50) | 不需要 K8s / 服务网格 |
| 成片量 | 图片 + 视频合计 < 500 条/日,峰值批次 200 条 | 任务项 < 5000/日 | 单 Redis + 4 个队列组足够 |
| 视频并发 | 单条 15s ≈ 4–6 镜;单镜图生视频 60–180s | 500 条 × 5 镜 × 90s ≈ 62.5 模型小时/日,均值 3 并发、峰值 12–15 | Worker 并发 20,按供应商限流分池 |
| 图片合成 | 单张 ≤ 10s(sharp + 模板合成) | 2000 张/日 ≈ 5.6 小时单核 | 2 台 8C worker 足够 |
| 存储 | 视频 1080×1920/15s ≈ 6–12 MB;中间产物约成片的 3–5 倍 | 成片 5 GB/日 ≈ 1.8 TB/年;含中间产物 ≈ 8–10 TB/年 | 对象存储分层 + 中间产物 30 天生命周期 |
| 数据库 | 任务项 + 审计 + 成本账是主要增长 | 首年 < 200 GB | 200 GB ESSD 数据盘 + 按月分区,足够 |
| 出网 | AI 调用全部出公网;下载走公网 | 峰值出网 100 Mbps 量级 | 单地域 + 弹性带宽即可,暂不做 CDN |
| 成本 | 模型调用是主要变动成本 | 单条视频 5 镜 × 图生视频 + 1 次 TTS | 必须逐任务记账,否则无法报价 |
### 1.3 架构原则
1. **模块化单体优先,不等于"一个文件"**:按业务域切模块(Nest 模块 + 独立 Prisma 访问层),模块间只走服务接口与事件,不共享表写权限。微服务只在出现"独立扩容 / 独立团队 / 独立发布"三个信号之一时才拆。
2. **异步是一等公民**:凡是超过 2 秒、会调外部模型、会产生文件的操作,一律"提交 → 任务 ID → 进度通道 → 结果",前端不阻塞等待。
3. **配置驱动,代码分支最后**:行业差异、字典、规格、路由、提示词、权限点全部数据化;新增行业不改代码路径。
4. **数据隔离在数据层强制**:应用层注入范围之外,库层再加一道(外键 + 强制 `customer_id NOT NULL` + P1 RLS)。
5. **每一分钱可追溯**:每次模型调用都写账本(谁、哪个任务、哪个模型、多少 token/秒、多少钱),成本看板从账本聚合,不从日志猜。
6. **可恢复优于可重试**:任务项级断点 + 幂等键,让"跑到 87 张失败"能从 88 张继续,而不是重跑 100 张。
7. **观测先于优化**:接口耗时、队列积压、模型成功率、单条成本四个指标从 M0 就埋,否则性能与成本问题无法定位。
8. **数据层自管,版本统一**:数据库与缓存不自建云托管服务,PostgreSQL 18 与 Redis 7.4 统一用容器镜像部署(开发机 Docker Compose、生产云主机),大版本、参数、扩展、备份策略都由我们掌握;外部只依赖 AI 模型 API。这样本地与生产的差异只剩「规格」和「数据」,不剩「版本」。
### 1.4 明确不做(以及什么条件下再谈)
| **不做** | **原因** | **重评条件** |
| --- | --- | --- |
| 微服务拆分 | 日均 < 500 条、团队 < 10 人,拆分带来的是运维成本而非收益 | 单个模块需要独立扩容 3 倍以上,或出现独立团队独立发布 |
| Kubernetes | Compose + 2–4 台机器足够,K8s 的学习与运维成本高于收益 | Worker 峰值需要 10 台以上弹性,或多环境多租户隔离要求 |
| 自建模型推理 / GPU | 生成能力是公共品,采购优于自建(见 PRD 附录 B 第 1 条) | 单月 API 费用超过自建 TCO,或出现无法采购的模型能力 |
| 通用低代码 / 流程引擎 | PRD 5.4.8 明确"受控设计器",只解决要素卡字段 | 出现第二个需要动态表单的业务域 |
| 实时协同编辑 | 内容以"生成 + 审核"为主,不是多人同文档编辑 | 审核环节出现明确的多人在线编辑诉求 |
| CDN | 下载量 < 100 GB/日,源站带宽足够 | 出网带宽成本超过对象存储费用,或出现跨地域访问 |
| 跨地域多活 | 单地域 + 每日备份 + 可接受 RTO 4 小时 | 业务要求 RTO < 1 小时或强合规要求异地双活 |
| 云托管数据库(RDS / 云 Redis) | 需要完全掌控 PG 18 新特性、扩展与备份策略;托管实例可定制空间小、成本高,而本场景规模(< 200 GB)自建运维量可控 | 可用性靠主备 + 自建备份与演练;若团队无人愿承担数据层运维,或 SLA 要求提升,再评估托管 |
### 1.5 术语
| **术语** | **含义** |
| --- | --- |
| 任务(Task) | 一次用户提交的批量作业,如"生成 20 条脚本" |
| 任务项(Task Item) | 任务下可独立成败的最小单元,如 1 条脚本、1 张图片、1 个镜头 |
| 产物(Artifact) | 任务项产出的文件对象(图片 / 视频 / 中间帧 / 音频) |
| 幂等键(Idempotency Key) | 由"任务 ID + 项序号 + 版本"生成的唯一键,保证重投不重复计费 |
| 范围片段(Scope Clause) | 由角色数据范围解析出的 SQL 条件,如 `customer_id IN (...)` |
| 物化索引(Materialized Index) | 把动态表单中声明"可检索"的字段落到独立表,供筛选与报表 |
| 账本(Ledger) | 每次外部模型调用的用量与费用记录,append-only |
---
## 2 技术选型
### 2.1 选型总表
| **层** | **选型** | **版本基线** | **关键理由** | **主要代价** |
| --- | --- | --- | --- | --- |
| 语言 | TypeScript(全栈统一) | 5.7+ | 前后端共享类型与 schema 校验,减少字段错位 | 媒体处理生态弱于 Python(用 ffmpeg CLI 弥补) |
| 运行时 | Node.js 24 LTS | 24.15.0(锁定小版本) | 与 TS 生态一致;worker 并发模型适合 I/O 密集;24 为当前 LTS | CPU 密集任务需多进程(worker 独立进程池) |
| 包管理 | pnpm | 10.x | monorepo 原生、安装快、磁盘省、无幽灵依赖;**唯一包管理器,禁 npm / yarn** | 团队需统一 pnpm 版本(`packageManager` 锁定),历史脚本需改写 |
| 前端框架 | React | 19.x | 生态最大;Semi Design 已支持 React 19 | 需在入口最顶部引入 `react19-adapter` |
| 构建 | Vite | 6.x | 启动快、配置少 | 与 SSR 无关(本期纯 SPA) |
| UI 组件 | Semi Design(React 19 构建 `@douyinfe/semi-ui-19`) | 2.x | 中后台组件齐全(Table / Form / Modal / Toast / Tree),设计语言统一;19 包 peer 锁定 react ^19,避免 peer 冲突 | 无 ProComponents 等价物,复杂表格页 / 查询表单需在 `packages/ui` 二次封装(约 3–4 人周);入口仍需导 adapter(见 2.2.1) |
| 图标 | Remix Icon(`@remixicon/react`,统一出口 `packages/ui/icons.ts`) | 4.x | 唯一图标来源,线性风格统一、体积可控 | 禁止其他图标库与内联 SVG(见 2.2.1) |
| 后端框架 | NestJS(Fastify 适配) | 11.x | 模块化 + DI + 守卫 / 拦截器,天然承载权限与审计 | 装饰器较多,团队需约定分层规范 |
| ORM | Prisma | 6.x | 类型安全、迁移可审阅、JSONB 支持好 | 复杂 SQL 需 `$queryRaw` 兜底 |
| 数据库 | PostgreSQL | 18 | JSONB + GIN、分区、强事务、RLS;18 的异步 I/O 提升大表扫描吞吐 | 动态表单查询需索引设计 |
| 缓存 / 队列 | Redis + BullMQ | 7.4 / 5.x | 缓存、限流、锁、队列一套搞定 | 队列持久化弱于 Kafka(任务可重建,可接受) |
| 对象存储 | S3 兼容(MinIO 起步) | MinIO 最新稳定 | 签名 URL、版本化、生命周期、可迁云 | 需自运维磁盘与副本 |
| 媒体处理 | ffmpeg 7 + sharp | 7.x / 0.33+ | 拼接 / 转码 / 抽帧 / 缩略图 / 合成 | 需做进程隔离与超时控制 |
| 鉴权 | 自建 JWT(access + refresh)+ OIDC 适配 | — | PRD 要求 SSO 可插拔,JWT 先落地 | SSO 接入点需预留接口 |
| 接口文档 | OpenAPI 3.1(Nest Swagger 生成) | — | 前后端契约 + 测试依据 | 需与 Zod 校验保持同步(用单一真源生成) |
| 校验 | Zod + Ajv(动态 schema) | 3.x / 8.x | 静态 DTO 用 Zod,动态表单用 Ajv | 两套校验器需约定边界 |
| 观测 | OpenTelemetry + Prometheus + Grafana + Loki + Sentry | — | 跨 API/Worker 链路追踪 | 自建 Loki 需磁盘规划 |
| 部署 | Docker + Docker Compose | — | 3 个月内最快落地 | 弹性扩容需手动 |
| CI/CD | GitHub Actions(或 GitLab CI) | — | 仓库已托管,流程简单 | 无(当前仓库私有) |
### 2.2 前端
| **能力** | **选型** | **说明** |
| --- | --- | --- |
| 路由 | React Router 7 | 后台型 SPA,按模块懒加载 |
| 服务端状态 | TanStack Query 5 | 列表 / 详情缓存、失效、轮询任务进度 |
| 客户端状态 | Zustand 5 | 轻量,避免 Redux 样板代码 |
| 表格 | Semi Table(`virtualized`) | 任务项、资产库动辄上千行,必须虚拟化;Semi Table 自带虚拟滚动 |
| 图标 | `@remixicon/react` + `packages/ui/icons.ts` 语义出口 | 全站唯一图标来源,业务不直接引具体图标名 |
| 动态表单渲染 | 自研轻量渲染器(基于行业包 schema) | 只支持 PRD 5.4.8 的 14 类字段白名单;比通用表单引擎可控 |
| 富文本 | Tiptap | 要素卡中的"多行文本 / 富文本"字段 |
| 拖拽排序 | dnd-kit | 资产排序、模板字段排序(仅列表内,不做自由布局) |
| 图表 | ECharts 5 | 产能 / 质量 / 成本看板 |
| 视频预览 | 原生 `<video>` + HLS.js(P2 再上) | M0–M2 用 progressive MP4,按需 HLS |
| 图片快筛 | 自研网格虚拟列表 + 键盘快捷键 | 50 张并排、勾选状态本地缓存并回写 |
| 代码质量 | ESLint + Prettier + Vitest + Playwright | 与后端共用 lint 配置 |
> **为什么自研表单渲染器而不是用 Formily / RJSF**:PRD 的 schema 是"JSON Schema + UI 元数据 + 变量映射 + 进入生成标记",与通用表单库的数据模型有偏移;自研渲染器约 2 人周,换来字段联动、校验、可检索标记与提示词变量的统一,后续接表单设计器也更顺。
#### 2.2.1 UI 组件库与图标硬约定
**组件库:只用 Semi Design**
| **项** | **约定** |
| --- | --- |
| 唯一组件库 | React 19 用 **`@douyinfe/semi-ui-19`**(底座 `@douyinfe/semi-foundation` 按需);**不得与 `@douyinfe/semi-ui` 混装**(两包 `lib/es` 内容完全一致,混装会重复打包组件与样式);也不引入第二套组件库(Ant Design / Arco / Element 等) |
| React 19 适配 | 入口文件**最顶部**(任何 Semi 组件之前)`import '@douyinfe/semi-ui-19/react19-adapter';`。原因:React 19 的 `react-dom` 已不再导出 `createRoot`(19.3.1 实测只剩 `createPortal`),Semi 的 `Modal` / `Toast` / `Notification` 靠注入的 `createRoot` 动态挂载,未注入时报 `[Semi UI] createRoot is not available`。注:官方 getting-started 只说「v19 换 `-19` 包」,react19 生态页要求导 adapter,**两件事都要做**(`-19` 包内已带 `react19-adapter` 入口) |
| 已知限制 | React 19 下 `Tooltip` / `Popover` / `Dropdown` 的 children 若是类组件可能定位异常,用 DOM 元素包裹即可(项目内统一用函数组件 + `forwardRef`) |
| 中后台补齐 | Semi 没有 ProComponents 等价物:查询表格、详情页、权限树、批量选择等封装在 `packages/ui`(约 3–4 人周),宁可自研也不引第二套库 |
| 视觉统一 | 只改 Semi 的 design token(`cssVar`)与主题变量,不在业务里写散落色值与圆角 |
| 表单分工 | 静态表单用 Semi `Form`;要素卡等动态表单用自研 schema 渲染器,底层控件仍是 Semi 的 `Input` / `Select` / `DatePicker` |
| 国际化 | 先 zh-CN,文案集中在 `packages/ui/locale`,预留 `LocaleProvider` 切换能力 |
**图标:只用 Remix Icon**
| **规则** | **落地方式** |
| --- | --- |
| 唯一图标库 | `@remixicon/react` 4.x;业务代码只从 `packages/ui/icons.ts` 按语义名导入(如 `IconExport`、`IconRefresh`),不直接引包名、不直接写 `RiDownloadLine` 这类具体图标名 |
| 禁止清单 | `@douyinfe/semi-icons`、`@ant-design/icons`、`react-icons`、`lucide-react`、`@heroicons/*`、iconfont 字体图标、内联手写 SVG(品牌 Logo 除外)一律禁用 |
| 强制执行 | ESLint `no-restricted-imports` 覆盖上述包名,并**禁止导入 `@douyinfe/semi-ui`**(只允许 `-19`);CI 额外扫描仓库内 `*.svg` 与 `@font-face` 引入;PR 自检清单加一条(图标来源) |
| 风格统一 | 只用 Remix Icon 的 Line 风格,不混用 Fill;尺寸用 `size={16}` / `size={20}` 语义值,颜色继承 `currentColor` |
| 组件内置图标 | Semi 组件内部的小图标(下拉箭头、勾选框)无法逐个替换,作为**已登记例外**保留;业务侧新增图标一律用 Remix Icon,P2 再评估样式覆盖 |
| 体积控制 | 按需引入单个图标(`import { RiExportLine } from '@remixicon/react'`),禁止 `import * as Icons` 全量引入 |
### 2.3 后端
| **能力** | **选型** | **说明** |
| --- | --- | --- |
| HTTP 层 | NestJS + Fastify | 吞吐高于 Express,拦截器/管道模型完整 |
| 模块划分 | 8 个业务模块(见 3.2) | 模块内 controller → service → repository 三层 |
| 参数校验 | Zod(静态 DTO)+ Ajv(动态 schema) | 动态表单按版本编译 Ajv 校验器并缓存 |
| 鉴权 | JWT(短 access 15min + refresh 7d)+ 权限守卫 | 特殊权限单独装饰器 `@RequireSpecial('export')` |
| 数据访问 | Prisma + 少量原生 SQL | 复杂报表用 `$queryRaw` + 视图 |
| 事务 | Prisma `$transaction` + 领域事件表(Outbox) | 任务入队与业务写入同一事务,避免"扣了额度没建任务" |
| 幂等 | 请求级 `Idempotency-Key` 头 + 任务项唯一键 | 重复提交返回原任务 ID |
| 审计 | 拦截器 + 领域事件,异步落库 | 只记录"变更类"操作,读操作仅记录敏感访问 |
| 定时任务 | BullMQ repeatable job | 复审提醒、月度预算结算、清理任务 |
| 日志 | Pino(结构化 JSON)+ traceId | 与 OTel 打通 |
**分层约定**(写进工程规范,评审时对照):
- Controller 只做参数解析与响应组装,不写业务判断;
- Service 承载业务规则(含权限点校验、状态机迁移),不直接拼 SQL;
- Repository 是唯一能访问 Prisma 的层,**必须接收 `ScopeContext`**(数据范围上下文),否则类型层面不允许编译;
- 跨模块调用走 Service 接口或领域事件,禁止跨模块直接读写对方表。
> 第 3 条是 R-04「任何查询必须带客户维度」的工程落点:把数据范围做成 Repository 方法的必传参数,比写规范文档更有效。
### 2.4 异步任务与媒体处理
**队列分组**(BullMQ,按"资源类型 + 优先级"分,不是按业务模块分):
| **队列** | **并发** | **典型耗时** | **重试策略** | **说明** |
| --- | --- | --- | --- | --- |
| `script:generate` | 8 | 5–30s | 3 次,指数退避 | LLM 调用,按供应商限流分池 |
| `image:matting` | 2(按实测内存夹取,默认 2) | 2–15s | 3 次 | 本地 ONNX 抠图(`@imgly/background-removal-node`),CPU 密集;失败降级原图并标「低质量」,必须落库 |
| `image:render` | 2–4(默认 2,再按可用核数夹取) | 1–10s | 3 次 | `@napi-rs/canvas` + `sharp` 套版合成,CPU 密集,独立进程池;禁止与 ZIP 打包混跑 |
| `delivery:zip` | 1 | 视批量 | 2 次 | 流式 `archiver`(level 9),压缩是纯 CPU,与合成互相抢核,故单独排队 |
| `video:keyframe` | 10 | 10–40s | 3 次 | 图生图,受供应商限流 |
| `video:shot` | 12 | 60–180s | 2 次(超时/限流可重试,内容违规不重试) | 图生视频,最贵,必须记账 |
| `video:compose` | 4 | 20–90s | 3 次 | ffmpeg 拼接 + 字幕 + Logo + 警示语 + 音频 |
| `media:verify` | 4 | 5–20s | 2 次 | 时长 / 比例 / 黑帧 / 静音 / 字幕错字 / AI 标识 |
| `export:package` | 2 | 10–120s | 2 次 | 打包 zip、命名、清单、归档 |
| `media:post` | 6 | 5–60s | 3 次 | 外部上传成片的抽帧、转码 HLS、水印、缩略图、元数据提取(非生成类,失败不影响原文件) |
| `embed:index` | 2 | 2–10s | 3 次 | 脚本语义块向量化与索引(M3 起启用,M1–M2 关闭) |
**媒体处理原则**:
1. **进程隔离**:ffmpeg / sharp 在 worker 子进程中执行,设超时(视频 10 分钟、图片 60 秒),超时杀进程并标记失败,不拖垮 worker。
2. **临时目录**:worker 本地 `/tmp/task/{taskItemId}` 存放中间文件,成功后上传对象存储并清理;失败保留 24 小时便于排查。
3. **统一编码参数**:视频 H.264 / AAC / 1080×1920 / 30fps / faststart;图片 JPG q=92 或 PNG;参数写入 `render_profile` 配置,不散落代码。
4. **可中断**:每个任务项完成即落库(含产物对象 key),重投时跳过已完成项(`task_items.status` 判断)。
5. **不重生成已成功镜头**:拼接任务只读已完成镜头对象,单镜替换只重跑该 shot。
### 2.5 数据与存储
| **用途** | **选型** | **关键配置** |
| --- | --- | --- |
| 业务库 | PostgreSQL 18(自建:云主机上跑 `postgres:18`,开发机同镜像) | 每日全备 + WAL 连续归档到 MinIO(PITR 7 天);审计表按月分区;参数与初始化脚本进仓库(`infra/postgres/`) |
| 缓存 / 队列 | Redis 7.4(自建:`redis:7.4`,主从 + AOF everysec) | 缓存逻辑库 db0、队列 db1、限流/锁 db2,避免互相干扰;队列消息可重建、缓存丢失可接受,但队列库不设淘汰策略 |
| 对象存储 | MinIO(2 节点,纠删码)+ 云 OSS 冷备 | 桶:`assets`(源资产)、`renders`(成片)、`temp`(中间产物)、`archive`(审计/导出归档) |
| 文件下载 | 签名 URL(15 分钟)+ 服务端 zip 流 | 大包走导出任务,不在请求内同步打包 |
| 备份 | pgBackRest(或 WAL-G)归档到 MinIO + MinIO 每日跨桶复制 | RPO:数据库 ≤ 5 分钟,对象 ≤ 24 小时;RTO 4 小时 |
> **PostgreSQL 18 说明**:18 提供的 `uuidv7()`、虚拟生成列与异步 I/O 可直接用于新表与既有慢查询;但**主键仍统一用应用侧生成的 ULID**(便于前端乐观渲染、未来分库与导入导出),不依赖库内函数,避免出现「两套主键来源」。jsonb 的 GIN 索引、分区与 RLS 用法与 17 一致,迁移无额外改造。
**桶与生命周期**:
| **桶** | **内容** | **生命周期** |
| --- | --- | --- |
| `assets` | 参考物料、模板文件、Logo、人物/场景素材 | 永久 + 版本化(保留 5 个版本) |
| `renders` | 成片(图片 / 视频) | 永久(删除走业务逻辑,不靠生命周期) |
| `temp` | 关键帧、单镜视频、TTS 音频、合成中间文件 | 30 天自动删除 |
| `archive` | 审计日志归档、导出清单快照 | ≥ 12 个月 |
| `uploads` | 外部上传的原文件(成片 / 图片)与分片临时区 | 分片 7 天自动清理;完整对象随素材永久 |
| `exports` | 打包产物(zip) | 3 天自动删除 |
> **秒传与直传**:`material_files.hash`(sha256)建唯一索引,预签名阶段先查 hash → 命中则直接引用已有对象(秒传);未命中返回分片预签名。下载一律短时效签名 URL(5–15 分钟),禁止桶公开读;对象存储统一经 `StorageProvider` 接口(`presignPut` / `presignGet` / `head` / `copy` / `delete` / `listMultipart`),便于在 MinIO / OSS / S3 之间切换。
### 2.6 AI 接入层
PRD 5.4.4 要求"多供应商并存、按用途路由、额度预算、健康降级、成本看板",因此**不能在各业务模块里散着调 SDK**,必须收口为一个适配层。
| **组件** | **职责** | **实现要点** |
| --- | --- | --- |
| Provider 适配器 | 文本 / 图生图 / 图生视频 / TTS 四类能力,每类一个统一接口 | `generate(req) -> jobId` + `poll(jobId)` 或 webhook 回调;供应商差异在适配器内消化 |
| 路由表 | 用途 → 模型链(主 / 备)→ 可按客户覆盖 | 数据表 `ai_routes`,带生效范围与优先级 |
| 限流池 | 按供应商 QPS / 并发数限流 | Redis 令牌桶,worker 取令牌后再调用 |
| 重试与降级 | 限流/超时/5xx 重试 → 主模型失败切备用 | 内容违规(4xx 语义)不重试,直接失败并记录原因 |
| 成本账本 | 每次调用写 `ai_usage`(token / 秒 / 张 / 金额) | 由适配器统一写入,业务层不重复记账 |
| 内容安全 | 调用前关键词与规则校验、调用后结果抽检 | 与行业包合规规则集联动;命中即拦截并留痕 |
| 配置四层 | 供应商 → 模型 → 任务映射 → 调用日志,参数逐层覆盖 | 表 `ai_providers` / `ai_models` / `ai_task_mappings` / `ai_call_logs`;合并顺序:任务级 > 模型级 > 供应商默认 |
| 提示词与行业指南 | 系统提示词 / 负面词 / 行业指南版本化,可回滚 | 存 `ai_configs` + `ai_config_versions`;任务记录引用版本号 |
| 方言适配 | thinking 开关、JSON 模式、联网搜索的供应商差异 | 适配器内映射:`thinking.type` ↔ `enable_thinking`(走 extra_body);提示词强制含 JSON;联网作为独立前置步骤 |
| 密钥与并发 | 密钥加密、模块级并发上限 | AES-256-GCM 加密(出参只给 `hasApiKey`);Redis 信号量限制全局限并发 |
**异步模型调用的两种形态**(必须在适配层统一):
- **提交 + 轮询**:适用于多数图生视频 / 图生图 API;worker 提交后进入退避轮询,最长等待时长写入配置。
- **提交 + Webhook**:供应商支持回调时优先用;回调入口 `/api/v1/callbacks/ai/{provider}` 做签名校验与幂等。
**四层合并与降级**(实现顺序):
| **层** | **举例** | **覆盖关系** |
| --- | --- | --- |
| 供应商 Provider | 火山方舟 / DeepSeek / 阿里百炼 | Base URL、密钥、平台类型;最底层的默认值 |
| 模型 Model | `doubao-*` / `deepseek-*` / `qwen-*` | 上下文、最大输出、默认参数(JSONB);覆盖供应商默认 |
| 任务映射 TaskMapping | 脚本生成 / 解析 / 打标 / 校核 / 图生图 / 图生视频 / TTS | 主模型必填 + 备用可选 + 任务级参数;覆盖模型默认 |
| 调用日志 CallLog | 每次调用一行 | 只写不改;记录模型 ID、参数快照、用量、估算金额、错误码 |
**方言差异必须收口在三处**:① 深度思考参数名(豆包 / DeepSeek 用 `thinking.type`,通义用 `enable_thinking` 且需 extra_body);② 默认值相反(有的默认开思考);③ JSON 模式(通义提示词须含 JSON 字样,DeepSeek 偶发空 content)。业务代码只传语义参数(开 / 关 / 强度),映射与兜底都在适配器内。
**成本口径**:调用前预占、调用后按实际用量结算,估算价与真实账单分列,月度对账;参考项目实测约解析 0.0007 元 / 条、生成 0.003 元 / 次,仅用于预估,不能当报价。
### 2.7 可观测性与运维
| **维度** | **工具** | **关键指标 / 动作** |
| --- | --- | --- |
| 指标 | Prometheus + Grafana | 接口 P95、队列积压与等待时长、任务成功率、单条成本、模型 P95 耗时、供应商错误率 |
| 日志 | Loki(结构化 JSON) | 按 traceId / taskId 检索;敏感字段脱敏 |
| 链路追踪 | OpenTelemetry | API → 队列 → worker → 外部 API 全链路;traceId 随任务透传 |
| 异常 | Sentry | 前后端错误聚合;按模块与版本聚合 |
| 告警 | Grafana Alerting → 企业微信 / 飞书 | 队列积压 > 10 分钟、任务失败率 > 5%、单日成本超预算 80%、模型可用性 < 95% |
| 巡检 | 每日定时任务 | 孤儿对象、失败任务滞留、审计分区、备份结果 |
### 2.8 安全与合规组件
| **要求**(PRD 出处) | **机制** |
| --- | --- |
| 客户数据隔离(R-04) | `customer_id` 强制 + Repository 范围注入 + P1 RLS |
| 导出全量审计(R-05) | 导出任务写 `export_records` + 审计事件;下载链接签名且 15 分钟过期 |
| 被引用资产禁止删除(R-02) | 引用关系表 + 删除前检查 + 软删除 |
| 批量前预估成本(R-06) | 提交接口强制返回预估;超预算需二次确认 |
| 审计只读(9.2) | 审计表仅 INSERT 权限;按月分区 + 归档到 `archive` 桶 |
| AI 生成标识(R-09) | 合成阶段写入角标与 MP4 元数据;校验任务检查标识存在性 |
| 肖像与素材授权(9.3) | 人物池资产带授权材料与到期日;到期前 30 天提醒 |
| 密钥管理 | 模型密钥存 KMS / 环境变量加密;不落库、不进日志 |
| 上传安全 | 预签名直传 + 类型/大小校验 + 病毒扫描(ClamAV 可选)+ 图片重编码 |
### 2.9 放弃的选项及原因
| **选项** | **为什么不用** | **代价** |
| --- | --- | --- |
| Java / Spring Boot | 团队选择了 TS 全栈;后台治理能力 Nest 已够 | 复杂并发与内存管理不如 JVM,但本场景无重 CPU 业务 |
| Python(FastAPI)全栈 | AI 生态好,但后台治理(RBAC / 表单引擎 / 强类型)工程化成本更高 | 媒体与 AI 仍可后续加 Python 旁路 |
| Python 独立媒体服务 | < 500 条/日时 CPU 够用,多一套服务多一份运维 | 若出现复杂视频模板(多层合成 / 动画),再评估 Remotion 或 Python 服务 |
| Kafka / RabbitMQ | 任务可重建、无跨团队消费方,Redis 队列足够 | 队列持久化与堆积能力弱,需监控兜底 |
| MongoDB | 业务是强关系(客户 / 资产 / 引用),JSONB 已解决动态字段 | — |
| Temporal | 工作流模型好,但引入学习成本;本期任务链路用 BullMQ + 状态机即可 | 未来若任务编排复杂化(多分支审批流)再评估 |
| 通用表单引擎(Formily / RJSF) | 与行业包 schema 的变量映射、可检索标记、进入生成标记不匹配 | 自研渲染器约 2 人周 |
| 云函数(FC / SCF) | 视频处理需要长时任务与本地临时盘,函数形态不合适 | — |
| S3 云服务直接用 OSS(不做 S3 抽象) | 避免绑死厂商;MinIO 可本地化验证 | 运维多一套组件(可用 OSS 取代,接口不变) |
| Ant Design 5 + ProComponents | 团队确定统一用 Semi Design;双组件库会让设计 token、包体、交互规范分裂,ProComponents 的收益不足以引入第二套体系 | 查询表格 / 详情页等需在 `packages/ui` 自研封装(约 3–4 人周) |
### 2.10 版本基线
| **组件** | **版本** | **锁定方式** |
| --- | --- | --- |
| Node.js | 24.15.0(24 LTS) | `.nvmrc` 写 `24.15.0` + 镜像基础镜像 `node:24.15.0-bookworm-slim` |
| pnpm | 10.x | 根 `package.json` 的 `packageManager` 字段 + CI 启动时校验 |
| TypeScript | 5.7+ | 根 `package.json` |
| React / Vite / Semi UI | 19 / 6 / 2.x(`@douyinfe/semi-ui-19`) | pnpm lockfile |
| Remix Icon | 4.x | pnpm lockfile(唯一图标库) |
| NestJS / Fastify | 11 / 5 | pnpm lockfile |
| Prisma | 6 | lockfile + `prisma migrate` |
| PostgreSQL | 18 | `postgres:18` 镜像(自建);参数文件与初始化脚本进仓库 |
| Redis | 7.4 | `redis:7.4` 镜像(自建) |
| BullMQ | 5 | lockfile |
| ffmpeg | 7.x | 镜像内固定版本 |
| sharp | 0.33+ | lockfile |
| MinIO | 最新稳定(RELEASE 标签) | 镜像 digest 固定 |
> **兼容性检查**:Node.js 24.15.0 属 24 LTS 线,NestJS 11 要求 Node ≥ 20、Prisma 6 要求 Node ≥ 18.18、sharp 通过 N-API 预编译包(不随 Node ABI 变化);上线前在 CI 用锁定镜像跑一次 `pnpm install --frozen-lockfile` + 全量单测,并以 `engines` 字段兜底。PostgreSQL 18 为自建部署:需确认 `postgres:18` 镜像与备份工具(pgBackRest / WAL-G)版本兼容、Prisma 对 18 的支持(走标准 pg 协议,通常仅需版本声明);另注意开发机 Docker 的 `shared_buffers` / `shm_size` 远小于生产,本地跑出的性能结论不可直接外推。
> 版本基线是"起步基线",不是"永远不动":第三方大版本升级走单独工单,与业务迭代解耦。
## 3 总体架构
### 3.1 分层职责
| **层** | **职责** | **不做什么** |
| --- | --- | --- |
| 接入层 | TLS、静态资源、反向代理、限流(IP 级)、请求体大小限制 | 不做业务鉴权、不缓存业务数据 |
| 应用层(API) | 鉴权与权限点、数据范围解析、参数校验、事务边界、审计埋点、任务投递 | 不做长耗时处理、不直接调外部模型 |
| 异步任务层 | 脚本生成、图片合成、视频逐镜与拼接、校验、导出打包 | 不直接对外提供 HTTP 接口(回调除外) |
| 领域层 | 8 个业务模块的业务规则与状态机 | 不感知 HTTP、不感知队列 |
| 数据访问层 | Prisma / 原生 SQL、范围注入、事务 | 不写业务判断 |
| 集成层 | AI Provider 适配、对象存储、短信/SSO、观测 Export | 不做业务决策,只做协议转换与重试 |
| 数据层 | PostgreSQL、Redis、对象存储 | — |
### 3.2 模块划分(8 个业务模块)
| **模块** | **对应 PRD** | **职责** | **拥有的表** | **对外能力** |
| --- | --- | --- | --- | --- |
| `identity` | 5.4.1 / 5.4.3 | 组织、用户、角色、权限点、数据范围、登录、SoD 校验 | users / roles / permissions / role_permissions / user_roles / data_scopes / departments | 鉴权守卫、范围解析、权限变更事件 |
| `client` | 5.4.2 | 行业目录、客户、客户组、客户级字段扩展与审批 | industries / customers / customer_groups / customer_group_members / customer_fields | 客户上下文、资产归属校验 |
| `catalog` | 5.4.5 / 5.3 | 数据字典、资产(15 类)、版本、引用关系、生命周期与复审 | dict_types / dict_items / assets / asset_versions / asset_refs / asset_reviews | 资产检索、引用校验、版本回溯 |
| `industry` | 5.4.7 / 5.4.8 | 行业包 9 槽位、schema 版本、发布审批、表单设计器后端、客户覆盖 | industry_packs / industry_pack_versions / form_schemas / compliance_rules / prompt_templates | schema 编译与校验、发布流水线 |
| `script` | 5.1 | 要素卡、卖点库、脚本模板、批量生成、质量校核、审核入库、版本 | brief_cards / brief_card_index / selling_points / script_templates / scripts / script_versions | 脚本生成、校核、入库 |
| `media` | 5.2 | 图片线、视频线、任务与任务项、分镜、快筛、预览、失败与成本 | media_tasks / media_items / shots / render_profiles / preview_marks | 任务编排、产物管理 |
| `delivery` | 5.5 | 导出、命名规则、交付清单、归档、下载签名 | exports / export_items / delivery_manifests | 打包、命名、审计 |
| `platform` | 5.4.4 / 5.4.6 | AI 配置(模型/路由/提示词/额度)、AI 调用账本、审计、系统配置、成本看板 | ai_providers / ai_routes / ai_prompts / ai_usage / audit_logs / system_configs / cost_budgets | AI 适配层入口、审计写入、成本聚合 |
**共享内核**(`packages/shared`,不拥有表):领域事件定义、错误码、权限点常量、Zod schema、分页/游标、ID 生成、时间与命名规则工具。
**边界纪律**(评审检查项):
1. 模块之间只通过 Service 接口或领域事件交互,禁止跨模块 `prisma.xxx` 直接读写(用 ESLint 规则 + code review 兜底)。
2. `identity` 与 `client` 是被依赖方,不允许反向依赖 `script / media`。
3. `platform.ai` 是唯一的出网模型调用入口,其他模块不得直接 `fetch` 供应商接口。
### 3.3 进程拓扑
| **进程** | **实例(生产)** | **职责** | **扩缩方式** |
| --- | --- | --- | --- |
| `web`(Nginx) | 1(可与 API 同机) | 静态资源、反代、TLS | 垂直 |
| `api` | 2 | HTTP 接口、SSE 进度、回调入口 | 水平(无状态) |
| `worker-script` | 1(并发 8) | 脚本生成 + 校核 | 按队列积压水平扩 |
| `worker-image` | 1(并发 16) | 图片合成 | 按 CPU 水平扩 |
| `worker-video` | 2(并发 12) | 关键帧、镜头生成、拼接 | 按队列积压水平扩 |
| `worker-export` | 1(并发 2) | 打包、命名、归档 | 按导出量水平扩 |
| `scheduler` | 1 | 定时任务(复审、预算结算、清理、巡检) | 单实例 + Redis 锁 |
| `minio` | 2 节点 | 对象存储 | 加节点/加盘 |
| `postgres` | 自建 1 主(M1 起 +1 从,流复制) | 业务库 | 升配 + 读副本(P2) |
| `redis` | 自建 1 主 1 从(AOF) | 缓存/队列/限流 | 升配 |
> worker 可以合并在同一台机器上跑(M0 用 2 台 8C16G 即可),但**队列分组必须分开**:视频任务一旦积压,不能把脚本生成一起拖死。
### 3.4 一次图片生产的端到端
以"20 个商品 × 2 个模板 × 3 个规格 = 120 张"为例:
| **步** | **发生位置** | **动作** | **落库 / 落对象** |
| --- | --- | --- | --- |
| 1 | API | 校验权限点(素材生成)+ 数据范围(客户可见) | — |
| 2 | API | 读客户资产包(模板 / 规格 / Logo / 警示语),解析行业包 schema | 读缓存(Redis,5 分钟) |
| 3 | API | 生成任务头 + 120 个任务项 + 幂等键,写入 Outbox | `media_tasks` / `media_items`(状态=排队) |
| 4 | API | 返回 `taskId` 与"预计 6 分钟、预估成本 ¥X" | — |
| 5 | Outbox→队列 | 投递 120 个 `image:render` 消息(按客户分片,避免单客户占满) | Redis 队列 |
| 6a | Worker | `image:matting`:抠图(ONNX CPU)→ 透明底 PNG;失败退原图并打 `low_quality` | `media_items.matting`(JSONB:模型版本、耗时、是否降级) |
| 6b | Worker | `image:render`:读 PSD 模板版本 → canvas 按命名图层落位(`image_replacement_` / `clipping_mask_` / `text_replacement_`)→ sharp 编码 → 上传 `renders` | `media_items.status=生成中→待预览`,`artifacts.object_key` |
| 7 | Worker | 质量校验(安全区 / Logo / 变形 / 警示语 / 缺图层 / 字体回退 / 尺寸超限) | `media_items.checks`(JSONB) |
| 8 | API/SSE | 前端订阅进度,网格占位逐步替换为缩略图 | — |
| 9 | API | 用户勾选 → 提交导出(`export:package`) | `exports` / `export_items` |
| 10 | Worker | `delivery:zip`:按命名规则(批次快照)生成文件名 → 流式 zip → 上传 → 写交付清单 → 归档成片资产 | `exports.object_key`、`assets`(成片) |
| 11 | API | 返回 15 分钟签名下载 URL;写审计 | `audit_logs` |
**异常路径**:
- 单张失败 → 该任务项置失败并记录分类(模板缺图层 / 物料损坏 / 合成超时),其余继续;支持"仅重跑失败项"。
- worker 崩溃 → 消息重回队列;任务项有幂等键与"已完成"判断,不重复计费。
- 用户中途取消 → 任务头置取消,已投递消息在 worker 取到后快速返回。
### 3.5 一次视频生产的端到端
以"3 条脚本 × 3 个人物 = 9 条 15s 视频"为例:
| **步** | **发生位置** | **动作** | **落库 / 落对象** |
| --- | --- | --- | --- |
| 1 | API | 校验权限、数据范围;校验脚本状态=已入库 | — |
| 2 | API | 分镜:读脚本的 shot 结构,展开 9 条 × 5 镜 = 45 个镜头项 | `media_tasks` / `media_items`(每条)/ `shots`(每镜) |
| 3 | API | 成本预估(45 次图生视频 + 9 次拼接 + 可选 TTS)与预算校验 | 超预算需二次确认 |
| 4 | 队列 | 投递 `video:keyframe`(45 个) | Redis |
| 5 | Worker | 关键帧:图生图,带参考物料(白底图)与人物、场景、光影提示词 | `shots.keyframe_object_key` |
| 6 | 队列 | 投递 `video:shot`(45 个,取供应商令牌) | Redis |
| 7 | Worker | 图生视频(提交 → 轮询/webhook → 下载) | `shots.clip_object_key`、`ai_usage` |
| 8 | Worker | 口播(如需):TTS → lip sync → `shots.audio_object_key` | `ai_usage` |
| 9 | 队列 | 全部镜头完成后投递 `video:compose`(9 个) | Redis |
| 10 | Worker | ffmpeg 拼接 + 字幕(逐字对齐)+ Logo + 警示语 + BGM + AI 标识 | `renders`(成片) |
| 11 | Worker | `media:verify`:时长 / 比例 / 黑帧 / 静音 / 字幕错字 / 标识 | `media_items.checks` |
| 12 | API | 在线预览:签名 URL + Range;缩略图;勾选 | `preview_marks` |
| 13 | API/Worker | 命名(占位符 + 固定文本)→ 下载或导出 → 归档 | `exports` / `assets` |
**成本控制点**:
- 任何一次 `video:shot` 调用前必须有账本记录(调用前预占、调用后结算),防止"提交了 45 个任务但预算只够 20 个"。
- 单镜失败可换模型重试(路由表切换),但**换模型产生的额外费用单独记录**,便于评估"便宜的模型是否真的省"。
### 3.6 状态机
```mermaid
stateDiagram-v2
[*] --> 排队
排队 --> 生成中: worker 取任务
生成中 --> 待预览: 全部任务项成功
生成中 --> 部分失败: 存在失败项
部分失败 --> 生成中: 重试失败项
待预览 --> 已勾选: 人工勾选
已勾选 --> 已导出: 打包完成
已导出 --> 已入库: 自动归档
生成中 --> 已取消: 用户取消
部分失败 --> 失败: 全部失败
```
| **对象** | **状态集合** | **迁移约束** |
| --- | --- | --- |
| 任务(media_tasks) | 排队 / 生成中 / 部分失败 / 待预览 / 已勾选 / 已导出 / 已入库 / 失败 / 已取消 | 只有"待预览 / 部分失败"可重试;已导出不可回退 |
| 任务项(media_items) | 排队 / 生成中 / 待预览 / 失败 / 已跳过 | 失败原因必填(枚举,来自字典) |
| 镜头(shots) | 排队 / 关键帧完成 / 生成中 / 完成 / 失败 | 单镜替换只影响本镜 |
| 脚本(scripts) | 草稿 / 生成中 / 待审核 / 已入库 / 使用中 / 已停用 / 失败 | 已入库修改 → 新版本 + 重新审核 |
| 资产(assets) | 草稿 / 已发布 / 已停用 / 已归档 / 只读 | 被引用不可删除,只能停用 |
| 行业包(versions) | 草稿 / 校验中 / 待复核 / 已发布 / 已弃用 | 发布后不可改,只能发新版本 |
| 素材(materials) | 上传中 / 待完善 / 待送审 / 送审中 / 已过审 / 已驳回 / 投放中 / 已下线 | 驳回后重传生成新版本并回待送审;已下线不可回投放中 |
| 送审批次(review_records) | 待提交 / 送审中 / 已通过 / 已驳回 / 已撤回 | 结论绑定提交时冻结版本;重审需新建批次 |
### 3.7 领域事件与 Outbox
**问题**:任务入队与业务写入必须"要么都成功、要么都不发生",否则会出现"任务记录建了但消息没投出去"(任务永远排队)或"消息投了但任务没建"(worker 找不到任务)。
**方案**:事务内写 `outbox_events`,提交后由 dispatcher(API 内定时任务或 scheduler)投递队列,成功后标记已投递;投递失败按退避重试。
| **事件** | **生产方** | **消费方** | **用途** |
| --- | --- | --- | --- |
| `media.task.created` | media | 队列投递器 | 拆任务项入队 |
| `media.item.completed` | media | media(聚合进度) | 判断整条任务是否完成 |
| `script.generated` | script | script(校核) | 生成后自动校核 |
| `asset.ref.changed` | catalog | 检索 / 报表 | 维护引用计数缓存 |
| `ai.usage.recorded` | platform | 成本聚合 | 实时预算告警 |
| `permission.changed` | identity | 会话失效 | 在线用户下次请求权限失效(PRD 5.4.3) |
| `industry.pack.published` | industry | 表单渲染缓存失效 | 保证新 schema 立即生效 |
### 3.8 为什么是模块化单体(而不是微服务)
| **维度** | **模块化单体(本期)** | **微服务** |
| --- | --- | --- |
| 部署复杂度 | 1 个 compose 文件,3–5 个进程 | 8 个服务 + 注册中心 + 网关 + 链路治理 |
| 事务 | 本地事务可覆盖"任务 + 账本 + 资产" | 需 Saga / 补偿,复杂度翻倍 |
| 一致性 | 强一致(Outbox 只解决入队) | 最终一致,需处理中间态 |
| 团队规模 | 2–4 人研发 | 至少每服务 1–2 人 |
| 扩容粒度 | 按进程类型(API / 图片 / 视频)分别扩 | 按服务扩 |
| 拆分时机 | 单个域 QPS 或计算量独立增长 3 倍以上,或有独立团队 | — |
> 关键点:**扩容粒度由"进程类型"提供,而不是由"服务拆分"提供**。视频 worker 可以独立扩到 10 台,而 API 仍是 2 台——这已经解决了 90% 的扩容诉求。
---
## 4 部署架构(公有云,单地域)
### 4.1 环境划分
| **环境** | **用途** | **数据** | **AI 调用** | **规格** |
| --- | --- | --- | --- | --- |
| dev | 日常开发联调 | 种子数据 + 1 个测试客户 | 沙箱 / 低价模型 | 单机 4C8G |
| staging | 回归、演示、发布前验收 | 脱敏样本(不含真实客户素材) | 与生产同一路由但限额度 | 2 台 8C16G |
| prod | 生产 | 真实客户数据 | 生产路由 | 见 4.3 |
**隔离要求**:三个环境使用独立 VPC 子网、独立数据库实例、独立对象存储桶与独立密钥;生产密钥只在生产环境注入。
#### 4.1.1 本地开发环境(Docker Compose,开发前先启动)
本地依赖全部容器化,与生产同大版本。**开发前先起依赖容器;容器没起时后端/迁移会直接连不上 5432 / 6379,报连接失败而不是代码错误。**
| **依赖** | **镜像** | **端口** | **数据卷** | **启动要点** |
| --- | --- | --- | --- | --- |
| PostgreSQL 18 | `postgres:18` | 5432 | `pgdata` | `POSTGRES_DB=lingxi_dev`;初始化脚本建 `pg_trgm`、`unaccent` 等扩展;`shared_buffers` 按开发机内存给 512 MB 即可 |
| Redis 7.4 | `redis:7.4` | 6379 | `redisdata` | `appendonly yes`;仅监听 `127.0.0.1`,本地不设密码 |
| MinIO | `minio/minio` | 9000 / 9001 | `miniodata` | 控制台 9001;初始化容器用 `mc` 自动建 `assets` / `renders` / `temp` / `archive` 四桶 |
```bash
# 1. 启动依赖(在 infra/ 下执行)
docker compose -f infra/compose.dev.yaml up -d
# 2. 等健康检查通过(pg_isready / redis-cli ping / minio 健康端点)
docker compose -f infra/compose.dev.yaml ps
# 3. 首次跑迁移与种子数据
pnpm db:migrate && pnpm db:seed
```
**四条约定**:
1. **数据卷持久化,禁止 `down -v`**(会清空本地库);需要干净环境用 `pnpm db:reset`(先备份再重建)。
2. `.env.local` 示例:`DATABASE_URL=postgresql://lingxi:lingxi@127.0.0.1:5432/lingxi_dev`、`REDIS_URL=redis://127.0.0.1:6379`、`S3_ENDPOINT=http://127.0.0.1:9000`;真实密码只出现在 staging / prod 的密钥里。
3. CI 用 Testcontainers 起**同版本** PG 18 + Redis + MinIO,与本地一致,避免「本地能跑、CI 挂」。
4. 开发机 `shared_buffers` / `shm_size` / 磁盘 IOPS 都远小于生产,**性能结论不在本地下**,压测一律在 staging 跑。
### 4.2 部署拓扑
```mermaid
flowchart TB
U["内部用户浏览器"] -->|HTTPS 443| SLB["负载均衡 / 弹性公网 IP"]
SLB --> NGX["web(Nginx)"]
NGX --> API1["api-1"]
NGX --> API2["api-2"]
API1 & API2 --> PG[("PostgreSQL 18(自建)<br/>主实例 + WAL 归档")]
API1 & API2 --> PG_R[("流复制只读副本<br/>报表 / 审计检索(P2)")]
API1 & API2 --> Redis[("Redis 7.4(自建)<br/>主从 + AOF")]
API1 & API2 --> MIO[("MinIO 集群<br/>2 节点 · 纠删码")]
API1 & API2 -->|出网 NAT| AI["AI 供应商 API<br/>文本 / 图生图 / 图生视频 / TTS"]
W["worker 组(script / image / video / export)<br/>2 × 8C16G"] --> Redis
W --> PG
W --> MIO
W -->|出网 NAT| AI
MIO -.->|每日复制| OSS[("云 OSS 冷备")]
PG -.->|WAL + 全备| OSS
W & API1 & API2 -.-> OBS["Prometheus / Grafana / Loki / Sentry"]
```
### 4.3 资源规格(生产,起步配置)
| **组件** | **规格** | **数量** | **说明** |
| --- | --- | --- | --- |
| 负载均衡 / EIP | 共享型 SLB,100 Mbps 弹性带宽 | 1 | 出网与下载共用 |
| 应用节点(api + web) | 4C8G,ESSD 100 GB | 2 | 无状态,挂 SLB |
| Worker 节点 | 8C16G,ESSD 200 GB(临时盘用本地盘更佳) | 2 | 图片 / 视频 / 导出混合部署,按队列并发控制 |
| PostgreSQL 18(自建) | 4C8G,200 GB ESSD 数据盘,每日全备 + WAL 归档(PITR 7 天) | 1 主(M1 起 +1 流复制从库) | 业务库;审计表分区;**独立数据节点,不与 worker 混部** |
| Redis 7.4(自建) | 2G,主从 + AOF everysec | 1 主 1 从 | 缓存 + 队列 + 限流;独立数据节点 |
| MinIO | 4C8G + 2 × 1 TB ESSD | 2 节点 | 纠删码;`archive` 桶可转 OSS 低频 |
| OSS | 标准存储 + 低频 | 按量 | 冷备与审计归档 |
| NAT 网关 | 按量 | 1 | AI 调用出网,固定出口 IP 便于供应商白名单 |
| 监控 | 可观测套件(托管或 2C4G 自建) | 1 | Prometheus + Grafana + Loki |
**量级估算**(起步配置下的余量):
| **资源** | **日均消耗** | **余量判断** |
| --- | --- | --- |
| vCPU(图片合成) | 2000 张 × 3s ≈ 1.7 核时 | 16 核可用,余量 > 80% |
| vCPU(视频拼接) | 500 条 × 40s ≈ 5.6 核时 | 同上 |
| 出网(AI 调用下载中间产物) | 约 10–30 GB/日 | 100 Mbps 弹性足够 |
| 出网(用户下载) | 5 GB/日 | 足够 |
| 数据库 IOPS | < 500 | ESSD 起步盘(3000 IOPS)足够 |
> 明确写"起步配置"而不是"最终配置":真正的规格应在 M1 上线后用 2 周真实数据校准,避免按峰值过度采购。
### 4.4 网络与安全组
| **项** | **配置** |
| --- | --- |
| VPC | 1 个 VPC,2 个子网(应用子网 / 数据子网) |
| 安全组 | `web`:仅 80/443 入站;`api`:仅来自 web 安全组;`worker`:无入站;`db/redis/minio`:仅来自应用与 worker 安全组 |
| 出网 | NAT 网关统一出网;固定 EIP,便于 AI 供应商 IP 白名单 |
| 域名与证书 | 内部域名 + 公网域名各一;证书用云托管证书自动续期 |
| 备案 | 公网域名需 ICP 备案(QT-01) |
| 数据库 | 不开放公网;仅内网访问;开启审计日志 |
| 对象存储 | 桶私有,仅签名 URL 可读;管理台仅内网访问 |
### 4.5 容器与编排
**M0 用 Docker Compose,服务清单**:
| **服务** | **镜像** | **启动要点** |
| --- | --- | --- |
| `web` | nginx:alpine + 前端产物 | 反代 `/api` 到 `api:3000`;`/files` 直接回源签名 URL(不代理大文件) |
| `api` | node:24.15.0-bookworm-slim + 构建产物 | `prisma migrate deploy` 由独立 init 容器执行,不在 api 启动时跑 |
| `worker-script` / `worker-image` / `worker-video` / `worker-export` | 同 api 镜像,不同入口 + ffmpeg 层 | 每个容器限制 CPU/内存;`--concurrency` 由环境变量控制 |
| `scheduler` | 同 api 镜像 | Redis 分布式锁保证单实例 |
| `minio` | minio/minio + 独立数据盘 | 纠删码模式;健康检查 `/minio/health/live` |
| `postgres` | postgres:18 + 独立数据盘 | 健康检查 `pg_isready`;`shared_buffers` 取内存 25%;WAL 归档目录挂载到独立卷;**不与 worker 同机**(M0 可同机但不推荐) |
| `redis` | redis:7.4 + 数据卷 | `appendonly yes`;队列逻辑库 `maxmemory-policy noeviction`(避免淘汰任务消息);健康检查 `redis-cli ping` |
| `otel-collector` / `prometheus` / `loki` / `grafana` | 官方镜像 | 仅内网暴露 |
**运行约束**:
1. 所有容器 `restart: unless-stopped`,健康检查必须真实(API 检查 DB 与 Redis,不只看端口)。
2. worker 容器设置 `shm_size` 与临时目录挂载,避免 ffmpeg 写满磁盘。
3. 日志统一输出 stdout(JSON),由 Loki 采集;不在容器内落文件。
4. 镜像内固定 ffmpeg 与字体(中文字体必须预置,否则字幕乱码——常见坑)。
### 4.6 CI/CD
| **阶段** | **动作** | **门禁** |
| --- | --- | --- |
| PR | lint + typecheck + 单测 + 迁移文件审查 | 必须通过;迁移文件需 DBA 视角 review |
| 合并到 main | 构建镜像(tag = commit sha)+ 推镜像仓库 | — |
| 部署 staging | `compose pull && up -d` + 迁移 + 冒烟测试 | 冒烟失败自动回滚上一镜像 |
| 部署 prod | 手动触发(首次),灰度:先 api-1,再 api-2 | 观察 10 分钟错误率与 P95 |
| 回滚 | 镜像回退 + 迁移只做"可向前兼容"的变更 | 见下方迁移策略 |
**数据库迁移策略(expand / contract)**:
1. 先加字段(可空或带默认),发布读新字段的代码;
2. 回填数据(独立脚本 / 任务);
3. 确认无旧代码使用后,再删旧字段(下一个版本)。
4. **禁止**在同一个发布里"删字段 + 改代码",否则回滚即数据事故。
### 4.7 备份与容灾
| **对象** | **策略** | **RPO** | **RTO** |
| --- | --- | --- | --- |
| PostgreSQL(自建) | 每日全备 + WAL 连续归档到 MinIO(PITR 7 天)+ 每周异地快照 | ≤ 5 分钟 | ≤ 4 小时 |
| Redis(自建) | 每日 RDB 快照 + AOF 保留 7 天(队列消息可重建、缓存可丢) | ≤ 1 小时 | ≤ 1 小时 |
| MinIO(assets / renders) | 每日跨桶复制到 OSS;版本化保留 5 版 | ≤ 24 小时 | ≤ 4 小时 |
| MinIO(temp) | 不备份(可重生成) | — | — |
| 配置与密钥 | 配置存 Git(脱敏),密钥存 KMS | — | ≤ 1 小时 |
| 审计日志 | 分区表 + 月度归档到 `archive` 桶 | ≤ 1 天 | ≤ 8 小时 |
**演练**:每季度一次"从备份恢复一个客户的数据"演练,验证 RTO;演练记录进审计。
### 4.8 扩容与成本控制
| **信号** | **阈值(建议)** | **动作** |
| --- | --- | --- |
| 队列等待 P95 | > 10 分钟 | worker 水平扩 1 台 |
| API P95 | > 800ms 持续 10 分钟 | api 扩到 3 台 |
| 出网带宽 | 持续 > 70% | 提升带宽或加 CDN |
| 存储增长 | 月增 > 20% | 中间产物生命周期收紧 / 冷数据转低频 |
| 单日 AI 成本 | > 预算 80% | 告警;> 100% 自动降级到备用低价模型(QT-08) |
| 数据库 CPU | > 70% 持续 15 分钟 | 慢查询治理 → 升配 |
**成本控制三条硬规则**:
1. 任何批量任务提交前显示预估成本(PRD R-06),接口层强制;
2. 账本按"客户 / 批次 / 模型"三维聚合,成本看板每日更新;
3. 中间产物(关键帧、单镜、TTS)30 天清理,避免"跑一年存十年"。
### 4.9 安全加固清单(上线前)
- [ ] 全站 HTTPS,HSTS;管理台不暴露公网(或加 IP 白名单)
- [ ] 数据库、Redis、MinIO 均不开放公网端口
- [ ] 默认账号全部改密;超管启用二次确认
- [ ] 上传文件类型白名单 + 大小上限 + 图片重编码
- [ ] 签名 URL 过期时间 ≤ 15 分钟,且与用户会话绑定校验
- [ ] 导出、权限变更、超管授予三类操作强制二次确认并审计
- [ ] 依赖漏洞扫描(`pnpm audit` + 镜像扫描)纳入 CI
- [ ] 密钥不落库、不进日志、不进前端包
- [ ] 备份恢复演练通过
## 5 数据模型
### 5.1 建模原则
1. **客户是隔离单元**:所有业务表带 `customer_id`(`NOT NULL` + 外键);行业由客户派生,冗余 `industry_id` 便于按行业统计与索引。
2. **不可变优先**:反映"当时发生了什么"的表(任务、账本、审计、快照)只追加不修改;反映"当前状态"的表才允许 UPDATE。
3. **版本显式化**:资产、schema、脚本、提示词都有独立版本表;业务记录存 `xxx_version_id`,不存"最新版本"的隐式引用。
4. **动态值不污染主表**:要素卡的值存 `JSONB`,可检索字段物化到 `brief_card_index`,报表与筛选走物化表。
5. **枚举来自字典**:失败原因、淘汰理由、平台规格等用字典表 + 快照值(`*_snapshot`),不把中文写进代码。
6. **软删除 + 归档**:业务数据一律软删除(`deleted_at`),物理删除只走"数据保留策略"任务并留审计。
7. **审计与业务解耦**:审计写独立表(append-only),不放在业务表的 `updated_by` 字段里凑合。
8. **ID 策略**:业务主键用 ULID(有序、可读、无中心),对外暴露的 ID 可直接使用;不使用自增 ID 暴露给前端(避免枚举)。
### 5.2 核心实体关系
```mermaid
erDiagram
DEPARTMENTS ||--o{ USERS : contains
USERS ||--o{ USER_ROLES : has
ROLES ||--o{ USER_ROLES : assigned
ROLES ||--o{ ROLE_PERMISSIONS : grants
ROLES ||--o{ DATA_SCOPES : scoped
INDUSTRIES ||--o{ CUSTOMERS : contains
CUSTOMERS ||--o{ CUSTOMER_GROUPS_MEMBERS : in
CUSTOMER_GROUPS ||--o{ CUSTOMER_GROUPS_MEMBERS : includes
INDUSTRIES ||--o{ INDUSTRY_PACKS : has
INDUSTRY_PACKS ||--o{ INDUSTRY_PACK_VERSIONS : versions
INDUSTRY_PACK_VERSIONS ||--o{ FORM_SCHEMAS : declares
CUSTOMERS ||--o{ BRIEF_CARDS : owns
FORM_SCHEMAS ||--o{ BRIEF_CARDS : interprets
CUSTOMERS ||--o{ ASSETS : owns
ASSETS ||--o{ ASSET_VERSIONS : versions
ASSETS ||--o{ ASSET_REFS : referenced
CUSTOMERS ||--o{ SCRIPTS : owns
SCRIPTS ||--o{ SCRIPT_VERSIONS : versions
SCRIPT_VERSIONS ||--o{ SHOTS : contains
CUSTOMERS ||--o{ MEDIA_TASKS : owns
MEDIA_TASKS ||--o{ MEDIA_ITEMS : contains
MEDIA_ITEMS ||--o{ SHOTS : renders
MEDIA_ITEMS ||--o{ EXPORT_ITEMS : exported
EXPORTS ||--o{ EXPORT_ITEMS : bundles
MEDIA_ITEMS ||--o{ AI_USAGE : consumes
USERS ||--o{ AUDIT_LOGS : acts
```
### 5.3 表清单
| **域** | **表** | **说明** | **关键约束** |
| --- | --- | --- | --- |
| 组织权限 | `departments` | 部门 / 小组两级 | `parent_id` 最多两级 |
| 组织权限 | `users` | 内部账号 | 手机号唯一;离职停用不删除 |
| 组织权限 | `roles` | 可配置角色 | 超管 `is_system=true` 不可改 |
| 组织权限 | `permissions` | 权限点目录(代码注册 + 表同步) | `code` 唯一,如 `script:review` |
| 组织权限 | `role_permissions` | 角色 → 权限点 | 复合主键 |
| 组织权限 | `user_roles` | 用户 → 角色(可多) | 复合主键 |
| 组织权限 | `data_scopes` | 角色数据范围 | `scope_type` + 目标 ID |
| 客户与行业 | `industries` | 行业目录(业务属性) | 名称全局唯一 |
| 客户与行业 | `customers` | 客户 | `(industry_id, name)` 唯一;`short_name` 必填 |
| 客户与行业 | `customer_groups` | 客户组 | 授权与范围最小单位 |
| 客户与行业 | `customer_group_members` | 客户组 ↔ 客户 | 复合主键 |
| 客户与行业 | `customer_fields` | 客户级字段扩展与审批 | 每客户 ≤ 5 个进入生成字段 |
| 行业包 | `industry_packs` | 行业包(逻辑实体) | 与行业 1:1 或 1:N |
| 行业包 | `industry_pack_versions` | 版本与状态机 | 发布后不可变 |
| 行业包 | `form_schemas` | 要素卡 schema(JSON Schema + UI 元数据) | 版本内 key 唯一、字段 ≤ 60 |
| 行业包 | `compliance_rules` | 校核与合规规则集 | 按行业包版本 |
| 行业包 | `prompt_templates` | 提示词模板(版本、生效范围) | 引用版本号 |
| 脚本 | `brief_cards` | 要素卡实例(动态值) | 存 `form_schema_id` 快照引用 |
| 脚本 | `brief_card_index` | 可检索字段物化 | 每卡每字段一行,带类型值 |
| 脚本 | `selling_points` | 卖点库(行业级 / 客户级) | `scope` + `customer_id` 可空 |
| 脚本 | `script_templates` | 脚本模板(结构化) | 版本化 |
| 脚本 | `scripts` | 脚本实例(当前状态) | 客户归属、状态机 |
| 脚本 | `script_versions` | 脚本内容版本 | 已入库修改产生新版本 |
| 脚本 | `script_serials` | 编号取号序列(客户 + 日期) | 原子自增 + 行锁,编号唯一 |
| 脚本 | `structure_templates` | 结构模板库(人工 / AI 挖掘) | 状态机 + `usage_count` |
| 脚本 | `script_references` | 脚本 → 引用脚本 / 事实来源 | 唯一约束,用于溯源与权重 |
| 脚本 | `review_batches` | 客户审核批次 | `access_token` 唯一 + 过期时间 |
| 脚本 | `review_changes` | 客户改稿前后快照 | 保留原稿与操作人 |
| 脚本 | `handoffs` | 交付包(Word / 飞书)记录 | 关联脚本与文件 key |
| 素材 | `media_tasks` | 任务头 | 状态机见 3.6;成本预估字段 |
| 素材 | `media_items` | 任务项(1 图 / 1 视频) | 幂等键唯一 |
| 素材 | `shots` | 镜头(视频线最小单元) | 归属 `media_item_id` |
| 素材 | `render_profiles` | 编码/规格配置 | 数据化,不写死 |
| 素材 | `preview_marks` | 勾选/淘汰记录 | 含淘汰理由(字典) |
| 素材 | `templates` / `template_versions` | PSD 模板与其版本(`layer_config` 快照、预览图、规格绑定) | 解析失败不入库;历史批次按版本快照复现 |
| 素材 | `production_batches` / `production_tasks` | 图片批次与任务项(参数快照、命名快照、进度与失败明细) | 批次快照保证历史可复现;任务项支持单项重试 |
| 素材 | `materials` | 素材主对象(图片 / 视频;**规格是属性不是对象**) | 客户归属 + 状态机;与脚本多对多 |
| 素材 | `material_versions` | 素材版本(v1..vn) | 每素材至多一个采用版本(部分唯一索引) |
| 素材 | `material_specs` | 规格明细(平台 / 比例 / 分辨率 / 时长上限 / 命名后缀) | 与版本一对多 |
| 素材 | `material_files` | 对象 key、hash、大小、mime、时长 / 分辨率 / 首帧 | `hash` 唯一索引(秒传) |
| 素材 | `material_links` | 素材 ↔ 脚本(多对多) | 复合唯一;**禁止 JSON 列存脚本 ID** |
| 素材 | `upload_sessions` | 分片上传会话(断点续传) | 过期清理;与 `media_items` 无关 |
| 交付 | `exports` | 导出任务 | 状态 + 包大小 + 签名 key |
| 交付 | `export_items` | 导出项 | 关联 `media_item_id` |
| 交付 | `delivery_manifests` | 交付清单快照 | 不可变 |
| 交付 | `review_records` | 送审批次与结论(内部 / 外部共用) | 审核基准 = 提交时冻结版本 |
| 交付 | `external_submissions` | 外部平台提审登记(平台 / 时间 / 状态 / 驳回原因 / 凭证) | 非投放 API 直连 |
| 交付 | `share_links` | 分享链接(token 哈希、有效期、撤销时间、限流桶) | 只存 token 哈希;撤销即失效 |
| 效果 | `placements` | 投放登记(平台 / 账户 / 素材 / 起止) | 素材外键;手工登记为主 |
| 效果 | `material_metrics_daily` | 素材日指标(消耗 / 曝光 / 点击 / CTR / CVR / 转化 / ROI) | 按日分区;**派生指标不落库** |
| 资产 | `assets` | 15 类资产(`asset_type` 区分) | 客户归属 + 软删除 |
| 资产 | `asset_versions` | 资产版本 | 文件 key、元数据 |
| 资产 | `asset_refs` | 引用关系(谁用了谁) | 唯一约束 + 计数缓存 |
| 资产 | `asset_reviews` | 复审任务 | 到期降权 |
| 平台 | `dict_types` / `dict_items` | 数据字典 | 变更只影响新数据 |
| 平台 | `ai_providers` | 供应商(Base URL / 密钥密文 / 状态) | 密钥只存密文,出参 `hasApiKey` |
| 平台 | `ai_models` | 模型(上下文 / 最大输出 / 默认参数 JSON) | 归属供应商;参数 JSONB |
| 平台 | `ai_task_mappings` | 任务类型 → 主 / 备模型 + 任务参数 | `task_type` 唯一,主模型必填 |
| 平台 | `ai_call_logs` | 调用账本(append-only) | 按月分区,禁 UPDATE / DELETE |
| 平台 | `ai_configs` / `ai_config_versions` | 提示词 / 行业指南等键值配置与版本 | 每键版本自增,可回滚 |
| 平台 | `ai_routes` | 客户级覆盖路由(P1) | 与 `ai_task_mappings` 合并求值 |
| 平台 | `ai_usage` | 调用账本(append-only) | 按月分区 |
| 平台 | `cost_budgets` | 预算与告警阈值 | 按角色/客户/月 |
| 平台 | `audit_logs` | 审计(append-only) | 按月分区,禁 UPDATE/DELETE |
| 平台 | `system_configs` | 系统配置(命名、导出、通知) | 带版本与变更审计 |
| 平台 | `outbox_events` | 事务性事件投递 | 已投递标记 |
| 平台 | `idempotency_keys` | 请求幂等 | TTL 24 小时 |
> 说明:脚本批量生成**复用** `media_tasks` / `media_items`(`type=script`),与素材共用任务协议、SSE 进度与成本账本,因此不单独建「脚本任务表」(对应 6.3.4 的 `/script-tasks` 接口)。
### 5.4 关键表字段设计
#### 5.4.1 `customers`(客户,隔离单元)
| **字段** | **类型** | **说明** |
| --- | --- | --- |
| `id` | ulid PK | — |
| `industry_id` | ulid FK → industries | 不可为空;跨行业移动禁止(应用层 + 触发器可选) |
| `name` | text | 同行业内唯一 |
| `short_name` | text | **必填**,用于自动命名,需满足通用字符规则 |
| `status` | enum | 在服务 / 暂停 / 结束 |
| `industry_pack_version_id` | ulid FK | 绑定的行业包版本(可被客户覆盖层覆盖) |
| `owner_user_id` | ulid FK → users | 客户负责人 |
| `preferences` | jsonb | 命名习惯、目录结构、常用规格、素材禁忌 |
| `custom_fields` | jsonb | 基础扩展字段(≤ 10,仅检索/报表) |
| `created_at` / `updated_at` / `deleted_at` | timestamptz | 软删除 |
索引:`(industry_id, name)` 唯一;`(owner_user_id)`;`(status)`。
#### 5.4.2 `brief_cards` + `brief_card_index`(动态表单)
| **表** | **字段** | **说明** |
| --- | --- | --- |
| `brief_cards` | `id` / `customer_id` / `form_schema_id` / `schema_version` / `values jsonb` / `status` | `values` 只存字段 key → 值;**不存字段中文名**(中文来自 schema 快照) |
| `brief_card_index` | `card_id` / `field_key` / `value_text` / `value_num` / `value_date` | 只有 schema 声明"可检索"的字段才写入;用于筛选与报表 |
**为什么不用「每行业一张表」**:行业包要能在 5 个工作日内上线(PRD 5.4.7),每接行业就改表结构意味着发版 + 迁移;JSONB + 物化索引让"加字段"退化为数据操作。
**如何保证查询性能**:筛选高频字段(平台、状态、时间、客户)走固定列;要素卡的可检索字段(如价格区间、品类)走 `brief_card_index` 的复合索引 `(customer_id, field_key, value_num)`。
#### 5.4.3 `media_tasks` / `media_items` / `shots`(异步任务三层)
| **表** | **关键字段** | **说明** |
| --- | --- | --- |
| `media_tasks` | `id` / `customer_id` / `type`(image / video / script)/ `status` / `params jsonb` / `estimate_cost` / `actual_cost` / `created_by` | 任务头;存提交时的参数快照 |
| `media_items` | `id` / `task_id` / `seq` / `idempotency_key` / `status` / `asset_refs jsonb` / `result_asset_id` / `checks jsonb` / `error_code` | 幂等键唯一;`checks` 存校验结果 |
| `shots` | `id` / `media_item_id` / `seq` / `script_version_id` / `prompt_version_id` / `keyframe_key` / `clip_key` / `audio_key` / `status` | 单镜可替换、可重试;对象 key 直接落库,回收站可定位孤立对象 |
**任务项状态与产物分离**:`media_items` 只记状态与结果引用,产物文件在对象存储;这样"重跑失败项"只改状态,不复制数据。
#### 5.4.4 `assets` + `asset_versions` + `asset_refs`
| **字段** | **说明** |
| --- | --- |
| `assets.asset_type` | 15 类枚举:industry_pack / script_template / video_direction / script / image_template / logo / product_position / warning_text / persona_pool / scene_pool / lighting_preset / bgm / prompt_template / brief_card_template / render |
| `assets.scope` | 行业级 / 客户级 / 批次级(PRD 5.3.2 三级) |
| `assets.version_id` | 指向当前启用的版本 |
| `asset_versions.file_key` | 对象存储 key |
| `asset_versions.meta` | 尺寸、时长、图层信息、授权到期日等 |
| `asset_refs` | `(source_asset_version_id, target_type, target_id)`;删除前检查 + 计数缓存 |
**删除保护实现**:`asset_refs` 有引用时,删除接口返回 409 + 引用清单(PRD R-02);同时数据库外键 `RESTRICT` 兜底。
#### 5.4.5 `audit_logs`(append-only)
| **字段** | **类型** | **说明** |
| --- | --- | --- |
| `id` | ulid | — |
| `occurred_at` | timestamptz | 分区键(按月) |
| `actor_user_id` | ulid | 操作人(系统操作为空) |
| `action` | text | 如 `export.create`、`role.update`、`industry_pack.publish` |
| `target_type` / `target_id` | text / ulid | 对象 |
| `customer_id` | ulid | 涉及客户(可空) |
| `before` / `after` | jsonb | 前后值(敏感字段脱敏) |
| `ip` / `user_agent` / `trace_id` | text | 追溯 |
| `result` | enum | success / denied |
**约束**:应用账号只有 INSERT;`REVOKE UPDATE, DELETE`;查询走只读副本(P2);月度分区 + `archive` 桶归档。
### 5.5 客户数据隔离的三层实现
| **层** | **机制** | **兜底能力** |
| --- | --- | --- |
| 库层 | 所有业务表 `customer_id NOT NULL` + 外键;跨表查询必须经客户路径 | 防止"忘记过滤"导致的写入污染 |
| 应用层 | `ScopeContext`(用户可见客户集合)由守卫解析,注入 Repository;**Repository 方法签名强制携带** | 编译期强制,评审可检查 |
| 数据层 | P1 开启 PG Row Level Security:`current_setting('app.customer_ids')` 与行 `customer_id` 匹配 | 防止绕过应用的直连查询(BI / 运维) |
**ScopeContext 解析顺序**(取交集):
1. 用户是否有效、角色是否停用(`permission.changed` 事件使会话失效);
2. 角色数据范围 → 客户集合(全部 / 本部门及下级 / 本部门 / 指定客户组 / 负责客户 / 本人创建);
3. 请求显式指定的客户(如 `/customers/{id}/...`)必须落在集合内,否则 403;
4. 多角色取并集,再与"显式指定"取交集。
### 5.6 版本与快照策略
| **对象** | **版本化对象** | **业务记录保存什么** | **历史如何解释** |
| --- | --- | --- | --- |
| 行业包 | `industry_pack_versions` | 版本 ID | 按当时版本渲染与校核(PRD R-11) |
| 要素卡 schema | `form_schemas` | `form_schema_id` | 字段弃用仍可解释历史值 |
| 脚本 | `script_versions` | `script_version_id` | 审核记录绑定版本 |
| 资产 | `asset_versions` | `asset_version_id` | 被引用时记录版本号 |
| 提示词 | `ai_prompts` | `prompt_version_id` | 可复现"这条素材用了哪版提示词" |
| 字典 | `dict_items` | `*_snapshot` 文本 | 历史统计不失真(PRD R-10) |
| 交付清单 | `delivery_manifests` | 快照 JSON | 导出当时的完整信息 |
### 5.7 分区、索引与查询约定
| **表** | **分区键** | **保留** | **说明** |
| --- | --- | --- | --- |
| `audit_logs` | 月 | ≥ 12 个月(热 3 个月) | 超期归档到对象存储,可检索索引保留 |
| `ai_usage` | 月 | ≥ 24 个月 | 成本趋势与对账 |
| `media_items` | 月(按 `created_at`) | 热 6 个月 | 历史明细归档 |
| `outbox_events` | 无 | 7 天 | 已投递即清理 |
**查询约定**:
1. 列表一律游标分页(`(created_at, id)`),不用 `OFFSET` 深翻页;
2. 计数类走缓存或近似值,不做实时 `COUNT(*)` 大表;
3. 报表查询走只读副本(P2)或预聚合表(`cost_daily` / `output_daily`);
4. 禁止 `SELECT *` 返回大 JSONB 字段到列表接口(列表只返回摘要);
5. 脚本检索用全文索引(`pg_trgm` GIN 或分词插件)+ 结构化条件(行业 / 客户 / 方向 / 卖点);**M1–M2 不引入向量检索**,M3 起在同一个 PG 18 内启用 `pgvector`(HNSW、维度 1024、cosine),语义块存 `script_chunks`,向量访问全部收敛到 `VectorStore` 接口(`upsert` / `search` / `delete` / `hybridSearch`);迁移 Qdrant 的触发条件:向量 > 1000 万、P95 > 200ms 且调优无效、需要多向量字段 + 复杂过滤 + > 200 QPS。参考脚本检索结果缓存 5 分钟(键 = 条件哈希)。
6. 生效阈值只从 `system_configs` 读取(爆量消耗阈值、审核超时等),前端上限、DTO 校验、任务判定共用同一 getter,禁止在切片内写常量;派生指标(汇总 / 均值 / 占比)查询时计算,不落库。
### 5.8 数据保留与删除
| **场景** | **策略** |
| --- | --- |
| 客户暂停 | 全部只读;不能发起新生成(PRD 5.4.2) |
| 客户结束 | 只读归档;资产转"只读资产";数据保留至合同约定到期 |
| 用户离职 | 停用账号,名下客户转交(PRD 5.4.1) |
| 资产删除 | 未引用可删(软删 + 30 天后物理清理);被引用只能停用 |
| 任务与产物 | 成片永久(随资产);中间产物 30 天;失败任务记录保留 90 天 |
| 审计 | ≥ 12 个月,归档后仍可检索 |
| 数据主体删除请求 | 按 QT-11 流程执行,删除前导出审计留痕 |
---
## 6 接口设计
### 6.1 风格与约定
| **约定** | **规则** |
| --- | --- |
| 风格 | REST,路径 `/api/v1/...`;资源名复数;动作用子资源或 `:action` |
| 版本 | URL 版本号;破坏性变更升 v2,不做 header 协商 |
| 认证 | `Authorization: Bearer <access_token>`;刷新走 `/auth/refresh` |
| 幂等 | 写接口支持 `Idempotency-Key` 头(24 小时去重);任务提交必带 |
| 分页 | `?cursor=xxx&limit=50`;返回 `next_cursor` |
| 排序 | `?sort=-created_at`(前缀 `-` 降序) |
| 过滤 | 显式白名单参数,禁止把 JSON 直接拼进查询 |
| 错误 | 统一结构 `{ code, message, details, trace_id }`;HTTP 状态语义化 |
| 时间 | 全部 ISO 8601 + 时区;存储 UTC |
| 大字段 | 列表不返回大文本 / 大 JSONB;详情接口按需 `?fields=` |
**错误码分段**:
| **段** | **含义** | **示例** |
| --- | --- | --- |
| 400xx | 参数与校验 | 40001 字段校验失败、40002 schema 版本不匹配 |
| 401xx | 认证 | 40101 token 过期、40102 账号停用 |
| 403xx | 授权 | 40301 无权限点、40302 数据范围外、40303 特殊权限缺失、40304 职责互斥冲突 |
| 404xx | 不存在 | 40401 资源不存在或不可见(统一措辞,避免探测) |
| 409xx | 冲突 | 40901 资产被引用、40902 名称重复、40903 状态不允许 |
| 422xx | 业务规则 | 42201 必填字段缺失、42202 超出批量上限、42203 预算不足 |
| 429xx | 限流 | 42901 供应商限流、42902 平台并发上限 |
| 500xx | 系统 | 50001 内部错误、50002 外部模型不可用 |
### 6.2 鉴权与权限校验链
```mermaid
sequenceDiagram
participant C as 前端
participant A as API
participant S as ScopeService
participant R as Repository
C->>A: 请求 + Bearer token
A->>A: 1. JWT 校验(签名/过期/吊销)
A->>A: 2. 权限点校验 @RequirePermission('script:review')
A->>A: 3. 特殊权限校验 @RequireSpecial('export')
A->>S: 4. 解析 ScopeContext
S-->>A: 可见客户集合 + 数据范围
A->>A: 5. 业务校验(状态机 / 数量上限 / 预算)
A->>R: 6. 查询(携带 ScopeContext)
R-->>A: 数据(自动过滤)
A-->>A: 7. 审计埋点(变更类)
A-->>C: 响应
```
| **守卫** | **职责** | **失败返回** |
| --- | --- | --- |
| `JwtGuard` | 验签、过期、`jti` 吊销 | 40101 / 40102 |
| `PermissionGuard` | 权限点(按角色聚合) | 40301 |
| `SpecialPermissionGuard` | 导出 / AI 配置 / 审计 / 发布等 | 40303 |
| `ScopeGuard` | 解析客户集合,注入请求上下文 | 40302 |
| `SoDGuard`(配置类接口) | 保存角色 / 发布行业包时互斥校验 | 40304 |
| `RateLimitGuard` | 每用户 / 每客户并发与频次 | 42902 |
### 6.3 核心端点清单
> 只列主链路端点;完整清单在 OpenAPI 中生成。权限点列表示"最低要求"。
#### 6.3.1 认证与账号(`identity`)
| **方法** | **路径** | **权限点** | **说明** |
| --- | --- | --- | --- |
| POST | `/auth/login` | — | 账号密码 / SSO 回调 |
| POST | `/auth/refresh` | — | 刷新 access(旋转 refresh) |
| POST | `/auth/logout` | — | 吊销会话 |
| GET | `/me` | — | 当前用户、角色、权限点、可见客户范围 |
| GET/POST/PATCH | `/users`、`/users/{id}` | `user:view` / `user:manage` | 用户管理;离职停用 |
| GET/POST/PATCH | `/roles`、`/roles/{id}` | `role:view` / `role:manage` | 角色配置;保存时 SoD 校验 |
| GET | `/permissions` | `role:view` | 权限点目录(前端渲染勾选树) |
| POST | `/users/{id}/transfer-customers` | `customer:transfer` | 客户转交 |
#### 6.3.2 客户与行业(`client`)
| **方法** | **路径** | **权限点** | **说明** |
| --- | --- | --- | --- |
| GET/POST/PATCH | `/industries`、`/industries/{id}` | `industry_pack:view` / `customer:manage` | 行业目录;删除前校验 |
| GET/POST/PATCH | `/customers`、`/customers/{id}` | `customer:view` / `customer:manage` | 客户;`short_name` 必填 |
| GET/POST | `/customer-groups`、`/customer-groups/{id}/members` | `customer_group:manage` | 客户组与成员 |
| POST | `/customers/{id}/apply-industry-pack` | `customer:manage` | 套用行业资产包(冷启动) |
| POST/POST | `/customers/{id}/fields`、`/customer-fields/{id}/approve` | `customer_field:apply` / `customer_field:approve` | 客户级字段扩展与审批 |
#### 6.3.3 行业包与表单(`industry`)
| **方法** | **路径** | **权限点** | **说明** |
| --- | --- | --- | --- |
| GET/POST | `/industry-packs`、`/industry-packs/{id}/versions` | `industry_pack:view` / `industry_pack:edit` | 行业包与版本 |
| GET/PUT | `/industry-packs/{id}/versions/{ver}/schema` | `industry_pack:edit` | 保存草稿 schema |
| POST | `/industry-packs/{id}/versions/{ver}/validate` | `industry_pack:edit` | 自动校验(变量/联动/上限) |
| POST | `/industry-packs/{id}/versions/{ver}/review` | `industry_pack:review` | 复核 |
| POST | `/industry-packs/{id}/versions/{ver}/publish` | `industry_pack:publish` | 发布(特殊权限) |
| GET | `/industry-packs/{id}/versions/{ver}/export` | `industry_pack:view` | 导出 JSON |
| GET | `/form-schemas/{id}` | `customer:view` | 前端渲染要素卡表单(按客户解析到实际版本) |
#### 6.3.4 脚本(`script`)
| **方法** | **路径** | **权限点** | **说明** |
| --- | --- | --- | --- |
| GET/POST/PATCH | `/brief-cards`、`/brief-cards/{id}` | `brief_card:view` / `brief_card:edit` | 要素卡(动态字段) |
| GET/POST | `/selling-points` | `selling_point:view` / `selling_point:edit` | 卖点库 |
| GET/POST/PATCH | `/script-templates`、`/script-templates/{id}` | `script_template:view` / `script_template:edit` | 脚本模板与版本 |
| POST | `/script-tasks` | `script:generate` | 提交批量生成(返回 taskId + 预估成本) |
| GET | `/script-tasks/{id}` | `script:view` | 任务状态与逐条结果 |
| POST | `/scripts/{id}/approve`、`/scripts/{id}/reject` | `script:review` | 审核;批量走 `/scripts:batch-review` |
| POST | `/scripts/{id}/regenerate` | `script:generate` | 局部重写 / 整条重生成 |
| GET | `/scripts` | `script:view` | 检索(含标签、引用) |
| POST | `/scripts/{id}/assign-code` | `script:review` | 过审时分配脚本编号(幂等,重复调用返回同号) |
| GET | `/scripts/{id}/versions/{v}` | `script:view` | 版本内容与 diff |
| POST | `/structure-templates`、`/structure-templates/{id}/publish` | `script_template:edit` | 结构模板库;AI 挖掘产物发布 |
| POST | `/scripts/{id}/handoff` | `script:export` | 生成 Word 交付包(异步,返回文件 key) |
| POST | `/review-batches` | `script:review` | 生成客户审核链接(返回 token 与二维码内容) |
| GET | `/public/review/{token}` | 无需登录 | 客户审核页数据(校验签名与有效期) |
| PATCH | `/public/review/{token}/scripts/{id}` | 无需登录 | 客户改稿,写 `review_changes` 快照 |
> `/public/review/*` 是唯一的无 JWT 路由组:不经过用户守卫,但必须校验 HMAC 签名、有效期与范围,并单独限流(IP + token 双维度);链接可随时作废,作废后立即 410。
#### 6.3.5 素材(`media`)
| **方法** | **路径** | **权限点** | **说明** |
| --- | --- | --- | --- |
| POST | `/media-tasks` | `media:generate` | 提交图片 / 视频任务(返回 taskId、预估成本与条数) |
| GET | `/media-tasks`、`/media-tasks/{id}` | `media:view` | 任务列表 / 详情(含逐项状态) |
| GET | `/media-tasks/{id}/events` | `media:view` | SSE 进度流(也支持轮询) |
| POST | `/media-tasks/{id}/cancel` | `media:generate` | 取消 |
| POST | `/media-tasks/{id}/retry` | `media:generate` | 仅重试失败项 |
| GET | `/media-items/{id}/preview` | `media:view` | 签名 URL + 缩略图 + Range |
| POST | `/media-items/{id}/mark` | `media:review` | 勾选 / 淘汰(含理由、快捷键批量) |
| POST | `/media-items/{id}/shots/{seq}/regenerate` | `media:generate` | 单镜替换 |
| POST | `/exports` | `export:create` | 提交导出(打包任务) |
| POST | `/uploads/presign` | `material:upload` | 预签名直传;入参 hash,命中则返回秒传结果 |
| POST | `/uploads/{uploadId}/complete` | `material:upload` | 校验分片(headObject)→ 写 `material_files` → 入 `media:post` 队列 |
| GET/POST | `/materials` | `material:view` / `material:edit` | 素材列表(游标分页)/ 登记外部成片 |
| POST | `/materials/{id}/versions` | `material:edit` | 重传新版本 / 标记采用版本 |
| POST | `/materials/{id}/links` | `material:edit` | 关联 / 解除关联脚本(多对多) |
| POST | `/materials/{id}/reviews` | `material:review` | 提交送审批次(内部审核) |
| POST | `/external-submissions` | `material:review` | 登记外部平台提审结果 |
| POST | `/share-links` | `material:share` | 签发分享链接(返回一次性 URL) |
| DELETE | `/share-links/{id}` | `material:share` | 撤销(立即失效) |
| GET | `/public/share/{token}` | 无(签名校验) | 免登录预览;独立前缀 + IP/token 限流 + 访问审计 |
| POST | `/placements` | `metric:import` | 投放登记 |
| POST | `/metrics/import` | `metric:import` | 日指标导入(手工 / 平台导出文件) |
#### 6.3.6 资产与交付(`catalog` / `delivery`)
| **方法** | **路径** | **权限点** | **说明** |
| --- | --- | --- | --- |
| GET/POST/PATCH | `/assets`、`/assets/{id}` | `asset:view` / `asset:edit` | 15 类资产统一入口(`?type=`) |
| POST | `/assets/{id}/versions` | `asset:edit` | 新版本 |
| DELETE | `/assets/{id}` | `asset:delete` | 未引用才可删;返回引用清单 |
| GET | `/assets/{id}/refs` | `asset:view` | 引用关系(双向) |
| GET | `/exports/{id}` | `export:create` | 导出进度与清单 |
| GET | `/exports/{id}/download` | `export:create` | 302 到签名 URL(zip 流) |
| GET | `/delivery-manifests/{id}` | `export:create` | 交付清单 |
#### 6.3.7 平台管理(`platform`)
| **方法** | **路径** | **权限点** | **说明** |
| --- | --- | --- | --- |
| GET/POST/PATCH | `/ai/providers`、`/ai/routes`、`/ai/prompts` | `ai:view` / `ai:configure` | 模型、路由、提示词 |
| GET/PUT | `/ai/budgets` | `ai:configure` | 额度与预算 |
| GET | `/ai/usage` | `ai:view` | 账本查询(按客户 / 模型 / 批次) |
| GET/POST/PATCH | `/dicts`、`/dicts/{type}/items` | `dict:view` / `dict:edit` | 字典 |
| GET | `/audit-logs` | `audit:view` | 审计检索(只读) |
| POST | `/audit-logs/export` | `audit:export` | 审计导出(自身也审计) |
| GET/PUT | `/system/configs` | `system:configure` | 命名规则、导出设置、通知 |
| GET | `/dashboards/output`、`/cost`、`/quality`、`/asset` | 对应看板权限点 | 看板数据(预聚合) |
### 6.4 异步任务协议(所有耗时操作统一)
| **环节** | **约定** |
| --- | --- |
| 提交 | `POST /xxx-tasks`,带 `Idempotency-Key`;响应 `{ taskId, estimatedItems, estimatedCost, estimatedSeconds }` |
| 进度 | 优先 SSE:`GET /xxx-tasks/{id}/events`(`text/event-stream`,事件类型 `item` / `progress` / `done` / `error`);降级为轮询 `GET /xxx-tasks/{id}` |
| 结果 | 逐项状态在任务详情返回;产物通过签名 URL 获取,**不把二进制塞进 JSON** |
| 取消 | `POST .../cancel`:任务头置取消,已投递消息在 worker 侧快速返回 |
| 重试 | `POST .../retry`:只重跑失败项;幂等键保证不重复计费 |
| 断线恢复 | 前端记录 `lastEventId`,重连后从断点续推(SSE `Last-Event-ID`) |
**为什么用 SSE 而不是 WebSocket**:进度是单向推送,SSE 基于 HTTP/2 更简单、可穿代理、断线重连语义原生;WebSocket 只在需要双向(如实时协同)时才引入。
### 6.5 文件上传与下载
**上传(预签名直传)**:
1. 前端 `POST /assets/upload-intents`,带文件名、大小、MIME → 后端校验类型/大小/配额,返回预签名 PUT URL 与对象 key;
2. 前端直传对象存储(不经过 API,避免大文件占内存);
3. 前端 `POST /assets` 提交对象 key 与元数据 → 后端 HEAD 校验(大小、类型、是否存在)→ 入队 `media:verify`(图片重编码 / 视频探测 / 病毒扫描可选)→ 资产进入"草稿"。
**下载**:
1. `GET /exports/{id}/download` 校验权限与状态 → 302 到 15 分钟签名 URL;
2. 大包走导出任务生成,不在请求内同步打包;
3. 支持 Range(断点续传),`Content-Disposition` 带文件名(按命名规则生成后的名称)。
### 6.6 AI 回调与幂等
| **项** | **约定** |
| --- | --- |
| 回调地址 | `/api/v1/callbacks/ai/{provider}`(无用户态,验签而非 JWT) |
| 验签 | 供应商签名头 / 共享密钥 HMAC;失败记 401 并审计 |
| 幂等 | 以 `provider_job_id` 为唯一键更新任务项;重复回调直接 200 |
| 竞态 | 回调与轮询可能同时到达:状态迁移用条件更新(`WHERE status IN (...)`),只允许前进 |
| 超时 | 超过配置时长未回调 → 轮询兜底;再超时 → 标记失败并允许重试 |
### 6.7 契约与测试
| **项** | **做法** |
| --- | --- |
| 单一真源 | DTO 用 Zod 定义,Nest 生成 OpenAPI;前端由 OpenAPI 生成类型与请求客户端 |
| 契约测试 | CI 中对 OpenAPI 做 breaking change 检测(`oasdiff`),破坏性变更必须升版本 |
| 权限测试 | 每个端点至少 3 条用例:有权限点 / 无权限点 / 数据范围外 |
| 任务链路测试 | 用 Mock AI Provider(固定延迟与固定产物)跑完整链路,保证 CI 不依赖外网 |
| 性能基线 | 列表接口 P95 < 800ms(生产数据量 10% 的样本库) |
## 7 关键链路设计
> 每个链路给"时序 + 关键实现 + 失败处理"。评审时重点看:事务边界在哪、幂等靠什么、失败后数据是否可解释。
### 7.1 登录与权限上下文
```mermaid
sequenceDiagram
participant C as 前端
participant A as API
participant I as identity 模块
participant R as Redis
C->>A: POST /auth/login(账号密码 / SSO code)
A->>I: 校验凭据与账号状态
I-->>A: 用户 + 角色 + 权限点并集
A->>R: 缓存权限上下文(key=userId,TTL 5min)
A-->>C: access(15min) + refresh(7d, HttpOnly Cookie)
C->>A: GET /me
A->>I: 解析数据范围(6 类 scope_type)
I-->>A: 可见客户集合(含客户组展开、部门子树)
A-->>C: 权限点 + 可见客户 + 特殊权限
```
| **要点** | **实现** |
| --- | --- |
| 权限点聚合 | 用户 → 角色(可多)→ 权限点并集;角色变更发 `permission.changed`,缓存按事件失效 |
| 数据范围解析 | `data_scopes(scope_type, target_id)` 六类:全部客户 / 本部门 / 本部门及下级 / 指定客户组 / 负责客户 / 本人创建;解析结果写入请求上下文与 `ScopeContext` |
| 可见客户集合上限 | 单用户可见客户 > 500 时,范围片段改为"客户组 + 例外清单"表达,避免 `IN (...)` 过长 |
| 会话失效 | access 短、refresh 旋转;停用账号即时在 Redis 置吊销标记,请求侧一次判定 |
| SSO | 预留 OIDC 适配器(企业微信 / 飞书 / 钉钉),登录成功后仍落内部账号,权限体系不变 |
**失败处理**:数据范围解析失败一律 403(**不降级为"全部客户"**)——这是隔离要求,宁可不可用也不越权。
### 7.2 脚本批量生成与入库
```mermaid
sequenceDiagram
participant C as 前端
participant A as API
participant Q as script:generate
participant W as Worker
participant M as AI 适配层
participant D as PostgreSQL
C->>A: POST /script-tasks(模板 + 要素卡 + 数量 / 矩阵 + Idempotency-Key)
A->>A: 校验要素卡必填完整、变量可解析、预算
A->>D: 建任务头 + N 个任务项(状态=排队)+ outbox
A-->>C: taskId + 预估条数 / 成本 / 时长
Q->>W: 消费者取任务项
W->>D: 组装提示词(模板版本 + 要素卡 + 卖点 + 行业包规则)
W->>M: 文本生成(按用途路由)
M-->>W: 结构化结果(钩子 / 分镜 / 口播 / 字幕 / 时长)
W->>W: 质量校核 7 项(与要素卡一致 / 时长 / 敏感词 / 结构完整 / 卖点覆盖 / 平台适配 / 重复度)
W->>D: 写 scripts(草稿)+ script_versions + 校核结果
W-->>C: SSE 逐条推送(含校核未通过项)
```
| **要点** | **实现** |
| --- | --- |
| 任务表复用 | 脚本任务复用 `media_tasks`(`type=script`),与素材共用任务协议、SSE 与成本账,避免两套异步机制 |
| 结果解析 | 要求模型输出 JSON(结构化字段),解析失败先重试 1 次,再失败则落"原文 + 解析失败"标记,交人工处理,不静默丢弃 |
| 校核 7 项 | 分为"阻断项"(必填缺失、时长超限、敏感词)与"提示项"(卖点覆盖不足、重复度高);阻断项自动重生成一次 |
| 变量完整性 | 生成前校验 R-14:要素卡可解析字段集合必须覆盖模板变量,缺一个即拒绝提交(避免生成一半才发现) |
| 入库 | 审核通过 → 写"已入库"版本;后续修改产生新版本,旧版本仍被成片引用 |
| 重复度 | 生成后两两相似度(向量或 n-gram)超阈值即标提示,避免"20 条几乎一样" |
| 生成上下文 | 三段拼装:系统提示词(JSON 契约 + 行业指南)→ 生成条件逐行 → 参考脚本(RAG);行业指南按「精确行业 → 二级回退一级 → default」匹配 |
| 参考检索 | 三级降级:全文索引 → 多维筛选打分 → 同客户近期过审兜底;每条截断 1500 字、最多 10 条、缓存 300s;结果编号供模型回填引用 |
| 候选决策 | `submit`:选中项转「待审核」、其余丢弃;`stash`:全部转草稿;两种都要写操作留痕 |
| 编号分配 | 审核通过事务内按(客户 + 日期)原子取号,编号回填脚本与交付清单;重复调用幂等 |
| 结构化解析 | JSON schema 校验 8 个字段;失败重试 1 次 → 落原文 + 标记「待人工处理」 |
### 7.3 图片批量合成(PSD 套版 + 抠图,确定性链路)
> 图片线**不是 AI 生成**:模板由设计师用 PS 做(分层 + 命名约定),平台只做「解析图层 → 抠图 → 落位合成 → 编码 → 打包」。这一判断见 8.11 D-11。
```mermaid
sequenceDiagram
participant D as 设计师
participant A as API
participant S as 对象存储
participant Q1 as image:matting
participant Q2 as image:render
participant Q3 as delivery:zip
participant DB as PostgreSQL
D->>A: 上传 PSD 模板
A->>A: ag-psd 解析图层(命名约定校验)
A->>S: 存 PSD 原文件 + 预览图
A->>DB: 写 template_versions(layer_config 快照)
Note over A: 无命名图层 / 解析失败 → 拒绝入库
A->>DB: 建批次(参数快照 + 命名快照)+ N 个任务项
A->>Q1: 投递抠图项
Q1->>S: 拉商品图
Q1->>Q1: ONNX 推理(CPU)
Q1-->>DB: 写 matting 结果(成功 / 降级 low_quality)
Q1->>Q2: 落位合成
Q2->>S: 拉模板版本 + 抠图结果 + Logo / 警示语
Q2->>Q2: canvas 按图层落位 → sharp 编码
Q2->>S: 上传 renders/{customer}/{batch}/xxx.jpg
Q2-->>DB: 更新任务项 + checks
A->>Q3: 打包导出
Q3->>S: 流式 zip + 交付清单
```
**图层命名约定**(模板能否入库的硬条件):
| **图层名前缀** | **语义** | **渲染行为** | **校验** |
| --- | --- | --- | --- |
| `image_replacement_` | 商品图位 | 抠图结果按位等比缩放 / 裁切落位 | 必须能找到边界与锚点 |
| `clipping_mask_` | 剪贴蒙版容器 | 上层图片被裁进形状(圆 / 圆角矩形 / 文字轮廓) | 必须与上层成对 |
| `text_replacement_` | 文字位 | 字段值按 PSD 样式绘制;字体从字体库解析 | 字体必须已注册,否则拒绝(不允许回退到 DejaVu) |
| **要点** | **实现** |
| --- | --- |
| 解析与入库一体 | `ag-psd` 读 PSD → 登记可替换层 → 存版本快照;解析失败 / 无命名图层 → 模板不入库(对应 PRD R-02 的图层语义风险,这里直接在校验层堵住) |
| 抠图 | `@imgly/background-removal-node` 本地 ONNX(CPU),图片不出内网;按内容 hash 缓存,同图多点复用;失败退原图并写 `low_quality=true` |
| 合成引擎 | `@napi-rs/canvas`(ag-psd 的 canvas 注入)+ `sharp` 编码;PNG 体积用 `colours` 控制(`quality` 对 PNG 无效),JPG 用 `quality` |
| 字体 | 镜像内置中文字体并注册通用别名(sans-serif / Arial);启动自检缺字体即失败退出,避免「豆腐块静默交付」 |
| 规格矩阵 | 由模板画布尺寸 + 目标规格决定缩放策略;主体区禁止非等比拉伸,越界进 checks |
| 并发 | 队列并发默认 2,按「实测单任务内存 + 可用核数」取小值;超并发只会 OOM 与抢占,没有吞吐收益 |
| 幂等 | 任务项按(模板版本 + 商品 hash + 规格 + 参数快照)幂等;对象按内容 hash 判重 |
| 临时文件 | 临时 PNG 路径必须带随机后缀——毫秒时间戳会在并发下撞名,导致「删除别人的临时文件 → 静默降级」 |
| 失败语义 | 重试 3 次仍失败才判失败;中间轮次不写失败事件(避免用户看到抖动);最终失败保留错误码与原因 |
### 7.4 视频逐镜生成与拼接
```mermaid
sequenceDiagram
participant Q as 队列
participant W as Worker
participant M as AI 适配层
participant F as ffmpeg
participant S as 对象存储
Q->>W: video:keyframe(每镜)
W->>M: 图生图(参考物料 + 人物 + 场景 + 光影)
M-->>W: 关键帧 URL
W->>S: 转存关键帧
Q->>W: video:shot(每镜)
W->>M: 图生视频(提交 → 轮询 / webhook)
M-->>W: 单镜视频 URL
W->>S: 转存单镜
Q->>W: video:compose(整条)
W->>S: 拉取 N 个镜头 + BGM + Logo + 警示语
W->>F: 拼接 + 字幕对齐 + 混音 + 叠加 + AI 标识
F-->>W: 成片 mp4
W->>S: 上传 renders + 生成缩略图与首帧
Q->>W: media:verify
W-->>S: 写 checks(时长 / 比例 / 黑帧 / 静音 / 标识)
```
| **要点** | **实现** |
| --- | --- |
| 镜头级幂等 | 幂等键 = `taskItemId:shotSeq:modelVersion`;单镜替换只生成新键,不影响其他镜头 |
| 轮询退避 | 1s → 2s → 4s → 8s(上限 15s),最长等待按供应商配置;超时先查一次状态再判失败 |
| 拼接参数 | 统一 H.264 / yuv420p / 30fps / 1080×1920 / faststart;音量归一化到 -16 LUFS,避免不同镜响度跳变 |
| 字幕 | 按脚本字幕与 TTS 时间戳对齐;无时间戳时按字数均分并允许人工微调(P1) |
| AI 标识 | 画面角标(可配置位置与透明度)+ MP4 元数据 `comment` 字段;`media:verify` 检查存在性 |
| 失败分类 | 内容违规(不重试,人工改提示词)、限流/超时(重试)、供应商 5xx(换路由)、素材问题(回退到编辑) |
### 7.5 预览、勾选、命名与导出
```mermaid
sequenceDiagram
participant C as 前端
participant A as API
participant Q as export:package
participant W as Worker
participant S as 对象存储
C->>A: GET /media-tasks/{id}(轮询或 SSE done)
C->>A: GET /media-items/{id}/preview
A-->>C: 签名 URL(15min)+ 缩略图
C->>A: POST /media-items/{id}/mark(勾选 / 淘汰 + 理由)
C->>A: POST /exports(勾选项 + 命名规则 + 目录结构)
A->>Q: 投递打包任务(清单快照)
Q->>W: 逐个复制 / 重命名 / 校验
W->>S: 写 zip 或按目录打包
W->>A: 写交付清单 + 归档成片资产
C->>A: GET /exports/{id}/download
A-->>C: 302 签名 URL(支持 Range / 断点续传)
```
| **要点** | **实现** |
| --- | --- |
| 勾选即状态 | `preview_marks` 记录勾选 / 淘汰与理由;前端本地缓存勾选状态,刷新后以服务端为准(PRD 9.1 恢复要求) |
| 命名 | 统一命名服务:占位符解析 → 字符白名单校验 → 冲突追加序号;命名规则进 `system_configs`,可按客户覆盖 |
| 导出即归档 | 导出成功自动把成片登记为 `assets(type=render)`,后续复用不必再找下载包 |
| 大包 | 超过阈值(如 2 GB 或 500 个文件)改为分卷或在后台生成,前端轮询进度 |
| 审计 | 导出记录含:操作人、客户、勾选项 ID 清单、命名规则版本、包 hash、下载次数 |
### 7.6 行业包从配置到发布
```mermaid
flowchart LR
A["草稿<br/>表单设计器 / JSON"] --> B["自动校验<br/>7 项"]
B -->|不通过| A
B -->|通过| C["待复核"]
C -->|复核驳回| A
C -->|复核通过| D["发布(P0)"]
D --> E["schema 快照不可变"]
D --> F["Ajv 校验器编译并缓存"]
D --> G["通知:客户 / 运营可见"]
```
| **校验项** | **规则** | **失败后果** |
| --- | --- | --- |
| 字段数与 key | ≤ 60 个字段;key 全局唯一且不可与系统字段冲突 | 禁止保存 |
| 类型白名单 | 仅允许 14 类受支持类型 | 禁止保存 |
| 必填与变量 | 进入生成的必填字段必须映射到变量槽 | 禁止发布 |
| 联动 | 最多 2 级;禁止循环 | 禁止保存 |
| 选项来源 | 字典项必须存在且未停用 | 禁止发布 |
| 合规规则 | 行业包必须至少声明 1 条阻断类合规规则(金融强制) | 禁止发布 |
| 影响面 | 发布前提示受影响客户数与要素卡数 | 仅提示 |
**发布事务**:版本状态迁移 + schema 快照落库 + 事件 `industry.pack.published` + 审计写入,同一事务;缓存失效通过事件异步执行,失败可重放(缓存过期兜底)。
### 7.7 AI 路由、限流、降级与记账
```mermaid
flowchart TB
A["worker 发起调用"] --> B["解析路由:用途 + 客户覆盖 + 优先级"]
B --> C{"取令牌"}
C -->|拿到| D["调用主模型"]
C -->|超时| E["排队等待 / 换备用池"]
D --> F{"结果"}
F -->|成功| G["结算记账 + 产物落库"]
F -->|限流/超时/5xx| H["退避重试 → 主失败切备用"]
F -->|内容违规/参数错误| I["不重试,落失败原因"]
H --> D
```
| **错误类型** | **判定** | **动作** |
| --- | --- | --- |
| 限流(429) | 供应商返回或本地令牌不足 | 指数退避 + 抖动,重试 3 次;持续则切备用模型 |
| 超时 | 超过该用途配置的超时 | 重试 1 次;再失败换路由 |
| 5xx / 网络 | 供应商不可用 | 熔断 60 秒,期间新任务直接走备用 |
| 内容违规 | 供应商安全拦截 | **不重试**(重试只会再花钱),落原因并提示修改提示词或素材 |
| 参数错误 | 4xx 语义(非限流) | 不重试,落原因,标记为"配置问题" |
**记账规则**:调用前预占(`estimate`),调用后按实际用量结算(`settle`);预占与结算都写 `ai_usage`,`cost_budgets` 按客户 / 角色 / 月聚合,80% 触发告警、100% 阻断新任务(超管可临时放宽,操作留痕)。
**配置合并顺序**:任务级参数 > 模型级默认参数 > 供应商默认;提示词同理,`ai_config` 整段覆盖代码兜底常量。任务创建时把「模型 ID + 参数快照 + 提示词版本号」写进任务头,事后可复现。
**方言与健壮性**:`thinking` / `enable_thinking` 由适配器映射,不支持的参数按供应商白名单过滤而不是静默透传;通义要求提示词含 JSON 字样否则 400,DeepSeek 可能返回空 content,统一做「空内容重试 + 解析兜底」;联网搜索作为独立前置步骤(Responses 支持、Chat 静默忽略),结果落库、失败降级不阻断主生成。
### 7.8 故障场景与恢复矩阵
| **场景** | **用户可见表现** | **系统动作** | **数据一致性** |
| --- | --- | --- | --- |
| worker 进程崩溃 | 任务卡在"生成中" | 队列消息重回;`media_items` 有幂等键,已完成项跳过 | 无重复计费;最多漏掉正在执行的那一项,重跑补上 |
| 队列积压 | 进度不动 | 告警;自动扩 worker(P2 自动,前期手动) | 无影响 |
| 供应商限流 | 生成变慢 / 部分失败 | 退避重试、切备用、降低并发令牌 | 失败项可单独重试 |
| 供应商整体不可用 | 大面积失败 | 熔断 + 切换备用路由;前端提示"当前使用备用模型" | 已成功项不重算 |
| 对象存储不可用 | 预览报错、上传失败 | 重试;暂停新任务入队(保护性限流) | 任务项保持"生成中",恢复后续跑 |
| Redis 不可用 | 队列停摆、缓存全部穿透 | 只读降级:列表与详情可看,提交任务返回 503 | 队列丢失的消息靠 `outbox` 重投 |
| 数据库主从切换 | 短时 5xx | 连接池重连 + 重试 | 事务已提交的不受影响 |
| 浏览器刷新 / 断网 | 进度丢 | SSE 支持 `Last-Event-ID` 续推;轮询兜底 | 勾选以服务端为准 |
| 导出中途失败 | 下载 404 / 包不完整 | 任务可重试;清单只在成功时落库 | 不产生"半成品清单" |
| 要素卡 schema 已发布后修改 | 老卡显示异常 | 按 `schema_version_id` 解释,不回改历史;只允许弃用字段 | 历史卡永远可解释 |
---
### 7.9 素材入库、转码与发布链路(外部成片 / 历史素材)
> 生成产物走 3.4 / 3.5;本节只覆盖**从外部进来的成片**(实拍、外包制作、历史素材)。参考项目(baoliangmofang)在这一段有可用设计,但**其自身没有出图实现**,本节是「入库与管理」而非「生成」。
```mermaid
sequenceDiagram
participant FE as Web
participant API as API
participant OS as 对象存储
participant Q as media:post 队列
FE->>FE: 计算 sha256(分片并行)
FE->>API: POST /uploads/presign(hash, 文件名, 大小, 分片数)
API-->>FE: 命中秒传 → 直接返回 fileId;否则返回分片预签名
FE->>OS: PUT 分片(断点续传,失败只重传该分片)
FE->>API: POST /uploads/{id}/complete
API->>OS: headObject 校验大小与 etag
API->>API: 写 material_files + materials(状态=待完善)
API->>Q: 入队 media:post
Q->>Q: 抽帧 / 转码 HLS / 水印 / 缩略图 / 元数据
Q->>API: 回写元数据,状态→待送审
```
| **步骤** | **关键约束** |
| --- | --- |
| 预签名 | 校验客户范围与配额;`Idempotency-Key` 防重复登记;秒传命中不覆盖已有引用 |
| 分片直传 | 单分片 5–16MB;并发 ≤ 4;断点续传按 `upload_sessions` 恢复;分片 7 天过期 |
| 完成校验 | 必须 `headObject` 核对大小与 etag,不信任前端声明 |
| 转码 | 统一 H.264 / AAC / faststart;竖版 1080×1920;预览生成 HLS + 水印版,原片不公开 |
| 状态推进 | 只有 `media:post` 成功才允许「待送审」;转码失败保留原文件并标记「待完善」 |
| 清理 | 分片过期清理;失败中间产物保留 24 小时;`exports` 产物 3 天 |
> **为什么不复用生成链路**:生成链路的 `media_items` 是「一次请求的产物」,有成对的重试与成本语义;外部成片没有成本、没有镜头,混用会让两套语义互相污染。因此素材入库走 `materials` + `upload_sessions`,与生成任务只在「送审」与「导出」两处汇合。
## 8 关键设计决策(ADR)
> 每条决策都写"背景 → 候选 → 选择 → 代价 → 重评条件"。评审时若不同意某条,只需要针对"代价"和"重评条件"提异议,不必重开整场讨论。
### 8.1 D-01 模块化单体而非微服务
| **项** | **内容** |
| --- | --- |
| 背景 | 8 个业务域、2–4 人研发、日 < 500 条成片、强事务(任务 + 账本 + 资产) |
| 候选 | A 单体分层;B 模块化单体;C 8 个微服务 |
| 决策 | B:单进程内按 Nest 模块划分边界,跨模块只走 Service / 事件;扩缩按进程类型(api / image / video / export) |
| 代价 | 模块边界靠纪律(ESLint + review)而非网络隔离;单库单 schema,无法按域独立选库 |
| 重评 | 某域 QPS 独立增长 3 倍以上、或出现独立团队独立发布 → 先拆"媒体处理"与"AI 接入"两个最脏的边 |
### 8.2 D-02 BullMQ + Redis 队列,而非 Kafka / 云 MQ
| **项** | **内容** |
| --- | --- |
| 背景 | 任务量大、可重建、无跨团队消费方;需要延迟、重试、并发控制、优先级 |
| 候选 | A BullMQ + Redis;B Kafka;C 阿里云 MNS/RocketMQ;D 数据库轮询 |
| 决策 | A:延迟/重试/并发/优先级开箱即用;Redis 同时承担缓存与限流,组件数最少 |
| 代价 | 消息持久化弱于 MQ(Redis 持久化 + AOF everysec 可接受);队列堆积依赖监控 |
| 重评 | 出现跨系统订阅、需要严格一次投递、或消息量 > 100 万/日 |
### 8.3 D-03 动态要素卡用 JSON Schema + 物化索引,而非每行业建表 / EAV
| **项** | **内容** |
| --- | --- |
| 背景 | 行业包要求"5 个工作日上新行业",要素卡字段随行业变化,字段有"可检索"标记 |
| 候选 | A 每行业一张宽表;B EAV(一行一字段);C JSONB + 按需物化索引;D 外部表单引擎 + 独立库 |
| 决策 | C:值存 JSONB,`brief_card_index` 只落"可检索"字段,schema 版本不可变 |
| 代价 | 任意字段的复杂查询受限(只能查已物化字段);JSONB 写放大 |
| 重评 | 出现"对未物化字段做高频组合筛选"的强需求,或单表 > 5000 万行 |
### 8.4 D-04 客户隔离三层:库层强制 + 应用层注入 +(P1)行级安全
| **项** | **内容** |
| --- | --- |
| 背景 | R-04 要求客户级隔离;越权一次就是事故 |
| 候选 | A 仅应用层过滤;B 应用层 + 库层强制列/外键;C B + PostgreSQL RLS;D 物理分库 |
| 决策 | B 先落地(`customer_id NOT NULL` + Repository 必传 `ScopeContext` + 外键),C 作为 P1 加固;D 明确不做 |
| 代价 | Repository 接口变重;RLS 会带来连接与会话变量管理成本(放 P1 评估) |
| 重评 | 出现监管级隔离要求、或第二套后端服务直连同库 |
### 8.5 D-05 媒体处理用 Node + ffmpeg CLI,不自建 Python 服务
| **项** | **内容** |
| --- | --- |
| 背景 | 日产 < 500 条、拼接与转码为主、无自建 GPU;团队 TS 全栈 |
| 候选 | A Node + ffmpeg/sharp;B Python + moviepy/ffmpeg;C 云媒体处理(MPS/智能媒体);D 前端 Remotion 渲染 |
| 决策 | A:进程隔离即可控,ffmpeg 覆盖拼接/转码/抽帧/水印;C 作为峰值兜底备选 |
| 代价 | 复杂多层动画与精细时间线弱于专业库;CPU 密集,需独立机器 |
| 重评 | 出现"多轨道动画 / 动效模板"强需求,或拼接耗时成为瓶颈 → 引入 Remotion 或 Python 服务 |
### 8.6 D-06 对象存储走 S3 协议,M0 用 MinIO
| **项** | **内容** |
| --- | --- |
| 背景 | 用户选择"对象存储(自建 MinIO)+ 浏览器打包下载";同时不希望绑死厂商 |
| 候选 | A 直接 OSS/COS SDK;B S3 兼容抽象 + MinIO;C 本地文件系统 |
| 决策 | B:接口层只用 S3 API;M0 自建 MinIO(2 节点纠删码),可无痛切云 OSS(同接口) |
| 代价 | 自运维磁盘与副本;跨地域复制需自建脚本 |
| 重评 | 对象量 > 50 TB、或需要 CDN 加速下载 → 切云 OSS + CDN |
### 8.7 D-07 权限 = 代码注册的权限点 + 数据化角色 + 特殊权限 + SoD
| **项** | **内容** |
| --- | --- |
| 背景 | 仅内置超管,其余角色可配置(PRD 3、5.4.3、R-16~R-18) |
| 候选 | A 硬编码角色判断;B 纯 RBAC 表驱动;C RBAC + 数据范围 + 特殊权限 + SoD 校验 |
| 决策 | C:权限点用常量表 + 装饰器注册(防"配了一个不存在的点"),角色/范围/特殊权限为数据 |
| 代价 | 权限点变更需发版(但这是期望的:权限点应受控);角色配置界面需要解释成本 |
| 重评 | 出现"客户侧自助分配权限"需求 → 增加租户内角色模板 |
### 8.8 D-08 异步任务统一协议:SSE 优先 + 轮询兜底
| **项** | **内容** |
| --- | --- |
| 背景 | 长任务进度、勾选状态恢复、多人同时看同一任务 |
| 候选 | A 纯轮询;B WebSocket;C SSE + 轮询兜底 |
| 决策 | C:SSE 简单、可穿代理、支持 `Last-Event-ID` 续推;轮询作为降级与兜底 |
| 代价 | SSE 单向(够用);连接数受 Nginx 配置限制(100 人规模无压力) |
| 重评 | 出现"任务间协同 / 实时评论"需求 → 引入 WebSocket |
### 8.9 D-09 导出走服务端打包 + 短签名 URL
| **项** | **内容** |
| --- | --- |
| 背景 | PRD 5.5:仅导出勾选项、命名规则、同名加序号、留痕;每日 < 500 条 |
| 候选 | A 前端逐张下载;B 服务端打 zip;C 共享网盘挂载 |
| 决策 | B:打包任务 + 清单快照 + 15 分钟签名 URL;大包分卷 |
| 代价 | 打包占 CPU 与临时盘;包内命名错误需重打整包 |
| 重评 | 单包 > 10 GB 或需要直投达人/媒体平台 → 改对象存储直链 + 客户端脚本 |
### 8.10 D-10 成本治理前置:预占 + 结算 + 账本 + 预算闸门
| **项** | **内容** |
| --- | --- |
| 背景 | 生成能力全部外采,单条视频 5 次图生视频,成本不可控会直接吃利润 |
| 候选 | A 事后统计;B 调用后记账;C 调用前预占 + 后结算 + 预算闸门 |
| 决策 | C:提交接口强返预估、超预算二次确认、调用前预占、调用后结算 |
| 代价 | 预占需要回收机制(超时释放),否则额度会被"僵尸任务"占住 |
| 重评 | 模型改为包年/包量计费后,改为"配额池 + 用量告警" |
### 8.11 D-11 图片线走确定性合成,不做 AI 生成
| **项** | **内容** |
| --- | --- |
| 背景 | PRD 5.2.1 要求图片批量产出;素材工坊(`sucaigongfang`)已验证「PSD 模板 + 剪贴蒙版 + 抠图 + 合成」可规模化生产;AI 生图需求文档存在但未上线 |
| 候选 | A AI 文生图 / 图生图为主;B PSD 模板套版为主(确定性);C 混合(模板为主 + AI 兜底) |
| 决策 | B:模板套版 + 抠图 + 多规格合成;AI 生图仅作 M5 预研,不进入本期 |
| 代价 | 创意上限受模板供给限制,设计产能成为新瓶颈;非电商行业(金融 / 本地生活)模板需要重新设计一批 |
| 重评 | 模板复用率连续两季度下降,或非电商行业素材占比 > 30% 且模板供给跟不上 → 评估 C(AI 生成作补充位) |
---
## 9 非功能落地(逐条对应 PRD 第 9 章)
### 9.1 性能预算分解
| **PRD 指标** | **预算分解** | **实现机制** | **验收方式** |
| --- | --- | --- | --- |
| 列表首屏 ≤ 2s | 接口 P95 ≤ 500ms + 前端渲染 ≤ 800ms + 静态资源 ≤ 300ms | 游标分页、覆盖索引、虚拟滚动、Nginx 强缓存静态资源(CDN 暂不做,见 1.4) | 生产样本库压测 + 真实用户监测 |
| 脚本单条 ≤ 30s | 模型 P95 ≤ 20s + 排队 ≤ 5s + 校核 ≤ 5s | 异步 + 队列优先、供应商路由选低延迟档 | 按模型统计 P95,超时告警 |
| 图片单张 ≤ 10s | 拉物料 ≤ 2s + 合成 ≤ 5s + 上传 ≤ 3s | 物料缓存、sharp 并行、对象存储内网直传 | 任务项耗时分布看板 |
| ≥ 50 张预览不卡 | 首屏只加载 12 张缩略图 + 其余懒加载 | 虚拟网格、缩略图 240px、浏览器缓存 | 前端性能测试(低配机) |
| 视频预览首帧 ≤ 2s | faststart MP4 + 首帧图预生成 + Range | 转码统一 faststart;首帧单独存 jpg | 抽样测首帧时间 |
| 断点续播 | 支持 Range 请求 | 对象存储原生 Range + 签名 URL | 手工 + e2e |
### 9.2 缓存策略
| **对象** | **位置** | **TTL** | **失效方式** |
| --- | --- | --- | --- |
| 权限点与数据范围 | Redis + 请求内 | 5 分钟 | `permission.changed` 事件主动删 |
| 行业包 schema(编译后 Ajv) | 进程内 LRU | 无(跟随版本) | `industry.pack.published` 事件 |
| 字典项 | Redis | 30 分钟 | 字典写操作删 key |
| 客户资产包 | Redis | 5 分钟 | 客户/资产变更事件 |
| 模板与物料缩略图 | 浏览器 + 对象存储 | 1 天 | 版本号变更即换 URL |
| 看板聚合 | Redis | 5 分钟 | 定时任务刷新 |
> 缓存原则:**权限与隔离相关的不做长缓存**;宁可多查一次,也不要拿旧权限放行。
### 9.3 并发、队列与限流
| **层级** | **限制** | **目的** |
| --- | --- | --- |
| 用户级 | 同时 ≤ 3 个批量任务;单任务条数上限(可配) | 防误操作与刷量 |
| 客户级 | 单客户并发任务 ≤ 5;单客户队列占比 ≤ 30% | 防一个大客户挤占全平台 |
| 供应商级 | 按 QPS / 并发令牌池,独立于业务队列 | 避免触发供应商封禁 |
| 接口级 | 登录 5 次/分钟;导出 10 次/分钟 | 防暴力与误点 |
| 公平性 | 队列按客户分片轮询(round-robin),而非 FIFO | 大客户不饿死小客户 |
### 9.4 可靠性
| **机制** | **落点** |
| --- | --- |
| 幂等 | 请求 `Idempotency-Key`;任务项唯一键;AI 回调 `provider_job_id` 唯一 |
| 超时 | 接口 15s、图片单张 60s、视频单镜 10 分钟、拼接 15 分钟;分层超时,外层必须大于内层 |
| 重试 | 仅对"可重试错误"(限流/超时/5xx)重试;内容违规不重试;重试次数与退避写配置 |
| 断点 | 任务项级状态落库,重投跳过已完成 |
| 降级 | 供应商熔断 → 备用路由;Redis 故障 → 只读 + 拒绝新任务;对象存储故障 → 保护性暂停 |
| 数据修复 | 提供"任务一致性巡检"脚本:孤儿对象、状态与产物不一致、账本缺记录,每日跑并告警 |
### 9.5 安全
| **面** | **措施** |
| --- | --- |
| 认证 | 密码 Argon2id;登录失败锁定;refresh 旋转 + 复用检测(复用即全端下线) |
| 授权 | 权限点 + 数据范围 + 特殊权限三层;越权返回 403 且审计 |
| 越权测试 | 每个端点的"数据范围外"用例进 CI(见 6.7) |
| 上传 | 预签名直传 + 类型/大小/尺寸校验 + 图片重编码(剥离 EXIF 与潜在脚本)+ 病毒扫描(可选) |
| SSRF | 出网仅允许白名单域名(供应商 API),禁止用户可控 URL 的服务端抓取 |
| 密钥 | KMS / 加密环境变量;日志与异常上报统一脱敏;密钥轮换流程 |
| 注入 | Prisma 参数化;原生 SQL 必须走参数绑定(lint 规则) |
| 审计 | 变更类操作全量;审计表只插不改;导出审计本身也审计 |
| 传输 | 全站 TLS 1.2+;内部服务间走内网;对象存储签名 URL 短时有效 |
### 9.6 合规
| **要求** | **实现** |
| --- | --- |
| AI 生成内容标识(显式 + 隐式) | 画面角标 + MP4 元数据;校验任务检查;导出清单标注 |
| 肖像与素材授权 | 人物池资产带授权材料与到期日;到期前 30 天提醒;过期禁止用于生成 |
| 分行业合规 | 行业包 `compliance_rules` 数据化;金融必须有风险提示与禁止承诺收益 |
| 数据保留 | 审计 ≥ 12 个月;成片默认永久(客户结束只读);中间产物 30 天 |
| 日志脱敏 | 手机号/身份证/账号 token 掩码;模型请求不落完整提示词(落 hash + 版本) |
| 数据出境 | 默认全部在境内云与境内供应商;若用境外模型,需单独评估并开关控制 |
### 9.7 可观测性
| **类别** | **清单** |
| --- | --- |
| 指标 | 接口 P50/P95/P99、错误率;队列长度/等待时长/失败率;任务成功率与耗时分布;单条成本与日成本;模型成功率/P95;缓存命中率;DB 连接与慢查询 |
| 日志 | 结构化 JSON,字段含 `traceId`、`taskId`、`itemId`、`customerId`、`action`;禁止打印 token 与完整提示词 |
| 链路 | API → 队列(消息头带 trace)→ worker → 外部 API,全链路可追 |
| 看板 | 产能(日成片/人效)、质量(保留率/淘汰理由分布)、成本(按客户/模型/批次)、稳定性(失败率/积压) |
| 告警 | 队列积压 > 10 分钟;任务失败率 > 5%;模型可用性 < 95%;日成本 > 预算 80%;审计写入失败;备份失败 |
| 巡检 | 孤儿对象、僵尸任务、账本缺口、分区剩余、磁盘水位 |
### 9.8 容量与成本联动(三条硬规则)
1. **提交即预估**:任何批量提交必须返回预估条数、预估成本、预估时长;缺一项不允许提交。
2. **成本超阈值即拦截**:客户/角色/月预算 100% 时阻断新任务(超管可临时放宽,留痕)。
3. **中间产物有生命周期**:`temp` 桶 30 天自动清理,避免"为了排查方便"把存储成本翻 4 倍。
---
## 10 研发交付路径
### 10.1 仓库结构(Monorepo)
```text
lingxi/
├─ apps/
│ ├─ api/ # NestJS 应用(HTTP + SSE + 回调)
│ ├─ worker/ # 各类队列消费者(同一镜像,按 QUEUE 环境变量启动)
│ └─ web/ # React SPA
├─ packages/
│ ├─ shared/ # 领域事件、错误码、权限点常量、Zod schema、工具
│ ├─ form-engine/ # 动态表单 schema 解析与渲染(前端)
│ ├─ render/ # 模板合成与 ffmpeg 封装(worker 用)
│ ├─ ai-sdk/ # 供应商适配器与路由
│ └─ ui/ # 组件库(Semi 封装 + icons.ts 统一出口 + 业务组件)
├─ prisma/ # schema 与迁移
├─ docs/ # PRD、技术架构、运维手册
└─ infra/ # compose、Nginx、监控配置
```
工程约定:pnpm workspaces + Turborepo;TypeScript strict;禁 `any`(`unknown` + 收窄);提交前 lint + typecheck + 单测;PR 必须带"影响面 + 回滚方式"。
**包管理:只用 pnpm(硬约定)**
| **规则** | **落地方式** |
| --- | --- |
| 唯一包管理器 | 根 `package.json` 声明 `"packageManager": "pnpm@10.x"`;人、CI、Dockerfile 一律用 pnpm |
| 禁用 npm / yarn | `preinstall` 脚本用 `only-allow pnpm` 直接失败;仓库根只提交 `pnpm-lock.yaml`,出现 `package-lock.json` / `yarn.lock` 视为错误(CI lint 检查) |
| 可重复安装 | CI 与镜像构建一律 `pnpm install --frozen-lockfile`(不一致即失败,不隐式升级) |
| 工作区依赖 | 内部包用 `workspace:*` 引用;禁止 `npm link` / 相对路径跨包子路径引用 |
| 例外 | 仅当某个全局 CLI 只能经 npm 安装(如个别脚手架)时,限定在构建镜像内并登记到运维手册,仓库依赖一律不得使用 |
| Node 版本 | `.nvmrc` / `engines` 与镜像统一 24.15.0,由 `pnpm` 启动前校验 |
### 10.2 里程碑(与 PRD M0–M3 对齐)
| **里程碑** | **PRD 范围** | **技术交付物** | **验收标准** | **估算人日** |
| --- | --- | --- | --- | --- |
| T0 骨架 | M0 前置 | Monorepo、CI、鉴权、权限点体系、客户/行业、行业包 v1(电商)、字典、审计、对象存储、队列骨架、观测基线 | 能登录、能建客户、能配置一个行业包、能跑通一个空任务 | 25–30 |
| T1 可用 | M0 | 要素卡与表单渲染、脚本生成 + 校核 + 审核入库、图片线(模板合成 + 规格矩阵 + 预览勾选 + 导出)、任务协议与 SSE | 单客户完成"要素卡 → 脚本 → 图片 → 导出"闭环 | 45–55 |
| T2 提效 | M1 | 视频线(逐镜 + 拼接 + 校验 + 预览下载)、AI 路由与预算、快筛、成片复用、金融包 v1 | 单条视频从脚本到成片跑通,成本可查 | 50–60 |
| T3 规模 | M2–M3 | 第三个行业包、批量变体矩阵、效果回流、看板完善、扩容与压测、RLS 评估 | 双行业并行生产,峰值 500 条/日无积压 | 40–50 |
> 人日为「净开发」,不含评审、联调与等待外部 API 的时间。按 2 后端 + 1 前端 + 1 全栈/基础设施 + 0.5 测试(约 4.5 人)测算,T0–T2 净开发 120–145 人日 ≈ 27–32 个并行工作日,叠加评审、联调与外部依赖等待(按 40%–60%)后,日历约 2.5–3.5 个月到 T2;T3 另需约 1.5–2 个月。
### 10.3 人力配置
| **角色** | **人数** | **主要职责** |
| --- | --- | --- |
| 后端 | 2 | 业务模块、权限与审计、任务编排、AI 适配 |
| 前端 | 1–2 | 后台页面、表单渲染器、预览与快筛、看板 |
| 全栈 / 基础设施 | 1 | 部署、CI/CD、观测、备份、成本与容量 |
| 测试 | 0.5–1 | 用例设计、接口与 e2e、性能与越权测试 |
| AI 工程(兼) | 0.5 | 提示词体系、模型评测与路由策略 |
### 10.4 测试策略
| **层** | **工具 / 做法** | **门槛** |
| --- | --- | --- |
| 单元测试 | Vitest;核心规则(命名、范围解析、状态机、schema 校验)必须覆盖 | 关键包行覆盖 ≥ 70% |
| 集成测试 | Testcontainers(PG + Redis + MinIO)跑真实依赖 | 主链路必须有 |
| 契约测试 | OpenAPI + oasdiff;Zod 单一真源 | 破坏性变更必须升版本 |
| 端到端 | Playwright:登录 → 生成 → 预览 → 勾选 → 导出 | 每条主链路 1 条 e2e |
| 权限测试 | 每个端点 3 类用例(有权限 / 无权限 / 范围外) | 全端点覆盖 |
| 性能测试 | k6:列表、提交、导出;Mock AI 固定延迟 | 达标 PRD 9.1 |
| 视觉回归 | 图片合成结果对模板做像素级基线 | 模板/渲染变更时必跑 |
### 10.5 上线与灰度
1. 先内测(1 个客户、1 个行业包、图片线),用真实批次校验命名、导出、审计;
2. 再灰度到 2–3 个客户,观察成本与失败率;
3. 视频线单独开关(客户级 feature flag),出问题只关视频不影响图片;
4. 每次上线带"回滚方式":镜像回滚 + 迁移向前兼容(expand/contract)。
---
## 11 风险与未决
### 11.1 技术风险
| **编号** | **风险** | **影响** | **概率** | **应对** | **监控** |
| --- | --- | --- | --- | --- | --- |
| R-T-01 | 外部模型限流或涨价 | 产能波动、成本上升 | 高 | 多供应商 + 路由切换 + 预占闸门 | 模型可用性、单条成本曲线 |
| R-T-02 | 视频质量不可控(人物抖动、商品变形) | 返工率高、保留率低 | 高 | 逐镜重生成、参考物料锁定、质量校核、快筛 | 保留率、淘汰理由分布 |
| R-T-03 | 单个视频任务耗时过长 | 用户等待、队列积压 | 中 | 镜头并行、超时控制、进度可见、部分交付 | 任务 P95 时长、积压 |
| R-T-04 | 动态表单复杂度失控 | 渲染慢、校验漏洞、配置难懂 | 中 | 字段上限、类型白名单、联动 2 级、发布前 7 项校验 | schema 复杂度统计 |
| R-T-05 | ffmpeg 资源争抢 | 拼接排队、机器打满 | 中 | 独立队列与机器、并发上限、进程级限流 | CPU/内存、队列等待 |
| R-T-06 | 越权或数据串客户 | 事故级,信任受损 | 低 | 三层隔离 + CI 权限用例 + 审计告警 | 403 分布、异常访问 |
| R-T-07 | 对象存储单点或磁盘故障 | 素材不可用 | 中 | 2 节点纠删码 + 云冷备 + 版本化 | 磁盘水位、复制延迟 |
| R-T-08 | 成本失控(尤其视频) | 毛利被吃 | 中 | 预占结算 + 预算闸门 + 看板 | 日/周成本、单条成本 |
| R-T-09 | 审计日志量增长 | 库膨胀、查询变慢 | 中 | 按月分区 + 归档 + 只保留必要字段 | 分区大小、查询耗时 |
| R-T-10 | AI 内容合规(虚假宣传、肖像) | 下架、投诉、监管 | 中 | 合规规则集 + 人工审核 + 标识 + 授权管理 | 合规拦截率、投诉记录 |
| R-T-11 | 单地域故障 | 短时不可用 | 低 | 每日备份 + RTO 4 小时 + 演练 | 备份成功率 |
| R-T-12 | 团队规模小、知识集中 | 交付延期 | 中 | 文档化、模块 owner、避免过度设计 | 里程碑偏差 |
| R-T-13 | 参考项目无图片 / 视频渲染实现,图片线无先例 | 工期估算偏乐观、链路返工 | 高 | 渲染服务按独立进程设计(sharp / ffmpeg);先做单模板端到端打通再谈规格矩阵;保留 2 周缓冲 | 单模板端到端耗时、返工次数 |
| R-T-14 | 客户审核链接泄露或被转发 | 内容外泄、投诉 | 低 | 签名 + 有效期 + 可作废 + IP / UA 限流 + 只读范围;改稿留痕 | 链接访问来源分布、异常高频访问 |
| R-T-15 | 外部成片入库后转码堆积或失败 | 素材长时间不可用、队列积压 | 中 | `media:post` 独立队列 + 失败保留原文件 + 状态「待完善」;转码失败不影响预览原片 | 队列等待、失败率、待完善存量 |
| R-T-16 | 对象存储增长失控(外部成片 + 多规格) | 存储成本超预算 | 中 | 秒传去重 + `temp/exports` 生命周期 + 冷数据转低频 + 月度容量看板 | 桶增长曲线、单素材平均体积 |
| R-T-17 | PSD 图层命名不规范(设计师漏标 / 命名漂移) | 模板解析不出替换位、批次整体失败 | 高 | 上传即校验 + 图纸规范文档 + 模板预览叠加替换位提示 + 拒绝入库而不静默跳过 | 模板入库失败率、缺图层告警数 |
| R-T-18 | 镜像缺中文字体导致文字渲染成豆腐块 | 成片文字全错但任务「成功」,属最危险的静默交付 | 高 | 镜像内置字体 + 启动自检 + 渲染前字体可用性断言 + 抽样 OCR 校验(可选) | 启动自检结果、字体回退告警 |
| R-T-19 | 抠图质量不达标(发丝 / 透明材质 / 复杂背景) | 主体带白边或残缺,返工与客诉 | 中 | 抠图降级原图并标「低质量」+ 低质量默认不勾选 + 质量看板 + 复杂品类转人工 | 抠图一次通过率、低质量产出率 |
### 11.2 待确认(技术侧)
| **编号** | **问题** | **影响** | **建议** |
| --- | --- | --- | --- |
| QT-01 | 云厂商与地域(阿里云 / 腾讯云 / 其他) | 资源选型、备案、出网策略 | 建议先用阿里云华东,境内模型供应商同地域 |
| QT-02 | 生产数据库 / Redis 的主机形态(独立数据节点 vs 与应用同机) | 可用性与备份窗口 | 已定自建 PG 18 + Redis;建议数据节点独立成机,不与 worker 混部 |
| QT-03 | 域名与备案主体 | 上线时间 | 需提前 2–3 周启动 |
| QT-04 | SSO 协议(企业微信 / 飞书 / 钉钉 / OIDC) | 登录实现 | 先账号密码,SSO 留适配器 |
| QT-05 | AI 供应商清单与账号主体、结算方式 | 成本与合规 | 需在 T1 前确认 2 家文本 + 2 家视频可商用 |
| QT-06 | 各供应商并发上限与单账号配额 | 队列并发设置 | 需要商务确认后写入路由配置 |
| QT-07 | 月度 AI 预算与超预算策略 | 闸门阈值 | PRD Q-09 未定,建议先给"软上限 + 告警" |
| QT-08 | 是否需要 CDN 与下载加速 | 成本与体验 | 首年不需要,出网超阈值再评估 |
| QT-09 | 备份 RPO / RTO 的正式要求 | 架构与费用 | 建议 RPO 5 分钟(DB)/ 24 小时(对象),RTO 4 小时 |
| QT-10 | 审计与成片的保留年限 | 存储与合规 | 建议审计 24 个月、成片永久(客户结束只读) |
| QT-11 | 是否允许使用境外模型 | 合规 | 默认不允许,需单独评估并开关控制 |
| QT-12 | 图片/视频单文件大小与格式上限 | 上传与转码 | PRD 待定,建议图片 ≤ 20 MB、视频 ≤ 200 MB |
| QT-13 | 是否需要私有化 / 内网部署版本 | 架构分叉 | 当前不做,S3 + Compose 已具备迁移条件 |
| QT-14 | 团队是否接受 Monorepo + TS 全栈 | 工程效率 | 已按用户选择落地;若团队以 Java 为主需重评 |
---
## 12 附录
### 附录 A 术语与缩写
| **术语** | **含义** |
| --- | --- |
| ADR | 架构决策记录(Architecture Decision Record) |
| SSE | Server-Sent Events,单向服务端推送 |
| Outbox | 事务性消息表,保证"业务写入 + 消息投递"一致 |
| RLS | PostgreSQL 行级安全(Row Level Security) |
| PITR | 时间点恢复(Point-In-Time Recovery) |
| LRU | 最近最少使用缓存淘汰 |
| Mock AI Provider | 固定延迟与固定产物的假供应商,用于 CI 与压测 |
| Feature Flag | 按客户/用户开关功能的配置 |
| faststart | MP4 moov 前置,支持边下边播 |
| LUFS | 音频响度单位,用于多镜音量归一 |
### 附录 B PRD 追溯矩阵
| **PRD 需求** | **架构落点** |
| --- | --- |
| S-01~S-05(要素卡 / 模板 / 生成 / 校核) | 3.2 `script`、5.4.2、6.3.4、7.2 |
| S-06~S-12(审核、入库、版本、检索、复用) | 5.6 版本与快照、6.3.4、7.2 |
| M-01~M-05(图片线、规格矩阵、预览勾选) | 3.4、5.4.3、7.3 |
| M-06~M-14(视频线、逐镜、拼接、快筛、一致性) | 3.5、3.6、7.4 |
| M-15~M-17(视频方向、预览、命名下载) | 2.4 队列、5.4.3、7.5 |
| M-18~M-20(成片入库、版本链、素材库批量操作) | 2.5 桶与直传、7.9、5.3 素材域 |
| M-21~M-24(关联血缘、分享链接、送审、效果回流) | 5.3 交付/效果域、6.3.5、5.7 阈值同源 |
| M-25~M-27(爆量反哺、看板、元素拆解) | 5.3 `material_metrics_daily`、8.10 成本治理、12 附录 F |
| A-01~A-11(15 类资产、三级继承、复用、复审) | 3.2 `catalog`、5.3、5.4.4、5.6 |
| P-01~P-06(组织、账号、角色、权限) | 3.2 `identity`、5.3、6.2、7.1 |
| P-07~P-08(行业包、表单设计器) | 3.2 `industry`、5.4.2、7.6 |
| P-09~P-13(AI 配置、字典、审计、系统配置、看板) | 3.2 `platform`、5.3、6.3.7、9.7 |
| P-14~P-18(行业包、设计器、发布、字段审批、合规规则) | 5.3、7.6、9.6 |
| E-01~E-06(导出规则、命名、清单、留痕) | 3.2 `delivery`、7.5、6.5 |
| R-01 命名规则 | 7.5 命名服务、`system_configs` |
| R-02 引用资产禁删 | 5.4.4 `asset_refs` + 外键 RESTRICT |
| R-04 客户隔离 | 5.5 三层、9.5 |
| R-05 导出审计 | 5.4.5、7.5 |
| R-25 素材粒度(素材为主对象) | 5.3 `materials` / `material_specs`、7.9 |
| R-26 阈值同源 | 5.7 查询约定第 6 条、`system_configs` |
| R-27 送审基准(提交时冻结版本) | 3.6 状态机、5.3 `review_records` |
| R-28 分享与外部访问 | 6.3.5 `/share-links`、9.5 安全 |
| R-06 成本预估 | 7.7 预占结算、9.8 |
| R-09 AI 标识与肖像 | 7.4、9.6 |
| R-11 schema 版本 / R-14 变量完整 | 5.4.2、7.2、7.6 |
| R-16~R-18 角色可配置与 SoD | 6.2、7.1、8.7 |
| R-19 视频方向边界 | 5.4.3、3.5 |
| R-20 行业/客户目录 | 5.3、5.4.1、9.5 |
| 9.1 性能 / 9.2 安全 / 9.3 合规 | 全部第 9 章 |
| AC-01~AC-04(DoD) | 10.2 验收标准 + 10.4 测试策略 |
### 附录 C 关键环境变量
| **变量** | **示例** | **说明** |
| --- | --- | --- |
| `NODE_ENV` | production | 运行环境 |
| `DATABASE_URL` | postgres://... | 主库连接(含连接池参数) |
| `REDIS_URL` | redis://... | 缓存与队列(按 db 分库) |
| `S3_ENDPOINT` | http://minio:9000 | S3 兼容地址 |
| `S3_BUCKET_ASSETS` / `S3_BUCKET_RENDERS` / `S3_BUCKET_TEMP` | assets / renders / temp | 桶名 |
| `S3_ACCESS_KEY` / `S3_SECRET_KEY` | — | 走密钥管理,不入库 |
| `JWT_SECRET` / `JWT_REFRESH_SECRET` | — | 签名密钥,支持轮换 |
| `AI_PROVIDER_*_API_KEY` | — | 各供应商密钥(KMS 注入) |
| `QUEUE` | video | worker 启动时消费的队列组 |
| `WORKER_CONCURRENCY` | 12 | 并发上限 |
| `FFMPEG_PATH` / `FFPROBE_PATH` | /usr/bin/ffmpeg | 二进制路径 |
| `MAX_UPLOAD_MB` | 200 | 上传上限 |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | http://otel:4318 | 链路导出 |
| `FEATURE_VIDEO` | on | 视频线开关 |
| `COST_SOFT_LIMIT` / `COST_HARD_LIMIT` | 0.8 / 1.0 | 预算闸门比例 |
### 附录 D 容量与成本测算口径
| **项** | **公式** | **示例(按 500 条/日估算)** |
| --- | --- | --- |
| 视频模型时长 | 条数 × 镜数 × 单镜耗时 | 500 × 5 × 90s ≈ 62.5 模型小时/日 |
| 峰值并发 | 日均时长 ÷ 有效工作小时 × 峰值系数 | 62.5 ÷ 8 × 1.5 ≈ 12 |
| 成片存储 | 条数 × 单条大小 × 天数 | 500 × 10 MB × 365 ≈ 1.8 TB/年 |
| 含中间产物 | 成片 × 3~5 | ≈ 8 TB/年(30 天清理后可降至 ~2.5 TB) |
| 数据库增长 | 任务项 + 审计 + 账本 | 约 150–200 GB/年 |
| 月成本 | 模型费用 + 云资源 + 人力 | 模型费用占大头,云资源约占 15–25% |
> 具体单价随云厂商与供应商报价变化,本文不写死金额;预算评审时按此口径填数即可。
### 附录 E 与同期文档的关系
| **文档** | **关系** |
| --- | --- |
| `docs/PRD/灵犀-信息流广告素材一站式生产平台PRD.md` V1.3 | 需求基线;本文所有"PRD 5.x / R-xx / Q-xx"均指该文档 |
| `docs/PRD/灵犀_信息流素材生产平台_PRD_V0.3.md` | 同事基线;本文已合并其行业属性/信息模块/视频方向口径 |
| `docs/立项建议/...立项建议书_V0.1.md` | 论证与自建 vs 采购结论;本文第 8 章决策以其为前提 |
| `docs/skills/电商-达人口播skill`、`电商-达人展示skill` | 提示词与分镜 know-how 来源;本文 7.2/7.4 的提示词体系需以其为初始资产 |
| `项目参考/爆量魔方`(已 gitignore,不随仓库分发) | 参考实现:脚本生成 / 结构化解析 / 审核 / AI 配置可复用;图片与视频渲染无实现;复用矩阵见附录 F |
---
## 附录 F 与参考项目(爆量魔方 / baoliangmofang)的复用矩阵
| **能力** | **参考实现** | **本文处理** | **理由** |
| --- | --- | --- | --- |
| 数据字典 / 受控词表 | `Enumeration` 单表(type + code + parent + industry) | 直接复用,对应 5.3 的 `dict_types` / `dict_items` | 一张表同时表达二级树与词表,已被验证 |
| 模型四层配置 | Provider / Model / TaskMapping / CallLog + 主备 + 计量 | 直接复用,见 2.6 与 5.3 | 多供应商是既定约束,四层是必需结构 |
| 提示词与行业指南 | `ai_config` 键值 + 版本历史,system 末尾追加行业指南 | 直接复用,见 2.6 / 7.2 | 行业差异用配置表达,与 PRD 行业包一致 |
| 结构化解析 | JSON schema 8 字段 + 失败重试 + 原文保留 | 直接复用,见 7.2 | 解析失败不静默丢稿 |
| 脚本编号 | `ScriptSerial` 原子取号 + 过审时分配 | 直接复用,见 7.2 | 并发不重号、草稿不占号 |
| 客户审核 | 批次 + HMAC 签名 token + 改稿快照 | 改造后复用(P1),见 6.3.4 | 原实现缺 IP 限流与作废机制,需补齐 |
| 批量异步交互 | 提交 → 轮询 → 三态 + 失败明细行号 | 改造后复用(前端),明细改走 SSE | 轮询在 500 条量级可用,但 SSE 更省资源 |
| 图片 / 视频渲染 | **无实现**(无 sharp / canvas / ffmpeg / 对象存储) | 自建,见 3.4 / 7.3 / 7.4 | 参考项目的「转素材制作」只是导出 Word 交付包 |
| 权限模型 | `@Roles` + `@RequirePermissions` 双轨、空数组默认放行 | 自建单轨 + 默认拒绝,见 6.2 | 默认放行不适合多客户平台 |
| 客户隔离 | 每个 service 自写 `getAuthorizedClientIds` | 自建 ScopeContext + 强制 customer_id,见 6.2 | 原实现重复 6+ 处、易漏;本文以守卫统一收口 |
| 任务状态存储 | 进程内 Map / Redis | 自建落库(`media_tasks` + `outbox`) | 进程重启即丢,无法审计与重试 |
| 权限隐藏 | 直接 `removeChild` DOM | 自建路由守卫 + 条件渲染 | 不可恢复、不可测试 |
| 素材 / 规格模型(baoliangmofang) | 素材为主对象、规格为属性(Q4 结论「同源多规格」) | 直接采纳,见 5.3 `materials` / `material_specs` | 多规格重复计费与重复审核是必然坑 |
| 分片直传与秒传(baoliangmofang) | hash → presign → 直传 → complete → 入队 | 直接采纳,见 7.9 | 应用服务器不中转大文件是硬约束 |
| 送审抽象(baoliangmofang) | 内部审核 + 外部提审登记共用批次;审核基准=提交时刻 | 直接采纳,见 3.6 / 6.3.5 | 两套批次并行会导致对账困难 |
| 效果回流与阈值同源(baoliangmofang) | 日指标落表、派生指标不落库、阈值单点 | 直接采纳,见 5.7 / `material_metrics_daily` | 历史项目在这类口径上反复对账 |
| 向量检索(baoliangmofang) | PG18 + pgvector(HNSW / 1024 / cosine),M3 引入 | 改为「M3 起可有」,见 5.7 | 与本项目「M1–M2 不上向量」的节奏一致 |
| 图片出图 / 套版渲染(`爆量魔方` / `baoliangmofang`) | **无实现**(两仓库均无 sharp / canvas / 出图 API 接入) | — | 这两个仓库只能参考管理与脚本侧 |
| 图片套版与抠图(素材工坊 `sucaigongfang`) | PSD 剪贴蒙版 + `ag-psd` 解析 + ONNX 抠图 + canvas/sharp 合成 + BullMQ 批次 + 并发建议器 | **按实现对齐**,见 3.4 / 7.3 / 8.11 | 唯一有生产验证的图片链路(位于 `/Users/norman/Documents/项目/sucaigongfang`,不在 `项目参考/` 内,不入库) |
> **结论**:三个参考来源分工明确——`爆量魔方` 提供「脚本生产」骨架(数据字典 → 提示词 → 解析 → 审核 → 编号 → 交付),`baoliangmofang`(其重写版)提供「素材管理」骨架(素材/规格模型、分片直传与秒传、送审登记、效果回流、阈值同源、pgvector 预研),`素材工坊`(`sucaigongfang`,本地目录、不入库)提供**图片批量生产的唯一实现**(PSD 图层约定、ONNX 抠图、canvas 合成、批次与并发控制)。图片线的工期与风险不再按"净新增"评估,而是按"对齐素材工坊实现 + 适配本项目资产与规格模型"评估(见 R-T-17~R-T-19)。
---
## 修订记录
| **版本** | **日期** | **修订内容** |
| --- | --- | --- |
| V0.1 | 2026-11-02 | 初稿:技术选型、总体架构、部署、数据模型、接口、关键链路、10 条关键决策、非功能落地、交付路径、风险与待确认 |
| V0.1.1 | 2026-11-02 | 基础设施版本校准:数据库统一 PostgreSQL 18;Node.js 锁定 24.15.0;依赖统一 pnpm(禁用 npm / yarn,`packageManager` 锁定 + CI 校验) |
| V0.1.2 | 2026-11-02 | 前端组件库统一为 **Semi Design 2.x**(移除 Ant Design / ProComponents,补 React 19 adapter 与二次封装说明);图标统一 **Remix Icon**,禁止其他图标库与内联 SVG,补 ESLint 白名单与统一出口 |
| V0.1.3 | 2026-11-02 | 数据层改为**自建**:移除阿里云 RDS / 云 Redis 托管假设,统一 PostgreSQL 18 与 Redis 7.4 容器镜像(开发机 Docker Compose、生产云主机 + 主备),备份改为 pgBackRest / WAL-G 归档到 MinIO;新增 4.1.1 本地开发环境(先 up 依赖再开发);更正 api 镜像为 Node 24.15.0 |
| V0.2 | 2026-11-02 | 吸收参考项目「爆量魔方」:AI 接入层补四层配置与方言抽象、表清单补 `ai_models` / `ai_task_mappings` / `ai_call_logs` / `ai_configs` 与脚本编号、模板库、审核、交付表;脚本端点补编号 / 版本 diff / 审核链接 / 交付包;7.2 补生成上下文与 RAG 三级降级;7.7 补配置合并与健壮性;新增 R-T-13 / R-T-14、附录 F 复用矩阵;明确图片与视频渲染无先例、按自研评估 |
| V0.4 | 2026-11-02 | 图片线改为确定性流水线(依据素材工坊 `sucaigongfang` 实测实现):`image:matting` / `image:render` / `delivery:zip` 三队列、PSD 图层命名约定与解析入库校验、ONNX 抠图与低质量降级、canvas 合成与字体自检、批次参数与命名快照、模板与批次表(`templates` / `template_versions` / `production_batches` / `production_tasks`);3.4 / 7.3 重写、新增 8.11 D-11、R-T-17~R-T-19、附录 F 补素材工坊行;需求基线对齐 PRD V1.6 |
| V0.3 | 2026-11-02 | 吸收 baoliangmofang:素材为主对象 + 规格为属性(`materials` / `material_versions` / `material_specs` / `material_files` / `material_links` / `upload_sessions`);分片直传 + hash 秒传 + 短时效签名 URL + `StorageProvider` 接口(2.5 / 7.9);送审与外部提审登记、分享链接安全口径(`share_links`);投放登记与日指标(`placements` / `material_metrics_daily`)、阈值同源与派生指标不落库(5.7);新增 `media:post` / `embed:index` 队列、素材端点 13 个、3.6 素材与送审状态机、7.9 入库转码链路、R-T-15 / R-T-16、附录 F 补 6 行;向量检索改为「M3 起 pgvector、访问收敛 VectorStore」;需求基线对齐 PRD V1.5 |
| V0.1.4 | 2026-11-02 | React 19 的 Semi 包名改为 **`@douyinfe/semi-ui-19`**(禁与 `@douyinfe/semi-ui` 混装,ESLint 强制);明确入口仍需 `import '@douyinfe/semi-ui-19/react19-adapter'`(React 19 已移除 `react-dom` 的 `createRoot` 导出) |
## 待办
- [x] 技术栈与部署形态确认(TS 全栈 / 公有云 / MinIO / 无 GPU)
- [x] 与 PRD V1.3 的章节追溯(附录 B)
- [x] 基础设施版本锁定(PostgreSQL 18 / Node.js 24.15.0 / pnpm 10)
- [x] 阅读参考项目「爆量魔方」并把可复用能力整理进本文(附录 F)
- [x] 阅读参考项目「baoliangmofang」素材/存储链路并整理进本文(附录 F、7.9)
- [ ] 两个参考仓库均无图片出图实现:出图链路(即梦 / 万相 / 自建服务)待用户提供或按 7.3 自研
- [ ] 评审后按意见更新 V0.3
- [ ] 补 1.5.1 竞品技术形态参考(可选,非必要)
- [ ] QT-01~QT-14 确认后回填(云厂商、SSO、供应商、预算、备份指标)
- [ ] 输出运维手册(部署、备份恢复、故障处理)——建议 T1 前完成