版权声明:本文首发 http://asing1elife.com ,转载请注明出处。 https://blog.csdn.net/asing1elife/article/details/82620021
Java微信扫码支付
以下内容是基于模式二开发
在开发之前需要先到微信支付官网注册账号,并获取到以下信息
appid:wx1137939101111111公众账号id
mch_id:1438111111 商户号
key:4Inn0va1eSxOnl1neqsxwuhan1111111密钥
send_url:https://api.mch.weixin.qq.com/pay/unifiedorder统一下单API
notify_url:http://127.0.0.1:8080/sop/order/notify/wechat支付成功回调地址
更多精彩
官网
- 微信支付官网
- 扫码支付开发者文档
模式
- 需要在公众平台后台设置支付回调URL ,用于接收用户扫码后微信支付系统回调的productid和openid
- 直接调用统一下单API 即可,相对于模式一更为简洁
定义接口对象
- 根据 统一下单接口API 定义四个对象,用于发送和接收数据
UnifiedOrderRequest.java 统一下单请求参数-必填项
public class UnifiedOrderRequest {
private String appid;
private String mch_id;
private String nonce_str;
private String sign;
private String body;
private String out_trade_no;
private Integer total_fee;
private String spbill_create_ip;
private String notify_url;
private String trade_type;
}
UnifiedOrderRequestExt.java 统一下单请求参数-非必填项
public class UnifiedOrderRequestExt extends UnifiedOrderRequest {
private String device_info;
private String sign_type;
private String detail;
private String attach;
private String fee_type;
private String time_start;
private String time_expire;
private String product_id;
private String limit_pay;
private String openid;
}
UnifiedOrderResponse.java 统一下单返回参数-必填项
public class UnifiedOrderResponse {
private String return_code;
private String appid;
private String mch_id;
private String nonce_str;
private String sign;
private String result_code;
private String trade_type;
private String prepay_id;
}
UnifiedOrderResponseExt.java 统一下单返回参数-非必填项
public class UnifiedOrderResponseExt extends UnifiedOrderResponse {
private String return_msg;
private String device_info;
private String err_code;
private String err_code_des;
private String code_url;
}
定义一个标签用于显示二维码
- 调用统一下单API成功后,会返回一系列XML数据,其中code_url表示返回的预支付交易链接,可将其生成二维码图片
<div class="order-pay-panel order-wechat-panel">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title">
<i class="icon-th-large"></i> 微信支付
</h4>
</div>
<div class="modal-body">
<div class="wechat-qrcode-panel margin-bottom-10">
<img src="${ctx}/api/order/pay/wechat/qrcode?orderId=${order.hexId}">
</div>
<div class="wechat-description-panel">
<p class="text-muted">使用微信扫描二维码完成支付</p>
<p class="text-danger">¥${order.price}</p>
</div>
</div>
</div>
根据统一下单API的要求生成订单
- 将系统内部订单号传入请求参数的out_trade_no中,用于后续操作的唯一标识符
- 请求参数中的sign是验证参数合法性的唯一标识,需要根据 微信支付签名算法 来生成
- 使用XStream将对象转换为XML,由于微信的请求参数中大量使用下划线,但下划线在XStream中是关键字,因此需要把下划线转换为双下划线,避免报错
private String generateOrderInfo(Long orderId) throws Exception {
OrderDTO order = orderManageService.getOrder(orderId);
UnifiedOrderRequestExt ext = new UnifiedOrderRequestExt();
ext.setAppid(SOPConstants.WECHAT_PAY_APP_ID);
ext.setMch_id(SOPConstants.WECHAT_PAY_MCH_ID);
ext.setBody("轻实训-" + order.getName());
ext.setOut_trade_no(order.getCode());
ext.setTotal_fee(order.getPrice() * 100);
ext.setSpbill_create_ip(super.getClientIP());
ext.setNotify_url(SOPConstants.WECHAT_PAY_NOTIFY_URL);
ext.setTrade_type("NATIVE");
ext.setProduct_id(order.getHexId());
ext.setNonce_str(makeNonceStr());
ext.setSign(makeSign(ext));
XStream xStream = new XStream(new XppDriver(new XmlFriendlyReplacer("_-", "_")));
xStream.alias("xml", UnifiedOrderRequestExt.class);
return xStream.toXML(ext);
}
private String makeNonceStr() {
StringBuffer str = new StringBuffer(DateUtil.getSysDateString("yyyyMMddHHmmssS"));
str.append((new Random().nextInt(900) + 100));
return str.toString();
}
private String makeSign(UnifiedOrderRequestExt ext) throws Exception {
SortedMap<String, String> signMaps = Maps.newTreeMap();
signMaps.put("appid", ext.getAppid());
signMaps.put("body", ext.getBody());
signMaps.put("mch_id", ext.getMch_id());
signMaps.put("nonce_str", ext.getNonce_str());
signMaps.put("notify_url", ext.getNotify_url());
signMaps.put("out_trade_no", ext.getOut_trade_no());
signMaps.put("spbill_create_ip", ext.getSpbill_create_ip());
signMaps.put("trade_type", ext.getTrade_type());
signMaps.put("total_fee", ext.getTotal_fee().toString());
signMaps.put("product_id", ext.getProduct_id());
return generateSign(signMaps);
}
private String generateSign(SortedMap<String, String> signMaps) throws Exception {
StringBuffer sb = new StringBuffer();
for (Map.Entry signMap : signMaps.entrySet()) {
String key = (String) signMap.getKey();
String value = (String) signMap.getValue();
if (null != value && !"".equals(value) && !"sign".equals(key) && !"key".equals(key)) {
sb.append(key + "=" + value + "&");
}
}
sb.append("key=" + SOPConstants.WECHAT_PAY_KEY);
return encoderByMd5(sb.toString()).toUpperCase();
}
调用统一下单API
- 将生成的订单发送给微信,同时接收微信的返回参数,读取其中的code_url
- 如果发送的订单信息不符合要求,则会在返回参数中告知问题
- 订单合法,返回参数中return_code=SUCCESS return_msg=OK result_code=SUCCESS
- 订单不合法,返回参数中return_code=FAIL return_msg=具体错误原因
private String sendHttpRequest(String orderInfo) throws IOException {
HttpURLConnection conn = (HttpURLConnection) new URL(SOPConstants.WECHAT_PAY_SEND_URL).openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
BufferedOutputStream bos = new BufferedOutputStream(conn.getOutputStream());
bos.write(orderInfo.getBytes());
bos.flush();
bos.close();
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
StringBuffer str = new StringBuffer();
while ((line = reader.readLine()) != null) {
str.append(line);
}
XStream xStream = new XStream(new XppDriver(new XmlFriendlyReplacer("_-", "_")));
xStream.alias("xml", UnifiedOrderResponseExt.class);
UnifiedOrderResponseExt ext = (UnifiedOrderResponseExt) xStream.fromXML(str.toString());
if (null != ext && "SUCCESS".equals(ext.getReturn_code()) && "SUCCESS".equals(ext.getResult_code())) {
return ext.getCode_url();
}
return null;
}
![]()
根据返回的code_url生成二维码图片
@RequestMapping(value = “/wechat/qrcode”, method = RequestMethod.GET)
public void wechatQRCode(HttpServletResponse response, @RequestParam("orderId") String orderId) {
try {
int width = 240;
int height = 240;
String format = "png";
String codeUrl = orderPayService.getQRCodeUrl(IdEncoder.decodeId(orderId));
Hashtable htable = new Hashtable();
htable.put(EncodeHintType.CHARACTER_SET, "UTF-8");
BitMatrix matrix = new MultiFormatWriter().encode(codeUrl, BarcodeFormat.QR_CODE, width, height, htable);
OutputStream out = response.getOutputStream();
MatrixToImageWriter.writeToStream(matrix, format, out);
out.flush();
out.close();
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.0</version>
</dependency>
接收回调
- 用户通过微信扫描二维码并支付成功后,微信会根据之前订单中的notify_url回调地址进行回执
- 此处提供给微信的回调地址必须是外网可访问的,否则无法正常接收回执信息
- 由于回执时并没有携带用户信息,所以如果使用了诸如shiro等安全框架的,需要给予该回执地址一个访问许可,否则会被安全框架屏蔽
- 发送回执是异步进行,由于网络等不确定因素,微信不保证回执一定成功
- 微信会通过一定的策略定期重启发送通知,通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒
- 虽然是异步回执,但并不需要采用ajax异步接收的方式来接收数据
@RequestMapping(value = "/wechat")
public String wechatNotify(HttpServletResponse response, HttpServletRequest request) throws Exception {
orderPayService.notify(response, request);
return "/redirect:/login";
}
处理回执内容
- 数据是通过IO流发送,所以也需要通过IO流接收
- 微信发送回执用户接收后,需要通过IO流的方式告知微信接收成功,否则微信认为回执失败
- 接收到回执信息后,最关键是验证签名来确保信息的有效性和安全性,验签的方式和发送订单签名的方式一致
- 验签成功,且回执信息中result_code=SUCCESS,则表示回执信息有效
- 从回执信息中可获取到out_trade_no,这是之前发送的用户订单唯一标识符,通过该信息可以继续处理用户订单
- 所有流程处理完毕后,必须以XML格式编写回执信息,并通过IO流的方式告知微信回执接收成功
public void notify(HttpServletResponse response, HttpServletRequest request) throws Exception {
HashMap<String, String> notifyMaps = readNotify(request);
if (notifyMaps == null || notifyMaps.isEmpty()) {
logger.error("未收到回执数据!");
throw new TSharkException("未收到回执数据!");
}
SortedMap<String, String> notifySorts = sortNotify(notifyMaps);
String sign = generateSign(notifySorts);
String notifySign = notifySorts.get("sign").toUpperCase();
if (!sign.equals(notifySign)) {
logger.error("签名验证失败!");
throw new TSharkException("签名验证失败!");
}
String resXml;
if ("SUCCESS".equals(notifySorts.get("result_code"))) {
updateOrderInfo(notifySorts.get("out_trade_no"));
resXml = "<xml>" +
"<return_code><![CDATA[SUCCESS]]></return_code>" +
"<return_msg><![CDATA[OK]]></return_msg>" +
"</xml> ";
} else {
resXml = "<xml>" +
"<return_code><![CDATA[FAIL]]></return_code>" +
"<return_msg><![CDATA[报文为空]]></return_msg>" +
"</xml> ";
}
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
}
private HashMap<String, String> readNotify(HttpServletRequest request) throws Exception {
InputStream inputStream = request.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String line;
StringBuffer str = new StringBuffer();
while ((line = reader.readLine()) != null) {
str.append(line);
}
reader.close();
inputStream.close();
return fromXml(str.toString());
}
- 读取的回执信息时XML格式,需要通过jDom的SAXBuilder解析为Map
private HashMap<String, String> fromXml(String xml) throws Exception {
xml = xml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
if (null == xml || "".equals(xml)) {
return null;
}
HashMap<String, String> m = Maps.newHashMap();
InputStream in = new ByteArrayInputStream(xml.getBytes("UTF-8"));
SAXBuilder builder = new SAXBuilder();
Document doc = builder.build(in);
Element root = doc.getRootElement();
List list = root.getChildren();
Iterator it = list.iterator();
while (it.hasNext()) {
Element e = (Element) it.next();
String k = e.getName();
String v = "";
List children = e.getChildren();
if (children.isEmpty()) {
v = e.getTextNormalize();
} else {
v = getXmlChildren(children);
}
m.put(k, v);
}
in.close();
return m;
}
private String getXmlChildren(List children) {
StringBuffer sb = new StringBuffer();
if (!children.isEmpty()) {
Iterator it = children.iterator();
while (it.hasNext()) {
Element e = (Element) it.next();
String name = e.getName();
String value = e.getTextNormalize();
List list = e.getChildren();
sb.append("<" + name + ">");
if (!list.isEmpty()) {
sb.append(getXmlChildren(list));
}
sb.append(value);
sb.append("</" + name + ">");
}
}
return sb.toString();
}
轮询订单状态,实现支付完成后页面自动跳转
- 由于支付回执是异步的,所以即使捕获到异步回执也无法实现支付页面的自动跳转
- 所以需要在支付页面打开时设置一个ajax轮询订单状态,一旦订单状态更新,则进行页面跳转
$(".modal-header button.close:last").click(function () {
clearInterval(checkTimer);
});
checkTimer = setInterval(function () {
if ($(".order-pay-panel").length <= 0) {
clearInterval(checkTimer);
}
$.ts.doAction("/api/order/review/check/", {
orderId: orderId
}, function () {
if (!this.data) {
$.ts.closeWindow();
g_index.loadMainContentWithState("/order/manage");
$.ts.toastr.success("订单已支付成功!");
}
}, "", "", "");
}, 3000);