上篇文章讲述了代理商注册商户号,因为接触的这个项目是一个微信小程序商城项目,里面包含用户小程序,门店App,代理商App三个端,每个代理商下有不同的门店,用户通过微信小程序下单,只会下单到具体的门店,所以说代理商有了商户号,用户才能支付成功.下面介绍聚富通H5埋名支付,说白了就是用户下单向工行发起支付,生成支付报文,然后由微信小程序/微信公众号来唤起微信支付,用户付款到代理商的商户号中.
相关文档在以下链接中,测试代码里面也有给出,只需要配置相关参数就可以进行测试了
聚富通H5埋名支付接口相关参数介绍
下面是我的测试demo,其实就是把文档中的代码拿出来,相关参数配置下
public class PayH5Test {
protected static final String APP_ID ="APP的编号";
protected static final String MY_PRIVATE_KEY = "应用方私钥,由应用方生成公私钥对,并将公钥上传至工行API开放平台";
protected static final String encryptKey = "应用方加密串,与工行API开放平台一致";
protected static final String APIGW_PUBLIC_KEY = "工行API网关公钥,请向工行联系获取";
public static void main(String[] args) {
DefaultIcbcClient client = new DefaultIcbcClient(APP_ID,"RSA2", MY_PRIVATE_KEY,"UTF-8",
"json",APIGW_PUBLIC_KEY,"AES",encryptKey,"","");
JftApiPayAddTracelessOrderForH5RequestV2 request = new JftApiPayAddTracelessOrderForH5RequestV2();
request.setServiceUrl("https://gw.open.icbc.com.cn/api/jft/api/pay/add/h5/traceless/order/V2");
JftApiPayAddTracelessOrderForH5RequestV2.JftApiPayAddTracelessOrderForH5V2Biz bizContent = new JftApiPayAddTracelessOrderForH5RequestV2.JftApiPayAddTracelessOrderForH5V2Biz();
bizContent.setAppId(APP_ID);//平台商户标识:提交支付请求的平台商户编号 10000000000000001
bizContent.setOutVendorId("003");//子商户标识:提交支付请求的子商户编号
bizContent.setOutUserId("A003");//用户标识
bizContent.setNotifyUrl("http://xx.com");//商户通知URL:商户接受支付成功通知消息URL
bizContent.setOutOrderId("20220610172830_1");//平台对接方ID:平台商户订单ID,确保唯一
bizContent.setGoodsId("3212");//商品编号(长度单位:字节)
bizContent.setGoodsName("测试商品");//商品名称(长度单位:字节)
bizContent.setMac("a3-b4-c5");//手机Mac:支付提交用户手机Mac地址
bizContent.setTrxIp("83.11.125.138");//交易IP:支付提交用户IP地址
bizContent.setTrxIpCity("北京");//交易IP所在地:支付提交用户IP所在地
bizContent.setTrxChannel("03");//交易渠道:固定取03
bizContent.setPayAmount("1.01");//支付金额(元)整数长度不能超过8位,小数位不能超过2位
bizContent.setSubMerRateWx("400");//子商户服务费率(微信),商户档案中支付费率类型为区间支付费率,则该字段为必输项
bizContent.setSubMerRateZfb("400");//子商户服务费率(支付宝),商户档案中支付费率类型为区间支付费率,则该字段为必输项
bizContent.setPayMode("01");//支付模式:微信小程序: 01,支付宝生活号: 02
bizContent.setPayType("01");
bizContent.setTpAppId("");//第三方应用ID;商户在微信公众号内或微信小程序内接入时必送,上送微信分配的公众账号ID;商户通过支付
bizContent.setTpOpenId("");//第三方用户标识;商户在微信公众号内或微信小程序内接入时必送,上送用户在商户appid下的唯一标识;商户通过支付宝生活号接入时不送。
// bizContent.setUnionId("");//第三方用户标识;商户在支付宝生活号接入时必送,上送用户的唯一标识;商户在微信公众号内或微信小程序内选送,上送用户唯一标识。
bizContent.setOutBusinessCode("10000000000000000001");//外部业务编码
bizContent.setFixedCommissionWx("0");//定额佣金金额(元)(微信支付),商户档案中支付费率类型为固定费用时,该字段为必输项
bizContent.setFixedCommissionZfb("0");//定额佣金金额(元)(支付宝支付),商户档案中支付费率类型为固定费用时,该字段为必输项
bizContent.setAutoConfirm("0");//是否即时解冻 1是 0否 商户为担保模式有效,默认为0
bizContent.setJDA("1.00"); //优惠金额
bizContent.setJD("1"); //是否优惠
bizContent.setJDT("1"); //聚富通优惠类型(1立减 2满减 3优惠券)
request.setBizContent(bizContent);
JftApiPayAddTracelessOrderForH5ResponseV2 response;
try {
response = client.execute(request, System.currentTimeMillis()+"");//msgId消息通讯唯一编号,要求每次调用独立生成,APP级唯一
if (response.isSuccess()) {
//6、业务成功处理,请根据接口文档用response.getxxx()获取同步返回的业务数据
System.out.println("ReturnCode:"+response.getReturnCode());
System.out.println("response:" + response);
System.out.println(response.getPaySign());
} else {
//失败
System.out.println("ReturnCode:"+response.getReturnCode());
System.out.println("ReturnMsg:"+response.getReturnMsg());
}
} catch (IcbcApiException e) {
e.printStackTrace();
}
}
}
从代码中可以看到需要配置的参数,工行Api网关公钥,App编号,应用方私钥,应用方加密串,以及第三方应用id,TPAppId,以及第三方用户标识TpOpenId,其他参数可以根据任务需要进行相关配置,注意在代码中有个NotifyUrl商户通知URL,也就是支付成功通知的异步回调,由于工行对异步回调支持80/443端口,一般在开发或测试环境中测支付回调是无法成功的,只能在线上环境才能进行测试,这个问题会在后面给出解决方案,以上代码还未到支付阶段,此接口只是生成支付报文,就是如果请求返回成功,里面会有paySign这个参数,他是一个字符串,需要将它转换为map形式,可以使用以下代码进行转换
String paySign = response.getPaySign();
Map map = JSON.parseObject(paySign, Map.class);
前端接收这个Map,就会发起微信支付,用户输入密码,支付完成.
在项目中我是这样使用的,用户下单购买商品后会生成一条订单,然后会调用聚富通这个接口向工行发送请求,在发送请求前,我会根据订单号查询其代理商是否注册了工行商户号,然后调用聚富通的查询商户号接口判断是否注册过了,商户号状态是否正确,审核状态是否审核通过,等以上判断都是true再去调用聚富通支付接口发送请求,对返回的结果进行判断,首先会判断返回的code值是否为10100000,是的话代表请求成功,然后获取返回的paySign参数,将其转换为map返回给前端,前端拿到这个参数唤起支付页面,用户进行支付.业务中的代码如下所示
**注意点:**在向工行发送支付请求时,不支持同一个订单号请求多次,一次请求一个订单号,这时可以关联系统订单号在随机生成一个不重复的订单号,随机订单号向工行发送请求,请求成功保存随机订单号,请求失败更新随机订单号,这样就能保证系统订单号关联到工行订单号了.
/**
* 工行支付
* @param icbcConfig 工行参数
* @param wechatConfig 微信参数
* @param idbcOrderNo 请求工行随机订单号
* @param orderNo 订单号
* @param openId 用户标识
* @return
*/
private ResponseData ICBCH5(ICBCConfig icbcConfig,WechatConfig wechatConfig,String idbcOrderNo, String orderNo, String openId){
//构建client
DefaultIcbcClient client = new DefaultIcbcClient(icbcConfig.getAppId(), IcbcConstants.SIGN_TYPE_RSA2,
icbcConfig.getMyPrivateKey(), IcbcConstants.CHARSET_UTF8, IcbcConstants.FORMAT_JSON, icbcConfig.getApigwPublicKey(),
IcbcConstants.ENCRYPT_TYPE_AES, icbcConfig.getEncryptKey(), "", "");
//构建查询请求对象
JftApiVendorInfoQueryRequestV1 request = new JftApiVendorInfoQueryRequestV1();
request.setServiceUrl(ICBCConfig.QUERY_INFO);
JftApiVendorInfoQueryRequestV1.JftApiVendorInfoQueryRequestV1Biz queryBizContent = new JftApiVendorInfoQueryRequestV1.JftApiVendorInfoQueryRequestV1Biz();
queryBizContent.setAppId(icbcConfig.getAppId());
//查询代理商id
TdCommodityUserOrder orderCommodity = tdCommodityUserOrderService.findByOrderNo(orderNo);
log.info("代理商id:agentInfoId = {}", orderCommodity.getAgentId());
SysAgentIcbcInfo icbcInfo = sysAgentIcbcInfoService.selectByAgentInfoId(orderCommodity.getAgentId());
if (StringUtils.isEmpty(icbcInfo)) {
return ResponseData.fail("代理商还没有绑定商户,请绑定商户");
}
if (StringUtils.isEmpty(icbcInfo.getOutVendorId())) {
return ResponseData.fail("客户在合作方的平台自定义编号为空");
}
if (StringUtils.isEmpty(icbcInfo.getOutUserId())) {
return ResponseData.fail("用户外部系统编号为空");
}
queryBizContent.setOutVendorId(icbcInfo.getOutVendorId());
request.setBizContent(queryBizContent);
log.info("查询工行商户请求参数 appId = {},outVendorId={},serviceUrl={}",icbcConfig.getAppId(),icbcInfo.getOutVendorId(),ICBCConfig.QUERY_INFO);
try {
JftApiVendorInfoQueryResponseV1 responseInfo = (JftApiVendorInfoQueryResponseV1) client.execute(request, System.currentTimeMillis() + "");
System.out.println(responseInfo.isSuccess());
if (!responseInfo.isSuccess()) {
log.info("该商户还没有注册,请先注册 returnCode={},returnMsg={}", responseInfo.getReturnCode(), responseInfo.getReturnMsg());
return ResponseData.fail(responseInfo.getReturnCode(), responseInfo.getReturnMsg());
}
//判断商户状态1-新建,2-正常,3-冻结,4-销户,5-预销户,6-预销户成功,7-待撤销,8-已撤销,9-作废
if (!responseInfo.getVendorStatus().equals("2")) {
log.info("该商户状态不正确 vendorStatus={}", responseInfo.getVendorStatus());
return ResponseData.fail(responseInfo.getReturnCode(), responseInfo.getReturnMsg());
}
//审核状态01-新建,02-代送网金部审核,03-网金部审核中,04-网金部审核通过,05-网金部审核拒绝
if (!responseInfo.getAuditStatus().equals("04")) {
log.info("审核状态不正确 auditStatus={}", responseInfo.getAuditStatus());
return ResponseData.fail(responseInfo.getReturnCode(), responseInfo.getReturnMsg());
}
} catch (IcbcApiException e) {
e.printStackTrace();
}
JftApiPayAddTracelessOrderForH5RequestV2 requestPay = new JftApiPayAddTracelessOrderForH5RequestV2();
requestPay.setServiceUrl(ICBCConfig.PAYH5_URL);
JftApiPayAddTracelessOrderForH5RequestV2.JftApiPayAddTracelessOrderForH5V2Biz bizContent = new JftApiPayAddTracelessOrderForH5RequestV2.JftApiPayAddTracelessOrderForH5V2Biz();
bizContent.setAppId(icbcConfig.getAppId());
bizContent.setOutVendorId(icbcInfo.getOutVendorId());
bizContent.setOutUserId(icbcInfo.getOutUserId());
bizContent.setNotifyUrl(icbcConfig.getIcbcNotifyUrl());
bizContent.setOutOrderId(idbcOrderNo);
bizContent.setGoodsName("超微电池");
bizContent.setTrxIp(getAttributeForIp());
bizContent.setTrxChannel(IcbcParamEnum.ICBC_PARAM_ENUM.getFiveValue());
bizContent.setPayAmount(String.valueOf(orderCommodity.getActualAmount().divide(new BigDecimal(100)).setScale(2,BigDecimal.ROUND_HALF_UP)));
bizContent.setPayMode(IcbcParamEnum.ICBC_PARAM_ENUM.getThreeValue());
bizContent.setPayType(IcbcParamEnum.ICBC_PARAM_ENUM.getThreeValue());
bizContent.setTpAppId(wechatConfig.getAppId());
bizContent.setTpOpenId(openId);
bizContent.setAutoConfirm(IcbcParamEnum.ICBC_PARAM_ENUM.getFirstValue());
requestPay.setBizContent(bizContent);
JftApiPayAddTracelessOrderForH5ResponseV2 response = null;
try {
log.info("发起工行支付请求 request={}", JSONObject.toJSONString(requestPay));
response = client.execute(requestPay,System.currentTimeMillis()+"");
log.info("请求结束 response={}",response);
log.info("判断请求结果");
} catch (IcbcApiException e) {
e.printStackTrace();
}
if (10100000 == response.getReturnCode()){
log.info("请求结果:code={},msg={},msgId={}",response.getReturnCode(),response.getReturnMsg(),response.getMsgId());
log.info("请求成功返回信息:response={}",JSONObject.toJSONString(response));
log.info("paysign={}",response.getPaySign());
String paySign = response.getPaySign();
Map map = JSON.parseObject(paySign, Map.class);
return new ResponseData<>(CodeIdEnum.Success, map);
}
log.info("请求失败,返回请求结果");
log.info("请求结果:code={},msg={},msgId={}",response.getReturnCode(),response.getReturnMsg(),response.getMsgId());
log.info("请求失败返回信息:response={}",JSONObject.toJSONString(response));
return new ResponseData<>(CodeIdEnum.FAILED,response);
}
上面说了工行回调只支持线上回调,端口号为80/443,在开发环境/测试环境是无法进行测试的,我的解决方案是在开发环境中主动调工行查询订单接口,查询订单状态,如果订单状态为02时,我就主动修改系统中的订单状态,异步回调还是要写的,异步回调写好升到线上去测试,如果测试没问题,就用异步回调.毕竟支付成功都是通过异步回调来修改订单状态,主动查询的话,相当于支付完成后又向后台,后台又向工行发送一次请求,这样比较耗时,也可能出现其他问题不可控.两种方法也会在下面进行介绍
以下是测试代码


