微信申请退款接口及调用模板消息接口

最近在做个微信公众号的项目,项目一期功能较为简单,跨境充值系统,齐核心为微信和运营商的接口,业务并不复杂,时间很紧,设计完成之后就要开发,

测试加开发时间才8天。下面总结下调用微信申请退款接口,即模板消息接口。。

1.下载双向证书,微信api文档上有详细说明:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3

2.随即字符串的生成和签名算法的生成,还要注意总金额和退款金额的单位只能为分


/**
* 生成随即字符串
* @return 
*/
public static String random_Str()
{
StringBuffer sb = new StringBuffer();
String str = "abcdefghijklmnopqrstuvwxyz0123456789";
Random ran = new Random();
while(sb.length() > 32)
{
int index = ran.nextInt(36);
char ch = str.charAt(index);
sb.append(ch);
}
return sb.toString();
}


/**
* 生成签名
* @param params 数据集合
* @param key  秘钥
* @return
*/
public static String getSign(Map<String, Object> params, String key)
{
String sign = "";
List<String> list = new ArrayList<String>();
for(Map.Entry<String, Object> entry : params.entrySet())
{
list.add(entry.getKey() + "=" + entry.getValue() + "&");
}
if(0 != list.size())
{
String[] dataSort = list.toArray(new String[list.size()]);
Arrays.sort(dataSort, String.CASE_INSENSITIVE_ORDER);
StringBuffer sb = new StringBuffer();
for(String str : dataSort)
{
sb.append(str);
}
sign = MD5Util.MD5Encode((sb.toString() + "key=" + key), "UTF-8");
}
return sign.toUpperCase();
}


/**
* 人民币金额元转分

* @param amount
* @return
*/
public static String changeY2F(String amount) {
String currency = amount.replaceAll("\\$|\\¥|\\,", ""); // 处理包含, ¥
// 或者$的金额
int index = currency.indexOf(".");
int length = currency.length();
Long amLong = 0l;
if (index == -1) {
amLong = Long.valueOf(currency + "00");
} else if (length - index >= 3) {
amLong = Long.valueOf((currency.substring(0, index + 3)).replace(".", ""));
} else if (length - index == 2) {
amLong = Long.valueOf((currency.substring(0, index + 2)).replace(".", "") + 0);
} else {
amLong = Long.valueOf((currency.substring(0, index + 1)).replace(".", "") + "00");
}
return amLong.toString();
}


3,下面开始组装参数,并调用微信申请退款接口

//组装请求参数
String data = ReqHelper.toXml("xml", params);
LOGGER.info("组装好的请求参数:" + data);
HttpEntity httpEntity = new StringEntity(data, "UTF-8");
HttpPost httpPost = new HttpPost(ConstantUtil.REFUNDADDRESS);
//设置请求和传输的超时时间
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(1000*60)
.setConnectTimeout(1000*60).build();
httpPost.setConfig(requestConfig);

Map<String, Object> helper = ReqHelper.postHelper();
for(String str : helper.keySet())
{
httpPost.addHeader(str, String.valueOf(helper.get(str)));
}//加上请求头,稍后会在下面的贴出代码
httpPost.setEntity(httpEntity);
inputStream = new FileInputStream(file);

//构造证书对象

//mch_id为商户号

