unionpay.md 10 KB

银联云闪付(UnionPay)技术文档

1. 概述

云闪付(UnionPay)是银联旗下移动支付 App,覆盖中国大陆绝大多数银行卡。本项目对接银联全渠道(open.unionpay.com)下的两种场景:

场景 接口 适用端 说明
App 支付(手机控件支付) appTransReq.do iOS / Android App 后端返回 tn(交易流水号),前端拉起云闪付 App 完成支付
手机网站支付 appTransReq.do + 收银台 移动端浏览器 / Web 后端返回 tn,客户端拼接银联收银台地址跳转

本项目(Flutter)移动端使用 App 支付uppay:// scheme 拉起云闪付),Web/桌面使用手机网站支付(银联收银台跳转)。本文档涉及的交易类型均为消费(CONSUME),银联交易类型对照:01 消费、02 消费撤销、04 退款、00 查询(来自 Pay-Java-Parent UnionPay 枚举)。

2. 申请与配置

2.1 需要申请的内容

项目 说明
商户号 merId 银联商户服务平台(open.unionpay.com)申请,需企业资质
商户私钥 unionpay_private_key.pem,下单签名用(App 支付场景)
银联公钥 unionpay_public_key.pem,回调验签用
签名方式 signMethod 11(RSA2 / SHA-256,推荐)、01(RSA / SHA-1)
回调地址 notifyUrl 公网 HTTPS,接收银联异步支付结果通知

银联提供官方沙箱环境:网关 https://gateway.test.95516.com,可用测试商户号(777290058110048 等)联调,无需真实扣款。

2.2 环境变量(server/.env)

# 云闪付
UNIONPAY_MER_ID=              # 商户号,如 777290058110048
UNIONPAY_PRIVATE_KEY_PATH=./certs/unionpay_private_key.pem
UNIONPAY_PUBLIC_KEY_PATH=./certs/unionpay_public_key.pem
# 签名方式:11=RSA2(SHA256) 推荐;01=RSA(SHA1)
UNIONPAY_SIGN_METHOD=11
# 沙箱网关 true;生产置 false
UNIONPAY_SANDBOX=true
# 回调地址(须公网可达,测试可用内网穿透)
UNIONPAY_NOTIFY_URL=http://localhost:3000/v1/payment/notify/unionpay

2.3 密钥生成(RSA2,openssl 示例)

# 商户私钥(2048 位 PKCS#1)
openssl genrsa -out unionpay_private_key.pem 2048
# 对应公钥(用于换取银联登记的商户公钥)
openssl rsa -in unionpay_private_key.pem -pubout -out unionpay_public_key.pem

商户公钥需在银联商户平台登记;回调验签用的是银联平台证书导出的公钥,生产环境应从银联商户平台下载。

3. 支付流程

sequenceDiagram
    participant App as Flutter App
    participant Backend as 业务后端
    participant UnionPay as 银联网关

    App->>Backend: POST /v1/payment/unionpay/ {product_id, source}
    Backend->>Backend: 创建订单、计算金额(单位:分)
    Backend->>UnionPay: appTransReq.do(商户私钥 RSA2 签名)
    UnionPay-->>Backend: tn(交易流水号)
    Backend-->>App: { payment_id, tn, sandbox }
    App->>App: uppay://sdkpay?tn=xxx 拉起云闪付 App
    App->>UnionPay: 用户在云闪付内完成扣款
    UnionPay->>Backend: 异步通知(表单POST, 银联私钥签名)
    Backend->>Backend: 验签 + respCode==00 + 幂等更新订单 + 发货
    App->>Backend: GET /v1/payment/{payment_id} 轮询最终状态

统一原则(与全项目一致):客户端回调结果仅做 UI 提示,最终支付状态一律以 GET /v1/payment/{payment_id} 后端查询为准。

4. 后端集成(Node.js/Express)

4.1 目录与职责

server/src/
  services/unionpay.service.js   # 银联签名/验签、网关表单 POST、创建支付
  services/pay_gateway.service.js # 渠道分发(CHANNELS 含 unionpay)
  services/notify.service.js     # handleUnionPayNotify 验签 + markPaidByOutTradeNo
  controllers/notify.controller.js # POST /v1/payment/notify/unionpay(express.urlencoded)
  mock/mock.factory.js           # MOCK_MODE 下返回 mock_tn_* 模拟参数
  config/index.js                # unionpay 配置段 + 网关 getter

4.2 下单参数(对齐银联全渠道 App 支付)

