跳到内容

Payment Starter

受众:开发者 摘要:统一支付 Facade。内置 mock / log / alipay / wechat / jd-pay / qq-pay / union-pay / bank-transfer / paypal / yungouos / web3 等 Provider。Web3 支付边界 + Cashier 后台运维 + 自动结算审计。

何时使用

场景建议
创建 / 查询 / 退款Payment.create / query / refund
平台向用户打款Payment.transfer / queryTransfer
异步通知接收POST /api/v1/admin/cashier/payment-notifies/{provider}
本地开发 / 调试mock Provider(默认)
真实支付服务商alipay / wechat / jd-pay / qq-pay / union-pay / paypal / yungouos
银行 / 三方 SDK 型法币打款bank-transfer Provider + 部署侧 BankTransferPaymentClient;分销 / 结算首版优先复用内部 Money 提现 + Payment.transfer,真实银行 SDK 后置接入
Web3 链上收款Payment.create("web3", ...),详见 web3.md
后台对账 / 补偿 / 结算审计/api/v1/admin/cashier/payment-* 后台接口
Stripe / 海外信用卡订阅可选扩展,在默认依赖(见下)

业务代码只调 Payment Facade,直接绑定 IJPay / 支付宝 / 微信 SDK。

快速开始

java
var result = Payment.create(PaymentCreateRequest.builder()
        .outTradeNo("ORDER_10001")
        .subject("测试订单")
        .amount(new BigDecimal("9.90"))
        .currency("CNY")
        .build());

默认 mock Provider,本地启动即可调试,需要真实账号。

Facade API

java
String provider = PaymentProviderNames.WECHAT;

// 创建
PaymentResult result = Payment.create(provider, PaymentCreateRequest.builder()
        .outTradeNo("ORDER_10001")
        .subject("订单标题")
        .amount(new BigDecimal("9.90"))
        .currency("CNY")
        .scene(PaymentSceneNames.NATIVE)
        .notifyUrl("https://example.com/api/v1/admin/cashier/payment-notifies/wechat")
        .attribute(PaymentAttributeKeys.CLIENT_IP, "1.2.3.4")
        .build());

// 查询
Payment.query(provider, PaymentQueryRequest.builder()
        .outTradeNo("ORDER_10001")
        .build());

// 退款
Payment.refund(provider, PaymentRefundRequest.builder()
        .outTradeNo("ORDER_10001")
        .refundNo("REFUND_10001")
        .amount(new BigDecimal("9.90"))
        .reason("用户申请退款")
        .build());

// 平台打款 / 转账
Payment.transfer(provider, PaymentTransferRequest.builder()
        .outTransferNo("TRANSFER_10001")
        .amount(new BigDecimal("80.00"))
        .currency("CNY")
        .recipientType("bank")
        .recipientAccount("6222000000000000")
        .recipientName("张三")
        .description("用户余额提现")
        .build());

// 查询打款结果
Payment.queryTransfer(provider, PaymentTransferQueryRequest.builder()
        .outTransferNo("TRANSFER_10001")
        .build());

Provider 名走 PaymentProviderNames 常量;扩展属性用 PaymentAttributeKeys / PaymentPayloadKeys 常量,散落字符串。

PaymentResult.status 取值:success / processing / pending / closed / refunded / failed。打款场景中 success 表示打款完成,processing / pending 表示服务商已受理但仍需后续查询或回调确认。

异步通知

支付回调统一入口由 Cashier 模块负责,URL 与权限归属 cashier 后台命名空间,在业务模块直接调用 IJPay / 支付宝 / 微信 SDK:

text
POST /api/v1/admin/cashier/payment-notifies/{provider}
  • {provider} 用业务 Provider 名(alipay / wechat / jd-pay / paypal / ...)
  • 支付宝表单参数 → PaymentNotifyRequest.parameters
  • 微信支付 v3 → 原始 JSON body + Wechatpay-* 请求头
  • 返回纯文本 success / failure走 Result 包装)
  • 验签失败、找不到订单、业务失败 → failure,服务商按机制重试

处理规则:

  • 每次回调写 cashier_payment_callback_log
  • 验签通过 + 命中 outTradeNo → 按支付结果更新 cashier_payment_order
  • 已成功 / 已退款 / 已关闭等终态不被 pending / failed 回退
  • 支付流水按 outTradeNo + transactionId + notify 查找,重复回调更新既有流水
  • 退款回调用 refund 事件 + refund 流水类型,重复退款不重复累加退款金额
  • 新增退款流水后会分发 CashierPaymentOrderRefundHandler,业务模块可按 scene 做退款后的幂等处理;余额充值场景已接入退款扣回已入账余额
  • 请求 + payload 写日志和流水

配置项