String keySecret = params.get("mch_id").toString();
KeyStore keyStore  = KeyStore.getInstance("PKCS12");
keyStore.load(inputStream, keySecret.toCharArray());
SSLContext sslContext = SSLContexts.custom().
loadKeyMaterial(keyStore, keySecret.toCharArray()).build();
SSLConnectionSocketFactory sslConnectionSocketFactory = new 
SSLConnectionSocketFactory(sslContext, new String[] { "TLSv1" }, 
null, SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
httpClient = HttpClients.custom().
setSSLSocketFactory(sslConnectionSocketFactory).build();

4,下面调用接口

httpResponse = httpClient.execute(httpPost);
if(httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK)
{
HttpEntity responseEntity = httpResponse.getEntity();
if(null != responseEntity)
{
String result = EntityUtils.toString(responseEntity, "UTF-8");
LOGGER.info("调用退款申请接口返回的xml---->" + result);
//下面这一步只是我把申请退款接口返回的信息,解析成bean
ApplyRefund applyRefund = ReqHelper.analysisResponseMessage(result);
wxRefundDaoImpl.insertRefundInfo(applyRefund);//入库
Map<String, TemplateData> map = null;
//会返回两个状态,return_code为发送成功,result_code为申请退款成功
if("SUCCESS".equalsIgnoreCase(applyRefund.getReturn_code()) &&
"SUCCESS".equalsIgnoreCase(applyRefund.getResult_code()))
{
//这不只是更新状态
wxRefundDaoImpl.updateOrderInfo(R.REFUND_PROCEED.getRstCode(), applyRefund.getTransaction_id());
//下面是我写的模板消息接口传入参数,具体格式详见微信模板消息接口api,下面贴出地址
map = new LinkedHashMap<String, TemplateData>();
map.put("first", new TemplateData("退款成功通知","#173177"));
map.put("refund_id", new TemplateData("1235548841313","#173177"));
map.put("settlement_refund_fee_$n", new TemplateData("25","#173177"));
map.put("settlement_refund_fee_$n", new TemplateData(
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()),"#173177"));
map.put("remark", new TemplateData("您的零钱已到账,注意查收","#173177"));
}
else
{
map.put("first", new TemplateData("退款失败通知","#173177"));
map.put("refund_id", new TemplateData("1235548841313","#173177"));
map.put("settlement_refund_fee_$n", new TemplateData("25","#173177"));
map.put("settlement_refund_fee_$n", new TemplateData(
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()),"#173177"));
map.put("remark", new TemplateData("您的零钱退款失败,稍后会重新发起退款。","#173177"));
}
templateInfoImpl.requestTemplate(map);//发送模板消息
}
else
{
LOGGER.info("调用微信申请退款接口返回的信息为空");
throw new BusinessException("调用微信申请退款接口返回的信息为空", R.WEINXIN_CALL_REFUND);
}
}
else
{
LOGGER.info("调用微信申请退款接口异常,返回码:" + httpResponse.getStatusLine().getStatusCode());
throw new BusinessException("调用微信申请退款接口异常,返回码:" + httpResponse.getStatusLine().getStatusCode(), R.WEINXIN_CALL_REFUND);
}

}


下面贴出完整代码

申请退款接口实现

/**
 * 处理退款请求
 * @author liuyc
 *
 */
