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.云閃付的支付結果最好以商戶后端結果為準。
一個火爆的東西,必然有著讓人們你爭我搶的緣由,就比如現在話題風浪的“小程序”。當小程序在雙十一創造出來的價值被人知曉后,這場流行算是正式彌漫而來。但是,現在技術快速更新的時代,有太多都是曇花一現,讓更多投資者變得有些猶豫,難以抉擇,畢竟風險總是高出能承受的范圍。
就像之間已經先后做過微商,淘寶等渠道營銷,并不理想,那么還要做一個小程序嗎?
小程序真的能給我帶來利益嗎?
小程序的交易量能和現在的巨頭平臺相比較嗎?
現在已經有很多人投進了小程序,我現在才進去還有市場嗎?
既然如此,小編來分享一下建站寶盒為有著這類疑問的商家給的答案。
第一,低開發成本。
傳統的B2C商城,在入駐都需要支付多種費用,還有每筆交易的抽成,還有每個月的平臺使用費,以及保證金,這些都大大增加開店的成本。
但在小程序上開商城,一般只需要承擔兩項費用:店鋪建設、維護成本,以及微信支付的費率。若是使用第三方的模板生成小程序,開發成本加上服務費一年一般不超過1萬。如此比較下來,小程序的經營成本遠遠低于傳統經營。
第二,交易量將不輸巨頭平臺
京東和天貓在去年的業務交易量加起來不到兩萬億。今年雙十一,在微信小程序直播間做銷售的蘑菇街總銷售額較去年增長了2818%,小程序成交占比高于蘑菇街APP的3.2倍。由此我們可以看出小程序所隱藏的潛力值。
第三,支付功能便捷
小程序商城需要申請微信支付,需要營業執照,只限企業和個體商戶,個人是無法申請的,更具有保障。
小程序商城需要申請微信支付,現在申請微信支付需要營業執照,要么是企業,要么是個體商戶,個人是無法申請的。而HTML5商城的支付接口一般是第三方的提供。HTML5商城交易完成后,錢先給到第三方平臺,之后再分給商戶,這中間可能有一定的時間間隔。而小程序是通過微信支付交易,交易完成后,錢直接到打商戶的賬戶上(扣除微信0.6%的分成)。賬期相對更短,這相當于一個較大的升級。
第四,流量來源更多樣
在H5商城時代,真正做微商掙到錢的人無非兩類:1,通過朋友圈做分銷;2,大的自媒體和公眾號;前一類難免存在違規,或者是把熟人坑了一把。后者則多依賴大的自媒體和公眾號。這類的方法爭議不少,而且付出與回報不成正比。
小程序現在提供了很多免費的流量入口,小程序與公眾號的關聯也更為完美,小程序嵌入公眾號的方式也讓用戶的整個閱讀體驗不一樣,交易流程也更為順暢。如小程序入口,二維碼,廣告欄,朋友分享,附近小程序等端口都可以為小程序帶來很多免費的流量。
第五,電商小程序模板豐富,創建零門檻
對于傳統門店,甚至是中小企業來說,想要自己開發小程序牽涉到人力、物力的投入,而開發者也需要學習新的開發語言,門檻較高。現在第三方小程序模版市場有一大批精品電商類模版,完全可以滿足絕大多數的小程序開店需求。不就無需學習開發語言,簡單方便的操作,即可獲得自己的小程序商城,還可以免費試用小程序!
我們都在這科技風暴中,不進則退,我們無法安穩的佇立在這浪涌其中,只能掛帆起航,乘風破浪。還有好多人問到底要不要入駐小程序,那就點擊微信的發現欄目,看看目前開發入駐小程序的有多少,附近的小程序也是每日劇增,你就知道答案了。
微信對小程序的支持力度前所未有的,擁有9.8億用戶的微信決心要做的事,成功的概率太高太高,我們已經在路上,那么你呢?
019年9月18日,中國銀行山東省分行(下稱“中國銀行”)為濟南市公安局出入境管理局成功上線財政非稅刷臉支付業務,成為出入境系統首家刷臉支付收單銀行。
陛下,您的Flash插件已過期,無法播放視頻了
建議您……
升級 Flash 插件
切換到 HTML5 播放器
或者
今年8月份,濟南市公安局出入境管理局向多家財政非稅收單合作銀行發出刷臉支付需求,中國銀行隨即與微信支付、支付寶和銀聯等多方溝通,最后選擇與微信支付合作,開展出入境刷臉支付業務。
刷臉支付屬于支付行業受理側創新業務,在業務測試階段,中國銀行與微信支付等多方對業務方案、接口開發、調試運行等環節進行了多次磋商和嘗試,最終為濟南出入境管理局上線定制化財政非稅刷臉支付產品。該產品結合了“中銀智慧付”與微信刷臉支付功能,可使財政非稅單位同時受理銀行卡、條碼、刷臉等多種支付方式,豐富商戶收款渠道、提升客戶繳費體驗。
2019年,央行擬定個人金融信息保護監管規則,在此新形勢下,移動金融機構如何做好個人信息收集和保護、開放模式下的信息和數據安全如何保障、刷臉支付如何平衡好安全與便捷?
藉此,北京移動金融產業聯盟、移動支付網將于11月5日在深圳舉辦以“安全合規 面向未來”為主題的——MFSC 2019第四屆中國移動金融安全大會,主題演講及報名通道已開啟。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。