首版收敛 · 先让一次收费跑通

一池积分,
先把一次收费做好。

团队共用余额。开工前占住这次需要的积分,执行后按有效产出结账。

从团队余额开始
充值:人工改库先给租户余额增加积分
价格:静态默认价先写在代码配置里
账户:只管租户余额所有成员共用可用积分
先看账户 · 一张团队预付卡

所有人,使用同一池积分

“租户”就是一个团队。开工只检查这个团队还剩多少可用积分。

首版已确认
人工改库充值增加租户余额
团队积分池 · 示例
100积分
可用积分 = 余额 − 已冻结积分
小林使用共享余额
小夏使用共享余额
小周使用共享余额
只过一道门:团队可用积分够不够?
余额足够,可以预留

余额不足时直接拒绝这次提交。人工配置保持待提交,Debug 不启动;人工充值后重新提交即可。这里采用首版建议,不增加等待和自动续跑流程。

再跟一次任务 · 开工到收尾

先占住 20,再按结果结账

团队初始可用 100 积分,本次最多需要 20。

点击体验建议流程
生成 2 首歌演示价:每首有效歌曲 10 积分
这一笔任务的过程
准备开工
最多需要 20 积分
先占住积分
防止被其他任务花掉
确认产出
看实际交付几首
结账并松开余量
这一步才正式扣费
任务 A · 歌曲生成还没开始
第 1 首待生成
第 2 首待生成
先占住这次最多需要的 20 积分。
“占住”叫预留,也叫冻结,此时还没有正式消费。
开工后,选择这次任务遇到的结果
这里会留下每一步的记录。
看这 100 积分去了哪里三部分相加,始终是 100
现在还可用
100积分
暂时占住
0积分
本次正式扣除
0积分

积分池里的余额是 100;目前没有任何冻结。

这里演示预留、扣费和释放。释放的是尚未消费的冻结额;已扣积分纠错返还不在首版范围。真实单价仍需填写。

最后看账 · 为什么扣这些积分

记录得细,账单看得懂

沿用上面的任务:内部保存详细过程,客户看到约定公开的计费依据。

记录与展示分开

同一次生成,两种视角

账单会跟随上方演示变化
内部用量记录
请求生成2首
有效产出本例计费依据—
未成功产出—
其他执行明细内部执行后记录

按节点保存实际用量和执行尝试。
歌词还可记录 token 与缓存用量。

按公开规则
取计费依据
客户账单等待任务
使用工具歌曲生成
有效歌曲—
示例单价10 积分 / 首
实际扣费—积分
供平台核查和分析,按内部权限访问
展示约定公开的数量、单价、费用和状态
不同工具各定计价规则。若歌词按 token 收费,账单应显示计费 token 数和单价;其他内部明细仍可保留。
首版只提供余额和基本消费明细。客户接口返回计费字段,内部用量不整包公开。
价格先写在代码里

每个收费项目采用静态默认价;计费操作保存当次单价和计算结果。

首版取舍已确认
待定内容 · 只留首版收费必需的决定

产品只需先填完这张收费表

每类节点确定是否收费、按什么单位、默认单价和有效产出标准。其余机制用简单规则收口。

哪些生成收费,成功一份收多少?

产品待确定

不同节点可以采用不同单位。表内实际价格尚未给出;页面里的每首 10 积分仅为演示。

首版节点 还要填的收费规则 成功与用量核对
AI/参考歌词 单位与默认单价 有效歌词数;若按 token 收费,明确 token 口径
Suno 各生成模式 单位与默认单价 有效歌曲数;请求次数不直接当歌曲首数
RVC/SVC、声伴分离 收费或免费;收费时填单位与单价 实际有效音频、请求音轨或可信时长
配置、选择、上传和保存 建议免费 生成步骤收费,父节点不再收一次

建议正式与 Debug 使用同一默认价;生成步骤可靠交付就结算。生成两首但只选一首,仍与只成功一首分别处理。默认价的小数精度和舍入随这张表一并明确。