@Service("wxInterfaceRefundImpl")
public class WXInterfaceRefundImpl implements WXInterfaceRefund {


private static final Logger LOGGER = Logger.getLogger(WXInterfaceRefundImpl.class);

@Resource (name = "wxRefundDaoImpl")
private WXRefundDao wxRefundDaoImpl;

@Resource (name = "templateInfoImpl")
private TemplateInfo templateInfoImpl;

/**
* 调用微信退款接口
* @throws KeyStoreException 
* @throws FileNotFoundException 
*/
@Override
public void refund(Map<String, Object> params) {
LOGGER.info("开始调用微信退款接口-----------");
FileInputStream inputStream = null;
CloseableHttpClient httpClient = null;
CloseableHttpResponse httpResponse = null;
try {
if(params == null || params.isEmpty()){
LOGGER.error("调用微信退款接口的参数为空");
throw new BusinessException("调用微信退款接口的参数为空", R.WEINXIN_CALL_REFUND);
}
File file = new File(ConstantUtil.CERTIFICATE_P12);
if(!file.isFile()){
LOGGER.error("加载证书文件错误");
throw new BusinessException("加载证书文件错误", R.WEINXIN_CALL_REFUND);
}
params.put("nonce_str", WXUtils.random_Str());
params.put("sign", WXUtils.getSign(params, ConstantUtil.KEYSECRET));
params.put("total_fee", WXUtils.changeY2F(String.valueOf(params.get("total_fee"))));
params.put("refund_fee", WXUtils.changeY2F(String.valueOf(params.get("refund_fee"))));


//组装请求参数
String data = ReqHelper.toXml("xml", params);
LOGGER.info("组装好的请求参数:" + data);
HttpEntity httpEntity = new StringEntity(data, "UTF-8");
HttpPost httpPost = new HttpPost(ConstantUtil.REFUNDADDRESS);
//设置请求和传输的超时时间
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(1000*60)
.setConnectTimeout(1000*60).build();
httpPost.setConfig(requestConfig);

Map<String, Object> helper = ReqHelper.postHelper();
for(String str : helper.keySet())
{
httpPost.addHeader(str, String.valueOf(helper.get(str)));
}
httpPost.setEntity(httpEntity);
inputStream = new FileInputStream(file);
//第二个参数为证书密码,默认为商户id
String keySecret = params.get("mch_id").toString();
KeyStore keyStore  = KeyStore.getInstance("PKCS12");
keyStore.load(inputStream, keySecret.toCharArray());
SSLContext sslContext = SSLContexts.custom().
loadKeyMaterial(keyStore, keySecret.toCharArray()).build();
SSLConnectionSocketFactory sslConnectionSocketFactory = new 
SSLConnectionSocketFactory(sslContext, new String[] { "TLSv1" }, 
null, SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
httpClient = HttpClients.custom().
setSSLSocketFactory(sslConnectionSocketFactory).build();
httpResponse = httpClient.execute(httpPost);
if(httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK)
{
HttpEntity responseEntity = httpResponse.getEntity();
if(null != responseEntity)
{
String result = EntityUtils.toString(responseEntity, "UTF-8");
LOGGER.info("调用退款申请接口返回的xml---->" + result);
//下面这一步只是我把申请退款接口返回的信息,解析成bean
ApplyRefund applyRefund = ReqHelper.analysisResponseMessage(result);
wxRefundDaoImpl.insertRefundInfo(applyRefund);//入库
Map<String, TemplateData> map = null;
//会返回两个状态,return_code为发送成功,result_code为申请退款成功
if("SUCCESS".equalsIgnoreCase(applyRefund.getReturn_code()) &&
"SUCCESS".equalsIgnoreCase(applyRefund.getResult_code()))
{
//这不只是更新状态
wxRefundDaoImpl.updateOrderInfo(R.REFUND_PROCEED.getRstCode(), applyRefund.getTransaction_id());
//下面是我写的模板消息接口传入参数,具体格式详见微信模板消息接口api,下面贴出地址
map = new LinkedHashMap<String, TemplateData>();
map.put("first", new TemplateData("退款成功通知","#173177"));
map.put("refund_id", new TemplateData("1235548841313","#173177"));
map.put("settlement_refund_fee_$n", new TemplateData("25","#173177"));
map.put("settlement_refund_fee_$n", new TemplateData(
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()),"#173177"));
map.put("remark", new TemplateData("您的零钱已到账,注意查收","#173177"));
}
else
{
map.put("first", new TemplateData("退款失败通知","#173177"));
map.put("refund_id", new TemplateData("1235548841313","#173177"));
map.put("settlement_refund_fee_$n", new TemplateData("25","#173177"));
map.put("settlement_refund_fee_$n", new TemplateData(
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()),"#173177"));
map.put("remark", new TemplateData("您的零钱退款失败,稍后会重新发起退款。","#173177"));
}
templateInfoImpl.requestTemplate(map);//发送模板消息
}
else
{
LOGGER.info("调用微信申请退款接口返回的信息为空");
throw new BusinessException("调用微信申请退款接口返回的信息为空", R.WEINXIN_CALL_REFUND);
}
}
else
{
LOGGER.info("调用微信申请退款接口异常,返回码:" + httpResponse.getStatusLine().getStatusCode());
throw new BusinessException("调用微信申请退款接口异常,返回码:" + httpResponse.getStatusLine().getStatusCode(), R.WEINXIN_CALL_REFUND);
}

}  catch (Exception e) {
LOGGER.error(e.getLocalizedMessage());
throw new SystemException(e.getLocalizedMessage(), R.WEINXIN_CALL_REFUND);
}
finally
{
try {
if(null != inputStream)
{
inputStream.close();
}
if(null != httpResponse)
{
httpResponse.close();
}
if(null != httpClient)
{
httpClient.close();
}
} catch (IOException e) {
LOGGER.error(e.getLocalizedMessage());
throw new SystemException(e.getLocalizedMessage(), R.WEINXIN_CALL_REFUND);
}
}
}

工具类