yaml
spring:
  open:
    payment:
      enabled: true
      default-provider: mock
      config:
        enabled: true                     # 数据库配置覆盖
        prefix: spring.open.payment
      providers:
        mock:
          provider: mock
          enabled: true
          create-status: success
          query-status:
          refund-status: refunded
          transfer-status: processing
          transfer-query-status:
        alipay:
          provider: alipay
          enabled: true
          app-id: ${SPRING_OPEN_PAYMENT_ALIPAY_APP_ID:}
          private-key: ${SPRING_OPEN_PAYMENT_ALIPAY_PRIVATE_KEY:}
          public-key: ${SPRING_OPEN_PAYMENT_ALIPAY_PUBLIC_KEY:}
    cashier:
      payment:
        enabled: true
        compensation:
          enabled: true
          auto-startup: true
          task-name: cashier.payment.compensation
          instance-id: default
          fixed-delay: 5m
          limit: 50
          older-than-minutes: 5

通用参数

配置默认值说明
enabledtrue启用开关
default-providermock默认 Provider
config.enabledtrue数据库配置覆盖
config.prefixspring.open.payment后台配置 key 前缀
providers.mock.create-statussuccessMock 下单默认状态
providers.mock.query-statusMock 查询支付状态覆盖;为空时沿用已创建订单状态,未知订单返回 pending
providers.mock.refund-statusrefundedMock 退款默认状态,可设置为 processing / failed 模拟退款补偿链路
providers.mock.transfer-statusprocessingMock 打款默认状态
providers.mock.transfer-query-statusMock 查询打款状态覆盖;为空时沿用已创建打款状态,未知打款返回 processing
providers.bank-transfer.*-银行 / 三方 SDK 型法币打款配置,常用字段包括 app-idmch-idservice-urlkeysign-typesandboxconfig-no

Cashier 补偿任务

配置默认值说明
cashier.payment.compensation.enabledtrue启用补偿能力
cashier.payment.compensation.auto-startuptrue应用启动自动注册
cashier.payment.compensation.fixed-delay5m自动补偿延迟
cashier.payment.compensation.limit50单次最多扫描
cashier.payment.compensation.older-than-minutes5只扫描创建超过该分钟数的 pending

后台配置同名 key 优先级高于本地文件。

Cashier 支付报价

Cashier 已提供支付 quote 生命周期,用于把“业务定价资产”固定为可审计快照,再按用户选择的支付资产生成应付金额。报价快照写入 cashier_payment_quote,保存 quoteNo、订单号、定价资产、支付资产、汇率、来源、快照时间、过期时间、舍入策略和状态。

用户端 API:

方法路径说明
POST/api/v1/cashier/payment-orders/{outTradeNo}/quotes创建或复用当前有效报价
GET/api/v1/cashier/payment-orders/{outTradeNo}/quotes/active查询当前有效报价;过期报价会自动标记为 expired
POST/api/v1/cashier/payment-orders/{outTradeNo}/quotes/{quoteNo}/refresh按旧报价资产组合刷新报价
POST/api/v1/cashier/payment-orders/{outTradeNo}/quotes/{quoteNo}/cancel取消当前有效报价
POST/api/v1/cashier/payment-orders/{outTradeNo}/pay提交支付,必须携带有效 quoteNo

当前状态规则:

  • 同一订单、同一定价资产、同一支付资产和同一场景已有未过期 active quote 时,重复创建会幂等返回旧 quote。
  • 创建不同支付资产的新 quote 会取消旧 active quote,避免同一支付订单长期存在多个可提交报价。
  • 同资产支付也生成 quote,汇率为 1,默认有效期 300 秒。
  • 常见组合均走同一套 quote:fiat:CNY -> fiat:CNYfiat:CNY -> crypto:USDTcrypto:USDT -> fiat:CNYcrypto:USDT -> crypto:USDT。业务模块只提交定价金额和定价资产,支付侧金额由 Cashier quote 生成。
  • 完整应用中,Money 会通过 CashierPaymentQuoteRatePort 提供资产对报价;仅单独使用 Cashier 且没有 Money 适配器时,只允许同资产 quote,跨资产 quote 会明确失败。
  • pay 提交请求新增 quoteNo 和可选 payAssetCode;Cashier 会校验 quote 属于当前支付订单、状态 active、未过期且支付资产一致。
  • Payment Provider 下单请求统一使用 quote 的 payAmountpaySymbol,并在 attributes / 交易请求快照写入 quoteNo、定价资产、支付资产、汇率和快照时间。
  • 法币渠道回调和补偿查询会在验签 / 查询成功后对比 quote 快照;Provider 返回的 receiptAmounttotalAmountamount 必须与 quote 的 payAmount 一致,返回币种时也必须与 quote 支付资产一致,否则 Cashier 会拒绝本次确认并记录失败交易 / 回调日志。
  • Web3 链上确认会读取 quote 快照中的 payAmount / payDecimals,换算为 Token 原始最小单位后与 ERC-20 Transfer 事件的 txHash、收款地址、Token 合约和实际转账数量对比;欠付或超付都会进入转账不匹配 / 结算复核链路,不直接放行。
  • Payment Platform 会把 quote 摘要写入订单 ledgerSnapshotJson 和商户 webhook 的 ledgerSnapshot;稳定 payAssetCode 下,商户应收、平台手续费和结算批次按实际支付资产金额计算。
  • 退款 API 的请求金额仍是商户订单定价资产金额;调用支付 Provider 时会按 quote 的 baseAmount / payAmount 等比例换算为原支付资产退款金额,并在 attributes 中携带 quoteNo、定价资产、支付资产、汇率和 Provider 退款金额。

