Просмотр исходного кода

Merge branch 'zhongqi-delivery'

shizhongqi 7 месяцев назад
Родитель
Сommit
4f104e22bb

+ 10 - 12
app-ghs/controllers/DeliveryController.php

@@ -15,7 +15,6 @@ use ghs\models\delivery\CancelOrderForm;
 use ghs\models\delivery\CreateOrderForm;
 use ghs\models\delivery\OrderDetailForm;
 use yii\web\Response;
-use yii\helpers\ArrayHelper;
 
 /**
  * 聚合配送
@@ -199,17 +198,12 @@ class DeliveryController extends BaseController
         util::success($ret, "success");
     }
 
-    // 获取多个平台报价
+    // 获取各跑腿平台报价
     public function actionAllDeliveryQuotes()
     {
-        $post = Yii::$app->request->post();
-        $orderTime = $post['pickupTime'];
-        $weight = $post['weight'];
-        $remark = $post['remark'] ?? '';
-
-        $adminId = $this->adminId;
-        util::checkRepeatCommit($adminId, 3);
+        util::checkRepeatCommit($this->adminId, 3);
 
+        $post = Yii::$app->request->post();
         $orderId = intval($post['orderId']);
         $order = OrderClass::getById($orderId);
         if (empty($order)) {
@@ -225,12 +219,16 @@ class DeliveryController extends BaseController
             util::fail('客户地址信息缺失');
         }
 
-        $shop = ShopClass::getById($this->shopId);
+        $pickupTime = $post['pickupTime'];
+        $weight = $post['weight'];
+        $remark = $post['remark'] ?? '';
         $order['weight'] = $weight;
         $order['remark'] = $remark;
+        $shop = ShopClass::getById($this->shopId);
 
         $ds = new DispatchService($this->mainId);
-        $platformQuotes = $ds->getAllPlatformPrice($order, $shop, $orderTime);
+        $ds->validateOrder($order); //对 $order 进行验证
+        $platformQuotes = $ds->getAllPlatformPrice($order, $shop, $pickupTime);
         if (isset($platformQuotes['error'])) {
             util::fail($platformQuotes['error']);
         }
@@ -238,7 +236,7 @@ class DeliveryController extends BaseController
         // 格式化各平台报价为前端展示格式
         $deliveryList = $ds->formatPlatformQuotesForDisplay($platformQuotes);
         // 按 price 从低到高排序
-        ArrayHelper::multisort($deliveryList, 'price', SORT_ASC);
+        yii\helpers\ArrayHelper::multisort($deliveryList, 'price', SORT_ASC);
 
         $ret['deliveryList'] = $deliveryList;
         util::success($ret, "success");

+ 19 - 15
app-ghs/controllers/ShopExtController.php

@@ -36,7 +36,7 @@ class ShopExtController extends BaseController
         }
 
         // 组建零售数据
-        if (!empty($post['hcMap'])) {
+        if (!empty($post['hcMap']) && $post['hcMap'] != '[]') {
             $str = $post['hcMap'];
             $map = json_decode($str, true);
             if (!empty($map)) {
@@ -49,19 +49,21 @@ class ShopExtController extends BaseController
                 }
                 $hdArr['hcMap'] = json_encode($map);
             }
+        }else{
+            $hdArr['hcMap'] = '';
         }
         if(isset($post['hsFreeKm'])){
-            $hdArr['hsFreeKm'] = $post['hsFreeKm'];
+            $hdArr['hsFreeKm'] = intval($post['hsFreeKm']);
         }
         if(isset($post['hsAddFee'])){
             $hdArr['hsAddFee'] = $post['hsAddFee'];
         }
         if(isset($post['hcFreeKm'])){
-            $hdArr['hcFreeKm'] = $post['hcFreeKm'];
+            $hdArr['hcFreeKm'] = intval($post['hcFreeKm']);
         }
 
         // 组建批发数据
-        if (!empty($post['pfHcMap'])) {
+        if (!empty($post['pfHcMap']) && $post['pfHcMap'] != '[]') {
             $str = $post['pfHcMap'];
             $map = json_decode($str, true);
             if (!empty($map)) {
@@ -74,23 +76,25 @@ class ShopExtController extends BaseController
                 }
                 $ghsArr['hcMap'] = json_encode($map);
             }
+        }else{
+            $ghsArr['hcMap'] = '';
         }
         if(isset($post['pfHcFreeKm'])){
-            $ghsArr['hcFreeKm'] = $post['pfHcFreeKm'];
+            $ghsArr['hcFreeKm'] = intval($post['pfHcFreeKm']);
         }
 
-        if (empty($hdArr) && empty($ghsArr)) {
-            util::fail('没有需要修改');
+        if(!empty($ghsArr)){
+            //批发端更新
+            $shopId = $this->shopId;
+            ShopExtClass::updateByCondition(['shopId' => $shopId], $ghsArr);
         }
 
-        //批发端更新
-        $shopId = $this->shopId;
-        ShopExtClass::updateByCondition(['shopId' => $shopId], $ghsArr);
-
-        //零售端更新
-        $shop = $this->shop;
-        $lsShopId = $shop->lsShopId;
-        ShopExtClass::updateByCondition(['shopId' => $lsShopId], $hdArr);
+        if(!empty($hdArr)){
+            //零售端更新
+            $shop = $this->shop;
+            $lsShopId = $shop->lsShopId;
+            ShopExtClass::updateByCondition(['shopId' => $lsShopId], $hdArr);
+        }
         util::complete('修改成功');
     }
 

+ 2 - 1
app-hd/controllers/DeliveryController.php

@@ -97,7 +97,7 @@ class DeliveryController extends BaseController
         util::success($ret, "success");
     }
 
-    // 花店向批发买花时 -- 获取多个平台报价
+    // 花店向批发买花时 -- 获取各跑腿平台报价
     public function actionAllDeliveryQuotes()
     {
         $post = Yii::$app->request->post();
@@ -140,6 +140,7 @@ class DeliveryController extends BaseController
         ];
         $ghsShop = ShopClass::getById($ghsShopId);
         $ds = new DispatchService($ghs->mainId);
+        $ds->validateOrder($order); //对 $order 进行验证
         $platformQuotes = $ds->getAllPlatformPrice($order, $ghsShop, $orderTime);
         if (isset($platformQuotes['error'])) {
             util::fail($platformQuotes['error']);

+ 2 - 1
common/components/delivery/platform/dada/Dada.php

@@ -33,7 +33,8 @@ class Dada
             $this->appSecret = '828a03677a1c00a3a5b3c59209c4433d';
         } else {
             // 测试环境配置
-            $this->baseUrl = self::TEST_BASE_URL;
+            $this->baseUrl = self::PROD_BASE_URL; // TODO
+            //$this->baseUrl = self::TEST_BASE_URL;
             $this->appKey = 'dadaf2279d901a5ba40';
             $this->appSecret = '828a03677a1c00a3a5b3c59209c4433d';
         }

+ 0 - 510
common/components/delivery/platform/huolala/AUTH_IMPLEMENTATION_GUIDE.md

@@ -1,510 +0,0 @@
-# 货拉拉 OAuth2.0 授权实现指南
-
-## 概述
-
-货拉拉开放平台采用标准的 OAuth 2.0 `Authorization Code` 模式进行商户授权。
-此文档详细说明了授权流程、API接口、参数说明以及在项目中的集成方法。
-
----
-
-## 目录
-
-1. [授权流程](#授权流程)
-2. [API 接口](#api-接口)
-3. [环境配置](#环境配置)
-4. [使用示例](#使用示例)
-5. [常见问题](#常见问题)
-6. [参考资源](#参考资源)
-
----
-
-## 授权流程
-
-### Authorization Code 流程
-
-货拉拉的 OAuth 2.0 授权采用三步流程:
-
-```
-┌─────────────────┐          ┌──────────────┐          ┌────────────────┐
-│  第三方平台      │          │  货拉拉授权页 │          │ 货拉拉授权服务器 │
-│   (ISV)         │          │              │          │                │
-└────────┬────────┘          └──────────────┘          └────────────────┘
-         │                           │                          │
-         │  1. 跳转到授权页面        │                          │
-         │  (generateAuthUrl)        │                          │
-         │─────────────────────────>│                          │
-         │                           │                          │
-         │                     用户授权                         │
-         │                           │                          │
-         │                           │   2. 返回授权码(code)    │
-         │<──────────────────────────┤<─────────────────────────┤
-         │   redirect_uri?code=xxx   │                          │
-         │                           │                          │
-         │  3. 使用code换取token     │                          │
-         │  (getAccessToken)         │                          │
-         │────────────────────────────────────────────────────>│
-         │                           │                          │
-         │                           │  返回 access_token       │
-         │<────────────────────────────────────────────────────│
-         │                           │                          │
-```
-
-### 流程步骤详解
-
-#### 步骤1:生成授权URL
-
-商户系统中生成授权链接,引导用户跳转到货拉拉授权页面:
-
-```php
-$auth = new \common\components\delivery\platform\huolala\Auth();
-$redirectUrl = 'https://your-domain.com/delivery/huolala-callback';
-$authUrl = $auth->generateAuthUrl($redirectUrl);
-
-// 在页面中跳转
-Yii::$app->response->redirect($authUrl);
-```
-
-**生成的URL格式:**
-```
-https://open.huolala.cn/#/oauth/authorize?response_type=code&client_id=xxx&redirect_uri=xxx&isSandbox=true
-```
-
-#### 步骤2:用户授权
-
-用户在货拉拉授权页面填写账号信息并同意授权。货拉拉会重定向回 `redirect_uri` 并携带授权码 `code` 参数。
-
-#### 步骤3:交换Access Token
-
-在回调页面获取 `code`,调用接口交换 Access Token:
-
-```php
-$code = Yii::$app->request->get('code');
-$auth = new \common\components\delivery\platform\huolala\Auth();
-$result = $auth->getAccessToken($code);
-
-if ($result['success']) {
-    // 保存到数据库
-    $accessToken = $result['access_token'];
-    $refreshToken = $result['refresh_token'];
-    $expiresIn = $result['expires_in']; // 3个月
-} else {
-    // 处理错误
-    $error = $result['error'];
-}
-```
-
----
-
-## API 接口
-
-### 1. 生成授权URL
-
-**方法:** `generateAuthUrl($redirectUri)`
-
-**说明:** 生成用户授权的URL,用户通过访问此URL进行授权。
-
-**参数:**
-
-| 参数 | 类型 | 必须 | 说明 |
-|------|------|------|------|
-| redirectUri | string | 是 | 授权回调地址,用户授权后会跳转到此地址 |
-
-**返回值:** 授权URL字符串
-
-**示例:**
-
-```php
-$auth = new Auth();
-$authUrl = $auth->generateAuthUrl('http://your-domain.com/callback');
-// https://open.huolala.cn/#/oauth/authorize?response_type=code&client_id=xxx&redirect_uri=xxx&isSandbox=true
-```
-
----
-
-### 2. 获取Access Token
-
-**方法:** `getAccessToken($code)`
-
-**说明:** 使用授权码获取访问令牌和刷新令牌。
-
-**接口:** 
-- 路径:`/oauth/token`
-- 请求方式:`GET`
-- 环境:根据 `YII_ENV` 自动判断沙箱或生产环境
-
-**请求参数:**
-
-| 参数 | 类型 | 必须 | 说明 |
-|------|------|------|------|
-| grant_type | string | 是 | 固定值:`authorization_code` |
-| client_id | string | 是 | 应用 App Key |
-| code | string | 是 | 授权码(来自授权回调) |
-| isSandbox | boolean | 是 | 沙箱环境标志(自动处理) |
-
-**返回值:**
-
-```php
-[
-    'success' => true,                  // 请求是否成功
-    'access_token' => 'xxx',           // 访问令牌
-    'refresh_token' => 'yyy',          // 刷新令牌
-    'expires_in' => 7776000,           // 过期时间(秒),3个月
-]
-```
-
-**示例:**
-
-```php
-$auth = new Auth();
-$code = Yii::$app->request->get('code');
-$result = $auth->getAccessToken($code);
-
-if ($result['success']) {
-    echo "Token: " . $result['access_token'];
-} else {
-    echo "Error: " . $result['error'];
-}
-```
-
----
-
-### 3. 刷新Access Token
-
-**方法:** `refreshAccessToken($refreshToken)`
-
-**说明:** 使用刷新令牌获取新的访问令牌,无需用户重新授权。
-
-**接口:**
-- 路径:`/oauth/token`
-- 请求方式:`GET`
-- 环境:根据 `YII_ENV` 自动判断沙箱或生产环境
-
-**请求参数:**
-
-| 参数 | 类型 | 必须 | 说明 |
-|------|------|------|------|
-| grant_type | string | 是 | 固定值:`refresh_token` |
-| client_id | string | 是 | 应用 App Key |
-| refresh_token | string | 是 | 刷新令牌 |
-| isSandbox | boolean | 是 | 沙箱环境标志(自动处理) |
-
-**返回值:**
-
-```php
-[
-    'success' => true,                  // 请求是否成功
-    'access_token' => 'xxx',           // 新的访问令牌
-    'refresh_token' => 'yyy',          // 新的刷新令牌
-    'expires_in' => 7776000,           // 新的过期时间(秒)
-]
-```
-
-**示例:**
-
-```php
-$auth = new Auth();
-$result = $auth->refreshAccessToken($oldRefreshToken);
-
-if ($result['success']) {
-    // 使用新的 token
-    $newAccessToken = $result['access_token'];
-    $newRefreshToken = $result['refresh_token'];
-} else {
-    echo "Error: " . $result['error'];
-}
-```
-
----
-
-## 环境配置
-
-### 环境变量
-
-**YII_ENV 变量的作用:**
-
-| 值 | 说明 | isSandbox | 基础URL |
-|-----|------|----------|--------|
-| production | 正式环境 | false | https://open.huolala.cn |
-| 其他 | 沙箱/测试环境 | true | https://open.huolala.cn(同一个域名,通过参数区分) |
-
-### 沙箱环境测试
-
-**沙箱测试账户:**
-- 验证码固定为:`6666`
-- 环境:`isSandbox=true`
-
-**切换到沙箱环境:**
-
-```bash
-# 设置环境变量
-export YII_ENV=dev
-
-# 或在 env.php 中配置
-define('YII_ENV', 'dev');
-```
-
-### 配置应用凭证
-
-在 `Auth.php` 构造函数中修改:
-
-```php
-// 生产环境配置
-$this->appKey = '正式环境的app_key';
-$this->appSecret = '正式环境的app_secret';
-
-// 沙箱环境配置
-$this->appKey = '沙箱环境的app_key';
-$this->appSecret = '沙箱环境的app_secret';
-```
-
-或通过环境变量注入(推荐):
-
-```php
-public function __construct()
-{
-    $isProduction = getenv('YII_ENV') == 'production';
-    
-    if ($isProduction) {
-        $this->appKey = getenv('HUOLALA_PROD_APP_KEY');
-        $this->appSecret = getenv('HUOLALA_PROD_APP_SECRET');
-    } else {
-        $this->appKey = getenv('HUOLALA_SANDBOX_APP_KEY');
-        $this->appSecret = getenv('HUOLALA_SANDBOX_APP_SECRET');
-    }
-    
-    $this->isSandbox = !$isProduction;
-}
-```
-
----
-
-## 使用示例
-
-### 完整的授权流程
-
-```php
-<?php
-namespace app\controllers;
-
-use common\components\delivery\platform\huolala\Auth;
-use Yii;
-
-class DeliveryController extends \yii\web\Controller
-{
-    /**
-     * 步骤1:跳转到授权页面
-     */
-    public function actionAuthorizeHuolala()
-    {
-        $auth = new Auth();
-        $redirectUrl = Yii::$app->urlManager->createAbsoluteUrl(['delivery/huolala-callback']);
-        $authUrl = $auth->generateAuthUrl($redirectUrl);
-        
-        return Yii::$app->response->redirect($authUrl);
-    }
-    
-    /**
-     * 步骤2:处理授权回调
-     */
-    public function actionHuoLalaCallback()
-    {
-        $code = Yii::$app->request->get('code');
-        
-        if (!$code) {
-            return $this->redirect(['index', 'error' => '授权失败']);
-        }
-        
-        $auth = new Auth();
-        $result = $auth->getAccessToken($code);
-        
-        if (!$result['success']) {
-            return $this->redirect(['index', 'error' => $result['error']]);
-        }
-        
-        // 保存 token 到数据库
-        $this->saveHuoLalaToken([
-            'access_token' => $result['access_token'],
-            'refresh_token' => $result['refresh_token'],
-            'expires_at' => time() + $result['expires_in'],
-        ]);
-        
-        return $this->redirect(['index', 'success' => '授权成功']);
-    }
-    
-    /**
-     * 刷新过期的 Token
-     */
-    public function actionRefreshHuoLalaToken()
-    {
-        // 从数据库获取存储的 refresh_token
-        $storedToken = $this->getHuoLalaToken();
-        
-        if (!$storedToken || !$storedToken['refresh_token']) {
-            throw new \Exception('未找到有效的刷新令牌');
-        }
-        
-        $auth = new Auth();
-        $result = $auth->refreshAccessToken($storedToken['refresh_token']);
-        
-        if (!$result['success']) {
-            throw new \Exception('Token 刷新失败:' . $result['error']);
-        }
-        
-        // 更新 token
-        $this->saveHuoLalaToken([
-            'access_token' => $result['access_token'],
-            'refresh_token' => $result['refresh_token'],
-            'expires_at' => time() + $result['expires_in'],
-        ]);
-        
-        return $result;
-    }
-    
-    /**
-     * 保存 Token
-     */
-    private function saveHuoLalaToken($tokenData)
-    {
-        // 实现数据库保存逻辑
-        // ...
-    }
-    
-    /**
-     * 获取 Token
-     */
-    private function getHuoLalaToken()
-    {
-        // 实现数据库获取逻辑
-        // ...
-    }
-}
-```
-
-### 处理Token过期
-
-```php
-<?php
-/**
- * 检查 Token 是否即将过期,并自动刷新
- */
-class HuoLalaTokenManager
-{
-    const REFRESH_THRESHOLD = 7 * 24 * 3600; // 提前7天刷新
-    
-    public static function ensureTokenValid()
-    {
-        $token = self::getToken();
-        
-        // 检查是否即将过期
-        if ($token['expires_at'] - time() < self::REFRESH_THRESHOLD) {
-            $auth = new Auth();
-            $result = $auth->refreshAccessToken($token['refresh_token']);
-            
-            if ($result['success']) {
-                self::saveToken([
-                    'access_token' => $result['access_token'],
-                    'refresh_token' => $result['refresh_token'],
-                    'expires_at' => time() + $result['expires_in'],
-                ]);
-                
-                return $result['access_token'];
-            }
-        }
-        
-        return $token['access_token'];
-    }
-    
-    private static function getToken()
-    {
-        // 从数据库获取
-    }
-    
-    private static function saveToken($data)
-    {
-        // 保存到数据库
-    }
-}
-```
-
----
-
-## 常见问题
-
-### Q1: 授权码(code)的有效期是多久?
-
-**A:** 授权码有效期为 1 分钟,且只能使用一次。超期或多次使用都会失效。
-
-### Q2: Access Token 有效期是多久?
-
-**A:** Access Token 的有效期为 3 个月(7776000 秒)。当即将过期时,使用 `Refresh Token` 进行更新。
-
-### Q3: 如何切换生产环境和沙箱环境?
-
-**A:** 通过 `YII_ENV` 环境变量控制:
-- `YII_ENV=production` 为生产环境(`isSandbox=false`)
-- 其他值为沙箱环境(`isSandbox=true`)
-
-### Q4: 刷新 Token 时会改变 Refresh Token 吗?
-
-**A:** 是的,刷新 Token 后会同时返回新的 `access_token` 和 `refresh_token`。建议每次更新都保存新的 Refresh Token。
-
-### Q5: 授权时如何传递自定义参数?
-
-**A:** 可以在 `redirect_uri` 中添加查询参数,例如:
-```php
-$redirectUrl = 'https://your-domain.com/callback?shop_id=123&user_id=456';
-$authUrl = $auth->generateAuthUrl($redirectUrl);
-```
-
-回调时可以直接从 URL 中获取这些参数。
-
-### Q6: 如何处理 HTTP 请求失败?
-
-**A:** Auth 类会自动捕获异常并返回标准格式的错误响应:
-```php
-$result = $auth->getAccessToken($code);
-if (!$result['success']) {
-    $error = $result['error'];
-    $statusCode = $result['status_code'] ?? null;
-    // 处理错误
-}
-```
-
----
-
-## 参考资源
-
-### 相关接口
-
-- **生成授权URL:** `generateAuthUrl($redirectUri)`
-- **获取Access Token:** `getAccessToken($code)`
-- **刷新Token:** `refreshAccessToken($refreshToken)`
-
-### 类方法
-
-- `getAppKey()` - 获取应用 App Key
-- `getAppSecret()` - 获取应用 App Secret
-- `isSandbox()` - 获取是否为沙箱环境
-
-### 官方文档
-
-- [货拉拉开放平台](https://open.huolala.cn)
-- OAuth 2.0 规范:https://tools.ietf.org/html/rfc6749
-
----
-
-## 文件位置
-
-```
-common/components/delivery/platform/huolala/Auth.php
-```
-
-## 版本历史
-
-| 版本 | 日期 | 描述 |
-|------|------|------|
-| 1.0 | 2024-10-24 | 初始实现,支持 Authorization Code 流程 |
-
----
-
-最后更新:2024-10-24

+ 0 - 300
common/components/delivery/platform/shansong/AUTH_IMPLEMENTATION_GUIDE.md

@@ -1,300 +0,0 @@
-# 闪送商户授权实现指南
-
-## 📋 概述
-
-该授权类 (`Auth`) 完全实现了闪送商户授权功能,支持两种授权方式:
-- **商户授权** - 授权后能为商户下所有门店发单
-- **门店授权** - 授权后只能为授权时选择的门店发单
-
-## 🔧 核心功能
-
-### 1. 生成授权URL
-
-#### 商户授权(推荐)
-```php
-$auth = new \common\components\delivery\platform\shanSong\Auth();
-$redirectUrl = 'http://your-domain.com/callback';
-$authUrl = $auth->generateMerchantAuthUrl($userId, $redirectUrl);
-
-// 重定向用户
-header('Location: ' . $authUrl);
-```
-
-**URL结构示例(生产环境):**
-```
-https://open.ishansong.com/auth?
-  isAllStoreAuth=true&
-  response_type=code&
-  scope=shop_open_api&
-  state=10583&
-  client_id=sseYlPL1Qo3j0lNfT&
-  redirect_uri=http%3a%2f%2fopen.s.bingex.com%2fopenqatest%2findex
-```
-
-#### 门店授权
-```php
-$auth = new \common\components\delivery\platform\shanSong\Auth();
-$thirdStoreId = 312313;  // 平台门店ID
-$userId = 10583;          // 用户标识
-$redirectUrl = 'http://your-domain.com/callback';
-
-$authUrl = $auth->generateStoreAuthUrl($thirdStoreId, $userId, $redirectUrl);
-header('Location: ' . $authUrl);
-```
-
-**URL结构示例:**
-```
-http://open.s.bingex.com/auth?
-  isAllStoreAuth=false&
-  thirdStoreId=312313&
-  response_type=code&
-  scope=shop_open_api&
-  state=10583&
-  client_id=ss2xpXL5NGhXIP7L2&
-  redirect_uri=http%3a%2f%2flanyinbin.cn%2faa%2fcallBack
-```
-
-### 2. 处理授权回调
-
-用户授权成功后,闪送会重定向回你设置的 `redirect_uri`,并携带以下参数:
-
-**商户授权返回参数:**
-```
-?code=520DD
-&state=10583
-&isAllStoreAuth=true
-&shopId=20000000000000348
-```
-
-**门店授权返回参数:**
-```
-?code=520DD
-&state=10583
-&thirdStoreId=312313
-&isAllStoreAuth=false
-&storeId=1331275
-&shopId=20000000000000348
-```
-
-#### 回调处理代码示例:
-
-```php
-public function actionCallback()
-{
-    $code = Yii::$app->request->get('code');
-    $state = Yii::$app->request->get('state');
-    $shopId = Yii::$app->request->get('shopId');
-    $isAllStoreAuth = Yii::$app->request->get('isAllStoreAuth');
-    $thirdStoreId = Yii::$app->request->get('thirdStoreId');
-    $storeId = Yii::$app->request->get('storeId');
-
-    // 验证state参数(防止CSRF)
-    if ($state != $yourUserId) {
-        throw new \yii\web\BadRequestHttpException('Invalid state parameter');
-    }
-
-    // 获取AccessToken
-    $auth = new \common\components\delivery\platform\shanSong\Auth();
-    $result = $auth->getAccessToken($code);
-
-    if (!$result['success']) {
-        throw new \yii\web\HttpException(500, '获取授权失败:' . $result['error']);
-    }
-
-    // 保存授权信息到数据库
-    $model = new ShansAuthToken();
-    $model->user_id = $state;
-    $model->shop_id = $shopId;
-    $model->access_token = $result['access_token'];
-    $model->refresh_token = $result['refresh_token'];
-    $model->expires_at = time() + $result['expires_in'];
-    $model->auth_type = $isAllStoreAuth ? 'all_store' : 'single_store';
-    
-    if (!$isAllStoreAuth) {
-        $model->third_store_id = $thirdStoreId;
-        $model->shans_store_id = $storeId;
-    }
-
-    if (!$model->save()) {
-        throw new \yii\web\HttpException(500, '保存授权信息失败');
-    }
-
-    return $this->redirect(['success']);
-}
-```
-
-### 3. 获取AccessToken
-
-```php
-$auth = new \common\components\delivery\platform\shanSong\Auth();
-$result = $auth->getAccessToken($authCode);
-
-// 返回格式
-$result = [
-    'success' => true,
-    'access_token' => '64850c4c-06fe-46fa-b30a-f680b193fe15',
-    'refresh_token' => '53521dad-60d2-4bf7-a81e-3882c2721bf1',
-    'expires_in' => 2592000,  // 30天
-];
-```
-
-### 4. 刷新AccessToken
-
-AccessToken 有效期为 30 天,建议在过期前 7 天开始刷新:
-
-```php
-$auth = new \common\components\delivery\platform\shanSong\Auth();
-$result = $auth->refreshAccessToken($refreshToken);
-
-if ($result['success']) {
-    // 更新数据库中的token
-    $model = ShansAuthToken::findOne(['user_id' => $userId]);
-    $model->access_token = $result['access_token'];
-    $model->expires_at = time() + $result['expires_in'];
-    $model->save();
-}
-```
-
-### 5. 取消授权
-
-```php
-$auth = new \common\components\delivery\platform\shanSong\Auth();
-$result = $auth->cancelAuthorization($accessToken);
-
-if ($result['success']) {
-    // 从数据库删除授权信息
-    ShansAuthToken::deleteAll(['user_id' => $userId]);
-}
-```
-
-## 📊 完整授权流程图
-
-```
-┌─────────────────────┐
-│  用户点击授权按钮   │
-└──────────┬──────────┘
-           │
-           ▼
-   ┌───────────────────────────────┐
-   │ generateMerchantAuthUrl()      │
-   │ 或 generateStoreAuthUrl()      │
-   └───────────┬───────────────────┘
-               │
-               ▼
-      ┌────────────────────────┐
-      │ 重定向到闪送授权页面   │
-      └────────┬───────────────┘
-               │
-               ▼
-      ┌────────────────────────────┐
-      │ 用户填写闪送账号授权       │
-      └────────┬───────────────────┘
-               │
-               ▼
-      ┌──────────────────────────┐
-      │ 闪送重定向回 redirect_uri │
-      │ 并携带 code 和 shopId    │
-      └────────┬────────────────┘
-               │
-               ▼
-      ┌─────────────────────────────┐
-      │ getAccessToken($code)       │
-      └────────┬────────────────────┘
-               │
-               ▼
-      ┌──────────────────────────┐
-      │ 保存 token 到数据库      │
-      └─────────────────────────┘
-```
-
-## 🗄️ 数据库表设计参考
-
-建议创建表来存储授权信息:
-
-```sql
-CREATE TABLE `xh_shans_auth_token` (
-  `id` int(11) NOT NULL AUTO_INCREMENT,
-  `user_id` int(11) NOT NULL COMMENT '用户ID',
-  `shop_id` varchar(50) NOT NULL COMMENT '闪送商户ID',
-  `access_token` varchar(100) NOT NULL COMMENT '访问令牌',
-  `refresh_token` varchar(100) NOT NULL COMMENT '刷新令牌',
-  `expires_at` int(11) NOT NULL COMMENT 'token过期时间戳',
-  `auth_type` enum('all_store','single_store') NOT NULL DEFAULT 'all_store' COMMENT '授权类型',
-  `third_store_id` int(11) COMMENT '第三方门店ID',
-  `shans_store_id` varchar(50) COMMENT '闪送门店ID',
-  `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
-  `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-  PRIMARY KEY (`id`),
-  UNIQUE KEY `uk_user_shop` (`user_id`, `shop_id`),
-  KEY `idx_expires_at` (`expires_at`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-```
-
-## 🔐 环境配置
-
-配置文件 (`common/config/params.php`) 中已包含:
-
-```php
-'shanAppId' => 'ss9QcKVXwvcMTkHzw',        // App Key
-'shanAppSecret' => '5ivRvyBXLrprGPssYUUvpKXwoZAojwDg',  // App Secret
-'expressShanUrl' => 'http://open.s.bingex.com',  // 测试环境
-// 或 'https://open.ishansong.com'  生产环境
-```
-
-环境自动识别:
-- 生产环境(YII_ENV=production): `https://open.ishansong.com`
-- 其他环境(YII_ENV=dev/test等): `http://open.s.bingex.com`
-
-## ⚠️ 重要注意事项
-
-### 1. **Token 生命周期管理**
-- AccessToken 有效期:30 天(2592000秒)
-- RefreshToken 长期有效
-- Code 有效期:1 分钟,只能使用 1 次
-
-### 2. **签名算法**
-- 算法:MD5
-- 签名包含的参数必须按字典序排序
-- 空值和 null 值需要过滤
-
-### 3. **URL 编码**
-- redirectUri 必须进行 URLEncode 编码
-- 该类自动处理 redirectUri 的编码
-
-### 4. **错误处理**
-所有方法返回统一的响应格式,包含 `success` 字段:
-
-```php
-// 成功响应
-['success' => true, 'access_token' => '...', ...]
-
-// 失败响应
-['success' => false, 'error' => '错误信息', ...]
-```
-
-### 5. **测试环境**
-- 使用测试账号进行授权测试
-- 需在闪送开放平台同步测试环境获取测试账号
-- 测试环境 URL:`http://open.s.bingex.com`
-
-### 6. **安全建议**
-- 妥善保管 `appSecret`,不要暴露在前端
-- State 参数建议使用用户ID或其他唯一标识
-- 定期验证和刷新 token
-- 实现 token 过期检测和自动刷新机制
-
-## 🚀 集成示例
-
-完整的集成示例可以参考项目中的:
-- `app-ghs/controllers/DeliveryController.php` - 快递相关的授权处理
-- `common/components/delivery/services/adapter/ShansongAdapter.php` - 闪送API调用示例
-
-## 📝 相关文档
-
-- 闪送开放平台:https://open.ishansong.com
-- 应用申请路径:登录闪送开放平台 -> 账号中心 -> 应用信息
-- 获取 AppKey 和 AppSecret:登录闪送开放平台 -> 账号中心 -> 应用信息
-
----
-
-如有问题,请参考闪送官方文档或查看类中的详细代码注释。

+ 69 - 27
common/components/delivery/services/DispatchService.php

@@ -104,6 +104,29 @@ class DispatchService
         }
     }
 
