java编写微信支付V2版本

· 首先记录几个开发过程中的坑:

  1. 提示签名错误,但是在微信官方提供的校验显示正确(官方校验只会校验输入字符串和生成的sign)。如果在没有其他可以运行的业务证明key没有问题的话,这可能是微信支付Key的初始化问题,这个key很可能在第一次初始化时出现错误,需要重置key的值,可以重置成一样的。
  2. body字段传入时,不能为空、中文需要指定编码
  3. 总金额必须要求整数,最终金额是/1000得到的
  4. trade_type=JSAPI时用户的openid一定要传,但是不是子商户的情况下sub_openid不是必传的
  5. 如果还会出现签名错误,请将所有的参数都和微信自己的实例demo调整到一致后尝试
  6. 微信小程序的支付完成回调会在用户点击支付成功页面的"完成"按钮后才会回调

制作小程序时需要用户支付定金功能,微信的支付功能接口依旧要求传输XML作为请求体,同时要求进行所有字段的签名操作,首先编写两个bean作为统一下单传输的XML和请求订单状态的XML。

import lombok.Data;

import java.io.Serializable;
/**
*统一下单的请求bean
**/
@Data
public class PaymentDto implements Serializable {
    private String appid;//小程序ID
    private String mch_id;//商户号
    private String device_info;//设备号
    private String nonce_str;//随机字符串
    private String sign;//签名
    private String body;//商品描述
    private String detail;//商品详情
    private String attach;//附加数据
    private String out_trade_no;//商户订单号
    private String fee_type;//货币类型
    private String spbill_create_ip;//终端IP
    private String time_start;//交易起始时间
    private String time_expire;//交易结束时间
    private String goods_tag;//商品标记
    private String total_fee;//总金额
    private String notify_url;//通知地址
    private String trade_type;//交易类型
    private String limit_pay;//指定支付方式
    private String openid;//用户标识
}
import lombok.Data;

import java.io.Serializable;
@Data
public class GetWxOrderDto implements Serializable {
    private String appid;
    private String mch_id;//商户号
    private String nonce_str;//随机字符串
    private String sign;//签名
    private String out_trade_no;//商户订单号
}

接下来进行一些常量的配置,如下代码所示。

    private final String mch_id = "";//商户号
    private final String spbill_create_ip = "";//终端IP
    private final String notify_url = "/wx-make-true";//回调通知地址,因为手动拉取订单状态,所以可以随便传
    private final String trade_type = "JSAPI";//交易类型
    private final String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";//统一下单API接口链接
    private final String key = "&key="; // 商户支付密钥,在生成sign时需要使用,格式为&key=XXXXX
    private final String appid = "";//小程序appid