App / PC 统一收银台已接入 quote 体验:页面从 Cashier 订单 attributes 读取 allowedPayAssetCodespreferredPayAssetCode,展示可选支付资产、应付金额、定价金额、汇率和有效期;报价过期或用户切换支付资产时需要刷新 quote,提交支付时会携带 quoteNopayAssetCode。Admin Cashier 支付订单详情会展示 quote 快照,便于排查报价金额、实收金额和结算资产。

Provider

Provider实现类当前状态
mock内置创建 / 查询 / 退款 / 打款 / 通知验签模拟
log内置日志输出,支持打款观测
alipayAlipayPaymentProvider(IJPay)创建 / 查询 / 退款 / 异步通知验签
wechatWeChatPaymentProvider(IJPay)Native / JSAPI / App / H5 下单 / 查询 / 退款 / 验签
jd-payJDPayPaymentProvider(IJPay)统一下单 / 查询 / 退款 / 通知解密解析
qq-payQQPayPaymentProvider统一下单 / 查询 / 退款 / 通知验签
union-payUnionPayPaymentProvider统一下单 / 查询 / 退款 / 通知验签
bank-transferBankTransferPaymentProvider只支持平台打款 / 查询打款;真实 SDK 调用由 BankTransferPaymentClient Bean 承载
paypalPayPalPaymentProvider创建订单 / 查询 / capture 退款 / webhook 验签
yungouosYunGouOSPaymentProvider微信 / 支付宝下单 / 查询 / 退款 / 通知验签

扩展点与源码骨架

入口文件说明
业务 FacadePayment业务模块发起创建、查询、退款、打款和回调验签的静态短入口
能力管理器PaymentManager / DefaultPaymentManager解析默认 provider、校验请求、选择 provider,不负责订单持久化
Provider SPIPaymentProvider支付服务商或支付形态扩展点,承载服务商协议、签名、验签和 payload 转换
统一请求PaymentCreateRequest / PaymentRefundRequest / PaymentTransferRequest标准字段承载通用语义,服务商专属字段放 attributes
统一结果PaymentResult返回 provider、交易号、受理结果、业务状态和前端需要的 payload
配置解析PaymentConfigResolver从本地配置和 ConfigManager 解析 provider 配置,字段 key 由属性 getter 推导为 kebab-case
运行时配置源PaymentRuntimeConfigSourcePayment 与配置中心之间的窄接口,后续可扩展缓存刷新和多租户配置
银行打款 ClientBankTransferPaymentClient部署侧接入真实银行、代付通道或三方 SDK;框架不默认引入具体 SDK 依赖

支付链路的分层原则:

  • 业务模块只依赖 Payment / PaymentManager / 请求对象 / 结果对象,不直接依赖服务商 SDK。
  • DefaultPaymentManager 只做请求基础校验和 provider 分发,不写订单、流水或业务状态。
  • Cashier 模块负责支付订单、支付流水、回调日志、补偿和对账运维。
  • Provider 只处理服务商协议、签名、验签、下单、查询、退款、打款和 payload 转换。
  • 服务商专属参数优先放 attributes,并使用 PaymentAttributeKeys / PaymentPayloadKeys 常量。
  • 后台配置同名 key 优先于本地文件,敏感配置仍按 Config / Secret 规则处理。

业务可见 Provider 必须是支付服务商或支付形态追加 -payment 后缀。jd-pay / qq-pay / union-pay 中的 pay 是产品名一部分;bank-transfer 表示银行 / 三方 SDK 型法币打款形态,只开放 transfer / queryTransfer,不承担普通下单、退款或支付通知。IJPay / 支付宝 SDK / 微信 SDK / 银行 SDK 只能作 Provider 内部实现细节,进业务代码、业务模块或配置 key。

银行 / 三方 SDK 型法币打款

bank-transfer Provider 用于“平台向用户银行卡、银行账户或代付通道打款”。它是外部银行 / 代付 SDK 扩展点,不是分销、结算等首版自动打款的默认首选。前期自动打款建议先复用 Money 提现状态机 + Payment.transfer 内部转账能力;只有内部 Payment Provider 不支持目标渠道、需要真实银行卡 / 代付通道或生产小额验收时,再接入 bank-transfer