在项目中通过订单号去查询代理商id,因为支付能成功,代理商肯定绑定了商户号
根据代理商查询工行子商户信息
根据订单号再去查询关联的发起支付的随机订单号
然后构建工行聚富通请求对象发请求,对返回消息里的订单状态判断是否已支付成功,支付成功就去修改订单状态等相关业务逻辑,并将工行商户单号保存到系统数据库中,以便财务对账等
相关代码会在下面展示:
@Log
@Token
@ApiOperation("主动查询回调")
@PostMapping("/activityCallBack")
public ResponseData activityCallBack(@ApiParam(required = true, name = "orderNo",
value = "订单编号") @RequestParam("orderNo") String orderNo){
ConstraintViolation validate = BeanValidator.validate(req);
if (validate != null) {
return new ResponseData<>(CodeIdEnum.ParameterMalformed.getValue(), validate.getMessage());
}
//根据订单号查询代理商
TdCommodityUserOrder tdCommodityUserOrder = tdCommodityUserOrderService.findByOrderNo(orderNo);
//根据代理商查询子商户
SysAgentIcbcInfo sysAgentIcbcInfo = sysAgentIcbcInfoService.selectByAgentInfoId(tdCommodityUserOrder.getAgentId());
//根据订单号查询传入工行的订单号
TdCommodityUserOrderIcbcResp resp = tdCommodityUserOrderIcbcService.selectByOrderNo(orderNo);
return updateOrderNoStatus(sysAgentIcbcInfo.getOutVendorId(), orderNo,resp);
}
/**
* 主动调支付修改订单状态
* @param outVendorId
* @param orderNo
* @param resp
* @return
*/
private ResponseData updateOrderNoStatus(String outVendorId,String orderNo,TdCommodityUserOrderIcbcResp resp){
//构建client
DefaultIcbcClient client = new DefaultIcbcClient(icbcConfig.getAppId(), IcbcConstants.SIGN_TYPE_RSA2,
icbcConfig.getMyPrivateKey(), IcbcConstants.CHARSET_UTF8, IcbcConstants.FORMAT_JSON, icbcConfig.getApigwPublicKey(),
IcbcConstants.ENCRYPT_TYPE_AES, icbcConfig.getEncryptKey(), "", "");
//构建查询请求对象
QueryOrderRequestV1 request = new QueryOrderRequestV1();
request.setServiceUrl(ICBCConfig.QUERY_ORDER_NO);
QueryOrderRequestV1.QueryOrderRequestV1Biz bizContent = new QueryOrderRequestV1.QueryOrderRequestV1Biz();
bizContent.setAppId(icbcConfig.getAppId());
bizContent.setOutVendorId(outVendorId);
bizContent.setOutOrderId(resp.getIcbcOrderNo());
request.setBizContent(bizContent);
QueryOrderResponseV1 response;
try {
response = client.execute(request,System.currentTimeMillis()+"");
log.info("订单查询结果 response={}",JSONObject.toJSONString(response));
if (("02").equals(response.getOrderStatus())){
//修改订单状态相关业务逻辑
payOrderFinish(orderNo,response.getIcbcOrderId(),(byte) 2);
TdCommodityUserOrderIcbcResp td1 = tdCommodityUserOrderIcbcService.selectByOrderNo(orderNo);
TdCommodityUserOrderIcbc td = new TdCommodityUserOrderIcbc();
td.setId(td1.getId());
td.setIcbcOrderId(response.getIcbcOrderId());
td.setjOrderId(response.getjOrderId());
td.setPayCompleteDateTime(response.getPayCompleteDate()+" "+response.getPayCompleteTime());
tdCommodityUserOrderIcbcService.updateByPrimaryKeySelective(td);
}
} catch (IcbcApiException e) {
e.printStackTrace();
}
return ResponseData.success();
}
异步通知报文格式{“notifyData”,“signData”},接收变量名为biz_content
报文内容:Json解析为map,键值对说明如下
| key | 长度定义 | 说明 |
|---|---|---|
| notifyData | 无限制 | 通知消息,json格式,对其明文串进行了base64编码; |
| signData | 无限制 | 通过API签名验证,此字段可以忽略。 |
notifyData数据定义,异步通知报文文档下载地址:异步通知文档
| 变量名称 | 变量名称 | 长度 | 说明 |
|---|---|---|---|
| 交易代码 | txCode | 2 | 02:支付 |
| 交易响应码 | respCode | MAX(10) | 成功为00000失败为00001 |
| 通知信息 | respMsg | MAX(200) | 支付平台返回的通知信息 |
| 交易金额 | trxAmount | MAX(22) | 交易金额 |
| 支付方式 | payMethod | 2 | 01:工银e支付 02:微信支付 03:支付宝 04:预付卡 05:转账 06:二维码主扫支付 07:POS支付 08:e支付有协议小额免密 09:微信线下支付 10:会员卡小额免密 11:支付宝线下支付 12:二维码被扫支付 13:合作方钱包支付 99:其他 |
| 订单状态 | orderStatus | 2 | 02:支付成功 03:支付失败 |
| 订单号 | orderId | MAX(20) | 订单号 |
| 支付完成日期 | completeDate | 8 | yyyyMMdd |
| 支付完成时间 | completeTime | 8 | HH:mm:ss |
| … | … | … | … |
从上面文档中可以看到我们需要的是notifyData这个数据,上面说明了为json格式的字符串,并进行了base64编码,首先对其参数进行验签操作,验签通过再对其进行base64解密操作,并将其转换为map从中获取需要的数据进行操作,最后再根据resCode和refundStatus判断是否支付成功,成功从中获取订单号执行修改业务系统订单号的操作.
/**
* 验签
* @param request
* @param path
* @return
* @throws IcbcApiException
*/
private boolean verifySignData(HttpServletRequest request,String path) throws IcbcApiException {
Map<String, String> params = new HashMap<String, String>();
String api = request.getParameter("api");
String from = request.getParameter("from");
String app_id = request.getParameter("app_id");
String charset = request.getParameter("charset");
String format = request.getParameter("format");
String encrypt_type = request.getParameter("encrypt_type");
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("encrypt_type", encrypt_type);
params.put("timestamp", timestamp);
params.put("biz_content", biz_content);
// 目前上行网关签名暂时仅支持RSA
params.put("sign_type", sign_type);
String signStr = WebUtils.buildOrderedSignStr(path, params);
log.info("验签入参:signStr={},sign_type={},APIGW_PUBLIC_KEY={},charset={},sign={}",signStr,sign_type,icbcConfig.getApigwPublicKey(),charset,sign);
boolean verify = IcbcSignature.verify(signStr, sign_type, icbcConfig.getApigwPublicKey(), charset, sign);
log.info("验签结果:verify={}",verify);
return verify;
}
/**
* 解密
* @param request
* @return
*/
public String dencrypt(HttpServletRequest request) throws JsonProcessingException {
String biz_content = request.getParameter("biz_content");
String key = "%2B";
String password = "+";
String bizContent = biz_content.replaceAll(key, password);
log.info("转换上送参数:bizContent={}",bizContent);
//解析请求数据map
ObjectMapper mapper = new ObjectMapper();
Map transMap = mapper.readValue(bizContent, Map.class);
//获取报文密文信息
String notifyData = (String) transMap.get("notifyData");
return new String(Base64.getDecoder().decode(notifyData));
}
/**
* 加签方法
* @param request
* @param copReturnCode
* @param copReturnMsg
* @return
*/
public String sign(HttpServletRequest request,String copReturnCode,String copReturnMsg) throws IcbcApiException {
String msg_id = new SimpleDateFormat("yyyyMMdd").format(new Date())+new SimpleDateFormat("HHmmss").format(new Date());
String responseBizContent = "{\"return_code\":\"" + copReturnCode + "\",\"return_msg\":\"" + copReturnMsg + "\""
+ ",\"msg_id\":\"" + msg_id + "\"}";
//调用过程中使用的编码格式
String charset = request.getParameter("charset");
//商户对消息返回响应进行签名,签名方式需与在API平台登记的sign_type保持一致(RSA2)
String signType = IcbcConstants.SIGN_TYPE_RSA2;
String signStr = "\"response_biz_content\":" + responseBizContent + "," + "\"sign_type\":" + "\"" + signType
+ "\"";
String signParam = IcbcSignature.sign(signStr, signType, icbcConfig.getMyPrivateKey(), charset);
String returnJson = "{\"response_biz_content\":" + responseBizContent + ",\"sign_type\":\"" + signType + "\""
+ ",\"sign\":\"" + signParam + "\"}";
return returnJson;
}
@Log
@ApiOperation("工行支付回调")
@RequestMapping("/receiveNotify")
@ResponseBody
public Object receiveNotify(HttpServletRequest request) throws IcbcApiException, JsonProcessingException {
log.info("进入回调");
log.info("回调地址...... url={}",icbcConfig.getIcbcNotifyUrl());
String returnJson = null;
boolean verifyFlag = false;
try {
verifyFlag = verifySignData(request, "/http/callback/receiveNotify");
} catch (IcbcApiException e) {
log.error("验签出错:",e);
e.printStackTrace();
}
if (verifyFlag) {
String bizContent = dencrypt(request);
log.info("解密后参数:" + bizContent);
ObjectMapper mapper = new ObjectMapper();
Map transMap = mapper.readValue(bizContent, Map.class);
log.info("map参数 map={}",transMap);
TdCommodityUserOrderIcbcResp resp = tdCommodityUserOrderIcbcService.selectByIcbcOrderNo((String) transMap.get("orderId"));
if ("00000".equals(transMap.get("respCode"))){
if ("02".equals(transMap.get("orderStatus"))){
payOrderFinish(resp.getOrderNo(), (String) transMap.get("icbcOrderId"),(byte) 2);
}else {
log.error("订单状态不正确,订单状态为:orderStatus = {}",transMap.get("orderStatus"));
}
}else {
log.error("交易响应码失败,交易码为:respCode={}",transMap.get("respCode"));
}
String copReturnCode = "0";
String copReturnMsg = "success";
returnJson = sign(request, copReturnCode, copReturnMsg);
return returnJson;
} else {
String copReturnCode = "-12345";
String copReturnMsg = "icbc jft sign not pass.";
returnJson = sign(request, copReturnCode, copReturnMsg);
}
return returnJson;
}
这里的成功和失败返回的代码是固定的,是返回给工行那边的,我们不需要处理,只需要在验签和解密后获取返回的参数对其进行判断,成功执行相关的修改订单的业务逻辑,再去进行判断返回信息,成功/失败设置相关的returnCode和returnMsg返回给工行那边即可.