微信支付--扫码支付模式二实现

最近再做微信支付--扫码支付的后台。总结一下吧,免得大家入坑,贴出的代码是可以直接运行的,并且没有问题,加密验证那一块都是一次性通过的,很幸运。

参考文档 http://blog.csdn.net/wangqiuyun/article/details/51241064。

在此感谢一下这个文档,代码没有问题,本人修改的也就是循环那一块。

首先说下,微信支付的一些常见知识点吧。

https://pay.weixin.qq.com/index.php/core/home/login?return_url=%2F 。 这个链接是商户平台,注意需要区分下,公众平台和开放平台 以及商户平台。


公众平台:公众平台支持的是公众号支付 模式,扫码支付 也在其中。

开放平台:开放平台支持的是移动端支付,也就是App支付,开发平台账号是需要给 APP人员使用的,当然,服务端人员需要用开放平台账号去生成订单的。

商户平台:当你申请了公众平台或者开放平台之后,微信会有一封邮件给你,里面有对应的商户平台信息。

本人公司申请了三个账户,一个公众平台,一个手机App开放平台账号,一个pad开放平台账号,这就对应了三个商户平台,(注意:每个账号都需要使用各自的商户平台账号去生成订单,也必须使用对应商户平台才能查询到各自的订单)


下面就说下扫码支付吧,流程不废话了,微信文档内就有UML图,这个自己去读,不难,这里使用模式二去开发,模式一太烦,不方便,一般公司都是使用模式二去开发,获取订单URL简单方便。

以下为一些常用配置项,根据自己去配置,appSecret暂时在支付时没有用到,应该是在分享时会用到,这一ApiKey这个配置项,这个配置项很重要,签名时需要额外添加的,详情见微信开发文档的签名算法,需要细读,不然会有很多问题。笔者很幸运没有碰到问题,也多谢了上面的参考文章。

WeiXinConfigUtils

   /*微信分配的公众号ID*/
    public static String appId = "11122223355";
    /*ID Secret*/
    public static String appSecret = "454878asdasda4545848sdr";
    /*微信支付分配的商户号ID*/
    public static String mchId = "4845754";
    /*api加密时使用的key*/
    public static String apiKey = "HNTYDJHFK4156787DSS";
    /*扫码支付交易类型*/
    public static String tradeNative = "NATIVE";
    /*回调地址*/
    public static String notifyUrl = "/weixinpay/wei-xin-notify.htm";
    /*统一下单Url*/
    public static String orderUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder";
    /*查询订单Url*/
    public static String queryUrl = "https://api.mch.weixin.qq.com/pay/orderquery";
    /*机器ip*/
    public static String ip;
    /*随机数位数*/
    public static int randomNumLength = 5;
以下即为获取订单url的方法,各项参数需要自己去组织

