Civitai 付费访问(Paid Access)收尾工程清单全解析:门禁语义、产品规则与 Creator Studio 界面打磨
2026/9/17 16:34:26 网站建设 项目流程

Civitai 付费访问(Paid Access)收尾工程清单全解析:门禁语义、产品规则与 Creator Studio 界面打磨

【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai

导读

本文以 docs/creator-studio/monetization/paid-access-followups.md 这份"跟进清单"为主体,系统梳理 Civitai 付费访问(Permanent Paid Access + Timed Early Access)功能在评审会议之后落地的全部收尾动作:从 Creator Studio 抽屉/批量输入/模型列表的界面打磨,到"不可删除已付费版本""EA 天数上限读取模型所有者分数"等产品规则的确认,再到两个已修复的工程缺陷(scheduled EA 永不 materializeendsAtmini/[id].ts免费试用额度 SQL 谓词错误)的源码级验证。读完本文,你将完整理解 Civitai 付费门禁的单表数据模型与endsAt语义,并掌握如何沿着源码定位每一个清单项的真实状态。

本文对应的代码实现参考 paid-access-current-state.md,数据表结构与termsJSON 语义见 paid-access-schema.md,四条货币化规则的权威定义见 monetization-rules.md。

一、背景:单表门禁模型与endsAt单一信号

在展开清单之前,需要先建立付费门禁的底层心智模型,否则清单中"隐藏 0 费用""不可删除已购买版本""scheduled EA 变永久门禁"等条目都无法被正确理解。

1.1PaidAccess表:行存在即被门禁

付费门禁统一存放在PaidAccess表(schema.full.prisma),以(entityType, entityId)为复合主键,其中entityType目前支持ModelVersionComicChapter

  • 行存在 ⇒ 被门禁;无行 ⇒ 免费。
  • 门禁只读一列endsAtendsAt IS NULL表示永久(Permanent Paid Access);值为未来时间表示计时门禁(Timed Early Access)正在生效;值为过去时间表示已过期(tombstone)。
  • ownerId在写入时冗余落库,使按 owner 的查询无需多态 join。
  • terms是 JSON,描述"可购买的东西"(标准价格),与"何时结束"(endsAt)完全解耦。

之所以不再引入kind枚举区分永久/计时,是因为endsAt IS NULL本身就是完美的永久判别式——任何第二个"事实副本"(当年earlyAccessPermanent列与earlyAccessConfig.permanentjsonb 两源并存)都会漂移,这正是本次重构要消灭的故障模式。

1.2terms语义:bundle 与免费试用

terms的类型定义(paid-access-schema.md)如下:

