
一、微信支付
1、業務平臺介紹:
(1)微信公眾平臺
微信公眾平臺是微信公眾賬號申請入口和管理后臺,商戶可以在公眾平臺提交基本資料、業務資料、財務資料申請開通微信支付功能,
(2) 微信開放平臺
微信開放平臺是商戶APP接入微信支付開放介面的申請入口,通過此平臺可申請微信APP支付,
(3) 微信商戶平臺
微信商戶平臺是微信支付相關的商戶功能集合,包括引數配置、支付資料查詢與統計、在線退款、代金券或立減優惠運營等功能,
2、支付產品介紹:
(1)付款碼支付
付款碼支付,即日常所說的被掃支付,這是一種純用于線下場景的支付方式,由用戶出示微信客戶端內展示的付款二維碼,商戶使用掃碼設備掃碼后完成支付,
(2)Native原生支付
Native原生支付,即日常所說的掃碼支付,商戶根據微信支付協議格式生成的二維碼,用戶通過微信“掃一掃”掃描二維碼后即進入付款確認界面,輸入密碼即完成支付,
(3) JSAPI網頁支付
JSAPI網頁支付,即日常所說的公眾號支付,可在微信公眾號、朋友圈、聊天會話中點擊頁面鏈接,或者用微信“掃一掃”掃描頁面地址二維碼在微信中打開商戶HTML5頁面,在頁面內下單完成支付,
(4) APP支付
APP支付是指商戶已有的APP,通過對接微信支付API及SDK,實作從商戶APP發起交易后跳轉到微信APP,用戶完成支付后跳回商戶APP的場景,
(5) H5支付
H5支付是指在微信外打開的H5頁面,通過對接微信支付API,實作拉起微信客戶端,完成支付后跳回外部瀏覽器的能力,
(6) 小程式支付
小程式支付是指在商戶既有的小程式內通過對接微信支付API,實作用戶在小程式內完成交易的場景,
3、申請應用APPID
由于微信支付的產品體系全部搭載于微信的社交體系之上,所以直連商戶或服務商商戶接入微信支付之前,都需要有一個微信社交載體,該載體對應的ID即為APPID,
對于直連商戶,該社交載體可以是公眾號,小程式或APP,而服務商的社交載體只能是公眾號,
如申請社交載體為公眾號,請前往公眾平臺申請(https://mp.weixin.qq.com)
如申請社交載體為小程式,請前往小程式平臺申請 (https://developers.weixin.qq.com/miniprogram/dev/framework/quickstart/getstart.html#申請帳號)
如商戶已擁有自己的APP,且希望該APP接入微信支付,請前往開放平臺申請(https://open.weixin.qq.com/)
各類社交載體一旦申請成功后,可以登錄對應平臺查看賬號資訊以獲取對應的appid,
4、申請商戶MCHID
商戶號申請平臺申請MCHID(https://pay.weixin.qq.com)
申請成功后,會向服務商填寫的聯系郵箱下發通知郵件,內容包含申請成功的MCHID及其登錄賬號密碼,請妥善保存,
注意:一個MCHID只能對應一個結算幣種,若需要使用多個幣種收款,需要申請對應數量的MCHID,
5、系結APPID及MCHID
APPID和MCHID全部申請完畢后,需要建立兩者之間的系結關系,在微信商戶后臺進行系結,
6、設定支付API密鑰
登錄微信商戶平臺,在賬戶設定-API安全,設定API密鑰,
7、微信掃碼支付示例
7.1 掃碼支付流程

7.2 掃碼支付統一下單示例:
/**
* 微信預創建訂單,生成微信二維碼
* @return
* @throws Exception
*/
@RequestMapping(value="/tradePrecreate", method = RequestMethod.POST)
@ResponseBody
public void toWxPayPrecreate(PayWay payWay) throws Exception {
//自定義商戶訂單號:長度不能超過32位
String out_trade_no = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
//封裝微信下單引數
SortedMap<String, String> paramMap = new TreeMap<>();
paramMap.put("appid", appid); //公眾賬號ID
paramMap.put("mch_id", mch_id); //商戶號
String nonce_str= UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
paramMap.put("nonce_str", nonce_str ); //隨機字串
paramMap.put("body", payWay.getGoodsName()); // 商品描述
paramMap.put("out_trade_no", out_trade_no); //商戶訂單號,商戶系統內部訂單號,要求32個字符內,只能是數字、大小寫字母_-|* 且在同一個商戶號下唯一
paramMap.put("total_fee",String.valueOf(payWay.getAmount())); //標價金額,單位分
paramMap.put("spbill_create_ip", IPUtils.localIp());
paramMap.put("notify_url", notifyUrl); //自定義后臺通知地址
paramMap.put("trade_type", "NATIVE"); //交易型別 JSAPI 公眾號支付 NATIVE 掃碼支付 APP APP支付
//第二步,簽名,引數轉xml
String requestXMl = WXPayUtil.generateSignedXml(paramMap, key, WXPayConstants.SignType.MD5);
try {
//發送請求(POST)(獲得資料包ID)
String result = HttpXmlUtil._doPost("https://api.mch.weixin.qq.com/pay/unifiedorder",requestXMl);
log.info("微信預創建訂單,回傳資料:"+result);
// 將決議結果存盤在HashMap中
Map map = WXPayUtil.xmlToMap(result);
String return_code = (String) map.get("return_code");//回傳狀態碼
String return_msg = (String) map.get("return_msg");//回傳狀態碼
String result_code = (String) map.get("result_code");//回傳狀態碼
String err_code_des = (String) map.get("err_code_des");//回傳狀態碼
String prepay_id = (String) map.get("prepay_id");//回傳狀態碼
if("SUCCESS".equals(return_code)){
if("SUCCESS".equals(result_code)){
log.info("=====微信統一下單成功");
}else{
if(err_code_des!=null && !"".equals(err_code_des)){
log.error("微信預創建訂單,回傳例外提示:"+err_code_des);
}else{
log.error("微信下單失敗");
}
}
}else{
log.error("微信下單失敗");
}
} catch (Exception e) {
e.printStackTrace();
}
}
其中涉及到的工具類:
IPUtils.java:
import java.net.*;
import java.util.Enumeration;
import java.util.List;
public class IPUtils {
/**
* 獲取本機Ip
*
* 通過 獲取系統所有的networkInterface網路介面 然后遍歷 每個網路下的InterfaceAddress組,
* 獲得符合 <code>InetAddress instanceof Inet4Address</code> 條件的一個IpV4地址
* @return
*/
@SuppressWarnings("rawtypes")
public static String localIp(){
String ip = null;
Enumeration allNetInterfaces;
try {
allNetInterfaces = NetworkInterface.getNetworkInterfaces();
while (allNetInterfaces.hasMoreElements()) {
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
List<InterfaceAddress> InterfaceAddress = netInterface.getInterfaceAddresses();
for (InterfaceAddress add : InterfaceAddress) {
InetAddress Ip = add.getAddress();
if (Ip != null && Ip instanceof Inet4Address) {
ip = Ip.getHostAddress();
}
}
}
} catch (SocketException e) {
// TODO Auto-generated catch block
e.getCause();
}
return ip;
}
}
HttpXmlUtil.java:
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.util.EntityUtils;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 此版本使用document 物件封裝XML,解決發送短信內容包涵特殊字符而出現無法決議,如 短信為:“你好,<%&*&*&><<<>fds測驗短信”
*
* @author 編程俠Java
*/
public class HttpXmlUtil {
/**
* 執行一個HTTP GET請求,回傳請求回應的HTML
*
* @param url 請求的URL地址
* @param params 請求的查詢引數,可以為null
* @return 回傳請求回應的HTML
*/
public static String doGet(String url,Map<String, Object> params) throws Exception {
// 構造HttpClient的實體
HttpClient httpClient = HttpClientFactory.getHttpClient();
if (params != null && !params.isEmpty()) {
List<org.apache.http.NameValuePair> pairs = new ArrayList<org.apache.http.NameValuePair>(params.size());
for (String key : params.keySet()) {
pairs.add(new org.apache.http.message.BasicNameValuePair(key, params.get(key).toString()));
}
url += "?" + EntityUtils.toString(new UrlEncodedFormEntity(pairs, "UTF-8"));
}
// 創建GET方法的實體
GetMethod getMethod = new GetMethod(url);
// 使用系統提供的默認的恢復策略
getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,new DefaultHttpMethodRetryHandler());
try {
// 執行getMethod
int statusCode = httpClient.executeMethod(getMethod);
if (statusCode != HttpStatus.SC_OK) {
System.err.println("Method failed: " + getMethod.getStatusLine());
}
// 讀取內容
byte[] responseBody = getMethod.getResponseBody();
// 處理內容
return new String(responseBody,"UTF-8");
} catch (HttpException e) {
// 發生致命的例外,可能是協議不對或者回傳的內容有問題
e.printStackTrace();
} catch (IOException e) {
// 發生網路例外
e.printStackTrace();
} finally {
// 釋放連接
getMethod.releaseConnection();
}
return null;
}
/**
* 執行一個HTTP POST請求,回傳請求回應的XML
* @param url 請求的URL地址
* @param params 請求的查詢引數,可以為null
* @return 回傳請求回應的XML
*/
public static String _doPost(String url, String params) throws Exception {
HttpClient client = HttpClientFactory.getHttpClient();
PostMethod myPost = new PostMethod(url);
String responseString = null;
try {
myPost.setRequestEntity(new StringRequestEntity(params, "text/xml", "utf-8"));
int statusCode = client.executeMethod(myPost);
if (statusCode == HttpStatus.SC_OK) {
BufferedInputStream bis = new BufferedInputStream(myPost.getResponseBodyAsStream());
byte[] bytes = new byte[1024];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int count = 0;
while ((count = bis.read(bytes)) != -1) {
bos.write(bytes, 0, count);
}
byte[] strByte = bos.toByteArray();
responseString = new String(strByte, 0, strByte.length, "utf-8");
bos.close();
bis.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
myPost.releaseConnection();
}
return responseString;
}
}
其他的工具類如:WXPayUtil、WXPayConstants 均使用微信官方demo中的,sdk與demo下載地址:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=11_1
7.3 掃碼支付微信退款:
微信支付介面中,涉及資金回滾的介面會使用到API證書,包括退款、撤銷介面等,可以在微信商戶平臺—》賬戶中心—》賬戶設定—》API安全,下載微信提供的證書生成工具,填寫商戶號和商戶名稱,再把將軟體生成的密鑰字串復制到微信商戶平臺,生成證書,
/**
* 微信退款
* @param tradeRefund
* @return
*/
public void toRefund(TradeRefund tradeRefund){
//退款訂單號
String out_refund_no = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
HashMap<String, String> data = new HashMap();
data.put("out_trade_no", tradeRefund.getOrderID());
data.put("out_refund_no", out_refund_no);
data.put("total_fee", String.valueOf(tradeRefund.getTotalFee()));
data.put("refund_fee", String.valueOf(tradeRefund.getRefundAmt()));//退款金額,單位分
data.put("refund_fee_type", "CNY");
data.put("op_user_id", mch_id);
data.put("refund_desc", tradeRefund.getRefundDescribe());
data.put("notify_url", refundNotify);
try {
this.config = WXPayConfigImpl.getInstance();
this.wxpay = new WXPay(this.config);
Map<String, String> resp = this.wxpay.refund(data);
System.out.println(resp);
if (!"SUCCESS".equals(resp.get("return_code"))) {
log.error("微信退款介面呼叫失敗,回傳回應資訊:"+resp.get("return_msg"));
}else{
if ("SUCCESS".equals(resp.get("result_code"))) {
log.info("微信退款成功,回傳回應資訊:"+resp.get("return_msg"));
}else{
log.error("微信退款失敗,回傳回應資訊:"+resp.get("err_code_des"));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
涉及到的退款資訊類TradeRefund.java:
public class TradeRefund {
private String Version;//版本
private String CharSet;//編碼費那事
private String OrderID;//訂單號
private String OperatorId;//操作人id
private short RefundFlag; //1:交易失敗退款、2:事務處理退款、3:非現金即時退款
private int RefundAmt;//退款金額
private String RefundDescribe;//訂單備注
private int totalFee;//訂單總金額
public String getVersion() {
return Version;
}
public void setVersion(String version) {
Version = version;
}
public String getCharSet() {
return CharSet;
}
public void setCharSet(String charSet) {
CharSet = charSet;
}
public String getOrderID() {
return OrderID;
}
public void setOrderID(String orderID) {
OrderID = orderID;
}
public String getOperatorId() {
return OperatorId;
}
public void setOperatorId(String operatorId) {
OperatorId = operatorId;
}
public short getRefundFlag() {
return RefundFlag;
}
public void setRefundFlag(short refundFlag) {
RefundFlag = refundFlag;
}
public int getRefundAmt() {
return RefundAmt;
}
public void setRefundAmt(int refundAmt) {
RefundAmt = refundAmt;
}
public String getRefundDescribe() {
return RefundDescribe;
}
public void setRefundDescribe(String refundDescribe) {
RefundDescribe = refundDescribe;
}
public int getTotalFee() {
return totalFee;
}
public void setTotalFee(int totalFee) {
this.totalFee = totalFee;
}
}
7.4 掃碼支付回呼:
微信支付完,會呼叫服務端后端的通知介面,回傳支付資訊,商戶需在微信公眾號后臺配置回呼地址,注意:回呼地址必須使用通過ICP備案的域名,不能是IP地址,并且鏈接不能帶引數,

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
@Slf4j
@Controller
public class NotifyController {
private String key = ""; //這里填應用的key
/**
* 異步接受微信掃碼支付通知
* 支付結果通用通知檔案:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_7&index=8
* @param request
* @param response
* @throws IOException
*/
@ResponseBody
@RequestMapping(value = "payNotify", produces = "application/json;charset=UTF-8")
public void payNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
BufferedReader reader = null;
reader = request.getReader();
String line = "";
String xmlString = null;
StringBuffer inputString = new StringBuffer();
while ((line = reader.readLine()) != null) {
inputString.append(line);
}
xmlString = inputString.toString();
request.getReader().close();
Map<String, String> packageParams = WXPayUtil.xmlToMap(xmlString);
//判斷簽名是否正確
if (checkSign(packageParams)) {
String resXml = "";
if("SUCCESS".equals((String)packageParams.get("result_code"))){//支付成功
String appid = packageParams.get("appid");
String mch_id = packageParams.get("mch_id");
String openid = packageParams.get("openid");
String is_subscribe = packageParams.get("is_subscribe");
String out_trade_no = packageParams.get("out_trade_no");
String total_fee = packageParams.get("total_fee");
//交易型別
String trade_type = packageParams.get("trade_type");
//付款銀行
String bank_type = packageParams.get("bank_type");
//現金支付金額
String cash_fee = packageParams.get("cash_fee");
// 微信支付訂單號
String transactionId = packageParams.get("transaction_id");
// // 支付完成時間,格式為yyyyMMddHHmmss
String time_end = packageParams.get("time_end");
//通知微信.異步確認成功.必寫.不然會一直通知后臺.八次之后就認為交易失敗了.
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
//處理自己的而業務
} else {
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[報文為空]]></return_msg>" + "</xml> ";
}
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
} else{
log.error("======簽名驗證失敗");
}
}
/**
* 簽名驗證
* @param map
* @return
*/
private boolean checkSign(Map<String, String> map) {
String signFromAPIResponse = map.get("sign");
if (signFromAPIResponse == "" || signFromAPIResponse == null) {
log.info("=========API回傳的資料簽名資料不存在");
return false;
}
//清掉回傳資料物件里面的Sign資料(不能把這個資料也加進去進行簽名),然后用簽名演算法進行簽名
map.put("sign", "");
//將API回傳的資料根據用簽名演算法進行計算新的簽名,用來跟API回傳的簽名進行比較
String signForAPIResponse = getSign(map);
if (!signForAPIResponse.equals(signFromAPIResponse)) {
//簽名驗不過,表示這個API回傳的資料有可能已經被篡改了
log.info("===========API回傳的資料簽名驗證不通過");
return false;
}
log.info("===========sign簽名驗證通過");
return true;
}
public String getSign(Map<String, String> map) {
SortedMap<String, String> signParams = new TreeMap<String, String>();
for (Map.Entry<String, String> stringStringEntry : map.entrySet()) {
signParams.put(stringStringEntry.getKey(), stringStringEntry.getValue());
}
signParams.remove("sign");
String sign = null;
try {
sign = WXPayUtil.generateSignature(signParams, key);
} catch (Exception e) {
e.printStackTrace();
}
return sign;
}
}
7.5 退款回呼:
private String key = ""; //這里填應用的key
/**
* 退款結果通知
* <p>
* 在申請退款介面中上傳引數“notify_url”以開通該功能
* 如果鏈接無法訪問,商戶將無法接收到微信通知,
* 通知url必須為直接可訪問的url,不能攜帶引數,示例:notify_url:“https://pay.weixin.qq.com/wxpay/pay.action”
* <p>
* 當商戶申請的退款有結果后,微信會把相關結果發送給商戶,商戶需要接收處理,并回傳應答,
* 對后臺通知互動時,如果微信收到商戶的應答不是成功或超時,微信認為通知失敗,微信會通過一定的策略定期重新發起通知,盡可能提高通知的成功率,但微信不保證通知最終能成功,
* (通知頻率為15/15/30/180/1800/1800/1800/1800/3600,單位:秒)
* 注意:同樣的通知可能會多次發送給商戶系統,商戶系統必須能夠正確處理重復的通知,
* 推薦的做法是,當收到通知進行處理時,首先檢查對應業務資料的狀態,判斷該通知是否已經處理過,如果沒有處理過再進行處理,如果處理過直接回傳結果成功,在對業務資料進行狀態檢查和處理之前,要采用資料鎖進行并發控制,以避免函式重入造成的資料混亂,
* 特別說明:退款結果對重要的資料進行了加密,商戶需要用商戶秘鑰進行解密后才能獲得結果通知的內容
* @param request
* @param response
* @throws IOException
*/
@ResponseBody
@RequestMapping(value = "refundNotify", produces = "application/json;charset=UTF-8")
public void refundNotify(HttpServletRequest request,HttpServletResponse response) throws Exception {
BufferedReader reader = null;
reader = request.getReader();
String line = "";
String xmlString = null;
StringBuffer inputString = new StringBuffer();
while ((line = reader.readLine()) != null) {
inputString.append(line);
}
xmlString = inputString.toString();
request.getReader().close();
log.info("===========異步接受微信退款回呼通知:" + xmlString);
Map<String, String> notifyMapData = WXPayUtil.xmlToMap(xmlString);
String resXml = "";
if("SUCCESS".equals(notifyMapData.get("return_code"))){//退款成功
// 獲得加密資訊
String reqInfo = notifyMapData.get("req_info");
/**
* 解密方式
* 解密步驟如下:
* (1)對加密串A做base64解碼,得到加密串B
* (2)對商戶key做md5,得到32位小寫key* ( key設定路徑:微信商戶平臺(pay.weixin.qq.com)-->賬戶設定-->API安全-->密鑰設定 )
* (3)用key*對加密串B做AES-256-ECB解密(PKCS7Padding)
*/
// 進行AES解密 獲取req_info中包含的相關資訊(解密失敗會拋出例外)
String keyB = MD5.MD5Encode2(key, "UTF-8");
AESUtils util = new AESUtils(keyB); // 密鑰
String refundDecryptedData = util.decryptData(reqInfo);
Map<String, String> aesMap = WXPayUtil.xmlToMap(refundDecryptedData);
/** 以下為回傳的加密欄位: **/
// 商戶退款單號
String out_refund_no = aesMap.get("out_refund_no");
// 退款狀態:SUCCESS-退款成功、CHANGE-退款例外、REFUNDCLOSE—退款關閉
String refund_status = aesMap.get("refund_status");
// 商戶訂單號
String out_trade_no = aesMap.get("out_trade_no");
// 微信訂單號
String transaction_id = aesMap.get("transaction_id");
// 微信退款單號
String refund_id = aesMap.get("refund_id");
// 訂單總金額,單位為分,只能為整數
String total_fee = aesMap.get("total_fee");
// 應結訂單金額
String settlement_total_fee = aesMap.get("settlement_total_fee");
// 申請退款金額,單位為分
String refund_fee = aesMap.get("refund_fee");
// 退款金額,退款金額=申請退款金額-非充值代金券退款金額,退款金額<=申請退款金額
String settlement_refund_fee = aesMap.get("settlement_refund_fee");
String success_time = aesMap.get("success_time");
// 退款是否成功
if (!"SUCCESS".equals(refund_status)) {
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[退款失敗]]></return_msg>" + "</xml> ";
WXPayUtil.getLogger().error("========================refund:微信支付回呼:退款失敗");
} else {
// 通知微信.異步確認成功.必寫.不然會一直通知后臺.八次之后就認為交易失敗了.
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
WXPayUtil.getLogger().info("微信支付回呼:退款成功");
}
} else {
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[退款例外]]></return_msg>" + "</xml> ";
log.error("==========微信掃碼退款例外");
}
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
}
其中涉及到的工具類AESUtils.java
import com.cn.util.Base64Util;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
/**
* AES加解密
* Created 編程俠Java
*/
public class AESUtils {
/**
* 密鑰演算法
*/
private static final String ALGORITHM = "AES";
/**
* 加解密演算法/作業模式/填充方式
*/
private static final String ALGORITHM_STR = "AES/ECB/PKCS5Padding";
/**
* SecretKeySpec類是KeySpec介面的實作類,用于構建秘密密鑰規范
*/
private static SecretKeySpec key;
public AESUtils(String hexKey) {
key = new SecretKeySpec(hexKey.getBytes(), ALGORITHM);
}
/**
* AES加密
* @param data
* @return
* @throws Exception
*/
public String encryptData(String data) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM_STR); // 創建密碼器
cipher.init(Cipher.ENCRYPT_MODE, key);// 初始化
byte[] encrypted = cipher.doFinal(data.getBytes());
return Base64Util.encodeBytes(encrypted);
}
/**
* AES解密
* @param base64Data
* @return
* @throws Exception
*/
public static String decryptData(String base64Data) throws Exception{
Cipher cipher = Cipher.getInstance(ALGORITHM_STR);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] original = cipher.doFinal(Base64Util.decode(base64Data));
return new String(original);
}
}
二、支付寶支付
支付寶支付一般分為代扣支付、掃碼支付等等,代扣支付,用戶需要先進行簽約,通常通過商戶APP跳轉到支付寶APP進行簽約,支付時拿用戶在支付寶APP中簽約時的協議號去扣款,

代扣服務需要在支付寶商戶后臺開通商戶代扣能力,而支付寶二維碼掃碼支付,需在支付寶商家后臺開通當面付能力,

(1)支付寶代扣支付
/**
* 支付寶代扣支付
* @param orderID 訂單編號
* @param transAmount 訂單金額
* @param userPayAccount 支付賬號
* @return
*/
public JSONObject alipayWithhold(String orderID, String transAmount, String userPayAccount){
JSONObject result = new JSONObject();
String returnUrl= "";//自定義扣款同步通知介面
String notifyUrl= "";//自定義扣款異步通知介面
String actual_order_time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
//初始化請求類
AlipayTradePayRequest alipayRequest = new AlipayTradePayRequest();
//組裝扣款引數
String bizContent = "{"
+ "\"out_trade_no\":\"" + orderID + "\","
+ "\"product_code\":\"GENERAL_WITHHOLDING\","
+ "\"total_amount\":\"" + transAmount + "\","
+ "\"subject\":\"扣款備注資訊\","
+ "\"promo_params\":{" + "\"actual_order_time\":\"" + actual_order_time + "\"},"
+ "\"agreement_params\":{" + "\"agreement_no\":\"" + userPayAccount + "\"}"
+ "}";
alipayRequest.setBizContent(bizContent);
alipayRequest.setReturnUrl(returnUrl);
alipayRequest.setNotifyUrl(notifyUrl);
//sdk請求客戶端,已將配置資訊初始化
AlipayClient alipayClient = DefaultAlipayClientFactory.getAlipayClient();
try {
//因為是介面服務,使用exexcute方法獲取到回傳值
AlipayTradePayResponse alipayResponse = alipayClient.execute(alipayRequest);
if (alipayResponse.isSuccess()) {
if (alipayResponse.getCode().equals("10000")) {
result.put("code","SUCCESS");
result.put("msg","支付寶扣款成功");
} else {
result.put("code","FAIL");
result.put("msg","支付寶扣款失敗,"+alipayResponse.getSubMsg());
log.error("=====支付寶扣款失敗");
}
} else {
result.put("code","FAIL");
result.put("msg","支付寶扣款失敗,"+alipayResponse.getSubMsg());
log.error("=====支付寶介面呼叫失敗");
}
} catch (AlipayApiException e) {
e.printStackTrace();
if (e.getCause() instanceof java.security.spec.InvalidKeySpecException) {
result.put("code","FAIL");
result.put("msg","商戶私鑰格式不正確,請確認組態檔是否配置正確");
log.error("=====商戶私鑰格式不正確,請確認組態檔是否配置正確");
}
}
return result;
}
其中支付寶封裝呼叫的工具類 DefaultAlipayClientFactory.java
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
/**
* @author 編程俠Java
* @description: 支付寶公共請求引數拼接類
* @date 2021/01/09 13:36
*/
public class DefaultAlipayClientFactory {
private static AlipayClient alipayClient = null;
/**
* 封裝公共請求引數
*
* @return AlipayClient
*/
public static AlipayClient getAlipayClient() {
if(alipayClient != null){
return alipayClient;
}
// 網關
String URL = "https://openapi.alipay.com/gateway.do";
// 商戶APP_ID
String APP_ID = "商戶APP_ID";
// 商戶RSA 私鑰
String APP_PRIVATE_KEY = "商戶RSA私鑰";
// 請求方式 json
String FORMAT = "json";
// 編碼格式,目前只支持UTF-8
String CHARSET = "UTF-8";
// 支付寶公鑰
String ALIPAY_PUBLIC_KEY = "支付寶公鑰";
// 簽名方式
String SIGN_TYPE = "RSA2";
return new DefaultAlipayClient(URL, APP_ID, APP_PRIVATE_KEY, FORMAT, CHARSET, ALIPAY_PUBLIC_KEY, SIGN_TYPE);
}
}
代扣支付回呼:
/**
* 異步接受支付寶代扣通知
* @param request
* @param response
* @throws IOException
*/
@ResponseBody
@RequestMapping(value="/notifyUrl.htm")
public void notifyObject(HttpServletRequest request, HttpServletResponse response) throws IOException {
String charset = "UTF-8"; // 編碼
String publicKey = "填寫支付寶公鑰"; //支付寶公鑰
String singType = "RSA2"; //簽名方式
Map<String, String> params = new HashMap<String, String>();
Map<String, String[]> requestParams = request.getParameterMap();
log.info("=======異步接受支付寶代扣渠道通知,請求引數:"+ JSON.toJSONString(requestParams));
for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ",";
}
params.put(name, valueStr);
}
try {
boolean validation = AlipaySignature.rsaCheckV1(params, publicKey, charset,singType);//驗簽
if (validation) {
//根據業務需要進行處理
String notify_type = params.get("notify_type");
if("dut_user_unsign".equals(notify_type)){//dut_user_unsign 解約
//處理解約業務
} else if("dut_user_sign".equals(notify_type)){//簽約 系結dut_user_sign
//處理簽約業務
}else if("trade_status_sync".equals(notify_type)){//訂單支付通知
String gmt_create = params.get("gmt_create");
String seller_email = params.get("seller_email");
String subject = params.get("subject");
String buyer_id = params.get("buyer_id");
String invoice_amount = params.get("invoice_amount");
String notify_id = params.get("notify_id");
String trade_status = params.get("trade_status");
String receipt_amount = params.get("receipt_amount");
String app_id = params.get("app_id");
String buyer_pay_amount = params.get("buyer_pay_amount");
String sign_type = params.get("sign_type");
String seller_id = params.get("seller_id");
String gmt_payment = params.get("gmt_payment");
String notify_time = params.get("notify_time");
String version = params.get("version");
String out_trade_no = params.get("out_trade_no");
String total_amount = params.get("total_amount");
String trade_no = params.get("trade_no");
String auth_app_id = params.get("auth_app_id");
String buyer_logon_id = params.get("buyer_logon_id");
String point_amount = params.get("point_amount");
//處理訂單支付之后的業務
}
//給支付寶回傳success,否則支付寶會連續多次發送
response.getOutputStream().write("success".getBytes());
response.flushBuffer();
}
} catch (AlipayApiException e) {
log.error("======異步接受支付寶代扣渠道通知,回呼例外");
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 同步回傳處理
* @param request
* @param response
* @throws IOException
*/
@MyLog(value = "同步接受支付寶通知")
@ResponseBody
@RequestMapping(value="/returnUrl.htm")
public void returnObject(HttpServletRequest request,HttpServletResponse response) throws IOException {
notifyObject(request,response);
}
(2)支付寶掃碼支付
注意:支付寶支付,先要初始化加載zfbinfo.properties檔案
static {
Configs.init("zfbinfo.properties");
}
private static AlipayTradeService tradeService = (AlipayTradeService)(new AlipayTradeServiceImpl.ClientBuilder()).build();
/**
* 支付寶訂單預創建
* @param payWay
* @return
* @throws IOException
*/
public void toPrecreate(PayWay payWay) throws IOException {
String outTradeNo = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32); //訂單號
String subject = payWay.getGoodsName();//商戶名稱
String totalAmount = (payWay.getAmount() * 0.01D) + "";//訂單金額
String undiscountableAmount = "0";
String sellerId = "";
String body = payWay.getGoodsName() + payWay.getNum() + "張共" + totalAmount + "元";
String operatorId = payWay.getOperatorId();
String storeId = String.valueOf(payWay.getStationID());
String terminalId = (payWay.getDevID().length() != 8) ? payWay.getDevID().substring(2, 10) : payWay.getDevID();
ExtendParams extendParams = new ExtendParams();
extendParams.setSysServiceProviderId("填寫支付寶商戶號");//商戶號
String timeoutExpress = "2m";//支付寶二維碼過期時間
List<GoodsDetail> goodsDetailList = new ArrayList<>();
GoodsDetail goods1 = GoodsDetail.newInstance("掃碼支付", payWay.getGoodsName(), payWay.getPrice(), Integer.valueOf(payWay.getNum()));
goodsDetailList.add(goods1);
AlipayTradePrecreateRequestBuilder builder = (new AlipayTradePrecreateRequestBuilder()).setSubject(subject)
.setTotalAmount(totalAmount).setOutTradeNo(outTradeNo).setUndiscountableAmount(undiscountableAmount)
.setSellerId(sellerId).setBody(body).setOperatorId(operatorId).setStoreId(storeId).setTerminalId(terminalId)
.setExtendParams(extendParams).setTimeoutExpress(timeoutExpress)
.setNotifyUrl("填寫支付回呼通知地址").setGoodsDetailList(goodsDetailList);
//發起向支付寶服務端預創建請求,并回傳創建結果
AlipayF2FPrecreateResult result = tradeService.tradePrecreate(builder);
switch (result.getTradeStatus()) {
case SUCCESS:
log.info("訂單號{" + outTradeNo + "}創建成功");
AlipayTradePrecreateResponse resp = result.getResponse();
this.dumpResponse(resp);
String path="D://alipay";
File folder = new File(path);
if (!folder.exists()) {
folder.setWritable(true);
folder.mkdirs();
}
String filePath = String.format(path +"/qr-%s.png", new Object[] { resp.getOutTradeNo() });
//將二維碼保存到本地filePath目錄
ZxingUtils.getQRCodeImge(result.getResponse().getQrCode(), 256, filePath);
case FAILED:
log.error("訂單號: " + outTradeNo + ",支付寶下單失敗");
case UNKNOWN:
log.error("訂單號: " + outTradeNo + ",預創建失敗,回傳例外");
}
}
在支付寶商戶后臺配置回到地址,在支付寶處理完業務(比如簽約、解約、支付等),用戶回呼接收之后處理具體業務,接收成功需要給支付寶回傳success字串,否則支付寶側25小時以內完成8次通知(通知的間隔頻率一般是4m,10m,10m,1h,2h,6h,15h)
/**
* 異步接受支付寶掃碼支付通知
* @param request
* @param response
* @throws IOException
*/
@ResponseBody
@RequestMapping(value="/facePayNotifyUrl.htm")
public void facePayNotifyUrl(HttpServletRequest request, HttpServletResponse response) throws IOException {
String charset = "UTF-8"; // 編碼
String publicKey = "填寫支付寶公鑰"; //支付寶公鑰
String singType = "RSA2"; //簽名方式
Map<String, String> params = new HashMap<String, String>();
Map<String, String[]> requestParams = request.getParameterMap();
log.info("============異步接受支付寶掃碼支付通知.請求引數:"+ JSON.toJSONString(requestParams));
for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {
String name = iter.next();
String[] values = requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ",";
}
params.put(name, valueStr);
}
try {
boolean validation = AlipaySignature.rsaCheckV1(params, publicKey, charset,singType);
if (validation) {
String gmt_create = params.get("gmt_create");
String seller_email = params.get("seller_email");
String subject = params.get("subject");
String buyer_id = params.get("buyer_id");
String invoice_amount = params.get("invoice_amount");
String notify_id = params.get("notify_id");
String trade_status = params.get("trade_status");
String receipt_amount = params.get("receipt_amount");
String app_id = params.get("app_id");
String buyer_pay_amount = params.get("buyer_pay_amount");
String sign_type = params.get("sign_type");
String seller_id = params.get("seller_id");
String gmt_payment = params.get("gmt_payment");
String notify_time = params.get("notify_time");
String version = params.get("version");
String out_trade_no = params.get("out_trade_no");
String total_amount = params.get("total_amount");
String trade_no = params.get("trade_no");
String auth_app_id = params.get("auth_app_id");
String buyer_logon_id = params.get("buyer_logon_id");
String point_amount = params.get("point_amount");
if("TRADE_SUCCESS".equals(trade_status)){
log.info("支付寶交易成功,自行處理業務");
}else{
log.error("支付寶交易失敗,排查原因");
}
//給支付寶回傳success,否則支付寶會連續多次發送
response.getOutputStream().write("success".getBytes());
response.flushBuffer();
}
} catch (AlipayApiException e) {
log.error("=======異步接受支付寶掃碼支付通知,回呼例外");
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
三、銀聯支付
我們比較常見的是銀聯簽約免密支付、銀聯閃付、銀聯掃碼支付等
(1)銀聯簽約免密支付
銀聯簽約免密支付一般在商家app內填寫銀行卡相關資訊(姓名、手機號、銀行卡號、證件號碼等),跳轉到銀聯頁面進行簽約,商戶端不需要保存銀行卡的相關資訊,銀聯側會回傳簽約后的token資訊,支付時使用token去支付(拼接token引數),類比支付寶代扣拿簽約時的協議號去支付,

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import com.cn.Controller.unionpay.sdk.*;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* 銀聯代扣支付邏輯:
* APP原生頁面填寫用戶姓名、手機號、身份證號碼、銀行卡號,點擊確定呼叫APP后臺介面獲取銀聯簽約跳轉頁面(后臺呼叫銀聯側介面生成銀聯簽約跳轉頁面),跳轉到銀聯簽約(業務開通)頁面(此頁面是銀聯側的頁面),獲取短信驗證碼確認開通;
* 若開通成功,銀聯后臺通知商戶(銀聯頁面自動回傳至商戶頁面),商戶保存銀聯回傳的銀行卡號末4位數字與token的對應關系,
*/
@Slf4j
@RequestMapping("/unionpay")
@Controller
public class UnionpayController {
private static String merId = "填寫商戶id";
private static String trId ="99988877766 ";//生產環境由業務分配,測驗環境可以使用99988877766
static {
SDKConfig.getConfig().loadPropertiesFromSrc();
}
/**
* 銀聯側開通
* 商戶APP內輸入姓名、手機號、身份證號碼、銀行卡號(輸入資訊的頁面是app自己的),輸入完成呼叫APP后臺服務,后臺服務呼叫銀聯側開通介面,獲取到銀聯回傳的自動跳轉的Html表單給app,app去跳轉(此時跳轉后的頁面是銀聯的)
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@ResponseBody
@RequestMapping(value="/tokenOpenCard")
public void tokenOpenCard(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/html; charset="+ DemoBase.encoding);
String certifId = req.getParameter("idcard");//用戶身份證號碼
String customerNm=req.getParameter("username");//用戶姓名
String phoneNo = req.getParameter("mobile");//用戶手機號
String accNo = req.getParameter("bankNo");//用戶銀行卡號
Map<String, String> contentData = new HashMap<String, String>();
/***銀聯全渠道系統,產品引數,除了encoding自行選擇外其他不需修改***/
contentData.put("version", DemoBase.version); //版本號
contentData.put("encoding", DemoBase.encoding); //字符集編碼 可以使用UTF-8,GBK兩種方式
contentData.put("signMethod", SDKConfig.getConfig().getSignMethod()); //簽名方法
contentData.put("txnType", "79"); //交易型別 79:開通交易
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", DemoBase.getOrderId()); //商戶訂單號,8-40位數字字母,不能含“-”或“_”,可以自行定制規則
contentData.put("txnTime", DemoBase.getCurrentTime()); //訂單發送時間,格式為yyyyMMddHHmmss,必須取當前時間,否則會報txnTime無效
contentData.put("accType", "01");
//生產環境由業務分配,測驗環境可以使用99988877766
contentData.put("tokenPayData", "{trId="+trId+"&tokenType=01}");
//選送卡號、手機號、證件型別+證件號、姓名
Map<String,String> customerInfoMap = new HashMap<String,String>();
customerInfoMap.put("certifTp", "01"); //證件型別
customerInfoMap.put("certifId", certifId); //證件號碼
customerInfoMap.put("customerNm", customerNm); //姓名
customerInfoMap.put("phoneNo", phoneNo); //手機號
//如果商戶號開通了【商戶對敏感資訊加密】的權限那么需要對 accNo,pin和phoneNo,cvn2,expired加密(如果這些上送的話),對敏感資訊加密使用:
contentData.put("accNo", AcpService.encryptData(accNo, "UTF-8")); //銀行卡號
contentData.put("encryptCertId",AcpService.getEncryptCertId()); //加密證書的certId,配置在acp_sdk.properties檔案 acpsdk.encryptCert.path屬性下
String customerInfoStr = AcpService.getCustomerInfoWithEncrypt(customerInfoMap,null,DemoBase.encoding);
contentData.put("customerInfo", customerInfoStr);
//前臺通知地址 (需設定為外網能訪問 http https均可),支付成功后的頁面 點擊“回傳商戶”的時候將異步通知報文post到該地址
//如果想要實作過幾秒中自動跳轉回商戶頁面權限,需聯系銀聯業務申請開通自動回傳商戶權限
//注:如果開通失敗的“回傳商戶”按鈕也是觸發frontUrl地址,點擊時是按照get方法回傳的,沒有通知資料回傳商戶
contentData.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 在后臺通知處理程式驗證簽名之前需要撰寫邏輯將這些欄位去掉再驗簽,否則將會驗簽失敗
contentData.put("backUrl", DemoBase.backUrl);
// 訂單超時時間,
// 超過此時間后,除網銀交易外,其他交易銀聯系統會拒絕受理,提示超時, 跳轉銀行網銀交易如果超時后交易成功,會自動退款,大約5個作業日金額返還到持卡人賬戶,
// 此時間建議取支付時的北京時間加15分鐘,
// 超過超時時間調查詢介面應答origRespCode不是A6或者00的就可以判斷為失敗,
contentData.put("payTimeout", new SimpleDateFormat("yyyyMMddHHmmss").format(new Date().getTime() + 15 * 60 * 1000));
/**請求引數設定完畢,以下對請求引數進行簽名并生成html表單,將表單寫入瀏覽器跳轉打開銀聯頁面**/
Map<String, String> reqData = AcpService.sign(contentData,DemoBase.encoding); //報文中certId,signature的值是在signData方法中獲取并自動賦值的,只要證書配置正確即可,
String requestFrontUrl = SDKConfig.getConfig().getFrontRequestUrl(); //獲取請求銀聯的前臺地址:對應屬性檔案acp_sdk.properties檔案中的acpsdk.frontTransUrl
String html = AcpService.createAutoFormHtml(requestFrontUrl,reqData,DemoBase.encoding); //生成自動跳轉的Html表單
LogUtil.writeLog("列印請求HTML,此為請求報文,為聯調排查問題的依據:"+html);
//將生成的html寫到瀏覽器中完成自動跳轉打開銀聯支付頁面;這里呼叫signData之后,將html寫到瀏覽器跳轉到銀聯頁面之前均不能對html中的表單項的名稱和值進行修改,如果修改會導致驗簽不通過
resp.getWriter().write(html);
}
/**
* 前臺通知
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@ResponseBody
@RequestMapping(value="/fontNotify")
public void fontNotify(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String encoding = req.getParameter(SDKConstants.param_encoding);
// 獲取銀聯通知服務器發送的后臺通知引數
Map<String, String> reqParam = getAllRequestParam(req);
LogUtil.printRequestLog(reqParam);
Map<String, String> valideData = null;
if (null != reqParam && !reqParam.isEmpty()) {
Iterator<Map.Entry<String, String>> it = reqParam.entrySet().iterator();
valideData = new HashMap<String, String>(reqParam.size());
while (it.hasNext()) {
Map.Entry<String, String> e = it.next();
String key = (String) e.getKey();
String value = (String) e.getValue();
valideData.put(key, value);
}
}
log.info("===銀聯前臺通知,請求引數:"+ JSON.toJSONString(valideData));
//重要!驗證簽名前不要修改reqParam中的鍵值對的內容,否則會驗簽不過
if (!AcpService.validate(valideData, encoding)) {
LogUtil.writeLog("驗證簽名結果[失敗].");
//驗簽失敗,需解決驗簽問題
} else {
//【注:為了安全驗簽成功才應該寫商戶的成功處理邏輯】交易成功,更新商戶訂單狀態
LogUtil.writeLog("驗證簽名結果[成功],暫不處理具體業務");
/** 交易型別.*/
String txnType = valideData.get("txnType");
/** 接入型別,商戶接入填0 ,不需修改(0:直連商戶, 1: 收單機構 2:平臺商戶)*/
String accessType = valideData.get("accessType");
/** 業務型別.*/
String bizType = valideData.get("bizType");
/* * 應答碼資訊.*/
String respMsg = valideData.get("respMsg");
/** 簽名方法.*/
String signMethod = valideData.get("signMethod");//簽名方法
/* * 簽名公鑰證書*/
//String signPubKeyCert = valideData.get("signPubKeyCert");
/** 版本號.*/
String version = valideData.get("version");
//開通交易
if("79".equals(txnType)){
/** 持卡人資訊.*/
String customerInfo = valideData.get("customerInfo");
/** 發卡機構代碼.*/
String issInsCode = valideData.get("issInsCode");
/** 銀行卡號.*/
String accNo = valideData.get("accNo");
/* * token資訊.*/
String tokenPayData = valideData.get("tokenPayData");
String phoneNo="";
if(null!=customerInfo){
Map<String,String> map = AcpService.parseCustomerInfo(customerInfo, "UTF-8");
log.info("customerInfo明文:"+map);
phoneNo = map.get("phoneNo");
}
//如果是配置了敏感資訊加密,如果需要獲取卡號的明文,可以按以下方法解密卡號
if(null!=accNo){
//回傳的是銀行卡號后四位
accNo = AcpService.decryptData(accNo, "UTF-8");
log.info("accNo明文:"+accNo);
}
if(null!=tokenPayData){
Map<String,String> tokenPayDataMap = SDKUtil.parseQString(tokenPayData.substring(1, tokenPayData.length() - 1));
log.info("tokenPayDataMap明文:"+tokenPayDataMap);
String token = tokenPayDataMap.get("token");//這樣取
log.info("token值:"+token);
//處理自己的業務
}
}
}
//回傳給銀聯服務器http 200 狀態碼
resp.getWriter().print("200");
}
/**
* 獲取請求引數中所有的資訊
* 當商戶上送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))) {
res.remove(en);
}
}
}
return res;
}
}
其中涉及到的公共類DemoBase、AcpService、SDKConfig、SDKConstants、SDKUtil 等直接用官方提供的demo中的示例,demo下載地址:無跳轉支付
銀聯代扣支付:
private static String merId = "填寫商戶id";
private static String trId ="99988877766 ";//生產環境由業務分配,測驗環境可以使用99988877766
/**
* 銀聯代扣
* @param orderId 訂單編號
* @param transAmount 訂單金額,單位分
* @param token 支付賬號
* @return
*/
public void unionpayWithhold(String orderId, String transAmount, String token) {
Map<String, String> contentData = new HashMap<String, String>();
/***銀聯全渠道系統,產品引數,除了encoding自行選擇外其他不需修改***/
contentData.put("version", DemoBase.version); //版本號
contentData.put("encoding", DemoBase.encoding); //字符集編碼 可以使用UTF-8,GBK兩種方式
contentData.put("signMethod", SDKConfig.getConfig().getSignMethod()); //簽名方法
contentData.put("txnType", "01"); //交易型別 01-消費
contentData.put("txnSubType", "01"); //交易子型別 01-消費
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); //商戶訂單號,如上送短信驗證碼,請填寫獲取驗證碼時一樣的orderId,此處默認取demo演示頁面傳遞的引數
contentData.put("txnTime", DemoBase.getCurrentTime()); //訂單發送時間,如上送短信驗證碼,請填寫獲取驗證碼時一樣的txnTime,此處默認取demo演示頁面傳遞的引數
contentData.put("currencyCode", "156"); //交易幣種(境內商戶一般是156 人民幣)
contentData.put("txnAmt", transAmount); //交易金額,單位分,如上送短信驗證碼,請填寫獲取驗證碼時一樣的txnAmt,此處默認取demo演示頁面傳遞的引數
contentData.put("accType", "01"); //賬號型別
//后臺通知地址(需設定為【外網】能訪問 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 在后臺通知處理程式驗證簽名之前需要撰寫邏輯將這些欄位去掉再驗簽,否則將會驗簽失敗
contentData.put("backUrl", DemoBase.backUrl);
//消費:token號(從前臺開通的后臺通知中獲取或者后臺開通的回傳報文中獲取),驗證碼看業務配置(默認要短信驗證碼),
contentData.put("tokenPayData", "{token="+token+"&trId="+trId+"}");
Map<String,String> customerInfoMap = new HashMap<String,String>();
customerInfoMap.put("smsCode", "111111"); //短信驗證碼
//customerInfoMap不送pin的話 該方法可以不送 卡號
String customerInfoStr = AcpService.getCustomerInfo(customerInfoMap,null,DemoBase.encoding);
contentData.put("customerInfo", customerInfoStr);
/**對請求引數進行簽名并發送http post請求,接收同步應答報文**/
Map<String, String> reqData = AcpService.sign(contentData,DemoBase.encoding); //報文中certId,signature的值是在signData方法中獲取并自動賦值的,只要證書配置正確即可,
String requestBackUrl = SDKConfig.getConfig().getBackRequestUrl(); //交易請求url從組態檔讀取對應屬性檔案acp_sdk.properties中的 acpsdk.backTransUrl
Map<String, String> rspData = AcpService.post(reqData,requestBackUrl,DemoBase.encoding); //發送請求報文并接受同步應答(默認連接超時時間30秒,讀取回傳結果超時時間30秒);這里呼叫signData之后,呼叫submitUrl之前不能對submitFromData中的鍵值對做任何修改,如果修改會導致驗簽不通過
/**對應答碼的處理,請根據您的業務邏輯來撰寫程式,以下應答碼處理邏輯僅供參考------------->**/
//應答碼規范參考open.unionpay.com幫助中心 下載 產品介面規范 《平臺接入介面規范-第5部分-附錄》
StringBuffer parseStr = new StringBuffer("");
if(!rspData.isEmpty()){
if(AcpService.validate(rspData, DemoBase.encoding)){
LogUtil.writeLog("驗證簽名成功");
String respCode = rspData.get("respCode") ;
if(("00").equals(respCode)){
//交易已受理(不代表交易已成功),等待接收后臺通知更新訂單狀態,也可以主動發起 查詢交易確定交易狀態,
}else if(("03").equals(respCode)|| ("04").equals(respCode)|| ("05").equals(respCode)){
//后續需發起交易狀態查詢交易確定交易狀態
}else{
//其他應答碼為失敗請排查原因
}
}else{
LogUtil.writeErrorLog("驗證簽名失敗");
//TODO 檢查驗證簽名失敗的原因
}
}else{
//未回傳正確的http狀態
LogUtil.writeErrorLog("未獲取到回傳報文或回傳http狀態碼非200");
}
}
(2)銀聯掃碼支付
銀聯掃碼支付與支付寶掃碼支付類似,先要加載銀聯掃碼支付組態檔acp_sdk.properties
private static String merId = "填寫商戶id";
static {
SDKConfig.getConfig().loadPropertiesFromSrc();
}
/**
* 生成銀聯二維碼,預創建訂單
* @return
* @throws Exception
*/
@RequestMapping(value="/tradePrecreate", method = RequestMethod.POST)
@ResponseBody
public void toBankPrecreate(PayWay payWay) throws Exception {
Map<String, String> contentData = new HashMap<String, String>();
/***銀聯全渠道系統,產品引數,除了encoding自行選擇外其他不需修改***/
contentData.put("version", DemoBase.version); //版本號 全渠道默認值
contentData.put("encoding", DemoBase.encoding); //字符集編碼 可以使用UTF-8,GBK兩種方式
contentData.put("signMethod", SDKConfig.getConfig().getSignMethod()); //簽名方法
contentData.put("txnType", "01"); //交易型別 01:消費
contentData.put("txnSubType", "07"); //交易子類 07:申請消費二維碼
contentData.put("bizType", "000000"); //填寫000000
contentData.put("channelType", "08"); //渠道型別 08手機
/***商戶接入引數***/
contentData.put("merId", merId); //商戶號碼,請改成自己申請的商戶號或者open上注冊得來的777商戶號測驗
contentData.put("accessType", "0"); //接入型別,商戶接入填0 ,不需修改(0:直連商戶, 1: 收單機構 2:平臺商戶)
String orderId = DemoBase.getOrderId();
contentData.put("orderId", orderId); //商戶訂單號,8-40位數字字母,不能含“-”或“_”,可以自行定制規則
contentData.put("txnTime", DemoBase.getCurrentTime());
contentData.put("txnAmt", String.valueOf(payWay.getAmount())); //交易金額 單位為分,不能帶小數點
contentData.put("currencyCode", "156"); //境內商戶固定 156 人民幣
contentData.put("backUrl", DemoBase.backUrl);
/**對請求引數進行簽名并發送http post請求,接收同步應答報文**/
Map<String, String> reqData = AcpService.sign(contentData,DemoBase.encoding); //報文中certId,signature的值是在signData方法中獲取并自動賦值的,只要證書配置正確即可,
String requestAppUrl = SDKConfig.getConfig().getBackRequestUrl(); //交易請求url從組態檔讀取對應屬性檔案acp_sdk.properties中的 acpsdk.backTransUrl
Map<String, String> rspData = AcpService.post(reqData,requestAppUrl,DemoBase.encoding); //發送請求報文并接受同步應答(默認連接超時時間30秒,讀取回傳結果超時時間30秒);這里呼叫signData之后,呼叫submitUrl之前不能對submitFromData中的鍵值對做任何修改,如果修改會導致驗簽不通過
/**對應答碼的處理,請根據您的業務邏輯來撰寫程式,以下應答碼處理邏輯僅供參考------------->**/
JSONObject jsonObject = new JSONObject();
if (!rspData.isEmpty()) {
if(AcpService.validate(rspData, DemoBase.encoding)){
LogUtil.writeLog("驗證簽名成功");
String respCode = rspData.get("respCode");
if (("00").equals(respCode)) {
String path="D://unionpay";
File folder = new File(path);
if (!folder.exists()) {
folder.setWritable(true);
folder.mkdirs();
}
//根據web訂單號,生成路徑,生成二維碼
String qrPath = String.format(path + "/qr-%s.png", orderId);
String qrCode =rspData.get("qrCode");
ZxingUtils.getQRCodeImge(qrCode, 256, qrPath);
} else {
//其他應答碼為失敗請排查原因或做失敗處理
}
} else {
//驗證簽名失敗
}
} else {
//未回傳正確的http狀態
}
}
其中涉及到的公共類DemoBase、AcpService、SDKConfig 直接用官方提供的demo中的示例,demo下載地址:在線網關支付
(3)銀聯閃付

a.客戶選擇云閃付支付,提交訂單給商戶后端,后端向銀聯后端請求tn(流水號);
b.商戶后端請求到tn,回傳給用戶的客戶端;
c.客戶端將tn,schema,viewController和mode傳入到銀聯SDK中,喚起云閃付app;
d.云閃付回傳用戶客戶端,將支付結果傳給客戶端,同時商戶后端也能收到銀聯后端的支付結果;
e.云閃付的支付結果最好以商戶后端結果為準,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/273657.html
標籤:其他
上一篇:Java多執行緒-任務拒絕策略
