Files
lingxi/docs/技术架构/灵犀_信息流素材生产平台_技术架构_V0.1.md
T
Stev_Wang 02c716d6f2 docs(plan): A4 实施报告与计划回写
背景:A4 开发完成,需要回写实施报告、技术架构与开发计划状态。\n改动:新增 A4 实施报告;技术架构补 system_config_versions 与 dict:manage 口径;开发计划 A4 标记完成并回写门禁/遗留;README 索引补 A4 报告。\n影响面:仅文档;不改变需求与技术边界。\n回滚:回滚本提交即可。\n验证:prettier 通过;文档链接已检查。
2026-11-04 11:50:30 +08:00

147 KiB
Raw Blame History

灵犀 · 信息流素材生产平台 技术架构设计

项 内容
文档版本 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 一页架构图

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 / system_config_versions / 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 状态机

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 四桶
# 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 部署拓扑

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 核心实体关系

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 系统配置(命名、导出、通知) 带版本与变更审计
平台 system_config_versions 系统配置变更历史(append-only:前后值 / 原因 / 操作人) A4 新增;配置版本与审计可查
平台 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 鉴权与权限校验链

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:manage 字典(A2 定稿的 30 个权限点中字典为单个 dict:manage,见 ADR-001)
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 登录与权限上下文

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 脚本批量生成与入库

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。

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 视频逐镜生成与拼接

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 预览、勾选、命名与导出

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 行业包从配置到发布

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 路由、限流、降级与记账

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)在这一段有可用设计,但其自身没有出图实现,本节是「入库与管理」而非「生成」。

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)

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 导出)
V0.4(A4 回写) 2026-11-04 A4 数据字典与系统配置:新增 system_config_versions(配置变更前后值 / 版本 / 审计);字典权限按 A2 定稿统一为 dict:manage(ADR-001);明确字典通配表 dict_types / dict_items 与三层合并、R-10 快照口径

待办

  • 技术栈与部署形态确认(TS 全栈 / 公有云 / MinIO / 无 GPU)
  • 与 PRD V1.3 的章节追溯(附录 B)
  • 基础设施版本锁定(PostgreSQL 18 / Node.js 24.15.0 / pnpm 10)
  • 阅读参考项目「爆量魔方」并把可复用能力整理进本文(附录 F)
  • 阅读参考项目「baoliangmofang」素材/存储链路并整理进本文(附录 F、7.9)
  • 两个参考仓库均无图片出图实现:出图链路(即梦 / 万相 / 自建服务)待用户提供或按 7.3 自研
  • 评审后按意见更新 V0.3
  • 补 1.5.1 竞品技术形态参考(可选,非必要)
  • QT-01~QT-14 确认后回填(云厂商、SSO、供应商、预算、备份指标)
  • 输出运维手册(部署、备份恢复、故障处理)——建议 T1 前完成