1. 程式人生 > >springMvc之銀聯閘道器支付(詳解)

springMvc之銀聯閘道器支付(詳解)

一、申請與配置

1、瞭解產品和申請測試賬號

無論接入什麼平臺,首先就是了解自己的需求,然後從開發文件中檢視接入方式。

  • 進入銀聯技術開放平臺 點選
  • 點選我是商戶,然後進行註冊。(注意:註冊或登入時用IE瀏覽器,其他常用瀏覽器外掛都沒法用)
  • 檢視所有的產品,在線上支付還是選擇閘道器支付
    這裡寫圖片描述
  • 點進去可以看看簡介,然後點選我要測試
    這裡寫圖片描述
  • 點選測試引數
    這裡寫圖片描述
  • 可以看到測試賬號需要的一切資料
    這裡寫圖片描述

2、檢視開發文件進行開發

    對於我來說,我覺得文件寫的東西挺亂的,一開始不知所云,在接入的時候直接給出程式碼,但是對於引數的講解比較模糊,對於簽名和驗籤方式也沒有詳細解說,所有的文件其實都在提醒你下載SDK。
這裡寫圖片描述


簡單來說整個支付流程就是以下幾個步驟:

  • 使用者下單後,系統後臺構造支付需要的資訊並簽名
  • 將簽名以後的報文整理成一個網頁,通過瀏覽器傳送消費請求。
  • 當報文沒有問題時,會轉到銀聯官方的頁面上,在此頁面上輸入使用者的資訊後,交易成功。
  • 交易成功後用戶點選返回商戶,這時就觸發了在生成訂單資訊時填寫的frontUrl,這個是前臺通知地址,轉到這個頁面時,會附帶支付結果的一些資訊過去。
  • 同時,在訂單支付以後,會馬上觸發後臺通知backUrl,通知後臺該使用者已經支付。
  • 後臺收到通知後,可以實現自己的業務功能。

瞭解了支付流程以後,那接下來就下載SDK吧,網址

二、發起支付

1、編碼

