本文目录
1. 场景
用户打开付款码,商户使用扫码枪等设备扫码用户的付款码完成支付,注意此时用户的二维码是被商户扫描的。
2. 开发说明
付款码支付可以在PC机、自助机的网页使用,只需要插入USB扫码器即可。
扫码器的原理需要简单说明下,扫码器扫码相当于键盘输入,扫码器会将二维码的识别结果(一般是数字或者英文字母或者网址)作为键盘输入内容通过USB输入我们的计算机。此处需要注意的是,大多数品牌的扫码器会在识别结果后面附带回车键,少部分品牌的扫码器会不附加任何内容(不附加的情况下,一般会通过指定长度的二维码内容确定输入结束)。
在网页点击开始支付后,需要展示倒计时信息。如果倒计时结束用户还没出示付款码,此时应结束本次支付行为。如果在倒计时期间,用户出示付款码且扫码成功,则发起付款码支付,并向用户提示付款结果。如果后台返回支付结果为USERPAYING,表示用户尚未完成支付(一般是扫码了,弹出输入密码框,但未完成输入密码),此时还需要通过定时器调用后端接口查询支付结果。
3. 普通商户付款码支付
3.1 项目构建
同样构建springboot项目,并配置pom.xml引入微信公众号、微信支付相关的依赖。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>cn.pandabrother</groupId>
<artifactId>wx-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.0.0</maven-jar-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- 添加swagger2相关功能 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!-- 添加swagger-ui相关功能 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!-- 微信公众号 -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.1.0</version>
</dependency>
<!-- 微信支付 -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>4.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.2 编写配置类
编写微信支付配置类:
/**
* 微信支付配置
*/
@Configuration
public class PayConfig {
public static boolean isServiceMode = false;
@Bean
public WxPayService wxService() {
WxPayConfig payConfig = new WxPayConfig();
// ----------------------------------------------------------------------------------普通商户
if (isServiceMode == false) {
payConfig.setAppId("");// 微信公众号或者小程序等的appid
payConfig.setMchId("");// 微信支付商户号
payConfig.setMchKey("");// 微信支付商户密钥
payConfig.setKeyPath("classpath:xxx.p12");// p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
}
// ----------------------------------------------------------------------------------服务商+特约商户
else if (isServiceMode == true) {
payConfig.setAppId("");// 微信公众号或者小程序等的appid
payConfig.setMchId("");// 服务商商户号
payConfig.setMchKey("");// 服务商商户密钥
payConfig.setSubAppId("");// 服务商模式下的子商户公众账号ID,注意此处如果子商户使用了服务商的appid,则无需填写
payConfig.setSubMchId("");// 服务商模式下的子商户号
payConfig.setKeyPath("classpath:xxx.p12");// p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
}
// 可以指定是否使用沙箱环境
payConfig.setUseSandboxEnv(false);
WxPayService wxPayService = new WxPayServiceImpl();
wxPayService.setConfig(payConfig);
return wxPayService;
}
}
3.3 编写网页
通过定时器完成倒计时,通过监听扫码器来实现扫码,扫码后发起付款码支付。支付结果如果不确定还需要继续通过定时器查询订单结果。
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<input id="btn-pay" type="button" value="开始付款码支付" onclick="btnPay()"> |
<div id="info-box">信息提示:</div>
<script src="https://cdn.staticfile.org/jquery/1.9.1/jquery.min.js"></script>
<script src="http://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<script>
// 支付倒计时
var curOrderId = ""; // 订单编号
var timer = null; //定时器
var seconds = 0; //倒计时剩余时间
function btnPay() {
if (timer == null) {
$("#btn-pay").blur();
seconds = 90;//倒计时90秒
timer = setInterval("clock()", 1000); // 开始计时
window.addEventListener('keypress', listenKeyboard); // 开始监听键盘
} else {
alert("请等待上次支付完成!");
}
}
function clock() {
var str = "请将付款码对准扫码器完成支付,剩余时间:" + seconds + "秒";
$("#info-box").text(str);
if (seconds <= 0) {
stopPay(); // 停止支付
alert("支付超时!");
} else if (curOrderId != "") {
//如果存在当前订单,且未结束支付,需要主动向后台查询订单状态
var param = {
outTradeNo : curOrderId,
};
$.ajax({
type : "POST",
url : "/wx-server/microPayQuery",
data : JSON.stringify(param),
contentType : "application/json",
dataType : "json",
success : function(re) {
console.log("query re:", re);
if (re.returnCode == "SUCCESS" && re.resultCode == "SUCCESS" && re.tradeState == "SUCCESS") {
stopPay();
alert("查询支付成功!");
}
}
});
}
seconds--;
}
function stopPay() {
clearInterval(timer);
timer = null;
window.removeEventListener('keypress', listenKeyboard); // 停止监听键盘
barCode = "";
lastTime = 0;
curOrderId = "";
}
// 扫码器监听(实际上就是键盘监听)
var barCode = "";
var lastTime = 0;
function listenKeyboard(e) {
console.log("按键按下");
e = e || window.event;
var curCode = e.keyCode || e.which || e.charCode;
var curTime = new Date().getTime();
if (lastTime == 0) {
// 第一次按键
barCode = String.fromCharCode(curCode);
} else {
if (curTime - lastTime <= 100) {
if (curCode != 13) {
// 回车不算输入内容
barCode += String.fromCharCode(curCode);
}
} else if (curTime - lastTime > 500) {
// 输入间隔500毫秒清空
barCode = "";
lastTime = 0;
}
}
lastTime = curTime;
if (curCode == 13) {
// 回车
if (barCode != "" && barCode.length >= 8) {
console.log("扫码结果:" + barCode);
realPay();
}
}
}
function realPay() {
curOrderId = '20000006'; // 设置订单号
var param = {
body : '电费',
outTradeNo : curOrderId,
spbillCreateIp : '123.135.154.64',
feeType : 'CNY',
totalFee : 1, //单位分
authCode : barCode,
};
$.ajax({
type : "POST",
url : "/wx-server/microPay",
data : JSON.stringify(param),
contentType : "application/json",
dataType : "json",
success : function(re) {
console.log("microyPay re:", re);
if (re.returnCode == "SUCCESS" && re.resultCode == "SUCCESS" && re.tradeType == "MICROPAY") {
stopPay();
alert("直接支付成功!");
}
}
});
}
</script>
</body>
</html>
3.4 编写后端方法
后端有两个方法,一个是生成订单,一个是查询订单。
/**
* 付款码支付控制器
*/
@Controller
@Api(tags = "付款码支付API")
public class MicroPayController {
@Autowired
private WxPayService wxPayService;
/**
* 付款码支付
*/
@PostMapping("/microPay")
@ResponseBody
public WxPayMicropayResult microPay(@RequestBody WxPayMicropayRequest request) throws WxPayException {
WxPayMicropayResult result = wxPayService.micropay(request);
return result;
}
/**
* 查询订单
*/
@PostMapping("/microPayQuery")
@ResponseBody
public WxPayOrderQueryResult microPayQuery(@RequestBody WxPayOrderQueryRequest wxPayOrderQueryRequest) throws WxPayException {
return wxPayService.queryOrder(wxPayOrderQueryRequest);
}
}
3.5 测试
3.5.1 直接支付成功
将支付金额设为1,即1分,此时金额较小,无需用户输入密码,所以可以直接扫码完成支付。即realPay方法后显示alert("直接支付成功!");
。
此时查看后台返回结果,可见付款码支付后立即返回成功了。
3.5.2 查询支付成功
将支付金额改为10000,即100元,然后修改订单号发起新的支付,我们故意晚一会输入密码,这样就会通过microPayQuery查询来获取到订单结果,从而显示alert("查询支付成功!");
。
此时查看后台结果,tradeState由USERPAYING
变为SUCCESS
,也就是用户支付中,变为支付成功。这说明有时候直接支付未成功,还是需要借助查询功能完成支付的。
3.6 订单状态处理
最后别忘记,后端的microPay和microPayQuery方法都有可能发现支付成功,所以在这两个方法中都得处理订单状态更新。
4.服务商模式付款码支付
4.1 修改配置类
修改PayConfig的isServiceMode值为true,同时配置服务商模式下的各个参数:
else if (isServiceMode == true) {
payConfig.setAppId("");// 微信公众号或者小程序等的appid
payConfig.setMchId("");// 服务商商户号
payConfig.setMchKey("");// 服务商商户密钥
payConfig.setSubAppId("");// 服务商模式下的子商户公众账号ID,注意此处如果子商户使用了服务商的appid,则无需填写
payConfig.setSubMchId("");// 服务商模式下的子商户号
payConfig.setKeyPath("classpath:xxx.p12");// p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
}
4.2 产品授权
此时发起支付,会提示【特约子商户商户号未授权服务商的产品权限】,此时需要先登录服务商平台发起授权邀请,然后登录商户平台授权。
授权邀请依次点击【服务商平台】-【产品中心】-【特约商户授权产品】-【服务商付款码支付】-【发起邀请】。
特约商户授权依次点击【商户平台】-【产品中心】-【我授权的产品】-【授权】。
4.3 支付测试
此时再次发起支付,会发现支付商户变为了特约商户,测试成功。
5. 小结
付款码支付的方式比较绕,还是需要先理顺思路,再实现代码。
另外如果支付超时,为了保证订单状态一直,还应该主动撤销订单。调用撤销订单后,如果此订单用户支付失败,微信支付系统会将此订单关闭;如果用户支付成功,微信支付系统会将此订单资金退还给用户。这样才能保证订单状态一致。