本文以微信统一支付接口模式二来进行开发
模式二与模式一相比,流程更为简单,不依赖设置的回调支付URL。商户后台系统先调用微信支付的统一下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。注意:code_url有效期为2小时(也可以根据统一支付接口内提供的定义订单有效期字段进行自定义设置,但是最好不要超过微信定义的这个两小时有效期),过期后扫码不能再发起支付(再次扫描二维码时会弹出提示,订单已过期)。
业务流程时序图
业务流程说明:
(1)商户后台系统根据用户选购的商品生成订单。
(2)用户确认支付后调用微信支付【统一下单API】生成预支付交易;
(3)微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码链接code_url。
(4)商户后台系统根据返回的code_url生成二维码。
(5)用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。
(6)微信支付系统收到客户端请求,验证链接有效性后发起用户支付,要求用户授权。
(7)用户在微信客户端输入密码,确认支付后,微信客户端提交授权。
(8)微信支付系统根据用户授权完成支付交易。
(9)微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。
(10)微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。
(11)未收到支付通知的情况,商户后台系统调用【查询订单API】。
(12)商户确认订单已支付后给用户发货。
根据微信官方提供的调用时序图可以看出模式二的接口需要调用微信API接口中的【统一下单API】和【查询订单API】两个接口就可以完成扫描支付这一动作。以下详细讲解这两个接口的实现步骤以及其中的坑
首先微信官方提供了接口如何调用的demo
demo下载链接:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=11_1
可以根据自己使用的开发语言下载对应的demo进行调试。我使用的是java所以下载的就是java对应的demo。
下载的demo如下图
demo导入Intellj后可以根据自己的需求进行项目目录的拆分,下面是我以springBoot项目进行拆分的目录
开发步骤:
首先根据官方示例继承WXPayConfig生成我们自己的配置文件
官方抽象类(根据自己的项目改造,下面是我改造之后的官方类):
package start.com.github.wxpay.sdk;
import java.io.InputStream;
public abstract class WXPayConfig {
/**
* 获取商户证书内容
*
* @return 商户证书内容
*/
public abstract InputStream getCertStream();
/**
* HTTP(S) 连接超时时间,单位毫秒
*
* @return
*/
public int getHttpConnectTimeoutMs() {
return 6*1000;
}
/**
* HTTP(S) 读数据超时时间,单位毫秒
*
* @return
*/
public int getHttpReadTimeoutMs() {
return 8*1000;
}
/**
* 获取WXPayDomain, 用于多域名容灾自动切换
* @return
*/
public abstract IWXPayDomain getWXPayDomain();
/**
* 是否自动上报。
* 若要关闭自动上报,子类中实现该函数返回 false 即可。
*
* @return
*/
public boolean shouldAutoReport() {
return true;
}
/**
* 进行健康上报的线程的数量
*
* @return
*/
public int getReportWorkerNum() {
return 6;
}
/**
* 健康上报缓存消息的最大数量。会有线程去独立上报
* 粗略计算:加入一条消息200B,10000消息占用空间 2000 KB,约为2MB,可以接受
*
* @return
*/
public int getReportQueueMaxSize() {
return 10000;
}
/**
* 批量上报,一次最多上报多个数据
*
* @return
*/
public int getReportBatchSize() {
return 10;
}
}
实现后的类:
package start.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import start.com.github.wxpay.sdk.IWXPayDomain;
import start.com.github.wxpay.sdk.WXPayConfig;
import start.com.github.wxpay.sdk.WXPayConstants;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
/**
* @author chengtonghua
* @date 2020-09-01
*/
@Component
public class MyConfig extends WXPayConfig {
private byte[] certData;
public MyConfig() throws Exception {
ClassPathResource resource = new ClassPathResource("credential/apiclient_cert.p12");
InputStream certStream = resource.getInputStream();
this.certData = new byte[(int)certStream.available()];
certStream.read(this.certData);
certStream.close();
}
@Value("${wxpay.keyAndId.appID}")
public String appID;
@Value("${wxpay.keyAndId.mchID}")
public String mchID;
@Value("${wxpay.keyAndId.key}")
public String key;
@Override
public InputStream getCertStream() {
ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);
return certBis;
}
@Override
public int getHttpConnectTimeoutMs() {
return 8000;
}
@Override
public int getHttpReadTimeoutMs() {
return 10000;
}
@Override
public IWXPayDomain getWXPayDomain() { // 这个方法需要这样实现, 否则无法正常初始化WXPay
IWXPayDomain iwxPayDomain = new IWXPayDomain() {
@Override
public void report(String domain, long elapsedTimeMillis, Exception ex) {
}
public DomainInfo getDomain(WXPayConfig config) {
return new DomainInfo(WXPayConstants.DOMAIN_API, true);
}
};
return iwxPayDomain;
}
}
只需要实现上述的config配置类就行了,其他的配置都可以使用微信已经配置好的sdk文件目录下的。上述文件内有通过注解注入的参数是放置在yml文件内的
所有的配置都完成后下面就是调用微信统一支付接口进行预创建订单:下单为post请求,这点一定要注意
@RequestMapping("/pay")
public void pay() throws Exception {
//注意,本文只是做一个请求demo的说明,正常情况下这类的数据请求应该放在service层去实现。
//获取当前系统时间
Calendar calendar= Calendar.getInstance();
SimpleDateFormat dateFormat= new SimpleDateFormat("yyyyMMddHHmmss");
Map<String, String> data = new HashMap<String, String>();
/**商品描述*/
data.put("body", "你好");
/**订单唯一编号*/
data.put("out_trade_no", WXPayUtil.generateNonceStr());
/**订单币种*/
data.put("fee_type", "CNY");
/**订单金额,单位分*/
data.put("total_fee", "331");
/**订单开始时间*/
data.put("time_start", dateFormat.format(calendar.getTime()));
/**订单结束时间,此处就可以指定生成订单的有效期,这个有效期就是二维码的有效期*/
calendar.add(Calendar.MINUTE, 10);
data.put("time_expire",dateFormat.format(calendar.getTime()));
/**下单IP*/
data.put("spbill_create_ip", "123.12.12.123");
/**回调方法地址,yml内配置的回调地址*/
data.put("notify_url",wxnotify);
/** 此处指定为扫码支付 */
data.put("trade_type", "NATIVE");
/** 商品ID,用来指定当前支付的商品是什么 */
data.put("product_id", "12");
try {
/**此处就会调用微信工具类的接口 进行创建订单,并解析微信返回值*/
Map<String, String> resp = wxPay.unifiedOrder(data);
System.out.println(resp);
} catch (Exception e) {
e.printStackTrace();
}
}
支付完成后微信回调方法:支付回调方法是post请求,如果系统有拦截器之类的配置,一定要将这个回调请求做成可以匿名访问的
/**
* 微信支付回调方法
* */
@ApiOperation("微信充值回调方法")
@PostMapping("/notify")
@AllowAnonymous(这个是匿名访问注解)
public void notify(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception{
log.info("[notify start------------------]");
String notifyXmlData = readXmlFromStream(httpServletRequest);
String resultXml = wxPayService.payAsyncNotifyVerificationSign(notifyXmlData);
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(httpServletResponse.getOutputStream());
bufferedOutputStream.write(resultXml.getBytes());
bufferedOutputStream.flush();
bufferedOutputStream.close();
log.info("[notify end-----------------------]");
}
/**
* 从流中读取微信返回的xml数据
*
* @param httpServletRequest
* @return
* @throws IOException
*/
private String readXmlFromStream(HttpServletRequest httpServletRequest) throws IOException {
InputStream inputStream = httpServletRequest.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
final StringBuffer sb = new StringBuffer();
String line = null;
try {
while ((line = bufferedReader.readLine()) != null) {
sb.append(line);
}
} finally {
bufferedReader.close();
inputStream.close();
}
return sb.toString();
}
回调方法service接口类:
/**
* 微信支付异步通知验证签名
* @param notifyXmlData
* @return
*/
@GetMapping("/notify")
String payAsyncNotifyVerificationSign(@RequestParam String notifyXmlData) throws Exception;
回调方法serviceImpl实现类:
@Override
@Transactional(rollbackFor = Exception.class)
public String payAsyncNotifyVerificationSign(String notifyXmlData) throws Exception {
String returnXmlMessage = null;
try {
Map<String, String> notifyMapData = WXPayUtil.xmlToMap(notifyXmlData);
log.info("[payAsyncNotifyVerificationSign] [xml转换为map数据成功] [notifyMapData:{}]", notifyMapData);
// 验证签名
boolean signatureValid = wxPay.isPayResultNotifySignatureValid(notifyMapData);
if (signatureValid) {
// TODO:订单支付成功之后相关业务逻辑...
// 一切正常返回的xml数据
returnXmlMessage = setReturnXml(WXPayConstants.SUCCESS, "OK");
}
log.info("[payAsyncNotifyVerificationSign] [out_trade_no:{}] [支付成功异步消息处理成功:{}]", notifyMapData.get("out_trade_no"), returnXmlMessage);
} else {
returnXmlMessage = setReturnXml(WXPayConstants.FAIL, "Verification sign failed!");
log.info("[payAsyncNotifyVerificationSign] [out_trade_no:{}] [验签失败:{}]", notifyMapData.get("out_trade_no"), returnXmlMessage);
}
} catch (IOException e) {
log.error("[payAsyncNotifyVerificationSign] [读取微信服务器返回流中xml数据时发生异常:{}] ", e);
returnXmlMessage = setReturnXml(WXPayConstants.FAIL, "An exception occurred while reading the WeChat server returning xml data in the stream.");
} catch (Exception e) {
log.error("[payAsyncNotifyVerificationSign] [xml数据:{}] [异常:{}] ", notifyXmlData, e);
returnXmlMessage = setReturnXml(WXPayConstants.FAIL, "Payment successful, exception occurred during asynchronous notification processing.");
log.warn("[payAsyncNotifyVerificationSign] [支付成功异步消息处理失败:{}]", returnXmlMessage);
}
return returnXmlMessage;
}
/**
* 设置返回给微信服务器的xml信息
*
* @param returnCode
* @param returnMsg
* @return
*/
private String setReturnXml(String returnCode, String returnMsg) {
return "<xml><return_code><![CDATA[" + returnCode + "]]></return_code><return_msg><![CDATA[" + returnMsg + "]]></return_msg></xml>";
}
至此就已经完成了微信支付接口的请求已经微信完成支付之后回调自己系统的全部方法。
但是呢为了防止微信支付成功后回调方法失败,需要再调用微信查询订单状态的方法去查询一下当前订单的状态。
@Autowired
private MyConfig config;
@Autowired
private WXPay wxPay;
@GetMapping("/search")
public void search() throws Exception {
Map<String, String> data = new HashMap<String, String>();
//需要查询的订单号
data.put("out_trade_no", "202009091059590000362");
try {
//此处就可以拿到我们请求微信的返回值
Map<String, String> resp = wxPay.orderQuery(data);
//拿到微信返回的订单就可以做自己想做的状态处理了
System.out.println(resp);
} catch (Exception e) {
e.printStackTrace();
}
}
需要注意的是微信如果是使用沙箱环境进行测试的话需要,需要生成沙箱环境专用的key,划重点:这个key微信官方没有说它的有效期,但是这个key三天就失效了需要重新生成。这个方法可以当作一个单元测试单独运行,当然你可以为了避免麻烦直接装这个方法嵌入微信支付接口调用的的位置例如下图(但是如果使用下面这种方式会导致测试环节中测试人员测试的代码和正式环境走的代码分支不一致所以这种方式非常不推荐,推荐使用最下方那种方式使用工具类生成沙箱key):
嵌入前:
/**
* 向 Map 中添加 appid、mch_id、nonce_str、sign_type、sign <br>
* 该函数适用于商户适用于统一下单等接口,不适用于红包、代金券接口
*
* @param reqData
* @return
* @throws Exception
*/
public Map<String, String> fillRequestData(Map<String, String> reqData) throws Exception {
reqData.put("appid", config.appID);
reqData.put("mch_id", config.mchID);
reqData.put("nonce_str", WXPayUtil.generateNonceStr());
if (SignType.MD5.equals(this.signType)) {
reqData.put("sign_type", WXPayConstants.MD5);
} else if (SignType.HMACSHA256.equals(this.signType)) {
reqData.put("sign_type", WXPayConstants.HMACSHA256);
}
reqData.put("sign", WXPayUtil.generateSignature(reqData, config.key, this.signType));
return reqData;
}
嵌入后:
/**
* 向 Map 中添加 appid、mch_id、nonce_str、sign_type、sign <br>
* 该函数适用于商户适用于统一下单等接口,不适用于红包、代金券接口
*
* @param reqData
* @return
* @throws Exception
*/
public Map<String, String> fillRequestData(Map<String, String> reqData) throws Exception {
reqData.put("appid", config.appID);
reqData.put("mch_id", config.mchID);
reqData.put("nonce_str", WXPayUtil.generateNonceStr());
if (SignType.MD5.equals(this.signType)) {
reqData.put("sign_type", WXPayConstants.MD5);
} else if (SignType.HMACSHA256.equals(this.signType)) {
reqData.put("sign_type", WXPayConstants.HMACSHA256);
}
//如果是进入沙盒系统
if(useSandbox){
Map<String,String> map = doGetSandboxSignKey();
reqData.put("nonce_str", map.get("nonce_str"));
reqData.put("sandbox_signkey", map.get("sandbox_signkey"));
//沙箱环境需要指定加密方式为md5加密,不传的话默认md5加密
reqData.put("sign", WXPayUtil.generateSignature(reqData, map.get("sandbox_signkey")));
}else{
reqData.put("sign", WXPayUtil.generateSignature(reqData, config.key, this.signType));
}
return reqData;
}
生成沙箱key的代码如下:
@Autowired
private WXPay wxPay;
@Test
public void getKy() throws Exception{
HashMap<String, String> data = new HashMap <String, String>();
/***商户号*/
data.put("mch_id","你自己的微信商户号获取");
/***获取随机字符串*/
data.put("nonce_str", WXPayUtil.generateNonceStr());
/***生成签名时候需要将生产环境的key传递进来*/
String sign = WXPayUtil.generateSignature(data, "你的微信公众号后台的key");
data.put("sign", sign);
//得到sandbox_signkey
String result = wxPay.requestWithoutCert("/sandboxnew/pay/getsignkey", data, 10000, 10000);
System.out.println(result);
Map<String,String> map = WXPayUtil.xmlToMap(result);
System.out.println(map.get("sandbox_signkey")+">>>>>>");
map.put("sign",sign);
map.put("nonce_str",data.get("nonce_str"));
}