public String getWeiXinPayUrl(String outTradeNo) throws Exception {
//公众账号ID
        String appid = WeiXinConfigUtils.appId;
        //商户号
        String mch_id = WeiXinConfigUtils.mchId;
        //加密key
        String apiKey = WeiXinConfigUtils.apiKey;
        //随机数,32位以内即可
        String nonce_str = PayCommonUtil.getNonceStr();
        //商品简单描述,可定义为商品名称
        String body = "测试商品名";
        //商户订单号,每次生成交易订单时需保证不一致
        String out_trade_no = outTradeNo;
        //商品总金额,单位为分
        int total_fee = 1;
        //终端IP
        String spbill_create_ip = WeiXinConfigUtils.ip;
        //回调地址
        String notify_url = WeiXinConfigUtils.notifyUrl;
        //交易类型
        String trade_type = WeiXinConfigUtils.tradeNative;
        //商品Id,交易类型为Native时必传
        String product_id = "2222";

        final Date date = new Date();
        //订单生成时间
        String time_start = HbdDateUtils.dateFormatCurrent2.format(date);
        //订单失效时间 间隔最短要求5分钟
        String time_expire = HbdDateUtils.dateFormatCurrent2.format(HbdDateUtils.getRelativeMinute(date,6));

        //请求参数,需排序
        SortedMap<Object,Object> packageParams = new TreeMap<Object,Object>();
        packageParams.put("appid", appid);
        packageParams.put("mch_id", mch_id);
        packageParams.put("nonce_str", nonce_str);
        packageParams.put("body", body);
        packageParams.put("out_trade_no", out_trade_no);
        packageParams.put("total_fee", total_fee);
        packageParams.put("spbill_create_ip", spbill_create_ip);
        packageParams.put("notify_url", notify_url);
        packageParams.put("trade_type", trade_type);
        packageParams.put("product_id", product_id);
        packageParams.put("time_start", time_start);
        packageParams.put("time_expire", time_expire);
        //签名
        String sign = PayCommonUtil.createSign(SysConst.DEFAULT_ENCODING,packageParams,apiKey);
        packageParams.put("sign", sign);
        //请求参数转换成XML
        String requestXML = PayCommonUtil.getRequestXml(packageParams);
        //返回信息
        String resXml = HttpUtils.postData(WeiXinConfigUtils.orderUrl, requestXML);
        //返回信息XML格式化为map
        Map map = PayCommonUtil.doXMLParse(resXml);
这是工具类。要注意APIKey是需要增加到尾部去参与签名,详情参考微信签名算法。
public class PayCommonUtil {

    private static String sign = "sign";
    
    private static String key = "key";

    private static String eg = "=";

    private static String and = "&";

    private static final String flagStart  = "<![CDATA[";
    private static final String flagEnd  = "]]>";
    private static final String xmlStart = "<xml>";
    private static final String xmlEnd = "</xml>";

    private static final String leftStartTag = "<";
    private static final String leftEndTag = "</";
    private static final String rightTag = ">";

    private static final String returnCodeTagStart = "<return_code>";
    private static final String returnCodeTagEnd = "</return_code>";
    private static final String returnMsgStart = "<return_msg>";
    private static final String returnMsgEnd = "</return_msg>";

    private static final String successCode = "<![CDATA[SUCCESS]]>";
    private static final String failCode = "<![CDATA[FAIL]]>";


    /**
     *  构建一个指定长度大小的随机数
      * @param length
     * @return
     */
    public static int buildRandom(int length) {
        int num = 1;
        double random = Math.random();
        if (random < 0.1) {
            random = random + 0.1;
        }
        for (int i = 0; i < length; i++) {
            num = num * 10;
        }
        return (int) ((random * num));
    }

    /**
     * 获取随机字符串
     * @return
     * @throws Exception
     */
    public static String getNonceStr() throws Exception{

        String currentTime = HbdDateUtils.dateFormatCurrent.format(HbdDateUtils.now());
        String strTime = currentTime.substring(8,currentTime.length());
        //微信推荐随机数生成算法
        String strRandom = buildRandom(WeiXinConfigUtils.randomNumLength) + "";

        return strTime + strRandom;
    }


    /**
     * 微信sign签名
     * @param characterEncoding
     * @param packageParams
     * @param apiKey
     * @return
     */
    public static String createSign(String characterEncoding, SortedMap<Object, Object> packageParams, String apiKey) {

        StringBuffer sb = new StringBuffer();

        Set<Map.Entry<Object,Object>> es = packageParams.entrySet();

        for(Map.Entry<Object,Object> entry : es){
            String k =  String.valueOf(entry.getKey());
            String v = String.valueOf(entry.getValue());
            //sign和key不参与签名计算
            if (StringUtils.isNotBlank(v) && !sign.equals(k) && !key.equals(k)) {
                //k + "=" + v + "&"
                sb.append(k + eg + v + and);
            }
        }
        sb.append(key + eg + apiKey);
        String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
        return sign;
    }

    /**
     * 微信请求参数转换为xml格式
     * @param parameters
     * @return
     */
    public static String getRequestXml(SortedMap<Object, Object> parameters) {
        StringBuffer sb = new StringBuffer();
        sb.append(xmlStart);

        Set<Map.Entry<Object,Object>> es = parameters.entrySet();

        for(Map.Entry<Object,Object> entry : es){
            String k = String.valueOf(entry.getKey());
            String v = String.valueOf(entry.getValue());
            if ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k) || "sign".equalsIgnoreCase(k)) {
//              ("<" + k + ">" + "<![CDATA[" + v + "]]></" + k + ">");
                sb.append(leftStartTag).append(k).append(rightTag)
                  .append(flagStart).append(v).append(flagEnd)
                  .append(leftEndTag).append(k).append(rightTag);
            } else {
//              ("<" + k + ">" + v + "</" + k + ">");
                sb.append(leftStartTag).append(k).append(rightTag)
                  .append(v)
                  .append(leftEndTag).append(k).append(rightTag);
            }
        }
        sb.append(xmlEnd);
        return sb.toString();
    }

    /**
     * 微信 签名正确,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。
     * @return boolean
     */
    public static boolean isTenPaySign(String characterEncoding, SortedMap<Object, Object> packageParams, String apiKey) {
        StringBuffer sb = new StringBuffer();

        Set<Map.Entry<Object,Object>> es = packageParams.entrySet();
        for(Map.Entry<Object,Object> entry : es){
            String k = String.valueOf(entry.getKey());
            String v = String.valueOf(entry.getValue());
            //sign和key不参与签名计算
            if(!sign.equals(k) && StringUtils.isNotBlank(v)) {
                sb.append(k + eg + v + and);
            }
        }
        sb.append(key + eg + apiKey);

        //算出摘要
        String mySign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toLowerCase();
        String tenPaySign = (String.valueOf(packageParams.get(sign))).toLowerCase();

        return tenPaySign.equals(mySign);
    }


    /**
     * 微信返回值 xml格式化为map
     * @param strxml
     * @return
     * @throws JDOMException
     * @throws IOException
     */
    public static Map doXMLParse(String strxml) throws JDOMException, IOException {

        strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");

        Map returnMap = Maps.newHashMap();

        if(StringUtils.isBlank(strxml)){
            return returnMap;
        }

        InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
        SAXBuilder builder = new SAXBuilder();
        Document doc = builder.build(in);
        Element root = doc.getRootElement();
        List list = root.getChildren();

        for(Object s: list){
            Element e = (Element)s;
            String k = e.getName();
            String v = "";
            List children = e.getChildren();
            if(children.isEmpty()){
                v = e.getTextNormalize();
            }else {
                v = getChildrenText(children);
            }
            returnMap.put(k, v);
        }

        //关闭流
        in.close();

        return returnMap;
    }

    /**
     * 获取子结点的xml
     * @param children
     * @return String
     */
    private static String getChildrenText(List children) {
        StringBuffer sb = new StringBuffer();
        if(!children.isEmpty()) {
            for (Object s : children){
                Element e = (Element)s;
                String k = e.getName();
                String v = e.getTextNormalize();
                List list = e.getChildren();
//              ("<" + k + ">");
                sb.append(leftStartTag).append(k).append(rightTag);
                if(!list.isEmpty()){
                    sb.append(getChildrenText(list));
                }
                sb.append(v);
//              ("</" + k + ">");
                sb.append(leftEndTag).append(k).append(rightTag);
            }
        }

        return sb.toString();
    }

    /**
     * 微信判断订单金额和支付返回金额是否相同
     * 避免假通知
     * @param orderFee
     * @param returnFee
     * @return
     */
    public static Boolean isMatchFee(Double orderFee, int returnFee){
        //换算成分
        Double pointsFee = orderFee * 100;

        int pointsIntFee = pointsFee.intValue();

        if(pointsIntFee == returnFee){
            return true;
        }else{
            return false;
        }
    }

    /**
     * 微信回传值封装
     *
     * <xml>
     * <return_code><![CDATA[SUCCESS]]></return_code>
     * <return_msg><![CDATA[OK]]></return_msg>
     * </xml>
     * @param isSuccess 是否成功
     * @param value 回传值
     * @return
     */
    public static String freezeReturnXml(Boolean isSuccess,String value){
        StringBuilder msg = new StringBuilder();


        if(isSuccess){
            msg.append(xmlStart)
               .append(returnCodeTagStart)
                    .append(successCode)
               .append(returnCodeTagEnd)
               .append(returnMsgStart)
                    .append(flagStart).append(value).append(flagEnd)
               .append(returnMsgEnd)
               .append(xmlEnd);
        }else{
            msg.append(xmlStart)
               .append(returnCodeTagStart)
                    .append(failCode)
               .append(returnCodeTagEnd)
               .append(returnMsgStart)
                    .append(flagStart).append(value).append(flagEnd)
               .append(returnMsgEnd)
               .append(xmlEnd);
        }

        return msg.toString();
    }


    public static String getTimeStamp() {
        return String.valueOf(System.currentTimeMillis() / 1000);
    }