余额不足:拒绝本次提交

充值后用户再提交。保留已有产物,先不做自动暂停、等待和续跑。

结果查不清:人工核对

先保留冻结;补查到结果再收尾。先不做自动判责、超时补偿和审批台。

单次预算:预先固定

确定最大计费数量或可信费用上限。不做执行中追加预算和换贵模型的交互。

以上为收敛建议。按有效产出收费、系统失败成本由平台承担、部分成功只收有效部分,继续保留。

研发核实 · 参数与结果

报价输入与真正执行的输入一致

默认数量和模型在 Orchestra 明确;预留后才放行。实际产出和所需用量能从 Conductor 读取,缺失值不当作 0。

研发验证 · 并发与重试

余额不会花两次,同一生成不会扣两次

用真实 PG 验证并发预留、重复提交、结算与释放竞争;覆盖放行回包丢失、旧 Debug 被替换和结算中断。

最小架构 · 仍在现有后端内完成

一个计费模块,两张业务表

只为共享余额、一次生成的预留与结算建模。实际单价继续使用静态配置。先看消费身份与职责 → · 再看数据库操作 → · 后端 API 规划 →

职责集中,沿用现有执行通道

Orchestra 掌握最终配置与放行时点,计费模块统一处理余额和本次消费。

现有 Orchestra 入口

人工配置提交
Debug 单步启动

BillingModule

计算默认费用
预留、结算、释放

现有 PostgreSQL

租户余额
本次计费记录

OrchestraConductor生成 worker

用量沿现有 Conductor 输出返回。worker 不新增直连 Orchestra 的计费许可请求。

模块内部怎样组织?

保留纯 TypeScript 的账户、操作及精确积分计算。应用用例负责准备计费、同步结果并收尾;一个 BillingRepository 原子保存两张表。静态价格是明确映射或普通函数。

src/modules/billing/
  domain/models/        CreditAccount、BillableOperation、Points
  domain/ports/         BillingRepository
  application/          准备计费、同步并结算、查询余额和消费
  presentation/http/    查询 Controller 与白名单 DTO
  infrastructure/       Prisma 仓库、Conductor 结果读取
  billing.module.ts     装配并导出需要的用例

human-tasks / canvas-run 调用这些用例,继续负责自己的放行。无需价格仓库、额度聚合、事件总线、通用策略引擎或独立计费服务。

一次消费,由谁负责?

业务模块确定消费身份和输入,执行模块处理运行,BillingModule 负责资金。以下为接入设计,当前业务代码尚未接入计费。

业务入口用户、权限、参数、消费键 执行模块启动、技术重试、可信结果 计费模块预留、结算、释放、消费记录

用户 A 发起一次 Debug

示例身份:租户 T、已登录用户 A、运行 R1、机器步骤 S。Run 保存发起用户和运行快照;消费归属由业务入口确定。

  1. 业务入口生成稳定消费键

    推进 R1 / S 时,业务用例从快照解析并校验输入,再根据这次执行身份生成 operationKey。正式流程同样使用实例、配置任务和生成步骤身份。key 在调用计费前确定,客户端不能自由指定。

  2. 计费按租户和 key 预留一次

    首次调用创建记录 B1,固定 consumerUserId = A,冻结 20。后续以同一 key 调用计费,只返回 B1;回调和技术重试不改消费用户,也不再次冻结。

  3. 执行模块运行,计费收尾原单

    启动、技术重试和运行结果核查沿现有执行通道完成。确定最终结果后,计费结算或释放 B1;结果未知时保留原冻结,继续核查。

多次技术尝试,只结算一笔消费

后端重试启动、worker 技术重试都沿用同一个 key 和 B1,增加的执行成本由平台承担。客户只按原授权范围内最终有效的产出或用量结算一次,不能把所有尝试简单累加收费。