具体的应用方法和生成对象的类,写的比较乱直接写在了一个方法里,没有封装一下,源代码来自网络哪位大佬的文字。

 /**
     * @param openId
     * @param total_fee 订单总金额,单位为分。
     * @param body      商品简单描述,该字段请按照规范传递。 例:腾讯充值中心-心悦会员充值
     * @param attach    附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用。 例:广州分店
     * @return
     * @throws UnsupportedEncodingException
     * @throws DocumentException
     */
    private JSONObject payment(String openId, String total_fee, String body, String out_trade_no, String attach) throws UnsupportedEncodingException, DocumentException, DocumentException, UnsupportedEncodingException {

        body = new String(body.getBytes("UTF-8"), "ISO-8859-1");
        String nonce_str = UUIDHexGenerator.generate();//随机字符串

        PaymentDto paymentPo = new PaymentDto();
        paymentPo.setAppid(appid);
        paymentPo.setMch_id(mch_id);
        paymentPo.setNonce_str(nonce_str);
        String newbody = new String(body.getBytes("ISO-8859-1"), "UTF-8");//以utf-8编码放入paymentPo,微信支付要求字符编码统一采用UTF-8字符编码
        paymentPo.setBody(newbody);
        paymentPo.setOut_trade_no(out_trade_no);
        paymentPo.setTotal_fee(total_fee);
        paymentPo.setSpbill_create_ip(spbill_create_ip);
        paymentPo.setNotify_url(notify_url);
        paymentPo.setTrade_type(trade_type);
        paymentPo.setOpenid(openId);
// 把请求参数打包成数组
        Map<String, Object> sParaTemp = new HashMap<>();
        sParaTemp.put("appid", paymentPo.getAppid());
        sParaTemp.put("mch_id", paymentPo.getMch_id());
        sParaTemp.put("nonce_str", paymentPo.getNonce_str());
        sParaTemp.put("body", paymentPo.getBody());
        sParaTemp.put("out_trade_no", paymentPo.getOut_trade_no());
        sParaTemp.put("total_fee", paymentPo.getTotal_fee());
        sParaTemp.put("spbill_create_ip", paymentPo.getSpbill_create_ip());
        sParaTemp.put("notify_url", paymentPo.getNotify_url());
        sParaTemp.put("trade_type", paymentPo.getTrade_type());
        sParaTemp.put("openid", paymentPo.getOpenid());
// 除去数组中的空值和签名参数
        Map sPara = PayUtils.paraFilter(sParaTemp);
        String prestr = PayUtils.createLinkString(sPara); // 把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
//MD5运算生成签名
        String mysign = PayUtils.sign(prestr, key, "utf-8").toUpperCase();
        paymentPo.setSign(mysign);
//打包要发送的xml
        String respXml = XmlUtil.messageToXML(paymentPo);
        System.out.println(respXml);
// 打印respXml发现,得到的xml中有“__”不对,应该替换成“_”
        respXml = respXml.replace("__", "_");
        String param = respXml;
//String result = SendRequestForUrl.sendRequest(url, param);//发起请求
        String result = PayUtils.httpRequest(url, "POST", param);
        System.out.println("请求微信预支付接口,发送:" + param);
        System.out.println("请求微信预支付接口,返回 result:" + result);
// 将解析结果存储在Map中
        Map map = new HashMap();
        InputStream in = new ByteArrayInputStream(result.getBytes());
// 读取输入流
        SAXReader reader = new SAXReader();
        Document document = reader.read(in);
// 得到xml根元素
        Element root = document.getRootElement();
// 得到根元素的所有子节点
        List<Element> elementList = root.elements();
        for (Element element : elementList) {
            map.put(element.getName(), element.getText());
        }
// 返回信息
        String return_code = map.get("return_code").toString();//返回状态码
        String return_msg = map.get("return_msg").toString();//返回信息
        String result_code = map.get("result_code").toString();//返回状态码

        System.out.println("请求微信预支付接口,返回 code:" + return_code);
        System.out.println("请求微信预支付接口,返回 msg:" + return_msg);
        JSONObject wxReturnJson = new JSONObject();
        if ("SUCCESS".equals(return_code) && "SUCCESS".equals(result_code)) {
// 业务结果
            String prepay_id = map.get("prepay_id").toString();//返回的预付单信息
            String nonceStr = UUIDHexGenerator.generate();
            wxReturnJson.put("nonceStr", nonceStr);
            wxReturnJson.put("package", "prepay_id=" + prepay_id);
            Long timeStamp = System.currentTimeMillis() / 1000;
            wxReturnJson.put("timeStamp", timeStamp + "");
            String stringSignTemp = "appId=" + appid + "&nonceStr=" + nonceStr + "&package=prepay_id=" + prepay_id + "&signType=MD5&timeStamp=" + timeStamp;

//再次签名
            String paySign = PayUtils.sign(stringSignTemp, key, "utf-8").toUpperCase();
            wxReturnJson.put("paySign", paySign);

        }
        return wxReturnJson;
    }

调用方法

JSONObject jsonObjectPayment = payment(wxUser.getOpenId(), "1", "qjh-member", out_trade_no, "支付定金");//支付订单,out_trade_no是生成的唯一订单