以下为回调接口。回调接口具体的内容自己去封装,大致的框架能直接拿来用。

首先判断 通信是否成功,然后才是真正的交易判断 resultCode为SUCCESS时才是交易成功,其他均为失败。需要修改本地订单一些信息。

@RequestMapping(value = "/wei-xin-notify", method = RequestMethod.GET)
    public void weiXinNotify(HttpServletRequest request,HttpServletResponse response) throws Exception{

        String returnValue = payBiz.getWeiXinReturnValue(request);

        //解析xml成map
        Map<String, String> returnValueMap = Maps.newHashMap();
        returnValueMap = PayCommonUtil.doXMLParse(returnValue.toString());

        //过滤空 设置 TreeMap
        SortedMap<Object,Object> packageParams = new TreeMap<Object,Object>();
        Set<Map.Entry<String,String>> value = returnValueMap.entrySet();
        for(Map.Entry<String,String> entry : value){
            if(null != entry.getValue()){
                packageParams.put(entry.getKey(),entry.getValue().trim());
            }
        }

        payBiz.handleOrderInfo(packageParams,WeiXinConfigUtils.apiKey,response);
    }

具体逻辑封装在handOrderInfo中。这个锁有没有必要加暂时不清楚,不过微信回调文档是提示需要加上锁来避免脏读,但是本例中的锁加的不是很完美,会有一些意外情况,后期再修复,正常情况下不加锁其实也没有多大问题。