框架只保留稳定 Provider 名、配置快照和统一请求 / 结果对象;真实银行、代付平台或三方 SDK 由部署侧实现 BankTransferPaymentClient

java
@Component
class AcmeBankTransferPaymentClient implements BankTransferPaymentClient {

    @Override
    public PaymentResult transfer(PaymentProviderConfig.BankTransfer config,
            PaymentTransferRequest request) {
        // 调用真实银行 / 代付 SDK,返回 processing / success / failed。
    }

    @Override
    public PaymentResult queryTransfer(PaymentProviderConfig.BankTransfer config,
            PaymentTransferQueryRequest request) {
        // 查询真实打款状态。
    }
}

BankTransferPaymentClient Bean 存在且 spring.open.payment.providers.bank-transfer.enabled=true 时,自动装配会注册 BankTransferPaymentProvider。Money 提现侧使用 provider=payment-bank-transfer 即可复用该 Provider;提现单冻结、失败释放、处理中同步和已打款出账仍由 Money 模块负责。

可选 Stripe

国际信用卡 / Stripe Checkout 场景。框架默认内置,避免默认依赖体积与漏洞扫描噪音。需要时按可选扩展接入:

  1. 单独引入 spring-open-starter-payment-stripe(或业务侧自建同名能力边界)
  2. 扩展包注册业务可见 Provider stripe
  3. 业务代码 Payment.create("stripe", ...),配置 spring.open.payment.providers.stripe.*

当前扩展包直接使用 Stripe HTTP API,不默认引入 stripe-java。如果业务侧需要官方 SDK,可以在业务扩展模块内单独引入并实现 PaymentProvider

yaml
spring:
  open:
    payment:
      providers:
        stripe:
          enabled: true
          provider: stripe
          service-url: https://api.stripe.com
          key: ${SPRING_OPEN_PAYMENT_STRIPE_SECRET_KEY:}
          secret: ${SPRING_OPEN_PAYMENT_STRIPE_WEBHOOK_SECRET:}
          currency: USD
          sandbox: true

Stripe Checkout 创建支付时,业务请求必须提供 returnUrl,并在 attributes 中提供 cancelUrl

java
Payment.create("stripe", PaymentCreateRequest.builder()
        .outTradeNo("ORDER_10001")
        .subject("Order 10001")
        .amount(new BigDecimal("12.34"))
        .currency("USD")
        .returnUrl("https://example.com/pay/success")
        .attribute("cancelUrl", "https://example.com/pay/cancel")
        .build());

异步通知使用 Stripe-Signature 请求头和 spring.open.payment.providers.stripe.secret 验签。真实 Stripe 账号联调属于外部验收,应按 docs/zh-CN/provider-acceptance.md 记录成功、上游失败、超时、限流和授权失败等场景。

服务商配置

支付宝

yaml
spring:
  open:
    payment:
      providers:
        alipay:
          app-id: ${SPRING_OPEN_PAYMENT_ALIPAY_APP_ID:}
          private-key: ${SPRING_OPEN_PAYMENT_ALIPAY_PRIVATE_KEY:}
          public-key: ${SPRING_OPEN_PAYMENT_ALIPAY_PUBLIC_KEY:}
          service-url: https://openapi.alipay.com/gateway.do
          charset: UTF-8
          sign-type: RSA2
          page-product-code: FAST_INSTANT_TRADE_PAY
字段说明
app-id / private-key / public-key应用 ID / 应用私钥 / 支付宝公钥(验签)
service-url沙箱用沙箱网关
sign-type默认 RSA2

微信支付

yaml
spring:
  open:
    payment:
      providers:
        wechat:
          app-id: ${SPRING_OPEN_PAYMENT_WECHAT_APP_ID:}
          mch-id: ${SPRING_OPEN_PAYMENT_WECHAT_MCH_ID:}
          api-key3: ${SPRING_OPEN_PAYMENT_WECHAT_API_KEY3:}
          serial-no: ${SPRING_OPEN_PAYMENT_WECHAT_SERIAL_NO:}
          private-key: ${SPRING_OPEN_PAYMENT_WECHAT_PRIVATE_KEY:}
          private-key-path: ${SPRING_OPEN_PAYMENT_WECHAT_PRIVATE_KEY_PATH:${APP_STORAGE_DIR:${user.dir}/storage}/private/keys/apiclient_key.pem}
          platform-cert: ${SPRING_OPEN_PAYMENT_WECHAT_PLATFORM_CERT:}
          platform-cert-path: ${SPRING_OPEN_PAYMENT_WECHAT_PLATFORM_CERT_PATH:${APP_STORAGE_DIR:${user.dir}/storage}/private/keys/wechatpay-platform.pem}

