云途物流API开发实战:寄件下订单接口完整实现

一、前言
在跨境电商、ERP物流系统开发中,物流对接是核心刚需环节。云途物流(YunExpress)作为主流跨境物流服务商,提供了成熟的开放API接口,支持在线创建物流订单、获取运单号、轨迹查询等核心能力。
本文基于 Java,完整实现云途物流寄件下单接口,包含参数校验、本地草稿落库、API请求体组装、响应解析、异常容错、数据回写全流程。代码可直接商用,同时解决了官方文档字段兼容、空值过滤、多格式响应解析等开发痛点。
核心实现思路:
-
参数合法性前置校验,拦截无效请求;
-
订单先以草稿状态落库,避免API调用失败数据丢失;
-
适配云途snake_case接口规范,智能组装请求参数;
-
调用云途官方下单API,兼容多种响应格式;
-
成功回写运单号、更新订单状态;失败记录错误信息、保障数据一致性;
-
全局异常捕获,友好返回前端提示。
二、开发环境与前置准备
2.1 技术栈
-
后端框架:RuoYi-Vue 开源框架
-
开发语言:Java 8+
-
JSON解析:Fastjson2
-
数据库:MySQL
-
对接接口:云途物流 /v1/order/package/create 包裹创建接口
2.2 前置配置
-
已申请云途物流开发者账号、获取API密钥、商户编码;
-
已封装云途通用请求客户端(YunExpressClient)、客户端工厂类;
-
已创建物流订单数据库实体
ErpLogisticsOrders及对应的Mapper层。
2.3 官方文档

三、核心业务逻辑设计
3.1 订单状态设计
为保障下单流程容错性,自定义三种订单状态:
-
0 草稿状态:参数校验通过,已落库,未调用云途API;
-
1 下单成功:API调用成功,已获取运单号、云途订单号;
-
2 下单失败:API调用异常,记录错误信息,可后续重试。
3.2 核心流程
-
参数校验:校验渠道代码、收件人信息、重量、申报信息等必填字段;
-
字段兼容处理:适配新旧字段,兼容历史数据展示;
-
草稿落库:优先保存订单数据,防止接口报错数据丢失;
-
构建请求体:严格按照云途snake_case规范组装参数,过滤空值;
-
调用第三方API:通过工厂类获取租户对应的云途客户端,发起下单请求;
-
响应解析:兼容云途多种返回格式,精准提取运单号、订单号;
-
数据回写:成功更新订单状态、运单号、提交时间、原始响应数据;
-
异常处理:捕获API异常、系统异常,更新失败状态并记录错误日志。
3.3 效果截图