key · 消费身份
operationKey 是业务反复定位同一笔消费的标识;BillingOperation.id 是记录 ID。BillingModule 接收业务提供的 key,按 tenantId + operationKey 去重,不解析节点或参数含义。
hash · 内容摘要
hash 可用于比较参数是否相同。参数能否修改、重复提交如何处理、是否开启新 session,均由来源业务判断。BillingModule 不保存 inputHash,也不负责生成参数冲突报错;参数校验通过后才进入预留。
ref · 关联依据
来源引用让 B1 找到 Run R1 / 步骤 S;usage 中的必要产出 ID 解释费用依据。参数、内容和文件保留在来源模块,调用回执与执行绑定沿用现有执行记录,无须在计费表复制请求或单设 outputRefs 列。
代码接入依据:人工任务与 Debug
正式人工任务

提交人来自登录态。入口检查租户、任务负责人、最新 runtimeTaskId 和输出契约;pending 任务可操作时调用 completeTask,completed 则进入重跑分支。目前生产重跑适配器报 WORKFLOW_RERUN_DEFERRED。

接入点:业务完成参数校验后、调用 completeTask 前预留。是否允许重复提交或重跑仍由人工任务模块判断。

Canvas Debug

Run.createdById 保存发起用户,graphSnapshot 保存运行配置。同一草稿已使用的 sessionId 报 409,新 session 创建新 Run;同一 Run 的恢复沿原快照、runId 和步骤进行。

接入点:resolveInput 并校验后、runtime.start 前预留。捕获启动异常后的技术重试继续复用原消费键。

核对版本:dev 40d5ebf。当前行为与上述计费提案分开,实际 Schema 和业务代码尚未修改。

一次生成,数据库怎样变化?

初始 100 积分,预留 20,每首 10。发起时占额,结果确定后结算;框内标明真实事务边界。

预留提交后,直接调用生成

预留事务
BEGIN → COMMIT

用稳定来源键新建计费记录,保存消费用户、收费类型与当次价格;余额足够时冻结 20。两张表一起提交,不足则全部回滚。

重复调用返回原计费记录,不再次占额;业务用例负责判断后续执行或恢复。

BillingAccount · 租户账户
字段 之前 之后
余额 balance 100 100
冻结 held 0 20
可用 balance − held 100 80
BillingOperation · 本次计费
字段 之前 之后
计费状态 无记录 HELD
原预留 / 已扣 无记录 20 / 未结算
消费用户 无记录 用户 A
查看预留事务 SQL

示意 SQL 仅展示核心字段。重复消费键只返回原计费记录,不再次增加 held。业务用例决定如何响应请求、继续执行或恢复。

BEGIN;  -- READ COMMITTED,一个准备事务
-- 来源模块在这里确认源任务/Run 仍有效,保护并发失效。
INSERT INTO billing_operations
  (id, tenant_id, consumer_user_id, charge_code, operation_key,
   reserved_amount, state, price_snapshot)
VALUES
  (:id, :tenantId, :consumerUserId, :chargeCode, :sourceKey,
   20, 'HELD', :price)
ON CONFLICT (tenant_id, operation_key) DO NOTHING
RETURNING id;
-- INSERT 返回 0 行:读原计费记录后结束事务并返回;
-- 不继续占额;重复业务提交、执行恢复由来源用例处理。
UPDATE billing_accounts
SET held = held + 20
WHERE tenant_id = :tenantId
  AND balance - held >= 20
RETURNING balance, held;
-- 账户更新必须为 1 行,否则整个准备事务 ROLLBACK。
COMMIT;
-- 来源用例确认有效预留后,在事务外按业务协议执行或恢复。
-- 此事务只保证预留幂等,不保证外部执行幂等。
事务外调用 Conductor

按本次已校验的原参数执行 completeTask / runtime.start。网络等待期间不持有数据库事务。

