### 准备工作
微信支付不支持对接个人,个体工商户或者企业必须完成微信认证才能开通支付。
参考官方文档 [接入前准备](https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_8_1.shtml) 中直连模式,开发前需要获取以下参数:
- 小程序 AppID
登录 [小程序后台](https://mp.weixin.qq.com/) ,在菜单 "开发"-"开发设置" 看到小程序的 **AppID**,如果没有需要 [注册小程序](https://mp.weixin.qq.com/wxopen/waregister?action=step1)。
- 商户号 mchid
登录 [商户平台](https://pay.weixin.qq.com/index.php/core/home/login?return_url=%2Fpublic%2Fwxpay%2Fapply_guidee) 获取 mchid,由数字组成。
- 绑定 AppID 和 mchid
登录 [小程序后台](https://mp.weixin.qq.com/) ,在菜单 "功能"-"微信支付" 绑定商户号,如果没有开通微信支付功能需先开通。
- 配置 APIv3 秘钥
按照文档配置 **APIv3 秘钥**,配置成功后需要记录下字符,开发时会用到。
- 下载并配置商户证书
按照文档操作后,查看证书文件夹有 3 个文件,其中 apiclient_key.pem 是 **商户 API 私钥**,要妥善保存,开发时会用到。另外还需要 **商户API 证书序列号**,使用 openssl 命令或者第三方证书 [解析工具](https://myssl.com/cert_decode.html) 可查看证书序列号。
- 小程序配置服务器域名
登录小程序后台,在菜单 "开发"-"开发设置" 中配置服务器域名(https 开头的后端服务器域名)。
准备好 AppID、mchid、APIv3 秘钥、商户 API 私钥、商户 API 证书序列号 后即可开始进入开发阶段。
### 开发阶段
业务流程参考官方文档 [开发指引](https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_8_2.shtml) 中的 快速接入-业务流程图。
- 创建系统订单
- 小程序下单
- 获取支付签名数据(可以单独接口,也可以在下单成功后返回)
- 小程序调起支付
- 处理支付通知内容,更新系统订单状态

下面重点说明几个在开发中容易出错的接口。
#### 初始化
JAVA 开发目前都使用 Spring 框架,所以首先初始化一个可以 [自动更新证书](https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient) 的 HttpClient 和专门请求微信支付接口的 RestTemplate。
> 说明:自动更新的证书指的是微信支付平台证书,和准备阶段的商户 API 证书不是同一个,微信支付平台证书是由微信支付负责申请的,包含微信支付平台标识、公钥信息。使用该证书中的公钥进行应答签名的验证。
引入 maven 依赖
```xml
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.2.2</version>
</dependency>
```
初始化
```java
@Configuration
public class RestTemplateConfig {
// 商户API私钥
private static PrivateKey merchantPrivateKey;
static {
try {
merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream("商户API私钥apiclient_key.pem文件中的内容".getBytes()));
} catch (Exception e) {
e.printStackTrace();
}
}
@Bean
public PrivateKey privateKey() {
return merchantPrivateKey;
}
@Value("${wx.shh.id}")
private String merchantId; // 商户号
@Value("${wx.shh.certNumber}")
private String merchantSerialNumber; // 商户api证书的证书序列号
@Value("${wx.shh.apiv3key}")
private String apiV3key; // 商户APIv3密钥
@Bean("wxPayHttpClient")
public HttpClient wxPayHttpClient(AutoUpdateCertificatesVerifier autoUpdateCertificatesVerifier) {
return WechatPayHttpClientBuilder.create()
.withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
.withValidator(new WechatPay2Validator(autoUpdateCertificatesVerifier))
.build();
}
@Bean
public AutoUpdateCertificatesVerifier autoUpdateCertificatesVerifier() {
// 自动更新微信支付平台证书
return new AutoUpdateCertificatesVerifier(
new WechatPay2Credentials(merchantId, new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)),
apiV3key.getBytes());
}
// 请求微信支付相关接口可以直接使用 wxPayRestTemplate
@Bean("wxPayRestTemplate")
public RestTemplate wxPayRestTemplate() {
return new RestTemplate(new HttpComponentsClientHttpRequestFactory(wxPayHttpClient());
}
}
```
#### 小程序下单
调用该接口在微信支付服务后台生成预支付交易单,成功时会返回预支付回话标识 prepay_id 用于后续接口调用中使用,该值有效期为2小时。
直接参考官方文档 API 列表 - [JSAPI下单](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml) 中描述,组装请求参数即可,需要注意的是其中通知地址 notify_url 是用来接收微信支付结果的回调地址,该地址需要是 https 开头并且外网可访问的地址。
#### 支付数据签名
通过 JSAPI下单 成功获取预支付交易会话标识(prepay_id)后,需要通过小程序调起支付API来调起微信支付收银台。[小程序调起支付 API](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml) 需要对请求数据签名,签名计算在后端完成。
签名数据实体类
```java
@Data
public class PaySign {
/**
* 小程序id
*/
private String appId;
/**
* 时间戳 单位:秒
*/
private String timeStamp;
/**
* 随机字符串,不长于32位
*/
private String nonceStr;
/**
* 订单详情扩展字符串
* 小程序下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=***
*/
private String package3;
/**
* 签名类型,默认为RSA,仅支持RSA
*/
private String signType = "RSA";
/**
* 签名,使用字段appId、timeStamp、nonceStr、package计算得出的签名值
*/
private String paySign;
}
```
计算签名值,小程序需要按以下方式生成签名数据
```java
// 商户API私钥
@Autowired
private PrivateKey privateKey;
/**
* 构造签名串
* app_id 小程序AppId
* prepay_id 预支付交易会话标识
*/
private PaySign generatePaySign(String prepay_id, String app_id) throws Exception {
PaySign paySign = new PaySign();
paySign.setAppId(app_id);
paySign.setTimeStamp(String.valueOf(System.currentTimeMillis()/1000));
paySign.setNonceStr(RandomStringUtils.randomAlphanumeric(32));
paySign.setPackage3("prepay_id="+prepay_id);
String message = paySign.getAppId() + "\n" +
paySign.getTimeStamp() + "\n" +
paySign.getNonceStr() + "\n" +
paySign.getPackage3() + "\n";
paySign.setPaySign(sign(message.getBytes("utf-8")));
paySign.setAppId("");
// paySign.setTime(LocalDateTime.now());
return paySign;
}
// 计算签名值
String sign(byte[] message) throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(privateKey);
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
```
#### 支付通知
用户支付完成后,微信会把相关支付结果和用户信息通过回调通知地址发送给后端,后端需要接收处理该消息,并返回应答。
重点是数据签名验证和解密。
```java
// 商户APIv3秘钥
@Value("${wx.shh.apiv3key}")
private String apiV3key;
@Autowired
private AutoUpdateCertificatesVerifier verifier;
/**
* 小程序支付结果通知
* 1 验签
* 2 解密
* 3 处理订单
* 4 响应
*
* 验证签名时,从微信请求头中获取如下参数:
* Wechatpay-Signature 应答签名
* Wechatpay-Serial 微信支付的平台证书序列号
* Wechatpay-Timestamp 应答时间戳
* Wechatpay-Nonce 应答随机串
*
* @return map
*/
@PostMapping("/callBack")
public Map<String, String> xcxPayCallBack(HttpServletRequest request) {
String signature = request.getHeader("Wechatpay-Signature");
String serial = request.getHeader("Wechatpay-Serial");
String timestamp = request.getHeader("Wechatpay-Timestamp");
String nonce = request.getHeader("Wechatpay-Nonce");
String body = "";
try {
StringBuilder sb = new StringBuilder();
BufferedReader br = request.getReader();
String str;
while((str = br.readLine()) != null){
sb.append(str);
}
body = sb.toString();
} catch (Exception e) {
log.info("小程序支付回调读取 body 异常", e);
}
log.info("小程序支付回调, Wechatpay-Signature:{}, Wechatpay-Serial:{}, Wechatpay-Timestamp:{}, Wechatpay-Nonce:{}, body:{}",
signature, serial, timestamp, nonce, body);
Map<String, String> result = new HashMap<>(2);
try {
// 1 签名验证,验证是否为微信服务器发送的报文(使用微信平台公钥对签名串和签名进行验签)
String text = timestamp + "\n" + nonce + "\n" + body + "\n";
if (!verifier.verify(serial, text.getBytes("utf-8"), signature)) {
log.info("小程序支付回调验签失败,text:{},signature:{},serial:{}", text, signature, serial);
result.put("code","FAIL");
result.put("message","验签失败");
return result;
}
// 2 解密
PayCallBack payCallBack = JSONObject.parseObject(body, PayCallBack.class);
if (payCallBack != null && payCallBack.getResource() != null) {
PayCallBack.Resource resource = payCallBack.getResource();
String decrypt = new AesUtil(apiV3key.getBytes("utf-8")).decryptToString(resource.getAssociated_data().getBytes(),
resource.getNonce().getBytes(), resource.getCiphertext());
log.info("小程序支付回调,解密后信息:{}", decrypt);
PayNotifyRequest notifyRequest = JSONObject.parseObject(decrypt, PayNotifyRequest.class);
// 2.1 保存通知内容(非必须)
// 3 根据解密后的信息更新订单状态
// updateOrderStatus();
}
result.put("code","SUCCESS");
result.put("message","成功");
} catch (Exception e) {
log.info("小程序支付回调异常", e);
result.put("code","FAIL");
result.put("message","失败");
}
// 4 响应
log.info("小程序支付回调,响应结果:{}", result);
return result;
}
```
相关实体类
```java
@Data
public class PayCallBack {
// 通知ID
private String id;
// 通知创建时间
private String create_time;
// 通知类型
private String event_type;
// 通知数据类型
private String resource_type;
// 回调摘要
private String summary;
// 通知数据
private Resource resource;
@Data
public static class Resource{
// 加密算法类型
private String algorithm;
// 数据密文
private String ciphertext;
// 附加数据
private String associated_data;
// 原始类型
private String original_type;
// 随机串
private String nonce;
}
}
```
```java
@Data
public class PayNotifyRequest {
// 应用id
private String appid;
// 直连商户号
private String mchid;
// 商户订单号
private String out_trade_no;
// 微信支付订单号
private String transaction_id;
// 交易类型 cc.wo66.haoke.wx.pay.TradeType
private String trade_type;
// 交易状态 cc.wo66.haoke.wx.pay.TradeStatus
private String trade_state;
// 交易状态描述
private String trade_state_desc;
// 付款银行
private String bank_type;
// 附加数据 (否)
private String attach;
// 支付完成时间 示例值:2018-06-08T10:34:56+08:00
private String success_time;
// 订单金额
private Amount amount;
// 支付者
private Payer payer;
// 优惠功能 promotion_detail
// 场景信息 scene_info
// 结算信息
@Data
public static class Payer {
// 用户标识
private String openid;
}
@Data
public static class Amount {
// 总金额 (单位是分)
private int total;
// 货币类型 (CHY)
private String currency;
// 用户支付金额
private int payer_total;
// 用户支付币种
private String payer_currency;
}
}
```
#### 下载账单
该接口响应的信息请求头中不包含微信接口响应的签名值,因此需要跳过验签的流程。所以可以在前面 RestTemplateConfig 类中再初始化一个跳过验签的 HttpClient。
```java
@Bean("wxPayDownloadClient")
public CloseableHttpClient wxPayDownloadClient() {
return WechatPayHttpClientBuilder.create()
.withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
.withValidator((response) -> true)
.build();
}
```
另外需要说明的是,如果某日期没有交易记录,不会返回空数据,而是返回状态码为 400,提示信息为账单文件不存在的错误。

微信小程序支付开发(v3版)