前段时间因公司业务开展,需要支持保税商品的下单,在接触的过程中,发现这块断断续续做了有两个月,所以趁这个时间,把我踩到的坑全记录一下。
目前跨境商品主要可分为两个途径,一是海外直邮,二是保税商品。
国家海关为了能够更好的规范跨境商品的流程,所以在前几年的时候对这块进行了改革,保税仓的商品必须要通过三单合一(订单、物流单、支付单三单校验)才能发货,其中支付单可以用第三方支付平台接口推送,订单和物流单某些第三方供应链平台也会帮你一起推送到海关去,大大降低了开发成本,对于电商来说只要申请了电商平台的备案,接一下支付平台的报关接口、供应链的推送订单接口(物流单推送需要物流仓储的备案才能推,一般电商企业不需要关心这个)、海关实时数据接口即可。
我这里先以支付宝举例子(坑超级多,支付宝的接口是真的难对接,写的不详细)。
这个是支付宝的报关接口文档 https://docs.open.alipay.com/155/104778/ 注意,只看这个就行了,还有另一个推送支付单的文档不需要管他。
报送支付单可以进行拆单报送,什么意思呢?比如用户购买了四件商品,花了100元,其中只有一件跨境商品A,价值30元。用户是一次性支付的,支付回调接口会返回一个支付单号paymentNo。但是你在推送支付单的时候,可以用同一paymentNo推送多个子支付单,用该支付单号只推30元的支付单也是可以的(只要每个子支付单推送的支付金额加起来不超过paymentNo的支付金额就可以,否则会返回错误)。
在推送结束时,把支付宝返回的相关数据最好也要记录一下,在后面推送订单信息的时候需要用到。
以下是支付宝推送支付单的主要代码
/**
* @Description: 支付宝报关接口 文档地址https://docs.open.alipay.com/155/104778/
* @param tradeNo 支付宝交易号
* @param orderNo 主订单号
* @return: void
* @Author: lvqiushi
* @Date: 2019-08-28
*/
private void alipayAcquireCustom(String tradeNo, String orderNo, String initResponse) {
// 根据主订单号获取所有的子订单信息
OrderDO orderInfo = this.getAllSubOrderByOrderNo(orderNo);
// 如果订单为多个,则需要拆单
Map<String, String> sParaTemp = new HashMap<>();
sParaTemp.put("service", AcquireCustomConstants.ALIPAY_METHOD_NAME);
sParaTemp.put("partner", AcquireCustomConstants.ALIPAY_PARTENER_ID);
sParaTemp.put("_input_charset", AlipayConfig.CHARSET);
sParaTemp.put("trade_no", tradeNo);
sParaTemp.put("merchant_customs_code", AcquireCustomConstants.MERCHANT_CUSTOMS_CODE);
sParaTemp.put("merchant_customs_name", AcquireCustomConstants.MERCHANT_CUSTOMS_NAME);
sParaTemp.put("customs_place", AliPayCostomsCodeEnum.ZONGSHU.getCostsmsName());
sParaTemp.put("buyer_name", orderInfo.getConsigneeName());
sParaTemp.put("buyer_id_no", orderInfo.getIdentityCard());
List<SubOrderVO> subOrderList = orderInfo.getSubOrderList();
if (CollectionUtils.isEmpty(subOrderList)) {
log.warn("推送支付单时,订单下无子订单数据");
return;
}
// 获取子订单下保税商品的总支付金额,进行拆单报关
Map<String, GrabaServiceImpl.SubOrderPrice> subOrderPriceMap = grabaService.getSubOrderPrice(orderInfo);
for (SubOrderVO subOrder : subOrderList) {
// 判断子订单是否包含保税商品
if (!this.judgeSubOrderContainBondItem(subOrder)) {
continue;
}
GrabaServiceImpl.SubOrderPrice subOrderPrice = subOrderPriceMap.get(subOrder.getSubOrderDO().getSubOrderNo());
if (null == subOrderPrice) {
continue;
}
sParaTemp.put("out_request_no", this.genertedAcquireCustomNo());
sParaTemp.put("is_split", "T");
sParaTemp.put("sub_out_biz_no", subOrder.getSubOrderDO().getSubOrderNo());
sParaTemp.put("amount", StringTool.priceFormatForNoFuhao(subOrderPrice.getPayAmount()));
// 执行支付宝报关接口方法
this.excuteAcquireRequest(sParaTemp, orderNo, subOrder.getSubOrderDO().getSubOrderNo(), initResponse);
}
}
其中我踩过的坑,
支付接口中的appId一定要为接口所传的合作伙伴id下的appId
out_request_no 报关流水号:自己按照规则生成就好,唯一标识符,没有业务含义
推送支付单直接推送总署就好,不需要推送到海关所在地,否则会导致海关接收不到推送的支付单。(杭州是这样,其他地方我没试过)
https://blog.csdn.net/ccbox_net/article/details/89031736
先放上参考资料,我一开始对接的时候主要是看的这篇博客
先说一下步骤
如果我上面说的前置条件你准备好了,你现在手头上,就有一个windows机器和秘钥U盘了
检查卡介质是否正确,在windows电脑上,登录customs.chinaport.gov.cn 选择使用卡介质登录时会弹出下载控件选项。点击控件下载到本地,运行控件的exe后再进行登录,初始密码88888888。在下载别人提供的一个工具https://pan.baidu.com/s/1xo0AcZZ4QZDeAu2DHOEQjg 提取码: cfpp,把证书序列号和证书获取出来,用于注册接口。
为什么windows机器和加签U盘这么重要呢,因为海关为了数据安全性,对接接口的所有参数必须要经过加密(加密还用的是websocket = =),所以就必须要安装控件和插U盘。
先加微信公告上的两位微信好友,让他们拉你进微信群,微信群里每天发送注册接口所需要用的的资料。测试环境接口私聊他们添加(把电商平台代码、名称、接口路径、证书、证书编号发他就好),线上接口在电子口岸网站上自行注册
之前提到过,加签掉的是加签服务期的websocket接口,这里先放一下我写的代码,以供参考
// 先定义几个接口请求的实体类
@Data
@JSONType(orders = { "sessionID", "payExchangeInfoHead", "payExchangeInfoLists", "serviceTime", "certNo", "signValue" })
public class HgCheckDTO {
/** 证书编号 */
private String certNo;
/** 签名结果值 */
private String signValue;
/** 海关发起请求时,平台接收的会话ID */
private String sessionID;
private Head179DTO payExchangeInfoHead;
private List<Body179DTO> payExchangeInfoLists;
/** 返回时的系统时间 时间戳的字符串 */
private String serviceTime;
}
@Data
@JSONType(orders = { "guid", "initalRequest", "initalResponse", "ebpCode", "payCode", "payTransactionId", "totalAmount", "currency", "verDept", "payType", "tradingTime", "note" })
public class Head179DTO {
/** 系统唯一序号 */
private String guid;
/** 支付原始请求 */
private String initalRequest;
/** 支付原始响应 */
private String initalResponse;
/** 电商平台代码 */
private String ebpCode;
/** 支付企业代码 */
private String payCode;
/** 交易流水号 */
private String payTransactionId;
/** 交易金额 单位:元 */
private Double totalAmount;
/** 币制 人民币:142 */
private String currency;
/** 验核机构 */
private String verDept;
/** 支付类型 */
private String payType;
/** 交易成功时间 */
private String tradingTime;
/** 备注 */
private String note;
}
@Data
@JSONType(orders = { "orderNo", "goodsInfoDTO", "recpAccount", "recpCode", "recpName" })
public class Body179DTO {
/** 订单编号 */
private String orderNo;
/** 商品信息 */
private List<GoodsInfoDTO> goodsInfoDTO;
/** 收款账号 */
private String recpAccount;
/** 收款企业代码 */
private String recpCode;
/** 收款企业名称 */
private String recpName;
}
@Data
@JSONType(orders = { "gname", "itemLink" })
public class GoodsInfoDTO {
/** 商品名称 */
private String gname;
/** 商品展示链接地址 */
private String itemLink;
}
/**
* @Description: 海关抓取数据接口
* @param openReq
* @return: java.util.Map
* @Author: lvqiushi
* @Date: 2019-09-26
*/
public Map<String, Object> doPlatDataOpen(@Param(value="openReq") String openReq) {
/**
* 海关抓取数据流程
* 1.海关调用企业注册地址
* 2.异步查询原始订单数据,向海关指定的地址调用接口返回,必须要在两分钟内上传完毕
* 3.向海关返回"code", "10000" 告诉海关收到响应
*/
JSONObject object = JSONObject.parseObject(openReq);
// 异步调用数据报送接口
ThreadEventBusImpl.commonExecute.submit(new Runnable() {
@Override
public void run() {
haiguanRepayService.repDataApply(object.getString("orderNo"), object.getString("sessionID"));
}
});
/* 返回报文给海关 */
Map<String, Object> result = new HashMap<>();
result.put("code", "10000");
result.put("message", "接收成功!");
result.put("serviceTime", System.currentTimeMillis());
return result;
}
/**
* @Description: 向海关返回原始订单数据接口
* @param orderNo
* @param sessionID
* @return: java.lang.String
* @Author: lvqiushi
* @Date: 2019-09-23
*/
public String repDataApply(String orderNo, String sessionID) {
// 只有线上环境才会进行支付单报关,因为支付宝的报关接口没有测试环境
if (!"online".equals(PropertyService.CURRENT_PROFILE)) {
return "";
}
HgCheckDTO entity;
try {
entity = this.getHaiguanOrginData(orderNo, sessionID);
} catch (Exception e) {
log.error("", e);
return null;
}
CountDownLatch waitSignLock = new CountDownLatch(1);
/*加签成海关指定数据格式(4项:不能多不能少)*/
StringBuffer sb = new StringBuffer();
sb.append("\"sessionID\":\"").append(entity.getSessionID()).append("\"").append("||");
String headerString = JSON.toJSONString(entity.getPayExchangeInfoHead(), SerializerFeature.SortField);
sb.append("\"payExchangeInfoHead\":\"").append(headerString).append("\"").append("||");
String listString = JSON.toJSONString(entity.getPayExchangeInfoLists(), SerializerFeature.SortField);
sb.append("\"payExchangeInfoLists\":\"").append(listString).append("\"").append("||");
sb.append("\"serviceTime\":\"").append(entity.getServiceTime()).append("\"");
String signValue = sb.toString();
ThreadEventBusImpl.commonExecute.submit(new Runnable() {
@Override
public void run() {
Map<String,String> args = new java.util.HashMap<>();
args.put("inData", signValue);
args.put("passwd", password);
JSONObject epdata = new JSONObject(true);
epdata.put("_method", "cus-sec_SpcSignDataAsPEM");
epdata.put("_id", 1);
epdata.put("args", args);
haiguanSign.sign(epdata.toJSONString(), new Sign179Callback() {
@Override
public void signReply(String reply) {
if (!StringUtils.isEmpty(reply)) {
log.info("-----sessionId:{}, sign:{}", entity.getSessionID(), reply);
entity.setCertNo(certNo);
entity.setSignValue(reply);
waitSignLock.countDown();
}
}
});
}
});
ThreadEventBusImpl.commonExecute.submit(new Runnable() {
@Override
public void run() {
try {
// 如果加签30秒都没有成功,则失败打印日志,目的是为了防止死锁。
if(!waitSignLock.await(30, TimeUnit.SECONDS)) {
log.error("海关抓取数据接口,等待加签超时 orderNo = " + orderNo);
return;
}
if (StringUtils.isEmpty(entity.getSignValue())) {
log.error("未获得加签结果,向海关返回数据失败");
return ;
}
String returnOrderParams = JSON.toJSONString(entity, SerializerFeature.SortField);
String encode = URLEncoder.encode(returnOrderParams, "UTF-8");
OkHttpClient client = new OkHttpClient();
MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded");
RequestBody body = RequestBody.create(mediaType, "payExInfoStr=" + encode);
Request request = new Request.Builder()
.url(uploadUrl)
.post(body)
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.build();
Response response = client.newCall(request).execute();
log.info("海关数据上报返回结果 " + response.body().string());
} catch (InterruptedException e) {
log.error("", e)
} catch (IOException e) {
log.error("", e)
} catch (Exception e) {
log.error("", e)
}
}
});
return null;
}