【免费下载链接】flexprice
Usage-based pricing and billing for developers 🔓 Cloud or self-hosted ⚙️ No-code UI 💰 Realtime usage metering 🎟 Credits & top-ups 🔑 Control feature access
本篇技术指南基于 FlexPrice 开源仓库中的 支付系统设计文档 展开,系统讲解该计费平台的支付模块如何统一承载线下(Offline)、钱包 Credits、银行卡(Card)与 ACH 等多种支付方式,如何通过 Payment 与 PaymentAttempt 两级实体实现幂等处理、尝试追踪与发票对账。读完本文,你将掌握支付实体的完整字段设计、状态机与合法迁移规则、处理流程的调用链,以及可直接调用的 REST API 用法,并能结合仓库源码(ent/schema/payment.go、internal/domain/payment/model.go、internal/types/payment.go)深入理解底层实现。
一、设计背景与目标
FlexPrice 是一个面向开发者的用量计费与定价平台,支付系统是其商业化闭环的关键一环。该支付系统的设计目标在文档中明确为:
- 支持多种支付方式:线下/人工支付(Offline)、钱包 Credits、银行卡(Card)、ACH 银行转账;
- 支付生命周期与尝试独立追踪:一笔支付(Payment)可对应多次处理尝试(Attempt),互不混淆;
- 与发票和钱包事务保持对账一致:支付成功后自动更新发票的支付状态与已付金额,Credits 支付自动扣减钱包余额;
- 幂等处理:通过全局唯一的 idempotency_key 防止重复创建与重复处理;
- 细粒度状态追踪与错误处理:记录每个状态节点的时间戳与错误信息,形成完整审计轨迹。
从源码看,该目标已落实到三层结构:ENT 持久层(Schema)→ 领域模型(Domain)→ 类型与状态机(Types)。下面逐层拆解。
二、支付方式与支付目的地
2.1 支付方式(Payment Methods)
文档定义了四类支付方式,但结合 internal/types/payment.go 的PaymentMethodType枚举,当前仓库实际支持六种类型,比文档规划更进一步:
| 类型常量 | 枚举值 | 说明 |
|---|---|---|
PaymentMethodTypeOffline | OFFLINE | 线下/人工支付,无需 payment_method_id,自动成功且不追踪尝试 |
PaymentMethodTypeCredits | CREDITS | 钱包 Credits 支付,需要有效 wallet ID 作为 payment_method_id,完整追踪尝试与事务历史 |
PaymentMethodTypeCard | CARD | 银行卡支付,接入支付网关,需要有效 card ID 作为 payment_method_id |
PaymentMethodTypeACH | ACH | ACH 银行转账,接入支付网关,需要有效 bank account ID 作为 payment_method_id |
PaymentMethodTypePaymentLink | PAYMENT_LINK | 支付链接支付,payment_method_id 必须为空 |
PaymentMethodTypeUPI | UPI | UPI 支付(仓库扩展) |
上述校验逻辑并非只停留在文档层面。在 internal/domain/payment/model.go 的Payment.Validate()中可以看到完整的字段校验实现:
- 金额必须大于 0(
amount为decimal.Decimal,使用 shopspring/decimal 精确计算,底层对应numeric(20,8)); OFFLINE与PAYMENT_LINK类型禁止携带payment_method_id,否则返回ErrValidation;CARD类型下payment_method_id可选,处理器会自动获取已保存的支付方式;- 其余类型必须提供
payment_method_id。
2.2 支付目的地(Payment Destinations)
文档定义了INVOICE目的地,支持订阅发票与一次性发票,自动更新支付状态、处理部分支付,并做币种匹配校验。而仓库在 internal/types/payment.go 中进一步扩展出CUSTOMER目的地——用于"为客户 tokenize 支付方式"的场景(先发起一笔认证扣款(auth charge),拿到 token 后将该支付 void 掉)。
2.3 网关抽象
文档提到payment_gateway字段(STRIPE、RAZORPAY,非网关支付为 null)。源码中该字段在 ent/schema/payment.go 为可空字符串,并额外增加了gateway_tracking_id(网关跟踪 ID)与gateway_metadata(网关专用元数据,jsonb)。领域模型 model.go 将其定义为*string指针类型,体现"网关信息仅在网关支付时存在"的设计。
三、核心实体:Payment 与 PaymentAttempt
3.1 Payments 表
文档给出 DDL 设计,仓库中 ENT 框架通过 ent/schema/payment.go 声明等价结构(含 BaseMixin 与 EnvironmentMixin 注入的tenant_id、environment_id、status、created_at等公共字段):
CREATE TABLE payments ( id VARCHAR(50) PRIMARY KEY, tenant_id UUID NOT NULL, environment_id UUID NOT NULL, -- 仓库新增:环境隔离 idempotency_key VARCHAR(50) UNIQUE NOT NULL, destination_type VARCHAR(50) NOT NULL, -- INVOICE, CUSTOMER destination_id VARCHAR(50) NOT NULL, payment_method_type VARCHAR(50) NOT NULL, -- OFFLINE, CREDITS, CARD, ACH, PAYMENT_LINK, UPI payment_method_id VARCHAR(50), -- Offline / PaymentLink 为空 payment_gateway VARCHAR(50), -- STRIPE, RAZORPAY, null gateway_payment_id VARCHAR(255), -- 网关外部支付 ID gateway_tracking_id VARCHAR(255), -- 仓库新增:网关跟踪 ID gateway_metadata JSONB, -- 仓库新增:网关专用元数据 amount NUMERIC(20,8) NOT NULL, currency VARCHAR(10) NOT NULL, payment_status VARCHAR(50) NOT NULL, track_attempts BOOLEAN NOT NULL DEFAULT false, metadata JSONB, succeeded_at TIMESTAMP WITH TIME ZONE, failed_at TIMESTAMP WITH TIME ZONE, refunded_at TIMESTAMP WITH TIME ZONE, voided_at TIMESTAMP WITH TIME ZONE, -- 仓库新增 recorded_at TIMESTAMP WITH TIME ZONE, -- 仓库新增:线下入账时间 error_message TEXT, status VARCHAR(20) NOT NULL DEFAULT 'published', created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, created_by VARCHAR(50) NOT NULL, updated_by VARCHAR(50) NOT NULL );索引设计要点(ent/schema/payment.go):
idx_tenant_destination_status:按租户+目的地(类型/ID)+状态查询,支撑"某张发票的全部支付记录"这类高频查询;idx_tenant_payment_method_status:按支付方式类型/ID+状态查询;idx_tenant_gateway_payment:部分索引(payment_gateway IS NOT NULL AND gateway_payment_id IS NOT NULL),用于按网关 ID 反查;idx_tenant_environment_payment_idempotency_key_unique:在(tenant, environment)维度上唯一,使不同租户可复用相同的调用方 idempotency key 而不冲突,同时保证幂等。仓库还专门导出了该约束名常量,供 repository 在翻译数据库冲突错误时做模式匹配(见 ent/schema/payment.go)。
3.2 PaymentAttempts 表
CREATE TABLE payment_attempts ( id VARCHAR(50) PRIMARY KEY, tenant_id UUID NOT NULL, environment_id UUID NOT NULL, payment_id VARCHAR(50) NOT NULL REFERENCES payments(id), attempt_number INTEGER NOT NULL, -- 默认 1,必须为正数 payment_status VARCHAR(20) NOT NULL, -- SUCCEEDED / FAILED gateway_attempt_id VARCHAR(255), -- 网关侧尝试 ID error_message TEXT, metadata JSONB, status VARCHAR(20) NOT NULL DEFAULT 'published', created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, created_by VARCHAR(50) NOT NULL, updated_by VARCHAR(50) NOT NULL, UNIQUE(payment_id, attempt_number) );仓库实现见 ent/schema/payment_attempt.go,与文档 DDL 相比的关键差异:
payment_status由VARCHAR(50)收敛为VARCHAR(20),且领域校验只允许SUCCEEDED或FAILED两种终态(见 model.go),说明"尝试"是支付结果的快照而非中间状态;- 唯一约束
(payment_id, attempt_number)保证重试编号不冲突(StorageKeyidx_payment_attempt_number_unique); - 通过
edge.To("attempts", PaymentAttempt.Type)(ent/schema/payment.go)与edge.From("payment", ...)(ent/schema/payment_attempt.go)建立了双向关联,FromEnt转换时会自动加载Edges.Attempts填充尝试列表(model.go)。
四、支付处理流程
4.1 创建与处理时序
文档给出了完整的创建+处理时序图,核心流程为:
- 创建支付(CreatePayment):校验请求 → 生成幂等键 → 以
PENDING状态落库; - 按需立即处理(process_payment=true):进入 PaymentProcessor → 创建 Attempt → 按支付方式分发处理 → 更新 Payment 与 Attempt 状态;
- 成功后执行 Post-Processing(更新发票状态、处理部分支付、更新钱包事务等)。
仓库中该流程的落点为 internal/api/v1/payment.go(REST 处理器)与 internal/domain/payment/repository.go(持久化层),其中IsIdempotencyKeyConflict(err error)用于把数据库唯一约束冲突翻译成语义化的幂等错误。
4.2 按支付方式分发处理
- Offline:无需网关,直接标记成功(
recorded_at记录人工入账时间点); - Credits:调用钱包服务扣减余额并生成钱包事务,实现自动对账;
- Card/ACH:交由 Stripe、Razorpay 等网关处理,状态通过 Webhook 回传更新(
gateway_payment_id/gateway_tracking_id关联外部单据)。
4.3 支付状态机
文档的状态图只覆盖了PENDING → PROCESSING → SUCCEEDED/FAILED与SUCCEEDED → REFUNDED/PARTIALLY_REFUNDED。仓库在 internal/types/payment.go 中实现了更完整的 9 种状态,并在paymentStatusTransitions表中严格定义了合法迁移矩阵:
| 状态 | 可迁移至 |
|---|---|
INITIATED | INITIATED, PENDING, PROCESSING, SUCCEEDED, OVERPAID, FAILED |
PENDING | PENDING, PROCESSING, SUCCEEDED, OVERPAID, FAILED |
PROCESSING | PROCESSING, SUCCEEDED, OVERPAID, FAILED |
SUCCEEDED | SUCCEEDED, OVERPAID, REFUNDED, PARTIALLY_REFUNDED, VOIDED |
OVERPAID | OVERPAID, REFUNDED, PARTIALLY_REFUNDED |
PARTIALLY_REFUNDED | PARTIALLY_REFUNDED, REFUNDED |
FAILED/REFUNDED/VOIDED | 仅自身(终态) |
源码设计要点:
INITIATED是文档之外新增的初始态:当process_payment=false(外部网关、回填数据、checkout 收尾)时,支付停留在 INITIATED,待集成方一次更新直接落到终态(payment.go 注释);OVERPAID处理"多付"场景,可继续走向部分退款/全额退款;SUCCEEDED不是终态,因为 AUTH 型支付仍可 VOID 或 REFUND(IsTerminal()仅将 VOIDED、REFUNDED、FAILED 视为终态,见 payment.go);IsDeletable()规定只有INITIATED(从未发给网关)与FAILED(确定未扣款)可删除,其余涉及资金变动的状态必须走 void/refund,以保留对账记录(payment.go);ValidateTransitionTo(target)拦截一切非法迁移,防止调用方绕过网关直接把支付置为 SUCCEEDED(payment.go)。对应测试见 internal/types/payment_transition_test.go。
五、实现细节
5.1 创建支付
- 校验目的地(如发票存在且可支付)、金额 > 0、币种非空、支付方式类型合法;
- 生成唯一幂等键
idempotency_key(在(tenant_id, environment_id)维度唯一); - 依据支付方式设置追踪标志
track_attempts(Offline 为 false,其余为 true); - 根据
process_payment参数决定是否立即触发处理。
5.2 处理支付
- 开启追踪时先创建 PaymentAttempt(
attempt_number从 1 递增,(payment_id, attempt_number)唯一); - 沿状态机迁移 Payment 状态(PENDING→PROCESSING→SUCCEEDED/FAILED);
- 执行按支付方式分发的处理逻辑(上文 4.2);
- 失败时写入
failed_at与error_message,成功时写入succeeded_at。
5.3 Post-Processing(成功后处理)
- 更新发票的支付状态与已付金额,支持部分支付场景(部分付款金额由发票侧累计核对);
- 更新关联记录(如 Credits 支付的钱包事务);
- 通过 Attempt 记录维护完整审计轨迹(每次尝试的 gateway_attempt_id 与 error_message 均可追溯)。
六、REST API 使用
文档给出了四个端点,当前仓库在 internal/api/v1/payment.go 中实现:
1. 创建支付
POST /v1/payments { "destination_type": "INVOICE", "destination_id": "inv_123", "payment_method_type": "CARD", "payment_method_id": "card_123", "amount": "100.00", "currency": "USD", "process_payment": true }参数说明(依据 internal/types/payment.go 与领域模型 model.go):
destination_type:INVOICE或CUSTOMER;payment_method_type:OFFLINE/CREDITS/CARD/ACH/PAYMENT_LINK/UPI;payment_method_id:Offline 与 Payment Link 必须省略,Card 可省略(自动取已保存卡);amount:十进制字符串,底层为numeric(20,8),须大于 0;currency:三位 ISO 币种代码,不可变;process_payment:true时创建后立即处理,false时先落为INITIATED,等待网关 Webhook 或后续显式处理。
2. 显式处理支付
POST /v1/payments/{id}/process适用于process_payment=false创建的支付,或失败后的手动重试。
3. 查询单笔支付
GET /v1/payments/{id}4. 分页/筛选列表
GET /v1/payments?destination_type=INVOICE&destination_id=inv_123仓库的PaymentFilter(internal/types/payment.go)还支持payment_ids、payment_method_type、payment_status、payment_gateway、currency、gateway_payment_id、gateway_tracking_id等筛选条件,并复用 QueryFilter 的分页、排序与 Expand 机制。
七、未来演进(文档规划)
文档将后续能力划分为两个阶段,可作为扩展方向的参考:
- Phase 2(增强特性):支付方式校验规则、高级重试策略、支付调度、Webhook 通知;
- Phase 3(高级特性):多币种支持、支付路由规则、欺诈检测、高级报表。
其中"多币种支持"在仓库中已有铺垫——支付模型中的currency字段为不可变且校验非空(ent/schema/payment.go),同时仓库存在独立的fxrate(汇率)模块与 自适应多币种设计文档,可结合阅读。
八、结语
FlexPrice 的支付系统在文档设计之上已经形成了完整可运行的实现:ENT 层负责持久化与唯一约束,领域层负责业务校验,types层以状态机矩阵守住状态合法性,REST 层对外暴露统一的支付 API。对于需要在计费产品中落地"多支付方式 + 幂等 + 尝试追踪 + 发票对账"的开发者,这套设计及其源码(ent/schema/payment.go、ent/schema/payment_attempt.go、internal/domain/payment/model.go、internal/types/payment.go、internal/api/v1/payment.go)提供了清晰的参考实现。
【免费下载链接】flexprice
Usage-based pricing and billing for developers 🔓 Cloud or self-hosted ⚙️ No-code UI 💰 Realtime usage metering 🎟 Credits & top-ups 🔑 Control feature access
相关推荐
Gatsby Cloud 账单与发票管理指南:更新支付方式、访问账单门户与处理待付发票
Gatsby Cloud 账单与发票管理指南:更新支付方式、访问账单门户与处理待付发票 导读 本文是一份面向 Gatsby Cloud 用户的实操指南,讲解如何
前端静态站点Web框架FlexPrice 多钱包支付系统设计:发票价格类型拆分、钱包额度限制与 Card-Wallet 拆分支付
FlexPrice 多钱包支付系统设计:发票价格类型拆分、钱包额度限制与 Card Wallet 拆分支付 多钱包支付系统是 FlexPrice 用量计费与账单
GameDevMind 支付系统接入实战:从下单验签到幂等发货与日终对账
GameDevMind 支付系统接入实战:从下单验签到幂等发货与日终对账 导读 支付是游戏内商品、订单和权益转换为稳定收入的"最后一公里",也是最容易出事故的环
文档知识库教程游戏开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考