/**
     * 处理微信支付回调后的业务逻辑
     * @param packageParams
     * @param apiKey
     */
    public void handleOrderInfo(SortedMap<Object,Object> packageParams,String apiKey,HttpServletResponse response){

        //返回给微信的通知
        String resXml = "";

        //判断此次通信是否成功
        if(PayCommonUtil.isTenPaySign(SysConst.DEFAULT_ENCODING, packageParams, apiKey)){

            PayInfo queryPayInfo = new PayInfo();

            queryPayInfo.setPayOrderNumber(String.valueOf(packageParams.get(tradeNum)));

            PayInfo payInfo = null;

            OrderInfo orderInfo = null;

            String redisKey = SysConst.WEI_XIN_PAY_LOCK_KEY + String.valueOf(packageParams.get(tradeNum));
            //针对每个交易订单号进行加锁
            if(redisBillLockSerivce.tryLock(redisKey,20, TimeUnit.SECONDS)){

                try{
                    payInfo = this.getPayInfo(queryPayInfo);
                    orderInfo = this.getOrderInfo(payInfo);

                }catch (Exception e){
                    //意外情况下全部直接return,不做任何处理
                    redisBillLockSerivce.unLock(redisKey);
                    return;
                }

                // result_code 为SUCCESS时才是支付成功
                if(SUCCESS.equals(String.valueOf(packageParams.get(resultCode)))){
                    //判断是否能够修改当前系统订单信息

                    try{
                        resXml = this.handleExceptionTrade(payInfo, orderInfo, packageParams);
                    }catch (Exception e){
                        redisBillLockSerivce.unLock(redisKey);
                        return;
                    }
                    if(StringUtils.isBlank(resXml)){
                        try{
                            payInfoService.modifyPayAndOrderInfo(true,payInfo,orderInfo);
                        }catch (Exception e){
                            logger.error("交易成功--修改本地表失败" + e.getMessage(),e);
                            redisBillLockSerivce.unLock(redisKey);
                            return;
                        }
                        logger.info("更新--成功交易, 交易Id: "+ payInfo.getPayId());
                        resXml = PayCommonUtil.freezeReturnXml(true,ok);
                    }
                }else {
                    logger.error("支付失败,错误信息:" + packageParams.get(errCode));
                    try{
                        payInfoService.modifyPayAndOrderInfo(false,payInfo,orderInfo);
                    }catch (Exception e){
                        logger.error("交易失败--修改本地表失败"+e.getMessage(),e);
                        redisBillLockSerivce.unLock(redisKey);
                        return;
                    }
                    logger.info("更新--失败交易, 交易Id: "+ payInfo.getPayId());
                    resXml = PayCommonUtil.freezeReturnXml(false, failed);
                }
            }else{
                logger.error("获取锁失败");
                return;
            }
            redisBillLockSerivce.unLock(redisKey);
            sendResponse(response,resXml);
        }else {
            //当前回调请求视为无效,只记录log,等待后续回调过来
            logger.error("通知签名验证失败: " + packageParams.get(returnMsg));
        }
    }
