保税仓商品通关注意事项

前段时间因公司业务开展,需要支持保税商品的下单,在接触的过程中,发现这块断断续续做了有两个月,所以趁这个时间,把我踩到的坑全记录一下。

1.跨境商品简单介绍

目前跨境商品主要可分为两个途径,一是海外直邮,二是保税商品。

  1. 海外直邮是指由国外直接发快递到国内,优点是不需要在下单的时候做额外的工作量,流程简单,只要通知商家发货就可以。缺点是时间长(十天半个月都是很常见的)、邮费贵、有可能被海关直接拦截(小概率)。
  2. 国内保税仓发货,保税仓相当于一个商品的前置仓(一般是由第三方供应链提供的服务),要事先把海外商品先囤积到保税仓中,收到订单后,再由保税仓直接发货到用户手中,优点是发货送货速度快(海关不会卡)、商品来源有保障(商品进保税仓要备案登记的,用户放心)、运费便宜。缺点时既然要囤积商品就是滞销风险、流程要复杂的多。

2.保税仓前置条件

  1. 公司要在相应海关备案登记(登记也有一个坑,企业类型一定要选择电商平台,否则你是拿不到电商平台代码号的),并拿到放密钥的U盘(这个东西超级坑,只能windows系统用,所以还要准备一台windows机器用于调试),备案后,你会得到海关备案的编号和名称。
  2. 要准备一台稳定的windows机器插上海关给的U盘当做线上的服务器(用于线上订单加密使用),登录海关的商户页面什么之类的操作都需要用到加密U盘,所以事先要多申请几个。
  3. 推支付单时,现在第三方支付平台都提供了推送支付单的接口,比如我们使用的是支付宝支付,通过支付宝推送订单时,要额外申请推送支付单的功能

3.保税仓整体流程概述

国家海关为了能够更好的规范跨境商品的流程,所以在前几年的时候对这块进行了改革,保税仓的商品必须要通过三单合一(订单、物流单、支付单三单校验)才能发货,其中支付单可以用第三方支付平台接口推送,订单和物流单某些第三方供应链平台也会帮你一起推送到海关去,大大降低了开发成本,对于电商来说只要申请了电商平台的备案,接一下支付平台的报关接口、供应链的推送订单接口(物流单推送需要物流仓储的备案才能推,一般电商企业不需要关心这个)、海关实时数据接口即可。

4.推送支付单

我这里先以支付宝举例子(坑超级多,支付宝的接口是真的难对接,写的不详细)。
这个是支付宝的报关接口文档 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 报关流水号:自己按照规则生成就好,唯一标识符,没有业务含义
推送支付单直接推送总署就好,不需要推送到海关所在地,否则会导致海关接收不到推送的支付单。(杭州是这样,其他地方我没试过)

5.推送订单

6.对接海关实时获取数据接口(超级超级坑)

https://blog.csdn.net/ccbox_net/article/details/89031736
先放上参考资料,我一开始对接的时候主要是看的这篇博客
先说一下步骤

  1. 如果我上面说的前置条件你准备好了,你现在手头上,就有一个windows机器和秘钥U盘了

  2. 检查卡介质是否正确,在windows电脑上,登录customs.chinaport.gov.cn 选择使用卡介质登录时会弹出下载控件选项。点击控件下载到本地,运行控件的exe后再进行登录,初始密码88888888。在下载别人提供的一个工具https://pan.baidu.com/s/1xo0AcZZ4QZDeAu2DHOEQjg 提取码: cfpp,把证书序列号和证书获取出来,用于注册接口。

  3. 为什么windows机器和加签U盘这么重要呢,因为海关为了数据安全性,对接接口的所有参数必须要经过加密(加密还用的是websocket = =),所以就必须要安装控件和插U盘。

  4. 保税仓商品通关注意事项_第1张图片
    先加微信公告上的两位微信好友,让他们拉你进微信群,微信群里每天发送注册接口所需要用的的资料。测试环境接口私聊他们添加(把电商平台代码、名称、接口路径、证书、证书编号发他就好),线上接口在电子口岸网站上自行注册
    保税仓商品通关注意事项_第2张图片

  5. 之前提到过,加签掉的是加签服务期的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;
	}

你可能感兴趣的:(保税仓商品通关注意事项)