微信公众号之微信退款

一、前言

   这次的项目主要是关于微信公众号的一个开发,本人这次分配的模块是后台微信公众号的支付和退款,第一次接触微信公众的项目刚开始一脸懵逼,开发过程中遇到各种坑,所以想自己写一篇详细的关于微信公众号的开发,希望能对小伙伴们有所帮助!

二、微信申请退款接口

微信退款接口文档:微信公众号退款申请接口开发文档

退款申请流程:前端调用微信退款申请接口,退款申请需要双向的证书验证,登录微信商户平台(pay.weixin.qq.com)-->账户中心-->账户设置-->API安全-->证书下载。具体安装请参考微信证书安装文档:商户证书安装指导。在微信退款申请接口调用时需要读取服务器安装的证书,然后才能想微信发送请求,否则请求回发送失败或者返回数据为空。如果退款接口中设置了退款结果通知的URL,那么在退款申请成功后会给设置的通知接口返回数据,当放回的结果为SUCCESS时,会携带一部分加密的数据,数据解密方式:

解密步骤如下: 

(1)对加密串A做base64解码,得到加密串B

(2)对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 )

(3)用key*对加密串B做AES-256-ECB解密(PKCS7Padding)

解密成功后可进行相应的业务处理,代码如下:

申请退款接口:

/**
	 * 
	* @Title: refund  
	* @Description: 微信退款 
	* @param @param request
	* @param @param response
	* @param @return
	* @param @throws Exception     
	* @return Map<String,String>    
	* @throws
	 */
	@ResponseBody
	@RequestMapping("/refund")
	public JsPayResult refund(HttpServletRequest request, HttpServletResponse response) throws Exception {
		
		// 公众账号ID
		String appid = Constants.APPID;
		// 商户号
		String mch_id = Constants.MCHID;
		// 随机字符串
		String nonce_str = CommonUtil.getRandomStr();
		// 商户订单号
		String out_trade_no = request.getParameter("out_trade_no");
		// 商户退款单号,订单号是唯一的,加上订单号防止在高并发下退款单号不唯一
		String out_refund_no = CommonUtil.getOrderIdByTime()+out_trade_no;
		// 订单金额
		String total_fee1 = CommonUtil.getMoney(request.getParameter("total_fee"));
		String total_fee = CommonUtil.getMoney("0.01");
		// 退款金额
		String refund_fee1 = request.getParameter("refund_fee");
		String refund_fee = CommonUtil.getMoney("0.01");
		//退款结果通知url
		String notify_url = Constants.REFOUND_NOTIFY_URL;
		// 将请求参数封装至Map集合中
		SortedMap<String, String> paramMap = new TreeMap<String, String>();
		paramMap.put("appid", appid);
		paramMap.put("mch_id", mch_id);
		paramMap.put("nonce_str", nonce_str);
		paramMap.put("out_trade_no", out_trade_no);
		paramMap.put("out_refund_no", out_refund_no);
		paramMap.put("total_fee", total_fee);
		paramMap.put("refund_fee", refund_fee);
		paramMap.put("notify_url", notify_url);
		logger.info(paramMap);
		// 签名
		String sign = SignUtil.createSign(paramMap, Constants.PARTNER_KEY);
		paramMap.put("sign", sign);
		// 请求的xml数据
		String requestXml = XMLUtil.map2Xml(paramMap, "xml");
		
	//1.指定读取证书格式为PKCS12
        KeyStore keyStore  = KeyStore.getInstance("PKCS12");
        //2.读取本机存放的PKCS12证书文件
        FileInputStream instream = new FileInputStream(new File(Constants.CERTIFICATE_PATH));
        try {
        	 //指定PKCS12的密码(商户ID)
            keyStore.load(instream, Constants.MCHID.toCharArray());
        } finally {
            instream.close();
        }
        //3.ssl双向验证发送http请求报文
        SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, Constants.MCHID.toCharArray()).build();
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext,new String[] { "TLSv1" },null,
                SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
        CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
        //4.发送数据到微信的退款接口
        HttpPost httpost= HttpClientConnectionManager.getPostMethod(Constants.REFUND_URL);
        httpost.setEntity(new StringEntity(requestXml, "UTF-8"));
        HttpResponse weixinResponse = httpClient.execute(httpost);
        String resposeXmL = EntityUtils.toString(weixinResponse.getEntity(), "UTF-8");
        
		//5.将返回的xml转换为map
		Map<String, String> responseMap = XMLUtil.xml2Map(resposeXmL);
		JsPayResult result = new JsPayResult();
		if (Constants.RETURN_CODE.equals(responseMap.get("return_code"))) {
			
			result.setAppId(responseMap.get("appid"));
			result.setMchId(responseMap.get("mch_id"));
			result.setNonceStr(responseMap.get("nonce_str"));
			// 微信订单号
			result.setTransactionId(responseMap.get("transaction_id"));
			// 商户订单号
			result.setOutRradeNo(responseMap.get("out_trade_no"));
			// 商户退款单号
			result.setOutRefundNo(responseMap.get("out_refund_no"));
			// 微信退款单号
			result.setRefundId(responseMap.get("refund_id"));
			// 退款金额
			result.setSettlementRefundRee(responseMap.get("settlement_refund_fee"));
			// 订单金额
			result.setTotalFee(responseMap.get("total_fee"));
			//申请退款金额
			result.setRefundFee(responseMap.get("refund_fee"));
			// 现金支付金额
			result.setCashFee(responseMap.get("cash_fee"));
			//退款状态
			result.setRefundStatus(responseMap.get("refund_status"));
			//退款成功时间
			result.setSuccessTime(responseMap.get("success_time"));
			//退款入账账户
			result.setRefundRecvAccout(responseMap.get("refund_recv_accout"));
			//退款资金来源
			result.setRefundAccount(responseMap.get("refund_account"));
            //退款发起来源
			result.setRefundRequestSource(responseMap.get("refund_request_source"));
			result.setResultCode(Constants.RESULT_CODE_SUCCESS);
			result.setMessage("退款成功!");
			logger.info("*******退款申请**********"+"退款成功!");
		}
		else {
			result.setResultCode(Constants.RESULT_CODE_FAIL);
		    result.setMessage("退款失败!");
		    logger.info("*******退款申请**********"+"退款失败!");
			
		}
		return result;
	}