将这个Json返回给小程序端,小程序可以直接通过传递这些参数就可以掉起微信支付,当然在小程序中生成sign和随机串也是可以的。这样解决了前端的一些麻烦。在上述代码中使用到了封装的XML Util类和Pay Util类,在XML类中用来转化对象和XML文档,PayUtil中用来完成排序和签名,如下代码所示。

import org.apache.commons.codec.digest.DigestUtils;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;

public class PayUtils {
    /**
     * 签名字符串
     *
     * @param text          需要签名的字符串
     * @param key           密钥
     * @param input_charset 编码格式
     * @return 签名结果
     */
    public static String sign(String text, String key, String input_charset) {
        text = text + key;
        return DigestUtils.md5Hex(getContentBytes(text, input_charset));
    }

    /**
     * 签名字符串
     *
     * @param text          需要签名的字符串
     * @param sign          签名结果
     * @param key           密钥
     * @param input_charset 编码格式
     * @return 签名结果
     */
    public static boolean verify(String text, String sign, String key, String input_charset) {
        text = text + key;
        String mysign = DigestUtils.md5Hex(getContentBytes(text, input_charset));
        return mysign.equals(sign);
    }

    /**
     * @param content
     * @param charset
     * @return
     * @throws UnsupportedEncodingException
     */
    public static byte[] getContentBytes(String content, String charset) {
        if (charset == null || "".equals(charset)) {
            return content.getBytes();
        }
        try {
            return content.getBytes(charset);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("MD5签名过程中出现错误,指定的编码集不对,您目前指定的编码集是:" + charset);
        }
    }

    /**
     * 生成6位或10位随机数 param codeLength(多少位)
     *
     * @return
     */
    public static String createCode(int codeLength) {
        String code = "";
        for (int i = 0; i < codeLength; i++) {
            code += (int) (Math.random() * 9);
        }
        return code;
    }

    private static boolean isValidChar(char ch) {
        if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'))
            return true;
        return (ch >= 0x4e00 && ch <= 0x7fff) || (ch >= 0x8000 && ch <= 0x952f);
    }

    /**
     * 除去数组中的空值和签名参数
     *
     * @param sArray 签名参数组
     * @return 去掉空值与签名参数后的新签名参数组
     */
    public static Map paraFilter(Map<String, Object> sArray) {
        Map result = new HashMap();
        if (sArray == null || sArray.size() <= 0) {
            return result;
        }
        for (String key : sArray.keySet()) {
            String value = (String) sArray.get(key);
            if (value == null || value.equals("") || key.equalsIgnoreCase("sign")
                    || key.equalsIgnoreCase("sign_type")) {
                continue;
            }
            result.put(key, value);
        }
        return result;
    }

    /**
     * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
     *
     * @param params 需要排序并参与字符拼接的参数组
     * @return 拼接后字符串
     */
    public static String createLinkString(Map params) {
        List keys = new ArrayList(params.keySet());
        Collections.sort(keys);
        String prestr = "";
        for (int i = 0; i < keys.size(); i++) {
            String key = (String) keys.get(i);
            String value = (String) params.get(key);
            if (i == keys.size() - 1) {// 拼接时,不包括最后一个&字符
                prestr = prestr + key + "=" + value;
            } else {
                prestr = prestr + key + "=" + value + "&";
            }
        }
        return prestr;
    }

