微信小程序支付官网 微信提供的SDK以及Demo 发送请求和MD5的工具hutool
后来偶然间看到微信官方提供的SDK以及demo。。。意外的是小程序支付文档里竟然没有,而是放在微信的扫码支付里。在使用SDK代替自己代码之前总结一下
统一下单
-
小程序内调用登录接口。 一般进行支付的用户都已经注册过商家小程序。所以我直接在数据库中查找用户对应的openId
-
准备参数,把必填的请求参数放入到HashMap<String, String>()中,需要注意的是签名的算法
-
签名算法:所有发送或者接收到的数据为集合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(); } -
在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; } -
把带签名的HashMap<String, String>()转换为Xml格式
XmlUtil.mapToXmlStr(map) -
发送请求,获取到请求结果
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; } -
处理请求结果,根据返回的预付单信息(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; } -
小程序接收到请求确认成功后发起微信支付
wx.requestPayment({ timeStamp: '', nonceStr: '', package: '', signType: 'MD5', paySign: '', success (res) { }, fail (res) { } }) -
接收微信支付结果通知
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(); }; } -
处理微信支付结果
/** * 校验微信返回的参数是否和本地一致 * @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); }
查询订单/关闭订单/查询退款
-
组织参数。 可以根据orderID查询订单、关闭订单、查询退款。
-
查询订单 微信订单号transaction_id > 商户订单号 out_trade_no
-
关闭订单 商户订单号 out_trade_no
-
查询退款 微信退款单号 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); } -
返回结果并进行处理
String resultStr = HttpRequest.post(URL).body(getParam(orderId)).execute().body(); Map<String, String> result = mapFormatConvert(XmlUtil.xmlToMap(resultStr));
申请退款
和其他请求的区别: 需要双向证书
-
证书文件不能放在web服务器虚拟目录,应放在有访问权限控制的目录中,防止被他人下载;
-
建议将证书文件名改为复杂且不容易猜测的文件名;
-
商户服务器要做好病毒和木马防护工作,不被非法侵入者窃取证书文件。
-
校验退款金额。 退款金额 < 订单金额 - 已退款金额
-
创建退款参数
-
读取证书
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; } -
发送请求并处理返回结果
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年起入驻的商户默认是开通免充值券后的结算对账单
-
创建参数请求,返回结果
// 下载对账单的日期,格式: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(); } -
处理返回的结果
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); } }