退款接口中涉及到的双向证书验证:

//1.指定读取证书格式为PKCS12
        KeyStore keyStore  = KeyStore.getInstance("PKCS12");
        //2.读取本机存放的PKCS12证书文件
        FileInputStream instream = new FileInputStream(new File(Constants.CERTIFICATE_PATH));//读取证书的安装路径
        try {
        	 //指定PKCS12的密码(商户ID)
            keyStore.load(instream, Constants.MCHID.toCharArray());
        } finally {
            instream.close();
        }
        //3.ssl双向验证发送http请求报文
        SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, Constants.MCHID.toCharArray()).build();
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext,new String[] { "TLSv1" },null,
                SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
        CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
        //4.发送数据到微信的退款接口
        HttpPost httpost= HttpClientConnectionManager.getPostMethod(Constants.REFUND_URL);
        httpost.setEntity(new StringEntity(requestXml, "UTF-8"));
        HttpResponse weixinResponse = httpClient.execute(httpost);
        String resposeXmL = EntityUtils.toString(weixinResponse.getEntity(), "UTF-8");

三、退款结果通知接口

/**
	 * 
	* @Title: refundNotify  
	* @Description: 退款结果通知  
	* @param @param request
	* @param @param response
	* @param @return
	* @param @throws Exception     
	* @return Map<String,String>    
	* @throws
	 */
	@ResponseBody
	@RequestMapping("/refundNotify")
	public JsPayResult refundNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
		// 将request请求中的数据转换为字符串
		String reqpXml = CommonUtil.readRequestStr(request);
		// 将返回串转换成 Map
		Map<String, String> xmlToMap = XMLUtil.xml2Map(reqpXml);
		// 返回给微信的结果
		String respXml = "";
		JsPayResult result = new JsPayResult();
		// 在return_code为SUCCESS的时候有返回 req_info
		if (Constants.RETURN_CODE.equals(xmlToMap.get("return_code"))) {
			// 退款返回加密信息
			String reqInfo = xmlToMap.get("req_info");
			// 解密后的信息
			String decodeReqInfo = AESUtil.decryptData(reqInfo);
			// 将解密后的信息换成 Map
			Map<String, String> reqInfoMap = XMLUtil.xml2Map(decodeReqInfo);

			ResponseResult responseResult = refundRegistration(Constants.BRANCHCODE, reqInfoMap.get("out_trade_no"),
					reqInfoMap.get("transaction_id"), "3300", DateUtil.getTradeTime(reqInfoMap.get("success_time")),
					Constants.YYSOURCE);
			if (Constants.RESULTCODE.equals(responseResult.getResultCode())) {
				respXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
						+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
				result.setMessage("退款成功!");
			} else {
				respXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
						+ "<return_msg><![CDATA[ERROR]]></return_msg>" + "</xml> ";
				result.setMessage("退款失败!");
			}
			
			logger.info("*******退款通知**********" + "退款成功!");
		} else {
			respXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA["
					+ xmlToMap.get("return_code") + "]]></return_msg>" + "</xml> ";
			result.setMessage("退款失败!");
			logger.info("*******退款通知**********" + "退款失败!");
		}

		result.setResultCode(xmlToMap.get("return_code"));
		BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
		out.write(respXml.getBytes());
		out.flush();
		out.close();
		return result;
	}

