微信支付从配置到开发
一、配置
1、开通公众平台支付功能
商户号
微信支付功能先要申请微信(企业)公众平台,然后开通企业公众平台付功能。下图为微信(企业)公众平台页面,可以看到商户号等信息
微信公众号APPID
从开发-基本配置中获取APPID
2、微信商户平台相关配置
微信商户平台相关配置
因为微信公众平台调整,公众平台微信支付公众号支付授权目录、扫码支付回调URL配置入口于2017年8月1日迁移至商户平台(pay.weixin.qq.com),所以微信支付配置和相关信息要登录商户平台才能拿到。(估计是微信想要把公众号的管理功能和开发功能分离)
回调链接
从微信商户平台的产品中心-开发配置-支付配置配置扫码回调链接(扫码回调链接就是你项目中微信支付回调函数名称,这里需要的是加了项目域名的函数全称,必须保证能从公网访问。为什么需要一个回调函数呢?这属于微信支付的回调机制:当用户使用微信支付完成后,你从本地是无法得知是否支付成功的,而微信这边在获取到支付完成的状态后,主动去访问你所设置的回调函数地址,将支付状态等相关信息返回,我们只要在回调函数中判断支付状态,就能够便捷的进行下一步操作!)
设置API密钥
下载微信sdk
微信sdk是微信官方给出的微信支付demo,其中有很多好用的工具类,demo本身也可以为我们的支付接口开发提供参考(https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=11_1)
把sdk导入到项目中
二、开发
主要业务流程
-
主要业务流程
(1)商户后台系统根据用户选购的商品生成订单。
(2)用户确认支付后调用微信支付【统一下单API】生成预支付交易;
(3)微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码链接code_url(code_url就是微信支付地址)。
(4)商户后台系统根据返回的code_url生成二维码(用第三方插件生成二维码)。
(5)用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。
(6)微信支付系统收到客户端请求,验证链接有效性后发起用户支付,要求用户授权。
(7)用户在微信客户端输入密码,确认支付后,微信客户端提交授权。
(8)微信支付系统根据用户授权完成支付交易。
(9)微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。
(10)微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。
这是官方给出的文档,这里再梳理一下。单纯做PC端扫一扫开发很简单,主要是向微信支付的【统一下单API】请求,发送订单信息和签名(签名比较麻烦,可能前期测试会报多次签名错误,不过官方SDK中有生成签名的方法,当然,自己也可以写),请求成功微信支付返回二维码链接code_url,注意这是微信支付的链接,不是二维码!不是二维码!不是二维码!二维码需要自己生成,不要直接就把code_url挂在页面上~
请求【统一下单API】的参数列表
好了,上代码~
与支付无关的业务逻辑
-
与支付无关的业务逻辑
这里我单独创建一个类PayController来写自己的业务逻辑,生成业务订单啊,业务订单保存在数据库啊,查询订单信息啊,验证是否支付完成啊等等,我的业务逻辑比较简单,仅供参考~
package com.xxx.controller;
import com.xxx.pojo.ProductOrders;
import com.xxx.service.ProOrdersService;
import com.xxx.util.testPay;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* 与支付无关的业务逻辑
* */
@Controller
public class PayController {
@Resource
private ProOrdersService proOrdersService;//订单增删查改的接口
/**
* 调用接口 生成业务订单信息保存在数据库,并返回订单号
*
* @param filetxt
* @return ordernum
*/
@RequestMapping(value = "getOrder.do")
@ResponseBody
public Map getorder(HttpServletRequest request, @Param("filetxt") String filetxt) {
Map<String, Object> map = new HashMap<>();
//获取当前用户
String username = (String) request.getSession().getAttribute("username");
if (username.isEmpty())
{
map.put("type","2");
map.put("data","用户登陆超时,请重新登陆");
return map;
}
//订单对象,用户存储用户的订单信息,这里有些参数是请求【统一下单API】需要的,等我需要的时候就根据订单号从数据库取出相关参数
ProductOrders productOrders = new ProductOrders();
productOrders.setUserId(username);//用户
productOrders.setOrdernumber(getOutTradeNo());//订单号是随机生成的16位唯一字符串,用于匹配订单
productOrders.setProductId("XB001");//商品
int wordnum = filetxt.trim().length();//字数
productOrders.setQuantity(wordnum);//数量
Integer pay1 = testPay.getPay1(wordnum);//计算价格
productOrders.setTotalPrice(pay1);//总价
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
String format = df.format(new Date());//日期格式转换
productOrders.setOrdertime(format);
productOrders.setOrderDetails(filetxt);//文章内容
productOrders.setStatus(0);
//设置订单详情格式
try {
int insert = proOrdersService.insert(productOrders);
} catch (Exception e) {
System.out.println("Exception:添加订单异常");
e.printStackTrace();
}
//封装返回值
map.put("orderid", productOrders.getOrdernumber());//订单号
return map;
}
/**
* 查询订单信息
*
* @param orderid
* @return filetxt
*/
@RequestMapping(value = "selectOrder.do")
@ResponseBody
public Map selectOrder(@Param("orderid") String orderid) {
ProductOrders productOrders = this.proOrdersService.selectByOrderId(orderid);
Map<String, Object> map = new HashMap<>();
map.put("wordnum", productOrders.getQuantity());
map.put("totelprice", productOrders.getTotalPrice());
map.put("filetxt", productOrders.getOrderDetails());
return map;
}
/**
* 验证支付状态,这个是查询是否支付完成的方法,微信在支付完成后访问了我的回调方法,修改数据库的订单状态
* 我通过此方法查询数据库中相关订单是否完成支付
* @Param orderid
*/
@RequestMapping(value = "OrderStatus.do")
@ResponseBody
public Map SelectOrderStatus(HttpServletRequest request, @Param("orderid") String orderid) {
Map<String, Object> map = new HashMap<>();
int i = this.proOrdersService.selectOrderStatus(orderid);
if (i == 1)//支付成功
{
map.put("type", "SUCCESS");
return map;
}
map.put("type", "FAIL");
return map;
}
/**
* 生成16位随机订单号
* @return key
*/
private static String getOutTradeNo() {
SimpleDateFormat format = new SimpleDateFormat("MMddHHmmss", Locale.getDefault());
Date date = new Date();
String key = format.format(date);
Random r = new Random();
key = key + r.nextInt();
key = key.replaceAll("-", "").substring(0, 15);
return key;
}
}
微信支付逻辑
-
微信支付逻辑
1、生成签名,然后打包成【统一下单API】要求格式的订单(参数列表),微信支付要求为XMl格式
2、调用【统一下单API】微信接口,将我们打包好XMl格式的参数列表发送给【统一下单接口】,调用成功会接收到XMl格式的返回值,解析成我们需要的格式,判断是否请求 成功,成功的话是会返回code_url的
3、然后我们把code_url生成二维码展现给用户就OK了!
4、用户支付完成后,微信会访问我们的回调接口,根据返回的结果修改数据库支付状态
请求【统一下单API】返回参数列表
将微信支付所需要的固定参数封装到类WXpayConfig中
封装固定参数
package com.xxx.conf;
public class WXpayConfig {
public static String APPID = "wx830cXXXXXXX";//微信公众号APPID
public static String WXPAYMENTACCOUNT = "xxxxxxxxxx";//微信公众号的商户号
public static String APIKEY = "xxxxxxxxxxx";//微信公众号的商户支付密钥
public static String basePath = "https://api.mch.weixin.qq.com/pay/unifiedorder";//统一下单请求地址
public static String notify_url = "http://www.xxxxx.com.cn/wxPayCallBack.do";//回调地址
}
WXPayController,主控制器
import com.xxx.pojo.ProductOrders;
import com.xxx.pojo.Products;
import com.xxx.service.ProOrdersService;
import com.xxx.service.ProductsService;
import com.xxx.util.GetIPAdder;
import com.xxx.util.QRCodeUtil;
import com.xxx.conf.WXpayConfig;
import com.github.wxpay.sdk.WXPayConstants;
import com.github.wxpay.sdk.WXPayConstants.SignType;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import static com.github.wxpay.sdk.WXPayUtil.*;
@Controller
public class WXPayController {
@Resource
private ProOrdersService proOrdersService;//订单操作接口
@Resource
private ProductsService productsService;//产品操作接口
/**
* 支付主接口,用于控制整体支付流程
* */
@RequestMapping(value = "pay")
@ResponseBody
public Map createQRCode(HttpServletRequest request, HttpServletResponse response,
@Param("orderid") String orderid) {
Map<String,String> map=new HashMap<>();
if (orderid.isEmpty())
{
map.put("type","2");
map.put("data","订单号为空");
return map;
}
ServletOutputStream sos = null;
try {
String orderInfo = createOrderInfo(orderid);//生成【统一下单API】所需参数的接口
String code_url = httpOrder(orderInfo);//调用统一下单接口
sos = response.getOutputStream();
QRCodeUtil.encode(code_url, sos);//调用生成二维码的方法
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
/**
* 生成统一下单格式的订单,生成一个XML格式的字符串
* @param orderId
* @return
*/
private String createOrderInfo(String orderId) throws Exception {
return createOrderInfo(orderId, 1);
}
private String createOrderInfo(String orderId, Integer productid) throws Exception {
Products products = productsService.selectByPrimaryKey(Long.valueOf(productid));//商品对象
ProductOrders productOrders = this.proOrdersService.selectByOrderId(orderId);//订单信息
//生成订单对象
Map<String, String> map = new HashMap<>();
map.put("appid", WXpayConfig.APPID);//公众账号ID
map.put("mch_id", WXpayConfig.WXPAYMENTACCOUNT);//商户号
map.put("body", productOrders.getOrderDetails());//商品描述
map.put("nonce_str", generateUUID());
map.put("notify_url", WXpayConfig.notify_url);//通知地址
map.put("out_trade_no", orderId);//订单号
map.put("spbill_create_ip", GetIPAdder.getMyIP());//终端ip
map.put("trade_type", "NATIVE");//交易类型
map.put("total_fee", String.valueOf(productOrders.getTotalPrice()));//总金额
String sign = createSign(map, WXpayConfig.APIKEY);//调用生成签名的方法,用以Map集合中的相关参数生成签名
map.put("sign", sign);//签名
//将订单对象转为xml格式
String s = null;
try {
return mapToXml(map);//maptoXml方法是微信sdk自带的方法
} catch (Exception e) {
e.printStackTrace();
}
return new String(s.getBytes("UTF-8"));
}
/**
* 调统一下单API
* @param orderInfo
* @return
*/
private String httpOrder(String orderInfo) {
String url = WXpayConfig.basePath;
try {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
//加入数据
conn.setRequestMethod("POST");
conn.setDoOutput(true);
BufferedOutputStream buffOutStr = new BufferedOutputStream(conn.getOutputStream());
buffOutStr.write(orderInfo.getBytes("UTF-8"));
buffOutStr.flush();
buffOutStr.close();
//获取输入流
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
String line = null;
StringBuffer sb = new StringBuffer();
while ((line = reader.readLine()) != null) {
sb.append(line);
}
Map<String, String> map = xmlToMap(sb.toString());
//返回字段很多,这里只取我们所需的字段
String return_msg = map.get("return_msg");//返回信息
String return_code = map.get("return_code");//状态码
String result_code = map.get("result_code");//业务结果
String code_url = map.get("code_url");
//根据微信文档return_code 和result_code都为SUCCESS的时候才会返回code_url
if (null != map && "SUCCESS".equals(return_code) && "SUCCESS".equals(result_code)) {
return code_url;
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 微信回调函数
* 支付成功后微信服务器会调用此方法,修改数据库订单状态
*/
@RequestMapping(value = "/wxPayCallBack.do")
@ResponseBody
public String wxPayCallBack(HttpServletRequest request, HttpServletResponse response) {
System.out.println("回调成功");
try {
InputStream inStream = request.getInputStream();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, len);
}
outSteam.close();
inStream.close();
String result = new String(outSteam.toByteArray(), "utf-8");// 获取微信调用我们notify_url的返回信息
Map<String, String> map = xmlToMap(result);
if (map.get("result_code").equalsIgnoreCase("SUCCESS")) {
//返回成功后修改订单状态
String out_trade_no = map.get("out_trade_no");
this.proOrdersService.updateByOrderId(out_trade_no);
}
} catch (Exception e) {
}
return "SUCCESS";
}
/**
* 生成签名
* 这个方法是从微信sdk里copy过来的,自己也可以写,要注意生成签名后UTF-8的转换,要不然容易报签名Body UTF-8错误
* @param data 待签名数据
* @param key API密钥
*/
public static String createSign(final Map<String, String> data, String key) throws Exception {
return createSign(data, key, SignType.MD5);
}
/**
* 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
*
* @param data 待签名数据
* @param key API密钥
* @param signType 签名方式
* @return 签名
*/
private static String createSign(final Map<String, String> data, String key, SignType signType) throws Exception {
//根据规则创建可排序的map集合
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (k.equals(WXPayConstants.FIELD_SIGN)) {
continue;
}
if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("key=").append(key);
//转换UTF-8
String str = new String(sb.toString().getBytes("UTF-8"));
if (WXPayConstants.SignType.MD5.equals(signType)) {
return MD5(sb.toString()).toUpperCase();
} else if (WXPayConstants.SignType.HMACSHA256.equals(signType)) {
return HMACSHA256(sb.toString(), key);
} else {
throw new Exception(String.format("Invalid sign_type: %s", signType));
}
}
}
如果请求【统一下单接口】的参数正确,签名也没有报错,那我们就能成功获取到code_url,从而生成二维码,让用户扫码支付了。
生成二维码工具类QRCodeUtil
使用了第三方工具类zxing,这里用到的zxing依赖包请自行下载
package com.xxx.util;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.OutputStream;
import java.util.Hashtable;
import java.util.Random;
/**
* 二维码工具类
*
*/
public class QRCodeUtil {
private static final String CHARSET = "utf-8";
private static final String FORMAT_NAME = "JPG";
// 二维码尺寸
private static final int QRCODE_SIZE = 300;
// LOGO宽度
private static final int WIDTH = 60;
// LOGO高度
private static final int HEIGHT = 60;
private static BufferedImage createImage(String content, String imgPath, boolean needCompress) throws Exception {
Hashtable<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>();
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
hints.put(EncodeHintType.CHARACTER_SET, CHARSET);
hints.put(EncodeHintType.MARGIN, 1);
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, QRCODE_SIZE, QRCODE_SIZE,hints);
int width = bitMatrix.getWidth();
int height = bitMatrix.getHeight();
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
}
}
if (imgPath == null || "".equals(imgPath)) {
return image;
}
// 插入图片
QRCodeUtil.insertImage(image, imgPath, needCompress);
return image;
}
/**
* 插入LOGO
*
* @param source
* 二维码图片
* @param imgPath
* LOGO图片地址
* @param needCompress
* 是否压缩
* @throws Exception
*/
private static void insertImage(BufferedImage source, String imgPath, boolean needCompress) throws Exception {
File file = new File(imgPath);
if (!file.exists()) {
System.err.println("" + imgPath + " 该文件不存在!");
return;
}
Image src = ImageIO.read(new File(imgPath));
int width = src.getWidth(null);
int height = src.getHeight(null);
if (needCompress) { // 压缩LOGO
if (width > WIDTH) {
width = WIDTH;
}
if (height > HEIGHT) {
height = HEIGHT;
}
Image image = src.getScaledInstance(width, height, Image.SCALE_SMOOTH);
BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = tag.getGraphics();
g.drawImage(image, 0, 0, null); // 绘制缩小后的图
g.dispose();
src = image;
}
// 插入LOGO
Graphics2D graph = source.createGraphics();
int x = (QRCODE_SIZE - width) / 2;
int y = (QRCODE_SIZE - height) / 2;
graph.drawImage(src, x, y, width, height, null);
Shape shape = new RoundRectangle2D.Float(x, y, width, width, 6, 6);
graph.setStroke(new BasicStroke(3f));
graph.draw(shape);
graph.dispose();
}
/**
* 生成二维码(内嵌LOGO)
*
* @param content
* 内容
* @param imgPath
* LOGO地址
* @param destPath
* 存放目录
* @param needCompress
* 是否压缩LOGO
* @throws Exception
*/
public static void encode(String content, String imgPath, String destPath, boolean needCompress) throws Exception {
BufferedImage image = QRCodeUtil.createImage(content, imgPath, needCompress);
mkdirs(destPath);
String file = new Random().nextInt(99999999) + ".jpg";
ImageIO.write(image, FORMAT_NAME, new File(destPath + "/" + file));
}
/**
* 当文件夹不存在时,mkdirs会自动创建多层目录,区别于mkdir.(mkdir如果父目录不存在则会抛出异常)
*
* @author lanyuan Email: mmm333zzz520@163.com
* @date 2013-12-11 上午10:16:36
* @param destPath
* 存放目录
*/
public static void mkdirs(String destPath) {
File file = new File(destPath);
// 当文件夹不存在时,mkdirs会自动创建多层目录,区别于mkdir.(mkdir如果父目录不存在则会抛出异常)
if (!file.exists() && !file.isDirectory()) {
file.mkdirs();
}
}
/**
* 生成二维码(内嵌LOGO)
*
* @param content
* 内容
* @param imgPath
* LOGO地址
* @param destPath
* 存储地址
* @throws Exception
*/
public static void encode(String content, String imgPath, String destPath) throws Exception {
QRCodeUtil.encode(content, imgPath, destPath, false);
}
/**
* 生成二维码
*
* @param content
* 内容
* @param destPath
* 存储地址
* @param needCompress
* 是否压缩LOGO
* @throws Exception
*/
public static void encode(String content, String destPath, boolean needCompress) throws Exception {
QRCodeUtil.encode(content, null, destPath, needCompress);
}
/**
* 生成二维码
*
* @param content
* 内容
* @param destPath
* 存储地址
* @throws Exception
*/
public static void encode(String content, String destPath) throws Exception {
QRCodeUtil.encode(content, null, destPath, false);
}
/**
* 生成二维码(内嵌LOGO)
*
* @param content
* 内容
* @param imgPath
* LOGO地址
* @param output
* 输出流
* @param needCompress
* 是否压缩LOGO
* @throws Exception
*/
public static void encode(String content, String imgPath, OutputStream output, boolean needCompress)
throws Exception {
BufferedImage image = QRCodeUtil.createImage(content, imgPath, needCompress);
ImageIO.write(image, FORMAT_NAME, output);
}
/**
* 生成二维码
*
* @param content
* 内容
* @param output
* 输出流
* @throws Exception
*/
public static void encode(String content, OutputStream output) throws Exception {
QRCodeUtil.encode(content, null, output, false);
}
public static void main(String[] args) throws Exception {
String text = "test";
QRCodeUtil.encode(text, "/Users/noahshen/Downloads/6BFAADD4-256D-447B-B742-1E1DFF11094F_meitu_1.png",
"/Users/noahshen/Downloads", true);
// QRCodeUtil.encode(text, null, "/Users/noahshen/Downloads", true);
}
}
前端轮询
当用户支付完成后,微信成功调用了我们的回调方法,数据库订单状态修改为“已支付”,Java后端的工作就基本完成了,那前端怎么知道用户完成了支付呢?现在普遍的办法是,前端写方法轮询支付状态,限定时间内查询到支付状态为“已支付”就进行下一步操作,限定时间后未支付就做支付超时的操作。本项目用户查询支付状态的代码已经写在了之前“与支付无关的业务逻辑”中了~