四、完整核心代码实现
本文只展示核心Service业务实现层,基础增删改查沿用若依通用模板,重点讲解createLogisticsOrder 下单核心方法。
4.1 Service层完整代码
import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.ecommerce.domain.ErpLogisticsOrders;
import com.ruoyi.ecommerce.mapper.ErpLogisticsOrdersMapper;
import com.ruoyi.ecommerce.service.IErpLogisticsOrdersService;
import com.ruoyi.ecommerce.yunexpress.YunExpressClient;
import com.ruoyi.ecommerce.yunexpress.YunExpressClientFactory;
import com.ruoyi.ecommerce.yunexpress.YunExpressException;
/**
* 物流订单Service业务层处理
*
* @author superjean
* @date 2026-07-08
*/
@Service
public class ErpLogisticsOrdersServiceImpl implements IErpLogisticsOrdersService {
@Autowired
private ErpLogisticsOrdersMapper erpLogisticsOrdersMapper;
@Autowired
private YunExpressClientFactory yunExpressClientFactory;
@Override
public ErpLogisticsOrders selectErpLogisticsOrdersById(Long id) {
return erpLogisticsOrdersMapper.selectErpLogisticsOrdersById(id);
}
@Override
public List<ErpLogisticsOrders> selectErpLogisticsOrdersList(ErpLogisticsOrders erpLogisticsOrders) {
return erpLogisticsOrdersMapper.selectErpLogisticsOrdersList(erpLogisticsOrders);
}
@Override
public int insertErpLogisticsOrders(ErpLogisticsOrders erpLogisticsOrders) {
return erpLogisticsOrdersMapper.insertErpLogisticsOrders(erpLogisticsOrders);
}
@Override
public int updateErpLogisticsOrders(ErpLogisticsOrders erpLogisticsOrders) {
return erpLogisticsOrdersMapper.updateErpLogisticsOrders(erpLogisticsOrders);
}
@Override
public int deleteErpLogisticsOrdersById(Long id) {
return erpLogisticsOrdersMapper.deleteErpLogisticsOrdersById(id);
}
@Override
public int deleteErpLogisticsOrdersByIds(Long[] ids) {
return erpLogisticsOrdersMapper.deleteErpLogisticsOrdersByIds(ids);
}
/**
* 创建物流订单:先按草稿落库,再调云途 /v1/package/create 提交,
* 成功回写运单号、状态置 1;失败状态置 2 并保留错误信息。
*/
@Override
public Map<String, Object> createLogisticsOrder(ErpLogisticsOrders order) {
// 必填项校验(与云途 snake_case 结构对齐)
if (StringUtils.isEmpty(order.getProductCode())) {
throw new ServiceException("渠道代码(product_code)不能为空");
}
String receiverFirstName = StringUtils.isNotEmpty(order.getReceiverFirstName())
? order.getReceiverFirstName() : order.getReceiverName();
if (StringUtils.isEmpty(receiverFirstName)) {
throw new ServiceException("收件人名(receiver.first_name)不能为空");
}
if (StringUtils.isEmpty(order.getReceiverCountry())) {
throw new ServiceException("收件人国家(receiver.country_code)不能为空");
}
if (StringUtils.isEmpty(order.getReceiverCity())) {
throw new ServiceException("收件人城市(receiver.city)不能为空");
}
if (StringUtils.isEmpty(order.getReceiverAddress())) {
throw new ServiceException("收件人地址(receiver.address_lines)不能为空");
}
if (StringUtils.isEmpty(order.getReceiverPostCode())) {
throw new ServiceException("收件人邮编(receiver.postal_code)不能为空");
}
if (order.getWeight() == null) {
throw new ServiceException("包裹重量(packages[].weight)不能为空");
}
if (StringUtils.isEmpty(order.getNameEn())) {
throw new ServiceException("申报英文名(declaration_info[].name_en)不能为空");
}
if (order.getQuantity() == null) {
throw new ServiceException("申报数量(declaration_info[].quantity)不能为空");
}
if (order.getUnitPrice() == null) {
throw new ServiceException("申报单价(declaration_info[].unit_price)不能为空");
}
if (order.getUnitWeight() == null) {
throw new ServiceException("申报单件重量(declaration_info[].unit_weight)不能为空");
}
// 回填兼容字段,便于本地展示
if (StringUtils.isEmpty(order.getReceiverName())) {
order.setReceiverName(receiverFirstName);
}
if (StringUtils.isEmpty(order.getCountryCode())) {
order.setCountryCode(order.getReceiverCountry());
}
// 默认草稿状态
if (StringUtils.isEmpty(order.getStatus())) {
order.setStatus("0");
}
// 落库
erpLogisticsOrdersMapper.insertErpLogisticsOrders(order);
Map<String, Object> result = new HashMap<>();
try {
YunExpressClient client = yunExpressClientFactory.create(order.getUserId());
JSONObject body = buildCreateBody(order);
JSONObject resp = client.createPackage(body);
// 解析云途返回:result/Items 中含运单号
String trackingNo = parseTrackingNumber(resp);
String yunOrderNo = parseOrderNo(resp);
order.setTrackingNumber(trackingNo);
order.setYunExpressOrderNo(yunOrderNo);
order.setStatus("1");
order.setSubmitMessage(StringUtils.isNotEmpty(trackingNo) ? trackingNo : "提交成功");
order.setSubmitTime(DateUtils.getNowDate());
order.setRawInfo(resp.toJSONString());
erpLogisticsOrdersMapper.updateErpLogisticsOrders(order);
result.put("success", true);
result.put("trackingNumber", trackingNo);
result.put("yunExpressOrderNo", yunOrderNo);
result.put("message", "提交成功,云途运单号:" + trackingNo);
} catch (YunExpressException e) {
order.setStatus("2");
order.setSubmitMessage(e.getMessage());
order.setSubmitTime(DateUtils.getNowDate());
erpLogisticsOrdersMapper.updateErpLogisticsOrders(order);
result.put("success", false);
result.put("message", "提交云途失败:" + e.getMessage());
// 抛出便于前端提示,本地草稿已保存
throw new ServiceException("提交云途失败:" + e.getMessage());
} catch (Exception e) {
order.setStatus("2");
order.setSubmitMessage(e.getMessage());
order.setSubmitTime(DateUtils.getNowDate());
erpLogisticsOrdersMapper.updateErpLogisticsOrders(order);
throw new ServiceException("提交云途失败:" + e.getMessage());
}
return result;
}
/**
* 构造云途 /v1/package/create 请求体(snake_case 结构)。
* 字段参考云途官方样例:product_code / receiver / packages / declaration_info / sender /
* customs_number / extra_services 等。空字符串字段不放入,避免覆盖默认。
*/
private JSONObject buildCreateBody(ErpLogisticsOrders o) {
JSONObject body = new JSONObject();
putIfNotEmpty(body, "product_code", o.getProductCode());
putIfNotEmpty(body, "customer_order_number", o.getCustomerOrderNo());
putIfNotEmpty(body, "platform_account_code", o.getPlatformAccountCode());
putIfNotEmpty(body, "source_code", o.getSourceCode());
putIfNotEmpty(body, "sensitive_type", o.getSensitiveType());
putIfNotEmpty(body, "label_type", o.getLabelType());
putIfNotEmpty(body, "point_relais_num", o.getPointRelaisNum());
putIfNotEmpty(body, "weight_unit", o.getWeightUnit());
putIfNotEmpty(body, "size_unit", o.getSizeUnit());
putIfNotEmpty(body, "dangerous_goods_type", o.getDangerousGoodsType());
// 订单号组
JSONObject orderNumbers = new JSONObject();
putIfNotEmpty(orderNumbers, "waybill_number", o.getWaybillNumber());
putIfNotEmpty(orderNumbers, "platform_order_number", o.getPlatformOrderNumber());
putIfNotEmpty(orderNumbers, "tracking_number", o.getTrackingNumber());
if (StringUtils.isNotEmpty(o.getReferenceNumbers())) {
JSONArray refArr = new JSONArray();
for (String s : o.getReferenceNumbers().split(",")) {
String t = s.trim();
if (StringUtils.isNotEmpty(t)) {
refArr.add(t);
}
}
if (!refArr.isEmpty()) {
orderNumbers.put("reference_numbers", refArr);
}
}
if (!orderNumbers.isEmpty()) {
body.put("order_numbers", orderNumbers);
}
// 收件人
body.put("receiver", buildAddress(o, true));
// 发件人(任一关键字段填写则放入)
if (StringUtils.isNotEmpty(o.getSenderFirstName()) || StringUtils.isNotEmpty(o.getSenderName())
|| StringUtils.isNotEmpty(o.getSenderAddress())) {
body.put("sender", buildAddress(o, false));
}
// 包裹(单包裹)
JSONObject pkg = new JSONObject();
if (o.getLength() != null) {
pkg.put("length", o.getLength());
}
if (o.getWidth() != null) {
pkg.put("width", o.getWidth());
}
if (o.getHeight() != null) {
pkg.put("height", o.getHeight());
}
pkg.put("weight", o.getWeight());
// 申报信息(单行)
JSONArray declArr = new JSONArray();
JSONObject decl = new JSONObject();
putIfNotEmpty(decl, "sku_code", StringUtils.isNotEmpty(o.getSkuCode()) ? o.getSkuCode() : o.getSku());
putIfNotEmpty(decl, "name_local", o.getNameLocal());
putIfNotEmpty(decl, "name_en", o.getNameEn());
if (o.getQuantity() != null) {
decl.put("quantity", o.getQuantity());
}
if (o.getUnitPrice() != null) {
decl.put("unit_price", o.getUnitPrice());
}
if (o.getUnitWeight() != null) {
decl.put("unit_weight", o.getUnitWeight());
}
putIfNotEmpty(decl, "hs_code", o.getHsCode());
putIfNotEmpty(decl, "sales_url", o.getSalesUrl());
putIfNotEmpty(decl, "currency", o.getDeclareCurrency());
putIfNotEmpty(decl, "material", o.getMaterial());
putIfNotEmpty(decl, "purpose", o.getPurpose());
putIfNotEmpty(decl, "brand", o.getBrand());
putIfNotEmpty(decl, "spec", o.getSpec());
putIfNotEmpty(decl, "model", o.getModel());
putIfNotEmpty(decl, "remark", o.getRemark());
declArr.add(decl);
JSONArray packages = new JSONArray();
packages.add(pkg);
body.put("packages", packages);
body.put("declaration_info", declArr);
// 清关号码
JSONObject customs = new JSONObject();
putIfNotEmpty(customs, "tax_number", o.getCustomsTaxNumber());
putIfNotEmpty(customs, "ioss_code", o.getCustomsIossCode());
putIfNotEmpty(customs, "vat_code", o.getCustomsVatCode());
putIfNotEmpty(customs, "eori_number", o.getCustomsEoriNumber());
if (!customs.isEmpty()) {
body.put("customs_number", customs);
}
// 增值服务
if (StringUtils.isNotEmpty(o.getExtraCode())) {
JSONArray extraArr = new JSONArray();
JSONObject extra = new JSONObject();
extra.put("extra_code", o.getExtraCode());
putIfNotEmpty(extra, "extra_value", o.getExtraValue());
extraArr.add(extra);
body.put("extra_services", extraArr);
}
return body;
}
/**
* 构造收件人/发件人地址对象(snake_case)。isReceiver=true 取收件人字段,否则取发件人字段。
*/
private JSONObject buildAddress(ErpLogisticsOrders o, boolean isReceiver) {
JSONObject addr = new JSONObject();
String firstName = isReceiver ? o.getReceiverFirstName() : o.getSenderFirstName();
String lastName = isReceiver ? o.getReceiverLastName() : o.getSenderLastName();
// 收件人兼容旧 receiver_name(发件人兼容旧 sender_name)
if (StringUtils.isEmpty(firstName)) {
firstName = isReceiver ? o.getReceiverName() : o.getSenderName();
}
putIfNotEmpty(addr, "first_name", firstName);
putIfNotEmpty(addr, "last_name", lastName);
putIfNotEmpty(addr, "company", isReceiver ? o.getReceiverCompany() : o.getSenderCompany());
putIfNotEmpty(addr, "country_code", isReceiver ? o.getReceiverCountry() : o.getSenderCountry());
putIfNotEmpty(addr, "province", isReceiver ? o.getReceiverState() : o.getSenderState());
putIfNotEmpty(addr, "city", isReceiver ? o.getReceiverCity() : o.getSenderCity());
String street = isReceiver ? o.getReceiverAddress() : o.getSenderAddress();
if (StringUtils.isNotEmpty(street)) {
JSONArray lines = new JSONArray();
lines.add(street);
addr.put("address_lines", lines);
}
putIfNotEmpty(addr, "postal_code", isReceiver ? o.getReceiverPostCode() : o.getSenderPostCode());
putIfNotEmpty(addr, "phone_number", isReceiver ? o.getReceiverPhone() : o.getSenderPhone());
putIfNotEmpty(addr, "email", isReceiver ? o.getReceiverEmail() : o.getSenderEmail());
putIfNotEmpty(addr, "certificate_type", isReceiver ? o.getReceiverCertificateType() : o.getSenderCertificateType());
putIfNotEmpty(addr, "certificate_code", isReceiver ? o.getReceiverCertificateCode() : o.getSenderCertificateCode());
return addr;
}
/**
* 仅在 value 非空时放入 key,避免覆盖默认。
*/
private void putIfNotEmpty(JSONObject obj, String key, String value) {
if (StringUtils.isNotEmpty(value)) {
obj.put(key, value);
}
}
/**
* 从云途创建响应中提取运单号(兼容多种结构)。
*/
private String parseTrackingNumber(JSONObject resp) {
if (resp == null) {
return null;
}
Object result = resp.get("result");
if (result == null) {
result = resp.get("data");
}
if (result instanceof JSONObject) {
JSONObject obj = (JSONObject) result;
// 可能直接是单个对象或 Items 数组
String[] keys = {"TrackingNumber", "WaybillNumber", "trackingNumber", "tracking_number", "waybill_number", "OrderNumber", "order_number"};
for (String k : keys) {
String v = obj.getString(k);
if (StringUtils.isNotEmpty(v)) {
return v;
}
}
JSONArray items = obj.getJSONArray("Items");
if (items != null && !items.isEmpty()) {
JSONObject first = items.getJSONObject(0);
for (String k : keys) {
String v = first.getString(k);
if (StringUtils.isNotEmpty(v)) {
return v;
}
}
}
}
if (result instanceof JSONArray) {
JSONArray arr = (JSONArray) result;
if (!arr.isEmpty()) {
JSONObject first = arr.getJSONObject(0);
String[] keys = {"TrackingNumber", "WaybillNumber", "trackingNumber", "tracking_number", "waybill_number"};
for (String k : keys) {
String v = first.getString(k);
if (StringUtils.isNotEmpty(v)) {
return v;
}
}
}
}
return null;
}
/**
* 从云途创建响应中提取云途订单号。
*/
private String parseOrderNo(JSONObject resp) {
if (resp == null) {
return null;
}
Object result = resp.get("result");
if (result == null) {
result = resp.get("data");
}
if (result instanceof JSONObject) {
JSONObject obj = (JSONObject) result;
String[] keys = {"YunExpressOrderNo", "yun_express_order_no", "OrderNo", "orderNo", "order_number"};
for (String k : keys) {
String v = obj.getString(k);
if (StringUtils.isNotEmpty(v)) {
return v;
}
}
}
return null;
}
}
五、核心功能亮点与踩坑总结
5.1 核心亮点
-
数据安全容错:先落库再调接口,杜绝API超时、异常导致的数据丢失,支持失败重试;
-
强参数校验:精准对齐云途API必填字段,提前拦截非法请求,减少第三方接口报错;
-
全字段兼容:适配新旧字段、snake_case官方规范,自动过滤空值,不覆盖接口默认参数;
-
多格式响应解析:兼容云途result/data、对象/数组多种返回格式,避免接口迭代导致解析失效;
-
完整日志留存:保存原始接口响应数据、错误信息、提交时间,便于问题排查。
5.2 开发踩坑记录
-
字段格式不匹配:云途API强制snake_case下划线格式,Java实体为驼峰,必须手动转换,不能直接序列化实体;
-
空值参数报错:直接传入空字符串会覆盖接口默认值,导致下单失败,必须自定义工具类过滤空值;
-
响应格式不固定:云途不同渠道返回参数大小写、层级不一致,必须多关键字、多结构兼容解析;
-
数据一致性问题:未提前落库会导致接口报错后无任何记录,无法追溯和重试,必须先草稿落库。
六、扩展优化方向
-
重试机制:针对网络超时、临时接口异常,添加定时任务自动重试下单;
-
幂等性设计:添加唯一订单幂等键,防止重复下单;
-
批量下单:适配云途批量创建包裹接口,支持批量订单提交;
-
轨迹同步:新增定时任务,调用轨迹查询API,自动同步物流状态;
-
日志优化:接入日志框架,打印完整请求、响应参数,便于线上问题排查。
七、总结
本文基于Java框架完整实现了云途物流寄件下单API的生产级代码,覆盖参数校验、请求组装、接口调用、响应解析、异常容错、数据落库全流程。代码适配官方接口规范,解决了跨境物流对接中的常见痛点,可直接用于ERP、跨境电商后台、物流管理系统的开发。
更多推荐




所有评论(0)