JAVA PC端扫码支付(一)微信支付

微信支付从配置到开发

一、配置

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: [email protected]
	 * @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后端的工作就基本完成了,那前端怎么知道用户完成了支付呢?现在普遍的办法是,前端写方法轮询支付状态,限定时间内查询到支付状态为“已支付”就进行下一步操作,限定时间后未支付就做支付超时的操作。本项目用户查询支付状态的代码已经写在了之前“与支付无关的业务逻辑”中了~

猜你喜欢

转载自my.oschina.net/u/3021675/blog/1629945