2016年5月6日有更新,请参考第三部分。
因为是自己开发了一个app应用,没资格去申请微信支付和支付宝支付,于是就采用了银联支付功能,银联支付分为了两种环境:测试环境和生产环境,一般前期开发的时候都是使用测试环境,数据都是测试数据,不会发生真实交易。第一次做Android项目+IDE为Android Studio+第一次集成支付功能,所以个人觉得整个集成过程可能有点复杂,而且银联支付产品众多:网关支付产品、手机控件支付、手机网页支付等等,第一次看的时候真是云里雾里,不知道选哪个,不知道他们的区别,最后自己选择了手机控件支付,先试试,光看没有用。自己做了多少写多少,好记性不如烂笔头,就怕后面想记录的时候忘记了前面的过程。
第一步:注册;
第二步:然后在帮助中心界面的产品分类下载里选择手机控件支付;
第三步:下载安卓版的开发包。
个人建议可以先把服务器端的工程跑一下,这样结合代码的时候就很容易明白怎么集成到自己的工程里了。所以这一小部分内容是官方Demo的运行情况,需要修改的配置很少,但是还是有小地方需要调整一下。
将这三个路径修改为测试环境证书的路径(证书在assets文件夹下),可以使用相对路径或者绝对路径都行,下面图中是绝对路径,我把assets/测试环境证书下的三个文件移动到了C盘。
我的测试环境证书地址:
先讲服务器端,因为自己也才完成了这部分工作。运行了官方Demo以及相关说明文档后,整体思路其实就有了。
首先试试配置!!!
将Demo工程lib中的jar包复制到自己工程的lib里(如果已经有jar包了,就不需要复制了)
第一部分 controller对应于Demo中的ACPSample_AppServer\src\com\unionpay\acp\demo,其中PayController是我自己写的Controller。
第二部分model对应于Demo中的ACPSample_AppServer\src\com\unionpay\acp\sdk
第三部分的两个java文件对应于Demo中的\ACPSample_AppServer\src\web中的两个java文件
<servlet>
<servlet-name>autoLoadServlet</servlet-name>
<servlet-class>com.XXX.component.pay.AutoLoadServlet</servlet-class>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>autoLoadServlet</servlet-name>
<url-pattern>/autoLoadServlet</url-pattern>
</servlet-mapping>
ok,配置过程就算完事了,现在就该写PayController来接受请求了。
@Controller
public class PayController extends BasicController{
@RequestMapping(value = "/pay/pay")
@ResponseBody
public void pay(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {
request.setCharacterEncoding(DemoBase.encoding_UTF8);//
response.setContentType("text/html; charset="+ DemoBase.encoding_UTF8);//这两句是我临时加的,因为出现了乱码
Map<String, String> contentData = new HashMap<String, String>();
/***银联全渠道系统,产品参数,除了encoding自行选择外其他不需修改***/
contentData.put("version", DemoBase.version); //版本号 全渠道默认值
contentData.put("encoding", DemoBase.encoding_UTF8); //字符集编码 可以使用UTF-8,GBK两种方式
contentData.put("signMethod", "01"); //签名方法 目前只支持01:RSA方式证书加密
contentData.put("txnType", "01"); //交易类型 01:消费
contentData.put("txnSubType", "01"); //交易子类 01:消费
contentData.put("bizType", "000201"); //填写000201
contentData.put("channelType", "08"); //渠道类型 08手机
String merId = request.getParameter("merId");
String txnAmt = request.getParameter("txnAmt");
String orderId = request.getParameter("orderId");
String txnTime = request.getParameter("txnTime");
/***商户接入参数***/
contentData.put("merId", merId); //商户号码,请改成自己申请的商户号或者open上注册得来的777商户号测试
contentData.put("accessType", "0"); //接入类型,商户接入填0 ,不需修改(0:直连商户, 1: 收单机构 2:平台商户)
contentData.put("orderId", orderId); //商户订单号,8-40位数字字母,不能含“-”或“_”,可以自行定制规则
contentData.put("txnTime", txnTime); //订单发送时间,取系统时间,格式为YYYYMMDDhhmmss,必须取当前时间,否则会报txnTime无效
contentData.put("accType", "01"); //账号类型 01:银行卡02:存折03:IC卡帐号类型(卡介质)
contentData.put("txnAmt", txnAmt); //交易金额 单位为分,不能带小数点
contentData.put("currencyCode", "156"); //境内商户固定 156 人民币
//contentData.put("reqReserved", "透传字段"); //商户自定义保留域,交易应答时会原样返回
//后台通知地址(需设置为外网能访问 http https均可),支付成功后银联会自动将异步通知报文post到商户上送的该地址,【支付失败的交易银联不会发送后台通知】
//后台通知参数详见open.unionpay.com帮助中心 下载 产品接口规范 网关支付产品接口规范 消费交易 商户通知
//注意:1.需设置为外网能访问,否则收不到通知 2.http https均可 3.收单后台通知后需要10秒内返回http200或302状态码
// 4.如果银联通知服务器发送通知后10秒内未收到返回状态码或者应答码非http200或302,那么银联会间隔一段时间再次发送。总共发送5次,银联后续间隔1、2、4、5 分钟后会再次通知。
// 5.后台通知地址如果上送了带有?的参数,例如:http://abc/web?a=b&c=d 在后台通知处理程序验证签名之前需要编写逻辑将这些字段去掉再验签,否则将会验签失败
contentData.put("backUrl", DemoBase.backUrl);//[其实还没搞明白这个地址的作用]
/**对请求参数进行签名并发送http post请求,接收同步应答报文**/
Map<String, String> reqData = AcpService.sign(contentData,DemoBase.encoding_UTF8); //报文中certId,signature的值是在signData方法中获取并自动赋值的,只要证书配置正确即可。
String requestAppUrl = SDKConfig.getConfig().getAppRequestUrl(); //交易请求url从配置文件读取对应属性文件acp_sdk.properties中的 acpsdk.backTransUrl
Map<String, String> rspData = AcpService.post(reqData,requestAppUrl,DemoBase.encoding_UTF8); //发送请求报文并接受同步应答(默认连接超时时间30秒,读取返回结果超时时间30秒);这里调用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)){
//成功,获取tn号
//String tn = resmap.get("tn");//这里应该是rspData.get("tn");
//TODO
}else{
//其他应答码为失败请排查原因或做失败处理
//TODO
}
}else{
LogUtil.writeErrorLog("验证签名失败");
//TODO 检查验证签名失败的原因
}
}else{
//未返回正确的http状态
LogUtil.writeErrorLog("未获取到返回报文或返回http状态码非200");
}
String reqMessage = DemoBase.genHtmlResult(reqData);
String rspMessage = DemoBase.genHtmlResult(rspData);
try {
response.getWriter().write("请求报文:<br/>"+reqMessage+"<br/>" + "应答报文:</br>"+rspMessage+"");
//response.getOutputStream().write(("请求报文:<br/>"+reqMessage+"<br/>" + "应答报文:</br>"+rspMessage+"").getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
然后浏览器输入(get方式,在Form05_6_2_AppConsume.java中get和post方式是一样的,我的PayController因为没有指定请求方式,所以两种方式也是兼容的,而且只是为了测试采用的get,后面把客户端搭好,就用post方式)
:http://123.XX.XXX.127:8080/intveh/pay/pay?merId=777290058110048&txnTime=20160505092851&orderId=20160505092851&txnAmt=5
效果和官方Demo一样:
目前就把服务器端的tn获取搭好了,还有退款退货等等以及客户端的搭建,接着搞~~
服务器端还应该有“后台通知接收处理”部分。可参见Demo中ACPSample_AppServer\src\com\unionpay\acp\demo\BackRcvResponse.java。这部分的功能很重要:见下图所示(银联支付流程图)
第七步是银联后台将支付结果返回给我们的服务器端,我们将根据返回结果更新订单的状态,请注意:虽然第八步是将客户端的支付控件也会支付结果,但是能作为订单支付结果的只有后台的通知,官方文档中有一处说的有歧义,客服是这样说的:
关于客户端支付结果+验签,在下一篇文档中说明。
请注意:这个backUrl必须填写真实ip地址,回路地址不行、localhost不行,银联后台返回通知必须能post到你的backUrl。
/** * 后台通知处理 * @param sign * @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("验证签名结果[成功].");
//【注:为了安全验签成功才应该写商户的成功处理逻辑】交易成功,更新商户订单状态
//valideData里封装了很多数据,可参考官方文档,做相应后续处理
}
}
LogUtil.writeLog("BackRcvResponse接收后台通知结束");
//返回给银联服务器http 200 状态码
response.getWriter().print("ok");
}
catch(Exception e){}
}
ok,backUrl配置正确,那么进行支付后,商户后台就会得到银联后台的支付通知,如下图:
这部分工作不能少,一定要记住后台通知处理的作用:真正作为接收订单支付结果的地方。