展開SDK看看
這裡寫圖片描述
毫無疑問,直接選擇java版本
這裡寫圖片描述
可以看到官方demo的目錄結構,其中src下的assets是證書配置或依賴包,而com下的則是原始碼。
在這裡我就不使用原始碼來進行支付了,還是根據自己的需求來進行處理。
建立IDEA maven專案,搭建springmvc環境:
這裡寫圖片描述
pom檔案內容:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <groupId>com.demo</groupId> <artifactId>UnionPay</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <properties> <!-- spring版本號 --> <spring.version>4.3.3.RELEASE</spring.version> <!-- mybatis版本號 --> <mybatis.version>3.3.1</mybatis.version> <!-- log4j日誌檔案管理包版本 --> <slf4j.version>1.7.7</slf4j.version> <log4j.version>1.2.17</log4j.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <!-- spring核心包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> </dependency> <!-- 日誌檔案管理包 --> <!-- log start --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>${log4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>${slf4j.version}</version> </dependency> <!-- json包--> <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.5.0</version> </dependency> <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.5.0</version> </dependency> <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.5.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.12</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.5</version> </dependency> <!-- hibernate資料校驗框架相關jar包 --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.1.3.Final</version> </dependency> <!-- 資料校驗框架相關jar包 --> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>1.1.0.Final</version> </dependency> <!-- https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils --> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.9.3</version> </dependency> <!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.60</version> </dependency> <!-- https://mvnrepository.com/artifact/commons-io/commons-io --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.6</version> </dependency> </dependencies> </project>

首先還是將官方demo中的src下的com.unionpay.acp.demo.sdk目錄下的工具包全部複製到專案中:
這裡寫圖片描述
然後檢視官方demo中的src下的com.unionpay.acp.demo.consume目錄下的Form_6_2_FrontConsume.java檔案,看此檔案的名稱就明白是消費者下單的demo,在自己專案中的controller中建立一個DemoController.java控制器,並把內容複製進去:

package com.demo.controller;

import com.demo.Const.SDKConstants;
import com.demo.bean.DemoBase;
import com.demo.bean.UnionPayRequest;
import com.demo.bean.UnionRefundRequest;
import com.demo.util.AcpService;
import com.demo.util.Generator;
import com.demo.util.LogUtil;
import com.demo.util.SDKConfig;
import com.fasterxml.jackson.databind.util.BeanUtil;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * @author xyd
 * @version V1.0
 * @Package com.demo.controller
 * @Description:
 * @date 2018/8/30 11:11
 */
@RestController
@RequestMapping("/demo")
public class DemoController {

    @GetMapping("/pay")
    public void pay(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.setContentType("text/html; charset="+ DemoBase.encoding);

        //前臺頁面傳過來的
        String merId = req.getParameter("merId");
        String txnAmt = req.getParameter("txnAmt");
        String orderId = req.getParameter("orderId");
        String txnTime = req.getParameter("txnTime");

        Map<String, String> requestData = new HashMap<String, String>();

        /***銀聯全渠道系統,產品引數,除了encoding自行選擇外其他不需修改***/
        requestData.put("version", DemoBase.version);                 //版本號,全渠道預設值
        requestData.put("encoding", DemoBase.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:手機

        /***商戶接入引數***/
        requestData.put("merId", merId);                              //商戶號碼,請改成自己申請的正式商戶號或者open上註冊得來的777測試商戶號
        requestData.put("accessType", "0");                           //接入型別,0:直連商戶 
        requestData.put("orderId",orderId);             //商戶訂單號,8-40位數字字母,不能含“-”或“_”,可以自行定製規則       
        requestData.put("txnTime", txnTime);        //訂單傳送時間,取系統時間,格式為YYYYMMDDhhmmss,必須取當前時間,否則會報txnTime無效
        requestData.put("currencyCode", "156");                       //交易幣種(境內商戶一般是156 人民幣)        
        requestData.put("txnAmt", txnAmt);                            //交易金額,單位分,不要帶小數點
        //requestData.put("reqReserved", "透傳欄位");                     //請求方保留域,如需使用請啟用即可;透傳欄位(可以實現商戶自定義引數的追蹤)本交易的後臺通知,對本交易的交易狀態查詢交易、對賬檔案中均會原樣返回,商戶可以按需上傳,長度為1-1024個位元組。出現&={}[]符號時可能導致查詢介面應答報文解析失敗,建議儘量只傳字母數字並使用|分割,或者可以最外層做一次base64編碼(base64編碼之後出現的等號不會導致解析失敗可以不用管)。        

        requestData.put("riskRateInfo", "{commodityName=測試商品名稱}");

        //前臺通知地址 (需設定為外網能訪問 http https均可),支付成功後的頁面 點選“返回商戶”按鈕的時候將非同步通知報文post到該地址
        //如果想要實現過幾秒中自動跳轉回商戶頁面許可權,需聯絡銀聯業務申請開通自動返回商戶許可權
        //非同步通知引數詳見open.unionpay.com幫助中心 下載  產品介面規範  閘道器支付產品介面規範 消費交易 商戶通知
        requestData.put("frontUrl", DemoBase.frontUrl);

        //後臺通知地址(需設定為【外網】能訪問 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 在後臺通知處理程式驗證簽名之前需要編寫邏輯將這些欄位去掉再驗籤,否則將會驗籤失敗
        requestData.put("backUrl", DemoBase.backUrl);

        // 訂單超時時間。
        // 超過此時間後,除網銀交易外,其他交易銀聯絡統會拒絕受理,提示超時。 跳轉銀行網銀交易如果超時後交易成功,會自動退款,大約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,DemoBase.encoding);  //報文中certId,signature的值是在signData方法中獲取並自動賦值的,只要證書配置正確即可。

        String requestFrontUrl = SDKConfig.getConfig().getFrontRequestUrl();  //獲取請求銀聯的前臺地址:對應屬性檔案acp_sdk.properties檔案中的acpsdk.frontTransUrl
        String html = AcpService.createAutoFormHtml(requestFrontUrl, submitFromData,DemoBase.encoding);   //生成自動跳轉的Html表單

        LogUtil.writeLog("列印請求HTML,此為請求報文,為聯調排查問題的依據:"+html);
        //將生成的html寫到瀏覽器中完成自動跳轉開啟銀聯支付頁面;這裡呼叫signData之後,將html寫到瀏覽器跳轉到銀聯頁面之前均不能對html中的表單項的名稱和值進行修改,如果修改會導致驗籤不通過
        resp.getWriter().write(html);
    }
}

這程式碼看著有點難受,到處都是註釋,但是還是很簡單的:

  • 從前端拿到商戶ID,訂單號,價格,下單時間
  • 和下單資訊一同組成一個map物件,然後呼叫工具類生成簽名
  • 簽名後通過工具類拼接字串組成一個HTML頁面後直接寫入前端

由於這程式碼看著難受,我選擇自己包裝一下下單的資訊:

package com.demo.bean;

/**
 * @author xyd
 * @version V1.0
 * @Package com.demo.bean
 * @Description:
 * @date 2018/8/30 11:29
 */
public class UnionPayRequest {
    /**
     * 商戶號碼
     */
    private String merId = "";
    /**
     * 交易金額,單位分,不要帶小數點
     */
    private String txnAmt = "";
    /**
     * 商戶訂單號,8-40位數字字母,不能含“-”或“_”,可以自行定製規則
     */
    private String orderId = "";
    /**
     * 訂單傳送時間,取系統時間,格式為YYYYMMDDhhmmss,必須取當前時間,否則會報txnTime無效
     */
    private String txnTime = "";
    /**
     * 版本號,全渠道預設值
     */
    private String version = "5.1.0";
    /**
     * 字符集編碼,可以使用UTF-8,GBK兩種方式
     */
    private String encoding = "UTF-8";
    /**
     * 簽名方法
     */
    private String signMethod = "01";
    /**
     * 交易型別 ,01:消費
     */
    private String txnType = "01";
    /**
     * 交易子型別, 01:自助消費
     */
    private String txnSubType = "01";
    /**
     * 業務型別,B2C閘道器支付,手機wap支付
     */
    private String bizType = "000201";
    /**
     * 渠道型別,這個欄位區分B2C閘道器支付和手機wap支付;07:PC,平板  08:手機
     */
    private String channelType = "07";
    /**
     * 接入型別,0:直連商戶
     */
    private String accessType = "0";
    /**
     * 交易幣種(境內商戶一般是156 人民幣)
     */
    private String currencyCode = "156";
    /**
     * 商品資訊
     */
    private String riskRateInfo = "{commodityName=測試商品名稱}";
    /**
     * 回撥地址
     */
    private String frontUrl = "http://test.kuyuntech.com/unionpay/success.jsp";
    /**
     * 非同步通知地址
     */
    private String backUrl = "http://test.kuyuntech.com/unionpay/demo/notify.action";
    /**
     * 訂單超時時間
     */
    private String payTimeout = "";

    public String getMerId() {
        return merId;
    }

    public void setMerId(String merId) {
        this.merId = merId;
    }

    public String getTxnAmt() {
        return txnAmt;
    }

    public void setTxnAmt(String txnAmt) {
        this.txnAmt = txnAmt;
    }

    public String getOrderId() {
        return orderId;
    }

    public void setOrderId(String orderId) {
        this.orderId = orderId;
    }

    public String getTxnTime() {
        return txnTime;
    }

    public void setTxnTime(String txnTime) {
        this.txnTime = txnTime;
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    public String getEncoding() {
        return encoding;
    }

    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }

    public String getSignMethod() {
        return signMethod;
    }

    public void setSignMethod(String signMethod) {
        this.signMethod = signMethod;
    }

    public String getTxnType() {
        return txnType;
    }

    public void setTxnType(String txnType) {
        this.txnType = txnType;
    }

    public String getTxnSubType() {
        return txnSubType;
    }

    public void setTxnSubType(String txnSubType) {
        this.txnSubType = txnSubType;
    }

    public String getBizType() {
        return bizType;
    }

    public void setBizType(String bizType) {
        this.bizType = bizType;
    }

    public String getChannelType() {
        return channelType;
    }

    public void setChannelType(String channelType) {
        this.channelType = channelType;
    }

    public String getAccessType() {
        return accessType;
    }

    public void setAccessType(String accessType) {
        this.accessType = accessType;
    }

    public String getCurrencyCode() {
        return currencyCode;
    }

    public void setCurrencyCode(String currencyCode) {
        this.currencyCode = currencyCode;
    }

    public String getRiskRateInfo() {
        return riskRateInfo;
    }

    public void setRiskRateInfo(String riskRateInfo) {
        this.riskRateInfo = riskRateInfo;
    }

    public String getFrontUrl() {
        return frontUrl;
    }

    public void setFrontUrl(String frontUrl) {
        this.frontUrl = frontUrl;
    }

    public String getBackUrl() {
        return backUrl;
    }

    public void setBackUrl(String backUrl) {
        this.backUrl = backUrl;
    }

    public String getPayTimeout() {
        return payTimeout;
    }

    public void setPayTimeout(String payTimeout) {
        this.payTimeout = payTimeout;
    }

    @Override
    public String toString() {
        return "UnionPayRequest{" +
                "merId='" + merId + '\'' +
                ", txnAmt='" + txnAmt + '\'' +
                ", orderId='" + orderId + '\'' +
                ", txnTime='" + txnTime + '\'' +
                ", version='" + version + '\'' +
                ", encoding='" + encoding + '\'' +
                ", signMethod='" + signMethod + '\'' +
                ", txnType='" + txnType + '\'' +
                ", txnSubType='" + txnSubType + '\'' +
                ", bizType='" + bizType + '\'' +
                ", channelType='" + channelType + '\'' +
                ", accessType='" + accessType + '\'' +
                ", currencyCode='" + currencyCode + '\'' +
                ", riskRateInfo='" + riskRateInfo + '\'' +
                ", frontUrl='" + frontUrl + '\'' +
                ", backUrl='" + backUrl + '\'' +
                ", payTimeout='" + payTimeout + '\'' +
                '}';
    }
}

這時controller中的程式碼就變成了這樣:

package com.demo.controller;

import com.demo.Const.SDKConstants;
import com.demo.bean.DemoBase;
import com.demo.bean.UnionPayRequest;
import com.demo.bean.UnionRefundRequest;
import com.demo.util.AcpService;
import com.demo.util.Generator;
import com.demo.util.LogUtil;
import com.demo.util.SDKConfig;
import com.fasterxml.jackson.databind.util.BeanUtil;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util