Java对接工商银行聚合支付(无界面)(微信小程序支付&回调验签&退款)
写在前面
最近两天整合对接了工商银行的聚合支付通道,目前已上线运行。踩了一些坑,以此记录供网友分享。
此文对接产品的主要是【线上POS聚合消费下单接口(无界面)】
准备工作
1.开发指南、SDK、API官方文档 https://open.icbc.com.cn/icbc/apip/docs_index.html
2.向工行客服申请并获得 APPID、商户编号、公私钥、应用网关、协议编号等等
3.了解RSA公私钥签名的基本概念,建议阅读-> 点击这里
4.下载SDK,https://open.icbc.com.cn/icbc/apip/docs_sdk&demo.html
开干
1、必须要准备的参数
public class IcbcConfig {
/** 聚合支付B2C线上消费下单接口url (无界面) */
public final static String API_PAY_URL = "https://gw.open.icbc.com.cn/api/cardbusiness/aggregatepay/b2c/online/consumepurchase/V1";
/** 退款url */
public final static String REFUND_URL = "https://gw.open.icbc.com.cn/api/cardbusiness/aggregatepay/b2c/online/merrefund/V1";
/** 我方私钥 */
public final static String MY_PRIVATE_KEY = "MIIEvgIBA......";
/** 我方公钥 (可以不用) */
public final static String MY_PUBLIC_KEY = "";
/** 对方(即工行)网关公钥 */
public final static String APIGW_PUBLIC_KEY = "MIGfMA0GCSqGS......";
/** APP的编号,应用在API开放平台注册时生成 eg:10000000000004095781 */
public final static String APP_ID = "10000000000004095781";
/** 商户编号 */
public final static String mer_id = "4402.......";
/** 收单产品协议编号 */
public final static String mer_prtcl_no = "44021......";
/** 设备号 (自定义不超过文档规定长度就行) */
public final static String decive_info = "1124........";
}
2.在工程中引入SDK
把需要的jar包导入本地仓库,并在pom.xml引入
mvn install:install-file -DgroupId=com.xxx -DartifactId=xxx-xxx -Dversion=1.x.x -Dpackaging=jar -Dfile=D:\xxx.jar
需要的jar包有三个(后面签名、验签等操作需要用到)
3.请求下单接口
public static void callPay() {
try {
//注意:这里一般使用的是RSA2签名方式,文档里面默认是RSA
DefaultIcbcClient client = new DefaultIcbcClient(
IcbcConfig.APP_ID, IcbcConstants.SIGN_TYPE_RSA2, IcbcConfig.MY_PRIVATE_KEY, IcbcConfig.APIGW_PUBLIC_KEY);
CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV1 request =
new CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV1();
request.setServiceUrl(IcbcConfig.API_PAY_URL);
CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV1.CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV1Biz bizContent = new
CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV1.CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV1Biz();
request.setBizContent(bizContent);
bizContent.setMer_id(IcbcConfig.mer_id);
bizContent.setMer_prtcl_no(IcbcConfig.mer_prtcl_no);
bizContent.setDecive_info(IcbcConfig.decive_info);
bizContent.setOut_trade_no("test123");
//交易日期时间,需要格式化为yyyyMM-dd'T'HH:mm:ss
bizContent.setOrig_date_time("2019‐07‐09T12:11:03");
//交易币种,目前工行只支持使用人民币(001)支付
bizContent.setFee_type("001");
bizContent.setSpbill_create_ip("122.12.12.12");
//金额,单位分
bizContent.setTotal_fee("100");
//支付成功后回调url
bizContent.setMer_url("http://www.test.com/testNotify123");
bizContent.setBody("测试支付");
//收单接入方式,5-APP,7-微信公众号,8-支付宝生活号,9-微信小程序
bizContent.setAccess_type("9");
//支付方式,9-微信;10-支付宝;
bizContent.setPay_mode("9");
//商户在微信开放平台注册的APPID,支付方式为微信时不能为空
bizContent.setShop_appid("wx8888888888888888");
// //第三方用户标识,商户在支付宝生活号接入时必送,即access_type为8时,上送用户的唯一标识;商户通过微信公众号内或微信小程序接入时不送
// bizContent.setUnion_id("");
//第三方用户标识,商户在微信公众号内或微信小程序内接入时必送,即access_type为7或9时,上送用户在商户APPID下的唯一标识;商户通过支付宝生活号接入时不送
bizContent.setOpen_id("");
bizContent.setIcbc_appid(IcbcConfig.APP_ID);
// bizContent.setMer_acct("6212880200000038618");
bizContent.setExpire_time("120");
//附加数据,在查询API和支付通知中原样返回,该字段主要用于商户携带订单的自定义数据
bizContent.setAttach("");
//通知类型,表示在交易处理完成后把交易结果通知商户的处理模式。取值“HS”:在交易完成后将通知信息,主动发送给商户,发送地址为notify_url指定地址; 取值“AG”:在交易完成后不通知商户。不送或送空,默认为"HS"
bizContent.setNotify_type("HS");
//结果发送类型,通知方式为HS时有效。取值“0”:无论支付成功或者失败,银行都向商户发送交易通知信息;取值“1”,银行只向商户发送交易成功的通知信息。默认是"0"
bizContent.setResult_type("0");
//支付方式限定,上送”no_credit“表示不支持信用卡支付;上送“no_balance”表示仅支持银行卡支付;不上送或上送空表示无限制
bizContent.setPay_limit("");
//订单附加信息
bizContent.setOrder_apd_inf("");
CardbusinessAggregatepayB2cOnlineConsumepurchaseResponseV1 response;
try {
response = client.execute(request);
if (response.getReturnCode() == 0) {
// 6、业务成功处理,请根据接口文档用response.getxxx()获取同步返回的业务数据
System.out.println("ReturnCode:"+response.getReturnCode());
System.out.println("response:" + JSON.toJSONString(response));
//response.getWx_data_package() 就是微信小程序调起支付所需要的参数(需要重新赋值一下字段名,工商银行的参数有的驼峰有的没有,很迷)
} else {
// 失败
System.out.println("response:" + JSON.toJSONString(response));
System.out.println("ReturnCode:"+response.getReturnCode());
System.out.println("ReturnMsg:"+response.getReturnMsg());
}
} catch (IcbcApiException e) {
log.error(e.toString(), e);
}
} catch (Exception e) {
log.error(e.toString(), e);
}
}
4.支付回调及验签
@PostMapping("/icbcWxNotify")
@ResponseBody
public void icbcWxNotify(HttpServletRequest request, HttpServletResponse response){
PrintWriter out = null;
try {
//签名是否验证成功
boolean signResult = false;
//响应参数
JSONObject bizContent = JSON.parseObject(request.getParameter("biz_content"));
//我们的订单编号
String orderCode = bizContent.getString("out_trade_no");
//订单金额(单位分)
String totalFee = bizContent.getString("total_amt");
//第三方订单流水号
String transactionId = bizContent.getString("third_trade_no");
//返回码
String returnCode = bizContent.getString("return_code");
//消息号
String msgId = bizContent.getString("msg_id");
// 1.验证签名
// 注意:当notify_url=http://122.20.29.133:8080/testNotify123时,notifyUrl=testNotify123
String notifyUrl = "testNotify123";
if (checkSign(request, notifyUrl)) {
signResult = true;
log.info("订单{}【工商银行】微信回调签名认证成功", orderCode);
if ("0".equals(returnCode)) {
// 返回码,交易成功返回0,其他表示业务报错 注意:调起支付未支付也会回调,所以此处必须判断return_code=0
// 2.执行我们的业务逻辑处理
//executeLogic(orderCode, totalFee, transactionId);
}
} else {
log.error("订单{}【工商银行】微信支付回调签名认值失败!", orderCode);
}
// 3.应答工行:在接收到工行的支付结果通知后,一定要返回应答,否则工行会认为该通知失败,在一定时间区间内多次发起通知
String results = notifyStr(signResult, msgId);
response.setContentType("application/json; charset=utf-8");
out = response.getWriter();
out.write(results);
} catch (Exception e) {
log.error("【工商银行】微信支付回调处理失败,请检查原因!!!,{}", e.getMessage());
} finally {
out.flush();
out.close();
}
}
/**
* 验证签名
* @param request 回调请求
* @param path 我方回调url
* @return true:验证成功 false:验证失败
*/
public static boolean checkSign(HttpServletRequest request, String path) {
Map<String, String> params = Maps.newHashMap();
String from = request.getParameter("from");
String api = request.getParameter("api");
String app_id = request.getParameter("app_id");
String charset = request.getParameter("charset");
String format = request.getParameter("format");
String timestamp = request.getParameter("timestamp");
String biz_content = request.getParameter("biz_content");
String sign_type = request.getParameter("sign_type");
String sign = request.getParameter("sign");
params.put("from", from);
params.put("api", api);
params.put("app_id", app_id);
params.put("charset", charset);
params.put("format", format);
params.put("timestamp", timestamp);
params.put("biz_content", biz_content);
//目前上行网关签名暂时仅支持RSA
params.put("sign_type", sign_type);
String signStr= WebUtils.buildOrderedSignStr(path, params);
try {
//注意:此次用的是RSA,和下单时不一样
return IcbcSignature.verify(signStr, IcbcConstants.SIGN_TYPE_RSA, IcbcConfig.APIGW_PUBLIC_KEY, charset, sign);
} catch (IcbcApiException e) {
return false;
}
}
/**
* 支付回调应答字符串
* @param checkResult 回调后签名是否校验成功
* @param msgId 消息号
* @return str
*/
public static String notifyStr(boolean checkResult, String msgId) {
// return_code为数字,成功时为0
String responseBizContent;
if (checkResult) {
responseBizContent = "{\"return_code\":0,\"return_msg\":\"success\",\"msg_id\":\"" + msgId + "\"}";
} else {
responseBizContent = "{\"return_code\":-1,\"return_msg\":\"icbc sign not pass.\"}";
}
// sign_type-商户在工行登记app的签名类型保持一致,一般为RSA2
// 返回字符串顺序不能变,为response_biz_content、sign_type、sign,中间不含空格换行符;
String signStr = "\"response_biz_content\":" + responseBizContent + "," + "\"sign_type\":\"RSA2\"";
String sign = null;
try {
sign = IcbcSignature.sign(signStr, IcbcConstants.SIGN_TYPE_RSA2, IcbcConfig.MY_PRIVATE_KEY,"UTF-8","");
} catch (IcbcApiException e) {
log.error("【工商银行】微信回调-应答签名失败", e);
}
return "{" + signStr + ",\"sign\":\"" + sign + "\"}";
}
5.退款
/**
* 退款
* @param orderCode 订单编号
* @param refundCode 退款编号
* @param refundMoney 部分退款金额
* @return true:退款成功 false:退款失败
*/
public static boolean refund(String orderCode, String refundCode, BigDecimal refundMoney) {
DefaultIcbcClient client = new DefaultIcbcClient(
IcbcConfig.APP_ID, IcbcConstants.SIGN_TYPE_RSA2, IcbcConfig.MY_PRIVATE_KEY, IcbcConfig.APIGW_PUBLIC_KEY);
CardbusinessAggregatepayB2cOnlineMerrefundRequestV1 request =
new CardbusinessAggregatepayB2cOnlineMerrefundRequestV1();
//根据测试环境和生产环境替换相应ip和端口
request.setServiceUrl(IcbcConfig.REFUND_URL);
//请对照接口文档用bizContent.setxxx()方法对业务上送数据进行赋值
CardbusinessAggregatepayB2cOnlineMerrefundRequestV1.CardbusinessAggregatepayB2cOnlineMerrefundRequestV1Biz bizContent = new
CardbusinessAggregatepayB2cOnlineMerrefundRequestV1.CardbusinessAggregatepayB2cOnlineMerrefundRequestV1Biz();
request.setBizContent(bizContent);
//工行订单号,工行订单号,商户订单号或行内订单号必须其中一个不为空
bizContent.setOrder_id("");
//商户编号‐必输项
bizContent.setMer_id(IcbcConfig.mer_id);
//商户订单号,工行订单号,商户订单号或行内订单号必须其中一个不为空
bizContent.setOut_trade_no(orderCode);
//退货流水号,商户系统生成的退款编号,每次部分退款需生成不同的退款编号
bizContent.setOuttrx_serial_no(refundCode);
//退货总金额‐必输项
BigDecimal realRefundMoney = MathUtil.multiply(refundMoney, 100, 0);
bizContent.setRet_total_amt(realRefundMoney.toString());
//交易币种‐必输项
bizContent.setTrnsc_ccy("001");
bizContent.setOrder_apd_inf("");
bizContent.setIcbc_appid(IcbcConfig.APP_ID);
bizContent.setMer_acct("");
CardbusinessAggregatepayB2cOnlineMerrefundResponseV1 response;
try {
response = client.execute(request);
if (response.isSuccess()) {
// 业务成功处理,请根据接口文档用response.getxxx()获取同步返回的业务数据
// System.out.println("ReturnCode:"+response.getReturnCode());
// System.out.println("response:" + response);
return true;
} else {
// 失败
// System.out.println("ReturnCode:"+response.getReturnCode());
// System.out.println("ReturnMsg:"+response.getReturnMsg());
}
} catch (IcbcApiException e) {
log.error(e.toString(), e);
}
return false;
}
总结
1.工行的支付通道其实就是他们自己封装了一下支付参数,原理上还是调用微信的接口
2.支付的订单流水记录不在我们自己的微信商户后台,在工商银行那边,查询记录要在他们的系统上面查询,比较麻烦,最好在自己的系统上区分一下工商银行通道的支付和官方微信支付
3.最好使用他们的SDK来进行签名和验签等操作,避免不必要的麻烦
4.如果使用APP-微信支付,流程是APP调起自己的微信小程序,再使用微信小程序唤起收银台,并且微信小程序调起支付必须要有openid,还多了一个中转的过程,需要权限利弊
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!