export type ModelVersionTerms = { download?: Grant; // 全量访问 tier,购买它同时授予 generation generation?: GenerationGrant; // 可选更便宜的 generation-only tier freeGeneration?: boolean; // 罕见:download 被门禁但 generation 免费 }; export type Grant = { price: number }; // 标准价格,单位 Buzz export type GenerationGrant = Grant & { trialLimit?: number }; // 免费预览次数上限

关键语义:

  • Bundle 语义:购买download同时授予 download + generation(权限位掩码3);仅购买generation只授予 generation(权限位1)。
  • download.price ≥ generation.price是校验不变量(download 是超集 tier,这正是升级按差价补费可良好定义的前提)。
  • 缺失generation不意味着免费——generation 仍被 download bundle 门禁,与旧行为完全一致。
  • generation.trialLimit是真实功能模式:购买 generation tier 前可免费预览 N 次。
  • 价格下限download.price ≥ 100generation.price ≥ 50trialLimit ≤ 1000,由 zod 在写入边界校验。

示例行:

// 全量 8000,cheaper generation-only 5000,含 10 次预览 { "download": { "price": 8000 }, "generation": { "price": 5000, "trialLimit": 10 } } // 仅 download(历史上 173 个版本):generation 仍被 bundle 门禁 { "download": { "price": 8000 } }

1.3 两条写路径与"三处强制"的规则分布

规则是否被强制,取决于写路径是否覆盖:通向门禁有两条写路径——REST 端点/api/v1/model-versions/early-access(Creator Studio 调用)与 tRPCmodelVersion.upsert(主应用表单调用);而 licensing fee 与 usage control 在 Creator Studio 中直接通过 Kysely 写 Postgres,根本不经过主应用。只在其中一条路径强制 = 未强制。这个认知是理解后续"两个实现副本需保持同步"(如pricing-slot.service.ts与 spoke 侧pricing-slot.ts)的前提。

二、跟进清单全景

以下是评审会(Justin/Ellie 录音回放 + Briant 笔记)与工程开放项的全景。清单按"界面打磨 → 产品规则 → 工程遗留项"三个层次组织,[x]表示已完成、[ ]表示待办:

区域条目状态
Creator Studio 抽屉小窗口滚动条
Creator Studio 抽屉Save 按钮标签跟随模式
Creator Studio 抽屉隐藏 0/过期费用
批量付费价格输入加 Buzz 图标前缀
批量付费"Access fee"/"Gen-only fee" 标签
模型列表桌面端渲染为表格
模型列表移动端徽章折叠价格 + 行堆叠
/join合格成员从 studio 加入 Creator Program
主应用用户导航加入 Creator Studio 链接
主应用licensing fee 整数化
产品验证永久付费购买端到端测试
产品验证捐赠目标启动后不可关闭(确认意图)
产品验证"有付费则不可删模型" 确认(buyer protection)
产品验证EA 天数上限读取 owner 分数
产品验证Justin 文章更新为月度定价额度
工程scheduled EA 永无endsAt✅ 已修复验证
工程mini/[id].tstrial-limit SQL 谓词✅ 已修复验证
工程免费预览 "clear" 语义跨应用对齐
工程CSV 导出/导入增加付费列
工程抽取的 Svelte 组件运行时冒烟测试
工程+layout.server.tsdev-gate 移除并提交

下面逐层展开,并对已确认修复的工程项给出源码级证据。

三、Creator Studio:per-version 抽屉的界面打磨

3.1 小窗口滚动条

抽屉(drawer)承载 licensing/paid-access 侧栏内容,在小窗口下内容溢出。修复为允许滚动——对应apps/creator-studio的抽屉组件。这条是纯 UI 收尾,但它是"付费配置区在窄屏可用性"的基础。

3.2 Save 按钮标签跟随模式

抽屉中"保存"按钮的文案由当前选中的门禁模式决定:

  • 选择Permanent(永久付费)→ 按钮显示"Save paid access"
  • 选择Timed(计时)→ 按钮显示"Save early access"

评审会上 Justin 曾提议统一为 "Save access fee",最终采用模式相关标签(mode-specific label)。这一决定与付费门禁的二分法保持一致:永久门禁与计时门禁在PaidAccess表中共享同一行,仅由timeframeDays/endsAt区分,但面向创作者的语言必须明确当前配置的是哪一种。

3.3 不显示 0 / 过期费用

问题:一个版本渲染出 "0 / 10" 的 licensing fee。根因(Justin ~11:37):该版本曾设置捐赠目标,达标后提前结束了 early access 窗口,费用随之消失,但 UI 仍显示残值。

修复语义为"fee 为 0 时不显示(Off)"——即一个已退出/已过期的门禁不渲染任何费用。这背后对应endsAt的语义:过期 ≠ 无门禁(过期行是 tombstone),但费用为 0 时对购买者而言与 Off 等价,UI 应当以"当前可购买状态"为准,而不是显示历史残值。

四、Creator Studio:批量付费(Bulk Paid Access)输入优化

批量操作涉及两个输入打磨,均源于评审现场的真实误读:

  • Buzz 图标前缀:价格输入框(access + gen-only 两个输入)加上 Buzz 图标前缀,使其明确读作 Buzz 金额而非人数/次数——Ellie 曾把 "100" 读成 "100 人/100 次生成"。
  • 更清晰的输入标签:由 "Access"/"Gen-only" 改为"Access fee" / "Gen-only fee",与抽屉中单版本编辑的术语保持一致。

从实现看,批量路径由apps/creator-studio/src/routes/(app)/models/+page.svelteBulkBar.svelteBulkActionDialog.svelte承载,写路径为+page.server.tsbulkSetPaidAccessaction(详见 paid-access-current-state.md)。批量操作还受usage-type 过滤?usage=download|generation)驱动列表与全选,保证价格字段语义无歧义;当选中数量超过当月剩余额度、或 owner 低于资格线(creator score ≥ 10k)时,对话框会发出警告。

