# 系统架构设计 ## 1. 总体选择 采用 **TypeScript 模块化单体 API + 独立任务进程 + 托管基础设施**。首版不引入 Kafka、服务网格、Kubernetes 或复杂微服务。所有 AI 推理在管理员配置的远程聚合平台进行;本机只承担业务和媒体处理。 后端采用按功能模块组织的 **MVC**:Controller 处理 HTTP;Service 与 Prisma 模型承载业务及持久化;View 使用 JSON 响应 DTO,而不是服务端 HTML 模板。优先使用 Nest 官方模块和成熟库,不重复实现路由、依赖注入、配置、校验、OpenAPI、健康检查或进程守护;不额外建设通用 Repository、DDD 聚合和多层转发。 当前只落地 `apps/api` 基础架子与根目录 PM2 配置;数据库 schema 不建占位业务表。下文业务模块、Worker、队列与生产闭环仍待逐项实施。 ### 组件职责表 | 组件 | 负责 | 不负责 | | --- | --- | --- | | Web | 创作与后台界面、编辑、预览、报价确认、状态订阅 | 平台密钥、直接调用上游、最终计费 | | API | 身份、权限、项目、规划报价、钱包、渠道、支付回调、OpenAPI | 维持几十分钟 HTTP 请求等待视频完成 | | AI Submit Worker | 提交权认领、协议转换、调用与句柄登记 | 不确定结果时盲目重发 | | Poll Worker | 查询同一任务、回调补偿、超时核对 | 更换上游重新生成 | | Ingest Worker | 安全下载、文件校验、转存、缩略图与元数据 | 因下载失败要求上游重新生成 | | Render Worker | 受控 FFmpeg 合成、预览和输出校验 | 执行用户提供的 shell 或任意 FFmpeg 表达式 | | PostgreSQL | 业务与资金事实、版本、执行身份、Outbox | 当作大文件存储 | | Redis / BullMQ | 调度、速率与容量协调、短缓存 | 唯一账本或唯一任务事实 | | 七牛云 Kodo | 私有输入、不可变产物、预览和导出 | 以公开空间或可猜测链接代替权限体系 | 不使用 ASCII 架构图;关系用上述组件表、后面的调用步骤和有类型的契约表达。 ## 2. 技术栈与选择理由 | 领域 | 主方案 | 替代/边界 | | --- | --- | --- | | 运行时 | 开工时仍在支持期内的 Node.js LTS,锁定具体版本 | 不让开发机与 Worker 随意漂移 | | 包管理 | pnpm workspace,单仓库 | 不把 14 个参考仓库直接变成运行依赖 | | Web | Next.js / React / TypeScript | 服务端渲染用于账户与页面壳;时间线与预览在客户端 | | UI | Tailwind、可访问组件、Lucide 类图标库 | 图标库和组件许可在实施时核验 | | API | NestJS(使用默认 HTTP 适配器) | 请求管道、模块、角色权限、OpenAPI;不同时重复建设 tRPC 公共契约 | | 校验 | Zod 共享业务输入 schema,服务端权威校验 | 客户端校验只是体验,不是权限边界 | | ORM | Prisma + PostgreSQL | 行锁、复杂唯一约束与账本事务允许参数化 SQL | | 队列 | BullMQ + Redis | 延迟轮询任务不占用昂贵工作线程 | | 存储 | 七牛云 Kodo,服务端使用官方 Node.js SDK | 复用现有 `QINIU_*` 配置;浏览器不决定对象 key 或公开权限 | | 渲染 | FFmpeg、FFprobe,TS 包装受控模板 | Remotion 可作为后续图文包装方案,先审商业许可与资源成本 | | 进程管理 | PM2,API 先采用单进程 fork | 使用现成重启、日志与退出管理;不将进程重启当成任务恢复 | | 可观测 | 结构化日志、OpenTelemetry、指标与告警 | 不记录敏感原始提示词、密钥或签名 URL | 具体框架版本在开发启动时结合安全公告选择并锁定;本文不假称已经完成兼容性联调。 ## 3. 建议工程边界 | 路径(拟建) | 内容 | | --- | --- | | `apps/web` | 用户端与后台路由、组件、页面状态 | | `apps/api` | NestJS HTTP 入口、模块装配和鉴权 | | `apps/worker` | 同一领域服务的后台进程入口 | | `packages/contracts` | DTO、schema、错误码、事件契约 | | `packages/domain` | 生产计划、版本、钱包等核心业务规则 | | `packages/database` | Prisma、迁移、事务与受控持久化 | | `packages/aiGateway` | 适配器、能力校验、路由与调用身份 | | `packages/media` | 安全媒体处理、探测、转存与合成 | | `packages/ui` | 基础组件和设计令牌 | | `packages/observability` | 日志脱敏、trace 和指标封装 | 这些是逻辑边界,不要求第一天创建所有空包。只为真实复用拆包,不搭没有消费者的抽象层。 ### 依赖规则 - 页面只通过自有 API 客户端调用平台;不能 import 上游 SDK。 - 业务领域声明需要的能力和输入;不能写供应商域名、凭据解析或厂商特判。 - 适配器只处理传输协议和结果归一;不能修改余额、发布模型、变更项目版本。 - Worker 与 HTTP 使用同一套价格、权限和领域服务;不能维护第二套“后台便捷逻辑”。 - 钱包只接受合法业务凭证,通过账本服务写入;任务表更新不能直接加减余额。 ## 4. 一个付费请求如何经过系统 1. Web 请求创建计划;API 解析当前工作区,验证资源所有权、输入版本、参数和渠道。 2. 价格引擎按规范化参数生成 Quote,与 Plan 绑定并设置期限。 3. 用户确认,API 在事务中锁定钱包、校验幂等身份、冻结资金、创建 Execution/Task 和 Outbox。 4. Outbox dispatcher 至少一次投递 BullMQ;Worker 先以数据库条件更新认领。 5. Worker 建立不可变 ProviderAttempt,冻结渠道/模型/协议/凭据版本,认领唯一 submit fence。 6. 适配器发出远程请求;同步结果进入 Ingest,异步句柄先落库再调度 Poll。 7. Ingest 以幂等对象标识转存、探测、校验,登记 AssetVersion。 8. 同一业务完成事务生成结算请求并提交事件;钱包独立幂等结算,失败进入 `settlementPending`。 9. 客户端收到 SSE 更新,必要时读取快照;断开不会改变任务执行。 数据库与远程 AI 之间不存在分布式 ACID。目标是**外部提交可追踪、账本效果幂等**,不是虚假的“跨网络 exactly once”。 ## 5. 多用户隔离 所有项目、资产、任务、计划与钱包都属于 Workspace。HTTP 的工作区来自登录用户和合法 Membership,不能直接信任客户端提交的 `workspaceId`。对象引用使用复合归属校验,防止“自己的 Task 引用了别人的 Asset”。 P0 使用共享数据库、共享 schema,显式工作区约束;可追加 PostgreSQL RLS 作为第二道防线,但连接池与事务上下文必须实测,不能只启用 RLS 后就认为隔离成立。后台跨租户查询走独立受审计入口。 ## 6. 缓存与配置发布 ### 数据库是事实源 管理员发布产生新的 ChannelRevision 和 CatalogRevision。API 与 Worker 缓存只保存非敏感快照引用,键包含版本。发布成功后发出失效事件,缓存失联时按版本从数据库读取,不能回到源码默认地址。 已有 Execution 使用其冻结版本;新配置只影响新计划。禁用渠道默认阻止新提交,但允许已有 ProviderJob 查询和转存;紧急封禁或密钥泄漏使用独立 `revoked` 语义,并进入人工核对。 ### 不缓存什么 钱包可用余额不能由 Redis 数字独立决定;授权判断不能依赖过期缓存;凭据明文不进入持久缓存或浏览器;私有素材预览需有过期与撤销机制。 ## 7. 事件与状态 - REST 提供完整当前快照,数据库中的对象状态是权威。 - SSE 只提供有序增量通知和对象版本号,支持断点游标。 - 事件过期或出现版本间隙时返回 `resyncRequired`,客户端重取快照。 - 客户端禁止根据“百分比到 100”推导成功;成功必须由已登记结果和任务终态给出。 - 同一 Task 完成可多次通知,但消费方通过事件 ID 和对象版本幂等处理。 ## 8. 存储与资产生命周期 对象存储已确定使用**七牛云 Kodo**。API 和需要文件访问的 Worker 通过服务端存储封装调用七牛官方 Node.js SDK,不要求另建其他对象存储服务。用户已准备好六项 `QINIU_*` 环境配置,含义见 [安全运维第 9 节](securityAndOperations.md#9-配置边界);本轮不读取实际配置值、不创建空间或域名、不进行上传下载联调。 上传分 `uploadIntent`、直传、服务端校验、登记可用四步。API 验证工作区与配额后,为指定 Bucket 和唯一 objectKey 签发短期、限制大小及类型的七牛上传凭证;浏览器凭此直传,不获得服务端 AK/SK 配置。生成结果和渲染成片由 Worker 上传到同一存储体系。所有文件先处于暂存/待校验状态,验证 MIME、尺寸、真实解码、时长、SHA-256 后发布为正式版本;七牛返回的文件 hash 不能直接当作 SHA-256。 objectKey 由服务端统一生成,以规范化的 `QINIU_KEY_PREFIX` 为应用前缀,再加入 `workspaces//` 与随机对象身份;前缀显式为空时仍保留工作区隔离路径。不使用原始文件名、密钥或用户邮箱,不允许客户端指定任意 key,上传凭证默认禁止覆盖已有对象。重复入库复用同一结果身份并检查内容,不覆盖历史资产版本。 引用记录保存 AssetVersion ID 与摘要,不保存临时签名 URL 作为业务身份。需要上游下载时生成短时、范围受限的媒体访问凭证;有效期覆盖真实取图时机,过期按同一素材身份续签,不能因此生成第二份视频。 `QINIU_PUBLIC_DOMAIN` 是已配置的文件访问域名或 CDN 入口,**变量名中的 PUBLIC 不代表用户素材默认公开**。预览、下载和上游取图均先校验用途及归属,再通过该域名生成短期受控链接;私有素材要求七牛空间与实际访问链路具备访问控制。签名规则须与实际直连/CDN 配置匹配,不能假定同一种签名适用所有链路,也不能退回永久裸链。数据库保存七牛 Bucket 与 objectKey,不将域名拼接后的 URL 作为资产身份。 删除采用逻辑删除、引用检查、保留窗口、物理清理;账本和法定记录不随项目删除。对象存储与数据库间用清理任务协调,不通过请求内连锁删除大批素材。 ## 9. 渲染架构 浏览器预览只是近似视图,导出以服务端 TimelineVersion 为准。时间使用整数时间单位,最终量化到输出帧与音频采样,不用 JS 浮点累计拼接时长。FFmpeg 参数由受控模板产生,文件路径限于任务目录,采用 `spawn` 参数数组而非 shell 拼接。 Render Worker 设置 CPU、内存、输出时长、磁盘和运行期限,隔离恶意文件。渲染重试复用同一输入和幂等结果位置;渲染失败不重新调用 AI。新的导出档位是新业务操作,是否收费由透明产品策略决定。 ## 10. 扩展顺序 先按模态分 Worker 池、再扩 API 副本,最后才拆真正瓶颈模块。远程任务等待不应与 CPU 渲染共用容量;用户公平性与渠道并发双重限制。 若复杂跨天工作流和人工审批的数量超出当前实现维护能力,再评估持久工作流引擎;不能为了“技术先进”首版就加入 Temporal 及第二套任务事实。 ## 11. 架构验收 - API 重启不影响已受理的上游生成身份。 - Redis 丢失后,数据库仍能识别待投递、待轮询和待核对任务。 - 两个 Worker 同时领取同一 Task,只能一方获得提交权。 - 渠道改地址后,旧任务不拿新地址查询旧 ID。 - 多租户测试覆盖所有关联资源与签名下载入口。 - 所有付费入口,包括 AI 助手工具调用,都走同一个 Quote/Execution/Wallet 边界。