我使用 生成运单接口生成运单后
https://api.weixin.qq.com/cgi-bin/express/business/order/add?access_token=ACCESS_TOKEN
1: 该运单无法在快递管家中查询到 https://vip.zto.com/login
73588021027366
2:我找到我们签约的中通快递员,说明运单号,他们的系统可以查询到
3:但是无法在无法在快递管家中查询到 https://vip.zto.com/login
4:网点面单也确认了是同一个
分别是 java中的配置,快递员查询到的信息,快递管家中的面单信息,小程序物流助手配置
所以这到底是为什么?
```java
@PostMapping("/create-order/{orderId}")
public ReturnBean> createLogisticsOrder(
@PathVariable("orderId") Long orderId,
@RequestBody(required = false) Map shopData) {
log.info("创建物流订单请求,orderId={}", orderId);
// 步骤1:验证订单状态
AccountsOrderEntity order = accountsOrderService.getById(orderId);
if (order == null) {
throw SystemException.error(ErrorCodeEnum.DATA_NOT_FOUND, "订单不存在");
}
if (!"pending_shipment".equals(order.getStatus())) {
throw SystemException.error(ErrorCodeEnum.OPERATION_FAILED,
"只有待发货的订单才能创建物流订单,当前状态: " + order.getStatus());
}
// 步骤2:检查是否已有物流订单
LambdaQueryWrapper logisticsWrapper = new LambdaQueryWrapper<>();
logisticsWrapper.eq(LogisticsOrderEntity::getOrderId, orderId);
LogisticsOrderEntity existingLogisticsOrder = logisticsOrderService.getOne(logisticsWrapper);
if (existingLogisticsOrder != null) {
// 如果运单已取消,清除Redis缓存并允许重新创建
if ("cancelled".equals(existingLogisticsOrder.getStatus())) {
log.warn("订单已存在但运单已取消,清除缓存并允许重新创建,orderId={}, waybillId={}",
orderId, existingLogisticsOrder.getWaybillId());
String cacheKey = LOGISTICS_REDIS_ADD_ORDER + orderId;
try {
RedisUtil.delete(cacheKey);
log.info("已清除已取消运单的Redis缓存,orderId={}", orderId);
} catch (Exception e) {
log.warn("清除Redis缓存失败,但不影响业务,orderId={}, error={}", orderId, e.getMessage());
}
// 继续执行,允许重新创建物流订单
} else {
// 运单未取消,返回现有运单信息
log.info("订单已存在物流订单,orderId={}, waybillId={}", orderId, existingLogisticsOrder.getWaybillId());
Map result = new HashMap<>();
result.put("waybill_id", existingLogisticsOrder.getWaybillId());
result.put("company_name", existingLogisticsOrder.getCompanyName());
result.put("status", existingLogisticsOrder.getStatus());
result.put("is_existing", true);
return ReturnBean.success("该订单已存在物流订单", result);
}
}
// 步骤3:验证收货信息
List missingFields = new ArrayList<>();
if (StringUtils.isBlank(order.getShippingName())) {
missingFields.add("收货人姓名");
}
if (StringUtils.isBlank(order.getShippingPhone())) {
missingFields.add("收货人电话");
}
if (StringUtils.isBlank(order.getShippingAddress())) {
missingFields.add("收货地址");
}
if (!missingFields.isEmpty()) {
throw SystemException.error(ErrorCodeEnum.PARAM_NOT_NULL,
"订单缺少以下信息:" + String.join("、", missingFields));
}
// 步骤4:获取用户OpenID
String openid = getUserOpenid(order);
if (StringUtils.isBlank(openid)) {
throw SystemException.error(ErrorCodeEnum.DATA_NOT_FOUND,
"无法获取用户OpenID,请确保订单已支付");
}
// 步骤5:查询订单商品项
LambdaQueryWrapper itemWrapper = new LambdaQueryWrapper<>();
itemWrapper.eq(AccountsOrderitemEntity::getOrderId, orderId);
List orderItems = accountsOrderitemService.list(itemWrapper);
if (orderItems == null || orderItems.isEmpty()) {
throw SystemException.error(ErrorCodeEnum.DATA_NOT_FOUND, "订单商品项为空");
}
// 步骤6:构建物流订单数据
Map logisticsData = buildLogisticsOrderData(order, orderItems, openid, shopData);
// 步骤7:检查Redis缓存,处理可能的异常情况(微信已创建但数据库未保存)
String cacheKey = LOGISTICS_REDIS_ADD_ORDER + orderId;
String cachedResult = RedisUtil.get(cacheKey);
Map createResult;
boolean isFromCache = false;
if (StringUtils.isNotBlank(cachedResult)) {
// Redis中有缓存,说明之前可能调用过微信API但数据库保存失败
log.warn("检测到Redis缓存,orderId={},可能之前创建过但数据库保存失败,尝试恢复", orderId);
try {
@SuppressWarnings("unchecked")
Map parsedResult = JSON.parseObject(cachedResult, Map.class);
// 检查缓存中的运单号是否已在数据库中存在
String cachedWaybillId = (String) parsedResult.get("waybill_id");
if (StringUtils.isNotBlank(cachedWaybillId)) {
LambdaQueryWrapper waybillWrapper = new LambdaQueryWrapper<>();
waybillWrapper.eq(LogisticsOrderEntity::getWaybillId, cachedWaybillId);
LogisticsOrderEntity existingByWaybill = logisticsOrderService.getOne(waybillWrapper);
if (existingByWaybill != null) {
// 检查运单状态:如果已取消,清除缓存并重新创建
if ("cancelled".equals(existingByWaybill.getStatus())) {
log.warn("检测到缓存中的运单已取消,清除缓存并重新创建,orderId={}, waybillId={}",
orderId, cachedWaybillId);
RedisUtil.delete(cacheKey);
// 继续执行,重新创建物流订单
createResult = weChatLogisticsService.createOrder(logisticsData);
} else {
// 数据库已存在该运单号且未取消,说明数据已恢复,直接返回
log.info("数据库已存在该运单号,数据已恢复,orderId={}, waybillId={}", orderId, cachedWaybillId);
Map result = new HashMap<>();
result.put("waybill_id", existingByWaybill.getWaybillId());
result.put("company_name", existingByWaybill.getCompanyName());
result.put("status", existingByWaybill.getStatus());
result.put("is_existing", true);
result.put("is_recovered", true);
return ReturnBean.success("该订单已存在物流订单(已恢复)", result);
}
} else {
// 数据库不存在,但缓存中有数据,可能是之前创建失败,使用缓存继续处理
createResult = parsedResult;
isFromCache = true;
log.info("使用Redis缓存数据继续处理,orderId={}, waybillId={}", orderId, cachedWaybillId);
}
} else {
// 缓存中没有运单号,删除无效缓存并重新创建
log.warn("Redis缓存中没有运单号,删除无效缓存并重新创建,orderId={}", orderId);
RedisUtil.delete(cacheKey);
createResult = weChatLogisticsService.createOrder(logisticsData);
}
} catch (Exception e) {
// 缓存数据解析失败,说明缓存可能损坏,删除缓存并重新创建
log.error("解析Redis缓存数据失败,删除损坏的缓存并重新创建,orderId={}, error={}", orderId, e.getMessage(), e);
RedisUtil.delete(cacheKey);
createResult = weChatLogisticsService.createOrder(logisticsData);
}
} else {
// Redis中没有缓存,说明是正常的新订单,可以安全创建
log.info("Redis无缓存,正常创建物流订单,orderId={}", orderId);
createResult = weChatLogisticsService.createOrder(logisticsData);
}
// 步骤8:如果调用微信API成功,立即保存到Redis(4天过期,预留bug修复时间)
if (Boolean.TRUE.equals(createResult.get("success"))) {
try {
String resultJson = JSON.toJSONString(createResult);
RedisUtil.saveString(cacheKey, resultJson, LOGISTICS_CACHE_EXPIRE_TIME_BUG_FIX);
log.info("微信API调用成功,结果已保存到Redis缓存(4天),orderId={}, waybillId={}",
orderId, createResult.get("waybill_id"));
} catch (Exception e) {
log.warn("保存物流订单创建结果到Redis失败,但不影响业务,orderId={}, error={}", orderId, e.getMessage());
}
} else {
// API调用失败,抛出异常
throw SystemException.error(ErrorCodeEnum.OPERATION_FAILED,
"创建物流订单失败: " + createResult.get("error"));
}
// 步骤9:保存物流订单到数据库
String waybillId = (String) createResult.get("waybill_id");
String companyCode = (String) createResult.get("company_code");
String companyName = (String) createResult.get("company_name");
LogisticsOrderEntity logisticsOrder = new LogisticsOrderEntity();
logisticsOrder.setOrderId(orderId);
logisticsOrder.setWaybillId(waybillId);
logisticsOrder.setCompanyCode(companyCode);
logisticsOrder.setCompanyName(companyName);
logisticsOrder.setStatus("created");
logisticsOrder.setWaybillData(JSON.toJSONString(createResult));
logisticsOrder.setCreatedAt(LocalDateTime.now());
// 设置物流费用字段(数据库字段不允许为NULL,必须设置默认值)
logisticsOrder.setLogisticsFee(BigDecimal.ZERO); // 实际费用,创建时默认为0
logisticsOrder.setEstimatedFee(BigDecimal.ZERO); // 预估费用,创建时默认为0
// 设置错误信息字段(数据库字段不允许为NULL,必须设置默认值)
logisticsOrder.setErrorMessage(""); // 错误信息,创建时默认为空字符串
try {
logisticsOrderService.save(logisticsOrder);
} catch (Exception e) {
// 数据库保存失败,但微信已创建,Redis中已有4天缓存,可以后续恢复
log.error("数据库保存失败,但微信已创建订单,Redis缓存保留4天用于恢复,orderId={}, waybillId={}, error={}",
orderId, waybillId, e.getMessage(), e);
throw SystemException.error(ErrorCodeEnum.OPERATION_FAILED,
"物流订单在微信中已创建,但数据库保存失败,请稍后重试或联系管理员恢复。运单号: " + waybillId);
}
// 步骤10:更新订单状态和物流信息
order.setLogisticsCompany(companyName);
order.setLogisticsNumber(waybillId);
order.setLogisticsStatus("created");
order.setStatus("shipped");
order.setShippedAt(LocalDateTime.now());
order.setUpdatedAt(LocalDateTime.now());
try {
accountsOrderService.updateById(order);
} catch (Exception e) {
// 订单更新失败,但物流订单已保存,记录警告
log.warn("订单状态更新失败,但物流订单已保存,orderId={}, waybillId={}, error={}",
orderId, waybillId, e.getMessage());
}
// 步骤11:数据库保存成功,将Redis缓存过期时间改为30分钟(或删除)
try {
// 方案1:缩短过期时间为30分钟(推荐,保留短期缓存提升性能)
String resultJson = JSON.toJSONString(createResult);
RedisUtil.saveString(cacheKey, resultJson, LOGISTICS_CACHE_EXPIRE_TIME_NORMAL);
log.info("数据库保存成功,Redis缓存过期时间已更新为30分钟,orderId={}, waybillId={}", orderId, waybillId);
} catch (Exception e) {
log.warn("更新Redis缓存过期时间失败,但不影响业务,orderId={}, error={}", orderId, e.getMessage());
}
log.info("物流订单创建成功,orderId={}, waybillId={}, isFromCache={}", orderId, waybillId, isFromCache);
Map result = new HashMap<>();
result.put("logistics_order_id", logisticsOrder.getId());
result.put("waybill_id", waybillId);
result.put("company_name", companyName);
result.put("status", "created");
return ReturnBean.success("物流订单创建成功", result);
}
```
---
## 调用的私有方法
### 2. getUserOpenid - 获取用户OpenID
**位置**: `org.example.huolingjava.logistics.controller.LogisticsController`
```java
/**
* 获取用户OpenID
* 优先从payment_transactions表获取,其次从订单表获取
*/
private String getUserOpenid(AccountsOrderEntity order) {
// 1. 优先从payment_transactions表获取(微信官方数据)
if (order.getPaymentOrderId() != null) {
PaymentOrdersEntity paymentOrder = paymentOrdersService.getById(order.getPaymentOrderId());
if (paymentOrder != null && StringUtils.isNotBlank(paymentOrder.getOpenid())) {
return paymentOrder.getOpenid();
}
}
// 2. 从payment_transactions表查询(根据订单号)
if (StringUtils.isNotBlank(order.getOrderNumber())) {
LambdaQueryWrapper transactionWrapper = new LambdaQueryWrapper<>();
transactionWrapper.like(PaymentTransactionsEntity::getOutTradeNo, order.getOrderNumber())
.orderByDesc(PaymentTransactionsEntity::getCreatedTime)
.last("LIMIT 1");
PaymentTransactionsEntity transaction = paymentTransactionsService.getOne(transactionWrapper);
if (transaction != null && StringUtils.isNotBlank(transaction.getOpenid())) {
return transaction.getOpenid();
}
}
// 3. 使用订单保存的信息
if (StringUtils.isNotBlank(order.getOrderOpenid())) {
return order.getOrderOpenid();
}
if (StringUtils.isNotBlank(order.getPaymentOpenid())) {
return order.getPaymentOpenid();
}
return null;
}
```
### 3. buildLogisticsOrderData - 构建物流订单数据
**位置**: `org.example.huolingjava.logistics.controller.LogisticsController`
```java
/**
* 构建物流订单数据
*/
private Map buildLogisticsOrderData(
AccountsOrderEntity order,
List orderItems,
String openid,
Map shopData) {
Map logisticsData = new HashMap<>();
logisticsData.put("orderId", order.getId());
logisticsData.put("orderNumber", order.getOrderNumber());
logisticsData.put("customerName", order.getShippingName());
logisticsData.put("customerPhone", order.getShippingPhone());
logisticsData.put("customerAddress", order.getShippingAddress());
logisticsData.put("openid", openid);
logisticsData.put("totalAmount", order.getActualAmount());
// 构建商品列表
List> products = new ArrayList<>();
for (AccountsOrderitemEntity item : orderItems) {
Map product = new HashMap<>();
product.put("id", item.getProductId());
product.put("name", item.getProductName());
product.put("quantity", item.getQuantity());
product.put("category", item.getProductCategory());
products.add(product);
}
logisticsData.put("products", products);
// 计算总重量(简化处理,默认0.1kg)
logisticsData.put("totalWeight", 0.1);
// 店铺信息(如果有)
if (shopData != null) {
logisticsData.put("shop", shopData);
}
return logisticsData;
}
```
---
## 调用的Service方法
### 4. WeChatLogisticsService.createOrder - 创建物流订单(微信API)
**位置**: `org.example.huolingjava.logistics.service.WeChatLogisticsService`
```java
/**
* 创建物流订单
*
* @param orderData 订单数据,包含:
* - orderId: 订单ID
* - orderNumber: 订单号
* - customerName: 收货人姓名
* - customerPhone: 收货人电话
* - customerAddress: 收货地址
* - openid: 用户微信OpenID
* - products: 商品列表
* - totalWeight: 总重量(kg)
* - totalAmount: 订单总金额(元)
* @return 创建结果,包含waybill_id、company_code、company_name等
*/
public Map createOrder(Map orderData) {
if (orderData == null) {
throw SystemException.error(ErrorCodeEnum.PARAM_NOT_NULL, "订单数据不能为空");
}
// 获取access_token
String accessToken = getAccessToken();
// 构建请求数据
Map requestData = buildCreateOrderRequest(orderData);
// 调用微信API(使用官方新接口路径)
// 官方文档:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/express/express-by-business/addOrder.html
String url = properties.getBaseUrl() + "/order/add";
url += "?access_token=" + accessToken;
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("User-Agent", "HuolingMP/1.0");
HttpEntity requestEntity = new HttpEntity<>(
JSON.toJSONString(requestData), headers);
ResponseEntity response = restTemplate.postForEntity(
url, requestEntity, String.class);
JSONObject result = JSON.parseObject(response.getBody());
if (result.getIntValue("errcode") == 0) {
String waybillId = result.getString("waybill_id");
String orderId = result.getString("order_id");
log.info("创建物流订单成功,waybillId={}, orderId={}", waybillId, orderId);
Map responseData = new HashMap<>();
responseData.put("success", true);
responseData.put("waybill_id", waybillId);
responseData.put("company_code", properties.getZtoCompanyCode());
responseData.put("company_name", properties.getZtoCompanyName());
responseData.put("order_id", orderId);
return responseData;
} else {
int errcode = result.getIntValue("errcode");
String errmsg = result.getString("errmsg");
log.error("创建物流订单失败,errcode={}, errmsg={}", errcode, errmsg);
// 处理特殊错误码
if (errcode == 40066) {
throw SystemException.error(ErrorCodeEnum.OPERATION_FAILED,
"物流订单创建失败:回调URL未在微信后台配置。回调URL: " + properties.getCallbackUrl());
}
throw SystemException.error(ErrorCodeEnum.OPERATION_FAILED,
"创建物流订单失败: " + errmsg);
}
} catch (SystemException e) {
throw e;
} catch (Exception e) {
log.error("创建物流订单异常", e);
throw SystemException.error(ErrorCodeEnum.OPERATION_FAILED,
"创建物流订单异常: " + e.getMessage());
}
}
```
### 5. WeChatLogisticsService.getAccessToken - 获取微信access_token
**位置**: `org.example.huolingjava.logistics.service.WeChatLogisticsService`
```java
/**
* 获取微信access_token
*
* @return access_token字符串
*/
public String getAccessToken() {
String url = "https://api.weixin.qq.com/cgi-bin/token";
Map params = new HashMap<>();
params.put("grant_type", "client_credential");
params.put("appid", properties.getAppId());
params.put("secret", properties.getAppSecret());
try {
StringBuilder urlBuilder = new StringBuilder(url);
urlBuilder.append("?grant_type=client_credential");
urlBuilder.append("&appid=").append(properties.getAppId());
urlBuilder.append("&secret=").append(properties.getAppSecret());
ResponseEntity response = restTemplate.getForEntity(urlBuilder.toString(), String.class);
JSONObject result = JSON.parseObject(response.getBody());
if (result.containsKey("access_token")) {
String accessToken = result.getString("access_token");
log.info("获取微信access_token成功");
return accessToken;
} else {
String errmsg = result.getString("errmsg");
int errcode = result.getIntValue("errcode");
log.error("获取微信access_token失败,errcode={}, errmsg={}", errcode, errmsg);
throw SystemException.error(ErrorCodeEnum.OPERATION_FAILED,
"获取微信access_token失败: " + errmsg);
}
} catch (Exception e) {
log.error("获取微信access_token异常", e);
throw SystemException.error(ErrorCodeEnum.OPERATION_FAILED,
"获取微信access_token异常: " + e.getMessage());
}
}
```
### 6. WeChatLogisticsService.buildCreateOrderRequest - 构建创建物流订单的请求数据
**位置**: `org.example.huolingjava.logistics.service.WeChatLogisticsService`
```java
/**
* 构建创建物流订单的请求数据
*/
@SuppressWarnings("unchecked")
private Map buildCreateOrderRequest(Map orderData) {
Map requestData = new HashMap<>();
// 基础信息
requestData.put("add_source", 2); // 2表示小程序
requestData.put("order_id", String.valueOf(orderData.get("orderNumber")));
requestData.put("delivery_id", properties.getZtoDeliveryId());
requestData.put("biz_id", properties.getZtoBizId());
requestData.put("openid", String.valueOf(orderData.get("openid")));
requestData.put("wx_appid", properties.getAppId());
requestData.put("notify_url", properties.getCallbackUrl());
// 发货人信息
WeChatLogisticsProperties.SenderInfo sender = properties.getSender();
Map senderMap = new HashMap<>();
senderMap.put("name", sender.getName());
senderMap.put("tel", sender.getTel());
senderMap.put("mobile", sender.getMobile());
senderMap.put("company", sender.getCompany());
senderMap.put("province", sender.getProvince());
senderMap.put("city", sender.getCity());
senderMap.put("area", sender.getArea());
senderMap.put("address", sender.getAddress());
requestData.put("sender", senderMap);
// 收货人信息
Map receiverMap = new HashMap<>();
String customerName = String.valueOf(orderData.get("customerName"));
String customerPhone = String.valueOf(orderData.get("customerPhone"));
String customerAddress = String.valueOf(orderData.get("customerAddress"));
receiverMap.put("name", customerName.length() > 20 ? customerName.substring(0, 20) : customerName);
receiverMap.put("tel", customerPhone);
receiverMap.put("mobile", customerPhone);
// 解析地址(简化处理,实际应该解析省市区)
receiverMap.put("province", "广东省");
receiverMap.put("city", "中山市");
receiverMap.put("area", "中山港街道");
receiverMap.put("address", customerAddress.length() > 50 ? customerAddress.substring(0, 50) : customerAddress);
requestData.put("receiver", receiverMap);
// 货物信息
Map cargoMap = new HashMap<>();
java.util.List> products = (java.util.List>) orderData.get("products");
if (products == null || products.isEmpty()) {
throw SystemException.error(ErrorCodeEnum.PARAM_NOT_NULL, "商品列表不能为空");
}
cargoMap.put("count", products.size());
// 计算总重量
Double totalWeight = orderData.get("totalWeight") != null ?
Double.parseDouble(String.valueOf(orderData.get("totalWeight"))) : 0.1;
if (totalWeight <= 0) {
totalWeight = 0.1; // 默认0.1kg
}
cargoMap.put("weight", totalWeight);
// 商品详情列表
java.util.List> detailList = new java.util.ArrayList<>();
for (Map product : products) {
Map detail = new HashMap<>();
String productName = String.valueOf(product.get("name"));
// 移除括号,限制长度
productName = productName.replace("(", "").replace(")", "");
detail.put("name", productName.length() > 30 ? productName.substring(0, 30) : productName);
detail.put("count", product.get("quantity"));
detailList.add(detail);
}
cargoMap.put("detail_list", detailList);
requestData.put("cargo", cargoMap);
// 店铺信息(使用第一个商品)
Map shopMap = new HashMap<>();
Map firstProduct = products.get(0);
String goodsName = String.valueOf(firstProduct.get("name"));
goodsName = goodsName.replace("(", "").replace(")", "");
shopMap.put("goods_name", goodsName.length() > 30 ? goodsName.substring(0, 30) : goodsName);
shopMap.put("goods_count", products.size());
requestData.put("shop", shopMap);
return requestData;
}