五、模型列表:定价一览(at-a-glance)

5.1 桌面端表格(待办)

仍未落地的条目:桌面端将模型/版本列表渲染为表格,列包括(version, status, licensing fee, access + Buzz price),让创作者无需打开面板即可一览所有价格;移动端保留当前卡片布局。这是清单中少数开放中的 UI 项之一。

5.2 移动端徽章折叠价格(已完成)

已完成的移动端打磨(Justin ~9:54, ~10:42):

  • early/paid-access 徽章现在折叠 Buzz 访问价格accessBadge()输出 "Permanent · 500 ⚡" 或 "Early access · 500 ⚡";
  • 移动端版本行堆叠布局:状态徽章右对齐到标题行,释放第二行给"价格 + 费用"chips;桌面端保持单行。

这与付费门禁的两种形态一一对应:"Permanent" 对应endsAt IS NULLtimeframeDays IS NULL),"Early access" 对应未过期的计时窗口。

六、Creator Program:从 Studio 加入(/join)

新能力:合格成员可直接从 Creator Studio 加入 Creator Program。判定条件是"拥有 Civitai 会员 + 创作者分数,但尚未加入";满足时在 studio 的/join页面浮现 "Join" 流程,镜像主应用 Buzz 仪表盘的 "Join now"。

这条与 R3 月度定价规则(见 monetization-rules.md)衔接:会员等级决定每月可设置的新价格数量(Free 3 / Bronze 10 / Silver 25 / Gold 无限),而不是旧的"并发永久门禁上限"。因此"从 studio 加入"是打通"成为创作者 → 获得定价额度"链路的关键入口。

七、主应用集成

两条已完成的主应用侧收尾:

  1. 用户导航加入 Creator Studio 链接:主应用用户导航(user navigation)新增指向 Creator Studio 的入口。
  2. Licensing fee 整数化:licensing fee 统一为整数"每 N 次生成多少 Buzz"的比例(而非按图的小数),与 studio 一致。这一改动消除了主应用与 spoke 之间对费用语义的认知分裂。

八、产品规则确认与验证(Verify / Product)

这一节是清单中"规则确认"密度最高的部分——评审会暴露的困惑,多数被验证为"设计如此"或"已由构造满足"。

8.1 永久付费购买端到端测试(待办)

早期访问购买已验证可用,但永久付费购买的端到端流程尚未被实际演练(Justin ~6:15)。这是清单中明确的测试空缺:需要覆盖"永久门禁 → 购买 → 权限授予(download=3 / generation=1)→ 永不过期"的完整链路。

8.2 捐赠目标启动后不可关闭(待确认)

捐赠目标(donation goal)被设计为create-once:端点从不更新或移除它。Justin ~12:30 提出"目标一旦启动是否可关闭"的问题,待产品确认——或增加关闭途径。参考 monetization-rules.md 的 R6:active: false只在目标完成时写入,没有取消路径(创作者和版主都没有)。注意:永久门禁对目标完成豁免isTimedGateActive),因此被资助的目标不会抹掉永久付费墙。

8.3 "有付费则不可删除模型"——确认是 buyer protection(已完成确认)

Ellie 报告的"无法删除有付费访问的模型"被确认是有意设计(买家保护)。关键实现事实:

  • 门禁键基于实际购买,而非仅仅存在门禁:版本meta.hadEarlyAccessPurchase在任何购买发生时置位(通过earlyAccessPurchase,计时永久购买都会置位——它不按timeframeDays分支)。
  • 非版主被阻止调用deleteModelByIdunpublishModelByIddeleteVersionById以及版本合并("…has had early access purchases…");版主绕过这些限制。
  • 零购买的门禁版本仍可删除