+    public function validateOrder($order)
+    {
+        if(empty($order['address'])){
+            throw new \Exception('客户地址缺失');
+        }
+    
+        if(empty($order['long']) || empty($order['lat'])){
+            throw new \Exception('客户地址信息缺失');
+        }
+
+        if(empty($order['customName'])){
+            throw new \Exception('客户姓名缺失');
+        }
+
+        if(empty($order['city'])){
+            throw new \Exception('客户城市信息缺失');
+        }
+
+        if(empty($order['customMobile']) && empty($order['receiveMobile']) && empty($order['bookMobile'])){
+            throw new \Exception('客户联系电话缺失');
+        }
+    }
+
     /**
      * @param $order
      * @param string $orderType 订单类型:xhOrder 或 xhGhsOrder
@@ -227,7 +250,7 @@ class DispatchService
     }
 
     /**
-     * 获取所有平台的最佳报价(使用 Guzzle 并发请求)
+     * 获取所有平台的报价(使用 Guzzle 并发请求)
      * 
      * 流程说明:
      * 1. 同步准备各平台的前置数据(如城市信息、订单商品等)
@@ -236,19 +259,20 @@ class DispatchService
      * 
      * @param array $order 订单信息
      * @param array $shop 店铺信息
+     * @param string $pickupTime 取件时间
      * @return array 返回所有平台的报价结果
      */
