微信小程序支付

480 阅读6分钟

微信小程序支付官网  微信提供的SDK以及Demo  发送请求和MD5的工具hutool

后来偶然间看到微信官方提供的SDK以及demo。。。意外的是小程序支付文档里竟然没有,而是放在微信的扫码支付里。在使用SDK代替自己代码之前总结一下

统一下单

  1. 小程序内调用登录接口。   一般进行支付的用户都已经注册过商家小程序。所以我直接在数据库中查找用户对应的openId

  2. 准备参数,把必填的请求参数放入到HashMap<String, String>()中,需要注意的是签名的算法

  3. 签名算法:所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA。

    public String concatMapToString(Map<String, String> map) {
            Map<String, String> paramterMap = new HashMap<>();
            map.forEach((key, value) -> paramterMap.put(key, value));
            // 按照key升续排序,然后拼接参数
            Set<String> keySet = paramterMap.keySet();
            String[] keyArray = keySet.toArray(new String[keySet.size()]);
            Arrays.sort(keyArray);
            StringBuilder sb = new StringBuilder();
            for (String k : keyArray) {
            	if("sign".equals(k)) {
            		continue;
            	}
                if (paramterMap.get(k).trim().length() > 0) {
                    // 参数值为空,则不参与签名
                    sb.append(k).append("=").append(paramterMap.get(k).trim()).append("&");
                }
            }
            return sb.toString();
        }
    
  4. 在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。

    public String createSign(Map<String, String> param) {
    	String stringSignTemp= concatMapToString(param);	
            stringSignTemp= stringSignTemp+ "key=1234567"; //注:key为商户平台设置的密钥key	
            String sign = SecureUtil.md5(stringSignTemp).toUpperCase(); //注:MD5签名方式	        return sign;
    }
    
  5. 把带签名的HashMap<String, String>()转换为Xml格式

    XmlUtil.mapToXmlStr(map)
    
  6. 发送请求,获取到请求结果

    String resultStr = HttpRequest.post(UNIFIED_ORDER).body(param).execute().body();
    Map<String, String> result = mapFormatConvert(XmlUtil.xmlToMap(resultStr));
    // 把结果Map<String, Object>转为Map<String, String>
    public Map<String, String> mapFormatConvert(Map<String, Object> map) {
    	Map<String, String> result = new HashMap<>();
    	for(Map.Entry<String, Object> entry:map.entrySet()) {
    		result.put(entry.getKey(), entry.getValue().toString());
    	}
    	return result;
    }
    
  7. 处理请求结果,根据返回的预付单信息(prepay_id)再次签名.并返回支付参数(五个参数和sign)

    // 通信标识成功
    if(PayOrderEnum.SUCCESS.getCode().equals(returnCode)) {
      String resultCode = result.get("result_code");	// 成功
    	if(PayOrderEnum.SUCCESS.getCode().equals(resultCode)) {
    		payResult = new HashMap<>();
    		payResult = getPayParam(result.get("prepay_id"));		
                    payResult.put("resultCode", PayOrderEnum.SUCCESS.getCode());
    		payResult.put("resultMessage", "");
    	} else {
    		String errCode = result.get("err_code");
    		// 失败
    		if(PayOrderEnum.NOTENOUGH.getCode().equals(errCode)) {
    			throw new ValidateException(PayOrderEnum.NOTENOUGH.getMessage());
    		} else if(PayOrderEnum.ORDERPAID.getCode().equals(errCode)) {
    			throw new ValidateException(PayOrderEnum.ORDERPAID.getMessage());
    		} else if(PayOrderEnum.ORDERCLOSED.getCode().equals(errCode)) {
    			throw new ValidateException(PayOrderEnum.ORDERCLOSED.getMessage());
    		} else if(PayOrderEnum.SYSTEMERROR.getCode().equals(errCode)) {
    			throw new ValidateException(PayOrderEnum.SYSTEMERROR.getMessage());
    		} else {
    			throw new ValidateException(PayOrderEnum.OTHER.getMessage());
    		}
    	}
    } else {
    	throw new ValidateException(PayOrderEnum.OTHER.getMessage());
    }
    // 根据预付单id生成签名并返回
    public Map<String, String> getPayParam(String prepayId) {
    	Map<String, String> map = new HashMap<String, String>();
    	map.put("appId", appId);	
            map.put("timeStamp", String.valueOf(new Date().getTime()));
    	map.put("nonceStr", RandomUtil.randomString(20));
    	map.put("package", "prepay_id=" + prepayId);
    	map.put("signType", "MD5");
    	map.put("paySign", createSign(map));
    	return map;
    }
    
  8. 小程序接收到请求确认成功后发起微信支付

    wx.requestPayment({
      timeStamp: '',
      nonceStr: '',
      package: '',
      signType: 'MD5',
      paySign: '',
      success (res) { },
      fail (res) { }
    })
    
  9. 接收微信支付结果通知

    public void wxnotify(HttpServletRequest request, HttpServletResponse response) throws IOException {
    	InputStream inputStream = request.getInputStream();
    	String charsetName = "UTF-8";
    	if(Validator.isNotEmpty(request.getCharacterEncoding())) {
    		charsetName = request.getCharacterEncoding();
    	}
            ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = inputStream.read(buffer)) != -1) {
                outSteam.write(buffer, 0, len);
            }
            // 获取微信调用我们notify_url的返回信息
            String result = new String(outSteam.toByteArray(), charsetName);
            // 处理微信通知结果并返回应答
            String resXml = handleWxNotify(result);
            // 处理业务完毕
    	try(BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream())) {
    		out.write(resXml.getBytes());
    		out.flush();
    	};
    }
    
  10. 处理微信支付结果

    /**
     * 校验微信返回的参数是否和本地一致
     * @param payResultStr
     * @return
     */
    public String handleWxNotify(String payResultStr) {
    	Map<String, String> result = new HashMap<>();
    	if(Validator.isNotEmpty(payResultStr)) {
              // 转换结果为map<String, String>		
                    Map<String, String> payResult = mapFormatConvert(XmlUtil.xmlToMap(payResultStr));
    		if("SUCCESS".equals(payResult.get("result_code"))) {
    			// 获取系统内部的订单id,查询结果进行签名验证,并校验返回的订单金额是否与商户侧的订单金额一致
    			int id = Integer.parseInt(payResult.get("out_trade_no"));
    			// 微信返回的金额和签名
    			int wxTotalFee = Integer.parseInt(payResult.get("total_fee"));
    			String wxSign = payResult.get("sign");
    
    			// 当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成
    			// 功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
    			synchronized(this){
    				Map<String, Object> orderSystem; // 系统订单信息
    				Integer orderStatus = Integer.parseInt(orderSystem.get("orderStatus").toString());				if(!NOT_COMPLETE_ORDER_STATUS.contains(orderStatus)) {
    					result.put("return_code", "SUCCESS");
    					result.put("return_msg", "");
    					return XmlUtil.mapToXmlStr(result);
    				}
    				// 查询系统,返回系统的金额
    				int systemTotalFee = Integer.parseInt(orderSystem.get("orderMoney").toString());				String systemSign = createSign(payResult);
    				boolean signIsTrue = systemSign.equals(wxSign);
    				boolean totalFeeIsTrue = systemTotalFee == wxTotalFee;
    				if(signIsTrue && totalFeeIsTrue) {
    					payOrderDAO.updateOrderIsPaid();
    					result.put("return_code", "SUCCESS");
    					result.put("return_msg", "");
    					payOrderDAO.flush();
    				} else if(!signIsTrue) {
    					result.put("return_code", "FAIL");
    					result.put("return_msg", "签名失败");
    				} else if(!totalFeeIsTrue) {
    					result.put("return_code", "FAIL");
    					result.put("return_msg", "参数格式校验错误");
    				}
    			}
    		}
    	} else {
    		result.put("return_code", "FAIL");
    		result.put("return_msg", "参数格式校验错误");
    	}
    	return XmlUtil.mapToXmlStr(result);
    }
    

