当前位置: > 财经>正文

微信退款流程实现整理(java)

2023-07-14 06:22:56 互联网 未知 财经

微信退款流程实现整理(java)

前言

此处整理为简便,将所有应用到的方法都整理到了一起,实际开发中尽量将controller,service,mapper,工具类分开。此文章着重注意退款回调,其中应用了数据解密(作者一开始困扰在此处);

微信退款请求

退款请求数据均来自客户支付的订单信息,以订单为依据进行退款;其中的逻辑以自己的业务需求来制定,此处仅此校验订单是否存在与订单状态是否符合退款需求; 此处请求退款需要小程序绑定商户平台的“退款证书”,在商户平台申请下载;

/** * @program: youpin * @description: 微信相关 * @author: Mr.Jkx * @create: 2023-03-23 15:13 */@RestController@RequestMapping(value = "/wxpay", produces = "application/json;charset=UTF-8")public class WxPayController { private static final Logger LOGGER = (Logger) LoggerFactory.getLogger(WxPayController.class); @Resource private TOrderMapper tOrderMapper; /** * @Description: 微信退款 * @Author: Mr.Jkx * @date: 2023/3/23 15:08 */ @RequestMapping(value = "/refundOrderByWx") public void refundOrderByWx(TOrder tOrder) throws Exception { // 退款证书所在服务器路径(商户平台申请下载) String certificatePath = ""; // 查询退款订单数据 TOrder tOrderData = tOrderMapper.selectOrderData(tOrder.getoId()); if (null != tOrderData && !StringUtils.equals(OrderStatus.ORDER_STATUS1.getCode(), tOrder.getoStatus())) { // 退款总金额(商品总价钱+运费),此处订单退款金额需要和订单支付金额相符,否则调取不成功 int refundTotalFee = new Double(tOrderData.getoTotalPrice() * 100 + tOrderData.getoWaybillPrice() * 100).intValue(); String out_refund_no = UUIDUtil.getUUID(); tOrderData.setoOutRefundNo(out_refund_no); PayUtil payUtil = new PayUtil(); payUtil.setAppid("appid"); payUtil.setMch_id("mch_id"); // 商户号 payUtil.setPayKey("payKey"); // 秘钥 payUtil.setOut_trade_no(tOrderData.getoCode()); payUtil.setNonce_str(tOrderData.getoId()); payUtil.setTotal_fee(refundTotalFee); payUtil.setRefund_fee(refundTotalFee); payUtil.setOut_refund_no(out_refund_no); payUtil.setTransaction_id(tOrderData.getoTransactionId()); payUtil.setNotify_url("https://XXX/wxpay/refundCardOrder"); Map map = wxRefund(payUtil, certificatePath); if (map.get("return_code").equals("SUCCESS")) { // 退款请求成功进行退款业务逻辑处理 TODO } else { System.out.println("微信退款请求失败"); } } else { System.out.println("订单不存在或订单状态不争取不能退款"); } } /** * @Description: 微信退款 * @Author: Mr.Jkx * @date: 2023/3/23 15:59 */ public static Map wxRefund(PayUtil payUtil, String certificatepath) throws Exception { String mch_id = payUtil.getMch_id(); String stringA = "appid=" + payUtil.getAppid() + "&mch_id=" + mch_id + "&nonce_str=" + payUtil.getNonce_str() + "¬ify_url=" + payUtil.getNotify_url() + "&out_refund_no=" + payUtil.getOut_refund_no() + "&out_trade_no=" + payUtil.getOut_trade_no() + "&refund_fee=" + payUtil.getRefund_fee() + "&total_fee=" + payUtil.getTotal_fee() + "&key=" + payUtil.getPayKey(); String sign = Md5Util.md5(stringA).toUpperCase(); String xml = "" + " " + payUtil.getAppid() + "" + " " + mch_id + "" + " " + payUtil.getNonce_str() + "" + " " + payUtil.getOut_refund_no() + "" + " " + payUtil.getOut_trade_no() + "" + " " + payUtil.getRefund_fee() + "" + " " + payUtil.getTotal_fee() + "" + " " + payUtil.getNotify_url() + "" + " " + sign + "" + " "; LOGGER.info("调试模式_退款接口 请求XML数据:{}", xml); //调用统一下单接口,并接受返回的结果 String result = payOfCertificate(Constants.WX_REFUND_URL, xml, certificatepath, mch_id); LOGGER.info("------退款回执信息{}-------" + result); // 将解析结果存储在HashMap中 Map map = doXMLParse(result); LOGGER.info("----订单退款-----回执数据:{}", map); return map; } /** * @Description: 加载证书 发送请求 * @Author: Mr.Jkx * @date: 2023/3/23 16:06 * url:退款请求地址 * data:请求数据 * certificatepath:证书路径 * mch_id:商品平台mch_id */ public static String payOfCertificate(String url, String data, String certificatepath, String mch_id) throws Exception { KeyStore keyStore = KeyStore.getInstance("PKCS12"); FileInputStream is = new FileInputStream(new File(certificatepath)); try { keyStore.load(is, mch_id.toCharArray()); } finally { is.close(); } SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, mch_id.toCharArray()).build(); SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( sslcontext, new String[]{"TLSv1"}, null, SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER ); CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build(); try { HttpPost httpost = new HttpPost(url); // 设置响应头信息 httpost.addHeader("Connection", "keep-alive"); httpost.addHeader("Accept", "*/*"); httpost.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); httpost.addHeader("Host", "api.mch.weixin.qq.com"); httpost.addHeader("X-Requested-With", "XMLHttpRequest"); httpost.addHeader("Cache-Control", "max-age=0"); httpost.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) "); httpost.setEntity(new StringEntity(data, "UTF-8")); CloseableHttpResponse response = httpclient.execute(httpost); try { HttpEntity entity = response.getEntity(); String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8"); EntityUtils.consume(entity); return jsonStr; } finally { response.close(); } } finally { httpclient.close(); } } /** * 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。 * * @param strxml * @return */ public static Map doXMLParse(String strxml) throws Exception { if (null == strxml || "".equals(strxml)) { return null; } Map m = new HashMap(); InputStream in = String2Inputstream(strxml); SAXBuilder builder = new SAXBuilder(); Document doc = builder.build(in); Element root = doc.getRootElement(); List list = root.getChildren(); Iterator it = list.iterator(); while (it.hasNext()) { Element e = (Element) it.next(); String k = e.getName(); String v = ""; List children = e.getChildren(); if (children.isEmpty()) { v = e.getTextNormalize(); } else { v = getChildrenText(children); } m.put(k, v); } //关闭流 in.close(); return m; } /** * 获取子结点的xml * * @param children * @return String */ public static String getChildrenText(List children) { StringBuffer sb = new StringBuffer(); if (!children.isEmpty()) { Iterator it = children.iterator(); while (it.hasNext()) { Element e = (Element) it.next(); String name = e.getName(); String value = e.getTextNormalize(); List list = e.getChildren(); sb.append(""); if (!list.isEmpty()) { sb.append(getChildrenText(list)); } sb.append(value); sb.append(""); } } return sb.toString(); } public static InputStream String2Inputstream(String str) { return new ByteArrayInputStream(str.getBytes()); }} 微信退款回调

微信退款分为两个步骤,首先请求微信退款,然后再异步给予退款回执请求,通知退款是否成功; 退款地址在请求时已经作为参数请求到微信,以此为依据请求回调地址; 退款回执接口,接收参数,参数解密,根据回执参数判断是否退款成功,并进行相应业务逻辑;

@RestController@RequestMapping(value = "/wxpay", produces = "application/json;charset=UTF-8")public class WxPayController { private static final Logger LOGGER = (Logger) LoggerFactory.getLogger(WxPayController.class); @Resource private TOrderMapper tOrderMapper; /** * @Description: 微信退款回调 * @Author: Mr.Jkx * @date: 2023/3/23 16:14 */ @RequestMapping(value = "/refundCardOrder") public void refundCardOrder(HttpServletRequest request, HttpServletResponse response) throws Exception { String xmlStr = NotifyServlet.getWxXml(request); LOGGER.info("---- refundCard order request data(xml) ----:{}", xmlStr); Map map2 = refundCardMessageDecrypt(xmlStr); //退款状态SUCCESSSUCCESS-退款成功、CHANGE-退款异常、REFUNDCLOSE—退款关闭 String refund_status = map2.get("refund_status"); String transactionId = map2.get("transaction_id"); LOGGER.info("---- refundCard order request data >> transactionId:{}", transactionId); // 根据支付微信回执订单号查询订单数据 TOrder tOrder = tOrderMapper.selOrderByTransactionId(transactionId); LOGGER.info("---- refundCard sel order data >> oStatus:{}", tOrder.getoStatus()); String refundId = map2.get("refund_id"); tOrder.setoOutRefundNo(refundId); if (null != tOrder && StringUtils.equals(OrderStatus.ORDER_STATUS5.getCode(), tOrder.getoStatus())) { if (WXPayConstants.SUCCESS.equals(refund_status)) { // 退款成功支付回调,退款成功,退款逻辑处理 TODO } } // 微信支付操作成功之后,回执操作 wxPayReceipt(response); } /** * @Description: 微信退款回调数据解密 * @Author: Mr.Jkx * @date: 2023/6/8 19:04 */ public Map refundCardMessageDecrypt(String xmlStr) throws Exception { LOGGER.info("-----微信退款回调请求参数解密 START ;xmlStr:{}", xmlStr); Map aesMap = new HashMap(); // xml转换为map Map map = xmlToMap(xmlStr); if (WXPayConstants.SUCCESS.equalsIgnoreCase(map.get("return_code"))) { /** 以下字段在return_code为SUCCESS的时候有返回: **/ // 加密信息:加密信息请用商户秘钥进行解密,详见解密方式 String req_info = map.get("req_info"); // 信息解密 String resultStr = AESUtil.decryptData(req_info); aesMap = xmlToMap(resultStr); LOGGER.info("-----微信退款回调请求参数解密 END ;strData:{}, mapData:{}", xmlStr, aesMap); } else { LOGGER.info("微信退款-----数据返回失败!!!"); } return aesMap; } /** * XML格式字符串转换为Map * * @param strXML XML字符串 * @return XML数据转换后的Map * @throws Exception */ public static Map xmlToMap(String strXML) throws Exception { try { Map data = new HashMap(); DocumentBuilder documentBuilder = WXPayXmlUtil.newDocumentBuilder(); InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8")); org.w3c.dom.Document doc = documentBuilder.parse(stream); doc.getDocumentElement().normalize(); NodeList nodeList = doc.getDocumentElement().getChildNodes(); for (int idx = 0; idx org.w3c.dom.Element element = (org.w3c.dom.Element) node; data.put(element.getNodeName(), element.getTextContent()); } } try { stream.close(); } catch (Exception ex) { // do nothing } return data; } catch (Exception ex) { WXPayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML); throw ex; } } /** * @Description: 微信回执信息 * @Author: Mr.Jkx * @date: 2023/5/11 11:19 */ private void wxPayReceipt(HttpServletResponse response) throws IOException { String resXml = ""; // 微信支付成功之后,回执操作 response.setCharacterEncoding("UTF-8"); response.setContentType("application/xml; charset=utf-8"); PrintWriter out = response.getWriter(); resXml = "" + "" + "" + " "; out.print(resXml); out.close(); }} 微信回调数据解密工具类 package com.pinto.youpin.util;import com.pinto.youpin.util.wxpay.WXPayUtil;import org.bouncycastle.jce.provider.BouncyCastleProvider;import javax.crypto.Cipher;import javax.crypto.spec.SecretKeySpec;import java.io.UnsupportedEncodingException;import java.security.Security;import java.util.Base64;/** * 微信支付AES加解密工具类 * * @author yclimb * @date 2018/6/21 */public class AESUtil { /** * 密钥算法 */ private static final String ALGORITHM = "AES"; /** * 加解密算法/工作模式/填充方式 */ private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS7Padding"; /** * 生成key */ private static SecretKeySpec KEY; static { try { // Constants.API_KEY(微信支付秘钥) KEY = new SecretKeySpec(WXPayUtil.MD5(Constants.API_KEY).toLowerCase().getBytes(), ALGORITHM); } catch (Exception e) { e.printStackTrace(); } } /** * AES加密 * * @param data d * @return str * @throws Exception e */ public static String encryptData(String data) throws Exception { // 创建密码器 Security.addProvider(new BouncyCastleProvider()); Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC"); // 初始化 cipher.init(Cipher.ENCRYPT_MODE, KEY); return base64Encode8859(new String(cipher.doFinal(data.getBytes()), "ISO-8859-1")); } /** * 解密方式 * 解密步骤如下: * (1)对加密串A做base64解码,得到加密串B * (2)对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 ) * (3)用key*对加密串B做AES-256-ECB解密(PKCS7Padding) */ public static String decryptData(String base64Data) throws Exception { Security.addProvider(new BouncyCastleProvider()); Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC"); cipher.init(Cipher.DECRYPT_MODE, KEY); return new String(cipher.doFinal(base64Decode8859(base64Data).getBytes("ISO-8859-1")), "utf-8"); } /** * Base64解码 * * @param source base64 str * @return str */ public static String base64Decode8859(final String source) { String result = ""; final Base64.Decoder decoder = Base64.getDecoder(); try { // 此处的字符集是ISO-8859-1 result = new String(decoder.decode(source), "ISO-8859-1"); } catch (final UnsupportedEncodingException e) { e.printStackTrace(); } return result; } /** * Base64加密 * * @param source str * @return base64 str */ public static String base64Encode8859(final String source) { String result = ""; final Base64.Encoder encoder = Base64.getEncoder(); byte[] textByte = null; try { //注意此处的编码是ISO-8859-1 textByte = source.getBytes("ISO-8859-1"); result = encoder.encodeToString(textByte); } catch (final UnsupportedEncodingException e) { e.printStackTrace(); } return result; } /** * @Description: 测试 * @Author: Mr.Jkx * @date: 2023/6/8 18:53 */ public static void main(String[] args) throws Exception { String A = "qS/pmvAXYetUObwHm9bAod9G3SVBKQK5CiIgETwHJT4ExUpJnIg87m37KlokIsBZCnQBIO2Ear7Q/IazZ6jDNsnmsITqYt1hPYloGjdjRGlqdSSBVRjk9NIkRRQIlb+5AOHJttfVKMsbMK8FzoysE+rL8yKaOzXvsNCA2g60z3bEw3x891ZwPPiUSkVJGeIHpafWdR94Y/j3hfsrEw5KOTGiPneH5d9zhC73MW/kDWu9+wDkJCtCf5fNc9GIC5x2zKNZozpQ9wT/WLyjSz/En166xbgUt9tApaaQSayFQ0eSokMjYYLKO5KJQ355QtkvZlW96rX9IO6hVHXDgPD7kJOTh/L99ZQtG5umLBfOd9i3xVH4qH+gvi/i0gEpvQOhTvxcrZeKs8Rsliua46u/aBdUy6GlICRxQPmvKBfL9cE2L5MZGqHkCMTmSr1i4L8Ubxoi3Yv6TCTTOo4MVc64igb9HttMVfOiLFrZKAyH64Y5C6+GATUMSzWhXDn089QyrZk+W6GFkQlA6dBlO7v0aucF8t3L6SFtnxm6XkH6eD4/FFxKz+wsqKDX1s+GnPGQdwjxsS3RLGjJuNoSB7N+v4AUbMgLT2sBzew89ow7/vEUMjJMQt3eISwprOaDZqZQBgdLVUwDyWnrWi50Rr2wEuJXv/m6x8f40wN93L8GvGbMsWGXlp9V9W3LR2LZD9CnrWAlhoYoDGMAwCKuPh+dfjXmVGttGxegM+PlUR8nq6Qr1zwHz4dV3PgzWlf3n5qR72tAJ/Y0045n3dT7Iw4UNzBHC6XkUIA884paHbZ3D0V95+WrdyVQ4icsgZIneaAMZfslVsnigUjnXl3m/qZGlW5A6d93VXNe8bQgA6s6lJeEsaZc3sLVPi5Tlr2nfbgdhB4XqYkR4DebEbUzalSOqM+OOeCsYj920+FboIxvShy2ECk6bjebMM3kYw0s1NUWXynKFTvbgZ35H9TNKaeom1qYVmbb/581N8+sO3yDDFzZaaqLqOtaUtgIe2SOS2A6GRnKSanqbsJVU4j2amWEpicl3WchYV9KPeuoqodu+4UCsaY2juUIcbbof/ygkG5NkDz27RA4fBxxAlqvtzEftw=="; Security.addProvider(new BouncyCastleProvider()); System.out.println(AESUtil.decryptData(A)); }} 微信请求数据组装对象 package com.pinto.youpin.entity.tool;public class PayUtil { private String appid;//微信分配的小程序ID 必填 private String mch_id;//微信支付分配的商户号 必填 private String device_info;//自定义参数,可以为终端设备号(门店号或收银设备ID),PC网页或公众号内支付可以传"WEB" 设备号 /** * 微信支付API接口协议中包含字段nonce_str,主要保证签名不可预测。我们推荐生成随机数算法如下:调用随机数函数生成,将得到的值转换为字符串。 */ private String nonce_str;//随机字符串,长度要求在32位以内。 必填 /** * stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA"; *

* stringSignTemp=stringA+"&key=192006250b4c09247ec02edce69f6a2d" //注:key为商户平台设置的密钥key *

* sign=MD5(stringSignTemp).toUpperCase()="9A0A8659F005D6984697E2CA0A9CF3B7" //注:MD5签名方式 *

* sign=hash_hmac("sha256",stringSignTemp,key).toUpperCase()="6A9AE1657590FD6257D693A078E1C3E4BB6BA4DC30B23E0EE2496E54170DACD6" //注:HMAC-SHA256签名方式 */ private String sign;//通过签名算法计算得出的签名值 必填 private String sign_type;//签名类型,默认为MD5,支持HMAC-SHA256和MD5。 private String body;//商品简单描述,该字段请按照规范传递 必填 private String detail;//商品详细描述,对于使用单品优惠的商户,改字段必须按照规范上传 private String attach;//附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用。 private String out_trade_no;//商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*且在同一个商户号下唯一 必填 private String fee_type;//符合ISO 4217标准的三位字母代码,默认人民币:CNY,详细列表请参见 private int total_fee;//订单总金额,单位为分 必填 private String spbill_create_ip;//APP和H5支付提交用户端ip,Native支付填调用微信支付API的机器IP。123.12.12.123 必填 private String time_start;//订单生成时间,格式为yyyyMMddHHmmss,如2009年12月25日9点10分10秒表示为20091225091010 private String time_expire;//订单失效时间,格式为yyyyMMddHHmmss,如2009年12月27日9点10分10秒表示为20091227091010。订单失效时间是针对订单号而言的,由于在请求支付的时候有一个必传参数prepay_id只有两小时的有效期,所以在重入时间超过2小时的时候需要重新请求下单接口获取新的prepay_id private String goods_tag;//订单优惠标记,使用代金券或立减优惠功能时需要的参数 private String notify_url;//异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 必填 private String trade_type;//小程序取值如下:JSAPI 必填 private String product_id;//trade_type=NATIVE时,此参数必传。此参数为二维码中包含的商品ID,商户自行定义。 private String limit_pay;//上传此参数no_credit--可限制用户不能使用信用卡支付 private String openid;//trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识。openid如何获取 必填 private String Out_refund_no;//商户退款单号 private int refund_fee;//退款金额 private String transaction_id; private String payKey; // 支付秘钥 public String getAppid() { return appid; } public void setAppid(String appid) { this.appid = appid; } public String getMch_id() { return mch_id; } public void setMch_id(String mch_id) { this.mch_id = mch_id; } public String getDevice_info() { return device_info; } public void setDevice_info(String device_info) { this.device_info = device_info; } public String getNonce_str() { return nonce_str; } public void setNonce_str(String nonce_str) { this.nonce_str = nonce_str; } public String getSign() { return sign; } public void setSign(String sign) { this.sign = sign; } public String getSign_type() { return sign_type; } public void setSign_type(String sign_type) { this.sign_type = sign_type; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } public String getDetail() { return detail; } public void setDetail(String detail) { this.detail = detail; } public String getAttach() { return attach; } public void setAttach(String attach) { this.attach = attach; } public String getOut_trade_no() { return out_trade_no; } public void setOut_trade_no(String out_trade_no) { this.out_trade_no = out_trade_no; } public String getFee_type() { return fee_type; } public void setFee_type(String fee_type) { this.fee_type = fee_type; } public int getTotal_fee() { return total_fee; } public void setTotal_fee(int total_fee) { this.total_fee = total_fee; } public String getSpbill_create_ip() { return spbill_create_ip; } public void setSpbill_create_ip(String spbill_create_ip) { this.spbill_create_ip = spbill_create_ip; } public String getTime_start() { return time_start; } public void setTime_start(String time_start) { this.time_start = time_start; } public String getTime_expire() { return time_expire; } public void setTime_expire(String time_expire) { this.time_expire = time_expire; } public String getGoods_tag() { return goods_tag; } public void setGoods_tag(String goods_tag) { this.goods_tag = goods_tag; } public String getNotify_url() { return notify_url; } public void setNotify_url(String notify_url) { this.notify_url = notify_url; } public String getTrade_type() { return trade_type; } public void setTrade_type(String trade_type) { this.trade_type = trade_type; } public String getProduct_id() { return product_id; } public void setProduct_id(String product_id) { this.product_id = product_id; } public String getLimit_pay() { return limit_pay; } public void setLimit_pay(String limit_pay) { this.limit_pay = limit_pay; } public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } public String getOut_refund_no() { return Out_refund_no; } public void setOut_refund_no(String out_refund_no) { Out_refund_no = out_refund_no; } public int getRefund_fee() { return refund_fee; } public void setRefund_fee(int refund_fee) { this.refund_fee = refund_fee; } public String getTransaction_id() { return transaction_id; } public void setTransaction_id(String transaction_id) { this.transaction_id = transaction_id; } public String getPayKey() { return payKey; } public void setPayKey(String payKey) { this.payKey = payKey; }} 作者能力有限,如有问题欢迎批评指正!!!

版权声明: 本站仅提供信息存储空间服务,旨在传递更多信息,不拥有所有权,不承担相关法律责任,不代表本网赞同其观点和对其真实性负责。如因作品内容、版权和其它问题需要同本网联系的,请发送邮件至 举报,一经查实,本站将立刻删除。