字段 说明
version 5.1.0 版本号
encoding UTF-8 编码
signMethod 11 / 01 RSA2 / RSA
txnType 01 交易类型:消费
txnSubType 01 交易子类:消费
bizType 000201 手机支付
channelType 08 渠道类型:手机
accessType 0 接入类型:商户直连接入
merId 商户号 银联分配
orderId 订单号 商户订单号(out_trade_no
txnTime YYYYMMDDHHmmss 交易时间(订单创建时间格式化)
txnAmt 金额(分) 交易金额,单位分
currencyCode 156 货币代码:人民币
notifyUrl 回调地址 银联异步通知地址

4.3 签名算法(sign / verifySign)

// 1. 取除 sign/signValue 外的全部参数,按 key 升序
// 2. 拼接 k=v&k2=v2&...
// 3. 用商户私钥签名(RSA-SHA256 或 RSA-SHA1),Base64 编码
function sign(params, key) {
  const sorted = Object.keys(params)
    .filter((k) => !['sign', 'signValue'].includes(k))
    .sort()
    .map((k) => `${k}=${params[k]}`)
    .join('&');
  return crypto.sign(signAlg(), Buffer.from(sorted, 'utf8'), key).toString('base64');
}

回调验签(verifySign):同样拼装原文,用银联公钥验证 signValue 是否匹配。

4.4 创建支付(create)

const resp = await postForm(config.unionpay.gateway, base); // 表单 POST
if (resp.respCode !== '00') throw ...;                    // 下单失败
const tn = resp.tn;                                        // 交易流水号
// App 支付:返回 { payment_id, tn, params: tn } → 前端拉起 uppay://
// Web 支付:返回 { payment_id, tn, redirect_url: gateway/transReceipt.do?tn=tn }
  • App 支付tn 是调起云闪付 App 的唯一凭证。
  • Web/手机网站支付:客户端拿 tn 拼银联收银台地址跳转。

4.5 回调处理(handleUnionPayNotify)

// 1. 验签(verifySign,MOCK 模式下跳过)
// 2. respCode === '00' 表示交易成功
// 3. 取 orderId 匹配本地订单 → markPaidByOutTradeNo 幂等更新

回调幂等:同一 orderId 多次回调只更新一次,状态机 PENDING → PAID

4.6 Mock 模式

MOCK_MODE=true 时(无真实商户号也能端到端跑通):

  • 下单返回 { tn: 'mock_tn_...', payment_id, sandbox: true }(web 额外返回 redirect_url)。
  • 前端看到 tnmock_ 开头即不拉起 App,调用 POST /v1/payment/simulate/success 模拟支付成功。
  • simulate 走与真实回调相同markPaid 幂等流程。

5. 前端集成(Flutter)

5.1 模型与 API(lib/)

lib/models/payment.dart        # UnionPayCreatedResponse { paymentId, tn, redirectUrl, params }
lib/service/api_service.dart   # createUnionPayPayment({productId, source}) → POST /v1/payment/unionpay/
lib/service/pay_service.dart   # payWithUnionPay:App/Web 分支编排 + 结果确认
lib/utils/pay_result.dart      # showPayResultDialog:统一支付结果弹窗

5.2 App 支付流程(payWithUnionPay)

final created = await _api.createUnionPayPayment(productId: productId, source: 'app');
final tn = created.tn;

// Mock 模式(tn 以 mock_ 开头):直接模拟支付成功
if (tn.startsWith('mock_')) {
  await _api.simulatePaymentSuccess(created.paymentId);
  await _queryFinalStatus(context, created.paymentId, channelName: '云闪付');
  return;
}

// 真实模式:uppay:// scheme 拉起云闪付 App
await launchUrl(Uri.parse('uppay://sdkpay?tn=$tn'),
    mode: LaunchMode.externalApplication);
// 用户支付完成后回到 App,轮询后端确认最终状态
await _queryFinalStatus(context, created.paymentId, channelName: '云闪付');

uppay:// scheme 拉起是演示用简化方案;正式接入推荐集成银联官方原生 SDK(UnionPay Android SDK / UPPayPlugin),由 SDK 完成唤起、支付结果回调与二次验签,可靠性更高。

5.3 Web/手机网站支付流程

final created = await _api.createUnionPayPayment(productId: productId, source: 'web');
await _handleWebRedirect(
  context: context,
  url: created.redirectUrl,        // 银联收银台
  paymentId: created.paymentId,
  channelName: '云闪付',
);

_handleWebRedirect 弹窗提供「打开支付页 / 模拟支付成功(Mock)/ 取消」,真实模式打开支付页后轮询后端。

5.4 最终状态确认

_queryFinalStatus 每 1s 轮询 GET /v1/payment/{payment_id}(最多约 12s 覆盖回调延迟),成功后弹出 showPayResultDialog(绿色对勾 + 渠道 + 订单号),点击「完成」回到商品页。

6. 接口定义

方法 路径 说明
POST /v1/payment/unionpay/ 创建云闪付支付。{product_id, source};App 返回 {payment_id, tn, sandbox},web 额外返回 redirect_url
GET /v1/payment/:payment_id 查询支付状态 {success, note}
POST /v1/payment/notify/unionpay 银联异步通知(表单 POST,验签后更新订单)
POST /v1/payment/simulate/success 仅 MOCK_MODE:{payment_id} 模拟支付成功

7. 安全与注意事项

  1. 签名与验签:下单必须用商户私钥签名;回调必须用银联公钥验签,防止伪造通知。
  2. 金额以服务端为准:金额一律后端按 product_id 计算(单位分),不信任客户端。
  3. 回调幂等markPaid 幂等,同一订单重复通知只处理一次。
  4. 回调地址公网可达:生产环境必须 HTTPS;本地联调可用内网穿透工具将 UNIONPAY_NOTIFY_URL 映射到外网。
  5. 沙箱与生产网关:沙箱 gateway.test.95516.com,生产 gateway.95516.com;上线前务必确认 UNIONPAY_SANDBOX=false
  6. 敏感信息:商户号、私钥证书不得进入版本控制(已加入 .gitignore)。
  7. 交易对账:生产建议定时调用银联 00 查询 接口对账,处理回调丢失场景。