回执未知:计费保留原单与 20 冻结。来源执行模块负责技术重试和结果核查,始终沿用原计费记录。

回执写回事务
BEGIN → COMMIT

由来源模块保存 HumanTask 完成状态或 Debug 执行绑定。BillingOperation 与账户资金均不变。

读取生成结果,收尾原单

在事务外读取 Conductor 的有效产出与用量,再处理下方数据库写入。

一首有效:扣 10,释放余下 10最终余额 90,冻结 0,可用 90。
结果保存事务
BEGIN → COMMIT

将已确认的完整用量及计费用到的产出标识一起保存到 usage。重复结果核对原值。

结算事务
BEGIN → COMMIT

仅当原操作为 HELD 时收尾:改为 SETTLED,记扣费 10;账户 balance 减 10、held 减 20。两张表一起提交。

结果已保存而结算失败:只重试原单结算,不重新生成。

BillingAccount · 租户账户
字段 之前 之后
余额 balance 100 90
冻结 held 20 0
可用 balance − held 80 90
BillingOperation · 本次计费
字段 之前 之后
计费状态 HELD SETTLED
原预留 reservedAmount 20 20
最终扣费 settledAmount 未结算 10

原预留 20 留作历史记录;SETTLED 后已不再占用账户冻结。

查看当前结果的保存与结算 SQL

先固定完整计费依据,再用原单的 HELD 条件收尾;成功转换 1 行才改账户。重复请求读取原结果,金额冲突须核查。

-- 先在事务外读取 Conductor,验证有效产出与必要用量。
BEGIN;  -- 保存这笔操作已接受的完整结果
UPDATE billing_operations
SET usage = :acceptedUsage
WHERE tenant_id = :tenantId AND id = :operationId
  AND state = 'HELD' AND usage IS NULL;
-- 已有依据时读原值核对,不用迟到结果覆盖原依据。
COMMIT;

-- 从已保存的原价和结果计算:1 首 × 10 = 10。
BEGIN;  -- 结算事务
UPDATE billing_operations
SET state = 'SETTLED', settled_amount = 10
WHERE tenant_id = :tenantId AND id = :operationId
  AND state = 'HELD' AND usage IS NOT NULL
  AND reserved_amount >= 10
RETURNING reserved_amount;
-- 更新 0 行:读原终态、校验结果,结束事务并返回;不执行账户 UPDATE。
-- 只有本次收尾成功,才继续使用返回的原预留 H = 20:
UPDATE billing_accounts
SET balance = balance - 10, held = held - 20
WHERE tenant_id = :tenantId
  AND balance >= 10 AND held >= 20;
-- 账户更新必须为 1 行,否则整个结算事务 ROLLBACK。
COMMIT;
同一预留 20 的四种收尾
结果 余额 冻结 操作状态 / 已扣
两首有效 80 0 SETTLED / 20
一首有效 90 0 SETTLED / 10
确认无有效产出 100 0 RELEASED / 0
结果未知 100 20 HELD / 未结算

数值仅为演示。SQL 可展开查看;资金写入统一按“操作 → 账户”,涉及来源生命周期时先保护来源记录。

正式流程和 Debug 在哪个入口接入?

正式流程:completeTask 前

Orchestra 校验并规范化人工配置,预留成功后才完成 Conductor 配置任务,放行后续生成。

Debug:runtime.start 前

先 resolveInput 并校验,按这次实际输入预留,再启动机器步骤。重复推进沿原操作。

当前生成数量、模型等仍可在运行中的人工配置提交时决定。首版沿上述两处控制点逐次预留,先不做整条工作流或批次的集中预算。

中断、重复提交和旧 Debug,最少怎样收尾?

消费身份、技术重试和数据归属见上方职责说明。

业务放行与一次预留

来源模块在同一短事务内确认源任务/Run 有效,唯一键创建计费操作并占用账户冻结。计费只保证相同消费键不重复占额;来源业务用例决定首次启动、技术重试和未知调用核查。Debug 启动尝试复用同一预留,不能每次尝试都新建收费单。