public class WXUtils {


/**
* 生成随即字符串
* @return 
*/
public static String random_Str()
{
StringBuffer sb = new StringBuffer();
String str = "abcdefghijklmnopqrstuvwxyz0123456789";
Random ran = new Random();
while(sb.length() > 32)
{
int index = ran.nextInt(36);
char ch = str.charAt(index);
sb.append(ch);
}
return sb.toString();
}

/**
* 生成签名
* @param params 数据集合
* @param key  秘钥
* @return
*/
public static String getSign(Map<String, Object> params, String key)
{
String sign = "";
List<String> list = new ArrayList<String>();
for(Map.Entry<String, Object> entry : params.entrySet())
{
list.add(entry.getKey() + "=" + entry.getValue() + "&");
}
if(0 != list.size())
{
String[] dataSort = list.toArray(new String[list.size()]);
Arrays.sort(dataSort, String.CASE_INSENSITIVE_ORDER);
StringBuffer sb = new StringBuffer();
for(String str : dataSort)
{
sb.append(str);
}
sign = MD5Util.MD5Encode((sb.toString() + "key=" + key), "UTF-8");
}
return sign.toUpperCase();
}

/**
* 人民币金额元转分

* @param amount
* @return
*/
public static String changeY2F(String amount) {
String currency = amount.replaceAll("\\$|\\¥|\\,", ""); // 处理包含, ¥
// 或者$的金额
int index = currency.indexOf(".");
int length = currency.length();
Long amLong = 0l;
if (index == -1) {
amLong = Long.valueOf(currency + "00");
} else if (length - index >= 3) {
amLong = Long.valueOf((currency.substring(0, index + 3)).replace(".", ""));
} else if (length - index == 2) {
amLong = Long.valueOf((currency.substring(0, index + 2)).replace(".", "") + 0);
} else {
amLong = Long.valueOf((currency.substring(0, index + 1)).replace(".", "") + "00");
}
return amLong.toString();
}
}



/**
 * 组装和解析响应信息
 * @author liuyc
 *
 */