-    public function getAllPlatformPrice($order, $shop, $orderTime)
+    public function getAllPlatformPrice($order, $shop, $pickupTime)
     {
         // 第一步:准备前置数据(同步进行,因为某些平台需要这些数据)
-        $preparedData = $this->preparePlatformData($order, $shop, $orderTime);
+        $preparedData = $this->preparePlatformData($order, $shop, $pickupTime);
 
         if (empty($preparedData)) {
             return ['error' => '所有平台数据准备失败'];
         }
 
         // 第二步:并发调用各平台的报价接口
-        $results = $this->concurrentGetPrices($preparedData, $orderTime);
+        $results = $this->concurrentGetPrices($preparedData, $pickupTime);
 
         if (empty($results['success']) && empty($results['failed'])) {
             return ['error' => '没有可用的物流平台'];
@@ -263,12 +287,12 @@ class DispatchService
 
     /**
      * 为各平台准备订单数据(前置数据同步获取)
-     * 
      * @param array $order 订单信息
      * @param array $shop 店铺信息
+     * @param string $pickupTime 取件时间
      * @return array 各平台的订单数据
      */
-    private function preparePlatformData($order, $shop, $orderTime)
+    private function preparePlatformData($order, $shop, $pickupTime)
     {
         $preparedData = [];
 
@@ -296,7 +320,7 @@ class DispatchService
         // 准备 Shansong 数据
         if (isset($this->adapters['shansong'])) {
             try {
-                $preparedData['shansong'] = $this->prepareShansongData($order, $shop, $orderTime);
+                $preparedData['shansong'] = $this->prepareShansongData($order, $shop, $pickupTime);
             } catch (\Exception $e) {
                 Yii::warning("Shansong 数据准备失败: {$e->getMessage()}");
             }
@@ -318,7 +342,7 @@ class DispatchService
                 if(in_array($isPersonDirect, [1, 2])){ // 支持专人直送
                     $customSettings = ['isPersonDirect' => 1];//自定义用户设置:专人直送
                     $preparedData['shunfeng_1v1'] = $this->prepareShunfengData($order, $shop, $customSettings);
-                    Yii::info('预创建订单 shunfeng_1v1 request data: ' . json_encode($preparedData['shunfeng_1v1']));
+                    // Yii::info('预创建订单 shunfeng_1v1 request data: ' . json_encode($preparedData['shunfeng_1v1']));
                 }
                 // else{
                 //     $customSettings = [];//自定义用户设置:默认不专人直送
@@ -334,7 +358,14 @@ class DispatchService
             try {
                 $dadaAdapter = $this->adapters['dada'];
                 $shopNo = $dadaAdapter->getShopNo();
-                $preparedData['dada'] = $this->prepareDadaData($order, $shop, $shopNo);
+
+                // 拼送
+                $customSettings = ['is_direct_delivery' => 0];
+                $preparedData['dada'] = $this->prepareDadaData($order, $shopNo, $customSettings);
+
+                // 专送
+                //$customSettings = ['is_direct_delivery' => 1];
+                //$preparedData['dada_1v1'] = $this->prepareDadaData($order, $shopNo, $customSettings);
             } catch (\Exception $e) {
                 Yii::warning("Dada 数据准备失败: {$e->getMessage()}");
             }
@@ -380,7 +411,7 @@ class DispatchService
         }
         $specReq = array_filter($specReq);
 
-        // 构建所车型数据
+        // 构建所车型数据
         $vehicleTypeList = [];
         foreach ($vehicleList as $vehicle) {
             if(!in_array($vehicle['vehicle_name'], ['跑腿', '小面', '微面'])){ // && !$this->adapters['huolala']->getIsSandbox()
@@ -463,13 +494,13 @@ class DispatchService
             'receiver_latitude' => (float)$order['lat'],
             'receiver_longitude' => (float)$order['long'],
             'receiver_address' => $order['fullAddress'],
-            'position_source' => 3,
+            'position_source' => 3, //坐标经纬度来源(1:腾讯地图,2:百度地图, 3:高德地图)
             'goods_count' => count($goodsItemList),
             'goods_weight' => (float)$order['weight'],
             'goods_total_amount_cent' => (int)($order['actPrice'] * 100),
             'goods_actual_amount_cent' => (int)($order['prePrice'] * 100),
             'goods_item_list' => $goodsItemList,
-            'order_type' => 1,
+            //'order_type' => 1,
             'chain_store_id' => $platformShopId, // 蜂鸟平台 shopId
             'order_remark' => $order['remark'] ?? '',
         ];
@@ -478,7 +509,7 @@ class DispatchService
     /**
      * 准备闪送订单数据
      */
-    private function prepareShansongData($order, $shop, $orderTime)
+    private function prepareShansongData($order, $shop, $pickupTime)
     {
         $data = [
             'platform' => 'shansong',
@@ -513,11 +544,15 @@ class DispatchService
             'sendNum' => $order['sendNum'],
         ];
 
-        $timestamp = strtotime($orderTime);
+        $timestamp = strtotime($pickupTime);
         $currentTime = time() + 3600;
         if($timestamp > $currentTime){
-            $data['appointType'] = 1; // 修改预约类型
-            $data['appointmentDate'] = $orderTime; // 预约时间, yyyy-MM-dd HH:mm格式(例如:2020-02-02 22:00),指的是预约取件时间,只支持一个小时以后两天以内
+            $data['appointType'] = 1; // 修改成 1预约单
+            $data['appointmentDate'] = $pickupTime; // 预约时间, yyyy-MM-dd HH:mm格式(例如:2020-02-02 22:00),指的是预约取件时间,只支持一个小时以后
+            //两天以内
+            if($timestamp > time() + 172800){
+                throw new \Exception('取件时间不能超过两天');
+            }
 
             // 'expectStartTime' 期望送达时间起始时间  -- 毫秒级时间戳。期望送达时间为时间点时,无需传本参数;期望送达时间为时间段时,期望送达时间为expectStartTime至expectEndTime
             // 'expectEndTime'  期望送达时间终止时间 -- 毫秒级时间戳。期望送达时间为时间点时,只需传本参数;期望送达时间为时间段时,期望送达时间为expectStartTime至expectEndTime
@@ -555,7 +590,7 @@ class DispatchService
             'user_lat' => (string)$order['lat'],                     // 用户地址纬度
             'user_address' => $order['fullAddress'],                 // 用户详细地址
             'weight' => $order['weight'] * 1000,                     // 物品重量(单位:克)
-            'product_type' => 14,                                     // 物品类型(14:鲜花)
+            'product_type' => 14,                                    // 物品类型(14:鲜花)
             'push_time' => time(),                                   // 推单时间(秒级时间戳)
             //shop 平台级开发者需要传入
             // 'shop' => [                                              // 发货店铺信息
@@ -578,7 +613,7 @@ class DispatchService
             'lbs_type' => 2,                                         // 坐标类型(2:高德坐标)
             'is_insured' => 0,                                       // 是否保价(0:非保价)
             'is_person_direct' => $customSettings['isPersonDirect'] ?? 0, // 是否专人直送(0:否  1:是)
-            'vehicle' => 0,                                          // 配送交通工具(0:否)
+            'vehicle' => 0,                                          // 配送交通工具,0:否;1:电动车;2:小轿车
             //'four_wheeler_type' => 0,                                // 车型 -- 只有vehicle = 2,⻋型字段才有效。
             //'declared_value' => 0,                                   // 保价金额(单位:分)
             //'gratuity_fee' => 0,                                     // 订单小费(单位:分)
@@ -591,7 +626,7 @@ class DispatchService
     /**
      * 准备达达订单数据
      */
-    private function prepareDadaData($order, $shop, $shopNo)
+    private function prepareDadaData($order, $shopNo, $customSettings)
     {
         $productList = [];
         $itemInfos = OrderItemClass::getAllByCondition(['orderSn' => $order['orderSn']], null, 'id, name, unitPrice, num, bigNum, smallNum, xhUnitName');
@@ -607,7 +642,7 @@ class DispatchService
         // 回调地址
         $callbackUrl = Yii::$app->params['ghsHost'] . '/delivery/dada-callback';
 
-        return [
+        $request = [
             'platform' => 'dada',
             'shop_no' => $shopNo,
             'origin_id' => $order['orderSn'],
@@ -624,6 +659,13 @@ class DispatchService
             'info' => $order['remark'] ?? '',
             'product_list' => $productList,
         ];
+
+        if(isset($customSettings['is_direct_delivery']) && $customSettings['is_direct_delivery'] == 1){
+            // $request['origin_id'] = $order['orderSn'] . '_1v1'; // 根据达达的情况,必然会造成订单号出错
+            $request['is_direct_delivery'] = 1; //是否选择直拿直送(0:不需要;1:需要。选择直拿直送后,同一时间骑士只能配送此订单至完成,同时,也会相应的增加配送费用)
+        }
+
+        return $request;
     }
 
     /**
@@ -659,7 +701,7 @@ class DispatchService
     /**
      * 并发获取各平台报价(核心实现)
      * 
-     * 使用 HttpClient::postConcurrent 实现真正的并发调用,每个平台请求 5 秒超时。
+     * 使用 HttpClient::postConcurrent 实现真正的并发调用,每个平台请求 10 秒超时。
      * 某个平台的超时或失败不会影响其他平台的执行。
      * 
      * @param array $preparedData 准备好的各平台数据
@@ -682,8 +724,8 @@ class DispatchService
                 $adapterKey = $platform; //由于要支持特殊逻辑处理,把 $this->adapters[xxx] 的 xxx 单独取变量
 
                 if (!isset($this->adapters[$platform])) {
-                    if($platform == 'shunfeng_1v1'){ //特殊处理 -- 专人直送的订单,使用顺丰平台适配器
-                        $adapterKey = 'shunfeng';
+                    if (substr($platform, -4) === '_1v1') { //特殊处理 -- 专人直送的订单,使用对应平台适配器
+                        $adapterKey = substr($platform, 0, -4);
                     } else {
                         $results['failed'][$platform] = '平台适配器未初始化';
                         Yii::warning("[DispatchService] {$platform} 平台适配器未初始化");
@@ -735,8 +777,8 @@ class DispatchService
 
             try {
                 $adapterKey = $platform; //由于要支持特殊逻辑处理,把 $this->adapters[xxx] 的 xxx 单独取变量
-                if($platform == 'shunfeng_1v1'){ //特殊处理 -- 专人直送的订单,使用顺丰平台适配器
-                    $adapterKey = 'shunfeng';
+                if (substr($platform, -4) === '_1v1') { //特殊处理 -- 专人直送的订单,使用对应平台适配器
+                    $adapterKey = substr($platform, 0, -4);
                 }
                 $adapter = $this->adapters[$adapterKey];
                 $quote = null;
@@ -768,7 +810,7 @@ class DispatchService
                         Yii::warning("[DispatchService] {$platform} 报价返回错误: {$errorMsg}");
                     }
                 } else {
-                    // 蜂鸟和闪送的响应处理
+                    // 响应处理
                     $quote = $adapter->processPriceResponse($response);
                     if ($quote && !isset($quote['error'])) {
                         $quote['_duration'] = $totalDuration;
@@ -980,7 +1022,7 @@ class DispatchService
                     break;
                 case 'dada':
                     $list = [
-                        'name' => '达达',
+                        'name' => '达达'. ($platformKey == 'dada_1v1' ? ' (专送)' : ''),
                         'en_name' => 'dada',
                         'price' => $item['fee'],
                         'distance' => $item['distance'],

+ 2 - 2
common/components/delivery/services/adapter/DadaAdapter.php

@@ -302,7 +302,7 @@ class DadaAdapter extends Dada implements Adapter
     }
 
     /**
-     * 重新下单
+     * 重新下单 -- /api/order/reAddOrder
      * 同一个商户的单号(根据三方单号识别)如果已经在达达侧创建了订单,在该订单被取消(已取消=5)、拒收返还(妥投异常物品返还完成=10)后,可以调用该接口重发订单,达达将根据传入的配送信息重新计算运费并创建达达物流订单。
      * @param string $deliveryNo
      * @param string $orderId 第三方订单ID
@@ -627,7 +627,7 @@ class DadaAdapter extends Dada implements Adapter
             // 预约发单时间(unix时间戳(10位),精确到分),可以支持未来3天内的订单发预约单,如预约发单时间早于当前时间会作为即时单下发。
             $apiData['delay_publish_time'] = $ts;
             $apiData['is_expect_finish_order'] = 1; // 是否根据期望送达时间预约发单(0-否,即时发单;1-是,预约发单),如传1则期望送达时间必传。
-            $apiData['expect_finish_time_limit'] = $ts; // 期望送达时间(单位秒,不早于当前时间)
+            $apiData['expect_finish_time_limit'] = $ts + 3000; // 期望送达时间(单位秒)== 预约发单时间 + 50分钟
         }
 
         // 添加可选参数

+ 14 - 28
common/components/delivery/services/adapter/DidiAdapter.php

@@ -30,12 +30,11 @@ class DidiAdapter extends Didi implements Adapter
 
     /**
      * 构建订单计费请求信息(供并发请求使用)
-     * 
      * @param array $data 订单数据
-     * @param string $orderTime 配送时间
+     * @param string $pickupTime 取件时间
      * @return array 包含 url, data, headers, timeout 的请求信息
      */
-    public function buildPriceRequest($data, $orderTime)
+    public function buildPriceRequest($data, $pickupTime)
     {
         $sender = $data['sender'];
         // 滴滴目前主要支持单点对单点,取第一个收件人
@@ -73,32 +72,23 @@ class DidiAdapter extends Didi implements Adapter
             ],
             // 普通货运业务类型
             [
-                'bizType' => 0,     // 业务类型 -- 普通货运
-                'carType' => 1012,   // 车型 -- 微面
-                'serviceLevel' => 0, // 服服务等级
+                'bizType' => 0,      // 业务类型 -- 普通货运
+                'carType' => 1020,   // 车型 -- 三轮车
+                'serviceLevel' => 0,
                 //'serviceType' => 1,
             ],
             [
-                'bizType' => 0,     // 业务类型 -- 普通货运
-                'carType' => 1001,   // 车型 -- 小面包
-                'serviceLevel' => 0, // 服服务等级
+                'bizType' => 0,      // 业务类型 -- 普通货运
+                'carType' => 1012,   // 车型 -- 微面
+                'serviceLevel' => 0,
                 //'serviceType' => 1,
             ],
-        ];
-        // 如果是预约单,则使用以下 拉货(普通货运)
-        $laHuo_serviceCategoryList = [
             [
                 'bizType' => 0,      // 业务类型 -- 普通货运
-                'carType' => 1020,   // 车型 -- 三轮车
-                'serviceLevel' => 0, // 服服务等级
-                //'serviceType' => 1,  // 服务类型(快送专用,拉货不需要传)
-            ],
-            [
-                'bizType' => 0,
-                'carType' => 1012,   // 微面
+                'carType' => 1001,   // 车型 -- 小面包
                 'serviceLevel' => 0,
                 //'serviceType' => 1,
-            ]
+            ],
         ];
         //$ret = $this->queryCityInfo(null,  (float)$receiver['toLongitude'], (float)$receiver['toLatitude']);
 
@@ -136,15 +126,11 @@ class DidiAdapter extends Didi implements Adapter
         // 预约单处理
         $modeType = 1; // 1-实时单
         $useTime = null;
-        if ($orderTime) {
-            $ts = strtotime($orderTime);
-            if ($ts > (time() + 300)) { // 5分钟后算预约? 滴滴快送暂不支持预约单,这里保留逻辑备用
+        if ($pickupTime) {
+            $ts = strtotime($pickupTime);
+            if ($ts > (time() + 300)) { // 5分钟后算预约? 滴滴快送暂不支持预约单,只能应用于普通货运
                 $modeType = 2;// 变更为2-预约单
                 $useTime = $ts * 1000;
-                // 注意:文档说“快送暂不支持预约单都按照实时单处理”,所以这里可能无需处理,或者传了也没用
-
-                //从快送 变更 拉货
-                //$serviceCategoryList = $laHuo_serviceCategoryList;
             }
         }
 
@@ -157,7 +143,7 @@ class DidiAdapter extends Didi implements Adapter
             'goodsType' => $goodsType,
             'weight' => $weightGram,
             'needPickupCode' => false,
-            'needReceiveCode' => true, // 通常需要收件码
+            'needReceiveCode' => false,
             'modeType' => $modeType,
         ];
 

+ 11 - 11
common/components/delivery/services/adapter/HuolalaAdapter.php

@@ -341,14 +341,14 @@ class HuolalaAdapter extends Huolala implements Adapter
             if (!empty($vehicle['addr_info']) && is_array($vehicle['addr_info'])) {
                 foreach ($vehicle['addr_info'] as $addr) {
                     $addrInfo[] = [
-                        'name' => $addr['name'] ?? '',
-                        'addr' => $addr['addr'] ?? '',
-                        'city_id' => (int)($addr['city_id'] ?? 0),
-                        'city_name' => $addr['city_name'] ?? '',
-                        'district_name' => $addr['district_name'] ?? '',
-                        'house_number' => $addr['house_number'] ?? '',
-                        'contacts_name' => $addr['contacts_name'] ?? '',
-                        'contacts_phone_no' => $addr['contacts_phone_no'] ?? '',
+                        'name' => $addr['name'],
+                        'addr' => $addr['addr'],
+                        'city_id' => (int)$addr['city_id'],
+                        'city_name' => $addr['city_name'],
+                        'district_name' => $addr['district_name'],
+                        'house_number' => $addr['house_number'],
+                        'contacts_name' => $addr['contacts_name'],
+                        'contacts_phone_no' => $addr['contacts_phone_no'],
                         'lat_lon' => [
                             'lat' => (double)($addr['lat_lon']['lat'] ?? 0),
                             'lon' => (double)($addr['lat_lon']['lon'] ?? 0),
@@ -358,9 +358,9 @@ class HuolalaAdapter extends Huolala implements Adapter
             }
             
             $apiData = [
-                'city_id' => (int)($data['city_id'] ?? 0),
-                'order_vehicle_id' => (int)($vehicle['order_vehicle_id'] ?? 0),
-                'city_info_revision' => (int)($vehicle['city_info_revision'] ?? 0),
+                'city_id' => (int)$data['city_id'],
+                'order_vehicle_id' => (int)$vehicle['order_vehicle_id'],
+                'city_info_revision' => (int)$vehicle['city_info_revision'],
                 'order_time' => $orderTime,
                 'addr_info' => $addrInfo,
             ];

+ 12 - 13
common/components/delivery/services/adapter/ShansongAdapter.php

@@ -96,18 +96,17 @@ class ShansongAdapter extends Shansong implements Adapter
 
     /**
      * 构建订单计费请求信息(供并发请求使用)
-     * 
      * @param array $data 订单数据
-     * @param string $orderTime 配送时间
+     * @param string $pickupTime 取件时间
      * @return array 包含 url, data, headers, timeout 的请求信息
      */
-    public function buildPriceRequest($data, $orderTime)
+    public function buildPriceRequest($data, $pickupTime)
     {
         // 预约类型
-        $timeDiff = strtotime($orderTime) - time();
-        $appointType = 0;
+        $timeDiff = strtotime($pickupTime) - time();
+        $appointType = 0; //0立即单
         if ($timeDiff > 3600 ) {
-            $appointType = 1;
+            $appointType = 1; //1预约单
         }
 
         // 构建发件人信息
@@ -126,12 +125,12 @@ class ShansongAdapter extends Shansong implements Adapter
         foreach ($data['receiverList'] as $receiver) {
             $receiverList[] = [
                 'orderNo' => $receiver['orderNo'],
-                'toAddress' => $receiver['toAddress'] ?? '',
-                'toAddressDetail' => $receiver['toAddressDetail'] ?? '',
-                'toReceiverName' => $receiver['toReceiverName'] ?? '',
-                'toMobile' => $receiver['toMobile'] ?? '',
-                'toLatitude' => $receiver['toLatitude'] ?? '',
-                'toLongitude' => $receiver['toLongitude'] ?? '',
+                'toAddress' => $receiver['toAddress'],
+                'toAddressDetail' => $receiver['toAddressDetail'],
+                'toReceiverName' => $receiver['toReceiverName'],
+                'toMobile' => $receiver['toMobile'],
+                'toLatitude' => $receiver['toLatitude'],
+                'toLongitude' => $receiver['toLongitude'],
                 'goodType' => $receiver['goodType'],  // 物品类型,默认10-其他
                 'weight' => (int) ($receiver['weight'] ?? 1),         // 物品重量(必须是整数kg)
                 'remarks' => $receiver['remarks'] ?? '', // 备注
@@ -164,7 +163,7 @@ class ShansongAdapter extends Shansong implements Adapter
         // 如果有预约日期,添加到请求体
         if ($appointType == 1) {
             // yyyy-MM-dd HH:mm格式(例如:2020-02-02 22:00),指的是预约取件时间,只支持一个小时以后两天以内
-            $requestData['appointmentDate'] = $orderTime;
+            $requestData['appointmentDate'] = $pickupTime;
         }
         $requestData = $this->filterEmptyValues($requestData);
         $requestData = json_encode($requestData, JSON_UNESCAPED_UNICODE);

+ 12 - 7
common/components/delivery/services/adapter/ShunfengAdapter.php

@@ -412,7 +412,7 @@ class ShunfengAdapter extends Shunfeng implements Adapter
                 // 还有其它返回值没加进来
             ];
         } else {
-            Yii::error('shunfeng 报价失败:'.json_encode($resp));
+            Yii::error('shunfeng 报价失败:'.json_encode($resp, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
             return [
                 'platform' => 'shunfeng',
                 'error' => isset($resp['error_msg']) ? $resp['error_msg'] : '',
@@ -422,16 +422,21 @@ class ShunfengAdapter extends Shunfeng implements Adapter
 
     /**
      * 构建运费报价请求信息(供并发请求使用)
-     *
      * @param array $data 报价参数
-     * @param string $orderTime 配送时间
+     * @param string $pickupTime 取件时间
      * @return array|null 包含 url, payload, headers, timeout 的请求信息
      */
-    public function buildPriceRequest($data, $orderTime)
+    public function buildPriceRequest($data, $pickupTime)
     {
         unset($data['platform']);
-        // 根据 $orderTime ,调整 $data 中的部分参数
-
+        
+        // 根据 $pickupTime ,调整 $data 中的部分参数
+        $timestamp = strtotime($pickupTime);
+        if($timestamp > time() + 300){ // 5分钟后算预约单
+            $data['is_appoint'] = 1; // 是否预约单(0:非预约单;1:预约单)-- 修改为预约单
+            $data['appoint_type'] = 2; // 预约类型 -- 1:预约送达单;2:预约上门单 (预约单的时候传入,即时单的时候不传)
+            $data['expect_pickup_time'] = $timestamp; // 期望取件时间(秒级时间戳)
+        }
 
         // 构建签名用的完整payload
         $payload = [
@@ -451,7 +456,7 @@ class ShunfengAdapter extends Shunfeng implements Adapter
             'timeout' => 10.0,
         ];
 
-        Yii::info('顺丰构建运费报价请求信息 -- ' .json_encode($request, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
+        // Yii::info('顺丰构建运费报价请求信息 -- ' .json_encode($request, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
         return $request;
     }