Civitai PaidAccess 门控体系现状全解:PaidAccess 表设计、usage-control 定价与 Creator Studio 批量收费
【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai
本指南以仓库内docs/creator-studio/monetization/paid-access-current-state.md(永久付费访问与 usage-control 定价落地后编写的"现状参考"文档,2026-07-29,commita0535931f5)为骨架,结合 paid-access-schema.md、monetization-rules.md 与两端源码,系统梳理 Civitai 模型版本的付费门控(Paid Access / Early Access)是如何建模、写入、读取、校验与缓存的。读完本文,你将掌握PaidAccess表的语义设计、terms的 bundle 定价规则、主应用与 Creator Studio 两条写入路径的职责边界、月度定价额度(PricingSlot)规则,以及当前已知的遗留问题,可直接作为继续开展付费访问工作的"接手文档"。
文档定位:一个"继续工作"的现状参考
这份文档不是一份新的设计方案,而是 phase-1 cutover 完成之后的现状快照。它解决的问题是:当旧的earlyAccessConfig/earlyAccessEndsAt/earlyAccessPermanent三列(外加earlyAccessTimeFrame)被彻底移除后,付费访问功能"现在长什么样、代码在哪里、有哪些坑"。
文档开头带有一条重要的Superseded 警告:在 monetization revamp(见 monetization-rules.md 的 R3)之后,原先的per-tier 价格上限和并发永久门控数量上限已被移除——现在会员体系限制的是"创作者每月可以新增多少个价格"(免费 3 / 青铜 10 / 白银 25 / 黄金 ∞),并且设置了10,000 创作者积分门槛(scores.total)才有资格变现。但文档明确说明:门控机制本身(gate mechanics)的表述仍然全部有效。因此阅读本文时需区分两层:机制层(PaidAccess行 +endsAt+terms)长期稳定;配额层(cap / allowance)以 revamp 后的 R3 为准。
核心模型:PaidAccess 表与endsAt唯一门控信号
整个门控体系建立在一张表、一个信号之上。数据库层定义(见 paid-access-schema.md 的 §1,迁移已应用,记录在prisma/migrations/):
enum PaidAccessEntityType { ModelVersion ComicChapter } model PaidAccess { entityType PaidAccessEntityType entityId Int ownerId Int // 写入时反规范化——无需多态 join 即可做 owner 维度计数 endsAt DateTime? // 物化结束时间。endsAt IS NULL ⟺ permanent(无窗口) timeframeDays Int? // 窗口天数;NULL = permanent terms Json // PaidAccessTerms,见下 createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@id([entityType, entityId]) // 每个实体最多一个门 @@index([ownerId, entityType, endsAt]) }行即门:一行存在 ⇒ 收费,无行 ⇒ 免费
这是建模的第一条不变量。"当前是否被门控"不依赖任何布尔列或派生列,只读endsAt一列:
endsAt取值 | 含义 |
|---|---|
NULL | 永久(Paid Access,无窗口) |
| 未来时间 | 活跃的限时窗口(Early Access) |
| 过去时间 | 已过期(tombstone,行仍保留) |
正是因为这个原因,当初的设计决策中刻意没有kind枚举列——endsAt IS NULL本身就是"永久"的判别式。旧实现里earlyAccessPermanent列 +earlyAccessConfig.permanentjsonb 两个事实源靠触发器强行保持一致,正是本次重构要消灭的漂移源。同理也没有termsVersion:zod 写入边界(见后文)才是对抗 jsonb 腐烂的真正防线。
timeframeDays:窗口长度,而非门控信号
timeframeDays是发布前窗口的长度(天数),NULL表示永久。文档特别强调了一个反直觉点(在 monetization-rules.md 中有完整表格):
endsAt为NULL可能表示两种不相关的情况:永久门控(本来就没有结束日期),或尚未开始的限时门控(endsAt在发布时才物化,见materializePaidAccessEndsAt)。- 它从不表示"没有门控"——那对应"没有行"。
所以"是否永久"要判timeframeDays IS NULL(对应共享助手 isPermanentGate),而"当前是否被门控"要判isPaidAccessActive(endsAt == null || endsAt > now)。两者语义不同,不可混用。
termsJSON:bundle 定价语义
terms描述"卖什么、卖多少钱"。对ModelVersion,类型定义在 packages/civitai-buzz/src/paid-access.ts:
export type Grant = { price: number }; // 标准价格,单位 Buzz export type GenerationGrant = | { free: true } // generation 免费/不设防(绕过 access 检查) | { price?: number; trialLimit?: number }; // 付费 generation-only 档位 export type ModelVersionTerms = { download?: Grant; // 全访问(bundle)档位 generation?: GenerationGrant; // 可选的更便宜 generation-only 档位 freeGeneration?: boolean; // 罕见:download 收费但 generation 免费 acceptsBlueBuzz?: boolean; // 接受 Blue Buzz 支付并按 Blue Buzz 结算 };Bundle 语义是理解一切的钥匙:购买download档位同时授予 generation 访问权(对应EntityAccess.permissions位掩码EarlyAccessDownload=2,购买时得到 3);generation档位缺失并不等于 generation 免费——generation 仍然被 download bundle 门控着。这正是当前线上 453 个活跃门控里那 173 个"只收 download 费"版本(历史数据,见 schema 文档 §6)映射到{ "download": { "price": … } }后行为完全不变的原因。
ComicChapter则是另一个形状{ access: Grant }(单档位解锁章节),由parseTerms(entityType, raw)按实体类型分发——但目前漫画只支持限时模式,尚未迁到PaidAccess表(stage 5 待做)。
示例行
// 全访问 8000 Buzz,可选更便宜的 generation-only 档 5000 Buzz,含 10 次预览生成 { "download": { "price": 8000 }, "generation": { "price": 5000, "trialLimit": 10 } } // 仅 download 档(历史上那 173 个):generation 仍被 bundle 门控 { "download": { "price": 8000 } }已构建的能力:从限时抢先体验到永久付费
文档的 "What's built" 一节罗列了当前已落地的功能,按此展开:
1. 限时 Early Access + 永久 Paid Access
- 永久=
endsAt IS NULL,对满足资格门槛的所有创作者开放;把它施加到一个尚无任何价格的版本上会消耗一个月度定价额度(free 3 / bronze 10 / silver 25 / gold ∞,见 R3)。 - 服务端在每一条写入路径上强制校验(
assertMonetizationWrite),并带already-priced 豁免(carve-out):重新保存一个已经带价格的版本永远免费,无论积分多少、剩余额度多少。 - 永久可在发布后设置;而限时窗口不能在发布后开始(R1:
initialPublishedAt <= now() OR status = 'Published'即视为已发布,拒绝开启窗口)。
2. Usage-control 感知的定价
"Price for access"(访问价格)只有一个必填项,但落点随 usage control 变化:
| usage control | 定价落点 | 说明 |
|---|---|---|
Download | download.price(bundle)+ 可选更便宜的generation.price | 购买 download 即获得 generation |
Generation(仅站内生成) | generation.price,无 download 档 | 访问价格就是生成价格;generation-only 字段隐藏 |
InternalGeneration/ExternalGeneration | 完全不能设门控 | 仅审核/API 使用,Studio 中显示横幅而非选择器 |
免费预览生成次数 =generation.trialLimit。这一整套映射被收敛到一个共享函数 buildModelVersionTerms(两个应用都调用),保证站内表单与 Creator Studio 对"创作者被告知的内容"不会漂移。
3. Donation goal:限时专属、create-once
- 仅限时:它会在目标达成时提前结束窗口(写入
endsAt = now());永久门控永不结束,因此没有 donation goal。 - create-once:端点从不更新/删除已有 goal,Studio 对已存在的 goal 只读展示。
4. 批量永久付费(Creator Studio)
Creator Studio 的批量操作按usage-type 过滤器(?usage=download|generation)驱动列表与全选,从而保证价格字段无歧义;选择数量不设上限,但当选择超过剩余月度额度、或创作者低于资格门槛时,对话框会给出警告。
代码地图:共享层、主应用、Creator Studio spoke
文档给出了精确的代码位置,结合源码逐项展开:
共享层(两个应用共用,杜绝规则漂移)
- packages/civitai-buzz/src/paid-access.ts:
terms类型、buildModelVersionTerms()(唯一的 config→terms 映射器)、门控/授权助手(grantsGeneration、isPaidAccessActive、isTimedGateActive、isPermanentGate、generationPrice、generationTrialLimit、migrateTermsForUsageControl)以及 sale/折扣相关助手(discountedPrice、bestSaleFor、saleDaysUsed等)。 - packages/civitai-buzz/src/pricing-allowance.ts:月度定价额度(
MONTHLY_PRICING_ALLOWANCE_BY_TIER:free 3 / founder 10 / bronze 10 / silver 25 / gold Infinity)、资格门槛(MONETIZATION_MIN_CREATOR_SCORE = 10_000)、pricingEligibility、pricingFloorMessage、共享文案PRICING_SLOT_EXPLAINER/EARLY_ACCESS_NOT_COUNTED。文档注明:旧的 per-tier permanent 上限曾住在这里,已被月度额度取代。
主应用(Next.js,src/)
- 表单:
src/components/Resource/Forms/ModelVersionUpsertForm.tsx—toPaidAccessInput/toFormEarlyAccessConfig/toGate、定价.refine()校验、canConfigurePaidAccess、isGenOnly/paidAccessUsageOkUI 门控。 - 服务端:
controllers/model-version.controller.ts— permanent membership +assertMonetizationWrite+ carve-out;services/model-version.service.ts— usage-control 守卫(upsert 路径与updateModelVersionPaidAccess双侧)、assertUserEarlyAccessLimits(R1 及窗口长度/并发数限制);services/paid-access.service.ts—getPaidAccess、assertMonetizationWrite、assertPaidAccessInput、writePaidAccessForModelVersion、materializePaidAccessEndsAt、endPaidAccessNow;services/pricing-slot.service.ts— 门槛 + 额度 + 插槽记录/释放;services/generation/paid-access-gating.ts—唯一的generation 门控执行点(内部用isPaidAccessActive+grantsGeneration判定);- download 门控在
services/file.service.ts。
- REST 边界(spoke 调用入口):
src/pages/api/v1/model-versions/early-access.ts,请求体{ id, paidAccess, donationGoal, rightsAffirmed };permanent 需要WEBHOOK_TOKEN。
Creator Studio spoke(SvelteKit,apps/creator-studio/)
- 页面:
src/routes/(app)/models/+page.svelte(抽屉 + 列表),使用src/lib/components/PaidAccessEditor.svelte、BulkBar.svelte、BulkActionDialog.svelte。 - Actions/schemas:
src/routes/(app)/models/+page.server.ts(setPaidAccess、bulkSetPaidAccess、查询与批量 schema);表单校验在src/lib/server/monetization/paid-access-schema.ts(paidAccessFormSchema与bulkPaidAccessSchema)。 - 读取:
src/lib/server/models.ts(paidAccessToConfig、usage过滤器、paidAccessFilter)。 - 写入:
src/lib/server/monetization/paid-access.ts(setPaidAccessConfig→ REST 端点)与pricing-slot.ts(本应用直接 SQL 写入的门槛 + 额度)。 - 共享类型/常量:
src/lib/monetization/paid-access.ts(PaidAccessConfig、MIN_ACCESS_PRICE=100、MIN_GENERATION_PRICE=50、DEFAULT_GENERATION_TRIAL_LIMIT=10、MAX_GENERATION_TRIAL_LIMIT=1000、创作者积分阶梯EARLY_ACCESS_SCORE_UNLOCK/EARLY_ACCESS_QUANTITY_UNLOCK)。
写入路径:三处写门,一条 REST 线
一个核心观察:付费门控只有一条真正承载副作用的写入线——主应用的 REST 端点。Creator Studio 的 gate 写入并不是直写数据库,而是通过callMainApp把调用者的 session cookie 原样转发给主应用:
// apps/creator-studio/src/lib/server/monetization/paid-access.ts const ENDPOINT = '/api/v1/model-versions/early-access'; export async function setPaidAccessConfig(cookie, versionId, config, genOnly = false, rightsAffirmed = false) { const terms = config && config.accessPrice != null ? buildModelVersionTerms({ accessPrice: config.accessPrice, generationPrice: config.generationPrice, freePreviewGenerations: config.freePreviewGenerations ?? 0, genOnly, freeGeneration: config.freeGeneration, acceptsBlueBuzz: config.acceptsBlueBuzz }) : {}; const paidAccess = !config ? null : config.permanent ? { permanent: true, terms } : { permanent: false, timeframeDays: config.timeframe, terms }; // permanent 配置必须带共享 webhook token,直接用户调用无法绕过 const path = config?.permanent ? `${ENDPOINT}?token=${encodeURIComponent(env.WEBHOOK_TOKEN ?? '')}` : ENDPOINT; // ... callMainApp(path, cookie, { method: 'POST', body: { id: versionId, paidAccess, donationGoal, rightsAffirmed } }) }之所以必须走主应用,是因为这个写有真实副作用:donation-goal 行、buzzTransactionId记账、发布状态守卫、缓存/搜索失效——只有主应用拥有这些。而主应用内部的 tRPCmodelVersion.upsert(站内表单)与 REST 端点(spoke 调用)都必须执行同一套校验,否则"只在其中一处执行的规则等于没执行"(monetization-rules.md 的明确警示)。
REST 端点的校验链(见 early-access.ts):
- zod 解析
updateModelVersionPaidAccessSchema,失败返回 400; - 所有权校验(非审核员必须为版本所有者);
paidAccessBlockedFor:POI(真人肖像)模型与 Private 模型拒绝设门(REST 拒绝而非静默剥离;tRPC upsert 则剥离,因为无关编辑不能被一个创作者已看不到的收费阻塞);- permanent 需要
WEBHOOK_TOKEN(否则 403); assertMonetizationWrite(fee 上限 + 10k 门槛 + 月度额度,按所有者而非操作者判定——审核员代存他人版本不得借用其积分/会员);assertUserEarlyAccessLimits(限时窗口最大天数 + 并发数量);- 写入后按
assertMonetizationWrite返回的spendsSlot/releasesSlot记录或释放PricingSlot。
对应的低层写函数writePaidAccessForModelVersion(paid-access.service.ts)定义了四态映射,这与旧的三列编码截然不同:
| 输入 | endsAt | timeframeDays |
|---|---|---|
| permanent | NULL | NULL |
| timed + 已发布 | publishedAt + timeframeDays | 保留 |
| timed + 未发布 | NULL(待发布时物化) | 保留 |
| 不设门 | 删除行 | — |
读取与缓存:缓存"行"而非"判定"
由于endsAt是物化的,isPaidAccessActive(row, now())可以在读取时实时派生——这带来一个关键的缓存安全性质:缓存永远不会因时间流逝而过期,失效只发生在配置变更时(编辑、阈值触发写endsAt、移除、实体删除)。这就是"缓存行、不缓存判定"的规则(onsite-monetization-parity.md 的 Caching 一节;createCachedObject批量原始组件见src/server/utils/cache-helpers.ts)。
getPaidAccess(entityType, entityIds)按entityType分桶缓存(ModelVersion 123与ComicChapter 123不会撞键),返回Record<number, PaidAccessRow>,为已持有的 id 集合装饰访问状态;而涉及筛选、排序、计数的查询必须用 SQL 谓词——两者互补,绝不能用getPaidAccess对分页结果做后置过滤(页面会缩水、计数会撒谎)。统一的 SQL 谓词收敛在 paid-access-sql.ts:
pa."entityType" = 'ModelVersion' AND (pa."endsAt" IS NULL OR pa."endsAt" > NOW()) AND mv.status = 'Published'::"ModelStatus"spoke 的读取侧有同构的paidAccessFilter(KyselyEXISTS子查询,见 models.ts),以及重建 UI 形状的paidAccessToConfig——注意其 gen-only 读取规则:访问价格 =t.download?.price ?? paidGen?.price(见 Gotchas)。
定价规则(revamp 后):会员限制的是"多频繁",不是"多贵"
依据 monetization-rules.md R3,结合 pricing-allowance.ts:
| 限制 | 来源 | 适用对象 |
|---|---|---|
| 资格门槛(积分 ≥ 10k) | Creator score(scores.total,经creatorScoreFromMeta读取) | 新增一个费用或新设一个门 |
| 每月新价格数 | 会员等级 | 新费用或永久门 |
| 许可费上限 | 固定 100/次生成 × 媒体类型(视频 500) | 费用 |
| 付费访问价格上限 | 无 | — |
| 窗口长度(天) | 创作者积分 | 限时 |
| 并发窗口数 | 创作者积分 | 限时 |
额度表:free 3 / bronze 10 / silver 25 / gold ∞。关键语义:
- "价格" = 许可费或永久门;限时 Early Access 窗口两者都不是(窗口关闭即自行"定价出局",且本身受更高的创作者积分门槛约束)。
- 给无价格的版本施加价格:需要积分门槛并消耗一个插槽;编辑、下调、清除已设价格:免费且不需积分;清除最后一个价格还可能退回插槽(R3b:需无任何交易——
EntityAccess无他人持有、且自插槽创建以来未发生过许可费收费;ClickHouse 的orchestration.resourceCompensations查询带 3s 超时,失败时以ModelVersionMetric.earnedAmount日镜像兜底,双端 fail-closed——无法回答的问题一律视为"插槽已花")。 PricingSlot以(entityType, entityId)为键(与PaidAccess同键,天然覆盖 ComicChapter),主键即幂等:已有价格旁的追加、或对已设价格的任何编辑,命中该行则零成本。- 会员等级滑坡(
incomplete/past_due/unpaid)立即把当月额度降回 free 档(主应用读实时订阅状态getCapTier);但未知/缺失等级解析为free 而非 0,失去会员绝不等于失去定价能力。已花的插槽不受影响。 - 权益声明(rights affirmation,R5):版本第一次收费时必填,按
ownerId记录,不随所有权转移而存续;MONETIZATION_RIGHTS_AFFIRMATION_VERSION升级会作废全部存量记录。批量场景中只要任一选中版本缺失即要求声明,已声明的服务端跳过。
已验证的端到端行为
文档 "Verified" 一节记录了审计结论(与源码一致):
- 购买 → 访问权端到端正确:买
download授予 download+generation(permissions= 3);买generation仅授予 generation(= 1); - 永久买家永不失去访问权:
EntityAccess无过期时间,expiry cron 排除 permanent 行; - orchestrator按用户计量 free-trial 次数并检查
EarlyAccessGeneration位(外部行为,勿当作 bug 复报); - 主应用
pnpm typecheck与 spokepnpm --filter @civitai/creator-studio-app typecheck均 0 错误。
另外从源码可确认:process-ending-early-access这个每分钟 cron(src/server/jobs/process-ending-early-access.ts)现在只做一件事——在限时窗口过期后把版本重新标记为"New"(SET "publishedAt" = NOW()以便重新浮出水面),绝不删除或改写PaidAccess行。过期门控是一个 tombstone(timeframeDays有值、endsAt在过去),这正是"行存在 ≠ 被门控"的原因;清除门、删除版本、合并版本是仅有的三个删行路径,且清除门时还会把availability从'EarlyAccess'回写为'Public',否则未购买者会被永久锁在门外。
Gotchas:非显而易见的坑
文档 "Gotchas" 一节浓缩了最容易踩的三个点,均有源码佐证:
- Gen-only 价格往返:gen-only 版本的访问价格存在
generation.price(无 download 档),因此读取必须用download?.price ?? paidGen?.price兜底(spoke 的paidAccessToConfig与主应用表单的toFormEarlyAccessConfig都是如此);而buildModelVersionTerms在genOnly时会忽略generationPrice参数——访问价格就是 generation 价格。 - Donation goal create-once:端点从不更新/移除它;
active: false只在目标达成时写入一次,没有取消路径。spoke 对已存在 goal 锁定只读。永久门控豁免于目标达成(isTimedGateActive只判限时窗口),因此一个打满的 goal 无法抹掉永久付费墙。另外注意DonationGoal仍有遗留的modelVersionId列双写,查询必须同时检查(entityType, entityId)与modelVersionId两路。 - 命名漂移:spoke 的写 API 仍用
earlyAccess*命名(setEarlyAccessConfig、?/setEarlyAccess、EarlyAccessConfig、hasEarlyAccess),尽管它现在同时覆盖永久 Paid Access;Svelte 组件已改名为PaidAccess*,API 重命名是延后的跟进项。
Open / next:待办与遗留发现
待办清单
- CSV 导出/导入尚未包含付费/抢先体验列:建议先导出列(只读往返),再做独立的导入通道(本质是经端点、带 cap/membership/usage 强制的 CSV 驱动批量变更);donation goal 应为只读列。
- 免费预览"清除"语义两应用不一致:spoke 空值 →0,主应用清除 → 默认10(
DEFAULT_GENERATION_TRIAL_LIMIT)。两边编辑的是同一批版本,需统一语义。 - Svelte 组件抽取的运行时冒烟测试:抽屉重开时的状态播种、跨批量栏的共享
selected响应性——类型检查抓不到响应性回归。
phase-1 review 遗留发现(需对照当前代码复核)
原 review 文档已删除,以下发现由现状文档代为保存,均标注为"money/access-relevant,需验证是否仍可复现"。
- Scheduled(定时发布)抢先体验永不物化
endsAt→ 变成永不释放的永久门。以未来publishedAt发布的版本状态为Scheduled,会跳过 EA 分支(要求Published),materializePaidAccessEndsAt因此永不运行;process-scheduled-publishing.ts(~:238)随后在不带publishedAt的情况下重新发布,于是再次被跳过。endsAt永远保持NULL= 永久,而process-ending-early-access(筛选endsAt <= NOW())永远等不到它。文档给出的修复形状是:让一个applyPublishedAt(tx, versionId, publishedAt)同时拥有 anti-bump SQL 与 materialize 逻辑,并被所有发布点使用。从当前源码看,materializePaidAccessEndsAt本身已经具备"跳过 tombstone、幂等、事务参数化"等防护(paid-access.service.ts),但 Scheduled 路径是否已完全接入,仍需对照process-scheduled-publishing.ts验证。 mini/[id].ts(~:148)免费试用限额 SQL 谓词错误:它用terms->'generation'->>'price' IS NOT NULL判定,但常见路径(download bundle 或 gen-only)发出的是generation: { trialLimit: N }没有price,于是默认情况下 SQL 返回NULL试用限额——免费预览次数可能不会上浮给 orchestrator。应改为terms ? 'generation' AND COALESCE(terms->'generation'->>'free','') <> 'true'。该文件在现仓库中路径已不可考(属历史 review 记录),当前@civitai/buzz导出的generationTrialLimit已用grant.trialLimit ?? DEFAULT_GENERATION_TRIAL_LIMIT统一口径。earlyAccess*命名重命名(延后):见 Gotchas。
历史与迁移:phase-1 cutover 如何收尾
PaidAccess的落地是一次行为保持型的结构迁移,按四个可独立部署的切片推进(详见 paid-access-schema.md 的 Migration 一节,均已应用):
- Slice 0(锚点):在
ModelVersion与ComicChapter上加写一次性的initialPublishedAt(仅在NULL时设置),回填COALESCE(earlyAccessConfig->>'originalPublishedAt', publishedAt),并让 expiry job 不再改写publishedAt——单靠这一步就独立修复了漫画"重新发布重启抢先体验时钟"的线上 bug,且不需要任何新表。 - Part 1(expand,可逆):建
PaidAccess表 + 枚举;只为当前被门控的版本回填(availability = 'EarlyAccess' AND (earlyAccessEndsAt > now() OR earlyAccessPermanent = true)——注意绝不能按"存在 config blob"回填,否则会把约 2.6 万个已过期版本映射成endsAt = NULL的"永久"行,永久付费墙掉免费内容);所有写路径(tRPC upsert、REST 端点、发布路径、donation-goal 完成与 expiry job 这两个裸 SQL 写入者)双写;旧的"已结束"编码反向映射(ended →now(),绝不写NULL);把约 37 处earlyAccessEndsAt读取连同旁边的availability = 'EarlyAccess'条件一起迁到共享助手。 - Part 1.5(flip,可逆):把
@civitai/buzz/paid-access.ts重实现为读PaidAccess(JSisPaidAccessActive+ SQLEXISTS子查询),主应用读取源翻转,旧列降级为双写备份。spoke 因自带 Kysely 谓词,单独在 Phase B 转换。 - Part 2(contract,破坏性,浸泡后执行):停止双写旧列、停止从付费路径写
availability='EarlyAccess';删除earlyAccessEndsAt/earlyAccessConfig/earlyAccessPermanent/earlyAccessTimeFrame四列;删除 DB 触发器(endsAt改为服务端从initialPublishedAt物化);expiry job 重定向为仅做缓存/搜索失效。发布门禁:必须先等 spoke 完成读取转换并部署,否则旧列一删,还在读旧列的 spoke 立即崩溃。Availability.EarlyAccess枚举值的移除是另一个独立、带日期的工单(涉及 Postgres 类型重建、事件引擎与 Meilisearch 文档形状、以及一次 reindex)。
快速接手指引
如果你要在当前代码库继续付费访问工作,最有效的入口依次是:
- 本文档的主骨架 paid-access-current-state.md(现状 + 待办 + 遗留发现);
- 数据形状与
terms语义 paid-access-schema.md(§1 表结构、§6 旧字段去向、§7 类型与 zod 边界); - 规则全集 monetization-rules.md(R1–R6 及"每条规则在哪个文件、是否覆盖所有写路径"的清单);
- 设计缘起 onsite-monetization-parity.md(为什么要物化
endsAt、为什么拒绝'infinity'哨兵、缓存的装饰 vs 谓词二分法); - 代码侧从共享助手 paid-access.ts 与 pricing-allowance.ts 读起,再进入主应用 paid-access.service.ts 与 spoke monetization/paid-access.ts。
牢记三条主线:一行即一门的PaidAccess表、endsAt物化带来的缓存安全与四态编码、双应用共享同一套纯函数规则、分头执行查询的防漂移架构——这三点串起了整个付费访问体系的设计与实现。
【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考