场景:

scene说明
native / qr默认,返回 codeUrl
jsapiattributes.openid / attributes.openId / attributes.open_id 任一别名,返回 prepayId + 调起参数
app返回 prepayId + App 调起参数
h5 / wapclientIp,返回 h5Url

部分退款扩展属性:

java
.attribute("totalAmount", new BigDecimal("9.90"))   // 或 totalCents(分,优先)
.attribute("currency", "CNY")

微信支付优先使用 private-keyplatform-cert 直接配置 PEM 内容,适合 Docker、Kubernetes 和多实例部署;private-key-path / platform-cert-path 仅作为本机文件部署兜底。后台配置中心维护同名 key 时同样按内容字段优先。

小程序场景可先调用 POST /api/v1/wechat/miniapp/payment-context 获取支付上下文,再把返回的 sceneattributes 传给 Cashier。PaymentAttributeKeys.OPENID / OPEN_ID / OPEN_ID_UNDERSCORE 统一维护 openid 别名,Cashier 会在下单时规范化 openidopenIdopen_id,避免不同 Provider 字段口径扩散到业务代码。

京东支付

字段默认说明
merchant商户号
rsa-private-key / rsa-public-keyRSA 私钥 / 京东公钥
des-key3DES 密钥
versionV2.0接口版本
order-type / trade-type / device0 / 0 / pc可被 attributes 覆盖
industry-category-code行业类目

金额按传给服务商。attributes.tradeTimeyyyyMMddHHmmss,未传用当前时间。

QQ 钱包

