设计指南:从单一 Plan 到多产品组合的架构演进`)
【免费下载链接】flexpriceUsage-based pricing and billing for developers Cloud or self-hosted ⚙️ No-code UI Realtime usage metering Credits top-ups Control feature access项目地址https://gitcode.com/gh_mirrors/fl/flexprice点击查看免费下载导读本文基于 FlexPrice 仓库中的设计文档 docs/prds/subscription_line_items.md完整讲解订阅行项目Subscription Line Items的诞生背景、数据模型设计、仓储层接口、API 结构、分阶段落地路径与迁移策略。读者将掌握为什么单一plan_id的订阅模型无法满足多产品混搭场景SubscriptionLineItem实体如何通过price_id quantity billing_period的组合精确定位计费配置以及 FlexPrice 当前仓库ent/schema/subscription_line_item.go、internal/domain/subscription/中该设计如何被真实落地并扩展出承诺消费commitment、Addon 关联等能力。一、问题陈述旧订阅模型的四大局限在引入行项目之前FlexPrice 的订阅系统被设计为一个订阅只对应一个计划这种模型在快速增长的按量计费场景中暴露出四个结构性限制单一 Plan 限制Single Plan Limitation每个订阅通过唯一的plan_id绑定一个计划客户无法在同一订阅内同时订购多个产品/计划。价格选择歧义Price Selection Ambiguity一个计划下可能同时存在多个币种与计费周期相同的价格系统无法确定客户究竟选择了哪一个价格。计划版本缺失Plan Version Tracking系统不追踪计划版本当计划被更新时难以维持价格的一致性即历史订阅应沿用旧价新订阅采用新价。产品组合受限Limited Product Flexibility客户无法在单个订阅中自由混搭不同的产品及其关联价格。现有架构的约束从 internal/domain/subscription/model.go 中的Subscription领域模型可以看到旧结构的痕迹订阅通过PlanID字段与计划强耦合币种Currency与计费周期BillingPeriod、BillingPeriodCount存储在订阅层级价格只能通过计划间接访问不存在指向具体价格的直接链接。其直接后果是无法在一个订阅中承载多个产品/计划计划与价格没有版本概念同一币种/周期存在多个价格时选择不明确订阅的组成方式缺乏灵活性。二、核心方案SubscriptionLineItem实体设计文档给出的解题思路是把订阅 - 计划的一对一关系拆解为订阅 - 多个行项目每个行项目独立承载price_id、quantity、currency、billing_period等信息从而让一个订阅可以自由组合任意数量的产品与价格。2.1 设计文档中的 Schema 原型PRD 中给出的SubscriptionLineItement 字段原型如下节选自 docs/prds/subscription_line_items.mdidvarchar(50)唯一、不可变subscription_id/customer_idvarchar(50)必填、不可变plan_id/plan_display_name可选、可空price_idvarchar(50)必填行项目与价格直接挂钩的核心price_type/meter_id/meter_display_name可选、可空display_name可选、可空quantitydecimal.DecimalPostgres 中映射为numeric(20,8)默认decimal.Zerocurrencyvarchar(10)必填billing_periodvarchar(50)必填start_date/end_datefield.Time可选、可空metadataJSON映射为jsonb可选。实体级联定义一条从subscription反向引用的边edge通过subscription_id字段关联Unique Required Immutable。索引设计针对高频查询路径(tenant_id, subscription_id, status)(tenant_id, customer_id, status)(tenant_id, plan_id, status)(tenant_id, price_id, status)(tenant_id, meter_id, status)(start_date, end_date)2.2 仓库中的实际落地Schema 的完整演进对比设计文档仓库中真实实现的 ent/schema/subscription_line_item.go 在原型基础上做了大量扩展这是理解PRD 如何变成生产代码的最佳样本多租户与多环境混入Mixin实际 Schema 注入了baseMixin.BaseMixin{}与baseMixin.EnvironmentMixin{}见 ent/schema/mixin/因此字段中自动带有tenant_id、environment_id、status、created_by、created_at等基础审计与隔离字段。相应地索引前缀从tenant_id扩展为tenant_id environment_id例如index.Fields(tenant_id, environment_id, subscription_id, status), index.Fields(tenant_id, environment_id, customer_id, status), index.Fields(tenant_id, environment_id, entity_id, entity_type, status), index.Fields(tenant_id, environment_id, price_id, status), index.Fields(tenant_id, environment_id, meter_id, status), index.Fields(subscription_id, status),其中(tenant_id, environment_id, subscription_id, price_id, entity_type)是一个部分索引entsql.IndexWhere带注释说明它专门服务于 plan-price 同步发现 CTEplan_price_sync_v2.go中的 line item already exists? NOT EXISTS 守卫查询entity_type作为尾键列使该索引对plan、addon、subscription三类实体类型的探测均为索引覆盖扫描。实体与来源追踪新增了entity_id/entity_type默认InvoiceLineItemEntityTypePlan不可变用于标识行项目来源实体计划、Addon 还是订阅级价格subscription_phase_id记录所属订阅阶段不可变addon_association_id记录该行项目由哪次 Addon 附加操作创建不可变非 Addon 来源则为空。价格快照字段除price_id外落地版还保存price_type、price_unit_id、price_unit使行项目不依赖实时查询价格对象即可完成计费展示。计费周期细化billing_period使用强类型types.BillingPeriod并新增billing_period_count创建时从价格快照默认 1与invoice_cadence不可变源码注释标注 TODO: Remove this once we have migrated all the data表明这是历史数据迁移期的过渡字段。承诺消费Commitment字段族commitment_amount、commitment_quantitynumeric(20,8)、commitment_type、commitment_overage_factornumeric(10,4)、commitment_true_up_enabled、commitment_windowed、commitment_durationvarchar(50)、commitment_time_bucketsJSONB类型types.TimeOfDayBuckets。这些字段支撑最低承诺消费 超额计费 True-up 补差 分桶承诺等高级计费能力。额外的边除指向subscription的反向边外实际 Schema 还定义了指向coupon_association的出边edge.To(coupon_associations, CouponAssociation.Type)表明每个行项目可以挂载多个优惠券关联。2.3 领域模型与行为方法internal/domain/subscription/line_item.go 将 ent 实体转换为领域对象SubscriptionLineItemFromEnt/GetLineItemFromEntList并暴露一组计费引擎高频使用的行为方法IsActive(t time.Time)仅当状态为published且start_date t end_date时判定为活跃支持事件后处理时传入事件时间戳而非time.Now()IsUsage()price_type USAGE且meter_id非空用于识别用量计费行项目IsOneTime()billing_period ONETIME识别一次性收费HasCommitment()/HasAnyCommitment()/HasTrueUpEnabled()承诺消费相关判定HasAnyCommitment同时考虑顶层承诺与按时间桶承诺GetPeriodStart(defaultPeriodStart)/GetPeriodEnd(defaultPeriodEnd)将行项目的start_date/end_date与账单周期边界做max/min裁剪防止行项目在周期中途创建时被重复计费。订阅模型本身在 internal/domain/subscription/model.go 中新增了LineItems []*SubscriptionLineItemJSON 序列化为line_items字段并提供了GetLineItems()与HasMixedBillingPeriods()判断订阅内是否存在多种计费周期多周期计费的关键判据等辅助方法。三、仓储层接口设计与事务语义3.1 行项目仓储接口设计文档给出了SubscriptionLineItemRepository的骨架Create / Get / Update / Delete / CreateBulk / ListBySubscription / ListByCustomer / GetByPriceID / GetByPlanID。仓库中的真实实现 internal/domain/subscription/line_item_repository.go 在此基础上调整为type LineItemRepository interface { Create(ctx context.Context, lineItem *SubscriptionLineItem) error CreateBulk(ctx context.Context, lineItems []*SubscriptionLineItem) error Get(ctx context.Context, id string) (*SubscriptionLineItem, error) GetForUpdate(ctx context.Context, id string) (*SubscriptionLineItem, error) // 事务内行锁 Update(ctx context.Context, lineItem *SubscriptionLineItem) error BulkTerminate(ctx context.Context, subscriptionID string, effectiveDate time.Time) (int, error) Delete(ctx context.Context, id string) error ListBySubscription(ctx context.Context, sub *Subscription) ([]*SubscriptionLineItem, error) List(ctx context.Context, filter *types.SubscriptionLineItemFilter) ([]*SubscriptionLineItem, error) Count(ctx context.Context, filter *types.SubscriptionLineItemFilter) (int, error) GetDistinctCustomerIDsWithCommitmentTrueUp(ctx context.Context) ([]string, error) SubscriptionIDsWithWindowedCommitment(ctx context.Context) ([]string, error) }与 PRD 骨架相比的实战差异GetForUpdate用于在周边事务中锁定行项目防止并发修改BulkTerminate支持一次性按生效日期终止某订阅的全部行项目计划变更/切换的核心操作ListBySubscription改为接收*Subscription对象以利用其上下文新增的承诺消费查询接口服务于 True-up 与窗口化承诺的批量调度扫描。ent 实现的仓储位于 internal/repository/ent/subscription_line_item.go内存测试实现位于 internal/testutil/inmemory_subscription_line_item_store.go。3.2 订阅仓储的扩展internal/domain/subscription/repository.go 中Repository接口按 PRD 要求增加了两个事务方法CreateWithLineItems(ctx context.Context, subscription *Subscription, items []*SubscriptionLineItem) error GetWithLineItems(ctx context.Context, id string) (*Subscription, []*SubscriptionLineItem, error)前者保证订阅 行项目在同一事务内原子写入后者一次查询同时返回订阅及其全部行项目被订阅创建、查询、变更、发票生成等流程广泛调用。3.3 行项目构建器internal/domain/subscription/line_item_builder.go 提供subscriptionLineItemBuilder支持从既有行项目深拷贝含 metadata 与 commitment_time_buckets后链式修改WithQuantity、WithStartDate、WithEndDate、WithPlan(planID, planName)同时设置entity_id/entity_type/plan_display_name、WithPrice(p)将行项目指向另一价格并同步price_type、billing_period、billing_period_count、invoice_cadence、price_unit、meter_id等镜像字段。这为价格换绑计划升级等操作提供了统一且不易出错的实现路径。四、API 结构创建请求与校验规则4.1 设计文档的初始 API创建阶段PRD 规划了第一阶段仅支持创建的 APItype CreateSubscriptionRequest struct { CustomerID string json:customer_id validate:required Currency string json:currency validate:required BillingPeriod string json:billing_period validate:required StartDate time.Time json:start_date,omitempty BillingAnchor time.Time json:billing_anchor,omitempty PlanID string json:plan_id,omitempty // 向后兼容 LineItems []SubscriptionLineItemRequest json:line_items,omitempty Metadata map[string]string json:metadata,omitempty } type SubscriptionLineItemRequest struct { PriceID string json:price_id validate:required Quantity decimal.Decimal json:quantity validate:required DisplayName string json:display_name,omitempty Metadata map[string]string json:metadata,omitempty }PlanID保留为可选字段用于向后兼容老客户端仍可只传plan_id创建订阅新客户端则通过line_items数组自由组合价格。4.2 仓库中的实际请求模型DTO 层拆分为两个文件internal/api/dto/subscription.go 与 internal/api/dto/subscription_line_item.go。CreateSubscriptionLineItemRequest的实际字段远比 PRD 丰富type CreateSubscriptionLineItemRequest struct { PriceID string // 引用既有价格plan / addon / subscription-scoped Price *SubscriptionPriceCreateRequest // 内联定义新价格服务端创建订阅级价格后挂载 Quantity decimal.Decimal StartDate *time.Time EndDate *time.Time Metadata map[string]string DisplayName string SubscriptionPhaseID *string SkipEntitlementCheck bool // 内部使用跳过资格校验 ProrationBehavior types.ProrationBehavior // 默认 none CommitmentAmount / CommitmentQuantity / CommitmentType / CommitmentOverageFactor / ... CommitmentTrueUpEnabled / CommitmentWindowed / CommitmentDuration / CommitmentTimeBuckets }关键校验规则Validate方法price_id与price必须二选一同时提供或同时缺失均报ErrValidationstart_date不得晚于end_date一次性计费价格忽略请求中的end_date行项目结束日期恒为start_date 1 天并钳制在订阅结束日期内提供订阅上下文时行项目及内联价格的起止日期必须落在订阅起止日期范围内订阅创建时line_items数量上限为100条见SubscriptionCreationConfig.Validate。CreateSubscriptionRequest还演化出更全面的SubscriptionCreationConfig支持OverrideLineItems覆盖计划价格、OverrideEntitlements、Addons、LineItemCommitments按price_id键控的承诺配置、Phases订阅阶段等每一层的ApplyDefaults()都会为承诺配置补上默认超额因子types.DefaultOverageFactor()。4.3 行项目的变更接口PRD 未来项的落地PRD 将UpdateLineItemRequest列为未来阶段仓库已将其实现为UpdateSubscriptionLineItemRequesteffective_from默认当前时间用于生效时间替换、amount/tier_mode/tiers/transform_quantity/bucket_size等价格覆盖字段以及完整的承诺字段族DeleteSubscriptionLineItemRequesteffective_from与proration_behavior控制周期中段的贷项发放默认none。配合 internal/ee/service/subscription_modification_quantity.go 等模块行项目的增删改都已具备生产级能力。五、服务层集成创建流程与发票计算以SubscriptionService.CreateSubscription为例internal/ee/service/subscription.go行项目贯穿整个创建链路请求校验SubscriptionCreationConfig.Validate含 100 条上限与逐项校验价格解析将每个price_id解析为价格对象必要时通过SubscriptionPriceCreateRequest.ToCreatePriceRequest为内联价格创建订阅级价格币种与实体从订阅上下文继承起止日期缺省取订阅开始日期试用窗口设置setCreateSubscriptionTrialWindow构建sub.LineItems并调用s.SubRepo.CreateWithLineItems(ctx, sub, sub.LineItems)原子落库创建订阅首张发票invoiceService.CreateSubscriptionInvoice行项目逐条生成发票行创建订阅级信用授予、结算/结账checkout等后置副作用。后续的订阅查询GetWithLineItems、用量统计行项目级GetUsageBySubscription、计划变更BulkTerminate 重建行项目与发票生成internal/ee/service/invoice.go均以行项目为基本计费单元。仓储的 ent 实现与内存实现分别位于 internal/repository/ent/subscription.go 和 internal/testutil/inmemory_subscription_store.go后者被 internal/ee/service/subscription_line_item_test.go、internal/ee/service/subscription_addon_test.go 等大量测试用于验证行项目相关业务逻辑。六、实施阶段与迁移策略Phase 1核心基础设施创建SubscriptionLineItem实体与 Schema实现仓储层含CreateWithLineItems事务写入改造订阅创建流程实现向后兼容层保留plan_id入参基础校验与错误处理。Phase 2API 增强订阅查询返回行项目GetWithLineItems行项目级用量跟踪错误处理与校验增强文档更新。Phase 3高级特性未来行项目管理端点增删改分摊Proration处理每行项目独立计费周期批量操作支持。迁移策略三轨并行数据库迁移新建subscription_line_items表且不破坏现有表为存量订阅回填数据。仓库中的 migrations/versioned/postgres/20260819000000_baseline.sql 与 migrations/baseline/postgres_baseline_20260819.sql 即为subscription_line_items表的基线定义含numeric(20,8)数量列、jsonb元数据列、外键关系与索引。代码迁移新特性置于功能开关feature flag之后逐步放量以保证稳定性invoice_cadence字段上的 TODO: Remove this once we have migrated all the data 注释正是这种过渡期字段策略的实例。客户端迁移提供迁移指南过渡期内新旧 API 并存plan_id与line_items均可作为创建入参。七、监控与度量迁移期间应重点跟踪新旧 API 端点的使用量对比性能指标尤其GetWithLineItems关联查询与部分索引命中情况迁移期错误率订阅创建成功率。八、未来演进方向PRD 展望每个行项目支持不同的计费周期与账单周期对齐选项多周期计费仓库中HasMixedBillingPeriods与 internal/ee/service/multi_cadence_addon_matrix_test.go 已为此奠定基础高级分摊规则ProrationBehavior已支持immediate/next_period/none等取值行项目批量操作增强的报告能力按行项目聚合收入行项目级试用期管理自定义显示名覆盖display_name/plan_display_name/meter_display_name行项目级元数据处理metadataJSONB用量聚合的进一步改进行项目级用量过滤、分桶聚合。总结Subscription Line Items是 FlexPrice 从订阅-计划二元模型走向订阅-多行项目-多价格组合模型的关键架构升级。设计文档解决了为什么改单一计划、价格歧义、版本缺失、组合受限与怎么改Schema、仓储、API、分阶段实施、迁移监控而仓库源码则证明了这套设计已完整落地并被进一步扩展多租户/多环境隔离、实体来源追踪、价格快照、承诺消费字段族、部分索引优化、行项目构建器、变更与删除接口以及贯穿创建-发票-用量-变更全链路的服务集成。对于希望理解 FlexPrice 计费核心或在其基础上扩展多产品订阅能力的开发者docs/prds/subscription_line_items.md 是权威的起点本文引用的 schema、领域模型、仓储接口与 DTO 则是逐行对应的实现索引。赞分享【免费下载链接】flexpriceUsage-based pricing and billing for developers Cloud or self-hosted ⚙️ No-code UI Realtime usage metering Credits top-ups Control feature access项目地址https://gitcode.com/gh_mirrors/fl/flexprice点击查看免费下载相关推荐OpenCart 4 订阅产品Subscription Products完整配置指南从订阅计划设计到自动续费落地OpenCart 4 订阅产品Subscription Products完整配置指南从订阅计划设计到自动续费落地 OpenCart 4 原生内置订阅产品能电商后端TypeGraphQL 订阅Subscriptions完全指南从 Subscription 装饰器到生产级 PubSub 架构TypeGraphQL 订阅Subscriptions完全指南从 Subscription 装饰器到生产级 PubSub 架构 导读 本文以 Type后端GraphQLAPI设计Flexprice 订阅调度与阶段Subscription Schedules Phases设计解析从未来日期变更到自动执行落地Flexprice 订阅调度与阶段Subscription Schedules Phases设计解析从未来日期变更到自动执行落地 导读 订阅调度Su上一篇Lottery 抽奖系统实战使用 Kafka MQ 解耦抽奖与发货流程下一篇PaddleSeg QualityInspector 中的 Mask R-CNN 实例分割模型库与配置指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。