背景:A3 需把行业 / 客户 / 客户组 / 客户级字段立为资产根节点;用户确认两级行业树与混搭 UI(行业 B / 客户列表 A / 详情 A / 新建 B / 客户组 B / 字段 A)。 改动: - 新增 ADR-002,行业改为两级树,客户挂末级行业;同步 PRD / 技术架构 / 开发计划。 - 新增 client 模块:industries / customers / customer-groups / customer-fields 接口、真实 PrismaCustomerDirectory、客户状态机与客户转交、客户组数据范围解析、跨客户 404。 - 新增迁移与幂等种子(电商/金融两级行业、2 客户、1 客户组、4 条字段)。 - 前端新增行业/客户/客户组/字段审批页面与路由、导航更名(客户管理 / 行业管理 / 客户组),复用现有 token。 - 补充 shared 契约单测、client 集成用例、A3 浏览器 e2e。 影响面:client 模块与 client 表、shared 契约、web 路由/导航、ScopeContext 数据范围、OpenAPI 与种子;A1 客户转交占位由 client 实现替换。 回滚:回滚本提交;数据库保留新增表或按报告执行 down SQL;CUSTOMER_DIRECTORY 可切回空集实现。 验证:format:check / lint / typecheck / test 全绿(shared 131 / api 90 / web 125,共 346);test:int 5 文件 39 条;A3 e2e 1/1;build 8/8;真实浏览器六页验证通过。
2019 lines
147 KiB
Markdown
2019 lines
147 KiB
Markdown
# 灵犀 · 信息流素材生产平台 技术架构设计
|
||
|
||
| **项** | **内容** |
|
||
| --- | --- |
|
||
| 文档版本 | 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}` | `customer:read` / `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 前完成
|