查询订单/关闭订单/查询退款

  1. 组织参数。 可以根据orderID查询订单、关闭订单、查询退款。

  2. 查询订单 微信订单号transaction_id  >  商户订单号 out_trade_no

  3. 关闭订单 商户订单号 out_trade_no

  4. 查询退款  微信退款单号 refund_id > 商户退款单号  out_refund_no > 微信订单号 transaction_id > 商户订单号 out_trade_no

    private String getParam(String orderId) {
    	Map<String, String> map = new HashMap<String, String>();
    	map.put("appid", APPID);	
            map.put("mch_id", MCH_ID);
    	map.put("out_trade_no", orderId);
    	map.put("nonce_str", RandomUtil.randomString(20));
    	map.put("sign", createSign(map));
    	return XmlUtil.mapToXmlStr(map);
    }
    
  5. 返回结果并进行处理

    String resultStr = HttpRequest.post(URL).body(getParam(orderId)).execute().body(); Map<String, String> result = mapFormatConvert(XmlUtil.xmlToMap(resultStr));

申请退款

和其他请求的区别: 需要双向证书

  • 证书文件不能放在web服务器虚拟目录,应放在有访问权限控制的目录中,防止被他人下载;

  • 建议将证书文件名改为复杂且不容易猜测的文件名;

  • 商户服务器要做好病毒和木马防护工作,不被非法侵入者窃取证书文件。

  1. 校验退款金额。 退款金额  <  订单金额 - 已退款金额

  2. 创建退款参数

  3. 读取证书

    public SSLSocketFactory readCertificate() throws Exception {
    	String mchId = payOrderConfig.getMchId();
    	KeyStore keyStore  = KeyStore.getInstance("PKCS12");
            FileInputStream instream = new FileInputStream(CERT_PATH);
            try {
                keyStore.load(instream, MCH_ID.toCharArray());
            } finally {
                instream.close();
            }
            SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, MCH_ID.toCharArray()).build();//这里也是写密码的        SSLSocketFactory sslSocketFactory = sslcontext.getSocketFactory();
            return sslSocketFactory;
    }
    
  4. 发送请求并处理返回结果

    String resultStr = HttpRequest.post(payOrderConfig.getRefundUrl()).setSSLSocketFactory(readCertificate()).body(xmlStr).execute().body();
    Map<String, String> payResult = mapFormatConvert(XmlUtil.xmlToMap(resultStr));
    

