java实现对接建行支付及其回调
最近公司要对接建行支付,也是查了很多资料,走了很多弯路,还问了建行的技术员,现把它记录下来,直接上代码。
调用支付所需常量
@Value("${ccb.MERCHANTID}")
private String MERCHANTID; //商户代码,固定写死的,需要申请
//@Value("${ccb.POSID}") //这里用yml提取出来就会报错..
private String POSID = "xxxxx"; //商户柜台代码,固定写死的,需要申请
@Value("${ccb.BRANCHID}")
private String BRANCHID; //分行代码,固定写死的,需要申请
@Value("${ccb.PUB32TR2}")
private String PUB32TR2; //公钥后30位
private String BEGORDERID = "";
private String ENDORDERID = "";
private String QUPWD = "xxxxx";//这里写建行支付的商户密码
//交易码 这个参数的值是固定的,不可以修改
private String TXCODE = "xxxxx";
/*必输项
1页面形式
2文件返回形式 (提供TXT和XML格式文件的下载)
3 XML页面形式*/
private String SEL_TYPE = "3";
/*不知道干嘛用的*/
private String CHANNEL = "";
/*不知道干嘛用的*/
private String OPERATOR = "";
@Autowired
private JinshiCCBPayMapper jinshiCCBPayMapper;
@Override
public JSONObject CCBPay(JSONObject jsonObject) {
String CURCODE = "01"; //付款币种,固定写01 代表支付金额
String TXCODE = "530550"; //由建行统一分配为530550
String REMARK1 = "";
String REMARK2 = "";
String RETURNTYPE = "3"; // 返回类型,固定参数是3 ,代表是返回带url的支付信息
String TIMEOUT = "";
String ORDERID = String.valueOf(jsonObject.get("orderId")); //订单号 由商户提供,最长40位,不能重复
String PAYMENT = String.valueOf(jsonObject.get("payment"));//支付金额
StringBuffer tmp = new StringBuffer();
tmp.append("MERCHANTID=");
tmp.append(MERCHANTID);
tmp.append("&POSID=");
tmp.append(POSID);
tmp.append("&BRANCHID=");
tmp.append(BRANCHID);
tmp.append("&ORDERID=");
tmp.append(ORDERID);
tmp.append("&PAYMENT=");
tmp.append(PAYMENT);
tmp.append("&CURCODE=");
tmp.append(CURCODE);
tmp.append("&TXCODE=");
tmp.append(TXCODE);
tmp.append("&REMARK1=");
tmp.append(REMARK1);
tmp.append("&REMARK2=");
tmp.append(REMARK2);
tmp.append("&RETURNTYPE=");
tmp.append(RETURNTYPE);
tmp.append("&TIMEOUT=");
tmp.append(TIMEOUT);
tmp.append("&PUB=");
tmp.append(PUB32TR2);
Map map = new HashMap();
map.put("CCB_IBSVersion", "V6");
map.put("MERCHANTID", MERCHANTID);
map.put("BRANCHID", BRANCHID);
map.put("POSID", POSID);
map.put("ORDERID", ORDERID);
map.put("PAYMENT", PAYMENT);
map.put("CURCODE", CURCODE);
map.put("TXCODE", TXCODE);
map.put("REMARK1", REMARK1);
map.put("REMARK2", REMARK2);
map.put("RETURNTYPE", RETURNTYPE);
map.put("TIMEOUT", TIMEOUT);
map.put("MAC", MD5.md5Str(tmp.toString()));
// 这个url是建设银行指定的,尽量不要换
String ret = HttpClientUtil.httpPost("https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6", map);
QrURLDemo qrURLDemo = JSON.parseObject(ret, QrURLDemo.class);
// 这个url触发get请求会获取到一个新的页面
String s = HttpClientUtil.httpGet(qrURLDemo.getPAYURL(), "UTF-8");
//获取QRURL
QrURLDemo qrURLDemo1 = JSON.parseObject(s, QrURLDemo.class);
String decode = URLDecoder.decode(qrURLDemo1.getQRURL());
String code = qrURLDemo1.getSUCCESS();
// 安卓通过这个url就可以支付了
JSONObject json = new JSONObject();
json.put("decode",decode); //返回的这个decode就是支付所需跳转的url
json.put("code",code); //这个是状态码
//还会返回其他东西,我这里只需要这两个,所以只写了url和code
return json;
}
附 yml文件里配置的参数
ccb:
# 商户代码,固定写死的,需要申请
MERCHANTID: xxxxxxx
POSID: xxxxxxx # 商户柜台代码,固定写死的,需要申请
BRANCHID: xxxxxxx # 分行代码,固定写死的,需要申请
CURCODE: xxxxxxx #付款币种,固定写01 代表支付金额
TXCODE: xxxxxxx # 由建行统一分配为530550
PUB32TR2: xxxxxxx # 公钥后30位
微信支付界面
支付完成后,建行会自动调用回调地址,这个地址是在建行商户平台配置的,反馈有两种,网页反馈(方法:get)和服务器反馈(方法:post),请看下图
页面反馈:付款人付款完成后,点击“返回商户网站”按钮,触发页面反馈
服务器反馈:只要支付成功,无需触发,由建行支付网关,以post 方法,发信息给反馈URL
还分为网上银行和手机银行反馈,网上银行就是微信支付宝调用url支付,手机银行就是建行手机银行支付客户端,其实手机、网银,区分不是很严格,一般都设置成一样的,同一笔支付,可能会触发多渠道的同时反馈。所以,反馈机制,在响应的时候,是允许重复的。一般来说,服务器、页面,是写成两个不同的回调处理。或者,加个条件判断,同时允许post和get,也行。写成一个,就不太好判断反馈的来源了。写成两个,再通过日志,能区分反馈的来源。
支付完成后,出现这个页面,如果用户不点完成的话,直接左上角叉叉掉,服务器反馈有,页面反馈没有
付款这个环节不能携带任何参数显示出来
我这里页面反馈和服务器反馈写的不同方法,请求方式不同,服务器反馈主要是操作数据库,更新支付成功信息到数据库,页面反馈主要是用来展示成功的页面,订单信息等等展示给用户
/**
* 支付回调(页面反馈 get)付款人付款完成后,点击“返回商户网站”按钮,触发页面反馈。
*
* @return
*/
@GetMapping("/payCallBackForPage")
@ResponseBody
public SuccessVo payCallBackForPage(PayCallBackEntity payCallBackEntity,
HttpServletResponse response) throws Exception {
System.out.println("payCallBackEntity = " + payCallBackEntity);
String success = payCallBackEntity.getSUCCESS();
String orderId = payCallBackEntity.getORDERID();
String payment = payCallBackEntity.getPAYMENT();
System.out.println("success: -" + success);
System.out.println("orderId: -" + orderId);
if ("Y".equals(success)) {
Map<String, Object> map = jinshiCCBPayService.selectByOrderId(orderId);
Object returnCode = map.get("returnCode");
Object returnMsg = map.get("returnMsg");
logger.info("returnCode: " + returnCode);
logger.info("returnMsg: " + returnMsg);
if ("000000".equals(returnCode)) {
//返回 "000000" 说明此订单号已支付成功
//写支付成功以后的操作
//todo
}
} else {
//支付失败
}
return new SuccessVo();
}
其中用到了根据订单号查询订单的方法,如下:
@Override
public Map<String, Object> selectByOrderId(String orderId) {
//订单号 如果有了订单号,下面的 ORDERDATE BEGORDERTIME ENDORDERTIME 就无效了..所以就置空了.
String ORDERID = orderId;
String ORDERDATE = "20200114"; // 因为有ORDERID,所以这个字段无效了,但是不能删
String BEGORDERTIME = "00:00:00";// 因为有ORDERID,所以这个字段无效了,但是不能删
String ENDORDERTIME = "23:59:59";// 因为有ORDERID,所以这个字段无效了,但是不能删
//txcode=410408
/* 流程类型
必输项
0支付流水
1退款流水*/
String TYPE = "0";
/*必输项(当日只有未结算流水可供查询)
0 未结算流水
1 已结算流水*/
String KIND = "1";
/*必输项
0失败
1成功
2不确定
3全部(已结算流水查询不支持全部)*/
String STATUS = "1";
//页码必输项,输入将要查询的页码。
String PAGE = "1";
String xmlString = this.getStringByHttpClient(ORDERDATE, BEGORDERTIME, ENDORDERTIME, ORDERID, TYPE, KIND, STATUS, PAGE);
Document document = (Document) this.getDocumentByXMLStr(xmlString);
List<QUERYORDER> queryorders = document.getQUERYORDER();
Map<String, Object> resultMap = new HashMap<>();
if (!CollectionUtils.isEmpty(queryorders)) { // 如果没有查询到定单就是空的
QUERYORDER queryOrder = queryorders.get(0); //因为只有一个结果,所以就获取索引为0的元素
resultMap.put("queryOrder", queryOrder);
}
//不管查询成功查询失败都会有这个消息
resultMap.put("returnCode", document.getRETURN_CODE());
resultMap.put("returnMsg", document.getRETURN_MSG());
return resultMap;
}
/**
* 发送请求获取 String格式的字符串
*
* @param ORDERDATE
* @param BEGORDERTIME
* @param ENDORDERTIME
* @param ORDERID
* @param TYPE
* @param KIND
* @param STATUS
* @param PAGE
* @return string 格式的xml
*/
private String getStringByHttpClient(Object ORDERDATE, Object BEGORDERTIME, Object ENDORDERTIME, String ORDERID, String TYPE, String KIND, String STATUS, String PAGE) {
String param = "MERCHANTID=" + MERCHANTID + "&BRANCHID=" + BRANCHID + "&POSID=" + POSID + "&ORDERDATE=" + ORDERDATE + "&BEGORDERTIME=" + BEGORDERTIME
+ "&ENDORDERTIME=" + ENDORDERTIME + "&BEGORDERID=" + BEGORDERID + "&ENDORDERID=" + ENDORDERID + "&QUPWD=&TXCODE=" + TXCODE
+ "&SEL_TYPE=" + SEL_TYPE + "&OPERATOR=" + OPERATOR;
if ("410408".equals(TXCODE)) {
param = "MERCHANTID=" + MERCHANTID + "&BRANCHID=" + BRANCHID + "&POSID=" + POSID + "&ORDERDATE="
+ ORDERDATE + "&BEGORDERTIME=" + BEGORDERTIME + "&ENDORDERTIME=" + ENDORDERTIME + "&ORDERID="
+ ORDERID + "&QUPWD=&TXCODE=" + TXCODE + "&TYPE=" + TYPE + "&KIND=" + KIND + "&STATUS=" + STATUS +
"&SEL_TYPE=" + SEL_TYPE + "&PAGE=" + PAGE + "&OPERATOR=" + OPERATOR + "&CHANNEL=" + CHANNEL;
}
Map map = new HashMap();
map.put("MERCHANTID", MERCHANTID);
map.put("BRANCHID", BRANCHID);
map.put("POSID", POSID);
map.put("ORDERDATE", ORDERDATE);
map.put("BEGORDERTIME", BEGORDERTIME);
map.put("ENDORDERTIME", ENDORDERTIME);
map.put("BEGORDERID", BEGORDERID);
map.put("ENDORDERID", ENDORDERID);
map.put("QUPWD", QUPWD);
map.put("TXCODE", TXCODE);
if ("410408".equals(TXCODE)) {
map.put("TYPE", TYPE);
map.put("KIND", KIND);
map.put("STATUS", STATUS);
map.put("ORDERID", ORDERID);
map.put("PAGE", PAGE);
map.put("CHANNEL", CHANNEL);
}
map.put("SEL_TYPE", SEL_TYPE);
map.put("OPERATOR", OPERATOR);
map.put("MAC", MD5.md5Str(param));
// 调用银行的接口 基本是固定的地址
String s = HttpClientUtil.httpPost("https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?", map);
//删除字符串防止解析xml报错
s = s.replaceAll("\\n", "").
replaceAll("\\t", "").
replaceAll("\\r", "");
return s;
}
/**
* 从 string 格式的xml 里面提出取出 Document 实体类
* @param ret string 格式的xml
* @return
*/
private Object getDocumentByXMLStr(String ret) {
XStream xStream = new XStream();
xStream.alias("DOCUMENT", Document.class);
xStream.processAnnotations(Document.class);
XStream.setupDefaultSecurity(xStream);
xStream.allowTypesByWildcard(
new String[]{"com.xxxxx.**"}
);
return xStream.fromXML(ret);
}
以下的实体类省略getset方法
Document 实体类
import com.thoughtworks.xstream.annotations.XStreamAlias;
import com.thoughtworks.xstream.annotations.XStreamImplicit;
import java.util.List;
@XStreamAlias("DOCUMENT")
public class Document {
@XStreamImplicit(itemFieldName ="QUERYORDER")
private List<QUERYORDER> QUERYORDER ; // 查询订单
private String RETURN_CODE; // 返回状态码
private String RETURN_MSG; // 返回消息
private String CURPAGE; //当前页
private String PAGECOUNT; //总页数
private String TOTAL; //总数
private String PAYAMOUNT; //付款方式
private String REFUNDAMOUNT; // 修正???
}
PayCallBackEntity 实体类
/**
* 建行支付回调实体类
*/
public class PayCallBackEntity {
private String POSID; //商户柜台代码
private String BRANCHID;//分行代码
private String ORDERID; //定单号
private String PAYMENT; //付款金额
private String CURCODE; //币种
private String REMARK1; //备注一
private String REMARK2; //备注二
private String ACC_TYPE; //账户类型 服务器通知中有此字段返回且参与验签
private String SUCCESS; //成功标志 成功-Y,失败-N
private String TYPE; //接口类型 分行业务人员在P2员工渠道后台设置防钓鱼的开关。 1.开关关闭时,无此字段返回且不参与验签 2.开关打开时,有此字段返回且参与验签。参数值为 1-防钓鱼接口
private String REFERER; //Referer信息 分行业务人员在P2员工渠道后台设置防钓鱼开关。 1.开关关闭时,无此字段返回且不参与验签。 2.开关打开时,有此字段返回且参与验签
private String CLIENTIP; //客户端IP 分行业务人员在P2员工渠道后台设置防钓鱼的开关。 1.开关关闭时,无此字段返回且不参与验签 2.开关打开时,有此字段返回且参与验签。参数值为 客户在建行系统中的IP
private String ACCDATE; //系统记账日期 商户登陆商户后台设置返回记账日期的开关 1.开关关闭时,无此字段返回且不参与验签。 2.开关打开时,有此字段返回且参与验签。参数值格式为YYYYMMDD(如20100907)。
private String USRMSG; //支付账户信息 分行业务人员在P2员工渠道后台设置防钓鱼开关和返回账户信息的开关。 1.开关关闭时,无此字段返回且不参与验签。2.开关打开但支付失败时,无此字段返回且不参与验签。3.开关打开且支付成功时,有此字段返回且参与验签。无PAYTYPE返回时,参数值格式如下:“姓名|账号加密后的密文”。有PAYTYPE返回时,该参数值为空。
private String USRINFO; //客户加密信息 分行业务人员在P2员工渠道后台设置防钓鱼开关和客户信息加密返回的开关。 1.开关关闭时,无此字段返回且不参与验签
private String PAYTYPE; //支付方式 ALIPAY:支付宝 WEIXIN:微信 为空:建行龙支付 该字段有返回时参与验签,无此字段返回时不参与验签。
private String SIGN; //数字签名
}
httpclient工具类
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
public class HttpClientUtil {
public static String httpReader(String url, String code){
System.out.println("GetPage:"+url);
HttpClient client = new HttpClient();
GetMethod method = new GetMethod(url);
String result = null;
try {
client.executeMethod(method);
int status = method.getStatusCode();
if (status == HttpStatus.SC_OK) {
result = method.getResponseBodyAsString();
} else {
System.out.println("Method failed: " + method.getStatusLine());
}
} catch (HttpException e) {
System.out.println("Please check your provided http address!");
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally{
if(method!=null)method.releaseConnection();
method = null;
client = null;
}
return result;
}
public static String httpGet(String url,String code) {
System.out.println("GetPage:"+url);
String content = null;
HttpClient httpClient = new HttpClient();
httpClient.getParams().setParameter(HttpMethodParams.USER_AGENT,"Mozilla/5.0 (X11; U; Linux i686; zh-CN; rv:1.9.1.2) Gecko/20090803 Fedora/3.5.2-2.fc11 Firefox/3.5.2");
GetMethod method = new GetMethod(url);
try {
int statusCode = httpClient.executeMethod(method);
System.out.println("httpClientUtils::statusCode="+statusCode);
System.out.println(method.getStatusLine());
content = new String(method.getResponseBody(), code);
} catch (Exception e) {
System.out.println("time out");
e.printStackTrace();
} finally {
if(method!=null)method.releaseConnection();
method = null;
httpClient = null;
}
return content;
}
public static String httpPost(String url, Map paramMap, String code) {
System.out.println("GetPage:"+url);
String content = null;
if (url == null || url.trim().length() == 0 || paramMap == null
|| paramMap.isEmpty())
return null;
HttpClient httpClient = new HttpClient();
httpClient.getParams().setParameter(HttpMethodParams.USER_AGENT,"Mozilla/5.0 (X11; U; Linux i686; zh-CN; rv:1.9.1.2) Gecko/20090803 Fedora/3.5.2-2.fc11 Firefox/3.5.2");//
PostMethod method = new PostMethod(url);
Iterator it = paramMap.keySet().iterator();
while (it.hasNext()) {
String key = it.next() + "";
Object o = paramMap.get(key);
if (o != null && o instanceof String) {
method.addParameter(new NameValuePair(key, o.toString()));
}
if (o != null && o instanceof String[]) {
String[] s = (String[]) o;
if (s != null)
for (int i = 0; i < s.length; i++) {
method.addParameter(new NameValuePair(key, s[i]));
}
}
}
try {
int statusCode = httpClient.executeMethod(method);
System.out.println("httpClientUtils::statusCode="+statusCode);
System.out.println(method.getStatusLine());
content = new String(method.getResponseBody(), code);
} catch (Exception e) {
System.out.println("time out");
e.printStackTrace();
} finally {
if(method!=null)method.releaseConnection();
method = null;
httpClient = null;
}
return content;
}
public static String httpPost(String url, Map paramMap) {
return HttpClientUtil.httpPost(url, paramMap, "UTF-8");
}
}
MD5工具类
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MD5 {
public static String md5Str(String str) {
if (str == null) return "";
return md5Str(str, 0);
}
public static String md5Str(String str, int offset) {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] b = str.getBytes("UTF8");
md5.update(b, offset, b.length);
return byteArrayToHexString(md5.digest());
} catch (NoSuchAlgorithmException ex) {
ex.printStackTrace();
return null;
} catch (UnsupportedEncodingException ex) {
ex.printStackTrace();
return null;
}
}
/**
* @param b byte[]
* @return String
*/
public static String byteArrayToHexString(byte[] b) {
String result = "";
for (int i = 0; i < b.length; i++) {
result += byteToHexString(b[i]);
}
return result;
}
private static String[] hexDigits =
{
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b",
"c", "d", "e", "f"};
public static String byteToHexString(byte b) {
int n = b;
if (n < 0) {
n = 256 + n;
}
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
}
QrURLDemo 实体类
/**
* 建行无感支付实体类
*/
public class QrURLDemo {
private String SUCCESS;
private String PAYURL;
private String QRURL; //安卓点这个会直接跳到支付页面
}
QUERYORDER 实体类
public class QUERYORDER {
private String MERCHANTID; //商户代码
private String BRANCHID; //分行代码
private String POSID; //柜台号
private String ORDERID; //订单号
private String ORDERDATE; //订单支付的时间
private String ACCDATE; //访问日期记录
private String AMOUNT; //支付金额
private String STATUSCODE; //状态码
private String STATUS; //交易状态 支付成功的话, 会返回 "成功"
private String REFUND; //退税???
private String SIGN; //签名
}
ami_m0_47026818: 大佬,这个聚合收款二维码可以指定金额么
J.P.C: 支付宝支付商户是直接到银行卡吗
M7CCC: 大佬 通知成功后给建行应答具体给啥,文档里也没有写
人如其名都很棒!: 只改了背景色呀?字体色怎么改呢?
ghgfgjjjn: 大佬 建行接口开发文档还有吗 这面网点业务员说找不到只给了个开发说明