
Civitai Creator Studio 收益归集重构从 modelVersionId 字典到 owner-keyed 的 ClickHouse 查询方案【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai导读本文基于 Civitai 仓库中 Creator Studio 的构建交接文档owner-rollup-handoff.md展开完整还原了一次 ClickHouse 收益数据架构的关键转折原本被假设必须构建modelVersionId → ownerUserId字典才能回答某创作者赚了多少钱的问题最终被证明大部分场景可以直接读取default.buzzTransactions表——因为它天然按toAccountId即创作者userId键控。读完本文你将掌握五种收益来源的权威过滤规则、ClickHouse Projection 投影的使用时机、货币维度toAccountType的分组必要性、accrual 与 settlement 的精度差异以及两个会悄悄扭曲报表的支付路径缺陷。一、问题定义usage 聚合缺少创作者维度Creator Studio 的/earnings、/analytics和仪表盘需要回答一个看似简单的问题创作者 X 的每个模型各赚了多少钱但 ClickHouse 中所有 usage 聚合表都按modelVersionId键控从来不以创作者userId为主键。已有的userId列如daily_user_resource、userModelDownloads指的是生成者/下载者generator/downloader而非拥有该资源的创作者creator。因此创作者 X 的按模型 usage没有直接索引只能先查出该创作者的全部modelVersionId再执行WHERE modelVersionId IN (…)——对高产创作者来说这个 IN 列表会膨胀到不可接受。这正是后端问题A1的由来详见 questions-koen-backend.md 的 A1 小节与 pre-implementation-decisions.md §A。关键转折earnings 与 usage 不是一回事最初的假设是这个问题同样适用于 earnings。审计后证明并非如此创作者是通过buzz transactions获得报酬的而default.buzzTransactions按toAccountId键控——toAccountId恰好就是创作者的userId1:1 关系见下文验证。也就是说earnings 从来没有丢失 owner 键我们只是读错了表。这一发现直接删掉了一大块计划中的工作不需要AggregatingMergeTree不需要回填backfillA1 也不再阻塞/earnings的来源卡片source cards、时间序列和仪表盘的总览数字。二、Part 1/earnings与仪表盘直接读buzzTransactions无需字典Justin 在 2026-07-14 给出方向性答复Youll probably use buzz transactions for all of it, actually, because they get their money given to them through buzz transactions.你可能全部用 buzz transactions因为他们就是通过 buzz transactions 拿到钱的。经线上数据确认五种收益来源全部已经存在于default.buzzTransactions中且都已按创作者键控。权威过滤器如下所有情况都基于toAccountId creatorId收益来源过滤条件Tip打赏type tipGeneration compensation生成补偿type compensationLicense fee许可费type licenseFee— ⚠️ 当前实际是27见下方阻塞项Access sale提前访问销售type purchase AND externalTransactionId LIKE early-access-%Cosmetic sale外观销售type sell为什么这条路行得通1.toAccountId就是创作者的userId1:1。通过两种独立方式验证1,791/1,791 条携带details.userId的purchase行与toAccountId完全匹配22,310/22,310 条tip行满足toAccountId ∈ details.targetUserIds。2. 表上已有 owner 键控的投影。PROJECTION byToAccount (SELECT * ORDER BY toAccountId, date, fromAccountId, transactionId)。基表的ORDER BY以date开头直接按toAccountId过滤会全表扫描这个 projection 正是把查询变成点查point lookup的关键。务必使用它在证明其过慢之前不要新增物化视图MV。3.toAccountType携带货币。类型为LowCardinality(String)全小写yellow、blue、green、creatorProgramBank、cashSettled、cashPending、club、creatorProgramBankGreen。写查询前必读的 Gotchas这些坑会实实在在咬人逐一列出type purchase不意味着一笔销售它绝大多数是用户给自己充值。90 天数据np-deposit-NOWPayments 充值 39,402 行 / 686M buzz而early-access- 29,993 行 / 54.8M buzz。充值行的toAccountId是买家本人所以朴素的toAccountId X AND type purchase会把创作者自己购买 buzz 的钱算成收益。必须按externalTransactionId前缀过滤或使用details.earlyAccessPurchase/details.modelVersionId。排除accountId 0—— 那是系统/平台账户不是用户 0。外观收入是type sell不是purchase。外观是两条腿的买家的purchase先进银行toAccountId: 0、externalTransactionId形如cosmetic-purchase-…随后一条独立的sell腿把约 70% 转发给cosmetic.createdById。对创作者展示的那一行是sell。details是 JSON字符串。每次实体提取都需要在查询期做JSONExtract。表上没有entityType/entityId列。这里的金额是整数而且是正确的。精度问题见下文Precision一节。 阻塞项license-fee 卡片的type字面量是27TransactionType.LicenseFee 27但 ClickHouse 的入库 MVbuzz.tx_to_staged_mv有一份手写的caseWithExpression映射只枚举了0..26其余回退到toString(Type)。结果就是2026-05-21 以来每笔 license fee 打款都被标成了27—— 92 行并且每晚 02:00 还在增长对应 deliver-creator-compensation.ts 中0 2 * * *的每日调度。这是该表历史中唯一的数字型 type 值。修复落地前查询必须写成type IN (licenseFee,27)否则 license-fee 卡片读出来是零。该修复由 Justin 负责MV 链是他构建的根因、已验证的替换流程与回填都在其私有计划文档中不要从这个工作流尝试 MV 手术——它存在数据丢失的失败模式。三、Part 2按模型收益——字典方案及其被取代最初的 Part 2 设计认为按模型per-model收益无法从buzzTransactions回答因为 compensation/licenseFee 交易是按天、按创作者聚合的并不携带modelVersionId。因此需要构建modelVersionId → ownerUserId字典KeymodelVersionIdUInt/IntAttributeownerUserId版本父模型Model.userId。数据源生产 PostgresModelVersion关联Model通过CDC / ClickPipe到达——ClickHouse 无法直接访问 Bastion 网关后的生产库而团队已在 Buzz DB 上跑 ClickPipes复用该模式。CDC 镜像ModelModelVersion为ReplacingMergeTree字典以镜像为后端。用法dictGet(mv_owner_dict, ownerUserId, modelVersionId)— O(1)无 join。⚠️不要直接从 Postgres 源字典。现有的两个 Postgres 源字典default.model_names、default.model_file_sizes带硬编码 IP其中model_names在生产上已经死亡——对它dictGet返回Connection refused。CDC 镜像支撑的字典在读取期没有外部网络依赖。附带教训字典如果被放进insert 路径的 MV 里一个死字典会让入库停摆本方案是 read-path only更安全但教训仍然成立。该方案已被取代owner 写入时盖章owner stamping交接文档发布之后结论不再成立。owner 现在在写入时就被盖到每一行ResourceCompensation上按模型收益读取变成简单的GROUP BY ownerId不需要字典、不需要 CDC、不存在过期问题。这条取代路径在 licensing-fee-owner-stamping.md 中有完整清单关键事实Mini endpointsrc/pages/api/v1/model-versions/mini/[id].ts在每个fees[]条目上输出recipientUserIdowner 的User.id单祖先扁平谱系每个资源至多一个祖先LoRa/Checkpoint → Ecosystem因此fees[]至多 2 条。已作为PR #3139发布afeabaa63e feat(licensing): expose owner user IDs on the mini endpoint。Orchestrator从 mini endpoint 拉取顶层用户 ID 与每条 fee 的recipientUserId写入orchestration.resourceCompensations的userId Int32 DEFAULT 0列。表引擎为SharedSummingMergeTreeORDER BY (date, userId, modelVersionId, accountType, source)PARTITION BY toYYYYMM(date)。对读者的含义读取时必须sum(amount)GROUP BY这是普通 summing 引擎不是 aggregating没有sumMerge。Backfill31.15M/31.25M 行99.7%追溯到 2024-08-01 已归属0.3% 落在userId 0的是无法映射的版本已删除模型在按创作者userId X读取时自然被排除。ClickHouse 读取实现apps/creator-studio/src/lib/server/models-earnings.ts的getModelEarnings以WHERE userId X … GROUP BY modelVersionId, accountType读取带缓存并用 Postgres 补全模型名/类型已接入仪表盘Top-earning model磁贴和/analytics按模型表。同时取消了modelVersionId → ownerUserId字典方案取代了 cdc-koen.md 中的 CDC/ClickPipe 镜像计划。归因语义因为 owner 是写入时盖章的模型转手自然解决——转手后写入的行归新 owner历史行保留旧 owner。即时点归因point-in-time attribution不追溯重分。两个半场如何共存按来源汇总by-source totals读buzzTransactions——今天就能免费拿到按模型拆分per-model breakdowns读resourceCompensations现在通过 owner 列——用于/analytics的按模型收益表和仪表盘top-earning models磁贴。二者是不会与 buzz 精确对账的一个是 accrual应计一个是 settlement结算。不要把两个数字当作同一个数展示。四、D1MV 键必须携带货币维度问题货币是否必须进入聚合键已确认必须。Justin 的回答是The account type is what we need. Thatll allow us to distinguish whats yellow versus green versus cash versus whatever. And you would use thetoAccountType.为什么这是一个真实的建模问题而非数据可用性问题货币即使存在于行上如果聚合把它加没了也没用。任何 rollup必须把toAccountType放进分组键否则 yellow/green/cash 会坍缩成一个无意义的桶——这违反产品决策B8按收到时的货币展示收益不做换算。在buzzTransactions读取路径上这表现为GROUP BY toAccountId, toAccountType, date, type复合键(toAccountId, toAccountType)才是真正的 owner 键——一个用户持有分开的各色余额。两个值得知道的褶皱 Access sales 永远记 yellow且已被确认为 bug。Justin 的原话Thats wrong. It should pay whatever the person paid in… If a buyer spends green, the creator should get green.修复方法就是把买家的buzzType作为toAccountType传递。查看当前源码model-version.service.ts 的earlyAccessPurchase约 L2296-L2440已经落地了这一修复在构造交易时显式传入toAccountType: buzzType注释说明naming the buyers own account is what stops the buzz service applying its yellow default, which had converted every green purchase into yellow since green became spendable。同时 buzz.service.ts 中仍保留accountType ?? yellow的默认回退约 L181作为未显式指定类型时的兜底。注意该修复是forward-only此前的所有 access sale 都记成了 yellow历史行无法重新着色在修复落地之前该来源的货币维度统一是 yellow——报表不能假设不是这样。Comp、license fee 和 cosmetic sales 都保留原始颜色因此货币维度在这些来源上是有意义的。标签历史——v1 无碍但放宽窗口前必须读Justin 确认I think we already capped the history… the furthest we go back with the analytics is 90 days, so it should be okay.正确——90 天窗口起点远晚于最后一次标签变更2025-08-26下面这些对 v1 均无影响。只有将来放宽窗口或构建全历史视图时才会咬人。orchestration.resourceCompensations.accountType的标签历史影响/analytics不影响buzzTransactions读取路径时期标签含义2024-08-01 → 2025-07-14Usercatch-allyellow 与 blue 合并2025-07-15 → 2025-08-26UserGeneration拆分User yellowGeneration blue2025-08-26 → 至今YellowBlue上述标签的改名单日切换所以User/Yellow与Generation/Blue是同一种货币改名不是双重标签——2025-08-26 的单日干净切换已验证User25,203→0Yellow16,308→23,908无持续重叠也没有创作者被重复支付按accountType的支付后缀在改名后七周才出现-User和-Generation后缀历史上零行。但 2025-07-15 之前的User不是 yellow——它是 yellowblue所以全历史按货币的图表会高估 yellow、低估 blue。90 天上限已经阻止了这一点要么保持上限要么在解除前先做时代归一化。五、D2source过滤器统一为单一词汇表问题source过滤器跨越多个 MV 吗已确认全部使用 buzz transactions。JustinYoure not going to be using resource compensations for all of those… things like access sale, cosmetic sale, those sorts of things… essentially, we will be looking at the buzz transactions, not resource compensation.因此/earnings的权威source标签集派生自buzzTransactions.type而非发明新词tip、compensation、licenseFee、accessSalepurchaseearly-access-前缀、cosmeticSalesell。A1 和 A5 现在共享同一套词汇因为它们本就是对同一张表的一次查询——这个问题原本担心的联合查询union问题已不存在。由此强制的两处修正均已应用A5 曾声称 access/cosmetic currently ride the generic purchase type, so we need a distinct type/flag——这是半错的在买家这一腿两者都是purchase但在创作者收款这一腿——也就是 earnings 唯一关心的那一侧——cosmetic 已经是sellaccess 是purchase 稳定的early-access-前缀。不需要新 type/flag不需要 schema 变更。一个独立类型会更干净但不构成阻塞。earnings.md曾声称resourceCompensations携带tip来源——并不存在真实的source值只有compensation、compensation_recovered_20260507和licenseFee。Tip 在buzzTransactions.type tip。这一点在结算作业源码中也可印证deliver-creator-compensation.ts 中注释明确写道resourceCompensations中从未存在过tip来源行compensation 桶只包含生成补偿这也解释了为何交易描述统一为 Generation compensation。六、Precisionaccrual应计与 settlement结算——must NOT FLOOR是错的Justin 的解释是完整答案The buzz transactions are at settlement point. I believe that the resource compensations are fractional. We make buzz transactions once a day to settle up, essentially. So those are not fractional.orchestration.resourceCompensations.amount是Float64带小数sub-buzz例如0.0234——这是accrual应计。default.buzzTransactions.amount是Int32——这是settlement结算。每日结算作业 deliver-creator-compensation.ts 展示了这一机制的源码级实现以0 2 * * *每天 02:00 UTC 运行updateCreatorResourceCompensation作业从orchestration.resourceCompensations拉取昨日WHERE date ${date}按modelVersionId, accountType, source分组的SUM(amount)通过 Postgres 把modelVersionId映射回userIdModelVersion JOIN Model取m.userIduserId -1跳过按source licenseFee ? licenseTotals : compTotals分桶licenseFee 独立成笔交易其余全部合并进一笔 compensation 打款——对应payoutChannel的! licenseFee规则在每日边界只 floor 一次Math.floor(amount)逐笔不 floor——这是 A2 规定的行为0.01 buzz/图的许可费按日累积而不是每笔被截成 0不是 bug代码注释明确标注FINANCE REVIEW确认 floor 还是 round以及 sub-buzz 余数是否应结转而非弃置。所以/earnings读取整数金额是正确的——它报告的是创作者实际收到的钱。应计与实付会按设计有微小差异sub-buzz 余数在每日边界被丢弃作业在代码中标记需要财务评审。只有*预测forecasting*视图才需要原始的小数应计。七、启动回退Option B——如今只对/analytics相关/earnings与仪表盘不再需要回退它们今天就读buzzTransactions。按模型的/analytics表在owner 盖章落地前仍需要回退应用侧WHERE modelVersionId IN (…)查询对小创作者是可接受的临时方案但带版本数量上限且 top earners 隐藏。如今盖章方案已上线models-earnings.ts 直接WHERE userId X该回退实际已被取代。八、两个支付路径 bug它们决定/earnings能诚实地声称什么两者都不是 Creator Studio 的 bug但都会改变数字的含义。不要静默地围绕它们设计。1. Access sales 永远记 yellow见 D1 一节。已确认是 bug修复 forward-only历史行保持 yellow。当前源码已传递买家buzzType但历史数据不可重新着色。2. Cosmetic 创作者打款是 best-effort——一笔销售可能成功而创作者从未入账。cosmetic-shop.service.ts 的purchaseCosmeticShopItem打款块把银行→创作者的sell腿包在withRetries(..., 3)和只向 Axiom 记日志的 catch 里约 L1137-L1143代码内注释原文We do this last mainly because we dont want to fail the purchase if this fails. We can divide the funds later if needed.我们把它放在最后主要是不想让购买因它失败以后需要的话可以分账。于是平台收下了买家的 buzz创作者却默默拿不到钱。/earnings会无信号地少报外观收入——因为失败的打款不产生任何行唯一痕迹是一条 Axiom 日志且没有任何持久化信息把它关联到创作者或金额。Justin 的判断2026-07-14是让购买失败会更糟这没错——但在出现持久化失败记录死信行或携带details中失败原因的零金额sell之前无人能对此做报表或结算。已标记未排期。九、⚠️ 禁止对整张resourceCompensations执行SELECT sum(amount)全历史有11 行携带二进制垃圾accountType值是乱码日期荒谬1970-02-05、2083-11-04金额高达2.03e267/-8.32e290。垃圾中包含长度前缀片段\x06Yellow、\x04Blue——这是ClickHouse RowBinary 帧被误解析为列数据orchestrator 发送了畸形批次行边界失去同步。每日结算作业是安全的它只过滤昨天但任何全历史sum(amount)都没有意义——一行就会淹没一切。读取时必须过滤match(accountType,^[A-Za-z]$)并加上合理的金额界限。这正是 models-earnings.ts 中CORRUPT_FILTER match(accountType, ^[A-Za-z]$) AND amount 0 AND amount 1e12的来源——该过滤被应用于该文件内所有resourceCompensations读取。此问题已单独上报上游该表由外部 .NET orchestrator 写入本仓库内没有任何代码向它插入数据。十、构建确认清单Part 1/earnings、仪表盘——基本已解除阻塞现在就可以构建用EXPLAIN indexes1确认byToAccount投影确实服务于toAccountId X AND date BETWEEN …查询计划。如果成立v1完全不需要新 MV。在 Justin 的入库修复 回填落地前过滤type IN (licenseFee,27)之后去掉27。排除accountId 0用externalTransactionId LIKE early-access-%隔离 access sales。决定top-earning models是否可从buzzTransactions回答——不能且 Justin 确认该磁贴会上线因此由resourceCompensations owner 列驱动在盖章落地前使用IN (…)回退。Part 2按模型收益/analytics表 top-earning models——⛔ 已被取代不要构建。字典 CDC 方案已退役改为写入时把 owner 盖到每一行ResourceCompensation使按模型读取变成纯GROUP BY ownerId。该方案已上线mini endpoint PR #3139orchestrator 回填完成详见 licensing-fee-owner-stamping.md。原清单中未勾选项已过时而非未完成工作仅保留作为审计上下文。参考文档本文所回答的后端问题 questions-koen-backend.md §A1并大幅收窄了A5。取代 Part 2 字典方案的 owner 盖章清单licensing-fee-owner-stamping.md。相关产品决策B3v1 上线哪些收益来源——五种现在都很廉价、A5access/cosmetic——无需新 MV、A2小数许可费、B8货币、不换算。审计依据2026-07-14 的线上 ClickHouse 审计schema、类型分布、标签历史、打款验证。源码佐证deliver-creator-compensation.ts每日 02:00 结算、每日边界单次 floor、models-earnings.tsowner 盖章读取 损坏行过滤 买家出资渠道查询、model-version.service.tsearlyAccessPurchase传递买家buzzType、cosmetic-shop.service.tsbest-effort 打款块。【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考