源码证据位于 model-version.service.ts:写 meta 时使用条件更新——

UPDATE "ModelVersion" SET meta = jsonb_set(COALESCE(meta, '{}'::jsonb), '{hadEarlyAccessPurchase}', to_jsonb(${true})) WHERE id = ${id} AND COALESCE((meta->>'hadEarlyAccessPurchase')::boolean, false) = false;

即:仅在尚未置位时置位(幂等)。相关测试可见 model-early-access-refund.service.test.ts(其中注释明确"门禁完全依赖hadEarlyAccessPurchase,因为PaidAccess已不在谓词中")与 model-version.unpublish-refund.service.test.ts。

8.4 EA 天数上限读取 owner 的分数——已由构造满足(无需改码)

Justin 以版主身份看到 30 天上限,而非模型所有者真实的 9 天上限。验证结论:已由构造满足,无需代码变更

  • model-version.router.tsisOwnerOrModerator(src/server/routers/model-version.router.ts,ownershipGuard({ authorizeInputModelId: true }))拒绝任何非版主且非模型所有者的调用者;
  • 版主在表单(isModerator分支)与服务端(assertUserEarlyAccessLimits提前返回)都被豁免于 EA 上限。

因此,只要上限被应用,操作者必然就是 owner,此时ctx.user.meta.scores.models读取的必然是 owner 的分数;版主看到的是全部 tier 解锁(而非 owner 的上限),这正是演示中的困惑来源。注意 monetization-rules.md 的 R3 更新:自 2026-09-04 起,所有对用户展示的"creator score"统一读取User.meta.scores.total(经creatorScoreFromMeta),早期访问阶梯的入门门槛为 10,000 分,与定价资格线一致。

8.5 Justin 文章更新为月度定价额度(待办)

待办:把 Justin 的文章更新为月度定价额度口径——Free 3 / Bronze 10 / Silver 25 / Gold 每月无限。这些数字取代了旧的并发永久门禁上限,数字相似但含义完全不同:文章必须表述为"每自然月的新价格",而不是"处于付费访问的模型数"。见 monetization-rules.md 的 R3。配套的PricingSlot账本按(entityType, entityId)记账,listPricingSlots回答"什么花掉了额度";资格线(score ≥ 10k)读取的是当前状态,而额度读取的是本自然月已花费的 slot 账本。

九、工程遗留项:两个已修复缺陷的源码验证

清单中两个"疑似缺陷"经过本轮验证均确认已修复,无需再改码。以下是逐行证据。

9.1 Scheduled Early Access 永不 materializeendsAt(已修复验证)

原缺陷:计划(scheduled)发布的计时门禁永远不会物化endsAt——publishedAt在未来时版本进入Scheduled状态,跳过了需要Published的 EA 分支,materializePaidAccessEndsAt从未执行;随后process-scheduled-publishing再次发布时不带publishedAt,又被跳过。结果endsAt永远为 NULL = 永久门禁,且process-ending-early-access(过滤endsAt <= NOW())永远不会释放它。

现实现验证publishModelVersionsWithEarlyAccess(model-version.service.ts)现在用 else-branch 覆盖 scheduled 情形:

} else if (currentVersion.publishedAt != null) { // Caller didn't pass publishedAt (the scheduled-publishing job sets it at schedule time and // only flips status here). Materialize the pending timed gate from the version's already-set // publishedAt — otherwise a scheduled EA version keeps endsAt NULL and becomes a PERMANENT // gate that never releases. materializePaidAccessEndsAt no-ops on permanent/tombstoned gates. await materializePaidAccessEndsAt(currentVersion.id, currentVersion.publishedAt, dbClient); }

同时,发布路径遵循anti-bump guardWHERE "publishedAt" IS NULL OR "publishedAt" > NOW()),仅在publishedAt实际被(重)设置时才物化endsAt = publishedAt + timeframeDays——无操作的重新发布不会覆盖已物化的endsAtmaterializePaidAccessEndsAt对永久/已墓碑门禁为 no-op。结论:无需代码变更(清单已勾选)。

9.2mini/[id].ts免费试用额度 SQL 谓词错误(已修复验证)

