海关总署公告:
http://www.customs.gov.cn/customs/302249/302266/302267/2134975/index.html 165号
http://www.customs.gov.cn/customs/302249/302266/302267/2155884/index.html 179号
这个接口对接已经懒得吐槽了,搞不懂一个技术文档怎么就写的这么官方!!!连个具体的sdk都没有!!!好了 我承认我有吐槽了。
现有的对接方法基本可看下网上的信息,可以了解80%
https://blog.csdn.net/u010955036/article/details/88712577
https://www.cnblogs.com/whtydn/p/10220209.html
尤其是第二个可以仔细看下 干货很多,流程也很细,唯一的缺点就是是用C#实现的报文,java的报文虽然有提及,但没有具体的实现方式,所以本文打算作一个补充说明。
package omni.purcotton.omni.customs.api;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.purcotton.omni.common.exception.CommonException;
import lombok.extern.java.Log;
import lombok.extern.slf4j.Slf4j;
import omni.purcotton.omni.customs.config.CustomsConfig;
import omni.purcotton.omni.customs.feign.OrderService;
import omni.purcotton.omni.customs.feign.PayService;
import omni.purcotton.omni.customs.pojo.data.OrderItemVo;
import omni.purcotton.omni.customs.pojo.data.OrderMainVo;
import omni.purcotton.omni.customs.pojo.data.PayInfoVo;
import omni.purcotton.omni.customs.pojo.data.SignReqDTO;
import omni.purcotton.omni.customs.pojo.request.GoodsInfo;
import omni.purcotton.omni.customs.pojo.request.PayExInfoStr;
import omni.purcotton.omni.customs.pojo.request.PayExchangeInfoHead;
import omni.purcotton.omni.customs.pojo.request.PayExchangeInfoList;
import omni.purcotton.omni.customs.pojo.response.RealTimeDataUploadResponse;
import omni.purcotton.omni.customs.socket.WebCallback;
import omni.purcotton.omni.customs.socket.WebSocketClientHandle;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.*;
/**
* [简要描述]:
* [详细描述]:
*/
@Slf4j
@RestController
@RequestMapping("/api/customs/realtime")
@Log
public class RealTimeDataUploadApi
{
@Autowired
private WebSocketClientHandle webSocketClientHandle;
@Autowired
private OrderService orderService;
@Autowired
private PayService payService;
private static final Logger LOGGER = LoggerFactory.getLogger(RealTimeDataUploadApi.class);
@Autowired
private CustomsConfig customsConfig;
static Gson gson = new GsonBuilder().disableHtmlEscaping().create();
@PostMapping(value = "/platDataOpen", headers = "content-type=application/x-www-form-urlencoded")
public JSONObject platDataOpen(@RequestParam String openReq)
{
openReq = StringUtils.replace(openReq, """, "\"");
JSONObject jsonObject = JSONObject.parseObject(openReq);
String orderNo = jsonObject.getString("orderNo");
String sessionID = jsonObject.getString("sessionID");
Long serviceTime = jsonObject.getLong("serviceTime");
PayExInfoStr payExInfoStr = getPayExInfoStr(orderNo, sessionID, customsConfig);
SignReqDTO signReqDTO = new SignReqDTO();
signReqDTO.setKey(payExInfoStr.getSessionID());
signReqDTO.setMessage(getSendData(payExInfoStr, customsConfig));
LOGGER.info("入参-{}", signReqDTO);
webSocketClientHandle.send(signReqDTO.getKey(), signReqDTO.getMessage(), new ImplWebCallback(payExInfoStr));
JSONObject result = new JSONObject();
result.put("code", "10000");
result.put("message", "");
result.put("serviceTime", System.currentTimeMillis());
return result;
}
private void setPostData(PayExInfoStr payExInfoStr)
{
CloseableHttpClient client = HttpClients.createDefault();
URIBuilder uriBuilder = null;
CloseableHttpResponse response = null;
List nameValuePairList = new ArrayList<>();
String json = gson.toJson(payExInfoStr);
LOGGER.info("上传给海关的json:" + json);
nameValuePairList.add(new BasicNameValuePair("payExInfoStr", json));
try
{
uriBuilder = new URIBuilder(customsConfig.getRealTimeUrl());
uriBuilder.addParameters(nameValuePairList);
HttpPost httpPost = new HttpPost(uriBuilder.build());
httpPost.setHeader(new BasicHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"));
try
{
response = client.execute(httpPost);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200)
{
HttpEntity entity = response.getEntity();
String result = EntityUtils.toString(entity, "UTF-8");
RealTimeDataUploadResponse responseData = gson.fromJson(result, RealTimeDataUploadResponse.class);
LOGGER.info(gson.toJson(responseData));
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
catch (URISyntaxException e)
{
e.printStackTrace();
}
}
public static String getSignJson(PayExInfoStr payExInfoStr)
{
StringBuilder buff = new StringBuilder();
buff.append("\"sessionID\"").append(":").append("\"" + payExInfoStr.getSessionID() + "\"").append("||");
buff.append("\"payExchangeInfoHead\"").append(":").append("\"" + gson.toJson(payExInfoStr.getPayExchangeInfoHead()) + "\"").append("||");
buff.append("\"payExchangeInfoLists\"").append(":").append("\"" + gson.toJson(payExInfoStr.getPayExchangeInfoLists()) + "\"").append("||");
buff.append("\"serviceTime\"").append(":").append("\"" + payExInfoStr.getServiceTime() + "\"");
LOGGER.info("加签准备报文:-------" + buff.toString());
return buff.toString();
}
public static String getSendData(PayExInfoStr payExInfoStr, CustomsConfig customsConfig)
{
LinkedHashMap map = new LinkedHashMap<>();
Map data = new HashMap<>(16);
map.put("_method", "cus-sec_SpcSignDataAsPEM");
map.put("_id", 1);
data.put("inData", getSignJson(payExInfoStr));
data.put("passwd", customsConfig.getPassword());
map.put("args", data);
return gson.toJson(map);
}
private PayExInfoStr getPayExInfoStr(String orderNo, String sessionID, CustomsConfig customsConfig)
{
OrderMainVo orderMainVo = orderService.find(orderNo);
if (orderMainVo == null)
{
LOGGER.info("订单:" + orderNo + "查询不到订单信息");
throw new CommonException(201, "订单:" + orderNo + "查询不到订单信息");
}
PayInfoVo payInfoVo = payService.getPayInfo(orderNo);
if (payInfoVo == null)
{
LOGGER.info("订单:" + orderNo + "查询不到支付信息");
throw new CommonException(201, "订单:" + orderNo + "查询不到支付信息");
}
//todo 拼接报文信息
PayExInfoStr payExInfoStr = new PayExInfoStr();
payExInfoStr.setSessionID(sessionID);
PayExchangeInfoHead payExchangeInfoHead = new PayExchangeInfoHead();
payExchangeInfoHead.setGuid(UUID.randomUUID().toString());
payExchangeInfoHead.setInitalRequest(payInfoVo.getInitalRequest());
payExchangeInfoHead.setInitalResponse(payInfoVo.getInitalResponse());
payExchangeInfoHead.setEbpCode(customsConfig.getEbpEntNo());
payExchangeInfoHead.setPayCode(customsConfig.getPayCode());
payExchangeInfoHead.setPayTransactionId(payInfoVo.getPayTransactionId());
payExchangeInfoHead.setTotalAmount(payInfoVo.getTotalAmount().doubleValue());
payExchangeInfoHead.setCurrency("142");
payExchangeInfoHead.setVerDept("3");
payExchangeInfoHead.setPayType("3");
payExchangeInfoHead.setTradingTime(DateUtil.format(payInfoVo.getTradingTime(), DatePattern.PURE_DATETIME_PATTERN));
payExchangeInfoHead.setNote("");
payExInfoStr.setPayExchangeInfoHead(payExchangeInfoHead);
PayExchangeInfoList payExchangeInfoList = new PayExchangeInfoList();
payExchangeInfoList.setOrderNo(orderNo);
List orderItemVos = orderMainVo.getOrderItemVos();
List goodsInfos = new ArrayList<>();
for (OrderItemVo orderItemVo : orderItemVos)
{
GoodsInfo goodsInfo = new GoodsInfo();
goodsInfo.setGname(orderItemVo.getProductName());
goodsInfo.setItemLink(orderItemVo.getImagePath());
goodsInfos.add(goodsInfo);
}
payExchangeInfoList.setGoodsInfo(goodsInfos);
payExchangeInfoList.setRecpAccount(customsConfig.getRecpAccount());
payExchangeInfoList.setRecpCode(customsConfig.getRecpCode());
payExchangeInfoList.setRecpName(customsConfig.getRecpName());
List payExchangeInfoLists = new ArrayList<>();
payExchangeInfoLists.add(payExchangeInfoList);
payExInfoStr.setPayExchangeInfoLists(payExchangeInfoLists);
payExInfoStr.setServiceTime(System.currentTimeMillis());
payExInfoStr.setCertNo(customsConfig.getCertNo());
return payExInfoStr;
}
class ImplWebCallback implements WebCallback
{
PayExInfoStr payExInfoStr;
public ImplWebCallback(PayExInfoStr payExInfoStr)
{
this.payExInfoStr = payExInfoStr;
}
@Override
public void message(String key, Object param)
{
LOGGER.info("key-{}, message-{}", key, param);
String signValue = null;
if (!StringUtils.isEmpty(param))
{
JSONObject jsonObject = JSON.parseObject(String.valueOf(param));
Integer id = jsonObject.getInteger("_id");
if (id != null && id == 1)
{
JSONObject args = jsonObject.getJSONObject("_args");
JSONArray data = args.getJSONArray("Data");
signValue = data.getString(0);
LOGGER.info("key-{}, signValue-{}", key, signValue);
payExInfoStr.setSignValue(signValue);
setPostData(payExInfoStr);
}
}
// todo 放入处理队列
}
@Override
public void error(Object param)
{
LOGGER.info("error-{}", param);
}
}
}
上面接口是经过修改联调后的最终版,最关键的报文拼接和websocket发送机制,大家可以参考一下,避免躺坑。
补充说明:
上面的代码经过测试有两个问题,现在修复如下:
1、CustomsConfig 是一个自定义配置信息,相关的海关参数配置都包含在里面,涉及系统敏感信息就不贴了
2、海关给出的报文属性是payExchangeInfoList!!!注意是错的 应该是payExchangeInfoLists,需要加s.