提交与外部调用之间可能中断

预留事务提交后,任务可能尚未调用,也可能已经调用。来源模块查询原任务/correlationId 并保存确定的执行绑定;回包未知保留冻结。不能仅凭查询暂时无结果认定没有执行。

来源失效与恢复

来源失效与准备事务共用源记录保护,顺序为源记录 → 操作 → 账户。失效发生在准备提交前则拒绝预留;已经提交的操作按可能放行核查,不凭旧 Debug/源 Run 终态释放。既有推进器发现来源失效时停止继续启动。

在现有定时机制内补查 HELD 操作。确认结果后只收尾原单;已有结果而结算失败,不重新生成。终态不被迟到消息重开。

Schema:先只新增两张表

账户保存余额,操作保存消费归属、当次价格与计费依据。技术尝试明细可以留存,但不直接累加为客户收费量。

BillingAccount

tenantId · 唯一
一个租户一行

balance / held
实际余额/当前冻结

balance ≥ held ≥ 0
可用 = balance − held

BillingOperation

tenantId + operationKey · 唯一
业务提供消费键,同一笔只预留一次

reservedAmount / settledAmount / state
本次预留、最终扣费与结算状态

consumerUserId
固定本次消费归属,按租户与用户统计已扣积分

priceSnapshot / usage
当次静态单价、实际用量及必要产出标识

chargeCode
稳定的收费项目代码,如 lyrics.generate、music.generate

Tenant ── 1 个账户 ── 多笔计费操作
字段、约束与用量 JSON 草案

拟放在 prisma/schema/billing.prisma,ID 沿用 String / cuid。两表与 Tenant 关联并防止跨租户引用,删除业务来源不级联删除消费记录。审计与软删除字段沿仓库约定,业务唯一键不随软删除释放。

记录 补充字段与约束
BillingAccount tenantId 唯一;balance、held 用 Decimal;CHECK 有限且 balance ≥ held ≥ 0。首次人工充值时创建或增加该租户账户余额。
BillingOperation 来源类型/业务来源 ID、配置 runtimeTaskId 或 runId、logicalNodeId / stepKey、nodeType / nodeVersion、consumerUserId 固定消费归属;稳定来源引用、chargeCode、priceSnapshot、usage、reason;保留预留与结算时间。索引 tenantId / consumerUserId / settledAt、tenantId / chargeCode / settledAt、tenantId / createdAt / id、state / updatedAt。
金额与终态 0 ≤ settledAmount ≤ reservedAmount;HELD 尚未结算,SETTLED 保存最终金额,RELEASED 金额为 0。只允许 HELD 收尾一次;账户冻结等于所有 HELD 预留的合计,由短事务维持。
usage JSON usage 初始为空,取得完整可结算结果后固定本次计费依据;按 Conductor taskId+scheduledTime 区分尝试并去重,在同一 JSON 中保留计费用到的产出标识,不另设 outputRefs 列,不复制生成内容。歌词可记录 token/缓存,歌曲记录请求量与有效首数,音频节点记录所需时长或音轨。首版无须独立 BillingAttempt 表。

金额采用精确十进制,候选存储 Decimal(28,8),按默认价约定统一量化,API 用字符串;不以 JavaScript number 做积分运算。价格快照只保存本次使用的单位、单价及计算依据,不引用可变的当前配置。

首版没有充值接口、充值表或独立资金流水表。人工充值使用余额增量写入,不覆盖其他已提交余额变化,也不修改 held。应用提供当前余额和消费记录;充值历史由首版人工操作自行留存。

收费类型与执行来源分别记录

chargeCode 表示收了什么费用:作词 lyrics.generate、作曲 music.generate。使用稳定字符串,在后端静态价格映射中登记并校验;新增项目不用修改数据库枚举。

