Payment Starter
受众:开发者 摘要:统一支付 Facade。内置
mock/log/alipay/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。
快速开始
var result = Payment.create(PaymentCreateRequest.builder()
.outTradeNo("ORDER_10001")
.subject("测试订单")
.amount(new BigDecimal("9.90"))
.currency("CNY")
.build());默认 mock Provider,本地启动即可调试,不需要真实账号。
Facade API
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:
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 写日志和流水
配置项
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通用参数
| 配置 | 默认值 | 说明 |
|---|---|---|
enabled | true | 启用开关 |
default-provider | mock | 默认 Provider |
config.enabled | true | 数据库配置覆盖 |
config.prefix | spring.open.payment | 后台配置 key 前缀 |
providers.mock.create-status | success | Mock 下单默认状态 |
providers.mock.query-status | 空 | Mock 查询支付状态覆盖;为空时沿用已创建订单状态,未知订单返回 pending |
providers.mock.refund-status | refunded | Mock 退款默认状态,可设置为 processing / failed 模拟退款补偿链路 |
providers.mock.transfer-status | processing | Mock 打款默认状态 |
providers.mock.transfer-query-status | 空 | Mock 查询打款状态覆盖;为空时沿用已创建打款状态,未知打款返回 processing |
providers.bank-transfer.* | - | 银行 / 三方 SDK 型法币打款配置,常用字段包括 app-id、mch-id、service-url、key、sign-type、sandbox 和 config-no |
Cashier 补偿任务
| 配置 | 默认值 | 说明 |
|---|---|---|
cashier.payment.compensation.enabled | true | 启用补偿能力 |
cashier.payment.compensation.auto-startup | true | 应用启动自动注册 |
cashier.payment.compensation.fixed-delay | 5m | 自动补偿延迟 |
cashier.payment.compensation.limit | 50 | 单次最多扫描 |
cashier.payment.compensation.older-than-minutes | 5 | 只扫描创建超过该分钟数的 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:CNY、fiat:CNY -> crypto:USDT、crypto:USDT -> fiat:CNY、crypto:USDT -> crypto:USDT。业务模块只提交定价金额和定价资产,支付侧金额由 Cashier quote 生成。 - 完整应用中,Money 会通过
CashierPaymentQuoteRatePort提供资产对报价;仅单独使用 Cashier 且没有 Money 适配器时,只允许同资产 quote,跨资产 quote 会明确失败。 pay提交请求新增quoteNo和可选payAssetCode;Cashier 会校验 quote 属于当前支付订单、状态 active、未过期且支付资产一致。- Payment Provider 下单请求统一使用 quote 的
payAmount和paySymbol,并在 attributes / 交易请求快照写入quoteNo、定价资产、支付资产、汇率和快照时间。 - 法币渠道回调和补偿查询会在验签 / 查询成功后对比 quote 快照;Provider 返回的
receiptAmount、totalAmount或amount必须与 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 读取 allowedPayAssetCodes 和 preferredPayAssetCode,展示可选支付资产、应付金额、定价金额、汇率和有效期;报价过期或用户切换支付资产时需要刷新 quote,提交支付时会携带 quoteNo 和 payAssetCode。Admin Cashier 支付订单详情会展示 quote 快照,便于排查报价金额、实收金额和结算资产。
Provider
| Provider | 实现类 | 当前状态 |
|---|---|---|
mock | 内置 | 创建 / 查询 / 退款 / 打款 / 通知验签模拟 |
log | 内置 | 日志输出,支持打款观测 |
alipay | AlipayPaymentProvider(IJPay) | 创建 / 查询 / 退款 / 异步通知验签 |
wechat | WeChatPaymentProvider(IJPay) | Native / JSAPI / App / H5 下单 / 查询 / 退款 / 验签 |
jd-pay | JDPayPaymentProvider(IJPay) | 统一下单 / 查询 / 退款 / 通知解密解析 |
qq-pay | QQPayPaymentProvider | 统一下单 / 查询 / 退款 / 通知验签 |
union-pay | UnionPayPaymentProvider | 统一下单 / 查询 / 退款 / 通知验签 |
bank-transfer | BankTransferPaymentProvider | 只支持平台打款 / 查询打款;真实 SDK 调用由 BankTransferPaymentClient Bean 承载 |
paypal | PayPalPaymentProvider | 创建订单 / 查询 / capture 退款 / webhook 验签 |
yungouos | YunGouOSPaymentProvider | 微信 / 支付宝下单 / 查询 / 退款 / 通知验签 |
扩展点与源码骨架
| 入口 | 文件 | 说明 |
|---|---|---|
| 业务 Facade | Payment | 业务模块发起创建、查询、退款、打款和回调验签的静态短入口 |
| 能力管理器 | PaymentManager / DefaultPaymentManager | 解析默认 provider、校验请求、选择 provider,不负责订单持久化 |
| Provider SPI | PaymentProvider | 支付服务商或支付形态扩展点,承载服务商协议、签名、验签和 payload 转换 |
| 统一请求 | PaymentCreateRequest / PaymentRefundRequest / PaymentTransferRequest | 标准字段承载通用语义,服务商专属字段放 attributes |
| 统一结果 | PaymentResult | 返回 provider、交易号、受理结果、业务状态和前端需要的 payload |
| 配置解析 | PaymentConfigResolver | 从本地配置和 ConfigManager 解析 provider 配置,字段 key 由属性 getter 推导为 kebab-case |
| 运行时配置源 | PaymentRuntimeConfigSource | Payment 与配置中心之间的窄接口,后续可扩展缓存刷新和多租户配置 |
| 银行打款 Client | BankTransferPaymentClient | 部署侧接入真实银行、代付通道或三方 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:
@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 场景。框架默认不内置,避免默认依赖体积与漏洞扫描噪音。需要时按可选扩展接入:
- 单独引入
spring-open-starter-payment-stripe(或业务侧自建同名能力边界) - 扩展包注册业务可见 Provider
stripe - 业务代码
Payment.create("stripe", ...),配置spring.open.payment.providers.stripe.*
当前扩展包直接使用 Stripe HTTP API,不默认引入 stripe-java。如果业务侧需要官方 SDK,可以在业务扩展模块内单独引入并实现 PaymentProvider。
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: trueStripe Checkout 创建支付时,业务请求必须提供 returnUrl,并在 attributes 中提供 cancelUrl:
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 记录成功、上游失败、超时、限流和授权失败等场景。
服务商配置
支付宝
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 |
微信支付
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 |
jsapi | 需 attributes.openid / attributes.openId / attributes.open_id 任一别名,返回 prepayId + 调起参数 |
app | 返回 prepayId + App 调起参数 |
h5 / wap | 需 clientIp,返回 h5Url |
部分退款扩展属性:
.attribute("totalAmount", new BigDecimal("9.90")) // 或 totalCents(分,优先)
.attribute("currency", "CNY")微信支付优先使用 private-key 和 platform-cert 直接配置 PEM 内容,适合 Docker、Kubernetes 和多实例部署;private-key-path / platform-cert-path 仅作为本机文件部署兜底。后台配置中心维护同名 key 时同样按内容字段优先。
小程序场景可先调用 POST /api/v1/wechat/miniapp/payment-context 获取支付上下文,再把返回的 scene 与 attributes 传给 Cashier。PaymentAttributeKeys.OPENID / OPEN_ID / OPEN_ID_UNDERSCORE 统一维护 openid 别名,Cashier 会在下单时规范化 openid、openId 和 open_id,避免不同 Provider 字段口径扩散到业务代码。
京东支付
| 字段 | 默认 | 说明 |
|---|---|---|
merchant | 空 | 商户号 |
rsa-private-key / rsa-public-key | 空 | RSA 私钥 / 京东公钥 |
des-key | 空 | 3DES 密钥 |
version | V2.0 | 接口版本 |
order-type / trade-type / device | 0 / 0 / pc | 可被 attributes 覆盖 |
industry-category-code | 空 | 行业类目 |
金额按分传给服务商。attributes.tradeTime 用 yyyyMMddHHmmss,未传用当前时间。
QQ 钱包
| 字段 | 说明 |
|---|---|
app-id / mch-id | 应用 / 商户号 |
sub-app-id / sub-mch-id | 子商户(服务商模式) |
partner-key | API 密钥 |
cert-path / cert-password | 退款证书(建议 storage/private/keys) |
trade-type | 默认 NATIVE |
金额按分。创建必须提供 clientIp 或 attributes.clientIp。扩展属性:timeStart / timeExpire / limitPay / contractCode / promotionTag / deviceInfo / miniAppParam / operatorId / operatorPassword / refundAccount。
银联
| 字段 | 默认 | 说明 |
|---|---|---|
mch-id / app-id / partner-key | 空 | 商户 / 应用 / 密钥 |
service-url | 空 | 网关地址 |
version | 2.0 | 接口版本 |
sign-type | MD5 | 签名类型 |
create-service | unified.trade.native | 默认服务类型 |
金额按分。创建必须 clientIp。扩展属性:service / timeStart / timeExpire / qrCodeTimeoutExpress / operatorId / goodsTag / productId / totalAmount / totalCents / refundChannel。
PayPal
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(transactionId 或 attributes.orderId)。退款用 capture id(transactionId 或 attributes.captureId)。Webhook 验签需原始 JSON body、paypal-* 请求头、webhook-id。
扩展属性:cancelUrl / intent / orderId / captureId / currency。
YunGouOS
适合个人开发者 / 轻量商户。不引入 YunGouOS Java SDK,直接调 HTTP API,避免旧传递依赖。
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: MD5payType:wechat(默认) / alipay。
本地或部署环境可用环境变量开启 YunGouOS:
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=MD5SPRING_OPEN_PAYMENT_YUNGOUOS_PAY_TYPE 只是默认通道。收银台同时展示微信和支付宝时,仍使用同一个 yungouos provider,由下单 attributes 指定通道:
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:
{
"gateway": "yungouos",
"provider": "yungouos",
"scene": "native",
"attributes": {
"paymentMethod": "yungouos-wechat",
"payType": "wechat"
}
}支付宝入口只需要把 paymentMethod 改为 yungouos-alipay,并把 payType 改为 alipay。paymentMethod 面向展示、筛选和运营统计;payType 面向 YunGouOS 通道选择。这样可以用一套商户号和密钥同时承载微信、支付宝,也能在收银台、Mall、Payment Platform 等业务页面清晰展示两个支付入口。
当前内置用户可见支付方式元数据只有一个 YunGouOS 入口;如果要直接在收银台展示为“微信支付 / 支付宝”两个按钮,需要在对应业务的支付方式元数据层增加上述虚拟 code 映射,底层 Payment Provider 仍保持 yungouos 不变。
回调地址不放在 Provider 全局配置里,由 Cashier / Open Payment 下单时通过 notifyUrl 和 returnUrl 传给 Payment starter。生产环境需要使用公网 HTTPS 回调地址,并在 YunGouOS 控制台保持一致;本地联调可用内网穿透地址。mch-id、key 等敏感配置只放环境变量、数据库配置中心或密钥管理系统,不提交到 Git。
场景:
| 场景 | scene | 说明 |
|---|---|---|
| 扫码 | native / qr | 返回 payUrl / codeUrl |
| H5 / WAP | h5 / wap | 返回 H5 地址 |
| App | app | 微信 App 需 app-id |
| 微信 JSAPI | jsapi | 需 attributes.openId / attributes.openid / attributes.open_id 任一别名 |
| 微信小程序 | mini-app / miniapp | 需 attributes.openId / attributes.openid / attributes.open_id 任一别名 |
扩展属性:payType / openId / buyerId / appId / attach / configNo / auto / autoNode / bizParams。
Payment Platform 开放支付的 extraJson 可携带 openId / openid / open_id、wechatAccountCode 和 wechatUnionId;模块会只把这些微信支付上下文字段提升到 Cashier attributes,不覆盖平台自身的 source=payment-platform 标记。
依赖边界
IJPay / 支付宝 SDK / 微信 SDK / 其他聚合 SDK 只留在具体 Provider 内部。业务代码、业务模块、配置文件不直接依赖这些生态库名称。
PaymentAttributeKeys / PaymentPayloadKeys 统一维护 attributes 扩展字段(tradeType / openid / openId / open_id / clientIp / orderId / captureId 等),不在项目散落字符串。
CVE 处理规则:
- 先在
spring-open-dependencies统一治理版本或 exclusion - Payment starter 显式声明受控依赖
- 不在每个业务模块重复写 exclusion
IJPay AliPay 相关旧传递依赖已收口:dom4j:dom4j / 旧 OkHttp / 旧 BouncyCastle 不进运行时;实际用 org.dom4j:dom4j / 受控 OkHttp / Okio / bcprov-jdk18on。
后台运维
| 资源 | 表 | API 前缀 | 权限 |
|---|---|---|---|
| 支付订单 | cashier_payment_order | /api/v1/admin/cashier/payment-orders;GET /api/v1/admin/cashier/payment-orders/export 导出筛选后的 CSV,不包含订单 body、付款人标识或 attributes 原文 | cashier:payment:order:* |
| 支付流水 | cashier_payment_transaction | /api/v1/admin/cashier/payment-transactions;GET /api/v1/admin/cashier/payment-transactions/export 导出筛选后的 CSV,不包含请求 / 响应 payload 原文 | cashier:payment:transaction:* |
| 回调日志 | cashier_payment_callback_log | /api/v1/admin/cashier/payment-callback-logs;GET /api/v1/admin/cashier/payment-callback-logs/export 导出筛选后的 CSV,不包含请求头、回调 payload 或结果消息原文 | cashier:payment:callback-log:* |
| 状态补偿 | 复用订单 + 流水 | /api/v1/admin/cashier/payment-compensations | cashier:payment:compensation:execute |
| 只读对账 | 复用订单 + 三方查询 | /api/v1/admin/cashier/payment-reconciliations | cashier:payment:reconciliation:execute |
后台配置字段元数据:GET /api/v1/config/configs/metadata(密钥、私钥、API key、证书路径标记敏感)。
状态补偿
POST /api/v1/admin/cashier/payment-compensations{
"outTradeNo": "ORDER_10001",
"limit": 50,
"olderThanMinutes": 5,
"traceId": "trace-10001"
}- 传
outTradeNo→ 单笔;不传 → 扫描指定数量 pending - 调用 Provider
query(...)查三方状态 - 查询成功 + 状态变化 → 幂等更新订单
- 每次查询写
compensate类型流水 - 查询 API 失败不改成失败,只记失败流水
同一补偿动作也可通过 Cashier 补偿命令异步驱动,适合把失败演练、人工重放和调度执行统一到 Outbox 运维页:
| 字段 | 值 |
|---|---|
consumer | cashier-payment |
commandType | cashier.payment.compensate-pending-orders |
aggregateType | 建议单笔使用 cashier-payment-order,批量扫描使用 cashier-payment-pending-orders |
aggregateId | 单笔使用 outTradeNo,批量扫描可使用业务批次号 |
payload | 与 /api/v1/admin/cashier/payment-compensations 请求一致,支持 outTradeNo、limit、olderThanMinutes、traceId |
补偿命令 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 幂等入账。
只读对账
POST /api/v1/admin/cashier/payment-reconciliations{
"outTradeNo": "ORDER_10001",
"status": "pending",
"limit": 50,
"createdBefore": "2026-05-20T08:00:00"
}只调 Provider query(...),不自动改订单和流水。返回本地 / 三方状态、金额、交易号是否一致,适合先定位差异再决定补偿。
Web3 支付边界
链上 RPC / 交易确认 / 事件扫描 / 签名 / 广播由 Web3 starter 承载。Payment 侧只创建收款上下文和查询交易确认状态,不托管私钥、不签名、不广播。
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 / transactionRequest。transactionRequest 是前端可直接交给注入钱包的链上交易参数:原生代币包含 to + value,ERC-20 包含 token 合约 to + transfer(receiver, amountRaw) 的 data,Payment / Web3 starter 只生成参数,不替用户签名或广播。
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-plans | dry-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。
| 配置 | 默认 | 说明 |
|---|---|---|
enabled | true | 启用 |
chain-name | 空 | 默认收款链,回退 spring.open.web3.default-chain |
receiver-address | 空 | 默认收款地址(必填) |
token-contract | 空 | 空 = 原生代币 |
token-symbol / token-decimals | 空 / 18 | Token 符号 / 精度 |
required-confirmations | 12 | 默认确认数 |
应用装配默认已经配置 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用条件更新 + 幂等流水,自动通过复用同一执行器
验证命令
./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参考资料
- IJPay 官方:https://ijpay.dreamlu.net/
- IJPay 微信支付:https://ijpay.dreamlu.net/ijpay/guide/wxpay/
- IJPay 京东 / QQ / 银联 / PayPal:https://ijpay.dreamlu.net/ijpay/guide/
- YunGouOS Java SDK:https://apidoc.gitee.com/YunGouOS/YunGouOS-PAY-SDK/
- YunGouOS 开放接口:https://open.pay.yungouos.com/
相关
- Web3 支付链上对账:web3.md / web3-payment-settlement.md
- 通知(支付状态变更触发):notification.md
- Cashier 运维入口:本页“后台运维”章节
- 配置中心 / 数据库覆盖:config.md
- Facade / Manager / Resolver / Provider 模型:provider.md