# 数据库与数据契约设计 > PostgreSQL 为权威数据源,Prisma 用于日常访问,关键财务事务与复杂约束使用参数化 SQL。本文为逻辑设计,不是已执行迁移。当前基础工程不创建占位业务表,下面的字段按实际功能评审后逐项落地,不整表照抄。 ## 1. 通用规范 - 主键采用不可猜测的 UUID;按实际审计与并发需求添加 `createdAt`、`updatedAt`、乐观锁 `version`,不为全部表机械添加同一组字段。 - 字段只保存有明确用途的事实,不重复保存可直接推导的名称、数量、标志或可重建展示内容;确需快照、索引投影或账本余额投影时,明确权威来源与一致性规则。工作区隔离、不可变执行身份和财务追溯所需字段不作为无意义冗余删除。 - 时间为 UTC `timestamptz`;前端按用户时区显示。 - 业务 API 字段用 camelCase;SQL 表与列使用一致的映射规范,迁移中显式声明。 - 金额/积分不能用 JS `number` 或 PostgreSQL 浮点;整数用 BigInt、精确价格用 Decimal,JSON 使用十进制字符串。 - 生产内容表显式有 `workspaceId`;平台全局配置表不伪造工作区归属。 - 引用需要归属一致,用复合外键或同事务完整检查,不能只检查两个 UUID 都存在。 - 资产内容不可变,用户可变的是“当前选中版本”;账本只追加,不能用更新覆盖历史金额。 - JSONB 仅用于有 schemaVersion 的参数、快照和协议扩展;权限、余额、所有权、幂等身份和关键状态不能藏在自由 JSON 中。 ## 2. 身份与工作区 | 表/实体 | 核心字段 | 约束与索引 | | --- | --- | --- | | User | id、emailNormalized、status、emailVerifiedAt、passwordHash(如采用密码)、lastLoginAt | 邮箱规范化唯一;封禁不删除资金事实 | | Session | id、userId、tokenHash、expiresAt、revokedAt、deviceSummary | 只存会话凭证 hash;按用户与有效期索引 | | Workspace | id、ownerUserId、name、status、storageQuotaBytes | P0 个人工作区,创建幂等,所有者变更不开放 | | Membership | workspaceId、userId、role、status | `(workspaceId,userId)` 唯一;P0 owner,P1 editor/viewer | | AdminRoleAssignment | userId、role、grantedBy、expiresAt | 平台管理员与工作区角色分开 | | AdminAuditEvent | actorId、action、targetType、targetId、requestId、redactedDiff、reason | 只追加,按目标/操作者/时间索引;不写凭据值 | 身份认证已选定为本项目独立的邮箱、密码与 JWT:邮箱规范化唯一、密码使用随机盐 scrypt 哈希,`AuthSession` 支持 JWT 撤销与到期检查。原外部身份字段仅为兼容旧数据保留;当前未实现邮箱验证、找回密码或 MFA。实际模型与启用方式见 `apps/api/prisma/schema.prisma` 和后端 README;业务表不绑定第三方支付或 OAuth 平台。 ## 3. 项目与故事内容 | 表/实体 | 核心字段 | 约束与索引 | | --- | --- | --- | | Project | workspaceId、title、language、aspectRatio、style、status、currentBibleVersionId、deletedAt | 工作区列表索引;默认私有 | | ProjectBibleVersion | workspaceId、projectId、revision、worldRules、characterRelations、styleRules、contentHash、confirmedAt | `(projectId,revision)` 唯一;确认内容不可变 | | Episode | workspaceId、projectId、orderKey、title、currentScriptVersionId、stage | 集顺序稳定,重排不改 ID | | ScriptVersion | workspaceId、episodeId、revision、content、schemaVersion、contentHash、sourceExecutionId、confirmedBy、confirmedAt | 版本只追加;当前指针属于 Episode | | Shot | workspaceId、episodeId、orderKey、currentShotVersionId、confirmationStatus | 只表示准备确认,不塞生成状态 | | ShotVersion | workspaceId、shotId、revision、scriptVersionId、description、camera、durationMs、dialogue、contentHash | 输入不可变;时长是内容意图,不代替上游计价规范 | | DialogueLine | workspaceId、shotVersionId、characterAssetId、text、language、orderKey | 分段对白与选用音频版本可关联 | | DependencyEdge | workspaceId、sourceType/Id/version、targetType/Id/version、role | 校验相同归属;支持计算 stale,不因更新直接删除旧产物 | 剧本内容可以同时保存结构化 JSON 和展示文本,但只有一个 canonical 来源,另一份是可重建投影;不能让两份各自编辑后分叉。 ## 4. 资产与版本 | 表/实体 | 核心字段 | 约束与索引 | | --- | --- | --- | | Asset | workspaceId、projectId、kind、name、currentVersionId、status、deletedAt | kind 为 character/scene/prop/image/video/audio 等 | | AssetVersion | workspaceId、assetId、revision、storageObjectId、sourceExecutionId、inputHash、metadata、rightsStatus、status | `(assetId,revision)` 唯一;同一输出身份幂等登记 | | StorageObject | workspaceId、storageProvider、bucket、objectKey、sha256、bytes、mime、width、height、durationMs、storageState | storageProvider 为 qiniu;`(storageProvider,bucket,objectKey)` 唯一;服务端检测真实媒体;不保存永久公开 URL | | UploadIntent | workspaceId、ownerId、storageProvider、bucket、objectKey、expectedLimits、expiresAt、completedAt | 固化七牛上传目标;受限直传;完成后服务端校验,不信任前端声明 | | ShotReference | workspaceId、shotVersionId、assetVersionId、role、sortIndex | role 显式为 firstFrame/lastFrame/referenceImage/referenceAudio/referenceVideo 等 | | VoiceSelection | workspaceId、characterAssetId、voiceProductId、parameters、consentRecordId | 不能含上游平台 key;自定义音色需要同意记录 | | ConsentRecord | workspaceId、subject、scope、proofObjectId、grantedAt、revokedAt | P0 无真人克隆时可后置实体实现,但不能开放无记录克隆 | | TimelineVersion | workspaceId、episodeId、revision、tracks、schemaVersion、contentHash、confirmedAt | tracks 引用明确 AssetVersion;内容只追加 | | ExportVersion | workspaceId、timelineVersionId、renderExecutionId、storageObjectId、preset、status | 导出请求幂等,时间线修改不影响进行中的导出 | 视频与音频实际时长用服务端探测值。用户希望 5 秒,不代表远程结果确实 5 秒;两种值都保存并明确用途。用户生成结果是否符合约定,与供应商用量事实也应分开。 七牛对象创建时将实际 `QINIU_BUCKET` 和含 `QINIU_KEY_PREFIX` 的完整 objectKey 固化到 StorageObject;前缀或默认空间变更不改写历史记录。访问域名取受控部署配置,预览/下载按资源权限临时签名,不存储 AK/SK 或签名 URL;更换域名须保持对应空间与鉴权策略一致。`sha256` 由平台独立计算,不能直接使用七牛返回的文件 hash 代替。 ## 5. 平台渠道与模型(全局表) | 表/实体 | 核心字段 | 约束与索引 | | --- | --- | --- | | Channel | id、name、status、currentRevisionId、createdBy | 初始 0 行;软停用优先,活动任务存在时不硬删 | | ChannelRevision | channelId、revision、baseUrl、protocolProfileId、protocolVersion、timeoutPolicy、rateLimits、credentialVersionId、publishedAt、configHash | 已发布不可变;baseUrl 无 DB DEFAULT,草稿允许 NULL,发布要求非空 | | CredentialVersion | channelId、ciphertext、nonce、authTag、masterKeyVersion、fingerprint、status、revokedAt | 初始 0 行;不在普通 DTO 返回密文;引用用 RESTRICT | | EndpointConfig | channelRevisionId、capability、relativePathConfig、transportConfig、configHash | 初始 0 行;禁止绝对 URL 旁路与执行脚本 | | ProductModel | id、publicName、description、modality、status、currentCatalogRevisionId | 只向用户投影非敏感目录 | | ProductModelRevision | productModelId、revision、capabilitySchema、parameterSchema、publishedAt | 与报价的模型版本绑定 | | ModelBinding | productModelRevisionId、channelRevisionId、upstreamModelId、capability、priority、enabled | 精确 binding;不靠模型名猜能力 | | CapabilityVerification | bindingId、configHash、checkKind、result、checkedAt、expiresAt、receiptSummary、approvedTestBudget | 验证随配置变更失效,付费检测要记录授权 | | PriceRevision | productModelRevisionId、revision、unit、retailMicrocreditRules、costCurrency、costRules、validFrom、publishedAt | 精确单位与档位;有效模型必须有价 | ProductModel 可在管理员导入后创建;不为了让页面热闹预置可调用模型。无连接的协议 schema 可以随代码发布,但它们不是有地址或凭据的渠道实例。 ### 配置历史与删除 Execution/Attempt 引用不可变发布版本;配置旧版本在活动任务与审计保留窗口内不可删除。管理员“删除渠道”通常是停止新计划并隐藏列表,而非 cascade 删除凭据与旧请求身份。 ## 6. 计划、执行与任务 | 表/实体 | 核心字段 | 约束与索引 | | --- | --- | --- | | PlanSnapshot | workspaceId、projectId、operation、canonicalInput、inputHash、resourceVersionRefs、modelRevisionId、allowedBindings、schemaVersion、createdBy | 创建后不可变;不含明文凭据/临时签名 URL | | Quote | workspaceId、planId、priceRevisionId、maximumMicrocredits、lineItems、expiresAt、status | 同一快照的报价有版本;用户批准确切 ID | | Grant | workspaceId、quoteId、planHash、approvedBy、scope、capMicrocredits、expiresAt、revokedAt | P0 精确 quote;P1 范围预算也必须有边界 | | Execution | workspaceId、projectId、planId、quoteId、grantId、requestKey、requestHash、status | `(workspaceId,operation,requestKey)` 唯一;不同 hash 冲突 | | BatchItem | workspaceId、executionId、itemKey、planFragmentHash、status、allocatedReserve | `(executionId,itemKey)` 唯一,成功项可独立查询 | | Task | workspaceId、executionId、itemKey、kind、phase、status、ownerId、leaseToken、leaseExpiresAt、heartbeatAt、checkpointVersion、nextRunAt | `(executionId,itemKey,kind)` 唯一;按状态/调度时间索引 | | ProviderAttempt | workspaceId、taskId、invocationKey、bindingId、channelRevisionId、credentialVersionId、requestHash、submissionState、submitFenceAt、failureCode | `(taskId,invocationKey)` 唯一;提交权条件更新 | | ProviderJob | workspaceId、attemptId、externalId、providerState、submittedAt、lastCheckedAt、deadlineAt、resultLocator、protocolCheckpoint | attemptId 唯一;external ID 按渠道命名空间去重,不假设全局唯一 | | UsageFact | workspaceId、attemptId、upstreamUsageId、unit、quantityDecimal、currencyCost、evidenceStatus | 用量身份唯一;估算和实测分开 | | OutboxEvent | id、aggregateType/Id、eventType、payload、publishedAt、attemptCount | 与业务提交同事务,重复发布消费者幂等 | | InboxReceipt | source、eventId、bodyHash、processedAt、result | `(source,eventId)` 唯一;支付与供应商回调分命名空间 | | TaskEvent | workspaceId、taskId、sequence、type、objectVersion、safePayload | `(taskId,sequence)` 唯一,可重放且有保留策略 | | ReconciliationCase | workspaceId、taskId/attemptId、reason、evidenceRefs、ownerId、status、resolution、resolvedBy | 不把未知副作用压成普通失败;人工操作审计 | ProviderJob 的结果定位可能含短期 URL,只能加密/受限保存并脱敏展示,不能拿它作为永久资产 URL。TaskEvent 是展示事实的投影,不能代替业务表或资金账本。 ## 7. 积分与支付 统一内部单位:**1 积分 = 1,000,000 microcredits**。所有余额和资金变动用有符号 64 位整数;API 使用字符串传输。充值法币使用最小货币单位和显式币种,绝不与积分字段混用。 | 表/实体 | 核心字段 | 约束与索引 | | --- | --- | --- | | Wallet | workspaceId、currencyUnit、status、version | 工作区唯一钱包;P0 单积分单位 | | LedgerAccount | walletId 或 platformOwner、accountType、balanceMicrocredits、version | userAvailable/userReserved 非负;平台发行账户可为负 | | LedgerTransaction | businessKey、kind、workspaceId、executionId/paymentOrderId、createdBy、reason、createdAt | businessKey 唯一,只追加 | | LedgerEntry | transactionId、accountId、deltaMicrocredits | 每笔交易分录和为 0;事务内过账 | | Reservation | workspaceId、executionId、itemKey、originalAmount、remainingAmount、settledAmount、releasedAmount、status | 稳定业务唯一 key;余额分解守恒 | | Settlement | workspaceId、reservationId、usageFactRefs、priceRevisionId、actualAmount、businessKey、status | 同一结算身份唯一,不能更换历史价格 | | PaymentOrder | workspaceId、merchantOrderNo、provider、currency、amountMinor、creditsToIssue、status、providerPaymentId、expiresAt | 商户单号唯一,上游付款身份唯一;套餐快照固化 | | PaymentEvent | provider、eventId、payloadHash、verificationStatus、processedAt | 事件唯一,原文按敏感策略处理 | | RefundOrder | paymentOrderId、reason、cashAmountMinor、creditsToReverse、providerRefundId、status、approvedBy | 现金退款与积分回收分开,禁止负数冲突 | | CostReconciliation | attemptId、usageFactId、providerStatementRef、costAmount、difference、status | 上游成本与用户售价分开,不对普通用户公开 | ### 必须维持的不变量 1. 单笔 LedgerTransaction 的全部 `deltaMicrocredits` 相加等于 0。 2. 用户可用和冻结账户不能为负,所有相关账户按稳定 ID 顺序加行锁。 3. Reservation 的 `originalAmount = remainingAmount + settledAmount + releasedAmount`;有追加额度时必须由新用户授权与明确调整交易形成新原始额度事实。 4. 不以 `Task.status` 推导是否已扣费;以 Settlement 和 Ledger 为准。 5. 请求重放返回同一 Execution;账本 businessKey 冲突必须比较原始业务 hash。 6. 管理员补偿、退款与人工调账都产生新交易,绝不 UPDATE 历史 entry。 “分录和为零”涉及跨行,不能只靠单行 CHECK 约束。统一过账服务、受控数据库权限和延迟约束触发器/等价事务校验共同保证;上线前必须验证并发和失败窗口。 ## 8. 索引、约束与大表策略 - 列表查询优先 `(workspaceId,createdAt,id)` 游标,避免无边界 OFFSET。 - 活跃任务索引 `(status,nextRunAt)`、`(leaseExpiresAt,status)`;按渠道/能力定位在途任务。 - 账本按钱包与时间查询,businessKey 全局唯一;财务查询不得只依赖可重建报表。 - 资产使用 `(workspaceId,projectId,kind,createdAt)`,实际文件 hash 不作为跨租户可见去重接口。 - 回调记录和事件表有保留期,必要时分区;账本、订单和审计按适用法律保留,不与短期任务事件一起清理。 - 合理限制 JSONB 大小;媒体二进制放对象存储,不把 Base64 塞入数据库或日志。 ## 9. 事务与锁顺序 统一锁顺序建议:幂等身份/Execution → Wallet → 按 ID 排序的 LedgerAccount → Reservation → Task/Attempt → 资产发布记录。不同服务遵守同一顺序,降低死锁风险;死锁重试只重试本地事务,不重发远程 POST。 创建执行、冻结资金与 Outbox 必须同事务;账本结算与相应 Reservation 更新同事务;产物存储与数据库不能原子提交,通过幂等对象 ID、暂存/发布状态和补偿清理协调。 ## 10. 初始数据、迁移与回滚 初始数据允许角色/权限、积分单位及没有连接地址的协议定义;首个管理员通过受控 bootstrap 创建,禁止默认账号密码。**渠道、凭据、Base URL、端点连接配置全部零行**。 迁移执行于单独运维步骤,API 启动不抢跑迁移。使用 expand/contract,先兼容再收缩;已发布迁移不可改写。对旧配置升级不能补入“默认官方地址”。备份恢复后先进入保护模式核对正在付费的请求,再开放新生成。 ## 11. 数据保留与删除 项目逻辑删除后不可创建新任务。正在产生付费结果的任务仍需按合同处理和核对,不能删除执行身份导致无法恢复。对象清理前检查版本引用、导出与账务保留;财务身份最小化/匿名化不能破坏必要的合法记录。具体保存期限待首发地区与隐私政策确认。