本人所在公司使用场景:用户在APP中开通免密支付以后消费都可以免密支付。使用银联无跳转支付需要先和银联签署协议申请商户,免密支付需要和银联申请,银联地址:https://open.unionpay.com/tjweb/acproduct/list?apiservId=449
此文中的代码是最初版的代码,仅供参考,需要根据自己实际情况进行调整。
最新代码不方便贴出。主要只给开通和支付的代码,回调和退款,公钥更新,对账都比较简单自己看着银联API就可以写出。
注意事项:1.每次调用银联接口时都要传订单号,并且唯一。
2.退款银联提供2个接口,1个是消费撤销接口,1个是退货接口;区别在于消费撤销只支持CUPS当日当批的消费,不是当日不支持,退货接口支持当日也支持非当日;所以直接选择退货接口就可以了。
3.如果在frontUrl或者backUrl这两个参数中增加额外参数,即?id=xxx;在回调时需要先将?后面的去掉,在进行验证才可以,不然会报错。
以下代码不是正式代码,仅供参考,需要根据自己项目需求进行更改。
银联无跳转支付有三种支付方式:银联侧开通支付、商户侧开通支付和直接支付。其中银联侧开通支付模式和商户侧开通支付模式根据是否通过银行卡对应的Token标识号发起支付,又分为标准版和Token版。我们使用的是银联侧token版(一般都是使用这种方式,有疑问可以咨询银联)
银联侧开通Token版是指银联全渠道系统首次支付或者开通时,通过跳转至银联的页面进行验证要素采集,送发卡行验证通过后,银联分配商户Token。我们将Token保存到数据库,下次进行消费时直接使用Token进行消费即可。
1.开通并获取token
@Override
public String tokenOpenCard(Integer clientId) {
String merId = SDKConfig.getConfig().getMerId();
String orderId = new SimpleDateFormat(“yyyyMMddHHmmssSSS”).format(new Date());// 订单号
String txnTime = new SimpleDateFormat(“yyyyMMddHHmmss”).format(new Date());// 订单时间例如20190424162903
Map
/*** 银联全渠道系统,产品参数,除了encoding自行选择外其他不需修改 /
contentData.put(“version”, SDKConfig.getConfig().getVersion()); // 版本号
contentData.put(“encoding”, DemoBase.encoding); // 字符集编码
contentData.put(“signMethod”, SDKConfig.getConfig().getSignMethod()); // 签名方法
contentData.put(“txnType”, “79”); // 交易类型 11-代收
contentData.put(“txnSubType”, “00”); // 交易子类型 00-默认开通
contentData.put(“bizType”, “000902”); // 业务类型 Token支付
contentData.put(“channelType”, “07”); // 渠道类型07-PC
/ 商户接入参数 ***/
contentData.put(“merId”, merId); // 商户号码(本商户号码仅做为测试调通交易使用,该商户号配置了需要对敏感信息加密)测试时请改成自己申请的商户号,【自己注册的测试777开头的商户号不支持代收产品】
contentData.put(“accessType”, “0”); // 接入类型,商户接入固定填0,不需修改
contentData.put(“orderId”, orderId); // 商户订单号,8-40位数字字母,不能含“-”或“_”,可以自行定制规则
contentData.put(“txnTime”, txnTime); // 订单发送时间,格式为YYYYMMDDhhmmss,必须取当前时间,否则会报txnTime无效
contentData.put(“accType”, “01”); // 账号类型
// 测试环境固定trId=62000000001&tokenType=01,生产环境由业务分配。测试环境因为所有商户都使用同一个trId,所以同一个卡获取的token号都相同,任一人发起更新token或者解除token请求都会导致原token号失效,所以之前成功、突然出现3900002报错时请先尝试重新开通一下。
contentData.put(“tokenPayData”, “{trId=62000000001&tokenType=01}”);
contentData.put(“encryptCertId”, AcpService.getEncryptCertId()); // 加密证书的certId,配置在acp_sdk.properties文件
contentData.put(“frontUrl”, SDKConfig.getConfig().getFrontUrl() + “?clientid=” + clientId);
contentData.put(“backUrl”, SDKConfig.getConfig().getBackUrl() + “?clientid=” + clientId);
contentData.put(“payTimeout”,
new SimpleDateFormat(“yyyyMMddHHmmss”).format(new Date().getTime() + 15 * 60 * 1000));
Map
String requestFrontUrl = SDKConfig.getConfig().getFrontRequestUrl(); // 获取请求银联的前台地址:对应属性文件acp_sdk.properties文件中的acpsdk.frontTransUrl
String html = AcpService.createAutoFormHtml(requestFrontUrl, reqData, DemoBase.encoding); // 生成自动跳转的Html表单
return html;
}
前端或者移动端将html代码输出即可
frontUrl和backUrl这两个分别是前台通知和后端通知,配置的时候必须需要外网可以访问的路径。
2.支付
@Override
public JsonResult> transactionToken(RefuelVo vo) {
JsonResult> cashRefuelResult = oilCardService.cashRefuel(vo);
Map
// Integer chargeOilRecordId = (Integer)
// resultCashRefuel.get(“recordId”);
Integer payId = (Integer) resultCashRefuel.get(“payId”);
JsonResult> ret = new JsonResult<>();
ret.setError_code(500);
String merId = SDKConfig.getConfig().getMerId();
String orderId = payId.toString();// 订单号
String txnTime = new SimpleDateFormat(“yyyyMMddHHmmss”).format(new Date());// 订单时间例如20190424162903
String token = “”;
Payment payment = paymentService.selectByPrimaryKey(payId);
// String txnAmt = payment.getPayMoney().toString();//金额
String txnAmt = “1000”;
// String smsCode=req.getParameter(“smsCode”);
String smsCode = “111111”;
if (null != payment.getClientUid() && !payment.getClientUid().equals("")) {
Map
params.put(“customerid”, payment.getClientUid());
List list = clientUnionpayDataService.find(params);
if (list.size() > 0) {
ClientUnionpayData clientUnionpayData = list.get(0);
String phoneNo = clientUnionpayData.getPhoneno();// 需要传入手机号,银联给用户发送短信
token = clientUnionpayData.getToken();
} else {
throw new OkoilRuntimeException(“用户银联信息不存在”);
}
Map
/*** 银联全渠道系统,产品参数,除了encoding自行选择外其他不需修改 /
contentData.put(“version”, SDKConfig.getConfig().getVersion()); // 版本号
contentData.put(“encoding”, DemoBase.encoding); // 字符集编码
contentData.put(“signMethod”, SDKConfig.getConfig().getSignMethod()); // 签名方法
contentData.put(“txnType”, “01”); // 交易类型 01-消费
contentData.put(“txnSubType”, “01”); // 交易子类型 01-消费
contentData.put(“bizType”, “000902”); // 业务类型 认证支付2.0
contentData.put(“channelType”, “07”); // 渠道类型07-PC
/ 商户接入参数 ***/
contentData.put(“merId”, merId); // 商户号码(本商户号码仅做为测试调通交易使用,该商户号配置了需要对敏感信息加密)测试时请改成自己申请的商户号,【自己注册的测试777开头的商户号不支持代收产品】
contentData.put(“accessType”, “0”); // 接入类型,商户接入固定填0,不需修改
contentData.put(“orderId”, orderId); // 商户订单号,8-40位数字字母,不能含“-”或“_”,可以自行定制规则
contentData.put(“txnTime”, txnTime); // 订单发送时间,格式为YYYYMMDDhhmmss,必须取当前时间,否则会报txnTime无效
contentData.put(“currencyCode”, “156”); // 交易币种(境内商户一般是156 人民币)
contentData.put(“txnAmt”, txnAmt); // 交易金额,单位分,不要带小数点
contentData.put(“accType”, “01”); // 账号类型
contentData.put(“backUrl”, unionBackUrl);
// 消费:token号(从前台开通的后台通知中获取或者后台开通的返回报文中获取),验证码看业务配置(默认要短信验证码)。
contentData.put(“tokenPayData”, “{token=” + token + “&trId=62000000001}”);
Map
customerInfoMap.put(“smsCode”, smsCode); // 短信验证码
// customerInfoMap不送pin的话 该方法可以不送 卡号
String customerInfoStr = AcpService.getCustomerInfo(customerInfoMap, null, DemoBase.encoding);
contentData.put(“customerInfo”, customerInfoStr);
Map
String requestBackUrl = SDKConfig.getConfig().getBackRequestUrl(); // 交易请求url从配置文件读取对应属性文件acp_sdk.properties中的
UnionpayTransactionLog transLog = new UnionpayTransactionLog();
transLog.setMerid(merId);
transLog.setPayid(payId);
unionpayTransactionLogService.insertSelective(transLog);
Map
payment.setStatus((byte) PayEnum.PAY_PROCESSING.getCode());
paymentService.updateByPrimaryKeySelective(payment);
if(!rspData.isEmpty()){
if(AcpService.validate(rspData, DemoBase.encoding)){
LogUtil.writeLog(“验证签名成功”);
String respCode = rspData.get(“respCode”) ;
if((“00”).equals(respCode)){
//交易已受理(不代表交易已成功),等待接收后台通知更新订单状态,也可以主动发起 查询交易确定交易状态。
//TODO
//如果是配置了敏感信息加密,如果需要获取卡号的铭文,可以按以下方法解密卡号
// String accNo1 = rspData.get(“accNo”);
// String accNo2 = AcpService.decryptData(accNo1, “UTF-8”); //解密卡号使用的证书是商户签名私钥证书acpsdk.signCert.path
// LogUtil.writeLog(“解密后的卡号:”+accNo2);
// parseStr.append(“解密后的卡号:”+accNo2);
}else if((“03”).equals(respCode)||
(“04”).equals(respCode)||
(“05”).equals(respCode)){
//后续需发起交易状态查询交易确定交易状态
//TODO
}else{
//其他应答码为失败请排查原因
//TODO
}
}else{
LogUtil.writeErrorLog(“验证签名失败”);
//TODO 检查验证签名失败的原因
}
}else{
//未返回正确的http状态
LogUtil.writeErrorLog(“未获取到返回报文或返回http状态码非200”);
}
}