一.前言
之前已经将银联支付功能进行了集成(服务器端戳这里,客户端戳这里),暂时将退款功能搁下了,今天抽了一小段光阴把这个洞给补上了。
其实有了上一次集成支付功能的经验,对退货退款的集成就很容易实现了。本文只讲服务器端的处理,客户端根据需求写好就行。
银联官方提供了一个退货退款流程图:
所以过程主要是:服务器端组织好请求报文->银联系统进行处理->将受理结果和处理结果返回给服务器。
二.实现
我在代码中做了一些注释,所以看完代码和注释基本就没问题了。前提条件依旧是,完成好各项配置工作,可以参考服务器端的博文。
只是请注意一点:银联支付成功后会返回一个流水号,该流水号是后续操作的输入(退货、退款、查询支付状态等操作)而不是订单号(原因很简单啊,订单号是我们按一定规则生成的,银联系统肯定不认),所以必须将该流水号和我们需要操作的订单进行绑定,当然最好的方式就是在订单表里增加一个流水号字段。
1.第一步
组织请求报文,向银联后台发起退货退款请求。
/**
* 退款流程
* @param orderId //需要退货退款的订单ID
* @param request
* @param response
* @throws UnsupportedEncodingException
*/
@RequestMapping(value = "/pay/refund/{orderId}")
@ResponseBody
public JSONObject refund(@PathVariable("orderId") String orderId,HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException
{
//防止乱码,根据业务需求,这两句可有可无
request.setCharacterEncoding(DemoBase.encoding_UTF8);
response.setContentType("text/html; charset="+ DemoBase.encoding_UTF8);
//json用于将数据返回给客户端
JSONObject json = new JSONObject();
System.out.println("退款开始");
//获得该订单的信息
Order order = orderDAO.getOrder(orderId);
if(order==null)
{
json.put("result", "0");
return json;
}
//获得该订单的流水号
String orderQueryId = order.getOrderQueryId();
//获得该订单的总价
String orderOilTotalPrice = order.getOrderOilTotalPrice();
Map<String, String> data = new HashMap<String, String>();
/***银联全渠道系统,产品参数,除了encoding自行选择外其他不需修改***/
data.put("version", DemoBase.version); //版本号
data.put("encoding", DemoBase.encoding_UTF8); //字符集编码 可以使用UTF-8,GBK两种方式
data.put("signMethod", "01"); //签名方法 目前只支持01-RSA方式证书加密
data.put("txnType", "04"); //交易类型 04-退货
data.put("txnSubType", "00"); //交易子类型 默认00
data.put("bizType", "000201"); //业务类型
data.put("channelType", "08"); //渠道类型,07-PC,08-手机
/***商户接入参数***/
data.put("merId", DemoBase.merId); //商户号码,请改成自己申请的商户号或者open上注册得来的777商户号测试
data.put("accessType", "0"); //接入类型,商户接入固定填0,不需修改
//一定要注意,该orderId并不是我们自己的订单id,而是退款申请这条业务的id,银联提供的DemoBase.getOrderId()是根据系统时间生成的。
data.put("orderId", DemoBase.getOrderId()); //商户订单号,8-40位数字字母,不能含“-”或“_”,可以自行定制规则,重新产生,不同于原消费
data.put("txnTime", DemoBase.getCurrentTime()); //订单发送时间,格式为YYYYMMDDhhmmss,必须取当前时间,否则会报txnTime无效
data.put("currencyCode", "156"); //交易币种(境内商户一般是156 人民币)
//一定注意,退款金额必须是整数,而且单位是分。
data.put("txnAmt", Double.valueOf(orderOilTotalPrice).intValue()*100+""); //****退货金额,单位分,不要带小数点。退货金额小于等于原消费金额,当小于的时候可以多次退货至退货累计金额等于原消费金额
//data.put("txnAmt",orderOilTotalPrice);
//这个透传字段很有用,因为前面的orderId已经不是我们自己的订单id了,而我们在后台通知里肯定还需要这个订单id,因为我们需要修改该订单的状态,可是data中根本不能put我们自己的订单id,那么这个字段就是用来存放一些我们想传递给后台通知的数据,因为data中的数据都会完整的全部返回给后台通知,我这里只是在透传字段里放了一个orderId,如果有更多的数据需要传递,只需要用map或者json存储数据,然后转成String就可以了
data.put("reqReserved", orderId); //请求方保留域,透传字段(可以实现商户自定义参数的追踪)本交易的后台通知,对本交易的交易状态查询交易、对账文件中均会原样返回,商户可以按需上传,长度为1-1024个字节
//后台通知地址必须是真实ip,因为银联后台要将通知post到这个后台地址。
data.put("backUrl", DemoBase.backUrl); //后台通知地址,后台通知参数详见open.unionpay.com帮助中心 下载 产品接口规范 网关支付产品接口规范 退货交易 商户通知,其他说明同消费交易的后台通知
/***要调通交易以下字段必须修改***/
//流水号,这才是银联后台认识的标识符,该流水号是在支付成功后获得的。
data.put("origQryId", orderQueryId); //****原消费交易返回的的queryId,可以从消费交易后台通知接口中或者交易状态查询接口中获取
/**请求参数设置完毕,以下对请求参数进行签名并发送http post请求,接收同步应答报文------------->**/
Map<String, String> reqData = AcpService.sign(data,DemoBase.encoding_UTF8); //报文中certId,signature的值是在signData方法中获取并自动赋值的,只要证书配置正确即可。
String url = SDKConfig.getConfig().getBackRequestUrl(); //交易请求url从配置文件读取对应属性文件acp_sdk.properties中的 acpsdk.backTransUrl
Map<String, String> rspData = AcpService.post(reqData, url,DemoBase.encoding_UTF8);//这里调用signData之后,调用submitUrl之前不能对submitFromData中的键值对做任何修改,如果修改会导致验签不通过
/**对应答码的处理,请根据您的业务逻辑来编写程序,以下应答码处理逻辑仅供参考------------->**/
//应答码规范参考open.unionpay.com帮助中心 下载 产品接口规范 《平台接入接口规范-第5部分-附录》
if(!rspData.isEmpty()){
if(AcpService.validate(rspData, DemoBase.encoding_UTF8)){
LogUtil.writeLog("验证签名成功");
String respCode = rspData.get("respCode") ;
if(("00").equals(respCode)){
//交易已受理(不代表交易已成功),等待接收后台通知更新订单状态,也可以主动发起 查询交易确定交易状态。
//TODO
json.put("result", "1");
return json;
}else if(("03").equals(respCode)||
("04").equals(respCode)||
("05").equals(respCode)){
//后续需发起交易状态查询交易确定交易状态
//TODO
}else{
//其他应答码为失败请排查原因
//TODO
}
}else{
LogUtil.writeErrorLog("验证签名失败");
//TODO 检查验证签名失败的原因
}
}else{
//未返回正确的http状态
LogUtil.writeErrorLog("未获取到返回报文或返回http状态码非200");
}
json.put("result", "0");
return json;
}
2.第二步
后台通知地址中接收银联后台的处理结果通知,在上一篇中已经写了后台通知处理,这次加上退款功能后就稍微修改了一下。
/**
* 后台通知处理
* @param request
* @param response
*/
@RequestMapping(value = "/pay/backRcvResponse")
@ResponseBody
public void backRcvResponse(HttpServletRequest request, HttpServletResponse response)
{
System.out.println("后台通知验签开始");
//return AcpService.validateAppResponse(sign, DemoBase.encoding_UTF8);
//System.out.println("验签开始");
String encoding = request.getParameter(SDKConstants.param_encoding);
// 获取银联通知服务器发送的后台通知参数
Map<String, String> reqParam = Tool.getAllRequestParam(request);
LogUtil.printRequestLog(reqParam);
Map<String, String> valideData = null;
try
{
if (null != reqParam && !reqParam.isEmpty()) {
Iterator<Entry<String, String>> it = reqParam.entrySet().iterator();
valideData = new HashMap<String, String>(reqParam.size());
while (it.hasNext()) {
Entry<String, String> e = it.next();
String key = (String) e.getKey();
String value = (String) e.getValue();
value = new String(value.getBytes(encoding), encoding);
valideData.put(key, value);
}
}
//重要!验证签名前不要修改reqParam中的键值对的内容,否则会验签不过
if (!AcpService.validate(valideData, encoding)) {
LogUtil.writeLog("验证签名结果[失败].");
//验签失败,需解决验签问题
} else {
LogUtil.writeLog("验证签名结果[成功].");
//【注:为了安全验签成功才应该写商户的成功处理逻辑】交易成功,更新商户订单状态
//String orderId =valideData.get("orderId"); //获取后台通知的数据
//Order order = orderDAO.getOrder(orderId);
//获取交易类型,可参考官方文档
String txnType = valideData.get("txnType");
System.out.println(Integer.valueOf(txnType));
Order order;
switch(Integer.valueOf(txnType))//交易类型
{
case 01://消费
String orderId =valideData.get("orderId"); //获取后台通知的数据,其他字段也可用类似方式获取
String payTime = valideData.get("txnTime");
String orderQueryId = valideData.get("queryId");
//String respCode =valideData.get("respCode"); //获取应答码,收到后台通知了respCode的值一般是00,可以不需要根据这个应答码判断。
//if(orderId!=null&&!"".equals(orderId))
//{
order = orderDAO.getOrder(orderId);
if(order!=null)
{
System.out.println("更新支付状态:"+orderId);
System.out.println("payTime"+payTime);
order.setOrderPayStatus(1);
order.setOrderPayTime(payTime+".0");
order.setOrderQueryId(orderQueryId);
//order.setOrderPayTime(new SimpleDateFormat(" yyyy-MM-dd HH:mm:ss ").parse(payTime));
boolean sucess = orderDAO.update(order);
System.out.println("sucess"+sucess);
}
//}
break;
case 04://退货&退款
//获得透传字段的数据,我这里就是订单id
order = orderDAO.getOrder(valideData.get("reqReserved"));
if(order!=null)
{
//更新订单状态
order.setOrderPayStatus(2);
order.setOrderStatus(2);
orderDAO.update(order);
}
break;
default:
break;
}
}
LogUtil.writeLog("BackRcvResponse接收后台通知结束");
//返回给银联服务器http 200 状态码
response.getWriter().print("ok");
}
catch(Exception e){}
}
ok,还是看一下效果吧。respCode = 00表示成功。
三.问题
不会遇到问题是不可能的,当然按照我上面代码的实现的也只是能把基本流程跑通,从安全性上来讲,还差太多…
1.退款金额必须是整数
比如下面图中的情况:
2.data中的orderId不是自己的orderId,而是本次退款交易的id。
3.后台通知地址一定是真是ip,localhost不行,回路地址也不行。
时间: 2024-11-02 23:50:40