【免费下载链接】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点击查看免费下载本文基于 docs/prds/line_item_discount_implementation.md 展开。该功能为 flexprice 计费引擎引入了行项目级优惠券能力在原有发票级invoice-level优惠券的基础上允许将优惠券精确绑定到发票的某一行明细line item上。本文从请求模型、折扣计算算法、持久化事务、订阅计费自动收集到测试验证完整梳理这一能力的落地方式读者可以借此掌握如何在 flexprice 中构造行项目级折扣请求、理解其计算顺序与防负价机制并为扩展组合折扣等能力建立基础。功能定位为什么需要行项目级优惠券在 flexprice 的计费体系中优惠券Coupon此前以发票整体为作用域一张发票无论包含多少行明细优惠券折扣都作用于整张发票的总金额。这在多产品/多服务场景下存在明显局限——商户希望A 产品打 9 折、B 产品满减 $5而这类差异化折扣无法用单一发票级优惠券表达。行项目级优惠券Line Item Level Coupon解决了这一问题折扣可以精确作用到指定 line item以price_id或subscription_line_item_id作为匹配桥梁行项目级优惠券与发票级优惠券可以在同一张发票上共存并按固定顺序依次计算在订阅计费场景中系统能够自动识别订阅行项目上关联的优惠券并带入生成的发票无需人工干预。从 ent 数据模型 的字段设计看coupon_application表天然带有invoice_line_item_id字段coupon_association表带有subscription_line_item_id字段这为行项目级折扣提供了底层数据支撑。请求模型DTO 层的新增与校验新增InvoiceLineItemCoupon结构体行项目级优惠券的核心类型定义在 internal/api/dto/invoice.go// InvoiceLineItemCoupon represents a coupon applied to a specific invoice line item. // Only coupon ID is needed - the service will fetch and validate the coupon. type InvoiceLineItemCoupon struct { LineItemID string json:line_item_id validate:required // price_id used to match the line item SubscriptionLineItemID *string json:subscription_line_item_id,omitempty CouponID string json:coupon_id validate:required CouponAssociationID *string json:coupon_association_id,omitempty }字段语义说明字段是否必填作用line_item_id是用于匹配目标行明细语义上承载的是price_id价格 IDsubscription_line_item_id否订阅计费场景下可指定订阅行项目 ID优先于line_item_id匹配coupon_id是要应用的优惠券 ID服务层会重新拉取并校验优惠券coupon_association_id否订阅场景中优惠券关联记录 ID用于幂等与追溯注意LineItemID字段的注释明确说明它承载的是price_id在CreateInvoiceRequest的行明细中price_id是连接订阅行项目与发票行明细的桥梁。CreateInvoiceRequest扩展CreateInvoiceRequest 同时接收两类优惠券// Invoice Coupons InvoiceCoupons []InvoiceCoupon json:invoice_coupons,omitempty // Invoice Line Item Coupons LineItemCoupons []InvoiceLineItemCoupon json:line_item_coupons,omitemptyinvoice_coupons沿用原有的发票级优惠券line_item_coupons新增的行项目级优惠券采用 opt-in 设计——不传即为原有行为天然向后兼容。请求级校验InvoiceLineItemCoupon.Validate()internal/api/dto/invoice.go强制line_item_id与coupon_id非空CreateInvoiceRequest.Validate()会遍历LineItemCoupons逐条校验internal/api/dto/invoice.go并将错误统一标记为ErrValidation。此外ApplyCouponsToInvoiceRequestinternal/api/dto/invoice.go同样具备完整校验Invoice必须存在发票级与行项目级优惠券逐条校验并携带 index 与 coupon_id 等可报告细节便于定位非法请求。CreateInvoiceRequest.ToComputeRequest()internal/api/dto/invoice.go将两类优惠券一并透传给发票计算流程保证创建与计算链路的一致性。服务层统一的折扣计算引擎从单一方法到统一入口的实现演进原设计文档中描述的是ApplyCouponsOnInvoiceWithLineItems方法。从当前源码看该能力已收敛为一个统一入口ApplyCouponsToInvoiceinternal/ee/service/coupon_application.go它同时处理发票级与行项目级优惠券并且与CalculateCouponsForInvoiceinternal/ee/service/coupon_application.go共享同一套纯计算逻辑prepareCouponApplicationsinternal/ee/service/coupon_application.go。接口定义internal/ee/service/coupon_application.go清晰划分了两种调用语义ApplyCouponsToInvoice完整执行计算 持久化 计数赎回CalculateCouponsForInvoice只计算不落库用于发票预览preview确保预览不消耗优惠券额度。CouponCalculationResultinternal/ee/service/coupon_application.go返回三类折扣合计TotalDiscountAmount总折扣行项目折扣 发票级折扣之和TotalInvoiceLineItemDiscount行项目级折扣小计TotalInvoiceLevelDiscount发票级折扣小计AppliedCoupons逐优惠券的应用明细预览场景回填。计算流程行项目优先、发票级其次、防负价兜底prepareCouponApplications是整个折扣算法的核心其执行顺序严格遵循设计文档的约定预取优惠券fail fast先将发票级与行项目级的所有coupon_id收集起来一次性CouponRepo.List拉取types.NewNoLimitCouponFilter无分页限制构建couponsMap。随后逐一检查任一优惠券不存在立即报错ErrNotFound并携带missing_coupon_id避免在事务中才发现缺失。建立行明细索引遍历发票行明细构建两张查找表internal/ee/service/coupon_application.gosubsLineItemIdMapsubscription_line_item_id→ 行明细priceIdMapprice_id→ 行明细。resolveInvoiceLineItemToBeDiscountedinternal/ee/service/coupon_application.go先按subscription_line_item_id精确匹配失败再回退到price_id匹配——这正好对应文档中基于 price_id 映射订阅行项目优惠券到发票行明细的约定。先应用行项目级优惠券internal/ee/service/coupon_application.go逐条调用couponService.ApplyDiscount原价取目标行明细的Amount币种取发票币种成功则累加到该行明细的LineItemDiscount与totalLineItemDiscount目标行明细不存在或优惠券不可用时跳过并记录日志s.Logger.Info保证单条失败不影响整张发票同时为每条应用构建coupon_application.CouponApplication实体CouponSnapshot中写入applied_to: line_item、line_item_id、price_id等追溯信息。计算行项目折扣后的子总计internal/ee/service/coupon_application.gosubtotalAfterLineItemDiscounts为每个行明细Amount - LineItemDiscount之和——这是后续发票级折扣的计算基数确保了行项目折扣先于发票级折扣的语义。再应用发票级优惠券internal/ee/service/coupon_application.go使用runningSubTotal迭代累减每一张发票级优惠券的原价是上一步折扣后的剩余总额防负价关键点若runningSubTotal 0立即break跳过剩余优惠券并记录日志——从机制上杜绝过度折扣产生负数账单每张发票级优惠券同样生成CouponApplicationCouponSnapshot标记applied_to: invoice。结果返回汇总appliedCoupons、两类折扣合计与couponsMap交由调用方决定是纯预览还是持久化。一个算例验证计算顺序沿用设计文档的示例docs/prds/line_item_discount_implementation.md行明细 1price_123$50应用固定折扣优惠券coupon_line_item_5_off→ $5 off折后 $45行明细 2price_456$50无行项目折扣保持 $50子总计行项目折扣后 $95发票级 10% 优惠券coupon_invoice_10_percent作用于 $95 → $9.50 off最终合计 $85.50。该算例正确体现了行项目折扣基于原始金额、发票级折扣基于行项目折扣后总额的分层语义。持久化与事务原子应用、幂等保护、折扣分发ApplyCouponsToInvoice的持久化阶段全部包裹在s.DB.WithTx单数据库事务中internal/ee/service/coupon_application.go与计算阶段纯函数、持久化阶段集中变更的设计一致幂等保护recompute 安全对每个出现的coupon_id先按(invoice_id, coupon_id)维度Count已存在的CouponApplication行internal/ee/service/coupon_application.go。已存在则标记alreadyPersisted后续跳过插入。这保证了ComputeInvoice重试、发票最终化finalization重算等场景不会产生重复的优惠券应用记录。赎回计数redemption仅对一次性优惠券CouponAssociationID为空执行CouponRepo.IncrementRedemptions并受MaxRedemptions约束订阅关联优惠券在createCouponAssociation阶段已计数此处跳过避免按发票数重复累加。插入应用记录逐条CouponApplicationRepo.Create落库。发票级折扣分发若totalInvoiceLevelDiscount非零且存在行明细调用invoiceService.DistributeInvoiceLevelDiscount将发票级折扣按比例分摊到各行明细的InvoiceLevelDiscount字段internal/ee/service/coupon_application.go。回写行明细凡LineItemDiscount或InvoiceLevelDiscount非零的行明细通过InvoiceLineItemRepo.Update持久化internal/ee/service/coupon_application.go。整个流程伴随s.Logger.Info/Debug/Error的完整日志埋点——入口记录优惠券数量与原始总额每条应用记录折扣明细最终汇总total_discount与applied_coupon_count构成审计追踪的基础。发票服务集成预览不烧券计算全链路发票服务internal/ee/service/invoice.go围绕行项目级折扣提供两类集成点applyPreviewCouponsinternal/ee/service/invoice.go预览发票时若InvoiceCoupons或LineItemCoupons任一非空即调用CalculateCouponsForInvoice计算折扣并设置inv.TotalDiscount返回逐优惠券明细不落库、不计赎回。预览流程还明确先折扣后计税——GetPreviewInvoice中先应用预览优惠券再以inv.TotalDiscount为基础计算税internal/ee/service/invoice.go。正式发票创建/计算路径则通过ApplyCouponsToInvoice完成计算与持久化。这解释了设计文档中ApplyCouponsOnInvoiceWithLineItems 处理两类优惠券的最终形态预览与真实应用共用同一套prepareCouponApplications纯计算差异仅在是否走事务持久化。订阅计费集成自动收集与 price_id 映射在订阅账单生成链路中billing 服务internal/ee/service/billing.go实现了设计文档描述的自动收集行项目优惠券PrepareSubscriptionInvoiceRequest通过selectSubscriptionCoupons拉取该订阅活跃的优惠券关联并按订阅级/行项目级拆分sel.SubLevel/sel.LineLevel同时维护sel.SubLineItemIDToPriceID的映射关系internal/ee/service/billing.go订阅级优惠券原样收集为InvoiceCoupon携带CouponAssociationID行项目级优惠券则以 price_id 为桥梁转成InvoiceLineItemCoupon只有当订阅行项目的 price_id 确实出现在发票行明细集合invoiceLineItemPriceIDs中时才纳入internal/ee/service/billing.go避免对不存在于当期发票的行项目空挂折扣最终两类优惠券随CreateInvoiceRequest一并进入发票生成流程internal/ee/service/billing.go。对于分组计费grouped invoicing场景子发票请求的行项目级优惠券会在合并到父发票时被保留合并invReq.LineItemCoupons append(...)internal/ee/service/billing.go并有专门测试覆盖子发票行项目优惠券合并到父发票的行为。数据库模型支撑行项目级折扣的数据落点在既有的表结构中设计文档指出现有 schema 已支持行项目折扣coupon_application表带invoice_line_item_id字段每条折扣应用记录可精确指向发票行明细ent/schema/coupon_application.gocoupon_association表带subscription_line_item_id字段用于在订阅层面表达该订阅行项目挂了哪些优惠券ent/schema/coupon_association.goinvoice_line_item实体存在指向coupon_applications的边关系且行明细实体上有LineItemDiscount与InvoiceLevelDiscount两个金额字段分别承载两类折扣ent/schema/invoice_line_item.go。外键关系在数据库层面保证了数据一致性三层校验请求层 → 业务逻辑层 → 数据库层与设计文档的描述一致。测试验证边界行为有据可依仓库中针对该功能的测试集中在 internal/ee/service/coupon_application_test.go可验证以下关键行为一次性行项目优惠券的赎回计数每次 one-off 应用TotalRedemptions递增达到MaxRedemptions后优惠券被静默跳过折扣为零且不落CouponApplication行internal/ee/service/coupon_application_test.go同券双作用域只计一次同一张发票上同一优惠券同时出现在行项目级与发票级列表时TotalRedemptions只 1internal/ee/service/coupon_application_test.go订阅关联优惠券不再重复计数携带CouponAssociationID的优惠券不触发增量internal/ee/service/coupon_application_test.go。此外 internal/ee/service/billing_grouped_invoicing_coupon_test.go 验证了分组计费下子发票行项目优惠券的合并保留行为。完整请求示例与实战要点结合设计文档的示例与 DTO 定义一次带行项目级折扣的一次性发票创建请求如下{ customer_id: cust_123, currency: usd, amount_due: 100.00, line_items: [ { price_id: price_123, amount: 50.00, quantity: 1 }, { price_id: price_456, amount: 50.00, quantity: 1 } ], invoice_coupons: [ { coupon_id: coupon_invoice_10_percent, type: percentage, percentage_off: 10 } ], line_item_coupons: [ { line_item_id: price_123, coupon_id: coupon_line_item_5_off, type: fixed, amount_off: 5.00 } ] }实战要点总结line_item_id语义为 price_id它只负责匹配到哪一行优惠券的真实类型与金额由服务层从coupon_id拉取定义请求体中给出的type/amount_off/percentage_off仅为表达性字段应用顺序不可颠倒行项目级折扣先基于原始金额计算发票级折扣后基于行项目折扣后子总计计算否则结果不一致防负价内置发票级优惠券遇到runningSubTotal 0即停止无需调用方自行防呆订阅场景自动生效只要在订阅行项目上建立了优惠券关联生成的账单会自动带上line_item_coupons无需手工填写幂等安全同一张发票的重复计算不会产生重复应用记录可放心依赖重试机制。向后兼容与扩展性向后兼容原有发票级优惠券的全部行为不变既有 API 端点继续工作行项目级优惠券完全通过新字段line_item_couponsopt-in 启用docs/prds/line_item_discount_implementation.md审计与追溯CouponApplication.CouponSnapshot记录了每张优惠券应用的层级applied_to、目标行明细与 price_id配合结构化日志可完整还原每张发票的折扣构成扩展基础统一的prepareCouponApplications纯计算与计算/持久化分离的结构为后续 bundle 折扣组合套餐、tiered pricing阶梯定价等复杂定价策略预留了清晰的扩展点——新折扣类型只需复用同一套 coupon 拉取、行明细匹配与事务持久化骨架。相关资源设计文档docs/prds/line_item_discount_implementation.mdDTO 与校验internal/api/dto/invoice.go折扣计算引擎internal/ee/service/coupon_application.go发票服务集成internal/ee/service/invoice.go订阅计费自动收集internal/ee/service/billing.go测试用例internal/ee/service/coupon_application_test.go、internal/ee/service/billing_grouped_invoicing_coupon_test.go数据模型ent/schema/coupon_application.go、ent/schema/coupon_association.go、ent/schema/invoice_line_item.go赞分享【免费下载链接】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 行项目级折扣Line Item Level Discount实现指南订阅与发票上的多层级优惠券体系FlexPrice 行项目级折扣Line Item Level Discount实现指南订阅与发票上的多层级优惠券体系 导读 本文围绕 FlexPriceFlexPrice 发票行项目与计费周期跟踪Invoice Line Items and Period 的完整实现指南FlexPrice 发票行项目与计费周期跟踪Invoice Line Items and Period 的完整实现指南 导读 本文基于 FlexPrice 仓WebShell检测与防御10个实用工具和最佳实践WebShell检测与防御10个实用工具和最佳实践 WebShell是一种恶意脚本攻击者通过它可以控制服务器窃取数据或发起进一步攻击。本文将介绍10个实用上一篇在 Webiny 中构建跨 API 与 Admin 的全栈扩展Full-Stack Developer 架构规范与实践指南下一篇Windows热键冲突终极解决方案Hotkey Detective快速定位占用进程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考