PC网站微信扫码支付(v3) Native支付对接流程以及注意事项
一.需要的参数
(1)获取商户号(mchID)
微信商户平台: https://pay.weixin.qq.com/
场景:Native支付
步骤:提交资料 =>签署协议 => 获取商户号
(2)获取APPID
微信公众平台: https://mp.weixin.qq.com/
步骤:注册服务号=>服务号认证 =>获取APPID => 绑定商户号(商户产品中心的APPID账号管理中把APPID配置进去)
(3)获取APIv3秘钥(v3key)
APIv3版本的接口需要此秘钥
步骤:登录商户平台=>选择账户中心=>安全中心=>AP安全=>设置APIV3密钥随机感码生成工具: https://suijimimashengcheng.bmcx.com/
(4)申请商户API证书(可通过代码拉取证书,需要拿到证书序列号,和私钥文件)
APIv3版本的所有接口都需要,步骤:登录商户平台=>选择账户中心=>安全中心=>API安全=>申请API证书
=>证书管理=>申请新证书=>下载证书工具
代码获取证书需要:证书序列号(mch_serial_no)
如何查看证书序列号?
登录商户平台:API安全]=>API证书 =>查看证书,可查看商户API证书序列号。
私钥文件是下载证书pem结尾的文件,如下:
可以把此文件放到支付项目的resources文件下,用以下方式读取:
/**
* 获取商户的私钥文件
* @param filename
* @return
*/
private PrivateKey getPrivateKey(String filename){
try {
return PemUtil.loadPrivateKey(new FileInputStream(filename));
} catch (FileNotFoundException e) {
throw new RuntimeException("私钥文件不存在", e);
}
}
也可以放在服务器,定义privateKeyPath变量参数,用一下方式读取:
/**
* 获取商户的私钥文件
*/
public PrivateKey getPrivateKey() {
try {
URL url=new URL(privateKeyPath);
URLConnection connection=url.openConnection();
InputStream stream=connection.getInputStream();
return PemUtil.loadPrivateKey(stream);
} catch (Exception e) {
throw new RuntimeException("私钥文件不存在", e);
}
}
(5)微信服务器IP
"wxIp":"https://api.mch.weixin.qq.com"
(6)回调路径:回调路径不需要在开放平台配置,如果项目有网关,只需在网关开放回调接口
例如: "notify":"http://gateway.jkcgy.com/payment/v1/wxpay/native/notify"
http://gateway.jkcgy.com:就是你网关访问地址
payment/v1/wxpay/native/notify:就是你的回调接口路径
可以在浏览器进行测试,如果出现如下:则回调路径开放成功。
二.可参考资料
JAVA对接Demo:https://github.com/LXT2017/JavaLearnProject
,可以拉到本地参考
对接中遇到问题可以在微信开放社区进行搜索,社区地址:https://developers.weixin.qq.com/community/pay/article
三.配置参数存入数据库
因为支付参数对安全要求高,所以一般在开发中不以配置文件的形式存放支付参数,需要把支付参数存放到数据库。
可以把所有的参数配置成一个json字段,如下:
json字段格式:
{
"mchID":"xxxxx",
"appID":"xxxxxxx",
"mch-serial-no":"xxxxxx",
"wxIp":"https://api.mch.weixin.qq.com",
"privateKeyPath":"https://服务器地址/apiclient_key.pem在服务器的地址",
"v3key":"xxxxxxx",
"notify":"xxxxxxx",
"notify_test":"xxxxx",
"notify_prod":"xxxxxxx"
}
在数据库配置好参数后,可以在代码中配置一个WxPayConfig的配置类,用来读取数据库参数。
读取数据库参数可以通过实现:InitializingBean,重写afterPropertiesSet(),InitializingBean接口为bean提供了属性初始化后的处理方法,它只有一个afterPropertiesSet方法,
凡是继承该接口的类,在bean的属性初始化后都会执行该方法。从而配置全局可引用的参数。
完整WxPayConfig配置,包含拉取证书,跳过验签的一些配置:
@Configuration
@Data
@Slf4j
public class WxPayConfig implements InitializingBean {
@Resource
private WxPaymentCfgMapper wxPaymentCfgMapper;
// 商户号
private String mchId;
// 商户API证书序列号
private String mchSerialNo;
// 商户私钥文件
private String privateKeyPath;
// APIv3密钥
private String apiV3Key;
// APPID
private String appid;
// 微信服务器地址
private String domain;
// 接收结果通知地址
private String notifyDomain;
/**
* 获取商户的私钥文件
* @param
* @return
*/
public PrivateKey getPrivateKey(){
try {
URL url = new URL(privateKeyPath);
URLConnection connection = url.openConnection();
InputStream stream = connection.getInputStream();
return PemUtil.loadPrivateKey(stream);
} catch (Exception e) {
throw new RuntimeException("私钥文件不存在", e);
}
}
/**
* 获取签名验证器
* @return
*/
@Bean
public ScheduledUpdateCertificatesVerifier getVerifier(){
log.info("获取签名验证器");
//获取商户私钥
PrivateKey privateKey = getPrivateKey();
//私钥签名对象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//身份认证对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 使用定时更新的签名验证器,不需要传入证书
ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
wechatPay2Credentials,
apiV3Key.getBytes(StandardCharsets.UTF_8));
return verifier;
}
/**
* 获取HttpClient,无需进行应答签名验证,跳过验签的流程
*/
@Bean(name = "wxPayNoSignClient")
public CloseableHttpClient getWxPayNoSignClient(ScheduledUpdateCertificatesVerifier verifier){
//获取商户私钥
PrivateKey privateKey = getPrivateKey();
//用于构造HttpClient
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
//设置商户信息
.withMerchant(mchId, mchSerialNo, privateKey)
//无需进行签名验证、通过withValidator((response) -> true)实现
.withValidator((response) -> true);
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
log.info("== getWxPayNoSignClient END ==");
return httpClient;
}
@Override
public void afterPropertiesSet(){
WxPaymentCfg wxPayment = wxPaymentCfgMapper.selectById(1);
String json = wxPayment.getJson();
WxPaymentCfgDto wxPayments = JSONObject.parseObject(json, WxPaymentCfgDto.class);
this.setMchId(wxPayments.getMchId());
this.setAppid(wxPayments.getAppId());
this.setPrivateKeyPath(wxPayments.getPrivateKeyPath());
this.setApiV3Key(wxPayments.getV3key());
this.setDomain(wxPayments.getWxIp());
this.setNotifyDomain(wxPayments.getNotify());
this.setMchSerialNo(wxPayments.getMchSerialNo());
}
}
四.对接注意事项
一.回调问题
回调通知:
同样的通知可能会多次发送给商户系统,商户系统必须能够正确处理重复的通知
确保回调URL是外部可正常访问的,且不能携带后缀参数。
回调通知重复问题:
重复通知的时候,微信的请求id是一样的,用这个做请求幂等性处理响应给微信的内容不规范 或者 超过5秒没响应。
并发下重复通知问题:
可以通过可重入锁 ReentrantLock的tryLock()解决
private final ReentrantLock lock = new ReentrantLock();
/**在对业务数据进行状态检查和处理之前,
要采用数据锁进行并发控制,
以避免函数重入造成的数据混乱**/
//尝试获取锁:成功获取则立即返回true,获取失败则立即返回false。不会一直等待锁的释放
if (lock.tryLock())
测试的问题:
如果有多个未响应的,则测试的请求id,可能有之前的请求继续回调过来 .
二.金额转换问题
微信支付的金额单位是分,必须转换为Int类型
Integer totalfree = order.getTotalAmount().multiply(new BigDecimal(100)).stripTrailingZeros().intValue();
三.退款原因问题
微信退款原因官方规定不能超过80个字,可以做一个截取。
五.核心流程
第一步 获取报文
第二步 验证签名(确保是微信传输过来的)
第三步 解密(AES对称解密出原始数据)
第四步 处理业务逻辑
第五步 响应请求