支付接入
1. 支付核心概念
1.1 标准支付流程
完整的支付生命周期包含以下核心环节:
下单 -> 支付 -> 回调 -> 查单 -> 退款| 环节 | 说明 | 关键点 |
|---|---|---|
| 下单 | 商户系统向支付平台发起支付请求,获取支付凭证 | 订单号唯一、金额准确、参数签名 |
| 支付 | 用户使用支付平台完成资金确认 | 不同渠道支付模式不同 |
| 回调 | 支付平台异步通知商户支付结果 | 验签、幂等处理、状态更新 |
| 查单 | 商户主动查询订单支付状态 | 补偿机制、超时处理 |
| 退款 | 商户向支付平台发起退款申请 | 原路退回、部分退款、退款幂等 |
1.2 支付模式
| 模式 | 适用场景 | 用户操作 | 典型渠道 |
|---|---|---|---|
| JSAPI | 公众号/服务号内 | 在 H5 页面调起支付控件 | 微信 JSAPI |
| Native | PC 网站 | 扫码支付 | 微信 Native |
| App | 移动 App | 调起 App 内的支付 SDK | 微信 App、支付宝 App |
| H5 | 手机浏览器网页 | 跳转支付平台收银台 | 支付宝手机网站支付 |
| 小程序 | 微信小程序内 | 调起小程序支付组件 | 微信小程序支付 |
| 当面付 | 线下扫码 | 用户扫码或出示付款码 | 支付宝当面付 |
1.3 对账与结算
- 对账:商户与支付平台每日核对交易记录,确保双方数据一致。
- 结算:支付平台将交易资金结算至商户收款账户,通常为 T+1 到账。
- 对账文件:支付平台每日提供对账文件(CSV / Excel),包含当日所有交易明细。
2. 微信支付
2.1 申请接入流程
- 注册微信商户平台账号,完成企业资质认证。
- 提交商户资料,包括营业执照、法人身份证、银行账户信息。
- 审核通过后获取
merchant_id(商户号)。 - 配置 API 密钥(
api_v3_key),下载商户证书(apiclient_cert.p12/apiclient_key.pem)。 - 设置支付回调通知地址(
notify_url)。 - 开发联调,验证支付流程。
2.2 V3 接口规范
微信支付 V3 接口采用 RESTful 设计风格:
- 请求方式:GET / POST
- 请求体:JSON 格式
- 认证方式:HTTP 请求头添加
Authorization: WECHATPAY2-SHA256-RSA2048签名 - 响应体:JSON 格式
- 平台证书验证:使用微信支付平台证书验证响应签名
请求签名示例:
// 构建签名串
String signStr = httpMethod + "\n"
+ url + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ body + "\n";
// 使用商户私钥签名
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(privateKey);
sign.update(signStr.getBytes(StandardCharsets.UTF_8));
byte[] signature = sign.sign();
// 设置 Authorization 头
String token = "WECHATPAY2-SHA256-RSA2048 "
+ "mchid=\"" + mchId + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + serialNo + "\","
+ "signature=\"" + Base64.getEncoder().encodeToString(signature) + "\"";平台证书验证:
// 使用微信支付平台证书验证响应签名
public boolean verifyResponse(String body, String signature, String timestamp,
String nonce, String serial, X509Certificate certificate) {
String signStr = timestamp + "\n" + nonce + "\n" + body + "\n";
try {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initVerify(certificate.getPublicKey());
sign.update(signStr.getBytes(StandardCharsets.UTF_8));
return sign.verify(Base64.getDecoder().decode(signature));
} catch (Exception e) {
return false;
}
}2.3 JSAPI 支付
JSAPI 支付适用于微信公众号、服务号内的 H5 页面。流程如下:
第一步:获取用户 openid
用户授权后,通过 OAuth2 接口获取用户的 openid。
第二步:调用统一下单接口获取 prepay_id
POST https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
// 请求体
{
"appid": "wx8888888888888888",
"mchid": "1900009191",
"description": "商品描述",
"out_trade_no": "2025060122000001",
"notify_url": "https://yourdomain.com/api/pay/wechat/notify",
"amount": {
"total": 100, // 单位:分
"currency": "CNY"
},
"payer": {
"openid": "oUpF8uMuAJ2pxb1Q9zNjWeS6o"
}
}第三步:前端调起支付
后端接收到 prepay_id 后,生成支付参数签名,返回给前端:
public Map<String, String> buildJsapiPayParams(String appId, String prepayId,
String privateKey) throws Exception {
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String nonceStr = UUID.randomUUID().toString().replace("-", "");
String packageStr = "prepay_id=" + prepayId;
// 构建签名串
String signStr = appId + "\n" + timestamp + "\n" + nonceStr + "\n" + packageStr + "\n";
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(getPrivateKey(privateKey));
sign.update(signStr.getBytes(StandardCharsets.UTF_8));
String paySign = Base64.getEncoder().encodeToString(sign.sign());
Map<String, String> params = new HashMap<>();
params.put("appId", appId);
params.put("timeStamp", timestamp);
params.put("nonceStr", nonceStr);
params.put("package", packageStr);
params.put("signType", "RSA");
params.put("paySign", paySign);
return params;
}前端使用 WeixinJSBridge 或 wx.chooseWXPayment 调起支付:
function onBridgeReady(params) {
WeixinJSBridge.invoke('getBrandWCPayRequest', {
appId: params.appId,
timeStamp: params.timeStamp,
nonceStr: params.nonceStr,
package: params.package,
signType: params.signType,
paySign: params.paySign
}, function(res) {
if (res.err_msg === 'get_brand_wcpay_request:ok') {
// 支付成功
}
});
}2.4 Native 支付
Native 支付适用于 PC 网站,通过生成二维码供用户扫码支付。
POST https://api.mch.weixin.qq.com/v3/pay/transactions/native
// 请求体
{
"appid": "wx8888888888888888",
"mchid": "1900009191",
"description": "商品描述",
"out_trade_no": "2025060122000001",
"notify_url": "https://yourdomain.com/api/pay/wechat/notify",
"amount": {
"total": 100,
"currency": "CNY"
}
}响应中包含 code_url,将其生成二维码图片展示给用户:
// 响应
{
"code_url": "weixin://wxpay/bizpayurl?pr=xxxxx"
}
// 生成二维码(使用 QRCode 库)
public BufferedImage generateQrCode(String codeUrl, int width, int height) {
QRCodeWriter writer = new QRCodeWriter();
BitMatrix matrix = writer.encode(codeUrl, BarcodeFormat.QR_CODE, width, height);
MatrixToImageRenderer matrixToImageRenderer = new MatrixToImageRenderer();
return matrixToImageRenderer.render(matrix);
}2.5 支付回调通知
微信支付以 POST 方式向商户 notify_url 发送回调通知,内容为 JSON 格式并包含签名信息。
@PostMapping("/api/pay/wechat/notify")
public ResponseEntity<String> handlePayNotify(
@RequestBody String body,
@RequestHeader("Wechatpay-Signature") String signature,
@RequestHeader("Wechatpay-Timestamp") String timestamp,
@RequestHeader("Wechatpay-Nonce") String nonce,
@RequestHeader("Wechatpay-Serial") String serial) {
try {
// 1. 获取微信支付平台证书(按 serial 查找)
X509Certificate certificate = wechatPayConfig.getCertificate(serial);
// 2. 验证签名
boolean verified = verifyResponse(body, signature, timestamp, nonce, serial, certificate);
if (!verified) {
return ResponseEntity.status(401).body("{\"code\":\"FAIL\",\"message\":\"签名验证失败\"}");
}
// 3. 解析通知数据
WechatPayNotify notify = JsonUtil.parseObject(body, WechatPayNotify.class);
Resource resource = decryptResource(notify.getResource()); // 解密 resource
// 4. 处理订单状态
String outTradeNo = resource.getOutTradeNo();
String transactionId = resource.getTransactionId();
String tradeState = resource.getTradeState();
if ("SUCCESS".equals(tradeState)) {
paymentService.handlePaidOrder(outTradeNo, transactionId);
}
// 5. 返回成功应答
return ResponseEntity.ok("{\"code\":\"SUCCESS\",\"message\":\"成功\"}");
} catch (Exception e) {
log.error("微信支付回调处理失败", e);
return ResponseEntity.status(500).body("{\"code\":\"FAIL\",\"message\":\"处理失败\"}");
}
}回调通知数据解密:
微信支付 V3 回调的 resource 字段使用 AES-GCM 加密,需解密获取明文:
public WechatPayResource decryptResource(EncryptedResource encrypted) {
// 获取 API V3 密钥
SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), "AES");
// 解析 nonce 和关联数据
byte[] nonce = encrypted.getNonce().getBytes(StandardCharsets.UTF_8);
byte[] associatedData = encrypted.getAssociatedData().getBytes(StandardCharsets.UTF_8);
byte[] ciphertext = Base64.getDecoder().decode(encrypted.getCiphertext());
// AES-GCM 解密
GCMParameterSpec gcmSpec = new GCMParameterSpec(128, nonce);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec);
cipher.updateAAD(associatedData);
byte[] plaintext = cipher.doFinal(ciphertext);
return JsonUtil.parseObject(new String(plaintext, StandardCharsets.UTF_8),
WechatPayResource.class);
}2.6 退款接口
POST https://api.mch.weixin.qq.com/v3/refund/domestic/refunds
// 请求体
{
"out_trade_no": "2025060122000001",
"out_refund_no": "2025060122000001_R1",
"reason": "商品已退款",
"notify_url": "https://yourdomain.com/api/pay/wechat/refund-notify",
"amount": {
"refund": 100, // 退款金额,单位:分
"total": 100, // 原订单金额
"currency": "CNY"
}
}退款结果同样通过异步回调通知,处理逻辑与支付回调类似。
3. 支付宝支付
3.1 申请接入流程
- 注册支付宝开放平台 / 商家中心账号。
- 创建应用(App),选择支付产品(当面付、手机网站支付等)。
- 配置应用公钥(上传 RSA 公钥),获取支付宝公钥。
- 签约产品,审核通过后获得支付能力。
- 配置回调地址(
notify_url)。 - 开发联调,使用支付宝沙箱环境测试。
3.2 开放平台 SDK 使用
支付宝提供官方 SDK(alipay-sdk-java),简化开发流程:
// 初始化 AlipayClient
AlipayClient alipayClient = new DefaultAlipayClient(
"https://openapi.alipay.com/gateway.do", // 网关地址
appId, // 应用 ID
privateKey, // 应用私钥
"json", // 返回格式
"utf-8", // 编码
alipayPublicKey, // 支付宝公钥
"RSA2" // 签名算法
);3.3 当面付(扫码支付)
当面付适用于线下扫码场景,商户生成二维码,用户扫码付款。
// 创建当面付请求
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
request.setNotifyUrl("https://yourdomain.com/api/pay/alipay/notify");
request.setBizContent(JsonUtil.toJsonString(new HashMap<String, Object>() {{
put("out_trade_no", "2025060122000001");
put("total_amount", "1.00"); // 单位:元
put("subject", "商品描述");
put("timeout_express", "30m"); // 超时时间
}}));
AlipayTradePrecreateResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
String qrCode = response.getQrCode(); // 二维码内容
// 将 qrCode 生成二维码图片展示给用户
}轮询查单:
当面付为异步通知模式,生成二维码后需轮询查询支付结果:
public void pollTradeResult(String outTradeNo, int maxRetries, long intervalMs) {
for (int i = 0; i < maxRetries; i++) {
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
request.setBizContent(JsonUtil.toJsonString(Map.of("out_trade_no", outTradeNo)));
AlipayTradeQueryResponse response = alipayClient.execute(request);
if ("TRADE_SUCCESS".equals(response.getTradeStatus())) {
paymentService.handlePaidOrder(outTradeNo, response.getTradeNo());
return;
}
Thread.sleep(intervalMs);
}
// 超时未支付,更新订单状态为已关闭
paymentService.handleTimeoutOrder(outTradeNo);
}3.4 手机网站支付
手机网站支付适用于手机浏览器网页,流程如下:
第一步:创建交易
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
request.setReturnUrl("https://yourdomain.com/pay/return"); // 同步跳转
request.setNotifyUrl("https://yourdomain.com/api/pay/alipay/notify"); // 异步通知
request.setBizContent(JsonUtil.toJsonString(new HashMap<String, Object>() {{
put("out_trade_no", "2025060122000001");
put("total_amount", "1.00");
put("subject", "商品描述");
put("product_code", "QUICK_WAP_WAY"); // 手机网站支付产品码
}}));第二步:生成 form 表单
// SDK 直接返回 form 表单 HTML
String formHtml = alipayClient.pageExecute(request).getBody();
// 响应给前端
response.setContentType("text/html;charset=utf-8");
response.getWriter().write(formHtml);前端接收到 HTML 后自动提交表单,跳转到支付宝收银台。
3.5 支付回调通知
支付宝以 POST 方式向商户 notify_url 发送表单格式通知,验签后处理业务逻辑:
@PostMapping("/api/pay/alipay/notify")
public ResponseEntity<String> handleAlipayNotify(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((key, values) -> params.put(key, values[0]));
try {
// 1. 验签
boolean signVerified = AlipaySignature.rsaCheckV1(
params, alipayPublicKey, "utf-8", "RSA2");
if (!signVerified) {
return ResponseEntity.ok("failure");
}
// 2. 验证关键参数
String outTradeNo = params.get("out_trade_no");
String tradeNo = params.get("trade_no");
String tradeStatus = params.get("trade_status");
String totalAmount = params.get("total_amount");
// 3. 验签通过后,验证金额、订单号与商户系统一致
// 4. 处理交易状态
if ("TRADE_SUCCESS".equals(tradeStatus)) {
paymentService.handlePaidOrder(outTradeNo, tradeNo);
} else if ("TRADE_CLOSED".equals(tradeStatus)) {
paymentService.handleClosedOrder(outTradeNo);
} else if ("TRADE_FINISHED".equals(tradeStatus)) {
paymentService.handleFinishedOrder(outTradeNo);
}
// 5. 返回成功应答(支付宝要求返回 "success")
return ResponseEntity.ok("success");
} catch (Exception e) {
log.error("支付宝回调处理失败", e);
return ResponseEntity.ok("failure");
}
}3.6 退款接口
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
request.setBizContent(JsonUtil.toJsonString(new HashMap<String, Object>() {{
put("out_trade_no", "2025060122000001");
put("refund_amount", "1.00"); // 单位:元
put("out_request_no", "2025060122000001_R1"); // 退款请求号(幂等字段)
put("refund_reason", "商品已退款");
}}));
AlipayTradeRefundResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
// 退款成功
}4. 银联支付
4.1 接入流程
- 向银联提交入网申请,完成商户资质审核。
- 获取商户号(
merId)、接入用户名、密码。 - 下载银联证书(PFX 格式)和银联公钥证书。
- 配置回调地址(
backUrl/frontUrl)。 - 开发联调,使用银联测试环境验证。
4.2 无跳转支付模式
银联无跳转支付(后台类交易)适用于商户后台直接发起扣款:
// 构建请求报文
Map<String, String> requestData = new HashMap<>();
requestData.put("version", "5.1.0");
requestData.put("encoding", "utf-8");
requestData.put("signMethod", "01"); // RSA 签名
requestData.put("txnType", "01"); // 交易类型 01=消费
requestData.put("txnSubType", "01"); // 交易子类型
requestData.put("bizType", "000201"); // 产品类型
requestData.put("channelType", "07"); // 渠道类型 07=互联网
requestData.put("merId", "888888888888888");
requestData.put("orderId", "2025060122000001");
requestData.put("txnTime", "20250601120000"); // YYYYMMDDHHmmss
requestData.put("txnAmt", "100"); // 单位:分
requestData.put("currencyCode", "156"); // 人民币
requestData.put("backUrl", "https://yourdomain.com/api/pay/union/notify");
// 签名
requestData = signData(requestData, certPath, certPwd);
// 发送请求
String responseData = HttpUtil.postForm("https://gateway.95516.com/gateway/api/backTransReq.do", requestData);
// 验签
boolean verified = verifyResponse(responseData, unionPublicKeyPath);4.3 回调与验签
@PostMapping("/api/pay/union/notify")
public ResponseEntity<String> handleUnionPayNotify(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((key, values) -> params.put(key, values[0]));
// 1. 验签
if (!verifyResponse(params, unionPublicKeyPath)) {
return ResponseEntity.ok("signature error");
}
// 2. 验证交易状态
String respCode = params.get("respCode");
String orderId = params.get("orderId");
String txnAmt = params.get("txnAmt");
String queryId = params.get("queryId");
// 3. 验证金额与订单信息
// 4. 处理业务
if ("00".equals(respCode) || "A6".equals(respCode)) {
paymentService.handlePaidOrder(orderId, queryId);
}
// 5. 返回应答(银联要求返回 "ok")
return ResponseEntity.ok("ok");
}5. 统一支付抽象
为屏蔽不同支付渠道的差异,采用策略模式设计统一支付抽象层。
5.1 策略模式设计
+-------------------+ +-----------------------+
| PaymentContext |------>| <<interface>> |
| (支付上下文) | | PaymentStrategy |
+-------------------+ +-----------------------+
| + pay(request) |
| + refund(request) |
| + query(request) |
+-----------+-----------+
|
+-----------------------+--------------------+
| | |
+-----------+--------+ +---------+---------+ +-------+--------+
| WechatPayStrategy | | AlipayStrategy | | UnionPayStrategy|
+---------------------+ +-------------------+ +-----------------+5.2 核心接口定义
/**
* 支付策略接口
*/
public interface PaymentStrategy {
/** 支付 */
PayResponse pay(PayRequest request);
/** 退款 */
RefundResponse refund(RefundRequest request);
/** 查询订单 */
QueryResponse query(QueryRequest request);
/** 处理回调 */
NotifyResponse handleNotify(NotifyRequest request);
/** 获取渠道类型 */
PayChannelEnum getChannel();
}5.3 统一请求/响应对象
/** 支付请求 */
@Data
public class PayRequest {
private String outTradeNo; // 商户订单号
private Long totalAmount; // 订单总金额,单位:分
private String description; // 商品描述
private String openid; // 用户标识(JSAPI 支付需要)
private String clientIp; // 客户端 IP
private String notifyUrl; // 回调地址
private String returnUrl; // 同步跳转地址(支付宝 H5)
private PayModeEnum payMode; // 支付模式:JSAPI / NATIVE / APP / H5 / BAR_CODE
private Integer expiresMinutes; // 订单过期时间
}
/** 支付响应 */
@Data
public class PayResponse {
private boolean success;
private String outTradeNo;
private String prepayId; // 微信 prepay_id
private String codeUrl; // 微信 Native 二维码 URL / 支付宝二维码
private String tradeNo; // 支付平台交易号
private String payForm; // 支付宝 H5 form 表单 HTML
private Map<String, Object> extraParams; // 额外参数(JSAPI 调起参数等)
private String errorCode;
private String errorMessage;
}
/** 统一回调通知 */
@Data
public class NotifyRequest {
private String channel; // 支付渠道
private String body; // 原始请求体
private Map<String, String> headers; // 原始请求头
private Map<String, String> params; // 原始请求参数
}
/** 退款请求 */
@Data
public class RefundRequest {
private String outTradeNo; // 原商户订单号
private String outRefundNo; // 商户退款单号
private Long refundAmount; // 退款金额,单位:分
private Long totalAmount; // 原订单总金额,单位:分
private String reason; // 退款原因
private String notifyUrl; // 退款结果回调地址
}5.4 支付网关接口设计
/** 支付网关统一入口 */
@RestController
@RequestMapping("/api/pay")
public class PaymentController {
@Autowired
private PaymentContext paymentContext;
/** 发起支付 */
@PostMapping("/pay")
public Result<PayResponse> pay(@RequestBody @Valid PayRequest request) {
PayResponse response = paymentContext.execute(request.getChannel(), strategy ->
strategy.pay(request));
return Result.success(response);
}
/** 退款 */
@PostMapping("/refund")
public Result<RefundResponse> refund(@RequestBody @Valid RefundRequest request) {
RefundResponse response = paymentContext.execute(request.getOutTradeNo(), strategy ->
strategy.refund(request));
return Result.success(response);
}
/** 查询订单 */
@GetMapping("/query/{outTradeNo}")
public Result<QueryResponse> query(@PathVariable String outTradeNo) {
QueryResponse response = paymentContext.query(outTradeNo);
return Result.success(response);
}
/** 微信支付回调 */
@PostMapping("/wechat/notify")
public ResponseEntity<String> wechatNotify(HttpServletRequest request) {
return paymentContext.handleNotify(PayChannelEnum.WECHAT, request);
}
/** 支付宝回调 */
@PostMapping("/alipay/notify")
public ResponseEntity<String> alipayNotify(HttpServletRequest request) {
return paymentContext.handleNotify(PayChannelEnum.ALIPAY, request);
}
/** 银联回调 */
@PostMapping("/union/notify")
public ResponseEntity<String> unionNotify(HttpServletRequest request) {
return paymentContext.handleNotify(PayChannelEnum.UNION_PAY, request);
}
}支付上下文实现:
@Component
public class PaymentContext {
@Autowired
private List<PaymentStrategy> strategyList;
private Map<PayChannelEnum, PaymentStrategy> strategyMap;
@PostConstruct
public void init() {
strategyMap = strategyList.stream()
.collect(Collectors.toMap(PaymentStrategy::getChannel, Function.identity()));
}
public <T> T execute(PayChannelEnum channel, Function<PaymentStrategy, T> action) {
PaymentStrategy strategy = strategyMap.get(channel);
if (strategy == null) {
throw new IllegalArgumentException("不支持的支付渠道: " + channel);
}
return action.apply(strategy);
}
public ResponseEntity<String> handleNotify(PayChannelEnum channel, HttpServletRequest request) {
NotifyRequest notifyRequest = buildNotifyRequest(channel, request);
PaymentStrategy strategy = strategyMap.get(channel);
NotifyResponse response = strategy.handleNotify(notifyRequest);
return buildNotifyResponse(channel, response);
}
}5.5 具体策略实现示例
@Component
public class WechatPayStrategy implements PaymentStrategy {
@Autowired
private WechatPayConfig wechatPayConfig;
@Override
public PayResponse pay(PayRequest request) {
// 根据支付模式选择不同接口
switch (request.getPayMode()) {
case JSAPI:
return payJsapi(request);
case NATIVE:
return payNative(request);
case APP:
return payApp(request);
case H5:
return payH5(request);
default:
throw new IllegalArgumentException("不支持的支付模式: " + request.getPayMode());
}
}
private PayResponse payJsapi(PayRequest request) {
// 调用微信 JSAPI 统一下单接口
// 返回 prepay_id 和 JSAPI 调起参数
}
private PayResponse payNative(PayRequest request) {
// 调用微信 Native 统一下单接口
// 返回 code_url
}
@Override
public RefundResponse refund(RefundRequest request) {
// 调用微信退款接口
}
@Override
public QueryResponse query(QueryRequest request) {
// 调用微信查单接口
}
@Override
public NotifyResponse handleNotify(NotifyRequest request) {
// 验证签名、解密数据、处理订单状态
}
@Override
public PayChannelEnum getChannel() {
return PayChannelEnum.WECHAT;
}
}6. 支付安全
6.1 回调验签
所有支付渠道的回调通知必须验签,防止伪造回调。
RSA SHA256withRSA 签名验证:
public boolean verifySignature(String content, String signature, PublicKey publicKey) {
try {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initVerify(publicKey);
sign.update(content.getBytes(StandardCharsets.UTF_8));
return sign.verify(Base64.getDecoder().decode(signature));
} catch (Exception e) {
log.error("签名验证异常", e);
return false;
}
}验签要点:
| 要点 | 说明 |
|---|---|
| 签名串构建 | 严格按支付平台文档要求的顺序和格式拼接 |
| 公钥来源 | 支付宝公钥、银联公钥从平台下载,微信平台证书定期更新 |
| 签名算法 | 统一使用 SHA256withRSA(RSA2) |
| 重放攻击防范 | 验证时间戳,5 分钟以上的回调拒绝处理 |
6.2 幂等处理
支付回调可能重复送达,退款请求同样需要幂等。
支付回调幂等:
@Transactional
public void handlePaidOrder(String outTradeNo, String transactionId) {
// 使用数据库唯一约束或分布式锁保证幂等
// 方案一:数据库行锁
Order order = orderDao.selectByOutTradeNoForUpdate(outTradeNo);
if (order.getPaid()) {
log.info("订单 {} 已支付,跳过重复回调", outTradeNo);
return;
}
// 更新订单状态
order.setStatus(OrderStatus.PAID);
order.setTransactionId(transactionId);
order.setPaidTime(LocalDateTime.now());
orderDao.updateById(order);
// 触发后续业务(发货、通知等)
eventPublisher.publishEvent(new OrderPaidEvent(order));
}
// 方案二:redis 分布式锁 + 回调记录表
public void handlePaidOrderWithIdempotent(String outTradeNo, String transactionId) {
String lockKey = "pay:callback:" + outTradeNo;
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!Boolean.TRUE.equals(locked)) {
log.info("订单 {} 回调正在处理中", outTradeNo);
return;
}
try {
// 查询回调记录表,避免重复处理
if (callbackDao.existsByOutTradeNo(outTradeNo)) {
log.info("订单 {} 回调已处理", outTradeNo);
return;
}
// 业务处理...
// 记录处理结果
CallbackRecord record = new CallbackRecord();
record.setOutTradeNo(outTradeNo);
record.setTransactionId(transactionId);
record.setProcessed(true);
callbackDao.insert(record);
} finally {
redisTemplate.delete(lockKey);
}
}退款幂等:
@Transactional
public void handleRefund(String outTradeNo, String outRefundNo, Long refundAmount) {
// outRefundNo 唯一索引保证幂等
// 数据库层面:refund_order 表 out_refund_no 唯一约束
RefundOrder refundOrder = refundDao.selectByOutRefundNo(outRefundNo);
if (refundOrder != null) {
log.info("退款单 {} 已处理,跳过", outRefundNo);
return;
}
// 执行退款逻辑...
}6.3 金额精度
不同支付渠道金额单位不同,需统一处理。
| 支付渠道 | 金额单位 | 示例 |
|---|---|---|
| 微信支付 | 分(整数) | 1 元 = 100 |
| 支付宝 | 元(字符串,两位小数) | 1 元 = "1.00" |
| 银联 | 分(整数) | 1 元 = 100 |
统一金额转换处理:
/** 金额精度工具类 */
public class MoneyUtil {
/** 元转分 */
public static Long yuanToFen(BigDecimal yuan) {
return yuan.multiply(BigDecimal.valueOf(100))
.setScale(0, RoundingMode.HALF_UP)
.longValue();
}
/** 分转元(保留两位小数) */
public static BigDecimal fenToYuan(Long fen) {
return BigDecimal.valueOf(fen)
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
}
/** 元转字符串(支付宝接口需要) */
public static String yuanToString(BigDecimal yuan) {
return yuan.setScale(2, RoundingMode.HALF_UP).toString();
}
/** 金额比较(double 类型避免浮点误差) */
public static boolean amountEquals(Long fen1, String yuanStr) {
Long fen2 = yuanToFen(new BigDecimal(yuanStr));
return fen1.equals(fen2);
}
}6.4 重复支付防范
用户可能在短时间内多次点击"支付"按钮,或在支付平台重复发起付款。防范措施如下:
- 订单状态校验:下单时校验订单尚未支付,支付成功后及时更新状态。
- 分布式锁:在创建支付请求和回调处理时加锁,防止并发。
- 支付关闭机制:订单超时后关闭支付通道,不再接收该订单的支付回调。
- 金额比对:回调通知中的金额必须与商户系统订单金额一致。
- 商户订单号唯一:每个支付订单使用全局唯一的
out_trade_no,支付平台会拒绝重复的订单号。
7. 支付对账
7.1 每日对账流程
获取对账文件 -> 逐笔核对 -> 差异处理 -> 自动调账7.2 对账文件获取
各支付平台每日提供对账文件(通常 T+1 提供):
/** 对账文件服务 */
@Service
public class ReconciliationService {
@Autowired
private PaymentContext paymentContext;
/** 每日对账任务 */
@Scheduled(cron = "0 30 9 * * ?") // 每天 9:30 执行
public void dailyReconciliation() {
LocalDate tradeDate = LocalDate.now().minusDays(1);
// 依次处理各渠道
for (PayChannelEnum channel : PayChannelEnum.values()) {
reconcile(channel, tradeDate);
}
}
public void reconcile(PayChannelEnum channel, LocalDate tradeDate) {
// 1. 下载对账文件
List<ChannelTransaction> channelTransactions = paymentContext.execute(channel,
strategy -> strategy.downloadBill(tradeDate));
// 2. 获取商户系统交易记录
List<MerchantTransaction> merchantTransactions =
transactionDao.selectByDate(channel, tradeDate);
// 3. 逐笔核对
List<ReconcileDiff> diffs = reconcileTransactions(
channelTransactions, merchantTransactions);
// 4. 处理差异
if (!diffs.isEmpty()) {
handleDiffs(diffs);
}
// 5. 记录对账结果
saveReconcileResult(channel, tradeDate, diffs);
}
}7.3 逐笔核对
/** 逐笔核对 */
public List<ReconcileDiff> reconcileTransactions(
List<ChannelTransaction> channelList,
List<MerchantTransaction> merchantList) {
List<ReconcileDiff> diffs = new ArrayList<>();
// 构建映射:outTradeNo -> Transaction
Map<String, ChannelTransaction> channelMap = channelList.stream()
.collect(Collectors.toMap(ChannelTransaction::getOutTradeNo, Function.identity()));
Map<String, MerchantTransaction> merchantMap = merchantList.stream()
.collect(Collectors.toMap(MerchantTransaction::getOutTradeNo, Function.identity()));
// 核对通道方有、商户方无的交易(长款)
for (ChannelTransaction ct : channelList) {
if (!merchantMap.containsKey(ct.getOutTradeNo())) {
diffs.add(new ReconcileDiff(
ct.getOutTradeNo(),
DiffType.EXTRA_AMOUNT, // 长款
ct.getAmount(),
BigDecimal.ZERO,
"通道方有交易,商户系统无记录"
));
}
}
// 核对商户方有、通道方无的交易(短款)
for (MerchantTransaction mt : merchantList) {
if (!channelMap.containsKey(mt.getOutTradeNo())) {
diffs.add(new ReconcileDiff(
mt.getOutTradeNo(),
DiffType.MISSING_AMOUNT, // 短款
BigDecimal.ZERO,
mt.getAmount(),
"商户系统有交易,通道方无记录"
));
}
}
// 核对金额不一致
for (ChannelTransaction ct : channelList) {
MerchantTransaction mt = merchantMap.get(ct.getOutTradeNo());
if (mt != null && !ct.getAmount().equals(mt.getAmount())) {
diffs.add(new ReconcileDiff(
ct.getOutTradeNo(),
DiffType.AMOUNT_MISMATCH, // 金额不一致
ct.getAmount(),
mt.getAmount(),
String.format("通道方金额 %s,商户系统金额 %s",
ct.getAmount(), mt.getAmount())
));
}
}
return diffs;
}7.4 差异处理
| 差异类型 | 说明 | 处理策略 |
|---|---|---|
| 长款 | 通道方有、商户方无 | 核对是否为测试交易;补录订单并人工确认 |
| 短款 | 商户方有、通道方无 | 检查是否通道方漏单;联系支付平台确认 |
| 金额不一致 | 双方金额不符 | 优先向通道方发起二次确认;人工介入核查 |
自动调账:
public void handleDiffs(List<ReconcileDiff> diffs) {
for (ReconcileDiff diff : diffs) {
switch (diff.getType()) {
case EXTRA_AMOUNT:
// 长款:补录订单
autoCorrectExtra(diff);
break;
case MISSING_AMOUNT:
// 短款:标记异常,人工处理
markManualReview(diff);
break;
case AMOUNT_MISMATCH:
// 金额不一致:发起查单确认
confirmWithChannel(diff);
break;
}
}
}
private void autoCorrectExtra(ReconcileDiff diff) {
// 自动补录交易记录
Transaction transaction = new Transaction();
transaction.setOutTradeNo(diff.getOutTradeNo());
transaction.setAmount(diff.getChannelAmount());
transaction.setStatus(TransactionStatus.PAID);
transaction.setRemark("对账自动补录");
transactionDao.insert(transaction);
}8. 总结
| 维度 | 微信支付 V3 | 支付宝 | 银联 |
|---|---|---|---|
| 接口协议 | RESTful JSON | HTTP XML/JSON | HTTP Form |
| 签名算法 | SHA256withRSA | SHA256withRSA | SHA256withRSA |
| 金额单位 | 分 | 元(字符串) | 分 |
| SDK 完善度 | 官方 SDK | 官方 SDK 完善 | 官方 SDK |
| 回调格式 | JSON + AES-GCM 加密 | Form 表单 | Form 表单 |
| 对账文件 | CSV | CSV / Excel | CSV |
统一支付抽象层核心目标:
- 业务代码只依赖
PaymentStrategy接口,不感知具体渠道实现。 - 新增支付渠道时只需实现
PaymentStrategy接口并注册为 Spring Bean。 - 回调验签、幂等处理、金额转换等公共安全逻辑在策略实现中完成,上层业务无需关注。