☰
FlexPrice 支付系统设计:多支付方式、幂等处理与发票对账的完整实现指南
2026/10/9 1:50:08 网站建设 项目流程

【免费下载链接】flexprice

Usage-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 开源仓库中的 支付系统设计文档 展开,系统讲解该计费平台的支付模块如何统一承载线下(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枚举,当前仓库实际支持六种类型,比文档规划更进一步:

类型常量枚举值说明
PaymentMethodTypeOfflineOFFLINE线下/人工支付,无需 payment_method_id,自动成功且不追踪尝试
PaymentMethodTypeCreditsCREDITS钱包 Credits 支付,需要有效 wallet ID 作为 payment_method_id,完整追踪尝试与事务历史
PaymentMethodTypeCardCARD银行卡支付,接入支付网关,需要有效 card ID 作为 payment_method_id
PaymentMethodTypeACHACHACH 银行转账,接入支付网关,需要有效 bank account ID 作为 payment_method_id
PaymentMethodTypePaymentLinkPAYMENT_LINK支付链接支付,payment_method_id 必须为空
PaymentMethodTypeUPIUPIUPI 支付(仓库扩展)

上述校验逻辑并非只停留在文档层面。在 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 创建与处理时序

文档给出了完整的创建+处理时序图,核心流程为:

  1. 创建支付(CreatePayment):校验请求 → 生成幂等键 → 以PENDING状态落库;
  2. 按需立即处理(process_payment=true):进入 PaymentProcessor → 创建 Attempt → 按支付方式分发处理 → 更新 Payment 与 Attempt 状态;
  3. 成功后执行 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表中严格定义了合法迁移矩阵:

状态可迁移至
INITIATEDINITIATED, PENDING, PROCESSING, SUCCEEDED, OVERPAID, FAILED
PENDINGPENDING, PROCESSING, SUCCEEDED, OVERPAID, FAILED
PROCESSINGPROCESSING, SUCCEEDED, OVERPAID, FAILED
SUCCEEDEDSUCCEEDED, OVERPAID, REFUNDED, PARTIALLY_REFUNDED, VOIDED
OVERPAIDOVERPAID, REFUNDED, PARTIALLY_REFUNDED
PARTIALLY_REFUNDEDPARTIALLY_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 创建支付

  1. 校验目的地(如发票存在且可支付)、金额 > 0、币种非空、支付方式类型合法;
  2. 生成唯一幂等键idempotency_key(在(tenant_id, environment_id)维度唯一);
  3. 依据支付方式设置追踪标志track_attempts(Offline 为 false,其余为 true);
  4. 根据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 机制。

七、未来演进(文档规划)

文档将后续能力划分为两个阶段,可作为扩展方向的参考:

  1. Phase 2(增强特性):支付方式校验规则、高级重试策略、支付调度、Webhook 通知;
  2. 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

项目地址:https://gitcode.com/gh_mirrors/fl/flexprice
点击查看免费下载

相关推荐

上一篇:docker-selenium Chrome 136 镜像发布全解析:Tag 命名规则与多架构发布脚本实战
下一篇:CANN/asc-devkit GlobalTensor形状获取

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

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

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

立即咨询