sourceType / sourceId 表示从哪里发生,如正式流程步骤或 Debug。nodeType / nodeVersion 只在节点来源时记录。未来的非节点消费也能使用 chargeCode,不必伪装成节点。显示名称随价格快照保存,代码不随名称变更。

按租户与用户统计消费

每笔操作在预留时固定 consumerUserId,人工触发必填。实际消费只汇总 SETTLED 的 settledAmount;HELD 单独显示为占用,RELEASED 不计消费。用户后来被转派或移出租户,不改历史归属。

归属建议:人工配置取实际提交人,Debug 取运行发起人;自动步骤沿业务来源的发起用户,确无用户的系统任务单列。消费用户由服务端确定并校验租户关系,回调和技术重试不改变它。

并发与事务提案

原子条件更新,依靠 PG 行级写锁

首版采用 READ COMMITTED + 条件 UPDATE + 短事务。UPDATE 自动取得目标账户行的写锁,同一行的并发写入可能等待。数据库这一层使用悲观行锁;应用层不采用 version 版本号式乐观锁。

余额是否足够,放进 UPDATE 的 WHERE 条件;检查与占额由数据库在一条语句里完成。当前每次事务处理一笔计费操作和一个账户。

按阶段查看完整数据库动作与事务边界 →

预留时,创建操作与占额一起成功。结算/释放先以 state = HELD 条件改变操作,再原子增减账户;两步任一失败全部回滚。重复结算与释放竞争时,只有一次能改变原操作。资金写入统一按“操作 → 账户”顺序;涉及源生命周期时先锁源记录。

例如可用 100,两个请求都要预留 80,最多一个更新成功。PG 的 UPDATE 仍会取得行锁、可能等待;READ COMMITTED 下等待后重新判断余额条件。行锁一直持有到事务提交或回滚。

依据:PostgreSQL 条件更新与 READ COMMITTED · UPDATE 的行锁行为。这仍是设计提案,真实并发需验证。

首版验证只围绕这三组结果
验证主题 必须成立
余额 100 同时预留两个 80,最多一个成功;不足不放行;手工增量充值不覆盖并发扣费。
同一操作 重复提交只冻结一次;业务重复调用不重复占额;参数和执行状态由来源业务判断;结算与释放竞争只生效一次,部分成功准确释放差额。
异常收尾 回包丢失或技术重试均复用原单,结果未知不释放;旧 Debug 失效阻止新预留,已提交预留继续核查;结算中断只重试原单结算。

后端 API 怎样对接?

前端查询余额和消费;生成仍走现有业务接口,资金动作由后端统一处理。以下为首版接口提案,尚未实现;权限安排也是建议,示例继续使用演示价。

沿用仓库的 REST 契约

统一前缀 /v1,请求携带 Authorization: Bearer 和 X-Tenant-Id。请求、响应字段用 snake_case;成功响应为 { code: 0, message: "success", data }。控制器返回 payload,由全局拦截器包装一次。

金额、单价和计费数量均用十进制字符串,时间用带时区的 ISO 8601。租户取已验证的请求上下文;查询参数不能切换租户。方案依据 现有 REST API 规范。余额示例取刚预留时,消费记录示例取随后部分成功时,两者不是同一时刻的快照。

当前租户余额

GET/v1/billing/accounts/current

有效租户成员可读。返回 balance、held、available,同一次查询读取余额与冻结,可用额由二者计算。

尚无账户时返回三个 "0",GET 不建账户。页面上的余额只是快照,实际放行仍以预留事务为准。

请求与响应示例
GET /v1/billing/accounts/current
Authorization: Bearer <access_token>
X-Tenant-Id: T
{
  "code": 0,
  "message": "success",
  "data": {
    "tenant_id": "T",
    "balance": "100",
    "held": "20",
    "available": "80"
  }
}

消费记录列表

GET/v1/billing/operations

管理员查看当前租户;普通成员仅查看 consumer_user_id 为本人的记录。建议支持状态、用户、charge_code、业务来源与创建时间筛选。