微信提示需要处理已处理的订单,还有金额的判断,可以参照官方文档,这边也是去判断了一下,避免一些非常情况。
/**
     * 处理回传交易异常情况
     * @param payInfo
     * @param orderInfo
     * @param packageParams
     * @return
     * @throws Exception
     */
    private String handleExceptionTrade(PayInfo payInfo,OrderInfo orderInfo, SortedMap<Object,Object> packageParams) throws Exception{

        String returnMsg = "";

        try{
            if(HbdEnum.PayInfoStatus.Normal.value() != payInfo.getPayStatus()){

                returnMsg = PayCommonUtil.freezeReturnXml(false,handled);

                logger.error("交易: " +payInfo.getPayOrderNumber()+"已经处理过");

                return returnMsg;
            }

            if(!PayCommonUtil.isMatchFee(orderInfo.getOrderPrice(), Integer.parseInt(packageParams.get(tradeFee).toString()))){

                returnMsg = PayCommonUtil.freezeReturnXml(false,unMatched);

                logger.error("订单: " + orderInfo.getOrderId() + "金额不匹配 "+ "订单价格: "+ orderInfo.getOrderPrice()+ " 交易价格: "+(int) packageParams.get(tradeFee));

                return returnMsg;
            }
        }catch (Exception e){
            //此处避免程序处理错误导致returnMsg依然为空,进行Order 和PayInfo的修改动作,
            //直接return等待下次微信请求。
            logger.error("程序处理错误: "+ e.getMessage(),e);
            throw new Exception();
        }

        return returnMsg;
    }

Md5工具类

public class MD5Util {
    private static String byteArrayToHexString(byte b[]) {

        StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++)
            resultSb.append(byteToHexString(b[i]));

        return resultSb.toString();
    }

    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0){
            n += 256;
        }
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    public static String MD5Encode(String origin, String charsetName) {
        String resultString = null;
        try {
            resultString = origin;
            MessageDigest md = MessageDigest.getInstance("MD5");
            if (charsetName == null || "".equals(charsetName))
                resultString = byteArrayToHexString(md.digest(resultString
                        .getBytes()));
            else
                resultString = byteArrayToHexString(md.digest(resultString
                        .getBytes(charsetName)));
        } catch (Exception exception) {
        }
        return resultString;
    }

    private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
}


最后需要指明的是,一个交易订单号(out_trade_no)在微信那边只能生成一次,下一次再去生成时如果交易订单号不变的话,微信会报错,所以本地表的设计就有讲究了。

本人项目中有一个订单关联表,里面会生成一个随机交易订单号,每次都拿这个交易订单号去微信生成交易URL,这样即使重复的生成二维码Url,但是这些Url都指向同一个订单,但是每次的交易订单号都不一样,满足了微信的要求,这边需要特别注意下。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值