解密方式:

public class AESUtil {

	/**
	 * 密钥算法
	 */
	private static final String ALGORITHM = "AES";
	/**
	 * 加解密算法/工作模式/填充方式
	 */
	private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS7Padding";
	/**
	 * 生成key(商户密钥)
	 */
	private static final String key = "qp2tMA7jIDLyRRhz83Ut2eVQh8qaI5PD";

	/**
	 * 对商户key做md5
	 */
	private static SecretKeySpec secretKey = new SecretKeySpec(MD5Util.MD5Encode(key, "UTF-8").toLowerCase().getBytes(),
			ALGORITHM);

	/**
	 * AES加密
	 * 
	 * @param data
	 * @return
	 * @throws Exception
	 */
	public static String encryptData(String data) throws Exception {
		Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
		// 创建密码器
		Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING);
		// 初始化
		cipher.init(Cipher.ENCRYPT_MODE, secretKey);
		return Base64Util.encode(cipher.doFinal(data.getBytes()));
	}

	/**
	 * AES解密
	 * 
	 * @param base64Data
	 * @return
	 * @throws Exception
	 */
	public static String decryptData(String base64Data) throws Exception {
		Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
		// 创建密码器
		Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING);
		// 初始化
		cipher.init(Cipher.DECRYPT_MODE, secretKey);
		return new String(cipher.doFinal(Base64Util.decode(base64Data)));
	}

	public static void main(String[] args) throws Exception {
		// String A 为测试字符串,是微信返回过来的退款通知字符串
		String A = "N0lJ0LWjHQlVOTEYdlRoVDbqWq+tRo9ZzvcwvIGHjFN6pYmHEX44W9l/jlyw8cwHueWk3m4hpldha73MgmCZJUu5LBZv27y/Vx2RkvHKCkiI5mV9pqQxiJmZTB1PN6s2wT3EVN1BFciGNzhozqQNIOyn/B9VOVKXkTeh1to8nI/UFVDexDE4ZyMBoB9oCQVcnkAuPaWqibHMU7i0iapB1UEMYJCgRKza9OGtvs0WbqIRgVVhtFxpxMhHmIaxzvH0JrdD/iOAYEV/NxUkye2HNJahatcYFBFQlbrTTBJ67MXZ6NzwFaOqqQYxZAKKEDrU++zu7hhX0lC5rZ5Uoyavn/sYTK3ZoCgAg/6O+S/f9sg+FoD8BrZmxC6tZwTfkDGsEO09m/JSTDxgU9ToCypyQt+bCxIFhLkGt8wKAtIEo0VOrfT9yMvHyBNLHtvNXj9gTQP+bMtpWAr0iMNgLwyXC2KY2FVxLmBEAnIIcXw7W15QItPcNVpQZceZooZwXn3QT6D4QDoHyg7ymHiAbtax0xHeYVGuGDB95E22q5C1Hh1a+7nyqkkJm1tzgJwyU+hhCw3Kw0Sj4JJVoLn6hFIBmVrDHf9x7j6VBULZ+39zk2upEudu/2TU1QVx96RCMW2O8EKXthPjzoOzZh1KeBsdkodrSn6gpBRNhIdbeimyAANVTyN+eeHThdx4tgEhodr9nVawFCSnD7jajowwaABFv/5AeWXSohfbbxAVrghNCjsfR4Grybr9fb6wB7hJ2yPZIKgdf8nGa9B7joKfZl2N7xIRawhGAVR3RRC2ajBEiabqaNhBCvhHPzR75oXViUL5OzVSyYznvrE1JEIgtGSN0rI/hUBIxhnTEv/X9C3NWiYRWoLMt29vJbQlk9hgQHVnTpH00khjXe8tdtMIkY7FUJmIsZH8D0jDMAvDj00Zl6r5z7FsyzRR+0xNsiyj8BPAxmSLqyrvXtgYx91N8I16TsgEBPJACL7tHkUr+kjQNXNzRp32mJFkB7/ZNQaXH8cm5aUAFk9eCuQAD4GqQxoYHOs/L7q2WMdajxPPxQSO6JU=";
		String B = AESUtil.decryptData(A);
		System.out.println(B);
	}

}


猜你喜欢

转载自blog.csdn.net/dc282614966/article/details/80862831