    /**
     * @param requestUrl    请求地址
     * @param requestMethod 请求方法
     * @param outputStr     参数
     */
    public static String httpRequest(String requestUrl, String requestMethod, String outputStr) {
// 创建SSLContext
        StringBuffer buffer = null;
        try {
            URL url = new URL(requestUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod(requestMethod);
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.connect();
//往服务器端写内容
            if (null != outputStr) {
                OutputStream os = conn.getOutputStream();
                os.write(outputStr.getBytes("utf-8"));
                os.close();
            }
// 读取服务器端返回的内容
            InputStream is = conn.getInputStream();
            InputStreamReader isr = new InputStreamReader(is, "utf-8");
            BufferedReader br = new BufferedReader(isr);
            buffer = new StringBuffer();
            String line = null;
            while ((line = br.readLine()) != null) {
                buffer.append(line);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return buffer.toString();
    }

    public static String urlEncodeUTF8(String source) {
        String result = source;
        try {
            result = java.net.URLEncoder.encode(source, "UTF-8");
        } catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
            e.printStackTrace();
        }
        return result;
    }
}
import com.x.x.model.GetWxOrderDto;
import com.x.x.model.PaymentDto;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.core.util.QuickWriter;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
import com.thoughtworks.xstream.io.xml.XppDriver;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class XmlUtil {
    public static Map<String, Object> parseXML(HttpServletRequest request) throws IOException, DocumentException {
        Map<String, Object> map = new HashMap<String, Object>();
        /* 通过IO获得Document */
        SAXReader reader = new SAXReader();
        Document doc = reader.read(request.getInputStream());
//得到xml的根节点
        Element root = doc.getRootElement();
        recursiveParseXML(root, map);
        return map;
    }

    private static void recursiveParseXML(Element root, Map<String, Object> map) {
//得到根节点的子节点列表
        List<Element> elementList = root.elements();
//判断有没有子元素列表
        if (elementList.size() == 0) {
            map.put(root.getName(), root.getTextTrim());
        } else {
//遍历
            for (Element e : elementList) {
                recursiveParseXML(e, map);
            }
        }
    }

    private static XStream xstream = new XStream(new XppDriver() {
        public HierarchicalStreamWriter createWriter(Writer out) {
            return new PrettyPrintWriter(out) {
                // 对所有xml节点都增加CDATA标记
                boolean cdata = true;

                public void startNode(String name, Class clazz) {
                    super.startNode(name, clazz);
                }

                protected void writeText(QuickWriter writer, String text) {
                    if (cdata) {
                        writer.write(text);
                    } else {
                        writer.write(text);
                    }
                }
            };
        }
    });

    public static String messageToXML(PaymentDto paymentPo) {
        xstream.alias("xml", PaymentDto.class);
//        PaymentDto root=new PaymentDto();
//        root.appid
        return xstream.toXML(paymentPo);
    }

    public static String getPayStatusToXML(GetWxOrderDto getWxOrderDto) {
        xstream.alias("xml", GetWxOrderDto.class);
        return xstream.toXML(getWxOrderDto);
    }
}

除了上述两个工具类,还使用了一个随机数的生成类,当然这个并没有必要使用这个类,毕竟不会存在这么多的订单和巧合,这只是一种随机数的生成方式,自己找到掉因子做做梅森旋转啥的也行。

import java.net.InetAddress;

public class UUIDHexGenerator {
    private static String sep = "";
    private static final int IP;
    private static short counter = (short) 0;
    private static final int JVM = (int) (System.currentTimeMillis() >>> 8);
    private static UUIDHexGenerator uuidgen = new UUIDHexGenerator();

    static {
        int ipadd;
        try {
            ipadd = toInt(InetAddress.getLocalHost().getAddress());
        } catch (Exception e) {
            ipadd = 0;
        }
        IP = ipadd;
    }

    public static UUIDHexGenerator getInstance() {
        return uuidgen;
    }

    public static int toInt(byte[] bytes) {
        int result = 0;
        for (int i = 0; i < 4; i++) {
            result = (result << 8) - Byte.MIN_VALUE + bytes[i];
// result = (result << - Byte.MIN_VALUE + (int) bytes);
        }
        return result;
    }

    protected static String format(int intval) {
        String formatted = Integer.toHexString(intval);
        StringBuffer buf = new StringBuffer("00000000");
        buf.replace(8 - formatted.length(), 8, formatted);
        return buf.toString();
    }

    protected static String format(short shortval) {
        String formatted = Integer.toHexString(shortval);
        StringBuffer buf = new StringBuffer("0000");
        buf.replace(4 - formatted.length(), 4, formatted);
        return buf.toString();
    }

    protected static int getJVM() {
        return JVM;
    }

    protected synchronized static short getCount() {
        if (counter < 0) {
            counter = 0;
        }
        return counter++;
    }

    protected static int getIP() {
        return IP;
    }

    protected static short getHiTime() {
        return (short) (System.currentTimeMillis() >>> 32);
    }

    protected static int getLoTime() {
        return (int) System.currentTimeMillis();
    }

    public static String generate() {
        return new StringBuffer(36).append(format(getIP())).append(sep).append(format(getJVM())).append(sep)
                .append(format(getHiTime())).append(sep).append(format(getLoTime())).append(sep)
                .append(format(getCount())).toString();
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        String id = "";
        UUIDHexGenerator uuid = UUIDHexGenerator.getInstance();
/*
for (int i = 0; i < 100; i++) {
id = uuid.generate();
}*/
        id = generate();
        System.out.println(id);
    }
}

在用户点击完成后,可以请求一个新的接口来实现主动在微信服务器中拉取订单状态,这样就不用等微信的回调,内网的机器也不用配置穿透就可以实现查看微信订单状态,如下代码所示。

    /**
     * 微信支付不一定会推送,直接自取
     * 触发器是用户主动的请求,会保留用户的订单状态
     */

    @PostMapping("/get-wx-pay-result-self")
    public ResultInfo getWxPayResultSelf(@RequestBody JSONObject jsonObject, HttpServletRequest request) throws DocumentException {
        WxUser wxUser = (WxUser) request.getAttribute("wxUserInfo");

        String nonceStr = UUIDHexGenerator.generate();
        String out_trade_no = jsonObject.getString("out_trade_no");
        GetWxOrderDto getWxOrderDto = new GetWxOrderDto();
        getWxOrderDto.setAppid(appid);
        getWxOrderDto.setMch_id(mch_id);
        getWxOrderDto.setNonce_str(nonceStr);
        getWxOrderDto.setOut_trade_no(out_trade_no);
        Long timeStamp = System.currentTimeMillis() / 1000;
//        签名
        // 把请求参数打包成数组
        Map<String, Object> sParaTemp = new HashMap<>();
        sParaTemp.put("appid", getWxOrderDto.getAppid());
        sParaTemp.put("mch_id", getWxOrderDto.getMch_id());
        sParaTemp.put("nonce_str", getWxOrderDto.getNonce_str());
        sParaTemp.put("out_trade_no", getWxOrderDto.getOut_trade_no());
// 除去数组中的空值和签名参数
        Map sPara = PayUtils.paraFilter(sParaTemp);
        String prestr = PayUtils.createLinkString(sPara); // 把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
        System.out.println(prestr);
//MD5运算生成签名
        String mysign = PayUtils.sign(prestr, key, "utf-8").toUpperCase();
        getWxOrderDto.setSign(mysign);

//打包要发送的xml
        String respXml = XmlUtil.getPayStatusToXML(getWxOrderDto);
        System.out.println(respXml);
        respXml = respXml.replace("__", "_");
        String param = respXml;
        String result = PayUtils.httpRequest("https://api.mch.weixin.qq.com/pay/orderquery", "POST", param);
        System.out.println("请求微信支付结果接口,发送:" + param);
        System.out.println("请求微信支付结果接口,返回 result:" + result);
        // 将解析结果存储在Map中
        Map map = new HashMap();
        InputStream in = new ByteArrayInputStream(result.getBytes());
// 读取输入流
        SAXReader reader = new SAXReader();
        Document document = reader.read(in);
// 得到xml根元素
        Element root = document.getRootElement();
// 得到根元素的所有子节点
        List<Element> elementList = root.elements();
        for (Element element : elementList) {
            map.put(element.getName(), element.getText());
        }
// 返回信息
        String return_code = map.get("return_code").toString();//返回状态码
        String result_code = map.get("result_code").toString();//业务结果
        String trade_state = map.get("trade_state").toString();//交易状态
        if (trade_state.equals("SUCCESS")) {
//            支付成功     
//            更改用户状态
            return new ResultInfo(Status.SUCCESS);
        } else {
            return new ResultInfo(Status.REQUEST_PARAMETER_ERROR);
        }
    }

链接