返回收费名称、消费用户、预留额、实际扣费和状态。按 created_at DESC, id DESC 稳定分页,列表与 total 使用相同权限和筛选。

请求与响应示例
GET /v1/billing/operations?page_index=1&page_size=20&state=SETTLED
Authorization: Bearer <access_token>
X-Tenant-Id: T
{
  "code": 0,
  "message": "success",
  "data": {
    "list": [
      {
        "id": "B1",
        "consumer_user_id": "A",
        "charge_code": "music.generate",
        "charge_name": "歌曲生成",
        "state": "SETTLED",
        "reserved_amount": "20",
        "settled_amount": "10",
        "created_at": "2026-10-20T01:00:00.000Z",
        "settled_at": "2026-10-20T01:02:00.000Z",
        "ref": {
          "consumer_user": {
            "id": "A",
            "display_name": "用户 A"
          }
        }
      }
    ],
    "page_index": 1,
    "page_size": 20,
    "total": 1
  }
}

一笔消费的明细

GET/v1/billing/operations/:id

与列表使用相同可见范围,按当前租户和记录 ID 查询。增加公开计费单位、计费数量、当次单价,以及来源 ID 和步骤标识;访问原任务仍遵守原业务权限。

HELD 的实际扣费和未确定计费数量为 null;RELEASED 实际扣费为 "0"。返回白名单 DTO,内部 usage、供应商成本、operationKey 和执行请求均不整包公开。

请求与响应示例
GET /v1/billing/operations/B1
Authorization: Bearer <access_token>
X-Tenant-Id: T
{
  "code": 0,
  "message": "success",
  "data": {
    "id": "B1",
    "consumer_user_id": "A",
    "charge_code": "music.generate",
    "charge_name": "歌曲生成",
    "state": "SETTLED",
    "reserved_amount": "20",
    "settled_amount": "10",
    "created_at": "2026-10-20T01:00:00.000Z",
    "settled_at": "2026-10-20T01:02:00.000Z",
    "ref": {
      "consumer_user": {
        "id": "A",
        "display_name": "用户 A"
      }
    },
    "source_type": "canvas_debug",
    "source_id": "R1",
    "step_key": "S",
    "billing_basis": {
      "unit": "song",
      "quantity": "1",
      "unit_price": "10"
    }
  }
}

按用户汇总消费

GET/v1/billing/user-consumptions

仅当前租户有效 admin/Owner 可读。按 consumer_user_id 分组,在指定结算时间内汇总 SETTLED 的实际扣费;支持用户和收费项目筛选。

total 是用户分组数,summary 汇总全部匹配记录,不只当前页。原用户被移除后保留历史 ID,关联摘要可为 null;无用户的系统消费单列。当前冻结在余额和 HELD 列表中查看。

请求与响应示例
GET /v1/billing/user-consumptions?settled_from=2026-10-01T00:00:00Z&settled_to=2026-11-01T00:00:00Z
Authorization: Bearer <access_token>
X-Tenant-Id: T
{
  "code": 0,
  "message": "success",
  "data": {
    "list": [
      {
        "consumer_user_id": "A",
        "settled_amount": "10",
        "operation_count": 1,
        "ref": {
          "consumer_user": {
            "id": "A",
            "display_name": "用户 A"
          }
        }
      }
    ],
    "page_index": 1,
    "page_size": 20,
    "total": 1,
    "summary": {
      "settled_amount": "10",
      "operation_count": 1
    }
  }
}

筛选、分页与权限