public class ReqHelper {


/**
* 模拟浏览器post提交
* @return
*/
public static Map<String, Object> postHelper()
{
Map<String, Object> helper = new HashMap<String, Object>();
helper.put("Content-Type", "text/xml");
helper.put("Connection", "keep-alive");
helper.put("Accept", "*/*");
helper.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
helper.put("Host", "api.mch.weixin.qq.com");
helper.put("X-Requested-With", "XMLHttpRequest");
helper.put("Cache-Control", "max-age=0");
helper.put("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0)");
return helper;
}

/**
* 把数据组装成xml格式 
* @param rootName 根元素name
* @param postData 请求数据集合
* @return
*/
@SuppressWarnings("rawtypes")
public static String toXml(String rootName, Map<String, Object> postData)
{
Element root = DocumentHelper.createElement(rootName);
Document document = DocumentHelper.createDocument(root);
Iterator iterator = postData.keySet().iterator();
while(iterator.hasNext())
{
String key = (String) iterator.next();
String value = (String) postData.get(key);
Element element = root.addElement(key);
element.addCDATA(value.isEmpty() ? "" : value);
}
return document.asXML();
}

/**
* 解析申请退款返回的结果
* @throws DocumentException 
* @throws UnsupportedEncodingException 
*/
@SuppressWarnings("rawtypes")
public static ApplyRefund analysisResponseMessage(String message) throws UnsupportedEncodingException, DocumentException
{
ApplyRefund applyRefund = new ApplyRefund();
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(new ByteArrayInputStream(message.getBytes("UTF-8")));
Element root = document.getRootElement();
for(Iterator iter = root.elementIterator();iter.hasNext();)
{
Element element = (Element) iter.next();
if("return_code".equals(element.getName()))
{
applyRefund.setReturn_code(element.getTextTrim());
}
if("return_msg".equals(element.getName()))
{
applyRefund.setReturn_msg(element.getTextTrim());
}
if("result_code".equals(element.getName()))
{
applyRefund.setResult_code(element.getTextTrim());
}
if("err_code".equals(element.getName()))
{
applyRefund.setErr_code(element.getTextTrim());
}
if("err_code_des".equals(element.getName()))
{
applyRefund.setErr_code_des(element.getTextTrim());
}
if("appid".equals(element.getName()))
{
applyRefund.setAppid(element.getTextTrim());
}
if("mch_id".equals(element.getName()))
{
applyRefund.setMch_id(element.getTextTrim());
}
if("device_info".equals(element.getName()))
{
applyRefund.setDevice_info(element.getTextTrim());
}
if("nonce_str".equals(element.getName()))
{
applyRefund.setNonce_str(element.getTextTrim());
}
if("sign".equals(element.getName()))
{
applyRefund.setSign(element.getTextTrim());
}
if("transaction_id".equals(element.getName()))
{
applyRefund.setTransaction_id(element.getTextTrim());
}
if("out_trade_no".equals(element.getName()))
{
applyRefund.setOut_trade_no(element.getTextTrim());
}
if("out_refund_no".equals(element.getName()))
{
applyRefund.setOut_refund_no(element.getTextTrim());
}
if("refund_id".equals(element.getName()))
{
applyRefund.setRefund_id(element.getTextTrim());
}
if("refund_channel".equals(element.getName()))
{
applyRefund.setRefund_channel(element.getTextTrim());
}
if("refund_fee".equals(element.getName()))
{
applyRefund.setRefund_fee(element.getTextTrim());
}
if("total_fee".equals(element.getName()))
{
applyRefund.setTotal_fee(element.getTextTrim());
}
if("fee_type".equals(element.getName()))
{
applyRefund.setFee_type(element.getTextTrim());
}
if("cash_fee".equals(element.getName()))
{
applyRefund.setCash_fee(element.getTextTrim());
}
if("cash_refund_fee".equals(element.getName()))
{
applyRefund.setCash_refund_fee(element.getTextTrim());
}
}
return applyRefund;
}


发送模板消息接口


@Override
public void requestTemplate(Map<String, TemplateData> data) {
CloseableHttpClient client = null;
CloseableHttpResponse  response = null;
Template template = new Template();
template.setTouser(TemplateUtils.TOUSER);//openid
template.setTemplate_id(TemplateUtils.TEMPLATE_ID);//模板id
template.setUrl(TemplateUtils.URL);//服务器地址
template.setData(data);

try {
JSONObject jsonObject = JSONObject.fromObject(template);
LOGGER.info("发送给模板消息参数---->" + jsonObject.toString());
client = HttpClientBuilder.create().build(); 
HttpEntity httpEntity = new StringEntity(jsonObject.toString(), "UTF-8");
HttpPost httpPost = new HttpPost(TemplateUtils.TEMPLATEMESSAGEADDRESS);
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(1000*60)
.setConnectTimeout(1000*60).build();
httpPost.setConfig(requestConfig);
httpPost.setEntity(httpEntity);

response =  client.execute(httpPost);
if(response.getStatusLine().getStatusCode() == HttpStatus.SC_OK)
{
HttpEntity responseEntity = response.getEntity();
if(null != responseEntity)
{
String info = EntityUtils.toString(responseEntity,"UTF-8");
JSONObject object = JSONObject.fromObject(info);
if("0".equals(object.get("errcode")) &&
"ok".equals(object.get("errmsg")))
{
LOGGER.info("发送模板消息成功");
}
else
{
LOGGER.info("发送模板消息失败,错误码:" + object.get("errcode"));
throw new BusinessException("发送模板消息失败,错误码:" + object.get("errcode"), R.TEMPLATE_CALL_INFO);
}
}
else
{
LOGGER.info("发送模板消息返回实体为空");
throw new BusinessException("发送模板消息返回实体为空", R.TEMPLATE_CALL_INFO);
}
}
else
{
LOGGER.info("调用模板消息接口异常,返回码:" + response.getStatusLine().getStatusCode());
throw new BusinessException("调用模板消息接口异常,返回码:" + response.getStatusLine().getStatusCode(), R.TEMPLATE_CALL_INFO);
}
} catch (Exception e) {
LOGGER.info(e.getLocalizedMessage());
throw new SystemException(e.getLocalizedMessage(), R.TEMPLATE_CALL_INFO);
} finally
{
try {
if(null != response)
{
response.close();
}
if(null != client)
{
client.close();
}
} catch (IOException e) {
LOGGER.info(e.getLocalizedMessage());
throw new SystemException(e.getLocalizedMessage(), R.TEMPLATE_CALL_INFO);
}
}

具体的模板,需要认证公众号后申请,也可以通过接口的方式。

退款查询接口这里没有列出来, 也可以在上诉代码中加上,陪个定时任务,每隔一段时间去请求退款查询接口,

退款查询接口有一定的延迟,详见api,

https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4


https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1433751277&token=&lang=zh_CN



写的有瑕疵请勿见外,谢谢,谢谢。


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值