微信支付商户系统接入Native支付
简易demo演示
Native支付介绍
目前微信支付有以下几种场景
- JSAPi支付,适合微信公众号及微信小程序
- APP支付
- H5支付
- Native支付,适合PC网站页面支付
微信支付商户平台
微信支付Native接口文档
Native支付是指商户系统按照微信支付协议生成支付二维码,用户再用微信“扫一扫”实现支付
前提准备
微信商家号、微信小程序或者微信公众号appId、商户证书
需要提前开启Native支付
需要将公众号或者小程序的appId在微信支付后台关联起来
在微信支付——账户中心——API安全,生成商户APIV3证书
业务流程图
主要步骤为,pc端生成订单调用Native下单接口生成微信的native 跳转链接,再生成二维码返回到页面,用户微信扫一扫完成支付,微信支付后台回调支付成功的链接。
支付接入
引入微信支付的SDK
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.14</version>
</dependency>
生成二维码的工具,这里我选择了zxing
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.4.1</version>
</dependency>
创建订单接口https://pay.weixin.qq.com/doc/v3/merchant/4012525095
需要商户号、商户APIV3密钥、商户证书序列号、商户API秘钥文件
正常秘钥文件放在服务器上读取,这里先直接放在项目文件resources下面
@Slf4j
@Service
public class WechatPayService extends AbstractPayService implements InitializingBean {
/**
* 商户号
*/
public static String merchantId = "xxx";
/**
* 商户证书序列号
*/
public static String merchantSerialNumber = "xxxx";
/**
* 商户APIV3密钥
*/
public static String apiV3Key = "xxxx";
private Config config;
/**
* 商户API私钥
*/
private String privateKeyPem;
@Override
public void afterPropertiesSet() throws Exception {
ClassPathResource resource = new ClassPathResource("wechat/apiclient_key.pem");
privateKeyPem = IOUtils.toString(resource.getInputStream());
config = new RSAAutoCertificateConfig.Builder()
.merchantId(merchantId)
.privateKey(privateKeyPem)
.merchantSerialNumber(merchantSerialNumber)
.apiV3Key(apiV3Key)
.build();
}
@Override
public String createPayOrder(String clientIp, OrderRequest orderRequest) {
final String orderNo = OrderUtils.generateOrderNo();
// 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错
// 构建service
NativePayService service = new NativePayService.Builder().config(config).build();
// request.setXxx(val)设置所需参数,具体参数可见Request定义
PrepayRequest request = new PrepayRequest();
Amount amount = new Amount();
amount.setTotal(1);
request.setAmount(amount);
request.setAppid("xxxx");
request.setMchid(merchantId);
request.setDescription("VIP体验卡");
request.setNotifyUrl("http://myrkfh.natappfree.cc/mark_day/wechat/pay/notify");
request.setOutTradeNo(orderNo);
// 调用下单方法,得到应答
PrepayResponse response = service.prepay(request);
// 使用微信扫描 code_url 对应的二维码,即可体验Native支付
log.info("调用微信生成订单返回结果:", response.toString());
if (StrUtil.isBlank(response.getCodeUrl())) {
throw new BaseException("创建订单失败");
}
String codeUrl = response.getCodeUrl();
// 生成二维码
QrConfig qrConfig = QrConfig.create();
String qrBase64Text = QrCodeUtil.generateAsBase64(codeUrl, qrConfig, "svg");
// 这里可以记录订单信息
return qrBase64Text;
}
orderNo在本系统中要唯一,不能有重复的订单号,否则下单失败
private static final String DATE_FORMAT = "yyyyMMddHHmmss";
public static String generateOrderNo() {
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
String timestamp = now.format(formatter);
return timestamp + RandomUtil.randomNumbers(18);
}
其中notifyUrl为支付成功回调地址,本地调试可以借助内网穿透工具,其他参数接口文档https://pay.weixin.qq.com/doc/v3/merchant/4012525095
微信返回的codeUrl为weixin://wxpay/bizpayurl?pr=Vd8RvS7z4格式的,放在手机上可以直接打开,我们要做的就是将codeUrl放入二维码的内容,也可以直接 使用Hutool,返回的二维码可以保存图片,将图片地址返回前端,也可以直接返回base64的图片地址,这里一般会记录订单数据为待支付状态数据,最后根据自己需要生成二维码的内容如下,有效期为两个小时
将地址返回过后后面就是等待微信支付的回调,
微信回调通知
微信支付通知接口文档 https://pay.weixin.qq.com/doc/v3/merchant/4012084431
如果处理成功了,需要返回HTTP状态码为200或204,否则微信会根据一定的策略进行重试
/**
* 微信支付回调实体
*
* @author liufuqiang
* @Date 2024-10-21 17:33:00
*/
@Data
public class WechatPayNotifyDTO {
private String id;
/**
* 通知创建的时间
*/
@JsonAlias("create_time")
private String createTime;
/**
* 通知的资源数据类型,支付成功通知为encrypt-resource。
*/
@JsonAlias("resource_type")
private String resourceType;
/**
* 通知的类型,支付成功通知的类型为TRANSACTION.SUCCESS
*/
@JsonAlias("event_type")
private String eventType;
/**
* 通知资源数据
*/
private Resource resource;
@Data
public static class Resource {
/**
* 加密算法AEAD_AES_256_GCM
*/
private String algorithm;
/**
* Base64编码后的开启/停用结果数据密文。
*/
private String ciphertext;
/**
* 附加数据。
*/
@JsonAlias("associated_data")
private String associatedData;
/**
* 原始回调类型,为transaction
*/
@JsonAlias("original_type")
private String originalType;
/**
* 加密使用的随机串。
*/
private String nonce;
}
/**
* 回调摘要
*/
private String summary;
接受回调
@PostMapping("/notify")
public void payNotify(HttpServletResponse response,
HttpServletRequest request,
@RequestHeader(Constant.WECHAT_PAY_SIGNATURE) String signature,
@RequestHeader(Constant.WECHAT_PAY_NONCE) String nonce,
@RequestHeader(Constant.WECHAT_PAY_TIMESTAMP) String timestamp,
@RequestHeader(Constant.WECHAT_PAY_SERIAL) String serialNo,
@RequestBody String requestBody
) {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.addHeader(Constant.WECHAT_PAY_SIGNATURE, signature);
httpHeaders.addHeader(Constant.WECHAT_PAY_NONCE, nonce);
httpHeaders.addHeader(Constant.WECHAT_PAY_TIMESTAMP, timestamp);
httpHeaders.addHeader(Constant.WECHAT_PAY_SERIAL, serialNo);
// 验证签名
boolean isSignValid = wechatPayService.validSignature(httpHeaders, requestBody);
if (!isSignValid) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
log.info("签名验证通过");
WechatPayNotifyDTO notifyDTO = JsonUtils.genBeanByJson(requestBody, WechatPayNotifyDTO.class);
wechatPayService.paymentHandle(notifyDTO);
response.setStatus(HttpServletResponse.SC_OK);
}
验签
防止伪造支付的回调,需要会返回的内容进行签名验证,
非文件/下载验证签名文档https://pay.weixin.qq.com/doc/v3/merchant/4012365350
如果嫌弃麻烦,也可以直接使用WechatPay2Validator
验证签名的时候尽量不要全部取出请求头的内容,根据HttpServletRequest全部取出的HeaderMap里面的key值全部是小写,签名的时候避免取到空值
签名的第三个参数为全部requestBody的内容,所以这里不能用实体对象接受数据,直接改用字符串接受数据
解密
根据API商户秘钥以及接口返回的associatedData、nonce对密文ciphertext进行解密,是AES对称加密解密
static final int TAG_LENGTH_BIT = 128;
public static String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext) {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(), "AES");
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData);
return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
} catch (NoSuchAlgorithmException | NoSuchPaddingException | UnsupportedEncodingException e) {
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException |
BadPaddingException e) {
}
return "";
}
解析完成是json字符串,这俩使用jackjson转为对象
@Data
public class OutOrderDTO {
private String appid;
@JsonAlias("out_trade_no")
private String outTradeNo;
@JsonAlias("trade_type")
private String tradeType;
@JsonAlias("trade_state")
private String tradeState;
@JsonAlias("trade_state_desc")
private String tradeStateDesc;
@JsonAlias("bank_ype")
private String bankType;
@JsonAlias("success_time")
private String payTime;
private Payer payer;
private Amount amount;
@Data
public static final class Payer {
private String openid;
}
@Data
public static final class Amount {
private Integer total;
@JsonAlias("payer_total")
private String payerTotal;
}
创建订单时的OutTradeNo订单号也会返回,为了避免重复处理,在订单表加状态判断或者加分布式锁
WechatPayNotifyDTO.Resource resource = wechatPayNotifyDTO.getResource();
String jsonStr = decryptToString(resource.getAssociatedData().getBytes(StandardCharsets.UTF_8),
resource.getNonce().getBytes(StandardCharsets.UTF_8), resource.getCiphertext());
log.info("微信支付回调信息:{}", jsonStr);
OutOrderDTO outOrderDTO = JsonUtils.genBeanByJson(jsonStr, OutOrderDTO.class);
// 订单处理
log.info("支付完成");
在手机端查看订单详情信息可以看到订单号,关联的appid以及其他的信息
同一个二维码也就是生成的native链接如果已经被支付过会提示订单已经支付,请勿重复发起支付