下载交易账单

  • 微信侧未成功下单的交易不会出现在对账单中。支付成功后撤销的交易会出现在对账单中,跟原支付单订单号一致;

  • 微信在次日9点启动生成前一天的对账单,建议商户10点后再获取;

  • 对账单中涉及金额的字段单位为“元”。

  • 对账单接口只能下载三个月以内的账单。

  • 对账单是以商户号纬度来生成的,如一个商户号与多个appid有绑定关系,则使用其中任何一个appid都可以请求下载对账单。对账单中的appid取自交易时候提交的appid,与请求下载对账单时使用的appid无关。

  • 自2018年起入驻的商户默认是开通免充值券后的结算对账单

  1. 创建参数请求,返回结果

    // 下载对账单的日期,格式:20140603
    public String downloadBill(String billDate) {
    	Map<String, String> map = new HashMap<String, String>();
    	map.put("appid", APPID);
    	map.put("mch_id", MCH_ID);
    	map.put("nonce_str", RandomUtil.randomString(20));
    	map.put("bill_date", billDate);
    	map.put("bill_type", "ALL");
    	map.put("sign", createSign(map));
    	String xmlStr = XmlUtil.mapToXmlStr(map);
    	return HttpRequest.post(DOWNLOAD_URL).body(xmlStr).execute().body();
    }
    
  2. 处理返回的结果

    public void handleWxBill(String result) {
    	String[] str = result.split("\n");//按行读取数据(*这个尤为重要*)
    	int len = str.length;
    	for (int i = 0; i < len; i++) {
    		String[] wxBill = str[i].replace("`", "").split(",");
                    // 最后两行一行为合计的表头,一行为合计的内容
    		if (i > 0 && i < (len - 2)) {
    			// 明细行数据[交易时间,公众账号ID,商户号,特约商户号,设备号,微信订单号,商户订单号,用户标识,交易类型,交易状态,付款银行,货币种类,应结订单金额,代金券金额,商品名称,商户数据包,手续费,费率,订单金额,费率备注]
    			PayOrderBill payOrderBill = new PayOrderBill();
    			Date tradeTime = DateUtil.parse(getArrayValue(wxBill, 0), "yyyy-MM-dd HH:mm:ss").toJdkDate();
    			payOrderBill.setTradeTime(tradeTime);// 交易时间
    			getArrayValue(wxBill, 1);// 公众账号ID
    			getArrayValue(wxBill, 2);// 商户号
    			getArrayValue(wxBill, 5);// 微信订单号
    			getArrayValue(wxBill, 6);// 商户订单号
    			getArrayValue(wxBill, 7);// 用户标识
    			getArrayValue(wxBill, 8);// 交易类型
    			getArrayValue(wxBill, 9);// 交易状态
    			getArrayValue(wxBill, 10);// 付款银行
    			getArrayValue(wxBill, 11); // 货币种类
    			getArrayDecimalValue(wxBill, 12); // 应结订单金额
    			getArrayDecimalValue(wxBill, 13); // 代金券金额
    			getArrayValue(wxBill, 14); // 微信退款单号
    			getArrayValue(wxBill, 15); // 商户退款单号
    			getArrayDecimalValue(wxBill, 16)); // 退款金额
    			getArrayDecimalValue(wxBill, 17)); // 充值券退款金额
    			getArrayValue(wxBill, 18); // 退款类型
    			getArrayValue(wxBill, 19); // 退款状态
    			getArrayValue(wxBill, 20);// 商品名称
    			getArrayValue(wxBill, 22);// 手续费
    			getArrayValue(wxBill, 23);// 费率
    			getArrayDecimalValue(wxBill, 24);// 订单金额
    			getArrayDecimalValue(wxBill, 25); // 申请退款金额
    			getArrayValue(wxBill, 26);// 费率备注
    		}
    	}
    }
    public static String getArrayValue(String[] wxBill, int index) {
    	try {
    		return wxBill[index];
    	} catch (Exception e) {
    		return "";
    	}
    }
    
    public static BigDecimal getArrayDecimalValue(String[] wxBill, int index) {
    	try {
    		return new BigDecimal(wxBill[index]);
    	} catch (Exception e) {
    		return new BigDecimal(0);
    	}
    }