淘东电商项目(59) -聚合支付(集成银联支付)

简介: 淘东电商项目(59) -聚合支付(集成银联支付)

引言

本文代码已提交至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. 其它说明

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');


目录
相关文章
|
2月前
|
JavaScript 前端开发
如何在项目中集成 Babel?
如何在项目中集成 Babel?
45 3
|
5月前
|
Java Maven
2022最新版超详细的Maven下载配置教程、IDEA中集成maven(包含图解过程)、以及导入项目时jar包下载不成功的问题解决
这篇文章是一份关于Maven的安装和配置指南,包括下载、环境变量设置、配置文件修改、IDEA集成Maven以及解决jar包下载问题的方法。
2022最新版超详细的Maven下载配置教程、IDEA中集成maven(包含图解过程)、以及导入项目时jar包下载不成功的问题解决
|
6月前
|
安全 Java 数据安全/隐私保护
在Java项目中集成单点登录(SSO)方案
在Java项目中集成单点登录(SSO)方案
|
3月前
|
存储 JavaScript 数据库
ToB项目身份认证AD集成(一):基于目录的用户管理、LDAP和Active Directory简述
本文介绍了基于目录的用户管理及其在企业中的应用,重点解析了LDAP协议和Active Directory服务的概念、关系及差异。通过具体的账号密码认证时序图,展示了利用LDAP协议与AD域进行用户认证的过程。总结了目录服务在现代网络环境中的重要性,并预告了后续的深入文章。
|
3月前
|
人工智能 JavaScript 网络安全
ToB项目身份认证AD集成(三完):利用ldap.js实现与windows AD对接实现用户搜索、认证、密码修改等功能 - 以及针对中文转义问题的补丁方法
本文详细介绍了如何使用 `ldapjs` 库在 Node.js 中实现与 Windows AD 的交互,包括用户搜索、身份验证、密码修改和重置等功能。通过创建 `LdapService` 类,提供了与 AD 服务器通信的完整解决方案,同时解决了中文字段在 LDAP 操作中被转义的问题。
|
3月前
|
jenkins Shell 持续交付
Jenkins持续集成GitLab项目 GitLab提交分支后触发Jenkis任务 持续集成 CI/CD 超级详细 超多图(二)
Jenkins持续集成GitLab项目 GitLab提交分支后触发Jenkis任务 持续集成 CI/CD 超级详细 超多图(二)
94 0
|
3月前
|
安全 Java 测试技术
ToB项目身份认证AD集成(二):快速搞定window server 2003部署AD域服务并支持ssl
本文详细介绍了如何搭建本地AD域控测试环境,包括安装AD域服务、测试LDAP接口及配置LDAPS的过程。通过运行自签名证书生成脚本和手动部署证书,实现安全的SSL连接,适用于ToB项目的身份认证集成。文中还提供了相关系列文章链接,便于读者深入了解AD和LDAP的基础知识。
|
3月前
|
Java Shell 开发工具
git集成IDEA,托管项目实现版本管理
git集成IDEA,托管项目实现版本管理
40 0
|
3月前
|
jenkins Shell 持续交付
Jenkins持续集成GitLab项目 GitLab提交分支后触发Jenkis任务 持续集成 CI/CD 超级详细 超多图(一)
Jenkins持续集成GitLab项目 GitLab提交分支后触发Jenkis任务 持续集成 CI/CD 超级详细 超多图(一)
281 0
|
4月前
|
存储 NoSQL 数据处理
组合和继承怎么集成一个性能较好的项目
组合与继承是面向对象编程的核心概念,前者通过对象间关联实现高效解耦,后者则重用代码以节省空间和内存。组合常用于现代项目,利用代理与依赖注入简化代码管理;而继承简化了子模块对父模块资源的应用,但修改会影响整体。随着分层解耦及微服务架构如SpringCloud的出现,这些技术进一步优化了数据处理效率和服务响应性能,尤其在分布式存储与高并发场景下。同步异步调用、Redis分布式应用等也广泛运用组合与继承,实现代码和内存空间的有效复用。