| @ -1,22 +0,0 @@ | |||||
| let a = { | |||||
| id : 1565465498,//一级订单id | |||||
| list : [ | |||||
| { | |||||
| price : 44.1,//价格 | |||||
| qualifiedNum : 2,//合格数量 | |||||
| noQualifiedNum : 1,//不合格数量 | |||||
| unrecyclable : 1,//不可回收数量 | |||||
| shopId : 123,//商品id,(质量问题、不可回收不填) | |||||
| pinId : 123,//品牌id,(质量问题、不可回收不填) | |||||
| commonOrderList : [ | |||||
| { | |||||
| testingInstructions : '199168761,654651745,61541684',//检测说明,多个id用逗号分割 | |||||
| testingImages : 'https://xxxx,https://xxxx',//检测报告图片 | |||||
| testingStatus : 0,//【质检合格:0】【质量问题:1】【不可回收:2】 | |||||
| } | |||||
| ] | |||||
| } | |||||
| ] | |||||
| } | |||||
| @ -0,0 +1,152 @@ | |||||
| # Java 版本兼容性说明 | |||||
| ## 📋 概述 | |||||
| 本微信物流模块已针对 **Java 8** 进行了兼容性优化,确保在大多数项目环境中都能正常运行。 | |||||
| ## 🔧 已解决的兼容性问题 | |||||
| ### 1. Map.of() 方法问题 | |||||
| **问题描述:** | |||||
| ```java | |||||
| // Java 9+ 语法 - 在 Java 8 中会报错 | |||||
| return Result.ok(Map.of( | |||||
| "key1", value1, | |||||
| "key2", value2 | |||||
| )); | |||||
| ``` | |||||
| **解决方案:** | |||||
| ```java | |||||
| // Java 8 兼容写法 | |||||
| Map<String, Object> result = new HashMap<>(); | |||||
| result.put("key1", value1); | |||||
| result.put("key2", value2); | |||||
| return Result.ok(result); | |||||
| ``` | |||||
| ### 2. 需要添加的 import 语句 | |||||
| 确保在使用 Map 和 HashMap 的地方添加以下导入: | |||||
| ```java | |||||
| import java.util.HashMap; | |||||
| import java.util.Map; | |||||
| ``` | |||||
| ## 🎯 支持的 Java 版本 | |||||
| | Java 版本 | 支持状态 | 说明 | | |||||
| |-----------|----------|------| | |||||
| | Java 8 | ✅ 完全支持 | 主要兼容目标 | | |||||
| | Java 11 | ✅ 完全支持 | 长期支持版本 | | |||||
| | Java 17 | ✅ 完全支持 | 最新长期支持版本 | | |||||
| | Java 21 | ✅ 完全支持 | 最新长期支持版本 | | |||||
| ## 💡 最佳实践 | |||||
| ### 1. 检查项目 Java 版本 | |||||
| ```bash | |||||
| # 检查 Java 版本 | |||||
| java -version | |||||
| # 检查 Maven 项目的 Java 版本 | |||||
| mvn -version | |||||
| ``` | |||||
| ### 2. 配置 Maven Java 版本 | |||||
| 在 `pom.xml` 中确保 Java 版本配置正确: | |||||
| ```xml | |||||
| <properties> | |||||
| <java.version>8</java.version> | |||||
| <maven.compiler.source>8</maven.compiler.source> | |||||
| <maven.compiler.target>8</maven.compiler.target> | |||||
| </properties> | |||||
| ``` | |||||
| ### 3. IDE 配置 | |||||
| 确保你的 IDE (IntelliJ IDEA, Eclipse 等) 项目设置中的 Java 版本与项目一致。 | |||||
| ## 🐛 常见错误及解决方案 | |||||
| ### 错误1:找不到符号 Map.of() | |||||
| **错误信息:** | |||||
| ``` | |||||
| java: 找不到符号 | |||||
| 符号: 方法 of(java.lang.String,...) | |||||
| 位置: 接口 java.util.Map | |||||
| ``` | |||||
| **解决方案:** | |||||
| - 使用 `HashMap` 替代 `Map.of()` | |||||
| - 确保已导入 `java.util.HashMap` | |||||
| ### 错误2:Lambda 表达式报错 | |||||
| **错误信息:** | |||||
| ``` | |||||
| java: lambda expressions are not supported in -source 7 | |||||
| ``` | |||||
| **解决方案:** | |||||
| - 检查 Maven 配置,确保 Java 版本为 8 或以上 | |||||
| - 更新 `maven-compiler-plugin` 配置 | |||||
| ### 错误3:Stream API 不可用 | |||||
| **错误信息:** | |||||
| ``` | |||||
| java: cannot find symbol | |||||
| symbol: method stream() | |||||
| ``` | |||||
| **解决方案:** | |||||
| - 确保 Java 版本为 8 或以上 | |||||
| - Stream API 是 Java 8 引入的特性 | |||||
| ## 🛠️ 验证方法 | |||||
| ### 1. 编译测试 | |||||
| ```bash | |||||
| # 编译项目 | |||||
| mvn clean compile | |||||
| # 运行测试 | |||||
| mvn test | |||||
| ``` | |||||
| ### 2. 功能测试 | |||||
| 启动项目后,访问以下测试接口: | |||||
| ```bash | |||||
| # 快速测试 | |||||
| GET /applet/logistics/test/quickTest | |||||
| # 检查是否能正常返回结果 | |||||
| ``` | |||||
| ## 📞 技术支持 | |||||
| 如果遇到其他兼容性问题,可以: | |||||
| 1. 检查本文档的解决方案 | |||||
| 2. 确认 Java 版本配置是否正确 | |||||
| 3. 查看完整的错误日志 | |||||
| 4. 检查项目依赖是否冲突 | |||||
| ## 🎉 总结 | |||||
| 本微信物流模块已经过 Java 8 兼容性测试,所有代码都使用了 Java 8 兼容的语法和 API。如果你的项目使用 Java 8 或更高版本,应该能够正常运行。 | |||||
| **主要兼容性改进:** | |||||
| - ✅ 替换 `Map.of()` 为 `HashMap` 方式 | |||||
| - ✅ 添加必要的 import 语句 | |||||
| - ✅ 确保所有 API 都兼容 Java 8 | |||||
| - ✅ 提供完整的使用示例 | |||||
| @ -0,0 +1,420 @@ | |||||
| # 微信物流功能使用说明 | |||||
| ## 功能概述 | |||||
| 本模块提供了微信小程序物流服务的对接功能,目前支持以下接口: | |||||
| - 获取所有绑定的物流账号 (`getAllAccount`) | |||||
| - 生成运单 (`addOrder`) | |||||
| ## 目录结构 | |||||
| ``` | |||||
| org.jeecg.common.logistics/ | |||||
| ├── config/ | |||||
| │ └── WeChatLogisticsConfig.java # 配置类 | |||||
| ├── controller/ | |||||
| │ └── WeChatLogisticsController.java # 控制器 | |||||
| ├── dto/ | |||||
| │ ├── WeChatLogisticsAccount.java # 物流账号实体类 | |||||
| │ ├── WeChatLogisticsAccountResponse.java # 账号响应实体类 | |||||
| │ ├── WeChatLogisticsServiceType.java # 服务类型实体类 | |||||
| │ ├── WeChatLogisticsAddOrderRequest.java # 下单请求实体类 | |||||
| │ ├── WeChatLogisticsAddOrderResponse.java # 下单响应实体类 | |||||
| │ ├── WeChatLogisticsPersonInfo.java # 发收件人信息实体类 | |||||
| │ ├── WeChatLogisticsCargo.java # 包裹信息实体类 | |||||
| │ ├── WeChatLogisticsCargoDetail.java # 货物详情实体类 | |||||
| │ ├── WeChatLogisticsShop.java # 商品信息实体类 | |||||
| │ ├── WeChatLogisticsShopDetail.java # 商品详情实体类 | |||||
| │ ├── WeChatLogisticsInsured.java # 保价信息实体类 | |||||
| │ ├── WeChatLogisticsServiceInfo.java # 服务信息实体类 | |||||
| │ └── WeChatLogisticsWaybillData.java # 运单数据实体类 | |||||
| ├── service/ | |||||
| │ └── WeChatLogisticsService.java # 服务类 | |||||
| ├── util/ | |||||
| │ └── WeChatLogisticsRequestBuilder.java # 请求构建工具类 | |||||
| └── README.md # 使用说明 | |||||
| ``` | |||||
| ## 配置说明 | |||||
| ### 1. 配置文件 | |||||
| 请在 `application.yml` 中添加以下配置: | |||||
| ```yaml | |||||
| wechat: | |||||
| enabled: true | |||||
| mp-app-id: "your_app_id" | |||||
| mp-app-secret: "your_app_secret" | |||||
| connect-timeout: 30000 | |||||
| read-timeout: 30000 | |||||
| ``` | |||||
| ### 2. 获取微信小程序凭证 | |||||
| 1. 登录微信公众平台:https://mp.weixin.qq.com | |||||
| 2. 申请小程序并获取 `AppID` 和 `AppSecret` | |||||
| 3. 在小程序管理后台开通物流服务功能 | |||||
| ## API 接口 | |||||
| ### 获取所有绑定的物流账号 | |||||
| **接口地址:** `GET /applet/logistics/getAllAccount` | |||||
| **接口描述:** 获取微信小程序绑定的所有物流账号信息 | |||||
| **请求参数:** 无 | |||||
| **响应参数:** | |||||
| ```json | |||||
| { | |||||
| "success": true, | |||||
| "message": "操作成功", | |||||
| "code": 200, | |||||
| "result": { | |||||
| "errcode": 0, | |||||
| "errmsg": "ok", | |||||
| "count": 2, | |||||
| "list": [ | |||||
| { | |||||
| "biz_id": "customer_code_123", | |||||
| "delivery_id": "SF", | |||||
| "create_time": 1640995200, | |||||
| "update_time": 1640995200, | |||||
| "status_code": 1, | |||||
| "alias": "顺丰快递", | |||||
| "remark_wrong_msg": "", | |||||
| "remark_content": "测试账号", | |||||
| "quota_num": 1000, | |||||
| "quota_update_time": 1640995200, | |||||
| "service_type": [ | |||||
| { | |||||
| "service_type": 1, | |||||
| "service_name": "标准快递" | |||||
| } | |||||
| ] | |||||
| } | |||||
| ] | |||||
| } | |||||
| } | |||||
| ``` | |||||
| **字段说明:** | |||||
| | 字段 | 类型 | 说明 | | |||||
| |------|------|------| | |||||
| | biz_id | String | 快递公司客户编码 | | |||||
| | delivery_id | String | 快递公司ID | | |||||
| | create_time | Long | 账号绑定时间(时间戳) | | |||||
| | update_time | Long | 账号更新时间(时间戳) | | |||||
| | status_code | Integer | 绑定状态 | | |||||
| | alias | String | 账号别名 | | |||||
| | remark_wrong_msg | String | 账号绑定失败的错误信息 | | |||||
| | remark_content | String | 账号绑定时的备注内容 | | |||||
| | quota_num | Long | 电子面单余额 | | |||||
| | quota_update_time | Long | 电子面单余额更新时间 | | |||||
| | service_type | Array | 该绑定账号支持的服务类型 | | |||||
| ### 生成运单(下单) | |||||
| **接口地址:** `POST /applet/logistics/addOrder` | |||||
| **接口描述:** 生成微信物流运单 | |||||
| **请求参数:** | |||||
| | 字段 | 类型 | 必填 | 说明 | | |||||
| |------|------|------|------| | |||||
| | order_id | String | 是 | 订单ID,须保证全局唯一 | | |||||
| | openid | String | 是 | 用户openid | | |||||
| | delivery_id | String | 是 | 快递公司ID | | |||||
| | biz_id | String | 是 | 快递客户编码 | | |||||
| | custom_remark | String | 否 | 快递备注信息 | | |||||
| | add_source | Integer | 是 | 订单来源,0为小程序订单,2为App或H5订单 | | |||||
| | sender | Object | 是 | 发件人信息 | | |||||
| | receiver | Object | 是 | 收件人信息 | | |||||
| | cargo | Object | 是 | 包裹信息 | | |||||
| | shop | Object | 是 | 商品信息 | | |||||
| | insured | Object | 是 | 保价信息 | | |||||
| | service | Object | 是 | 服务类型 | | |||||
| **响应参数:** | |||||
| ```json | |||||
| { | |||||
| "success": true, | |||||
| "message": "操作成功", | |||||
| "code": 200, | |||||
| "result": { | |||||
| "errcode": 0, | |||||
| "errmsg": "ok", | |||||
| "order_id": "order_123456", | |||||
| "waybill_id": "SF123456789", | |||||
| "waybill_data": [ | |||||
| { | |||||
| "key": "SF_bagAddr", | |||||
| "value": "广州" | |||||
| } | |||||
| ], | |||||
| "is_correct_sender": 1, | |||||
| "is_correct_receiver": 1 | |||||
| } | |||||
| } | |||||
| ``` | |||||
| **响应字段说明:** | |||||
| - `errcode`: 微信侧错误码,0表示成功 | |||||
| - `errmsg`: 微信侧错误信息 | |||||
| - `order_id`: 订单ID | |||||
| - `waybill_id`: 运单ID(快递单号) | |||||
| - `waybill_data`: 运单信息数组,包含运单模板等信息 | |||||
| - `is_correct_sender`: 发件人信息是否正确,1-正确,0-错误 | |||||
| - `is_correct_receiver`: 收件人信息是否正确,1-正确,0-错误 | |||||
| ### 查询运单轨迹响应字段说明 | |||||
| - `openid`: 用户openid | |||||
| - `waybill_id_status`: 运单状态码,0-未处理,1-已处理 | |||||
| - `path_item_num`: 轨迹节点数量 | |||||
| - `path_item_list`: 轨迹节点列表 | |||||
| - `contact_list`: 联系人列表(快递员信息) | |||||
| **完整请求示例:** | |||||
| ```json | |||||
| { | |||||
| "order_id": "order_123456", | |||||
| "openid": "oUpF8uMuAJOM2pxb1Q9zNjWeS6o", | |||||
| "delivery_id": "SF", | |||||
| "biz_id": "customer_code_123", | |||||
| "custom_remark": "易碎物品", | |||||
| "add_source": 0, | |||||
| "sender": { | |||||
| "name": "张三", | |||||
| "mobile": "13800138000", | |||||
| "company": "发件公司", | |||||
| "province": "广东省", | |||||
| "city": "广州市", | |||||
| "area": "天河区", | |||||
| "address": "天河路123号" | |||||
| }, | |||||
| "receiver": { | |||||
| "name": "李四", | |||||
| "mobile": "13900139000", | |||||
| "company": "收件公司", | |||||
| "province": "上海市", | |||||
| "city": "上海市", | |||||
| "area": "浦东新区", | |||||
| "address": "张江路456号" | |||||
| }, | |||||
| "cargo": { | |||||
| "count": 1, | |||||
| "weight": 1.2, | |||||
| "space_x": 20.0, | |||||
| "space_y": 15.0, | |||||
| "space_z": 10.0, | |||||
| "detail_list": [ | |||||
| { | |||||
| "name": "手机", | |||||
| "count": 1 | |||||
| } | |||||
| ] | |||||
| }, | |||||
| "shop": { | |||||
| "wxa_path": "pages/order/detail?id=123", | |||||
| "img_url": "https://example.com/image.jpg", | |||||
| "goods_name": "苹果手机", | |||||
| "goods_count": 1 | |||||
| }, | |||||
| "insured": { | |||||
| "use_insured": 1, | |||||
| "insured_value": 500000 | |||||
| }, | |||||
| "service": { | |||||
| "service_type": 1, | |||||
| "service_name": "标准快递", | |||||
| "expect_time": 1640995200 | |||||
| } | |||||
| } | |||||
| ``` | |||||
| ## 使用示例 | |||||
| ### 1. 在 Controller 中使用 | |||||
| ```java | |||||
| @RestController | |||||
| @RequestMapping("/api/logistics") | |||||
| public class LogisticsController { | |||||
| @Autowired | |||||
| private WeChatLogisticsService weChatLogisticsService; | |||||
| @GetMapping("/accounts") | |||||
| public Result getLogisticsAccounts() { | |||||
| try { | |||||
| WeChatLogisticsAccountResponse response = weChatLogisticsService.getAllAccount(); | |||||
| return Result.ok(response); | |||||
| } catch (Exception e) { | |||||
| return Result.error("获取物流账号失败:" + e.getMessage()); | |||||
| } | |||||
| } | |||||
| } | |||||
| ``` | |||||
| ### 2. 在 Service 中使用 | |||||
| ```java | |||||
| @Service | |||||
| public class OrderService { | |||||
| @Autowired | |||||
| private WeChatLogisticsService weChatLogisticsService; | |||||
| public void processOrder() { | |||||
| try { | |||||
| // 获取可用的物流账号 | |||||
| WeChatLogisticsAccountResponse response = weChatLogisticsService.getAllAccount(); | |||||
| List<WeChatLogisticsAccount> accounts = response.getList(); | |||||
| // 创建下单请求 | |||||
| WeChatLogisticsAddOrderRequest request = new WeChatLogisticsAddOrderRequest(); | |||||
| request.setOrderId("order_" + System.currentTimeMillis()); | |||||
| request.setOpenid("oUpF8uMuAJOM2pxb1Q9zNjWeS6o"); | |||||
| request.setDeliveryId("SF"); | |||||
| request.setBizId("customer_code_123"); | |||||
| request.setAddSource(0); | |||||
| // 设置发件人信息 | |||||
| WeChatLogisticsPersonInfo sender = new WeChatLogisticsPersonInfo(); | |||||
| sender.setName("张三"); | |||||
| sender.setMobile("13800138000"); | |||||
| sender.setProvince("广东省"); | |||||
| sender.setCity("广州市"); | |||||
| sender.setArea("天河区"); | |||||
| sender.setAddress("天河路123号"); | |||||
| request.setSender(sender); | |||||
| // 设置收件人信息 | |||||
| WeChatLogisticsPersonInfo receiver = new WeChatLogisticsPersonInfo(); | |||||
| receiver.setName("李四"); | |||||
| receiver.setMobile("13900139000"); | |||||
| receiver.setProvince("上海市"); | |||||
| receiver.setCity("上海市"); | |||||
| receiver.setArea("浦东新区"); | |||||
| receiver.setAddress("张江路456号"); | |||||
| request.setReceiver(receiver); | |||||
| // 设置包裹信息 | |||||
| WeChatLogisticsCargo cargo = new WeChatLogisticsCargo(); | |||||
| cargo.setCount(1); | |||||
| cargo.setWeight(1.2); | |||||
| cargo.setSpaceX(20.0); | |||||
| cargo.setSpaceY(15.0); | |||||
| cargo.setSpaceZ(10.0); | |||||
| request.setCargo(cargo); | |||||
| // 设置商品信息 | |||||
| WeChatLogisticsShop shop = new WeChatLogisticsShop(); | |||||
| shop.setGoodsName("苹果手机"); | |||||
| shop.setGoodsCount(1); | |||||
| request.setShop(shop); | |||||
| // 设置保价信息 | |||||
| WeChatLogisticsInsured insured = new WeChatLogisticsInsured(); | |||||
| insured.setUseInsured(1); | |||||
| insured.setInsuredValue(500000); | |||||
| request.setInsured(insured); | |||||
| // 设置服务类型 | |||||
| WeChatLogisticsServiceInfo service = new WeChatLogisticsServiceInfo(); | |||||
| service.setServiceType(1); | |||||
| service.setServiceName("标准快递"); | |||||
| request.setService(service); | |||||
| // 生成运单 | |||||
| WeChatLogisticsAddOrderResponse orderResponse = weChatLogisticsService.addOrder(request); | |||||
| String waybillId = orderResponse.getWaybillId(); | |||||
| // 处理订单逻辑 | |||||
| // ... | |||||
| } catch (Exception e) { | |||||
| log.error("处理订单失败", e); | |||||
| } | |||||
| } | |||||
| } | |||||
| ``` | |||||
| ### 3. 使用工具类构建请求(推荐) | |||||
| ```java | |||||
| @Service | |||||
| public class OrderService { | |||||
| @Autowired | |||||
| private WeChatLogisticsService weChatLogisticsService; | |||||
| public void createLogisticsOrder() { | |||||
| try { | |||||
| // 使用工具类构建请求 | |||||
| WeChatLogisticsAddOrderRequest request = WeChatLogisticsRequestBuilder.createOrderRequest() | |||||
| .orderId("order_" + System.currentTimeMillis()) | |||||
| .openid("oUpF8uMuAJOM2pxb1Q9zNjWeS6o") | |||||
| .deliveryId("SF") | |||||
| .bizId("customer_code_123") | |||||
| .customRemark("易碎物品") | |||||
| .addSource(0) | |||||
| .sender("张三", "13800138000", "广东省", "广州市", "天河区", "天河路123号") | |||||
| .receiver("李四", "13900139000", "上海市", "上海市", "浦东新区", "张江路456号") | |||||
| .cargo(1, 1.2, 20.0, 15.0, 10.0) | |||||
| .cargoDetail("手机", 1) | |||||
| .shop("pages/order/detail?id=123", "https://example.com/image.jpg", "苹果手机", 1) | |||||
| .insured(1, 500000) | |||||
| .service(1, "标准快递") | |||||
| .build(); | |||||
| // 生成运单 | |||||
| WeChatLogisticsAddOrderResponse response = weChatLogisticsService.addOrder(request); | |||||
| log.info("运单生成成功,运单号:{}", response.getWaybillId()); | |||||
| } catch (Exception e) { | |||||
| log.error("生成运单失败", e); | |||||
| } | |||||
| } | |||||
| } | |||||
| ``` | |||||
| ## 注意事项 | |||||
| 1. **权限要求**:需要小程序开通物流服务功能 | |||||
| 2. **频率限制**:微信API有调用频率限制,请避免高频调用 | |||||
| 3. **错误处理**:请妥善处理接口返回的错误信息 | |||||
| 4. **安全性**:生产环境中请将 `app-secret` 配置在环境变量中 | |||||
| 5. **日志记录**:建议开启日志记录以便调试和监控 | |||||
| ## 扩展功能 | |||||
| ### 已实现的接口 | |||||
| - ✅ 获取所有绑定的物流账号 (`getAllAccount`) | |||||
| - ✅ 生成运单 (`addOrder`) | |||||
| ### 后续可扩展的接口 | |||||
| 后续可以根据需求添加更多微信物流接口,如: | |||||
| - 绑定物流账号 | |||||
| - 解绑物流账号 | |||||
| - 查询运单状态 | |||||
| - 取消运单 | |||||
| - 获取支持的快递公司列表 | |||||
| - 获取打印员列表 | |||||
| - 获取电子面单模板 | |||||
| ## 技术支持 | |||||
| 如遇到问题,请查看日志文件或联系技术支持。 | |||||
| @ -0,0 +1,59 @@ | |||||
| package org.jeecg.common.logistics.config; | |||||
| import lombok.Data; | |||||
| import org.springframework.boot.context.properties.ConfigurationProperties; | |||||
| import org.springframework.stereotype.Component; | |||||
| /** | |||||
| * 微信物流配置类 | |||||
| */ | |||||
| @Data | |||||
| @Component | |||||
| @ConfigurationProperties(prefix = "wechat") | |||||
| public class WeChatLogisticsConfig { | |||||
| /** | |||||
| * 微信小程序AppID | |||||
| */ | |||||
| private String mpAppId; | |||||
| /** | |||||
| * 微信小程序AppSecret | |||||
| */ | |||||
| private String mpAppSecret; | |||||
| /** | |||||
| * 是否启用微信物流服务 | |||||
| */ | |||||
| private boolean enabled = true; | |||||
| /** | |||||
| * 连接超时时间(毫秒) | |||||
| */ | |||||
| private int connectTimeout = 30000; | |||||
| /** | |||||
| * 读取超时时间(毫秒) | |||||
| */ | |||||
| private int readTimeout = 30000; | |||||
| /** | |||||
| * 获取Access Token的URL | |||||
| */ | |||||
| private String accessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s"; | |||||
| /** | |||||
| * 获取所有物流账号的URL | |||||
| */ | |||||
| private String getAllAccountUrl = "https://api.weixin.qq.com/cgi-bin/express/business/account/getall?access_token=%s"; | |||||
| /** | |||||
| * 生成运单(下单)的URL | |||||
| */ | |||||
| private String addOrderUrl = "https://api.weixin.qq.com/cgi-bin/express/business/order/add?access_token=%s"; | |||||
| /** | |||||
| * 查询运单轨迹的URL | |||||
| */ | |||||
| private String getPathUrl = "https://api.weixin.qq.com/cgi-bin/express/business/path/get?access_token=%s"; | |||||
| } | |||||
| @ -0,0 +1,74 @@ | |||||
| package org.jeecg.common.logistics.controller; | |||||
| import io.swagger.annotations.Api; | |||||
| import io.swagger.annotations.ApiOperation; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.jeecg.common.api.vo.Result; | |||||
| import org.jeecg.common.logistics.dto.WeChatLogisticsAccountResponse; | |||||
| import org.jeecg.common.logistics.dto.WeChatLogisticsAddOrderRequest; | |||||
| import org.jeecg.common.logistics.dto.WeChatLogisticsAddOrderResponse; | |||||
| import org.jeecg.common.logistics.dto.WeChatLogisticsPathRequest; | |||||
| import org.jeecg.common.logistics.dto.WeChatLogisticsPathResponse; | |||||
| import org.jeecg.common.logistics.service.WeChatLogisticsService; | |||||
| import org.springframework.beans.factory.annotation.Autowired; | |||||
| import org.springframework.web.bind.annotation.*; | |||||
| import javax.validation.Valid; | |||||
| /** | |||||
| * 微信物流控制器 | |||||
| */ | |||||
| @Api(tags = "微信物流管理") | |||||
| @RestController | |||||
| @RequestMapping("/applet/logistics") | |||||
| @Slf4j | |||||
| public class WeChatLogisticsController { | |||||
| @Autowired | |||||
| private WeChatLogisticsService weChatLogisticsService; | |||||
| /** | |||||
| * 获取所有绑定的物流账号 | |||||
| */ | |||||
| @ApiOperation(value = "获取所有绑定的物流账号", notes = "获取微信小程序绑定的所有物流账号") | |||||
| @GetMapping("/getAllAccount") | |||||
| public Result<WeChatLogisticsAccountResponse> getAllAccount() { | |||||
| try { | |||||
| WeChatLogisticsAccountResponse response = weChatLogisticsService.getAllAccount(); | |||||
| return Result.ok(response); | |||||
| } catch (Exception e) { | |||||
| log.error("获取物流账号失败", e); | |||||
| return Result.error("获取物流账号失败:" + e.getMessage()); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 生成运单(下单) | |||||
| */ | |||||
| @ApiOperation(value = "生成运单", notes = "生成微信物流运单") | |||||
| @PostMapping("/addOrder") | |||||
| public Result<WeChatLogisticsAddOrderResponse> addOrder(@Valid @RequestBody WeChatLogisticsAddOrderRequest request) { | |||||
| try { | |||||
| WeChatLogisticsAddOrderResponse response = weChatLogisticsService.addOrder(request); | |||||
| return Result.ok(response); | |||||
| } catch (Exception e) { | |||||
| log.error("生成运单失败", e); | |||||
| return Result.error("生成运单失败:" + e.getMessage()); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 查询运单轨迹(获取快递员手机号) | |||||
| */ | |||||
| @ApiOperation(value = "查询运单轨迹", notes = "查询运单轨迹信息,包含快递员手机号") | |||||
| @PostMapping("/getPath") | |||||
| public Result<WeChatLogisticsPathResponse> getPath(@Valid @RequestBody WeChatLogisticsPathRequest request) { | |||||
| try { | |||||
| WeChatLogisticsPathResponse response = weChatLogisticsService.getPath(request); | |||||
| return Result.ok(response); | |||||
| } catch (Exception e) { | |||||
| log.error("查询运单轨迹失败", e); | |||||
| return Result.error("查询运单轨迹失败:" + e.getMessage()); | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,206 @@ | |||||
| package org.jeecg.common.logistics.controller; | |||||
| import io.swagger.annotations.Api; | |||||
| import io.swagger.annotations.ApiOperation; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.jeecg.common.api.vo.Result; | |||||
| import org.jeecg.common.logistics.dto.WeChatLogisticsAddOrderRequest; | |||||
| import org.jeecg.common.logistics.dto.WeChatLogisticsAddOrderResponse; | |||||
| import org.jeecg.common.logistics.dto.WeChatLogisticsPathRequest; | |||||
| import org.jeecg.common.logistics.dto.WeChatLogisticsPathResponse; | |||||
| import org.jeecg.common.logistics.service.WeChatLogisticsService; | |||||
| import org.jeecg.common.logistics.util.WeChatLogisticsRequestBuilder; | |||||
| import org.springframework.beans.factory.annotation.Autowired; | |||||
| import org.springframework.web.bind.annotation.*; | |||||
| import java.util.HashMap; | |||||
| import java.util.Map; | |||||
| /** | |||||
| * 微信物流测试控制器 | |||||
| */ | |||||
| @Api(tags = "微信物流测试") | |||||
| @RestController | |||||
| @RequestMapping("/applet/logistics/test") | |||||
| @Slf4j | |||||
| public class WeChatLogisticsTestController { | |||||
| @Autowired | |||||
| private WeChatLogisticsService weChatLogisticsService; | |||||
| /** | |||||
| * 测试生成运单 | |||||
| */ | |||||
| @ApiOperation(value = "测试生成运单", notes = "使用模拟数据测试生成微信物流运单") | |||||
| @PostMapping("/addOrder") | |||||
| public Result<WeChatLogisticsAddOrderResponse> testAddOrder( | |||||
| @RequestParam(required = false, defaultValue = "otvnw62EqdpKnCvDQYUjCeNG99XY") String openid, | |||||
| @RequestParam(required = false, defaultValue = "DB") String deliveryId, | |||||
| @RequestParam(required = false, defaultValue = "1102311359") String bizId) { | |||||
| try { | |||||
| // 使用工具类构建测试请求 | |||||
| WeChatLogisticsAddOrderRequest request = WeChatLogisticsRequestBuilder.createOrderRequest() | |||||
| .orderId("TEST_ORDER_" + System.currentTimeMillis()) | |||||
| .openid(openid) | |||||
| .deliveryId(deliveryId) | |||||
| .bizId(bizId) | |||||
| .customRemark("测试订单-易碎物品") | |||||
| .addSource(0) // 小程序订单 | |||||
| // 发件人信息(测试数据) | |||||
| .sender("张三", "13800138000", "广东省", "广州市", "天河区", "天河路123号科技大厦A座15楼") | |||||
| // 收件人信息(测试数据) | |||||
| .receiver("李四", "13900139000", "上海市", "上海市", "浦东新区", "张江高科技园区科苑路399号") | |||||
| // 包裹信息 | |||||
| .cargo(1, 1.5, 25.0, 20.0, 15.0) | |||||
| .cargoDetail("测试商品", 1) | |||||
| // 商品信息 | |||||
| .shop("pages/order/detail?id=test123", "https://example.com/test.jpg", "测试商品", 1) | |||||
| // 保价信息(保价100元) | |||||
| .insured(1, 10000) | |||||
| // 服务类型(需要根据实际快递公司支持的服务类型设置) | |||||
| .serviceWithExpectTime(1, "标准快递", System.currentTimeMillis() / 1000 + 3600) // 1小时后上门取件 | |||||
| .build(); | |||||
| log.info("测试下单请求:{}", request); | |||||
| // 调用下单服务 | |||||
| WeChatLogisticsAddOrderResponse response = weChatLogisticsService.addOrder(request); | |||||
| log.info("测试下单成功,运单号:{}", response.getWaybillId()); | |||||
| // 输出地址验证信息 | |||||
| if (response.getIsCorrectSender() != null) { | |||||
| log.info("发件人信息验证:{}", response.getIsCorrectSender() == 1 ? "正确" : "错误"); | |||||
| } | |||||
| if (response.getIsCorrectReceiver() != null) { | |||||
| log.info("收件人信息验证:{}", response.getIsCorrectReceiver() == 1 ? "正确" : "错误"); | |||||
| } | |||||
| return Result.ok(response); | |||||
| } catch (Exception e) { | |||||
| log.error("测试下单失败", e); | |||||
| return Result.error("测试下单失败:" + e.getMessage()); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 测试查询运单轨迹(获取快递员手机号) | |||||
| */ | |||||
| @ApiOperation(value = "测试查询运单轨迹", notes = "测试查询运单轨迹,获取快递员手机号") | |||||
| @PostMapping("/getPath") | |||||
| public Result<WeChatLogisticsPathResponse> testGetPath( | |||||
| @RequestParam(required = false, defaultValue = "TEST_ORDER_1751711179674") String orderId, | |||||
| @RequestParam(required = false, defaultValue = "DPK364740295014") String waybillId, | |||||
| @RequestParam(required = false, defaultValue = "DB") String deliveryId, | |||||
| @RequestParam(required = false, defaultValue = "otvnw62EqdpKnCvDQYUjCeNG99XY") String openid) { | |||||
| try { | |||||
| WeChatLogisticsPathRequest request = new WeChatLogisticsPathRequest(); | |||||
| request.setOrderId(orderId); | |||||
| request.setWaybillId(waybillId); | |||||
| request.setDeliveryId(deliveryId); | |||||
| request.setOpenid(openid); | |||||
| log.info("查询运单轨迹请求:{}", request); | |||||
| WeChatLogisticsPathResponse response = weChatLogisticsService.getPath(request); | |||||
| // 输出运单状态信息 | |||||
| log.info("===== 运单状态信息 ====="); | |||||
| log.info("运单状态码:{}", response.getWaybillIdStatus()); | |||||
| log.info("轨迹节点数量:{}", response.getPathItemNum()); | |||||
| // 检查是否有轨迹信息 | |||||
| if (response.getPathItemNum() == null || response.getPathItemNum() == 0) { | |||||
| log.info("暂无运单轨迹信息,可能原因:"); | |||||
| log.info("1. 刚下单,快递公司还未处理"); | |||||
| log.info("2. 快递公司系统更新有延迟"); | |||||
| log.info("3. 建议等待几分钟后再查询"); | |||||
| } | |||||
| // 输出快递员信息 | |||||
| if (response.getContactList() != null && !response.getContactList().isEmpty()) { | |||||
| response.getContactList().forEach(contact -> { | |||||
| if (contact.getType() == 1) { | |||||
| log.info("===== 快递员信息 ====="); | |||||
| log.info("快递员姓名:{}", contact.getName()); | |||||
| log.info("快递员手机:{}", contact.getPhone()); | |||||
| log.info("=================="); | |||||
| } | |||||
| }); | |||||
| } else { | |||||
| log.info("暂无快递员信息,可能原因:"); | |||||
| log.info("1. 快递员尚未分配"); | |||||
| log.info("2. 快递公司未提供快递员信息"); | |||||
| log.info("3. 需要等待快递员接单"); | |||||
| } | |||||
| log.info("===================="); | |||||
| return Result.ok(response); | |||||
| } catch (Exception e) { | |||||
| log.error("测试查询运单轨迹失败", e); | |||||
| return Result.error("测试查询运单轨迹失败:" + e.getMessage()); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 完整流程测试:下单 -> 查询轨迹 | |||||
| */ | |||||
| @ApiOperation(value = "完整流程测试", notes = "测试下单后查询轨迹获取快递员信息") | |||||
| @PostMapping("/fullTest") | |||||
| public Result<Object> fullTest( | |||||
| @RequestParam(required = false, defaultValue = "oUpF8uMuAJOM2pxb1Q9zNjWeS6o") String openid, | |||||
| @RequestParam(required = false, defaultValue = "SF") String deliveryId, | |||||
| @RequestParam(required = false, defaultValue = "test_biz_code") String bizId) { | |||||
| try { | |||||
| log.info("===== 开始完整流程测试 ====="); | |||||
| // 步骤1:下单 | |||||
| log.info("步骤1:生成运单"); | |||||
| Result<WeChatLogisticsAddOrderResponse> orderResult = testAddOrder(openid, deliveryId, bizId); | |||||
| if (!orderResult.isSuccess()) { | |||||
| return Result.error("下单失败:" + orderResult.getMessage()); | |||||
| } | |||||
| WeChatLogisticsAddOrderResponse orderResponse = orderResult.getResult(); | |||||
| log.info("下单成功,运单号:{}", orderResponse.getWaybillId()); | |||||
| // 等待一段时间让快递公司处理订单 | |||||
| Thread.sleep(5000); | |||||
| // 步骤2:查询轨迹 | |||||
| log.info("步骤2:查询运单轨迹"); | |||||
| Result<WeChatLogisticsPathResponse> pathResult = testGetPath( | |||||
| orderResponse.getOrderId(), | |||||
| orderResponse.getWaybillId(), | |||||
| deliveryId, | |||||
| openid | |||||
| ); | |||||
| if (!pathResult.isSuccess()) { | |||||
| log.warn("查询轨迹失败,但下单成功。可能需要等待快递公司更新状态"); | |||||
| return Result.ok("下单成功,运单号:" + orderResponse.getWaybillId() + "。轨迹查询失败:" + pathResult.getMessage()); | |||||
| } | |||||
| // 返回完整结果 | |||||
| Map<String, Object> result = new HashMap<>(); | |||||
| result.put("orderResponse", orderResponse); | |||||
| result.put("pathResponse", pathResult.getResult()); | |||||
| result.put("message", "完整流程测试成功"); | |||||
| return Result.ok(result); | |||||
| } catch (Exception e) { | |||||
| log.error("完整流程测试失败", e); | |||||
| return Result.error("完整流程测试失败:" + e.getMessage()); | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,79 @@ | |||||
| package org.jeecg.common.logistics.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Data; | |||||
| import java.util.List; | |||||
| /** | |||||
| * 微信物流账号信息 | |||||
| */ | |||||
| @Data | |||||
| public class WeChatLogisticsAccount { | |||||
| /** | |||||
| * 快递公司客户编码 | |||||
| */ | |||||
| @JsonProperty("biz_id") | |||||
| private String bizId; | |||||
| /** | |||||
| * 快递公司ID | |||||
| */ | |||||
| @JsonProperty("delivery_id") | |||||
| private String deliveryId; | |||||
| /** | |||||
| * 账号绑定时间 | |||||
| */ | |||||
| @JsonProperty("create_time") | |||||
| private Long createTime; | |||||
| /** | |||||
| * 账号更新时间 | |||||
| */ | |||||
| @JsonProperty("update_time") | |||||
| private Long updateTime; | |||||
| /** | |||||
| * 绑定状态 | |||||
| */ | |||||
| @JsonProperty("status_code") | |||||
| private Integer statusCode; | |||||
| /** | |||||
| * 账号别名 | |||||
| */ | |||||
| @JsonProperty("alias") | |||||
| private String alias; | |||||
| /** | |||||
| * 账号绑定失败的错误信息(EMS审核结果) | |||||
| */ | |||||
| @JsonProperty("remark_wrong_msg") | |||||
| private String remarkWrongMsg; | |||||
| /** | |||||
| * 账号绑定时的备注内容(提交EMS审核需要) | |||||
| */ | |||||
| @JsonProperty("remark_content") | |||||
| private String remarkContent; | |||||
| /** | |||||
| * 电子面单余额 | |||||
| */ | |||||
| @JsonProperty("quota_num") | |||||
| private Long quotaNum; | |||||
| /** | |||||
| * 电子面单余额更新时间 | |||||
| */ | |||||
| @JsonProperty("quota_update_time") | |||||
| private Long quotaUpdateTime; | |||||
| /** | |||||
| * 该绑定账号支持的服务类型 | |||||
| */ | |||||
| @JsonProperty("service_type") | |||||
| private List<WeChatLogisticsServiceType> serviceType; | |||||
| } | |||||
| @ -0,0 +1,37 @@ | |||||
| package org.jeecg.common.logistics.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Data; | |||||
| import java.util.List; | |||||
| /** | |||||
| * 微信物流获取所有绑定账号响应 | |||||
| */ | |||||
| @Data | |||||
| public class WeChatLogisticsAccountResponse { | |||||
| /** | |||||
| * 错误码 | |||||
| */ | |||||
| @JsonProperty("errcode") | |||||
| private Integer errCode; | |||||
| /** | |||||
| * 错误信息 | |||||
| */ | |||||
| @JsonProperty("errmsg") | |||||
| private String errMsg; | |||||
| /** | |||||
| * 账号数量 | |||||
| */ | |||||
| @JsonProperty("count") | |||||
| private Integer count; | |||||
| /** | |||||
| * 账号列表 | |||||
| */ | |||||
| @JsonProperty("list") | |||||
| private List<WeChatLogisticsAccount> list; | |||||
| } | |||||
| @ -0,0 +1,95 @@ | |||||
| package org.jeecg.common.logistics.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 微信物流下单请求 | |||||
| */ | |||||
| @Data | |||||
| public class WeChatLogisticsAddOrderRequest { | |||||
| /** | |||||
| * 订单ID,须保证全局唯一,不超过512字节 | |||||
| */ | |||||
| @JsonProperty("order_id") | |||||
| private String orderId; | |||||
| /** | |||||
| * 用户openid,当add_source=2时无需填写(不发送物流服务通知) | |||||
| */ | |||||
| @JsonProperty("openid") | |||||
| private String openid; | |||||
| /** | |||||
| * 快递公司ID,参见getAllDelivery | |||||
| */ | |||||
| @JsonProperty("delivery_id") | |||||
| private String deliveryId; | |||||
| /** | |||||
| * 快递客户编码或者现付编码 | |||||
| */ | |||||
| @JsonProperty("biz_id") | |||||
| private String bizId; | |||||
| /** | |||||
| * 快递备注信息,比如"易碎物品",不超过1024字节 | |||||
| */ | |||||
| @JsonProperty("custom_remark") | |||||
| private String customRemark; | |||||
| /** | |||||
| * 订单标签id,用于平台型小程序区分平台上的入驻方,tagid须与入驻方账号一一对应,非平台型小程序无需填写该字段 | |||||
| */ | |||||
| @JsonProperty("tagid") | |||||
| private Integer tagid; | |||||
| /** | |||||
| * 订单来源,0为小程序订单,2为App或H5订单,填2则不发送物流服务通知 | |||||
| */ | |||||
| @JsonProperty("add_source") | |||||
| private Integer addSource; | |||||
| /** | |||||
| * App或H5的appid,add_source=2时必填,需和开通了物流助手的小程序绑定同一open帐号 | |||||
| */ | |||||
| @JsonProperty("wx_appid") | |||||
| private String wxAppid; | |||||
| /** | |||||
| * 发件人信息 | |||||
| */ | |||||
| @JsonProperty("sender") | |||||
| private WeChatLogisticsPersonInfo sender; | |||||
| /** | |||||
| * 收件人信息 | |||||
| */ | |||||
| @JsonProperty("receiver") | |||||
| private WeChatLogisticsPersonInfo receiver; | |||||
| /** | |||||
| * 包裹信息,将传递给快递公司 | |||||
| */ | |||||
| @JsonProperty("cargo") | |||||
| private WeChatLogisticsCargo cargo; | |||||
| /** | |||||
| * 商品信息,会展示到物流服务通知和电子面单中 | |||||
| */ | |||||
| @JsonProperty("shop") | |||||
| private WeChatLogisticsShop shop; | |||||
| /** | |||||
| * 保价信息 | |||||
| */ | |||||
| @JsonProperty("insured") | |||||
| private WeChatLogisticsInsured insured; | |||||
| /** | |||||
| * 服务类型 | |||||
| */ | |||||
| @JsonProperty("service") | |||||
| private WeChatLogisticsServiceInfo service; | |||||
| } | |||||
| @ -0,0 +1,67 @@ | |||||
| package org.jeecg.common.logistics.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Data; | |||||
| import java.util.List; | |||||
| /** | |||||
| * 微信物流下单响应 | |||||
| */ | |||||
| @Data | |||||
| public class WeChatLogisticsAddOrderResponse { | |||||
| /** | |||||
| * 微信侧错误码,下单失败时返回 | |||||
| */ | |||||
| @JsonProperty("errcode") | |||||
| private Integer errCode; | |||||
| /** | |||||
| * 微信侧错误信息,下单失败时返回 | |||||
| */ | |||||
| @JsonProperty("errmsg") | |||||
| private String errMsg; | |||||
| /** | |||||
| * 订单ID,下单成功时返回 | |||||
| */ | |||||
| @JsonProperty("order_id") | |||||
| private String orderId; | |||||
| /** | |||||
| * 运单ID,下单成功时返回 | |||||
| */ | |||||
| @JsonProperty("waybill_id") | |||||
| private String waybillId; | |||||
| /** | |||||
| * 快递侧错误码,下单失败时返回 | |||||
| */ | |||||
| @JsonProperty("delivery_resultcode") | |||||
| private Integer deliveryResultCode; | |||||
| /** | |||||
| * 快递侧错误信息,下单失败时返回 | |||||
| */ | |||||
| @JsonProperty("delivery_resultmsg") | |||||
| private String deliveryResultMsg; | |||||
| /** | |||||
| * 运单信息,下单成功时返回 | |||||
| */ | |||||
| @JsonProperty("waybill_data") | |||||
| private List<WeChatLogisticsWaybillData> waybillData; | |||||
| /** | |||||
| * 发件人信息是否正确,0-错误,1-正确 | |||||
| */ | |||||
| @JsonProperty("is_correct_sender") | |||||
| private Integer isCorrectSender; | |||||
| /** | |||||
| * 收件人信息是否正确,0-错误,1-正确 | |||||
| */ | |||||
| @JsonProperty("is_correct_receiver") | |||||
| private Integer isCorrectReceiver; | |||||
| } | |||||
| @ -0,0 +1,49 @@ | |||||
| package org.jeecg.common.logistics.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Data; | |||||
| import java.util.List; | |||||
| /** | |||||
| * 微信物流包裹信息 | |||||
| */ | |||||
| @Data | |||||
| public class WeChatLogisticsCargo { | |||||
| /** | |||||
| * 包裹数量, 默认为1 | |||||
| */ | |||||
| @JsonProperty("count") | |||||
| private Integer count; | |||||
| /** | |||||
| * 货物总重量,比如1.2,单位是千克(kg) | |||||
| */ | |||||
| @JsonProperty("weight") | |||||
| private Double weight; | |||||
| /** | |||||
| * 货物长度,比如20.0,单位是厘米(cm) | |||||
| */ | |||||
| @JsonProperty("space_x") | |||||
| private Double spaceX; | |||||
| /** | |||||
| * 货物宽度,比如15.0,单位是厘米(cm) | |||||
| */ | |||||
| @JsonProperty("space_y") | |||||
| private Double spaceY; | |||||
| /** | |||||
| * 货物高度,比如10.0,单位是厘米(cm) | |||||
| */ | |||||
| @JsonProperty("space_z") | |||||
| private Double spaceZ; | |||||
| /** | |||||
| * 货物详情列表 | |||||
| */ | |||||
| @JsonProperty("detail_list") | |||||
| private List<WeChatLogisticsCargoDetail> detailList; | |||||
| } | |||||
| @ -0,0 +1,23 @@ | |||||
| package org.jeecg.common.logistics.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 微信物流货物详情 | |||||
| */ | |||||
| @Data | |||||
| public class WeChatLogisticsCargoDetail { | |||||
| /** | |||||
| * 商品名,不超过128字节 | |||||
| */ | |||||
| @JsonProperty("name") | |||||
| private String name; | |||||
| /** | |||||
| * 商品数量 | |||||
| */ | |||||
| @JsonProperty("count") | |||||
| private Integer count; | |||||
| } | |||||
| @ -0,0 +1,29 @@ | |||||
| package org.jeecg.common.logistics.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 微信物流联系人信息(快递员信息) | |||||
| */ | |||||
| @Data | |||||
| public class WeChatLogisticsContact { | |||||
| /** | |||||
| * 联系人姓名 | |||||
| */ | |||||
| @JsonProperty("name") | |||||
| private String name; | |||||
| /** | |||||
| * 联系人手机号 | |||||
| */ | |||||
| @JsonProperty("phone") | |||||
| private String phone; | |||||
| /** | |||||
| * 联系人类型:1-快递员 | |||||
| */ | |||||
| @JsonProperty("type") | |||||
| private Integer type; | |||||
| } | |||||
| @ -0,0 +1,23 @@ | |||||
| package org.jeecg.common.logistics.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 微信物流保价信息 | |||||
| */ | |||||
| @Data | |||||
| public class WeChatLogisticsInsured { | |||||
| /** | |||||
| * 是否保价,0 表示不保价,1 表示保价 | |||||
| */ | |||||
| @JsonProperty("use_insured") | |||||
| private Integer useInsured; | |||||
| /** | |||||
| * 保价金额,单位是分,比如: 10000 表示 100 元 | |||||
| */ | |||||
| @JsonProperty("insured_value") | |||||
| private Integer insuredValue; | |||||
| } | |||||
| @ -0,0 +1,29 @@ | |||||
| package org.jeecg.common.logistics.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 微信物流运单轨迹 | |||||
| */ | |||||
| @Data | |||||
| public class WeChatLogisticsPath { | |||||
| /** | |||||
| * 轨迹节点 Unix 时间戳 | |||||
| */ | |||||
| @JsonProperty("action_time") | |||||
| private Long actionTime; | |||||
| /** | |||||
| * 轨迹节点类型 | |||||
| */ | |||||
| @JsonProperty("action_type") | |||||
| private Integer actionType; | |||||
| /** | |||||
| * 轨迹节点详情 | |||||
| */ | |||||
| @JsonProperty("action_msg") | |||||
| private String actionMsg; | |||||
| } | |||||
| @ -0,0 +1,35 @@ | |||||
| package org.jeecg.common.logistics.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 微信物流查询运单轨迹请求 | |||||
| */ | |||||
| @Data | |||||
| public class WeChatLogisticsPathRequest { | |||||
| /** | |||||
| * 订单ID,须保证全局唯一 | |||||
| */ | |||||
| @JsonProperty("order_id") | |||||
| private String orderId; | |||||
| /** | |||||
| * 用户openid | |||||
| */ | |||||
| @JsonProperty("openid") | |||||
| private String openid; | |||||
| /** | |||||
| * 快递公司ID | |||||
| */ | |||||
| @JsonProperty("delivery_id") | |||||
| private String deliveryId; | |||||
| /** | |||||
| * 运单ID | |||||
| */ | |||||
| @JsonProperty("waybill_id") | |||||
| private String waybillId; | |||||
| } | |||||
| @ -0,0 +1,79 @@ | |||||
| package org.jeecg.common.logistics.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Data; | |||||
| import java.util.List; | |||||
| /** | |||||
| * 微信物流查询运单轨迹响应 | |||||
| */ | |||||
| @Data | |||||
| public class WeChatLogisticsPathResponse { | |||||
| /** | |||||
| * 微信侧错误码 | |||||
| */ | |||||
| @JsonProperty("errcode") | |||||
| private Integer errCode; | |||||
| /** | |||||
| * 微信侧错误信息 | |||||
| */ | |||||
| @JsonProperty("errmsg") | |||||
| private String errMsg; | |||||
| /** | |||||
| * 订单ID | |||||
| */ | |||||
| @JsonProperty("order_id") | |||||
| private String orderId; | |||||
| /** | |||||
| * 运单ID | |||||
| */ | |||||
| @JsonProperty("waybill_id") | |||||
| private String waybillId; | |||||
| /** | |||||
| * 快递公司ID | |||||
| */ | |||||
| @JsonProperty("delivery_id") | |||||
| private String deliveryId; | |||||
| /** | |||||
| * 运单状态 | |||||
| */ | |||||
| @JsonProperty("order_state") | |||||
| private Integer orderState; | |||||
| /** | |||||
| * 轨迹节点数量 | |||||
| */ | |||||
| @JsonProperty("path_item_num") | |||||
| private Integer pathItemNum; | |||||
| /** | |||||
| * 轨迹节点列表 | |||||
| */ | |||||
| @JsonProperty("path_item_list") | |||||
| private List<WeChatLogisticsPath> pathItemList; | |||||
| /** | |||||
| * 联系人信息(快递员信息) | |||||
| */ | |||||
| @JsonProperty("contact_list") | |||||
| private List<WeChatLogisticsContact> contactList; | |||||
| /** | |||||
| * 用户openid | |||||
| */ | |||||
| @JsonProperty("openid") | |||||
| private String openid; | |||||
| /** | |||||
| * 运单状态码 | |||||
| */ | |||||
| @JsonProperty("waybill_id_status") | |||||
| private Integer waybillIdStatus; | |||||
| } | |||||
| @ -0,0 +1,71 @@ | |||||
| package org.jeecg.common.logistics.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 微信物流发件人/收件人信息 | |||||
| */ | |||||
| @Data | |||||
| public class WeChatLogisticsPersonInfo { | |||||
| /** | |||||
| * 发/收件人姓名,不超过64字节 | |||||
| */ | |||||
| @JsonProperty("name") | |||||
| private String name; | |||||
| /** | |||||
| * 发/收件人座机号码,若不填写则必须填写 mobile,不超过32字节 | |||||
| */ | |||||
| @JsonProperty("tel") | |||||
| private String tel; | |||||
| /** | |||||
| * 发/收件人手机号码,若不填写则必须填写 tel,不超过32字节 | |||||
| */ | |||||
| @JsonProperty("mobile") | |||||
| private String mobile; | |||||
| /** | |||||
| * 发/收件人公司名称,不超过64字节 | |||||
| */ | |||||
| @JsonProperty("company") | |||||
| private String company; | |||||
| /** | |||||
| * 发/收件人邮编,不超过10字节 | |||||
| */ | |||||
| @JsonProperty("post_code") | |||||
| private String postCode; | |||||
| /** | |||||
| * 发/收件人国家,不超过64字节 | |||||
| */ | |||||
| @JsonProperty("country") | |||||
| private String country; | |||||
| /** | |||||
| * 发/收件人省份,比如:"广东省",不超过64字节 | |||||
| */ | |||||
| @JsonProperty("province") | |||||
| private String province; | |||||
| /** | |||||
| * 发/收件人市/地区,比如:"广州市",不超过64字节 | |||||
| */ | |||||
| @JsonProperty("city") | |||||
| private String city; | |||||
| /** | |||||
| * 发/收件人区/县,比如:"海珠区",不超过64字节 | |||||
| */ | |||||
| @JsonProperty("area") | |||||
| private String area; | |||||
| /** | |||||
| * 发/收件人详细地址,比如:"XX路XX号XX大厦XX",不超过512字节 | |||||
| */ | |||||
| @JsonProperty("address") | |||||
| private String address; | |||||
| } | |||||
| @ -0,0 +1,35 @@ | |||||
| package org.jeecg.common.logistics.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 微信物流服务类型信息 | |||||
| */ | |||||
| @Data | |||||
| public class WeChatLogisticsServiceInfo { | |||||
| /** | |||||
| * 服务类型ID,详见已经支持的快递公司基本信息 | |||||
| */ | |||||
| @JsonProperty("service_type") | |||||
| private Integer serviceType; | |||||
| /** | |||||
| * 服务名称,详见已经支持的快递公司基本信息 | |||||
| */ | |||||
| @JsonProperty("service_name") | |||||
| private String serviceName; | |||||
| /** | |||||
| * Unix 时间戳, 单位秒,顺丰必须传。 预期的上门揽件时间,0表示已事先约定取件时间;否则请传预期揽件时间戳,需大于当前时间 | |||||
| */ | |||||
| @JsonProperty("expect_time") | |||||
| private Long expectTime; | |||||
| /** | |||||
| * 分单策略,【0:线下网点签约,1:总部签约结算】,不传默认线下网点签约。目前支持圆通 | |||||
| */ | |||||
| @JsonProperty("take_mode") | |||||
| private Integer takeMode; | |||||
| } | |||||
| @ -0,0 +1,23 @@ | |||||
| package org.jeecg.common.logistics.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 微信物流服务类型 | |||||
| */ | |||||
| @Data | |||||
| public class WeChatLogisticsServiceType { | |||||
| /** | |||||
| * 服务类型ID | |||||
| */ | |||||
| @JsonProperty("service_type") | |||||
| private Integer serviceType; | |||||
| /** | |||||
| * 服务类型名称 | |||||
| */ | |||||
| @JsonProperty("service_name") | |||||
| private String serviceName; | |||||
| } | |||||
| @ -0,0 +1,43 @@ | |||||
| package org.jeecg.common.logistics.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Data; | |||||
| import java.util.List; | |||||
| /** | |||||
| * 微信物流商品信息 | |||||
| */ | |||||
| @Data | |||||
| public class WeChatLogisticsShop { | |||||
| /** | |||||
| * 商家小程序的路径,建议为订单页面 | |||||
| */ | |||||
| @JsonProperty("wxa_path") | |||||
| private String wxaPath; | |||||
| /** | |||||
| * 商品缩略图 url;shop.detail_list为空则必传,shop.detail_list非空可不传 | |||||
| */ | |||||
| @JsonProperty("img_url") | |||||
| private String imgUrl; | |||||
| /** | |||||
| * 商品名称, 不超过128字节;shop.detail_list为空则必传,shop.detail_list非空可不传 | |||||
| */ | |||||
| @JsonProperty("goods_name") | |||||
| private String goodsName; | |||||
| /** | |||||
| * 商品数量;shop.detail_list为空则必传。shop.detail_list非空可不传,默认取shop.detail_list的size | |||||
| */ | |||||
| @JsonProperty("goods_count") | |||||
| private Integer goodsCount; | |||||
| /** | |||||
| * 商品详情列表,适配多商品场景,用以消息落地页展示。(新规范,新接入商家建议用此字段) | |||||
| */ | |||||
| @JsonProperty("detail_list") | |||||
| private List<WeChatLogisticsShopDetail> detailList; | |||||
| } | |||||
| @ -0,0 +1,29 @@ | |||||
| package org.jeecg.common.logistics.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 微信物流商店详情 | |||||
| */ | |||||
| @Data | |||||
| public class WeChatLogisticsShopDetail { | |||||
| /** | |||||
| * 商品名称 | |||||
| */ | |||||
| @JsonProperty("goods_name") | |||||
| private String goodsName; | |||||
| /** | |||||
| * 商品数量 | |||||
| */ | |||||
| @JsonProperty("goods_count") | |||||
| private Integer goodsCount; | |||||
| /** | |||||
| * 商品缩略图 url | |||||
| */ | |||||
| @JsonProperty("img_url") | |||||
| private String imgUrl; | |||||
| } | |||||
| @ -0,0 +1,23 @@ | |||||
| package org.jeecg.common.logistics.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 微信物流运单数据 | |||||
| */ | |||||
| @Data | |||||
| public class WeChatLogisticsWaybillData { | |||||
| /** | |||||
| * 运单信息 key | |||||
| */ | |||||
| @JsonProperty("key") | |||||
| private String key; | |||||
| /** | |||||
| * 运单信息 value | |||||
| */ | |||||
| @JsonProperty("value") | |||||
| private String value; | |||||
| } | |||||
| @ -0,0 +1,316 @@ | |||||
| package org.jeecg.common.logistics.service; | |||||
| import com.fasterxml.jackson.databind.ObjectMapper; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.jeecg.common.logistics.config.WeChatLogisticsConfig; | |||||
| import org.jeecg.common.logistics.dto.WeChatLogisticsAccountResponse; | |||||
| import org.jeecg.common.logistics.dto.WeChatLogisticsAddOrderRequest; | |||||
| import org.jeecg.common.logistics.dto.WeChatLogisticsAddOrderResponse; | |||||
| import org.jeecg.common.logistics.dto.WeChatLogisticsPathRequest; | |||||
| import org.jeecg.common.logistics.dto.WeChatLogisticsPathResponse; | |||||
| import org.springframework.beans.factory.annotation.Autowired; | |||||
| import org.springframework.stereotype.Service; | |||||
| import java.io.BufferedReader; | |||||
| import java.io.IOException; | |||||
| import java.io.InputStreamReader; | |||||
| import java.io.OutputStream; | |||||
| import java.net.HttpURLConnection; | |||||
| import java.net.URL; | |||||
| import java.nio.charset.StandardCharsets; | |||||
| /** | |||||
| * 微信物流服务类 | |||||
| */ | |||||
| @Slf4j | |||||
| @Service | |||||
| public class WeChatLogisticsService { | |||||
| @Autowired | |||||
| private WeChatLogisticsConfig config; | |||||
| private final ObjectMapper objectMapper = new ObjectMapper(); | |||||
| /** | |||||
| * 获取微信Access Token | |||||
| */ | |||||
| private String getAccessToken() throws Exception { | |||||
| String url = String.format(config.getAccessTokenUrl(), config.getMpAppId(), config.getMpAppSecret()); | |||||
| String response = sendGetRequest(url); | |||||
| if (response == null || response.isEmpty()) { | |||||
| throw new RuntimeException("获取Access Token失败:响应为空"); | |||||
| } | |||||
| try { | |||||
| // 解析响应获取access_token | |||||
| com.fasterxml.jackson.databind.JsonNode jsonNode = objectMapper.readTree(response); | |||||
| if (jsonNode.has("errcode") && jsonNode.get("errcode").asInt() != 0) { | |||||
| String errMsg = jsonNode.has("errmsg") ? jsonNode.get("errmsg").asText() : "未知错误"; | |||||
| throw new RuntimeException("获取Access Token失败:" + errMsg); | |||||
| } | |||||
| if (!jsonNode.has("access_token")) { | |||||
| throw new RuntimeException("获取Access Token失败:响应中没有access_token字段"); | |||||
| } | |||||
| return jsonNode.get("access_token").asText(); | |||||
| } catch (IOException e) { | |||||
| log.error("解析Access Token响应失败", e); | |||||
| throw new RuntimeException("解析Access Token响应失败", e); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 获取所有绑定的物流账号 | |||||
| */ | |||||
| public WeChatLogisticsAccountResponse getAllAccount() throws Exception { | |||||
| try { | |||||
| // 获取Access Token | |||||
| String accessToken = getAccessToken(); | |||||
| // 构建请求URL | |||||
| String url = String.format(config.getGetAllAccountUrl(), accessToken); | |||||
| // 发送GET请求 | |||||
| String response = sendGetRequest(url); | |||||
| if (response == null || response.isEmpty()) { | |||||
| throw new RuntimeException("获取物流账号失败:响应为空"); | |||||
| } | |||||
| // 解析响应 | |||||
| WeChatLogisticsAccountResponse result = objectMapper.readValue(response, WeChatLogisticsAccountResponse.class); | |||||
| // 检查错误码 | |||||
| if (result.getErrCode() != null && result.getErrCode() != 0) { | |||||
| String errMsg = result.getErrMsg() != null ? result.getErrMsg() : "未知错误"; | |||||
| throw new RuntimeException("获取物流账号失败:" + errMsg); | |||||
| } | |||||
| log.info("成功获取物流账号,共{}个账号", result.getCount()); | |||||
| return result; | |||||
| } catch (IOException e) { | |||||
| log.error("获取物流账号失败", e); | |||||
| throw new RuntimeException("获取物流账号失败", e); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 生成运单(下单) | |||||
| */ | |||||
| public WeChatLogisticsAddOrderResponse addOrder(WeChatLogisticsAddOrderRequest request) throws Exception { | |||||
| try { | |||||
| // 获取Access Token | |||||
| String accessToken = getAccessToken(); | |||||
| // 构建请求URL | |||||
| String url = String.format(config.getAddOrderUrl(), accessToken); | |||||
| // 将请求对象转换为JSON | |||||
| String requestJson = objectMapper.writeValueAsString(request); | |||||
| // 发送POST请求 | |||||
| String response = sendPostRequest(url, requestJson); | |||||
| if (response == null || response.isEmpty()) { | |||||
| throw new RuntimeException("生成运单失败:响应为空"); | |||||
| } | |||||
| // 解析响应 | |||||
| WeChatLogisticsAddOrderResponse result = objectMapper.readValue(response, WeChatLogisticsAddOrderResponse.class); | |||||
| // 检查错误码 | |||||
| if (result.getErrCode() != null && result.getErrCode() != 0) { | |||||
| String errMsg = result.getErrMsg() != null ? result.getErrMsg() : "未知错误"; | |||||
| throw new RuntimeException("生成运单失败:" + errMsg); | |||||
| } | |||||
| // 检查快递侧错误码 | |||||
| if (result.getDeliveryResultCode() != null && result.getDeliveryResultCode() != 0) { | |||||
| String deliveryErrMsg = result.getDeliveryResultMsg() != null ? result.getDeliveryResultMsg() : "快递侧未知错误"; | |||||
| throw new RuntimeException("生成运单失败(快递侧错误):" + deliveryErrMsg); | |||||
| } | |||||
| log.info("成功生成运单,订单ID:{},运单ID:{}", result.getOrderId(), result.getWaybillId()); | |||||
| return result; | |||||
| } catch (IOException e) { | |||||
| log.error("生成运单失败", e); | |||||
| throw new RuntimeException("生成运单失败", e); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 查询运单轨迹(获取快递员手机号) | |||||
| */ | |||||
| public WeChatLogisticsPathResponse getPath(WeChatLogisticsPathRequest request) throws Exception { | |||||
| try { | |||||
| // 获取Access Token | |||||
| String accessToken = getAccessToken(); | |||||
| // 构建请求URL | |||||
| String url = String.format(config.getGetPathUrl(), accessToken); | |||||
| // 将请求对象转换为JSON | |||||
| String requestJson = objectMapper.writeValueAsString(request); | |||||
| // 发送POST请求 | |||||
| String response = sendPostRequest(url, requestJson); | |||||
| if (response == null || response.isEmpty()) { | |||||
| throw new RuntimeException("查询运单轨迹失败:响应为空"); | |||||
| } | |||||
| // 解析响应 | |||||
| WeChatLogisticsPathResponse result = objectMapper.readValue(response, WeChatLogisticsPathResponse.class); | |||||
| // 检查错误码 | |||||
| if (result.getErrCode() != null && result.getErrCode() != 0) { | |||||
| String errMsg = result.getErrMsg() != null ? result.getErrMsg() : "未知错误"; | |||||
| throw new RuntimeException("查询运单轨迹失败:" + errMsg); | |||||
| } | |||||
| log.info("成功查询运单轨迹,订单ID:{},运单ID:{},状态:{}", | |||||
| result.getOrderId(), result.getWaybillId(), result.getOrderState()); | |||||
| // 输出运单状态信息 | |||||
| if (result.getWaybillIdStatus() != null) { | |||||
| log.info("运单状态码:{}", result.getWaybillIdStatus()); | |||||
| } | |||||
| // 检查是否有轨迹信息 | |||||
| if (result.getPathItemNum() == null || result.getPathItemNum() == 0) { | |||||
| log.info("暂无运单轨迹信息,可能是刚下单还未更新"); | |||||
| } else { | |||||
| log.info("共有{}个轨迹节点", result.getPathItemNum()); | |||||
| } | |||||
| // 输出联系人信息(快递员信息) | |||||
| if (result.getContactList() != null && !result.getContactList().isEmpty()) { | |||||
| result.getContactList().forEach(contact -> { | |||||
| if (contact.getType() == 1) { // type=1表示快递员 | |||||
| log.info("快递员信息 - 姓名:{},手机号:{}", contact.getName(), contact.getPhone()); | |||||
| } | |||||
| }); | |||||
| } | |||||
| return result; | |||||
| } catch (IOException e) { | |||||
| log.error("查询运单轨迹失败", e); | |||||
| throw new RuntimeException("查询运单轨迹失败", e); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 发送POST请求 | |||||
| */ | |||||
| private String sendPostRequest(String urlString, String jsonData) throws Exception { | |||||
| HttpURLConnection connection = null; | |||||
| BufferedReader reader = null; | |||||
| OutputStream outputStream = null; | |||||
| try { | |||||
| URL url = new URL(urlString); | |||||
| connection = (HttpURLConnection) url.openConnection(); | |||||
| connection.setRequestMethod("POST"); | |||||
| connection.setConnectTimeout(config.getConnectTimeout()); | |||||
| connection.setReadTimeout(config.getReadTimeout()); | |||||
| connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); | |||||
| connection.setDoOutput(true); | |||||
| // 写入请求体 | |||||
| outputStream = connection.getOutputStream(); | |||||
| outputStream.write(jsonData.getBytes(StandardCharsets.UTF_8)); | |||||
| outputStream.flush(); | |||||
| int responseCode = connection.getResponseCode(); | |||||
| if (responseCode != HttpURLConnection.HTTP_OK) { | |||||
| throw new RuntimeException("HTTP POST请求失败,状态码:" + responseCode); | |||||
| } | |||||
| reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)); | |||||
| StringBuilder response = new StringBuilder(); | |||||
| String line; | |||||
| while ((line = reader.readLine()) != null) { | |||||
| response.append(line); | |||||
| } | |||||
| return response.toString(); | |||||
| } catch (IOException e) { | |||||
| log.error("发送HTTP POST请求失败", e); | |||||
| throw new RuntimeException("发送HTTP POST请求失败", e); | |||||
| } finally { | |||||
| if (outputStream != null) { | |||||
| try { | |||||
| outputStream.close(); | |||||
| } catch (IOException e) { | |||||
| log.error("关闭输出流失败", e); | |||||
| } | |||||
| } | |||||
| if (reader != null) { | |||||
| try { | |||||
| reader.close(); | |||||
| } catch (IOException e) { | |||||
| log.error("关闭输入流失败", e); | |||||
| } | |||||
| } | |||||
| if (connection != null) { | |||||
| connection.disconnect(); | |||||
| } | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 发送GET请求 | |||||
| */ | |||||
| private String sendGetRequest(String urlString) throws Exception { | |||||
| HttpURLConnection connection = null; | |||||
| BufferedReader reader = null; | |||||
| try { | |||||
| URL url = new URL(urlString); | |||||
| connection = (HttpURLConnection) url.openConnection(); | |||||
| connection.setRequestMethod("GET"); | |||||
| connection.setConnectTimeout(config.getConnectTimeout()); | |||||
| connection.setReadTimeout(config.getReadTimeout()); | |||||
| connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); | |||||
| int responseCode = connection.getResponseCode(); | |||||
| if (responseCode != HttpURLConnection.HTTP_OK) { | |||||
| throw new RuntimeException("HTTP请求失败,状态码:" + responseCode); | |||||
| } | |||||
| reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)); | |||||
| StringBuilder response = new StringBuilder(); | |||||
| String line; | |||||
| while ((line = reader.readLine()) != null) { | |||||
| response.append(line); | |||||
| } | |||||
| return response.toString(); | |||||
| } catch (IOException e) { | |||||
| log.error("发送HTTP请求失败", e); | |||||
| throw new RuntimeException("发送HTTP请求失败", e); | |||||
| } finally { | |||||
| if (reader != null) { | |||||
| try { | |||||
| reader.close(); | |||||
| } catch (IOException e) { | |||||
| log.error("关闭输入流失败", e); | |||||
| } | |||||
| } | |||||
| if (connection != null) { | |||||
| connection.disconnect(); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,170 @@ | |||||
| package org.jeecg.common.logistics.util; | |||||
| import org.jeecg.common.logistics.dto.*; | |||||
| import java.util.ArrayList; | |||||
| import java.util.List; | |||||
| /** | |||||
| * 微信物流请求构建工具类 | |||||
| */ | |||||
| public class WeChatLogisticsRequestBuilder { | |||||
| /** | |||||
| * 创建下单请求构建器 | |||||
| */ | |||||
| public static OrderRequestBuilder createOrderRequest() { | |||||
| return new OrderRequestBuilder(); | |||||
| } | |||||
| /** | |||||
| * 下单请求构建器 | |||||
| */ | |||||
| public static class OrderRequestBuilder { | |||||
| private WeChatLogisticsAddOrderRequest request; | |||||
| public OrderRequestBuilder() { | |||||
| this.request = new WeChatLogisticsAddOrderRequest(); | |||||
| } | |||||
| public OrderRequestBuilder orderId(String orderId) { | |||||
| request.setOrderId(orderId); | |||||
| return this; | |||||
| } | |||||
| public OrderRequestBuilder openid(String openid) { | |||||
| request.setOpenid(openid); | |||||
| return this; | |||||
| } | |||||
| public OrderRequestBuilder deliveryId(String deliveryId) { | |||||
| request.setDeliveryId(deliveryId); | |||||
| return this; | |||||
| } | |||||
| public OrderRequestBuilder bizId(String bizId) { | |||||
| request.setBizId(bizId); | |||||
| return this; | |||||
| } | |||||
| public OrderRequestBuilder customRemark(String customRemark) { | |||||
| request.setCustomRemark(customRemark); | |||||
| return this; | |||||
| } | |||||
| public OrderRequestBuilder addSource(Integer addSource) { | |||||
| request.setAddSource(addSource); | |||||
| return this; | |||||
| } | |||||
| public OrderRequestBuilder wxAppid(String wxAppid) { | |||||
| request.setWxAppid(wxAppid); | |||||
| return this; | |||||
| } | |||||
| public OrderRequestBuilder sender(String name, String mobile, String province, String city, String area, String address) { | |||||
| WeChatLogisticsPersonInfo sender = new WeChatLogisticsPersonInfo(); | |||||
| sender.setName(name); | |||||
| sender.setMobile(mobile); | |||||
| sender.setProvince(province); | |||||
| sender.setCity(city); | |||||
| sender.setArea(area); | |||||
| sender.setAddress(address); | |||||
| request.setSender(sender); | |||||
| return this; | |||||
| } | |||||
| public OrderRequestBuilder receiver(String name, String mobile, String province, String city, String area, String address) { | |||||
| WeChatLogisticsPersonInfo receiver = new WeChatLogisticsPersonInfo(); | |||||
| receiver.setName(name); | |||||
| receiver.setMobile(mobile); | |||||
| receiver.setProvince(province); | |||||
| receiver.setCity(city); | |||||
| receiver.setArea(area); | |||||
| receiver.setAddress(address); | |||||
| request.setReceiver(receiver); | |||||
| return this; | |||||
| } | |||||
| public OrderRequestBuilder cargo(Integer count, Double weight, Double spaceX, Double spaceY, Double spaceZ) { | |||||
| WeChatLogisticsCargo cargo = new WeChatLogisticsCargo(); | |||||
| cargo.setCount(count); | |||||
| cargo.setWeight(weight); | |||||
| cargo.setSpaceX(spaceX); | |||||
| cargo.setSpaceY(spaceY); | |||||
| cargo.setSpaceZ(spaceZ); | |||||
| request.setCargo(cargo); | |||||
| return this; | |||||
| } | |||||
| public OrderRequestBuilder cargoDetail(String goodsName, Integer goodsCount) { | |||||
| if (request.getCargo() == null) { | |||||
| request.setCargo(new WeChatLogisticsCargo()); | |||||
| } | |||||
| if (request.getCargo().getDetailList() == null) { | |||||
| request.getCargo().setDetailList(new ArrayList<>()); | |||||
| } | |||||
| WeChatLogisticsCargoDetail detail = new WeChatLogisticsCargoDetail(); | |||||
| detail.setName(goodsName); | |||||
| detail.setCount(goodsCount); | |||||
| request.getCargo().getDetailList().add(detail); | |||||
| return this; | |||||
| } | |||||
| public OrderRequestBuilder shop(String wxaPath, String imgUrl, String goodsName, Integer goodsCount) { | |||||
| WeChatLogisticsShop shop = new WeChatLogisticsShop(); | |||||
| shop.setWxaPath(wxaPath); | |||||
| shop.setImgUrl(imgUrl); | |||||
| shop.setGoodsName(goodsName); | |||||
| shop.setGoodsCount(goodsCount); | |||||
| request.setShop(shop); | |||||
| return this; | |||||
| } | |||||
| public OrderRequestBuilder shopDetail(String goodsName, Integer goodsCount, String imgUrl) { | |||||
| if (request.getShop() == null) { | |||||
| request.setShop(new WeChatLogisticsShop()); | |||||
| } | |||||
| if (request.getShop().getDetailList() == null) { | |||||
| request.getShop().setDetailList(new ArrayList<>()); | |||||
| } | |||||
| WeChatLogisticsShopDetail detail = new WeChatLogisticsShopDetail(); | |||||
| detail.setGoodsName(goodsName); | |||||
| detail.setGoodsCount(goodsCount); | |||||
| detail.setImgUrl(imgUrl); | |||||
| request.getShop().getDetailList().add(detail); | |||||
| return this; | |||||
| } | |||||
| public OrderRequestBuilder insured(Integer useInsured, Integer insuredValue) { | |||||
| WeChatLogisticsInsured insured = new WeChatLogisticsInsured(); | |||||
| insured.setUseInsured(useInsured); | |||||
| insured.setInsuredValue(insuredValue); | |||||
| request.setInsured(insured); | |||||
| return this; | |||||
| } | |||||
| public OrderRequestBuilder service(Integer serviceType, String serviceName) { | |||||
| WeChatLogisticsServiceInfo service = new WeChatLogisticsServiceInfo(); | |||||
| service.setServiceType(serviceType); | |||||
| service.setServiceName(serviceName); | |||||
| request.setService(service); | |||||
| return this; | |||||
| } | |||||
| public OrderRequestBuilder serviceWithExpectTime(Integer serviceType, String serviceName, Long expectTime) { | |||||
| WeChatLogisticsServiceInfo service = new WeChatLogisticsServiceInfo(); | |||||
| service.setServiceType(serviceType); | |||||
| service.setServiceName(serviceName); | |||||
| service.setExpectTime(expectTime); | |||||
| request.setService(service); | |||||
| return this; | |||||
| } | |||||
| public WeChatLogisticsAddOrderRequest build() { | |||||
| return request; | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,184 @@ | |||||
| # 微信物流功能实现总结 | |||||
| ## 🎯 功能概述 | |||||
| 成功实现了微信物流服务的对接功能,包括: | |||||
| - ✅ 获取所有绑定的物流账号 (`getAllAccount`) | |||||
| - ✅ 生成运单 (`addOrder`) | |||||
| ## 📁 项目结构 | |||||
| ``` | |||||
| org.jeecg.common.logistics/ | |||||
| ├── config/ | |||||
| │ └── WeChatLogisticsConfig.java # 配置类 | |||||
| ├── controller/ | |||||
| │ └── WeChatLogisticsController.java # 控制器 | |||||
| ├── dto/ | |||||
| │ ├── WeChatLogisticsAccount.java # 物流账号实体类 | |||||
| │ ├── WeChatLogisticsAccountResponse.java # 账号响应实体类 | |||||
| │ ├── WeChatLogisticsServiceType.java # 服务类型实体类 | |||||
| │ ├── WeChatLogisticsAddOrderRequest.java # 下单请求实体类 | |||||
| │ ├── WeChatLogisticsAddOrderResponse.java # 下单响应实体类 | |||||
| │ ├── WeChatLogisticsPersonInfo.java # 发收件人信息实体类 | |||||
| │ ├── WeChatLogisticsCargo.java # 包裹信息实体类 | |||||
| │ ├── WeChatLogisticsCargoDetail.java # 货物详情实体类 | |||||
| │ ├── WeChatLogisticsShop.java # 商品信息实体类 | |||||
| │ ├── WeChatLogisticsShopDetail.java # 商品详情实体类 | |||||
| │ ├── WeChatLogisticsInsured.java # 保价信息实体类 | |||||
| │ ├── WeChatLogisticsServiceInfo.java # 服务信息实体类 | |||||
| │ └── WeChatLogisticsWaybillData.java # 运单数据实体类 | |||||
| ├── service/ | |||||
| │ └── WeChatLogisticsService.java # 服务类 | |||||
| ├── util/ | |||||
| │ └── WeChatLogisticsRequestBuilder.java # 请求构建工具类 | |||||
| ├── README.md # 使用说明 | |||||
| └── 功能实现总结.md # 实现总结 | |||||
| ``` | |||||
| ## 🔧 核心特性 | |||||
| ### 1. 分类实体类设计 | |||||
| - **请求相关**:`WeChatLogisticsAddOrderRequest`、`WeChatLogisticsPersonInfo`、`WeChatLogisticsCargo` 等 | |||||
| - **响应相关**:`WeChatLogisticsAddOrderResponse`、`WeChatLogisticsAccountResponse` 等 | |||||
| - **基础数据**:`WeChatLogisticsServiceType`、`WeChatLogisticsWaybillData` 等 | |||||
| ### 2. 完整的API接口 | |||||
| - **获取账号**:`GET /applet/logistics/getAllAccount` | |||||
| - **生成运单**:`POST /applet/logistics/addOrder` | |||||
| ### 3. 配置化管理 | |||||
| - 支持外部配置微信小程序参数 | |||||
| - 可配置超时时间、API地址等 | |||||
| ### 4. 工具类支持 | |||||
| - 提供 `WeChatLogisticsRequestBuilder` 工具类 | |||||
| - 支持链式调用构建请求对象 | |||||
| ## 📝 使用示例 | |||||
| ### 简单使用 | |||||
| ```java | |||||
| @Autowired | |||||
| private WeChatLogisticsService weChatLogisticsService; | |||||
| // 获取物流账号 | |||||
| WeChatLogisticsAccountResponse accounts = weChatLogisticsService.getAllAccount(); | |||||
| // 生成运单 | |||||
| WeChatLogisticsAddOrderRequest request = new WeChatLogisticsAddOrderRequest(); | |||||
| // ... 设置请求参数 | |||||
| WeChatLogisticsAddOrderResponse response = weChatLogisticsService.addOrder(request); | |||||
| ``` | |||||
| ### 使用工具类(推荐) | |||||
| ```java | |||||
| WeChatLogisticsAddOrderRequest request = WeChatLogisticsRequestBuilder.createOrderRequest() | |||||
| .orderId("order_123") | |||||
| .openid("user_openid") | |||||
| .deliveryId("SF") | |||||
| .bizId("biz_code") | |||||
| .sender("张三", "13800138000", "广东省", "广州市", "天河区", "天河路123号") | |||||
| .receiver("李四", "13900139000", "上海市", "上海市", "浦东新区", "张江路456号") | |||||
| .cargo(1, 1.2, 20.0, 15.0, 10.0) | |||||
| .shop("pages/order/detail", "image.jpg", "商品名", 1) | |||||
| .insured(1, 500000) | |||||
| .service(1, "标准快递") | |||||
| .build(); | |||||
| WeChatLogisticsAddOrderResponse response = weChatLogisticsService.addOrder(request); | |||||
| ``` | |||||
| ## 🛠️ 技术实现 | |||||
| ### 1. HTTP客户端 | |||||
| - 使用 `HttpURLConnection` 发送请求 | |||||
| - 支持GET和POST方法 | |||||
| - 完善的错误处理和资源管理 | |||||
| ### 2. JSON序列化 | |||||
| - 使用 `Jackson` 进行JSON序列化/反序列化 | |||||
| - 使用 `@JsonProperty` 注解映射字段 | |||||
| ### 3. 异常处理 | |||||
| - 统一的异常处理机制 | |||||
| - 详细的错误信息记录 | |||||
| - 微信侧和快递侧错误码检查 | |||||
| ### 4. 日志记录 | |||||
| - 使用 `Slf4j` 记录关键操作 | |||||
| - 记录请求成功和失败信息 | |||||
| ## 🎨 设计亮点 | |||||
| ### 1. 模块化设计 | |||||
| - 按功能分包:dto、service、config、util | |||||
| - 每个实体类职责单一,便于维护 | |||||
| ### 2. 可扩展性 | |||||
| - 配置类支持添加新的API接口 | |||||
| - 工具类支持添加新的构建方法 | |||||
| ### 3. 易用性 | |||||
| - 提供多种使用方式 | |||||
| - 详细的文档和示例 | |||||
| - 工具类简化使用复杂度 | |||||
| ## 🔒 安全性 | |||||
| ### 1. 配置管理 | |||||
| - 支持外部配置文件 | |||||
| - 敏感信息可通过环境变量配置 | |||||
| ### 2. 请求验证 | |||||
| - 使用 `@Valid` 注解验证请求参数 | |||||
| - 完善的错误处理机制 | |||||
| ## 📊 性能优化 | |||||
| ### 1. 连接管理 | |||||
| - 可配置连接和读取超时时间 | |||||
| - 及时关闭连接和流 | |||||
| ### 2. 资源管理 | |||||
| - 使用 try-with-resources 或 finally 确保资源释放 | |||||
| - 避免内存泄漏 | |||||
| ## 🚀 后续扩展 | |||||
| ### 可添加的功能 | |||||
| 1. 查询运单状态 | |||||
| 2. 取消运单 | |||||
| 3. 获取快递公司列表 | |||||
| 4. 绑定/解绑物流账号 | |||||
| 5. 获取电子面单模板 | |||||
| 6. 缓存Access Token | |||||
| 7. 批量操作支持 | |||||
| ### 性能优化 | |||||
| 1. 引入连接池 | |||||
| 2. 异步处理 | |||||
| 3. 请求重试机制 | |||||
| 4. 限流控制 | |||||
| ## 📋 测试建议 | |||||
| 虽然按要求未编写测试方法,但建议在实际使用时进行以下测试: | |||||
| 1. **单元测试**:测试各个方法的正确性 | |||||
| 2. **集成测试**:测试完整的业务流程 | |||||
| 3. **压力测试**:测试高并发场景 | |||||
| 4. **容错测试**:测试异常情况处理 | |||||
| ## 🎯 总结 | |||||
| 本次实现完成了微信物流服务的核心功能,具有以下特点: | |||||
| ✅ **完整性**:实现了完整的获取账号和下单流程 | |||||
| ✅ **规范性**:代码结构清晰,遵循Java开发规范 | |||||
| ✅ **易用性**:提供多种使用方式,文档详细 | |||||
| ✅ **扩展性**:设计灵活,便于后续功能扩展 | |||||
| ✅ **安全性**:考虑了配置安全和异常处理 | |||||
| ✅ **性能**:合理的资源管理和超时控制 | |||||
| 代码已准备就绪,可以直接集成到项目中使用。 | |||||
| @ -0,0 +1,322 @@ | |||||
| # 微信物流模块常见问题排查指南 | |||||
| ## 🔍 JSON反序列化错误 | |||||
| ### 问题1:UnrecognizedPropertyException | |||||
| **错误信息:** | |||||
| ``` | |||||
| com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "xxx" (class xxx), not marked as ignorable | |||||
| ``` | |||||
| **原因分析:** | |||||
| - 微信API返回的JSON包含了实体类中未定义的字段 | |||||
| - Jackson默认不允许未知字段 | |||||
| **解决方案:** | |||||
| #### 方案1:添加缺失字段(推荐) | |||||
| 在实体类中添加对应字段: | |||||
| ```java | |||||
| @Data | |||||
| public class WeChatLogisticsAddOrderResponse { | |||||
| // ... 其他字段 | |||||
| /** | |||||
| * 发件人信息是否正确,0-错误,1-正确 | |||||
| */ | |||||
| @JsonProperty("is_correct_sender") | |||||
| private Integer isCorrectSender; | |||||
| /** | |||||
| * 收件人信息是否正确,0-错误,1-正确 | |||||
| */ | |||||
| @JsonProperty("is_correct_receiver") | |||||
| private Integer isCorrectReceiver; | |||||
| } | |||||
| ``` | |||||
| #### 方案2:忽略未知字段 | |||||
| 在实体类上添加注解: | |||||
| ```java | |||||
| @Data | |||||
| @JsonIgnoreProperties(ignoreUnknown = true) | |||||
| public class WeChatLogisticsAddOrderResponse { | |||||
| // ... 字段定义 | |||||
| } | |||||
| ``` | |||||
| #### 方案3:全局配置 | |||||
| 在配置类中设置: | |||||
| ```java | |||||
| @Configuration | |||||
| public class JacksonConfig { | |||||
| @Bean | |||||
| @Primary | |||||
| public ObjectMapper objectMapper() { | |||||
| ObjectMapper mapper = new ObjectMapper(); | |||||
| mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); | |||||
| return mapper; | |||||
| } | |||||
| } | |||||
| ``` | |||||
| ### 问题2:字段映射错误 | |||||
| **错误信息:** | |||||
| ``` | |||||
| Unrecognized field "service_type" (class xxx.WeChatLogisticsServiceType) | |||||
| ``` | |||||
| **原因分析:** | |||||
| - 微信API返回的字段名与实体类字段名不匹配 | |||||
| - @JsonProperty注解配置错误 | |||||
| **解决方案:** | |||||
| 检查并修正字段映射: | |||||
| ```java | |||||
| // 错误示例 | |||||
| @JsonProperty("service_type_id") // 微信API实际返回的是 "service_type" | |||||
| private String serviceType; | |||||
| // 正确示例 | |||||
| @JsonProperty("service_type") // 与微信API返回字段名一致 | |||||
| private String serviceType; | |||||
| ``` | |||||
| ### 问题3:轨迹查询反序列化错误 | |||||
| **错误信息:** | |||||
| ``` | |||||
| Unrecognized field "openid" (class WeChatLogisticsPathResponse) | |||||
| Unrecognized field "waybill_id_status" (class WeChatLogisticsPathResponse) | |||||
| ``` | |||||
| **原因分析:** | |||||
| - 微信轨迹查询API返回了额外的字段 | |||||
| - 响应实体类中缺少对应的字段定义 | |||||
| **解决方案:** | |||||
| 在 `WeChatLogisticsPathResponse` 类中添加缺失字段: | |||||
| ```java | |||||
| @Data | |||||
| public class WeChatLogisticsPathResponse { | |||||
| // ... 其他字段 | |||||
| /** | |||||
| * 用户openid | |||||
| */ | |||||
| @JsonProperty("openid") | |||||
| private String openid; | |||||
| /** | |||||
| * 运单状态码 | |||||
| */ | |||||
| @JsonProperty("waybill_id_status") | |||||
| private Integer waybillIdStatus; | |||||
| } | |||||
| ``` | |||||
| ## 🔧 微信API调用问题 | |||||
| ### 问题1:Access Token获取失败 | |||||
| **错误信息:** | |||||
| ``` | |||||
| 获取Access Token失败:invalid appid | |||||
| ``` | |||||
| **排查步骤:** | |||||
| 1. **检查配置参数** | |||||
| ```yaml | |||||
| wechat: | |||||
| mp-app-id: "your_app_id" # 检查是否正确 | |||||
| mp-app-secret: "your_app_secret" # 检查是否正确 | |||||
| ``` | |||||
| 2. **验证APP ID和Secret** | |||||
| - 登录微信公众平台 | |||||
| - 确认APP ID和Secret是否正确 | |||||
| - 确认是否开通了微信物流服务 | |||||
| 3. **检查网络连接** | |||||
| ```bash | |||||
| # 测试网络连接 | |||||
| curl -I https://api.weixin.qq.com | |||||
| ``` | |||||
| ### 问题2:下单失败 | |||||
| **错误信息:** | |||||
| ``` | |||||
| delivery_resultcode: 非0值 | |||||
| delivery_resultmsg: 具体错误信息 | |||||
| ``` | |||||
| **排查步骤:** | |||||
| 1. **检查快递公司参数** | |||||
| - 确认 `delivery_id` 是否正确 | |||||
| - 确认 `biz_id` 是否与快递公司签约 | |||||
| 2. **验证地址信息** | |||||
| - 发件人地址是否完整准确 | |||||
| - 收件人地址是否在快递公司配送范围内 | |||||
| 3. **检查服务类型** | |||||
| - 确认快递公司是否支持指定的服务类型 | |||||
| - 顺丰快递必须传 `expect_time` 参数 | |||||
| ### 问题4:查询轨迹失败 | |||||
| **常见原因:** | |||||
| - 运单号不存在或未生效 | |||||
| - 查询时间过早(下单后立即查询) | |||||
| - 快递公司尚未更新物流信息 | |||||
| - `waybill_id_status = 0` 表示运单未处理 | |||||
| **解决方案:** | |||||
| - 等待几分钟后再查询 | |||||
| - 确认运单号是否正确 | |||||
| - 检查快递公司官网状态 | |||||
| - 观察 `waybill_id_status` 状态码变化 | |||||
| ## 🐛 调试技巧 | |||||
| ### 1. 开启详细日志 | |||||
| ```yaml | |||||
| logging: | |||||
| level: | |||||
| org.jeecg.common.logistics: DEBUG | |||||
| ``` | |||||
| ### 2. 打印请求和响应 | |||||
| ```java | |||||
| // 在 WeChatLogisticsService 中添加 | |||||
| log.info("请求URL: {}", url); | |||||
| log.info("请求参数: {}", requestJson); | |||||
| log.info("响应结果: {}", response); | |||||
| ``` | |||||
| ### 3. 使用测试接口 | |||||
| ```bash | |||||
| # 快速测试 | |||||
| GET /applet/logistics/test/quickTest | |||||
| # 完整流程测试 | |||||
| POST /applet/logistics/test/fullTest | |||||
| ``` | |||||
| ### 4. 检查微信开发者工具 | |||||
| - 查看微信开发者工具的网络请求 | |||||
| - 验证小程序端的参数传递 | |||||
| - 检查用户的openid是否正确 | |||||
| ## 📞 获取技术支持 | |||||
| ### 1. 查看日志文件 | |||||
| 重要日志信息: | |||||
| - 请求参数是否正确 | |||||
| - 响应状态码和错误信息 | |||||
| - 网络连接是否正常 | |||||
| ### 2. 收集问题信息 | |||||
| 提供以下信息: | |||||
| - 完整的错误日志 | |||||
| - 请求参数 | |||||
| - 微信返回的完整响应 | |||||
| - 使用的快递公司和服务类型 | |||||
| ### 3. 测试环境验证 | |||||
| - 在测试环境中重现问题 | |||||
| - 使用提供的测试接口进行验证 | |||||
| - 确认配置参数是否正确 | |||||
| ## 🎯 最佳实践 | |||||
| ### 1. 错误处理 | |||||
| ```java | |||||
| try { | |||||
| WeChatLogisticsAddOrderResponse response = weChatLogisticsService.addOrder(request); | |||||
| // 检查微信侧错误 | |||||
| if (response.getErrCode() != null && response.getErrCode() != 0) { | |||||
| log.error("微信侧错误:{}", response.getErrMsg()); | |||||
| return Result.error("下单失败:" + response.getErrMsg()); | |||||
| } | |||||
| // 检查快递公司侧错误 | |||||
| if (response.getDeliveryResultCode() != null && response.getDeliveryResultCode() != 0) { | |||||
| log.error("快递公司错误:{}", response.getDeliveryResultMsg()); | |||||
| return Result.error("下单失败:" + response.getDeliveryResultMsg()); | |||||
| } | |||||
| // 检查地址验证 | |||||
| if (response.getIsCorrectSender() != null && response.getIsCorrectSender() == 0) { | |||||
| log.warn("发件人地址可能不正确"); | |||||
| } | |||||
| if (response.getIsCorrectReceiver() != null && response.getIsCorrectReceiver() == 0) { | |||||
| log.warn("收件人地址可能不正确"); | |||||
| } | |||||
| // 检查运单状态(针对轨迹查询) | |||||
| if (response instanceof WeChatLogisticsPathResponse) { | |||||
| WeChatLogisticsPathResponse pathResponse = (WeChatLogisticsPathResponse) response; | |||||
| if (pathResponse.getWaybillIdStatus() == 0) { | |||||
| log.info("运单未处理,建议稍后再查询"); | |||||
| } | |||||
| if (pathResponse.getPathItemNum() == 0) { | |||||
| log.info("暂无轨迹信息"); | |||||
| } | |||||
| } | |||||
| return Result.ok(response); | |||||
| } catch (Exception e) { | |||||
| log.error("下单异常", e); | |||||
| return Result.error("下单失败:" + e.getMessage()); | |||||
| } | |||||
| ``` | |||||
| ### 2. 参数验证 | |||||
| ```java | |||||
| // 验证必填参数 | |||||
| if (StringUtils.isEmpty(request.getOrderId())) { | |||||
| return Result.error("订单ID不能为空"); | |||||
| } | |||||
| if (StringUtils.isEmpty(request.getOpenid())) { | |||||
| return Result.error("用户openid不能为空"); | |||||
| } | |||||
| if (request.getSender() == null || StringUtils.isEmpty(request.getSender().getName())) { | |||||
| return Result.error("发件人信息不完整"); | |||||
| } | |||||
| ``` | |||||
| ### 3. 重试机制 | |||||
| ```java | |||||
| @Retryable(value = {Exception.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000)) | |||||
| public WeChatLogisticsAddOrderResponse addOrder(WeChatLogisticsAddOrderRequest request) throws Exception { | |||||
| // ... 下单逻辑 | |||||
| } | |||||
| ``` | |||||
| 这个排查指南应该能帮助你快速定位和解决微信物流模块的常见问题。 | |||||
| @ -0,0 +1,196 @@ | |||||
| # 微信物流测试结果示例 | |||||
| ## 📋 测试环境 | |||||
| - **测试时间:** 2025-01-05 18:34:02 | |||||
| - **快递公司:** 德邦快递 (DB) | |||||
| - **运单号:** DPK364740295014 | |||||
| - **订单号:** TEST_ORDER_1751711179674 | |||||
| ## 🎯 测试结果 | |||||
| ### 1. 下单测试结果 | |||||
| **成功输出示例:** | |||||
| ``` | |||||
| 测试下单成功,运单号:DPK364740295014 | |||||
| 发件人信息验证:正确 | |||||
| 收件人信息验证:正确 | |||||
| ``` | |||||
| **响应JSON:** | |||||
| ```json | |||||
| { | |||||
| "success": true, | |||||
| "result": { | |||||
| "errcode": 0, | |||||
| "errmsg": "ok", | |||||
| "order_id": "TEST_ORDER_1751711179674", | |||||
| "waybill_id": "DPK364740295014", | |||||
| "delivery_resultcode": 0, | |||||
| "delivery_resultmsg": "成功", | |||||
| "waybill_data": [ | |||||
| { | |||||
| "key": "DB_PaintMarker", | |||||
| "value": "上海市" | |||||
| } | |||||
| ], | |||||
| "is_correct_sender": 1, | |||||
| "is_correct_receiver": 1 | |||||
| } | |||||
| } | |||||
| ``` | |||||
| ### 2. 轨迹查询测试结果 | |||||
| **成功输出示例:** | |||||
| ``` | |||||
| ===== 运单状态信息 ===== | |||||
| 运单状态码:0 | |||||
| 轨迹节点数量:0 | |||||
| 暂无运单轨迹信息,可能原因: | |||||
| 1. 刚下单,快递公司还未处理 | |||||
| 2. 快递公司系统更新有延迟 | |||||
| 3. 建议等待几分钟后再查询 | |||||
| 暂无快递员信息,可能原因: | |||||
| 1. 快递员尚未分配 | |||||
| 2. 快递公司未提供快递员信息 | |||||
| 3. 需要等待快递员接单 | |||||
| ==================== | |||||
| ``` | |||||
| **响应JSON:** | |||||
| ```json | |||||
| { | |||||
| "success": true, | |||||
| "result": { | |||||
| "openid": "otvnw62EqdpKnCvDQYUjCeNG99XY", | |||||
| "delivery_id": "DB", | |||||
| "waybill_id": "DPK364740295014", | |||||
| "path_item_num": 0, | |||||
| "path_item_list": [], | |||||
| "waybill_id_status": 0, | |||||
| "contact_list": null | |||||
| } | |||||
| } | |||||
| ``` | |||||
| ## 🔧 修复前后对比 | |||||
| ### 修复前错误 | |||||
| ``` | |||||
| ERROR: UnrecognizedPropertyException: Unrecognized field "openid" | |||||
| ERROR: UnrecognizedPropertyException: Unrecognized field "waybill_id_status" | |||||
| ``` | |||||
| ### 修复后成功 | |||||
| ``` | |||||
| INFO: 成功查询运单轨迹,订单ID:TEST_ORDER_1751711179674 | |||||
| INFO: 运单状态码:0 | |||||
| INFO: 暂无运单轨迹信息,可能是刚下单还未更新 | |||||
| ``` | |||||
| ## 📊 字段解析 | |||||
| ### 下单响应字段 | |||||
| | 字段名 | 值 | 含义 | | |||||
| |--------|----|----| | |||||
| | `errcode` | 0 | 微信API成功 | | |||||
| | `order_id` | TEST_ORDER_1751711179674 | 订单ID | | |||||
| | `waybill_id` | DPK364740295014 | 运单号 | | |||||
| | `delivery_resultcode` | 0 | 快递公司成功 | | |||||
| | `is_correct_sender` | 1 | 发件人地址正确 | | |||||
| | `is_correct_receiver` | 1 | 收件人地址正确 | | |||||
| ### 轨迹查询响应字段 | |||||
| | 字段名 | 值 | 含义 | | |||||
| |--------|----|----| | |||||
| | `openid` | otvnw62EqdpKnCvDQYUjCeNG99XY | 用户标识 | | |||||
| | `waybill_id_status` | 0 | 运单未处理 | | |||||
| | `path_item_num` | 0 | 暂无轨迹节点 | | |||||
| | `contact_list` | null | 暂无快递员信息 | | |||||
| ## 💡 结果分析 | |||||
| ### 1. 下单成功 | |||||
| - ✅ 德邦快递接受了订单 | |||||
| - ✅ 发件人和收件人地址验证通过 | |||||
| - ✅ 获得了有效的运单号 | |||||
| ### 2. 轨迹查询正常 | |||||
| - ✅ API调用成功,无反序列化错误 | |||||
| - ⏳ 运单状态为0,表示刚下单,快递公司还未处理 | |||||
| - ⏳ 暂无轨迹信息,需要等待快递公司更新 | |||||
| ### 3. 快递员信息获取 | |||||
| - ⏳ 由于运单刚创建,快递员尚未分配 | |||||
| - ⏳ 需要等待快递员接单后再查询 | |||||
| - ⏳ 通常在1-2小时后会有快递员信息 | |||||
| ## 🕐 时间线预期 | |||||
| ### 立即(0-5分钟) | |||||
| - 下单成功,获得运单号 | |||||
| - 运单状态为0(未处理) | |||||
| - 无轨迹信息,无快递员信息 | |||||
| ### 短期(5-30分钟) | |||||
| - 运单状态可能变为1(已处理) | |||||
| - 开始出现轨迹信息 | |||||
| - 快递公司系统开始处理 | |||||
| ### 中期(30分钟-2小时) | |||||
| - 快递员接单 | |||||
| - 出现快递员联系信息 | |||||
| - 可以获取快递员手机号 | |||||
| ### 长期(2小时以上) | |||||
| - 快递员上门取件 | |||||
| - 运单状态更新为已揽件 | |||||
| - 开始物流轨迹跟踪 | |||||
| ## 🎯 测试建议 | |||||
| ### 1. 立即测试 | |||||
| - 测试下单功能 | |||||
| - 验证基本的轨迹查询功能 | |||||
| ### 2. 延迟测试 | |||||
| - 1小时后再次查询轨迹 | |||||
| - 检查是否有快递员信息 | |||||
| - 验证轨迹信息是否更新 | |||||
| ### 3. 持续监控 | |||||
| - 设置定时任务查询轨迹 | |||||
| - 监控快递员信息变化 | |||||
| - 记录完整的物流状态变化 | |||||
| ## 🚀 下一步操作 | |||||
| 1. **等待快递员接单** | |||||
| - 预计1-2小时内会有快递员信息 | |||||
| - 可以定期查询轨迹接口 | |||||
| 2. **验证快递员信息** | |||||
| - 确认快递员姓名和手机号 | |||||
| - 测试联系快递员功能 | |||||
| 3. **完整流程测试** | |||||
| - 从下单到签收的全流程测试 | |||||
| - 验证各个状态的快递员信息变化 | |||||
| ## 📞 总结 | |||||
| 本次测试验证了: | |||||
| - ✅ Jackson反序列化错误已完全修复 | |||||
| - ✅ 德邦快递下单功能正常 | |||||
| - ✅ 轨迹查询功能正常 | |||||
| - ✅ 地址验证功能正常 | |||||
| - ✅ 错误处理和日志记录完善 | |||||
| **下一步:** 等待快递员接单,测试快递员信息获取功能。 | |||||
| @ -0,0 +1,277 @@ | |||||
| # 获取快递员手机号说明 | |||||
| ## 📱 功能概述 | |||||
| 通过微信物流API可以获取快递员的手机号,主要用于: | |||||
| - 快递上门取件时联系快递员 | |||||
| - 获取快递员的联系方式 | |||||
| - 查询运单状态和轨迹信息 | |||||
| ## 🔧 技术实现 | |||||
| ### 1. 接口说明 | |||||
| 快递员手机号通过 **查询运单轨迹接口** 获取: | |||||
| **接口地址:** `POST /applet/logistics/getPath` | |||||
| **微信API:** `POST https://api.weixin.qq.com/cgi-bin/express/business/path/get?access_token=ACCESS_TOKEN` | |||||
| ### 2. 请求参数 | |||||
| ```json | |||||
| { | |||||
| "order_id": "订单ID", | |||||
| "openid": "用户openid", | |||||
| "delivery_id": "快递公司ID", | |||||
| "waybill_id": "运单号" | |||||
| } | |||||
| ``` | |||||
| ### 3. 响应数据 | |||||
| ```json | |||||
| { | |||||
| "success": true, | |||||
| "result": { | |||||
| "errcode": 0, | |||||
| "errmsg": "ok", | |||||
| "order_id": "order_123456", | |||||
| "waybill_id": "SF123456789", | |||||
| "delivery_id": "SF", | |||||
| "order_state": 2, | |||||
| "path_item_num": 3, | |||||
| "path_item_list": [...], | |||||
| "contact_list": [ | |||||
| { | |||||
| "type": 1, | |||||
| "name": "王师傅", | |||||
| "phone": "13800138000" | |||||
| } | |||||
| ], | |||||
| "openid": "otvnw62EqdpKnCvDQYUjCeNG99XY", | |||||
| "waybill_id_status": 0 | |||||
| } | |||||
| } | |||||
| ``` | |||||
| **重要字段说明:** | |||||
| - `contact_list`: 联系人列表 | |||||
| - `type`: 联系人类型,`1` 表示快递员 | |||||
| - `name`: 快递员姓名 | |||||
| - `phone`: 快递员手机号 ⭐ | |||||
| - `openid`: 用户openid | |||||
| - `waybill_id_status`: 运单状态码,`0` 表示未处理 | |||||
| ## 🚀 使用方法 | |||||
| ### 方法1:直接调用接口 | |||||
| ```java | |||||
| @Autowired | |||||
| private WeChatLogisticsService weChatLogisticsService; | |||||
| public String getCourierPhone(String orderId, String waybillId, String deliveryId, String openid) { | |||||
| try { | |||||
| WeChatLogisticsPathRequest request = new WeChatLogisticsPathRequest(); | |||||
| request.setOrderId(orderId); | |||||
| request.setWaybillId(waybillId); | |||||
| request.setDeliveryId(deliveryId); | |||||
| request.setOpenid(openid); | |||||
| WeChatLogisticsPathResponse response = weChatLogisticsService.getPath(request); | |||||
| // 查找快递员信息 | |||||
| if (response.getContactList() != null) { | |||||
| for (WeChatLogisticsContact contact : response.getContactList()) { | |||||
| if (contact.getType() == 1) { // type=1表示快递员 | |||||
| return contact.getPhone(); | |||||
| } | |||||
| } | |||||
| } | |||||
| return "未找到快递员信息"; | |||||
| } catch (Exception e) { | |||||
| log.error("获取快递员手机号失败", e); | |||||
| return null; | |||||
| } | |||||
| } | |||||
| ``` | |||||
| ### 方法2:使用测试接口 | |||||
| **测试单个查询:** | |||||
| ``` | |||||
| POST /applet/logistics/test/getPath | |||||
| 参数: | |||||
| - orderId: 订单ID | |||||
| - waybillId: 运单号 | |||||
| - deliveryId: 快递公司ID | |||||
| - openid: 用户openid(可选) | |||||
| ``` | |||||
| **完整流程测试:** | |||||
| ``` | |||||
| POST /applet/logistics/test/fullTest | |||||
| 参数: | |||||
| - openid: 用户openid(可选) | |||||
| - deliveryId: 快递公司ID(可选) | |||||
| - bizId: 商家编码(可选) | |||||
| ``` | |||||
| ## ⏰ 时机说明 | |||||
| ### 何时可以获取快递员信息? | |||||
| 1. **下单成功后**:运单生成,但可能还没有分配快递员 | |||||
| 2. **快递员接单后**:通常在下单后几分钟到几小时内 | |||||
| 3. **快递员准备上门取件时**:最佳获取时机 | |||||
| 4. **运输过程中**:快递员信息可能会更新 | |||||
| ### 运单状态对应关系 | |||||
| | 状态码 | 状态描述 | 快递员信息 | | |||||
| |--------|----------|------------| | |||||
| | 1 | 已下单 | 通常没有 | | |||||
| | 2 | 已揽件 | 有(取件快递员) | | |||||
| | 3 | 运输中 | 有(当前负责快递员) | | |||||
| | 4 | 派件中 | 有(派件快递员) | | |||||
| | 5 | 已签收 | 有(派件快递员) | | |||||
| ### 运单状态码说明 | |||||
| | waybill_id_status | 含义 | | |||||
| |-------------------|------| | |||||
| | 0 | 未处理/刚下单 | | |||||
| | 1 | 已处理 | | |||||
| | 其他 | 具体状态(待确认) | | |||||
| ## 📝 实际应用场景 | |||||
| ### 场景1:用户查询快递员联系方式 | |||||
| ```java | |||||
| import java.util.HashMap; | |||||
| import java.util.Map; | |||||
| @GetMapping("/getCourierInfo/{orderId}") | |||||
| public Result getCourierInfo(@PathVariable String orderId) { | |||||
| // 根据订单ID查询运单信息 | |||||
| Order order = orderService.getById(orderId); | |||||
| if (order == null || StringUtils.isEmpty(order.getWaybillId())) { | |||||
| return Result.error("订单不存在或未生成运单"); | |||||
| } | |||||
| // 查询快递员信息 | |||||
| try { | |||||
| WeChatLogisticsPathRequest request = new WeChatLogisticsPathRequest(); | |||||
| request.setOrderId(orderId); | |||||
| request.setWaybillId(order.getWaybillId()); | |||||
| request.setDeliveryId(order.getDeliveryId()); | |||||
| request.setOpenid(order.getOpenid()); | |||||
| WeChatLogisticsPathResponse response = weChatLogisticsService.getPath(request); | |||||
| // 提取快递员信息 | |||||
| WeChatLogisticsContact courier = response.getContactList().stream() | |||||
| .filter(contact -> contact.getType() == 1) | |||||
| .findFirst() | |||||
| .orElse(null); | |||||
| if (courier != null) { | |||||
| Map<String, Object> result = new HashMap<>(); | |||||
| result.put("courierName", courier.getName()); | |||||
| result.put("courierPhone", courier.getPhone()); | |||||
| result.put("orderState", response.getOrderState()); | |||||
| return Result.ok(result); | |||||
| } else { | |||||
| return Result.error("暂未分配快递员"); | |||||
| } | |||||
| } catch (Exception e) { | |||||
| return Result.error("查询快递员信息失败:" + e.getMessage()); | |||||
| } | |||||
| } | |||||
| ``` | |||||
| ### 场景2:定时查询并更新快递员信息 | |||||
| ```java | |||||
| @Scheduled(fixedRate = 300000) // 每5分钟执行一次 | |||||
| public void updateCourierInfo() { | |||||
| // 查询所有运输中的订单 | |||||
| List<Order> orders = orderService.getTransportingOrders(); | |||||
| for (Order order : orders) { | |||||
| try { | |||||
| // 查询最新的快递员信息 | |||||
| WeChatLogisticsPathResponse response = weChatLogisticsService.getPath( | |||||
| buildPathRequest(order) | |||||
| ); | |||||
| // 更新快递员信息到数据库 | |||||
| updateOrderCourierInfo(order, response); | |||||
| } catch (Exception e) { | |||||
| log.error("更新订单{}快递员信息失败", order.getOrderId(), e); | |||||
| } | |||||
| } | |||||
| } | |||||
| ``` | |||||
| ## ⚠️ 注意事项 | |||||
| ### 1. 时间延迟 | |||||
| - 下单后立即查询可能获取不到快递员信息 | |||||
| - 建议在下单后等待一段时间再查询 | |||||
| - 可以设置定时任务定期更新 | |||||
| - 如果 `waybill_id_status = 0` 表示运单未处理,需要等待 | |||||
| ### 2. 状态依赖 | |||||
| - 只有快递公司处理订单后才会有快递员信息 | |||||
| - 不同快递公司的响应时间不同 | |||||
| - 某些快递公司可能不提供快递员手机号 | |||||
| ### 3. 隐私保护 | |||||
| - 快递员手机号属于敏感信息 | |||||
| - 使用时需要遵守隐私保护规定 | |||||
| - 建议加密存储或者仅临时使用 | |||||
| ### 4. 错误处理 | |||||
| - 查询失败时要有友好的提示 | |||||
| - 网络异常时需要重试机制 | |||||
| - 快递公司接口异常时的降级方案 | |||||
| ## 🔍 调试建议 | |||||
| ### 1. 查看日志 | |||||
| ```java | |||||
| // 在 WeChatLogisticsService 中会输出: | |||||
| log.info("快递员信息 - 姓名:{},手机号:{}", contact.getName(), contact.getPhone()); | |||||
| ``` | |||||
| ### 2. 使用测试接口 | |||||
| - 先用 `/applet/logistics/test/addOrder` 生成测试订单 | |||||
| - 等待几分钟后用 `/applet/logistics/test/getPath` 查询 | |||||
| - 或者直接用 `/applet/logistics/test/fullTest` 完整测试 | |||||
| ### 3. 检查返回数据 | |||||
| - 确认 `contact_list` 是否为空 | |||||
| - 检查 `type` 字段是否为 1 | |||||
| - 验证 `phone` 字段格式是否正确 | |||||
| - 查看 `waybill_id_status` 状态码 | |||||
| - 检查 `path_item_num` 是否为 0(表示暂无轨迹) | |||||
| ## 📞 总结 | |||||
| 获取快递员手机号的关键步骤: | |||||
| 1. ✅ **下单成功** - 获取 `order_id` 和 `waybill_id` | |||||
| 2. ⏰ **等待处理** - 等待快递公司分配快递员(几分钟到几小时) | |||||
| 3. 🔍 **查询轨迹** - 调用 `getPath` 接口查询运单状态 | |||||
| 4. 📱 **提取信息** - 从 `contact_list` 中找到 `type=1` 的快递员信息 | |||||
| 5. ☎️ **获取手机号** - 得到快递员的 `name` 和 `phone` | |||||
| **最佳实践:** 在用户需要联系快递员时再实时查询,确保获取到最新的快递员信息。 | |||||
| @ -0,0 +1,27 @@ | |||||
| # 微信物流配置示例 | |||||
| # 请将以下配置添加到你的 application.yml 文件中 | |||||
| wechat: | |||||
| # 微信小程序AppID | |||||
| mp-app-id: "wxe934cebcbc89d869" | |||||
| # 微信小程序AppSecret | |||||
| mp-app-secret: "78e4e80a2096b362114a7afeefd2ef23" | |||||
| # 是否启用微信物流服务 | |||||
| enabled: true | |||||
| # 连接超时时间(毫秒) | |||||
| connect-timeout: 30000 | |||||
| # 读取超时时间(毫秒) | |||||
| read-timeout: 30000 | |||||
| # 获取Access Token的URL(一般不需要修改) | |||||
| access-token-url: "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s" | |||||
| # 获取所有物流账号的URL(一般不需要修改) | |||||
| get-all-account-url: "https://api.weixin.qq.com/cgi-bin/express/business/account/getall?access_token=%s" | |||||
| # 生成运单(下单)的URL(一般不需要修改) | |||||
| add-order-url: "https://api.weixin.qq.com/cgi-bin/express/business/order/add?access_token=%s" | |||||
| # 使用说明: | |||||
| # 1. 请在微信公众平台(https://mp.weixin.qq.com)申请小程序,获取正确的 mp-app-id 和 mp-app-secret | |||||
| # 2. 确保小程序已开通物流服务功能 | |||||
| # 3. 根据网络情况调整超时时间 | |||||
| # 4. 在生产环境中,建议将敏感信息(如 mp-app-secret)放在环境变量中 | |||||
| # 5. 如果不需要微信物流功能,可以将 enabled 设置为 false | |||||