原缺陷mini/[id].ts的 free-trial-limit SQL 以terms->'generation'->>'price' IS NOT NULL为谓词;但常见路径(download bundle 或 gen-only)发出的generation: { trialLimit: N }没有price,于是 SQL 返回 NULL 免费试用额度——免费预览计数无法上达 orchestrator。

现实现验证(src/pages/api/v1/model-versions/mini/[id].ts):

) AS "checkPermission", -- 谓词:terms 有 generation 且 free != 'true' ( CASE WHEN pa."terms"->'generation' IS NOT NULL AND COALESCE(pa."terms"->'generation'->>'free', '') <> 'true' THEN COALESCE(CAST(pa."terms"->'generation'->>'trialLimit' AS int), ${DEFAULT_GENERATION_TRIAL_LIMIT})::int ELSE NULL END ) AS "freeTrialLimit"

checkPermission(line 137-141)与freeTrialLimit(line 164-171)现在都以terms->'generation' IS NOT NULL AND COALESCE(terms->'generation'->>'free','') <> 'true'为门控条件,与paidGenerationGrant()镜像。结论:无需代码变更(清单已勾选)。文档还建议将零调用点的paidAccessActiveSql/isPaidAccessGated共享 helper 收编约 6 处手写 "active gate" 原始 SQL,以消灭此类问题(待办,非阻塞)。

9.3 免费预览 "clear" 语义跨应用对齐(已完成)

两个应用对"清空免费预览"的语义不一致:spoke 清空 → 0,主应用清空 → 默认 10。两者编辑同一批版本,必须统一语义。清单标注已对齐(pick one and align)。

十、剩余开放项与运行注意事项

10.1 CSV 导出/导入增加付费列(待办)

CSV 导出/导入尚不包含paid/early-access 列。推荐方案:先导出后导入(export-first);导出列包含付费访问相关字段,捐赠目标列只读(因捐赠目标是 create-once)。导入本质上是经由端点的 CSV 驱动批量变更,需带上 cap/会员资格/usage 强制。

10.2 运行时冒烟测试抽取的 Svelte 组件(待办)

付费编辑器与批量条被抽取为独立 Svelte 组件后,需要运行时冒烟测试覆盖:抽屉重开时的 state 重新播种(re-seed),以及批量条之间共享selected的响应式(reactivity)——类型检查无法捕获响应式回归。对应组件路径为apps/creator-studio/src/lib/components/PaidAccessEditor.svelteBulkBar.svelteBulkActionDialog.svelte

10.3 dev-gate 移除(已完成,待提交)

+layout.server.ts的 dev-gate 移除尚未提交(当前未提交状态)。一旦提交,它将把 Creator Studio 开放给所有已认证用户(移除 moderator/testing-id 门禁)。这是上线前的最后一扇门。

10.4 命名漂移提醒

Creator Studio spoke 的写 API 仍沿用earlyAccess*命名(setEarlyAccessConfig?/setEarlyAccessEarlyAccessConfighasEarlyAccess),尽管它们现在也覆盖永久付费访问;Svelte 组件已重命名为PaidAccess*,API 重命名是推迟的后续项。阅读 spoke 代码时需注意这一历史命名。

十一、小结

这份清单展示了付费访问功能从"评审会发现 → 规则确认 → 缺陷修复验证"的完整闭环:

  • 界面层:抽屉滚动、模式化按钮、Buzz 前缀、清晰标签、移动端徽章折叠价格——消除创作者对价格的误读;
  • 产品规则层:删除保护键在hadEarlyAccessPurchase实际购买而非门禁存在、EA 上限天然读取 owner 分数、捐赠目标 create-once 且永久门禁豁免——三条规则均有源码与测试支撑;
  • 工程层:scheduled EA 的endsAt物化与mini/[id].ts的 trial-limit 谓词,均已通过当前源码验证为已修复,无需再改码。

继续深入可阅读 paid-access-current-state.md(代码位置与 Gotchas)、paid-access-schema.md(迁移设计记录)与 monetization-rules.md(规则权威定义)。

【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询