From fa267c4413a7e4fcd921ad2395fef3c1a6e27256 Mon Sep 17 00:00:00 2001 From: wushumin Date: Thu, 11 Jun 2026 14:34:12 +0800 Subject: [PATCH] feat: add third-party order logistics APIs --- docs/api/third-party-openapi.md | 297 ++++++++++++++++-- .../app/controller/open/OrdersController.php | 97 ++++++ .../app/support/EnterpriseOrderService.php | 130 ++++++++ server-api/config/route.php | 2 + 4 files changed, 496 insertions(+), 30 deletions(-) diff --git a/docs/api/third-party-openapi.md b/docs/api/third-party-openapi.md index aa0aa49..7f976d1 100644 --- a/docs/api/third-party-openapi.md +++ b/docs/api/third-party-openapi.md @@ -1,11 +1,11 @@ # 第三方订单对接文档 -版本:v1 -更新日期:2026-05-08 +版本:v1.1 +更新日期:2026-06-11 ## 1. 对接说明 -本文档用于第三方系统对接安心验开放接口。第三方推送订单时,只需要提供第三方自己的订单号 `external_order_no`,不需要提前传物品信息。具体物品信息会在鉴定师鉴定时由平台侧补充完善。 +本文档用于第三方系统对接安心验开放接口。第三方推送订单时,最小只需要提供第三方自己的订单号 `external_order_no`。如第三方已具备服务套餐、物品信息、寄回地址、鉴定资料或寄入物流,也可以在创建订单时一并传入,平台会直接落入订单资料,减少后续人工补录。 接口域名以实际环境为准,本文统一使用: @@ -113,29 +113,161 @@ function sign_request(string $method, string $pathWithQuery, string $body, strin | `422` | 请求参数不合法 | | `500` | 服务端处理失败 | -## 4. 创建订单 +## 4. 套餐获取 + +第三方创建订单前,可以先调用本接口获取当前可用服务套餐和价格,再将返回的 `price_package_code` 传入创建订单接口。 + +```text +GET /api/open/v1/service-price-packages +``` + +### 4.1 查询参数 + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `service_provider` | string | 否 | 服务方,可选 `anxinyan`、`zhongjian`;不传返回全部服务方启用套餐 | + +### 4.2 cURL 示例 + +```bash +curl -X GET 'https://{api-domain}/api/open/v1/service-price-packages?service_provider=anxinyan' \ + -H 'Content-Type: application/json' \ + -H 'X-AXY-App-Key: your_app_key' \ + -H 'X-AXY-Timestamp: 1715155200' \ + -H 'X-AXY-Nonce: random_nonce' \ + -H 'X-AXY-Signature: calculated_signature' +``` + +GET 请求参与签名的 `raw_body` 为空字符串,`path_with_query` 需要包含实际查询字符串。 + +### 4.3 成功响应示例 + +```json +{ + "code": 0, + "message": "ok", + "data": { + "service_providers": [ + { + "service_provider": "anxinyan", + "service_provider_text": "安心验鉴定", + "sla_hours": 48, + "default_price_package_code": "anxinyan_basic", + "packages": [ + { + "service_provider": "anxinyan", + "service_provider_text": "安心验鉴定", + "price_package_name": "安心验基础套餐", + "price_package_code": "anxinyan_basic", + "price_package_price": 99, + "description": "默认服务价格套餐", + "is_default": true, + "sla_hours": 48 + } + ] + } + ] + } +} +``` + +### 4.4 响应说明 + +- 接口只返回启用套餐,不返回停用套餐。 +- `price_package_code` 可直接作为创建订单接口的 `price_package_code` 参数。 +- `default_price_package_code` 表示该服务方当前默认套餐;创建订单不传 `price_package_code` 时,平台会使用当前服务方默认启用套餐。 + +## 5. 创建订单 ```text POST /api/open/v1/orders ``` -第三方创建订单时只需要传 `external_order_no`。平台会创建一笔待收货订单,后续物品信息由鉴定师在鉴定工作台补充。 +第三方创建订单时,最小只需要传 `external_order_no`。平台会创建一笔待寄送商品订单;如请求中包含套餐、物品、地址、资料或寄入物流,平台会同步写入订单主表、商品资料、寄回地址、初始鉴定资料和寄入物流记录。 -### 4.1 请求参数 +### 5.1 请求参数 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `external_order_no` | string | 是 | 第三方订单号。同一对接客户下必须唯一 | | `service_provider` | string | 否 | 服务方,可选 `anxinyan`、`zhongjian`,默认 `anxinyan` | -| `product_info` | object | 否 | 物品信息,当前可不传 | -| `materials` | array | 否 | 鉴定资料图片 URL 列表,当前可不传 | -| `return_address` | object | 否 | 退回地址,当前可不传;如传任一地址字段,则必填完整地址 | -| `inbound_logistics` | object | 否 | 寄入物流信息,当前可不传 | +| `price_package_code` | string | 否 | 服务价格套餐编码,可通过套餐获取接口取得;不传时使用当前服务方的默认启用套餐;传入无效或已停用编码时返回 `422` | +| `product_info` | object | 否 | 物品信息。不传时订单会保留待完善物品信息 | +| `materials` | array | 否 | 鉴定资料图片 URL 列表或资料对象列表。不传时不会生成初始资料文件 | +| `return_address` | object | 否 | 寄回地址。不传时后续由平台或用户补充;如传任一地址字段,则必填完整地址 | +| `inbound_logistics` | object | 否 | 寄入物流信息。不传时后续可由入库台按订单号、鉴定单号或外部订单号匹配入库 | | `express_company` | string | 否 | 寄入快递公司,可替代 `inbound_logistics.express_company` | | `tracking_no` | string | 否 | 寄入运单号,可替代 `inbound_logistics.tracking_no` | -| `extra_info` | object | 否 | 扩展信息,当前可不传 | +| `extra_info` | object | 否 | 购买、成色、附件、备注等扩展信息 | -### 4.2 最小请求示例 +### 5.2 可选对象字段 + +`product_info` 支持: + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `category_id` | integer | 否 | 平台品类 ID。第三方无法确定 ID 时可不传 | +| `category_name` | string | 否 | 品类名称 | +| `brand_id` | integer | 否 | 平台品牌 ID。第三方无法确定 ID 时可不传 | +| `brand_name` | string | 否 | 品牌名称 | +| `product_name` | string | 否 | 商品名称;不传时平台会尝试用品牌和品类拼接展示 | +| `color` | string | 否 | 颜色 | +| `size_spec` | string | 否 | 规格或尺寸 | +| `serial_no` | string | 否 | 序列号、刻印号或其他唯一标识 | + +`return_address` 支持: + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `consignee` | string | 条件必填 | 收件人 | +| `mobile` | string | 条件必填 | 手机号 | +| `province` | string | 条件必填 | 省份 | +| `city` | string | 条件必填 | 城市 | +| `district` | string | 条件必填 | 区县 | +| `detail_address` | string | 条件必填 | 详细地址 | + +只要 `return_address` 中任意字段有值,上述字段都必须完整填写。 + +`materials` 支持两种格式: + +```json +[ + "https://example.com/item-front.jpg", + { + "item_code": "front", + "item_name": "商品正面图", + "file_url": "https://example.com/item-front.jpg", + "thumbnail_url": "https://example.com/item-front-thumb.jpg", + "is_required": true + } +] +``` + +资料文件当前只支持 `http` 或 `https` 图片 URL。对象格式中 `file_url` 与 `url` 等价;`thumbnail_url` 不传时默认使用原图 URL。 + +`extra_info` 支持: + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `purchase_channel` | string | 否 | 购买渠道 | +| `purchase_price` | number | 否 | 购买价格 | +| `purchase_date` | string | 否 | 购买日期,建议格式 `YYYY-MM-DD` | +| `usage_status` | string | 否 | 使用状态 | +| `condition_desc` | string | 否 | 成色描述 | +| `has_accessories` | boolean | 否 | 是否有附件 | +| `accessories` | array | 否 | 附件列表 | +| `remark` | string | 否 | 备注 | + +`inbound_logistics` 支持: + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `express_company` | string | 成对填写 | 寄入快递公司 | +| `tracking_no` | string | 成对填写 | 寄入运单号 | + +如果希望创建订单时同步写入寄入物流,需要同时提供快递公司和运单号。也可以直接使用顶层 `express_company` 和 `tracking_no`,含义与 `inbound_logistics` 内字段一致。 + +### 5.3 最小请求示例 ```json { @@ -143,12 +275,21 @@ POST /api/open/v1/orders } ``` -### 4.3 带可选字段请求示例 +### 5.4 带可选字段请求示例 ```json { "external_order_no": "THIRD202605080002", "service_provider": "anxinyan", + "price_package_code": "anxinyan_basic", + "product_info": { + "category_name": "箱包", + "brand_name": "CHANEL", + "product_name": "Classic Flap 手袋", + "color": "黑色", + "size_spec": "中号", + "serial_no": "A12345678" + }, "inbound_logistics": { "express_company": "顺丰速运", "tracking_no": "SF1234567890" @@ -160,11 +301,30 @@ POST /api/open/v1/orders "city": "杭州市", "district": "西湖区", "detail_address": "文三路 1 号" + }, + "materials": [ + { + "item_code": "front", + "item_name": "商品正面图", + "file_url": "https://example.com/materials/front.jpg", + "thumbnail_url": "https://example.com/materials/front-thumb.jpg", + "is_required": true + } + ], + "extra_info": { + "purchase_channel": "专柜", + "purchase_price": 68000, + "purchase_date": "2026-06-01", + "usage_status": "轻微使用", + "condition_desc": "外观轻微使用痕迹", + "has_accessories": true, + "accessories": ["防尘袋", "盒子"], + "remark": "第三方同步订单" } } ``` -### 4.4 cURL 示例 +### 5.5 cURL 示例 ```bash curl -X POST 'https://{api-domain}/api/open/v1/orders' \ @@ -176,7 +336,7 @@ curl -X POST 'https://{api-domain}/api/open/v1/orders' \ -d '{"external_order_no":"THIRD202605080001"}' ``` -### 4.5 成功响应示例 +### 5.6 成功响应示例 ```json { @@ -194,6 +354,9 @@ curl -X POST 'https://{api-domain}/api/open/v1/orders' \ "order_status": "pending_shipping", "display_status": "待寄送商品", "payment_status": "paid", + "price_package_name": "安心验基础套餐", + "price_package_code": "anxinyan_basic", + "price_package_price": 99, "pay_amount": 99, "estimated_finish_time": "2026-05-09 12:00:00", "created_at": "2026-05-08 12:00:00", @@ -219,21 +382,22 @@ curl -X POST 'https://{api-domain}/api/open/v1/orders' \ } ``` -### 4.6 幂等规则 +### 5.7 幂等规则 同一个对接客户下,`external_order_no` 作为幂等键: - 第一次请求会创建订单。 - 后续使用相同 `external_order_no` 且请求内容一致时,不会重复创建订单,会返回已有订单,`data.idempotent` 为 `true`。 - 后续使用相同 `external_order_no` 但请求内容不一致时,返回 `409`。 +- 如第一次只传最小字段,后续不能再用同一个 `external_order_no` 重推补充字段;如需补充资料,应走平台补录、入库或补料流程。 建议第三方重试创建订单时保持请求 JSON 内容一致,仅重新生成 `timestamp`、`nonce` 和 `signature`。 -## 5. 查询订单 +## 6. 查询订单 支持按第三方订单号或平台订单号查询订单进度。 -### 5.1 按第三方订单号查询 +### 6.1 按第三方订单号查询 ```text GET /api/open/v1/orders/{external_order_no} @@ -250,14 +414,14 @@ curl -X GET 'https://{api-domain}/api/open/v1/orders/THIRD202605080001' \ -H 'X-AXY-Signature: calculated_signature' ``` -### 5.2 通过查询参数查询 +### 6.2 通过查询参数查询 ```text GET /api/open/v1/orders?external_order_no=THIRD202605080001 GET /api/open/v1/orders?order_no=AXY20260508120000123 ``` -### 5.3 响应示例 +### 6.3 响应示例 ```json { @@ -274,6 +438,9 @@ GET /api/open/v1/orders?order_no=AXY20260508120000123 "order_status": "report_published", "display_status": "报告已发布", "payment_status": "paid", + "price_package_name": "安心验基础套餐", + "price_package_code": "anxinyan_basic", + "price_package_price": 99, "pay_amount": 99, "estimated_finish_time": "2026-05-09 12:00:00", "created_at": "2026-05-08 12:00:00", @@ -300,13 +467,81 @@ GET /api/open/v1/orders?order_no=AXY20260508120000123 } ``` -## 6. 订单状态 +## 7. 发货通知 + +第三方在商品实际寄出后,可以调用本接口通知平台写入寄入物流。创建订单接口中的 `inbound_logistics` 仍然可用;但如果订单创建和商品寄出不是同一时点,建议创建订单时只建单,实际寄出后再调用本接口提交快递信息。 + +```text +POST /api/open/v1/orders/shipping +``` + +### 7.1 请求参数 + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `external_order_no` | string | 是 | 第三方订单号。只能提交当前 `app_key` 所属客户下的订单 | +| `express_company` | string | 是 | 寄入快递公司 | +| `tracking_no` | string | 是 | 寄入运单号 | + +### 7.2 请求示例 + +```json +{ + "external_order_no": "THIRD202606110001", + "express_company": "顺丰速运", + "tracking_no": "SF1234567890" +} +``` + +### 7.3 成功响应示例 + +```json +{ + "code": 0, + "message": "运单已提交", + "data": { + "idempotent": false, + "updated": false, + "logistics": { + "express_company": "顺丰速运", + "tracking_no": "SF1234567890", + "tracking_status": "submitted", + "latest_desc": "客户已提交寄送运单:顺丰速运 SF1234567890,等待鉴定中心签收。", + "latest_time": "2026-06-11 12:00:00" + }, + "order": { + "customer_id": "CUST001", + "customer_code": "CUST001", + "external_order_no": "THIRD202606110001", + "order_no": "AXY20260611120000123", + "order_status": "pending_shipping", + "display_status": "已提交运单", + "inbound_logistics": { + "express_company": "顺丰速运", + "tracking_no": "SF1234567890", + "tracking_status": "submitted", + "latest_desc": "客户已提交寄送运单:顺丰速运 SF1234567890,等待鉴定中心签收。", + "latest_time": "2026-06-11 12:00:00" + } + } + } +} +``` + +### 7.4 重复提交规则 + +- 相同 `external_order_no`、`express_company`、`tracking_no` 重复提交时,接口返回成功,`idempotent` 为 `true`,不会重复写物流节点或订单时间线。 +- 同一订单在 `pending_shipping` 状态下提交不同快递公司或运单号时,会更新最新一条寄入物流,`updated` 为 `true`,并追加“已更新运单”时间线。 +- 非 `pending_shipping` 状态的订单不允许提交或更新寄入运单,返回 `422`。 +- 找不到当前客户下的 `external_order_no` 时返回 `404`。 + +## 8. 订单状态 常见订单状态如下,最终以接口返回的 `order_status` 和 `display_status` 为准。 | order_status | display_status | 说明 | | --- | --- | --- | -| `pending_shipping` | 待寄送商品 | 订单已创建,等待物品到仓或人工确认收货 | +| `pending_shipping` | 待寄送商品 / 已提交运单 | 订单已创建,等待物品到仓或人工确认收货 | | `received` | 鉴定中心已收货 | 物品已到仓 | | `appraising` | 物品鉴定中 | 鉴定师正在鉴定 | | `generating_report` | 物品鉴定完成 | 鉴定完成,报告生成中 | @@ -315,7 +550,7 @@ GET /api/open/v1/orders?order_no=AXY20260508120000123 | `completed` | 已完成 | 订单完成 | | `pending_supplement` | 需要补充资料 | 需要补充资料 | -## 7. Webhook 事件回调 +## 9. Webhook 事件回调 如需接收订单状态变化通知,第三方需向平台提供可公网访问的 `webhook_url`,并由平台开启回调。 @@ -336,7 +571,7 @@ GET /api/open/v1/orders?order_no=AXY20260508120000123 | 总超时 | 6 秒 | | 成功判定 | HTTP 状态码为 2xx 且无网络错误 | -### 7.1 回调报文 +### 9.1 回调报文 ```json { @@ -355,7 +590,7 @@ GET /api/open/v1/orders?order_no=AXY20260508120000123 } ``` -### 7.2 事件类型 +### 9.2 事件类型 | event_code | event_text | status_code | status_text | | --- | --- | --- | --- | @@ -368,7 +603,7 @@ GET /api/open/v1/orders?order_no=AXY20260508120000123 | `completed` | 订单已完成 | `completed` | 已完成 | | `supplement_required` | 需要补充资料 | `pending_supplement` | 需要补充资料 | -### 7.3 回调接收建议 +### 9.3 回调接收建议 第三方接收 webhook 时建议: @@ -376,10 +611,12 @@ GET /api/open/v1/orders?order_no=AXY20260508120000123 - 收到事件后返回 HTTP 2xx。 - 如需强一致的最新状态,可以收到 webhook 后再调用订单查询接口确认。 -## 8. 对接流程建议 +## 10. 对接流程建议 1. 平台分配 `app_key` 和 `app_secret`。 2. 第三方完成签名调试。 -3. 第三方调用创建订单接口,只传 `external_order_no` 即可。 -4. 第三方可通过查询接口主动查询订单状态。 -5. 如启用 webhook,平台在订单状态变化时主动通知第三方。 +3. 第三方调用套餐获取接口,确认可用套餐和 `price_package_code`。 +4. 第三方调用创建订单接口。最小只传 `external_order_no` 即可;如需要减少后续人工补录,建议同步传 `price_package_code`、`product_info`、`return_address`、`materials` 和 `inbound_logistics`。 +5. 商品实际寄出后,第三方调用发货通知接口提交 `express_company` 和 `tracking_no`。 +6. 第三方可通过查询接口主动查询订单状态。 +7. 如启用 webhook,平台在订单状态变化时主动通知第三方。 diff --git a/server-api/app/controller/open/OrdersController.php b/server-api/app/controller/open/OrdersController.php index 54a3640..f09ad83 100644 --- a/server-api/app/controller/open/OrdersController.php +++ b/server-api/app/controller/open/OrdersController.php @@ -2,6 +2,7 @@ namespace app\controller\open; +use app\support\AppraisalServicePricePackageService; use app\support\EnterpriseOpenApiAuthService; use app\support\EnterpriseOrderService; use support\Request; @@ -36,6 +37,65 @@ class OrdersController return api_success($result, !empty($result['idempotent']) ? '订单已存在' : '订单已创建'); } + public function shipping(Request $request) + { + try { + $auth = (new EnterpriseOpenApiAuthService())->authenticate($request); + } catch (\Throwable $e) { + return api_error($e->getMessage(), 401); + } + + $payload = json_decode($request->rawBody(), true); + if (!is_array($payload)) { + return api_error('请求体必须是合法 JSON 对象', 422); + } + + try { + $result = (new EnterpriseOrderService())->submitShipping($auth['customer'], $payload); + } catch (\InvalidArgumentException $e) { + return api_error($e->getMessage(), 422); + } catch (\RuntimeException $e) { + return api_error($e->getMessage(), 404); + } catch (\Throwable $e) { + return api_error('运单提交失败', 500, [ + 'detail' => $e->getMessage(), + ]); + } + + return api_success($result, '运单已提交'); + } + + public function servicePricePackages(Request $request) + { + try { + (new EnterpriseOpenApiAuthService())->authenticate($request); + } catch (\Throwable $e) { + return api_error($e->getMessage(), 401); + } + + try { + $service = new AppraisalServicePricePackageService(); + $serviceProvider = trim((string)$request->input('service_provider', '')); + $allowedProviders = array_column($service->providerOptions(), 'service_provider'); + if ($serviceProvider !== '' && !in_array($serviceProvider, $allowedProviders, true)) { + return api_error('service_provider 无效', 422); + } + + $providers = array_values(array_filter( + $service->serviceOptions(), + fn (array $item) => $serviceProvider === '' || (string)$item['service_provider'] === $serviceProvider + )); + + return api_success([ + 'service_providers' => array_map(fn (array $item) => $this->formatOpenServiceProvider($item), $providers), + ]); + } catch (\Throwable $e) { + return api_error('套餐获取失败', 500, [ + 'detail' => $e->getMessage(), + ]); + } + } + public function detail(Request $request) { try { @@ -66,4 +126,41 @@ class OrdersController 'order' => $order, ]); } + + private function formatOpenServiceProvider(array $item): array + { + $packages = array_map(fn (array $package) => $this->formatOpenPricePackage($package), (array)($item['packages'] ?? [])); + $defaultPackageCode = ''; + foreach ($packages as $package) { + if (!empty($package['is_default'])) { + $defaultPackageCode = (string)$package['price_package_code']; + break; + } + } + if ($defaultPackageCode === '' && isset($packages[0])) { + $defaultPackageCode = (string)$packages[0]['price_package_code']; + } + + return [ + 'service_provider' => (string)$item['service_provider'], + 'service_provider_text' => (string)$item['service_provider_text'], + 'sla_hours' => (int)$item['sla_hours'], + 'default_price_package_code' => $defaultPackageCode, + 'packages' => $packages, + ]; + } + + private function formatOpenPricePackage(array $package): array + { + return [ + 'service_provider' => (string)$package['service_provider'], + 'service_provider_text' => (string)$package['service_provider_text'], + 'price_package_name' => (string)$package['package_name'], + 'price_package_code' => (string)$package['package_code'], + 'price_package_price' => (float)$package['price'], + 'description' => (string)$package['description'], + 'is_default' => (bool)$package['is_default'], + 'sla_hours' => (int)$package['sla_hours'], + ]; + } } diff --git a/server-api/app/support/EnterpriseOrderService.php b/server-api/app/support/EnterpriseOrderService.php index 21b0f83..d3a4440 100644 --- a/server-api/app/support/EnterpriseOrderService.php +++ b/server-api/app/support/EnterpriseOrderService.php @@ -169,6 +169,136 @@ class EnterpriseOrderService return $this->buildOrderProgress((int)$customer['id'], $ref, (string)$customer['customer_code']); } + public function submitShipping(array $customer, array $payload): array + { + $externalOrderNo = trim((string)($payload['external_order_no'] ?? '')); + $expressCompany = trim((string)($payload['express_company'] ?? '')); + $trackingNo = trim((string)($payload['tracking_no'] ?? '')); + + if ($externalOrderNo === '') { + throw new \InvalidArgumentException('external_order_no 不能为空'); + } + if ($expressCompany === '' || $trackingNo === '') { + throw new \InvalidArgumentException('快递公司和运单号不能为空'); + } + + $ref = Db::name('enterprise_customer_order_refs') + ->where('customer_id', (int)$customer['id']) + ->where('external_order_no', $externalOrderNo) + ->find(); + if (!$ref) { + throw new \RuntimeException('订单不存在'); + } + + $order = Db::name('orders')->where('id', (int)$ref['order_id'])->find(); + if (!$order) { + throw new \RuntimeException('订单不存在'); + } + if ((string)$order['order_status'] !== 'pending_shipping') { + throw new \InvalidArgumentException('当前订单状态不支持提交运单'); + } + + $existing = Db::name('order_logistics') + ->where('order_id', (int)$order['id']) + ->where('logistics_type', 'send_to_center') + ->order('id', 'desc') + ->find(); + $sameLogistics = $existing + && (string)$existing['express_company'] === $expressCompany + && (string)$existing['tracking_no'] === $trackingNo; + if ($sameLogistics) { + return [ + 'idempotent' => true, + 'updated' => false, + 'logistics' => $this->formatLogistics($existing), + 'order' => $this->buildOrderProgress((int)$customer['id'], $ref, (string)$customer['customer_code']), + ]; + } + + $now = date('Y-m-d H:i:s'); + $latestDesc = sprintf('客户已提交寄送运单:%s %s,等待鉴定中心签收。', $expressCompany, $trackingNo); + $updated = (bool)$existing; + $logisticsId = 0; + $resetLogisticsSync = false; + + Db::startTrans(); + try { + if ($existing) { + $logisticsId = (int)$existing['id']; + $resetLogisticsSync = true; + Db::name('order_logistics')->where('id', $logisticsId)->update([ + 'logistics_type' => 'send_to_center', + 'express_company' => $expressCompany, + 'tracking_no' => $trackingNo, + 'tracking_status' => 'submitted', + 'latest_desc' => $latestDesc, + 'latest_time' => $now, + 'updated_at' => $now, + ]); + $nodeText = '已更新运单'; + $nodeDesc = sprintf('客户更新了寄送运单:%s %s', $expressCompany, $trackingNo); + } else { + $logisticsId = (int)Db::name('order_logistics')->insertGetId([ + 'order_id' => (int)$order['id'], + 'logistics_type' => 'send_to_center', + 'express_company' => $expressCompany, + 'tracking_no' => $trackingNo, + 'tracking_status' => 'submitted', + 'latest_desc' => $latestDesc, + 'latest_time' => $now, + 'created_at' => $now, + 'updated_at' => $now, + ]); + $nodeText = '已提交运单'; + $nodeDesc = sprintf('客户已提交寄送运单:%s %s', $expressCompany, $trackingNo); + } + + Db::name('order_logistics_nodes')->insert([ + 'logistics_id' => $logisticsId, + 'node_time' => $now, + 'node_desc' => $latestDesc, + 'node_location' => '第三方', + 'created_at' => $now, + ]); + + Db::name('orders')->where('id', (int)$order['id'])->update([ + 'display_status' => '已提交运单', + 'updated_at' => $now, + ]); + + Db::name('order_timelines')->insert([ + 'order_id' => (int)$order['id'], + 'node_code' => 'tracking_submitted', + 'node_text' => $nodeText, + 'node_desc' => $nodeDesc, + 'operator_type' => 'system', + 'operator_id' => null, + 'occurred_at' => $now, + 'created_at' => $now, + ]); + + Db::commit(); + } catch (\Throwable $e) { + Db::rollback(); + throw $e; + } + + $syncService = new OrderLogisticsSyncService(); + if ($resetLogisticsSync) { + Db::name('order_logistics_syncs')->where('logistics_id', $logisticsId)->delete(); + } + $syncService->subscribeAsync($logisticsId); + + $logistics = Db::name('order_logistics')->where('id', $logisticsId)->find(); + + return [ + 'idempotent' => false, + 'updated' => $updated, + 'logistics' => $this->formatLogistics($logistics), + 'order' => $this->buildOrderProgress((int)$customer['id'], $ref, (string)$customer['customer_code']), + ]; + } + public function buildOrderProgress(int $customerId, array $ref, string $customerCode = ''): array { $order = Db::name('orders')->where('id', (int)$ref['order_id'])->find(); diff --git a/server-api/config/route.php b/server-api/config/route.php index 842f889..a77cc3d 100644 --- a/server-api/config/route.php +++ b/server-api/config/route.php @@ -212,8 +212,10 @@ Route::post('/api/app/address/default', [AppAddressesController::class, 'setDefa Route::post('/api/app/address/delete', [AppAddressesController::class, 'delete']); Route::post('/api/open/v1/orders', [OpenOrdersController::class, 'create']); +Route::post('/api/open/v1/orders/shipping', [OpenOrdersController::class, 'shipping']); Route::get('/api/open/v1/orders', [OpenOrdersController::class, 'detail']); Route::get('/api/open/v1/orders/{external_order_no}', [OpenOrdersController::class, 'detail']); +Route::get('/api/open/v1/service-price-packages', [OpenOrdersController::class, 'servicePricePackages']); Route::post('/api/open/kuaidi100/callback', [OpenKuaidi100Controller::class, 'callback']); Route::post('/api/open/shouqianba/payment/notify', [OpenShouqianbaPaymentController::class, 'notify']);