约定 首版请求规则
分页 列表和用户汇总均采用 page_index 默认 1、page_size 默认 20、最大 100;返回 data.list、page_index、page_size、total。空结果返回 [] 与 total = 0。
记录筛选 state 可选 HELD/SETTLED/RELEASED;consumer_user_id、charge_code 可选。source_type 与 source_id 成对传入;created_from / created_to 成对可选,按创建时间筛选。
汇总周期 settled_from / settled_to 必填,起点包含、终点不包含,带时区且最长 366 天;按 settled_at 过滤后聚合。按 settled_amount DESC、consumer_user_id ASC 排序,系统消费排在金额相同时的最后;不把 HELD 或 RELEASED 计为消费。
逐请求鉴权 JwtAuthGuard + TenantContextGuard 校验身份与有效成员;汇总再使用 TenantRolesGuard。普通成员筛选其他用户返回 403;明细不在当前可见范围统一返回 404。Owner 沿用有效 admin 角色,平台管理员不绕过租户授权。
一致性与展示 同一分页响应中的 list、total 和 summary 使用同一数据库快照;多次翻页之间可能有新消费。ref 只返回可公开的关联摘要。冻结与待结算不显示为已消费;结果缺失不显示成 0。

生成请求怎样接入?

前端继续提交既有业务参数,不提交消费 key、价格或结算金额。业务用例确定用户、输入和稳定消费身份,再调用 BillingModule。

人工配置提交

POST /v1/human-tasks/operations

校验并规范化 output → 后端预留 → completeTask。预留不足建议返回 409,保留待提交状态,不放行生成。

Canvas Debug

POST /v1/canvas-debug-run

创建 Run → resolveInput → 后端预留 → runtime.start。若 Run 已创建后推进因余额不足失败,沿现有 201 + Run.failure 结构表达,failure.code 建议为 billing_insufficient_balance,不伪装为整个创建请求回滚。余额不足按业务失败处理,不作为启动瞬时异常自动重试;充值后按既有会话规则重新提交。

后端成功响应建议增加可选 billing_operation_ids,列出本次业务调用关联的计费记录,非收费步骤为空数组;既有任务/Run 状态继续表达业务执行状态。前端据 ID 查询消费详情,并在现有运行状态刷新时更新余额与账单。

技术重试始终复用原操作。网络错误后先查询原任务/Run 与消费记录,再沿原业务协议恢复;HTTP 返回成功不表示资金已经结算。

后端模块只导出两个资金用例

进程内用例 输入与保证
prepareOperation 业务模块传租户、消费用户、稳定 operationKey、合法 chargeCode、来源及已规范化的计价输入。计费按静态价格计算预留,在短事务内创建或读取原单并占额;返回原操作 ID、状态和预留额。相同 key 不再次占额,不比较生成参数。
syncAndSettleOperation 业务结果同步或现有定时任务传租户与原操作 ID;通过可信来源读取结果与用量,固定计费依据后按原价计算费用,再在资金事务内结算或释放。结果未知保留 HELD;终态重复调用返回原结果。调用方不传“扣多少钱”,无需自行配对保存用量和扣余额。

两个用例在现有 Nest 进程内调用,不新增浏览器预留、扣费、释放或 worker 扣费许可 HTTP 接口。充值继续人工增量改库,调价继续静态配置,首版不提供对应写接口。

错误响应与首版验收
HTTP / code 含义
400 / 40000 缺少租户头、筛选或分页不合法、时间范围不合法。
401 / 40100 未登录,或 Token 无效/过期。
403 / 40300 账号、租户或成员无效,或没有所请求的数据范围/管理员权限。
404 / 40400 明细不存在,或不在当前租户与用户的可见范围。
409 / 40900 建议用于人工提交时预留不足;沿现有错误信封,data 为 null。Debug 已创建后的推进失败按上方 Run.failure 表达。
500 / 50000 查询或存储异常。不能用空列表、零余额或成功结算掩盖失败。

验收覆盖未登录、跨租户、普通成员越权和管理员降级;核对分页筛选与统计时间边界、用户移除后的历史、十进制字符串与 null;确认客户接口不泄漏内部用量。两个资金用例还需验证并发、重复调用和未知结果。以上是待实现接口的验收计划。