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

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