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组。
* 获得符合 InetAddress instanceof Inet4Address
条件的一个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 = "" + " "
+ " " + " ";
//处理自己的而业务
} else {
resXml = "" + " "
+ " " + " ";
}
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
/**
* 退款结果通知
*
* 在申请退款接口中上传参数“notify_url”以开通该功能
* 如果链接无法访问,商户将无法接收到微信通知。
* 通知url必须为直接可访问的url,不能携带参数。示例:notify_url:“https://pay.weixin.qq.com/wxpay/pay.action”
*
* 当商户申请的退款有结果后,微信会把相关结果发送给商户,商户需要接收处理,并返回应答。
* 对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。
* (通知频率为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 = "" + " "
+ " " + " ";
WXPayUtil.getLogger().error("========================refund:微信支付回调:退款失败");
} else {
// 通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了.
resXml = "" + " "
+ " " + " ";
WXPayUtil.getLogger().info("微信支付回调:退款成功");
}
} else {
resXml = "" + " "
+ " " + " ";
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下载地址:在线网关支付
a.客户选择云闪付支付,提交订单给商户后端,后端向银联后端请求tn(流水号);
b.商户后端请求到tn,返回给用户的客户端;
c.客户端将tn,schema,viewController和mode传入到银联SDK中,唤起云闪付app;
d.云闪付返回用户客户端,将支付结果传给客户端,同时商户后端也能收到银联后端的支付结果;
e.云闪付的支付结果最好以商户后端结果为准。