引言
本文代码已提交至Github(版本号:
31cef39a6e92b1d5857ed7432693df23bb681fe5
),有兴趣的同学可以下载来看看:https://github.com/ylw-github/taodong-shop
在上一篇博客《淘东电商项目(58) -聚合支付(基于设计模式自动跳转支付接口)》,已经讲解完了如下图的第1 - 5个步骤,接下来本文要讲解集成银联支付,也就是下面的第6-8个步骤。
本文目录结构:
l____引言
l____ 1. 集成银联支付
l________ 1.1 银联支付插件
l________ 1.2 银联支付插件的使用
l____ 2. 测试
l____ 3. 其它说明
1. 集成银联支付
1.1 银联支付插件
①首先新建支付插件模块,支付插件模块下有银联支付插件:
②把银联支付demo里面的代码拷贝过来,如下:
③新建InitUnionPayProject
启动类,此类在程序运行时会自动加载,主要加载acp_sdk.properties
文件里面的配置信息,代码如下:
/**
* description: 银联支付项目初始化
* create by: YangLinWei
* create time: 2020/5/15 9:45 上午
*/
@Component
public class InitUnionPayProject implements ApplicationRunner {
// springboot 项目启动的时候 执行该方法
@Override
public void run(ApplicationArguments args) throws Exception {
SDKConfig.getConfig().loadPropertiesFromSrc();
}
}
④acp_sdk.properties
配置文件,修改证书路径,注意这里的证书路径必须是绝对路径,不能写相对路径(证书的申请,查看我前面写的文章https://yanglinwei.blog.csdn.net/article/details/106013626):
1.2 银联支付插件的使用
①支付服务(taodong-shop-service-pay
)引入银联支付插件:
<dependency>
<groupId>com.ylw</groupId>
<artifactId>taodong-shop-union-plugin</artifactId>
<version>1.0-RELEASE</version>
</dependency>
②编写银联支付策略代码,完整代码如下:
/**
* description: 银联支付渠道实现
* create by: YangLinWei
* create time: 2020/5/13 4:41 下午
*/
@Slf4j
public class UnionPayStrategy implements PayStrategy {
@Override
public String toPayHtml(PaymentChannelEntity paymentChannel, PayMentTransacDTO payMentTransacDTO) {
log.info(">>>>>>>>银联支付组装参数开始<<<<<<<<<<<<");
Map<String, String> requestData = new HashMap<String, String>();
/*** 银联全渠道系统,产品参数,除了encoding自行选择外其他不需修改 ***/
requestData.put("version", UnionPayBase.version); // 版本号,全渠道默认值
requestData.put("encoding", UnionPayBase.encoding); // 字符集编码,可以使用UTF-8,GBK两种方式
requestData.put("signMethod", SDKConfig.getConfig().getSignMethod()); // 签名方法
requestData.put("txnType", "01"); // 交易类型 ,01:消费
requestData.put("txnSubType", "01"); // 交易子类型, 01:自助消费
requestData.put("bizType", "000201"); // 业务类型,B2C网关支付,手机wap支付
requestData.put("channelType", "07"); // 渠道类型,这个字段区分B2C网关支付和手机wap支付;07:PC,平板
// 08:手机
/*** 商户接入参数 ***/
String merchantId = paymentChannel.getMerchantId();
requestData.put("merId", merchantId); // 商户号码,请改成自己申请的正式商户号或者open上注册得来的777测试商户号
requestData.put("accessType", "0"); // 接入类型,0:直连商户
String paymentId = payMentTransacDTO.getPaymentId();
// 在微服务电商项目中 订单系统(orderId) 支付系统 支付id
requestData.put("orderId", paymentId); // 商户订单号,8-40位数字字母,不能含“-”或“_”,可以自行定制规则
requestData.put("txnTime", format(payMentTransacDTO.getCreatedTime())); // 订单发送时间,取系统时间,格式为YYYYMMDDhhmmss,必须取当前时间,否则会报txnTime无效
requestData.put("currencyCode", "156"); // 交易币种(境内商户一般是156 人民币)
Long payAmount = payMentTransacDTO.getPayAmount();
requestData.put("txnAmt", payAmount + ""); // 交易金额,单位分,不要带小数点
// requestData.put("reqReserved", "透传字段");
// //请求方保留域,如需使用请启用即可;透传字段(可以实现商户自定义参数的追踪)本交易的后台通知,对本交易的交易状态查询交易、对账文件中均会原样返回,商户可以按需上传,长度为1-1024个字节。出现&={}[]符号时可能导致查询接口应答报文解析失败,建议尽量只传字母数字并使用|分割,或者可以最外层做一次base64编码(base64编码之后出现的等号不会导致解析失败可以不用管)。
//商品名称 或者商品描述
requestData.put("riskRateInfo", payMentTransacDTO.getProductName());
//
// 前台通知地址 (需设置为外网能访问 http https均可),支付成功后的页面 点击“返回商户”按钮的时候将异步通知报文post到该地址
// 如果想要实现过几秒中自动跳转回商户页面权限,需联系银联业务申请开通自动返回商户权限
// 异步通知参数详见open.unionpay.com帮助中心 下载 产品接口规范 网关支付产品接口规范 消费交易 商户通知\
String syncUrl = paymentChannel.getSyncUrl();
requestData.put("frontUrl", syncUrl);
// 后台通知地址(需设置为【外网】能访问 http
// https均可),支付成功后银联会自动将异步通知报文post到商户上送的该地址,失败的交易银联不会发送后台通知
// 后台通知参数详见open.unionpay.com帮助中心 下载 产品接口规范 网关支付产品接口规范 消费交易 商户通知
// 注意:1.需设置为外网能访问,否则收不到通知 2.http https均可 3.收单后台通知后需要10秒内返回http200或302状态码
// 4.如果银联通知服务器发送通知后10秒内未收到返回状态码或者应答码非http200,那么银联会间隔一段时间再次发送。总共发送5次,每次的间隔时间为0,1,2,4分钟。
// 5.后台通知地址如果上送了带有?的参数,例如:http://abc/web?a=b&c=d
// 在后台通知处理程序验证签名之前需要编写逻辑将这些字段去掉再验签,否则将会验签失败
String asynUrl = paymentChannel.getAsynUrl();
requestData.put("backUrl", asynUrl);
// 订单超时时间。
// 超过此时间后,除网银交易外,其他交易银联系统会拒绝受理,提示超时。
// 跳转银行网银交易如果超时后交易成功,会自动退款,大约5个工作日金额返还到持卡人账户。
// 此时间建议取支付时的北京时间加15分钟。
// 超过超时时间调查询接口应答origRespCode不是A6或者00的就可以判断为失败。
requestData.put("payTimeout",
new SimpleDateFormat("yyyyMMddHHmmss").format(new Date().getTime() + 15 * 60 * 1000));
//
//
// 报文中特殊用法请查看 PCwap网关跳转支付特殊用法.txt
//
//
/** 请求参数设置完毕,以下对请求参数进行签名并生成html表单,将表单写入浏览器跳转打开银联页面 **/
Map<String, String> submitFromData = AcpService.sign(requestData, UnionPayBase.encoding); // 报文中certId,signature的值是在signData方法中获取并自动赋值的,只要证书配置正确即可。
String requestFrontUrl = SDKConfig.getConfig().getFrontRequestUrl(); // 获取请求银联的前台地址:对应属性文件acp_sdk.properties文件中的acpsdk.frontTransUrl
String html = AcpService.createAutoFormHtml(requestFrontUrl, submitFromData, UnionPayBase.encoding); // 生成自动跳转的Html表单
LogUtil.writeLog("打印请求HTML,此为请求报文,为联调排查问题的依据:" + html);
// 将生成的html写到浏览器中完成自动跳转打开银联支付页面;这里调用signData之后,将html写到浏览器跳转到银联页面之前均不能对html中的表单项的名称和值进行修改,如果修改会导致验签不通过
return html;
}
private String format(Date timeDate) {
String date = new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(timeDate);
return date;
}
}
③编写前台通知和后台通知的代码,如下:
/**
* description: 银联前台通知和后台通知地址
* create by: YangLinWei
* create time: 2020/5/15 11:04 上午
*/
@Controller
public class UnionResponseController {
@RequestMapping("/frontRcvResponse")
@ResponseBody
public String frontRcvResponse(HttpServletRequest req, HttpServletResponse resp) throws UnsupportedEncodingException {
LogUtil.writeLog("FrontRcvResponse前台接收报文返回开始");
String encoding = req.getParameter(SDKConstants.param_encoding);
LogUtil.writeLog("返回报文中encoding=[" + encoding + "]");
String pageResult = "";
Map<String, String> respParam = getAllRequestParam(req);
// 打印请求报文
LogUtil.printRequestLog(respParam);
Map<String, String> valideData = null;
StringBuffer page = new StringBuffer();
if (null != respParam && !respParam.isEmpty()) {
Iterator<Map.Entry<String, String>> it = respParam.entrySet()
.iterator();
valideData = new HashMap<String, String>(respParam.size());
while (it.hasNext()) {
Map.Entry<String, String> e = it.next();
String key = (String) e.getKey();
String value = (String) e.getValue();
value = new String(value.getBytes(encoding), encoding);
page.append("<tr><td width=\"30%\" align=\"right\">" + key
+ "(" + key + ")</td><td>" + value + "</td></tr>");
valideData.put(key, value);
}
}
if (!AcpService.validate(valideData, encoding)) {
page.append("<tr><td width=\"30%\" align=\"right\">验证签名结果</td><td>失败</td></tr>");
LogUtil.writeLog("验证签名结果[失败].");
} else {
page.append("<tr><td width=\"30%\" align=\"right\">验证签名结果</td><td>成功</td></tr>");
LogUtil.writeLog("验证签名结果[成功].");
System.out.println(valideData.get("orderId")); //其他字段也可用类似方式获取
String respCode = valideData.get("respCode");
//判断respCode=00、A6后,对涉及资金类的交易,请再发起查询接口查询,确定交易成功后更新数据库。
}
return page.toString();
}
@RequestMapping("/BackRcvResponse")
@ResponseBody
public String BackRcvResponse(HttpServletRequest req, HttpServletResponse resp) {
LogUtil.writeLog("BackRcvResponse接收后台通知开始");
String encoding = req.getParameter(SDKConstants.param_encoding);
// 获取银联通知服务器发送的后台通知参数
Map<String, String> reqParam = getAllRequestParam(req);
LogUtil.printRequestLog(reqParam);
//重要!验证签名前不要修改reqParam中的键值对的内容,否则会验签不过
if (!AcpService.validate(reqParam, encoding)) {
LogUtil.writeLog("验证签名结果[失败].");
//验签失败,需解决验签问题
} else {
LogUtil.writeLog("验证签名结果[成功].");
//【注:为了安全验签成功才应该写商户的成功处理逻辑】交易成功,更新商户订单状态
String orderId =reqParam.get("orderId"); //获取后台通知的数据,其他字段也可用类似方式获取
String respCode = reqParam.get("respCode");
//判断respCode=00、A6后,对涉及资金类的交易,请再发起查询接口查询,确定交易成功后更新数据库。
}
LogUtil.writeLog("BackRcvResponse接收后台通知结束");
//返回给银联服务器http 200 状态码
return "ok";
}
/**
* 获取请求参数中所有的信息
* 当商户上送frontUrl或backUrl地址中带有参数信息的时候,
* 这种方式会将url地址中的参数读到map中,会导多出来这些信息从而致验签失败,这个时候可以自行修改过滤掉url中的参数或者使用getAllRequestParamStream方法。
* @param request
* @return
*/
public static Map<String, String> getAllRequestParam(
final HttpServletRequest request) {
Map<String, String> res = new HashMap<String, String>();
Enumeration<?> temp = request.getParameterNames();
if (null != temp) {
while (temp.hasMoreElements()) {
String en = (String) temp.nextElement();
String value = request.getParameter(en);
res.put(en, value);
// 在报文上送时,如果字段的值为空,则不上送<下面的处理为在获取所有参数数据时,判断若值为空,则删除这个字段>
if (res.get(en) == null || "".equals(res.get(en))) {
// System.out.println("======为空的字段名===="+en);
res.remove(en);
}
}
}
return res;
}
}
好了代码到此基本写完,下面开始测试。
2. 测试
首先启动项目(Eureka注册中心、单点服务中心、会员服务、支付服务、支付门户):
①首先模拟提交订单,获取token,浏览器访问:http://localhost:8600/cratePayToken?payAmount=999&orderId=20200513141452&userId=27&productName=辣条,获取tokenpay_d520140b06e249e0bf67692c3802fde1
:
②模拟跳转到支付详情页,浏览器访问:http://localhost:8079/pay?payToken=pay_d520140b06e249e0bf67692c3802fde1
③点击银联支付,可以看到自动跳转到银联支付页面:
④填写信息支付,可以看到支付成功:
⑤点击返回商户,可以看到前台通知内容如下:
3. 其它说明
-
①有些代码细节没有提及,同学们可以从我的github clone 详细的代码来看https://github.com/ylw-github/taodong-shop(版本号:
31cef39a6e92b1d5857ed7432693df23bb681fe5
) -
②证书要自己注册下载,具体可以参考我之前写过的博客《淘东电商项目(53) -银联支付案例源码分析》
-
③商户号测试使用的是
777290058110048
,由数据库中配置,同时也配置了前台和后台通知地址,数据库的插入语句如下:
INSERT INTO `taodong-pay`.`payment_channel`(`ID`, `CHANNEL_NAME`, `CHANNEL_ID`, `MERCHANT_ID`, `SYNC_URL`, `ASYN_URL`, `PUBLIC_KEY`, `PRIVATE_KEY`, `CHANNEL_STATE`, `REVISION`, `CREATED_BY`, `CREATED_TIME`, `UPDATED_BY`, `UPDATED_TIME`, `CLASS_ADDRES`) VALUES (1, '银联支付', 'yinlian_pay', '777290058110048', 'http://localhost:8600/frontRcvResponse', 'http://swx5c3.natappfree.cc/BackRcvResponse', NULL, NULL, 0, NULL, NULL, NULL, NULL, NULL, 'com.ylw.service.pay.strategy.impl.UnionPayStrategy');