字段说明
app-id / mch-id应用 / 商户号
sub-app-id / sub-mch-id子商户(服务商模式)
partner-keyAPI 密钥
cert-path / cert-password退款证书(建议 storage/private/keys
trade-type默认 NATIVE

金额按。创建必须提供 clientIpattributes.clientIp。扩展属性:timeStart / timeExpire / limitPay / contractCode / promotionTag / deviceInfo / miniAppParam / operatorId / operatorPassword / refundAccount

银联

字段默认说明
mch-id / app-id / partner-key商户 / 应用 / 密钥
service-url网关地址
version2.0接口版本
sign-typeMD5签名类型
create-serviceunified.trade.native默认服务类型

金额按。创建必须 clientIp。扩展属性:service / timeStart / timeExpire / qrCodeTimeoutExpress / operatorId / goodsTag / productId / totalAmount / totalCents / refundChannel

PayPal

yaml
spring:
  open:
    payment:
      providers:
        paypal:
          client-id: ${SPRING_OPEN_PAYMENT_PAYPAL_CLIENT_ID:}
          secret: ${SPRING_OPEN_PAYMENT_PAYPAL_SECRET:}
          sandbox: true
          webhook-id: ${SPRING_OPEN_PAYMENT_PAYPAL_WEBHOOK_ID:}
          currency: USD
          intent: CAPTURE
          user-action: PAY_NOW

创建返回 orderId + approveUrl。查询用 PayPal order id(transactionIdattributes.orderId)。退款用 capture id(transactionIdattributes.captureId)。Webhook 验签需原始 JSON body、paypal-* 请求头、webhook-id

扩展属性:cancelUrl / intent / orderId / captureId / currency

YunGouOS

适合个人开发者 / 轻量商户。引入 YunGouOS Java SDK,直接调 HTTP API,避免旧传递依赖。

yaml
spring:
  open:
    payment:
      providers:
        yungouos:
          mch-id: ${SPRING_OPEN_PAYMENT_YUNGOUOS_MCH_ID:}
          key: ${SPRING_OPEN_PAYMENT_YUNGOUOS_KEY:}
          app-id: ${SPRING_OPEN_PAYMENT_YUNGOUOS_APP_ID:}
          service-url: https://api.pay.yungouos.com
          pay-type: wechat
          native-type: 2
          sign-type: MD5

payTypewechat(默认) / alipay

本地或部署环境可用环境变量开启 YunGouOS:

dotenv
SPRING_OPEN_PAYMENT_DEFAULT_PROVIDER=yungouos
SPRING_OPEN_PAYMENT_YUNGOUOS_ENABLED=true
SPRING_OPEN_PAYMENT_YUNGOUOS_MCH_ID=你的YunGouOS商户号
SPRING_OPEN_PAYMENT_YUNGOUOS_KEY=你的YunGouOS支付密钥
SPRING_OPEN_PAYMENT_YUNGOUOS_APP_ID=微信App或小程序AppID
SPRING_OPEN_PAYMENT_YUNGOUOS_SERVICE_URL=https://api.pay.yungouos.com
SPRING_OPEN_PAYMENT_YUNGOUOS_PAY_TYPE=wechat
SPRING_OPEN_PAYMENT_YUNGOUOS_NATIVE_TYPE=2
SPRING_OPEN_PAYMENT_YUNGOUOS_SIGN_TYPE=MD5

SPRING_OPEN_PAYMENT_YUNGOUOS_PAY_TYPE 只是默认通道。收银台同时展示微信和支付宝时,仍使用同一个 yungouos provider,由下单 attributes 指定通道:

java
PaymentCreateRequest request = PaymentCreateRequest.builder()
        .outTradeNo("ORDER_10001")
        .subject("会员套餐")
        .amount(new BigDecimal("99.00"))
        .scene("native")
        .attribute("payType", "wechat")
        .build();

支付宝通道把 payType 改为 alipay。当前实现的 YunGouOS 配置读取固定路径 spring.open.payment.providers.yungouos.*,不要用 yungouos-wechat / yungouos-alipay 两个 Provider 名拆配置;如果需要不同商户号或不同签约主体,建议后续扩展 Payment Platform 通道路由,而不是在业务代码里硬编码第二套 Provider。

同时展示微信和支付宝

如果产品上需要“微信支付”和“支付宝”同时存在,推荐做成两个用户可见支付方式,而不是两套 YunGouOS Provider 配置:

用户可见 code展示名实际 gateway / provider下单 attributes
yungouos-wechat微信支付yungouos{"payType":"wechat"}
yungouos-alipay支付宝yungouos{"payType":"alipay"}

前端或业务模块提交支付时仍调用同一个 YunGouOS Provider:

json
{
  "gateway": "yungouos",
  "provider": "yungouos",
  "scene": "native",
  "attributes": {
    "paymentMethod": "yungouos-wechat",
    "payType": "wechat"
  }
}

支付宝入口只需要把 paymentMethod 改为 yungouos-alipay,并把 payType 改为 alipaypaymentMethod 面向展示、筛选和运营统计;payType 面向 YunGouOS 通道选择。这样可以用一套商户号和密钥同时承载微信、支付宝,也能在收银台、Mall、Payment Platform 等业务页面清晰展示两个支付入口。

当前内置用户可见支付方式元数据只有一个 YunGouOS 入口;如果要直接在收银台展示为“微信支付 / 支付宝”两个按钮,需要在对应业务的支付方式元数据层增加上述虚拟 code 映射,底层 Payment Provider 仍保持 yungouos 不变。

回调地址不放在 Provider 全局配置里,由 Cashier / Open Payment 下单时通过 notifyUrlreturnUrl 传给 Payment starter。生产环境需要使用公网 HTTPS 回调地址,并在 YunGouOS 控制台保持一致;本地联调可用内网穿透地址。mch-idkey 等敏感配置只放环境变量、数据库配置中心或密钥管理系统,不提交到 Git。

场景:

场景scene说明
扫码native / qr返回 payUrl / codeUrl
H5 / WAPh5 / wap返回 H5 地址
Appapp微信 App 需 app-id
微信 JSAPIjsapiattributes.openId / attributes.openid / attributes.open_id 任一别名
微信小程序mini-app / miniappattributes.openId / attributes.openid / attributes.open_id 任一别名

扩展属性:payType / openId / buyerId / appId / attach / configNo / auto / autoNode / bizParams

Payment Platform 开放支付的 extraJson 可携带 openId / openid / open_idwechatAccountCodewechatUnionId;模块会只把这些微信支付上下文字段提升到 Cashier attributes,不覆盖平台自身的 source=payment-platform 标记。

依赖边界

IJPay / 支付宝 SDK / 微信 SDK / 其他聚合 SDK 只留在具体 Provider 内部。业务代码、业务模块、配置文件直接依赖这些生态库名称。

PaymentAttributeKeys / PaymentPayloadKeys 统一维护 attributes 扩展字段(tradeType / openid / openId / open_id / clientIp / orderId / captureId 等),在项目散落字符串。

CVE 处理规则:

  1. 先在 spring-open-dependencies 统一治理版本或 exclusion
  2. Payment starter 显式声明受控依赖
  3. 在每个业务模块重复写 exclusion

IJPay AliPay 相关旧传递依赖已收口:dom4j:dom4j / 旧 OkHttp / 旧 BouncyCastle 进运行时;实际用 org.dom4j:dom4j / 受控 OkHttp / Okio / bcprov-jdk18on

后台运维

资源API 前缀权限
支付订单cashier_payment_order/api/v1/admin/cashier/payment-ordersGET /api/v1/admin/cashier/payment-orders/export 导出筛选后的 CSV,不包含订单 body、付款人标识或 attributes 原文cashier:payment:order:*
支付流水cashier_payment_transaction/api/v1/admin/cashier/payment-transactionsGET /api/v1/admin/cashier/payment-transactions/export 导出筛选后的 CSV,不包含请求 / 响应 payload 原文cashier:payment:transaction:*
回调日志cashier_payment_callback_log/api/v1/admin/cashier/payment-callback-logsGET /api/v1/admin/cashier/payment-callback-logs/export 导出筛选后的 CSV,不包含请求头、回调 payload 或结果消息原文cashier:payment:callback-log:*
状态补偿复用订单 + 流水/api/v1/admin/cashier/payment-compensationscashier:payment:compensation:execute
只读对账复用订单 + 三方查询/api/v1/admin/cashier/payment-reconciliationscashier:payment:reconciliation:execute

后台配置字段元数据:GET /api/v1/config/configs/metadata(密钥、私钥、API key、证书路径标记敏感)。

状态补偿

http
POST /api/v1/admin/cashier/payment-compensations
json
{
  "outTradeNo": "ORDER_10001",
  "limit": 50,
  "olderThanMinutes": 5,
  "traceId": "trace-10001"
}
  • outTradeNo → 单笔;不传 → 扫描指定数量 pending
  • 调用 Provider query(...) 查三方状态
  • 查询成功 + 状态变化 → 幂等更新订单
  • 每次查询写 compensate 类型流水
  • 查询 API 失败改成失败,只记失败流水

同一补偿动作也可通过 Cashier 补偿命令异步驱动,适合把失败演练、人工重放和调度执行统一到 Outbox 运维页:

字段
consumercashier-payment
commandTypecashier.payment.compensate-pending-orders
aggregateType建议单笔使用 cashier-payment-order,批量扫描使用 cashier-payment-pending-orders
aggregateId单笔使用 outTradeNo,批量扫描可使用业务批次号
payload/api/v1/admin/cashier/payment-compensations 请求一致,支持 outTradeNolimitolderThanMinutestraceId

补偿命令 handler 会复用 CashierPaymentCompensationService。如果本轮补偿结果包含失败项,命令会标记为 failed 并进入补偿命令自身的重试 / 重放节奏;全部补偿项成功或没有可补偿订单时命令标记为 succeeded

  • 自动补偿默认启用,5 分钟扫描一次 5 分钟+ 的 pending;关 compensation.auto-startup
  • 用户侧 Web3 支付 /cashier/payment-orders/{outTradeNo}/sync 提交交易 hash 后,如果本次链上查询仍未成功,会按 spring.open.cashier.payment.sync.web3.confirmation-trigger-delay(默认 10s,配置中心优先)延迟投递当前支付单补偿;订单仍为 pending 时按 spring.open.cashier.payment.sync.web3.confirmation-retry-initial-attempt(默认 1)和 spring.open.cashier.payment.sync.web3.confirmation-retry-max-attempts(默认 5)记录投递序号并继续重投。补偿只更新 Cashier 支付单,业务入账仍走原有支付成功处理器和 Money 幂等入账。

只读对账

http
POST /api/v1/admin/cashier/payment-reconciliations
json
{
  "outTradeNo": "ORDER_10001",
  "status": "pending",
  "limit": 50,
  "createdBefore": "2026-05-20T08:00:00"
}

只调 Provider query(...)自动改订单和流水。返回本地 / 三方状态、金额、交易号是否一致,适合先定位差异再决定补偿。

Web3 支付边界

链上 RPC / 交易确认 / 事件扫描 / 签名 / 广播由 Web3 starter 承载。Payment 侧只创建收款上下文和查询交易确认状态,托管私钥、签名、广播。

java
PaymentResult result = Payment.create("web3", PaymentCreateRequest.builder()
        .outTradeNo("ORDER_202605210001")
        .provider("bsc")
        .amount(new BigDecimal("1.23"))
        .currency("BNB")
        .build());

返回 pending + payload:chainName / chainId / receiverAddress / amount / amountRaw / currency / scene / notifyUrl / returnUrl / tokenContract / tokenSymbol / tokenDecimals / requiredConfirmations / transactionRequesttransactionRequest 是前端可直接交给注入钱包的链上交易参数:原生代币包含 to + value,ERC-20 包含 token 合约 to + transfer(receiver, amountRaw)data,Payment / Web3 starter 只生成参数,不替用户签名或广播。

java
PaymentResult result = Payment.query("web3", PaymentQueryRequest.builder()
        .outTradeNo("ORDER_202605210001")
        .provider("bsc")
        .transactionId("0x...")
        .attribute("requiredConfirmations", 12)
        .build());

查询:失败 → failed,成功 + 达到确认数 → success,其他 → pending

Web3 支付字段由 Web3 starter 内 Web3PaymentPayloadKeys / Web3PaymentAttributes 统一维护。新代码用推荐 key;旧业务通过 web3ChainName / web3TransactionHash 等兼容 key 接入。

普通业务接入 Web3 支付时,推荐流程是:业务订单调用 Payment.create("web3", ...) 创建待支付意图,前端在具备注入钱包能力的运行环境中用 transactionRequest 发起 eth_sendTransaction,拿到 tx hash 后通过业务侧支付同步接口传回 transactionId / attributes.web3TransactionHash,再由 Payment query / Web3 确认链路判断 pending / success / failed。不具备钱包签名能力的运行环境应提示用户切换到支持注入钱包的浏览环境,不能假装已经完成链上付款。

链上确认 + 结算审计

API用途
POST /api/v1/admin/web3/payment-confirmations单笔只读确认
POST /api/v1/admin/web3/payment-confirmations/batch批量只读确认(默认 provider=web3, status=pending
POST /api/v1/admin/web3/payment-confirmations/settled-audits已结算订单链上二次确认
POST /api/v1/admin/web3/payment-confirmations/settlement-plansdry-run 结算计划
POST /api/v1/admin/web3/payment-confirmations/settlement-plans/verify复核指纹校验
GET/POST /api/v1/admin/web3/payment-settlement-audit-logs/...风险审计日志
POST /api/v1/admin/web3/payment-settlement-review-logs人工复核日志
POST /api/v1/admin/web3/payment-settlement-review-logs/{id}/settle显式结算执行
POST /api/v1/admin/web3/payment-settlement-review-logs/{id}/retry重试
POST /api/v1/admin/web3/payment-settlement-review-logs/auto-handle-expired自动处理过期

结算检查器返回稳定原因码:transaction-hash-missing / chain-transaction-not-mined / chain-confirmations-insufficient / local-order-already-paid。ERC-20 Transfer 事件原因码:transfer-missing / receiver-address-mismatch / token-contract-mismatch / amount-mismatch / transfer-query-failed

settlementReady=true 表示当前只读检查未发现阻断,等于自动结算。

显式 /settle 入口走幂等条件:订单仍 pending + 链上确认满足 + ERC-20 转账匹配 + 无 web3-settlement 流水 → 条件更新订单为 success + 写流水。自动通过场景复用同一执行器,无另一套绕过校验的逻辑。

自动重试任务按 spring.open.money.web3.settlement.retry.* 配置;max-attempts 限制同订单 + 复核指纹重试次数。

Web3 starter 提供 Web3PaymentTransferMatcher 作为转账事件匹配底座。结算 / 审计 / 补偿建议都用它对比 ERC-20 Transfer 与订单期望。

详细状态机、幂等 key、链重组处理、人工复核、审计边界见 web3-payment-settlement.md

Web3 支付参数

属于 Web3 starter,前缀 spring.open.web3.payment。Payment starter 只保留通用 Provider SPI,直接依赖 Web3 SDK。

配置默认说明
enabledtrue启用
chain-name默认收款链,回退 spring.open.web3.default-chain
receiver-address默认收款地址(必填)
token-contract空 = 原生代币
token-symbol / token-decimals空 / 18Token 符号 / 精度
required-confirmations12默认确认数

应用装配默认已经配置 BSC + USDT 收款上下文,但不会给 receiver-address 填假钱包。生产或联调时设置 SPRING_OPEN_WEB3_PAYMENT_RECEIVER_ADDRESS=0xYourTreasuryWallet,或在 Config 配置中心写入 spring.open.web3.payment.receiver-address。未配置时 Web3 支付会返回明确错误,避免把真实付款引导到不可控地址;地址本里的 0xREPLACE_* 只作为后台占位模板,不参与真实收款。

接入边界

仓库不再维护独立 Payment 样例模块。支付体验以 Cashier 的正式订单、流水、回调、补偿和对账链路为准;本地联调可继续使用 mock Provider,但不要新增临时样例接口。

开发约定

  • Provider 名走 PaymentProviderNames业务代码手写字符串
  • 扩展属性走 PaymentAttributeKeys / PaymentPayloadKeys
  • 密钥、私钥、API key、证书路径走环境变量或后台配置,进 Git
  • 证书放 storage/private/keys
  • IJPay / 服务商 SDK 只留 Provider 内部,进业务代码或 业务模块
  • 回调统一走 /api/v1/admin/cashier/payment-notifies/{provider}业务模块自己写
  • 终态订单被 pending / failed 回退
  • Web3 支付托管私钥、签名、广播
  • 显式结算 /settle 用条件更新 + 幂等流水,自动通过复用同一执行器

验证命令

bash
./mvnw -pl spring-open-starters/spring-open-starter-payment -am test -DskipITs
./mvnw -pl spring-open-core/spring-open-core-cashier -am -Dtest='*Payment*Test' -Dsurefire.failIfNoSpecifiedTests=false test -DskipITs
./mvnw -pl spring-open-application -am test -DskipITs -DskipFrontend

参考资料

相关

Released under the Apache License 2.0.