ソースを参照

1. 顺丰同城平台接入
2. 货拉拉创建订单时,添加订单消息回调地址

shizhongqi 8 ヶ月 前
コミット
e3d0b34b75

+ 69 - 20
app-ghs/controllers/DeliveryController.php

@@ -9,6 +9,7 @@ use common\components\delivery\services\adapter\HuolalaAdapter;
 use common\components\util;
 use common\components\util;
 use common\components\delivery\services\DispatchService;
 use common\components\delivery\services\DispatchService;
 use ghs\models\delivery\CancelOrderForm;
 use ghs\models\delivery\CancelOrderForm;
+use ghs\models\delivery\CreateOrderForm;
 use ghs\models\delivery\OrderDetailForm;
 use ghs\models\delivery\OrderDetailForm;
 use Yii;
 use Yii;
 
 
@@ -19,7 +20,8 @@ use Yii;
  */
  */
 class DeliveryController extends BaseController
 class DeliveryController extends BaseController
 {
 {
-    public $guestAccess = ['callback', 'shansong-auth-callback', 'huolala-auth-callback', 'fengniao-callback', 'shansong-callback'];
+    public $guestAccess = ['callback', 'shansong-auth-callback', 'huolala-auth-callback',
+        'fengniao-callback', 'shansong-callback', 'huolala-callback'];
 
 
     public function actionList()
     public function actionList()
     {
     {
@@ -48,10 +50,6 @@ class DeliveryController extends BaseController
             $platformList[] = $item;
             $platformList[] = $item;
         }
         }
         util::success(['platformList'=>$platformList]);
         util::success(['platformList'=>$platformList]);
-//        [
-//            { id: 'dada', name: '达达', logo: '📦', is_authorized: true, balance: 178.0, access_type: 'oauth' },
-//            { id: 'sf-tongcheng', name: '顺丰同城', logo: '🚚', is_authorized: true, balance: 174.0, access_type: 'oauth' },
-//        ];
     }
     }
 
 
     public function actionGetAuthUrl()
     public function actionGetAuthUrl()
@@ -71,8 +69,9 @@ class DeliveryController extends BaseController
                 return $this->actionDadaAuth();
                 return $this->actionDadaAuth();
                 break;
                 break;
             case 'shunfeng':
             case 'shunfeng':
+                return $this->actionShunfengAuth();
                 break;
                 break;
-            case '':
+            case 'didi':
                 break;
                 break;
             default:
             default:
                 util::error(-1, $platform . '不存在');
                 util::error(-1, $platform . '不存在');
@@ -112,7 +111,6 @@ class DeliveryController extends BaseController
         $redirectUrl = 'https://api.shop.hzghd.com/delivery/fengniao-auth-callback' . '?mainId=' . $mainId;
         $redirectUrl = 'https://api.shop.hzghd.com/delivery/fengniao-auth-callback' . '?mainId=' . $mainId;
         $authUrl = $fengnaioAuth->generateAuthUrl($redirectUrl);
         $authUrl = $fengnaioAuth->generateAuthUrl($redirectUrl);
 
 
-        //header('Location: ' . $authUrl);
         util::success(['url'=>$authUrl]);
         util::success(['url'=>$authUrl]);
     }
     }
 
 
@@ -135,15 +133,14 @@ class DeliveryController extends BaseController
         $shunfengAuth = new \common\components\delivery\platform\shunfeng\Auth();
         $shunfengAuth = new \common\components\delivery\platform\shunfeng\Auth();
 
 
         $mainId = $this->mainId;
         $mainId = $this->mainId;
-        $redirectUrl = 'https://api.shop.hzghd.com/delivery/dada-auth-callback' . '?mainId=' . $mainId;
-        $shopNumber = '40d8391f9b05477b';
-        $state = 'huidiao_biaoshi';
-        $authUrl = $shunfengAuth->generateAuthUrl($state, $shopNumber);
+        $authUrl = $shunfengAuth->generateAuthUrl($mainId);
         util::success(['url'=>$authUrl]);
         util::success(['url'=>$authUrl]);
     }
     }
 
 
     public function actionCancelAuth()
     public function actionCancelAuth()
     {
     {
+        // 有平台有对应的取消授权接口,有的没有。
+        // 有的就对接下,然后再删除数据。没有的直接删除数据就行。
         $platform = Yii::$app->request->get('platform');
         $platform = Yii::$app->request->get('platform');
         $ret = DeliveryAuthTokenClass::deleteByCondition(['mainId'=>$this->mainId, 'platform'=>$platform]);
         $ret = DeliveryAuthTokenClass::deleteByCondition(['mainId'=>$this->mainId, 'platform'=>$platform]);
         if($ret > 0) {
         if($ret > 0) {
@@ -153,6 +150,8 @@ class DeliveryController extends BaseController
         }
         }
     }
     }
 
 
+
+    // --------------------------------------------------------------------------------------------------------------
     public function actionHuolalaVehicle()
     public function actionHuolalaVehicle()
     {
     {
         $mainId = $this->mainId;
         $mainId = $this->mainId;
@@ -256,6 +255,15 @@ class DeliveryController extends BaseController
         $deliveryList = [];
         $deliveryList = [];
         foreach ($platforms['quotes'] as $item) {
         foreach ($platforms['quotes'] as $item) {
             switch ($item['platform']) {
             switch ($item['platform']) {
+                case 'shunfeng':
+                    $deliveryList[] = [
+                        'name' => '顺丰',
+                        'en_name' => 'shunfeng',
+                        'price' => $item['real_pay_money'],
+                        'distance' => $item['distance'],
+                        'type' => ''
+                    ];
+                    break;
                 case 'shansong':
                 case 'shansong':
                     $deliveryList[] = [
                     $deliveryList[] = [
                         'name' => '闪送',
                         'name' => '闪送',
@@ -322,11 +330,11 @@ class DeliveryController extends BaseController
     // 创建订单(真实下单)
     // 创建订单(真实下单)
     public function actionCreateOrder()
     public function actionCreateOrder()
     {
     {
-//        $form = new CreateOrderForm();
-//        $form->loadAndValidate();
-//
-//        $post = $form->getAttributes();
-        $post = Yii::$app->request->post();
+        $form = new CreateOrderForm();
+        $form->loadAndValidate();
+
+        $post = $form->getAttributes();
+        //$post = Yii::$app->request->post();
         $orderId = $post['orderId']; // xhGhsOrder.id
         $orderId = $post['orderId']; // xhGhsOrder.id
         $params = $post['allParams'];
         $params = $post['allParams'];
         $platform = $params['en_name'];// $post['platform'] 是中文
         $platform = $params['en_name'];// $post['platform'] 是中文
@@ -343,10 +351,10 @@ class DeliveryController extends BaseController
         // if ($ghsOrder['status'] != 2) {
         // if ($ghsOrder['status'] != 2) {
         //     util::fail('订单状态不是待配送');
         //     util::fail('订单状态不是待配送');
         // // }
         // // }
-        // if(!in_array($ghsOrder['sendStatus'], [-4, -1])) {
-        //     Yii::error('订单已经发过跑腿');
-        //     util::fail('订单已经发过跑腿');
-        // }
+         if(!in_array($ghsOrder['sendStatus'], [-4, -1])) {
+             Yii::error('订单已经发过跑腿');
+             util::fail('订单已经发过跑腿');
+         }
 
 
         $ds = new DispatchService($this->mainId, $platform);
         $ds = new DispatchService($this->mainId, $platform);
         $ret = $ds->createOrder($ghsOrder, $shopId, $params);
         $ret = $ds->createOrder($ghsOrder, $shopId, $params);
@@ -489,6 +497,8 @@ class DeliveryController extends BaseController
         util::success($ret);
         util::success($ret);
     }
     }
 
 
+
+    // ---------------------------------------------------- 普通回调接口 ----------------------------------------------------------
     // 第三方回调入口
     // 第三方回调入口
     /**
     /**
      * 回调接口
      * 回调接口
@@ -572,6 +582,25 @@ class DeliveryController extends BaseController
         }
         }
     }
     }
 
 
+    /**
+     * 货拉拉回调接口
+     */
+    public function actionHuolalaCallback()
+    {
+
+    }
+
+    /**
+     * 顺丰回调接口
+     */
+    public function actionShunfengCallback()
+    {
+        // 1.配送状态更改  2.订单完成  3.顺丰原因取消  4.订单配送异常  5.骑士撤单  6.自动建店回调
+
+    }
+
+
+    // ---------------------------------------------------- 授权回调接口 ----------------------------------------------------------
     /**
     /**
      * 闪送授权回调接口
      * 闪送授权回调接口
      */
      */
@@ -725,4 +754,24 @@ class DeliveryController extends BaseController
         }
         }
         Yii::info('达达授权回调:' . json_encode($callbackData, JSON_UNESCAPED_UNICODE), 'dada');
         Yii::info('达达授权回调:' . json_encode($callbackData, JSON_UNESCAPED_UNICODE), 'dada');
     }
     }
+
+    /**
+     * 顺丰授权与取消授权回调接口
+     */
+    public function actionShunfengAuthCallback()
+    {
+        $get = Yii::$app->request->get();
+        $getData = json_encode($get, JSON_UNESCAPED_UNICODE);
+        Yii::info($getData);
+
+        $callbackData = Yii::$app->request->post();
+        if (empty($callbackData)) {
+            $postStr = file_get_contents('php://input');
+            $callbackData = json_decode($postStr, true);
+            if (empty($callbackData)) {
+                Yii::info('回调请求的数据为空');
+            }
+        }
+        Yii::info('顺丰授权与取消授权回调接口:' . json_encode($callbackData, JSON_UNESCAPED_UNICODE), 'dada');
+    }
 }
 }

+ 0 - 1
common/components/delivery/helpers/HttpClient.php

@@ -40,7 +40,6 @@ class HttpClient
 
 
                 $status = $response->getStatusCode();
                 $status = $response->getStatusCode();
                 $respBody = (string) $response->getBody();
                 $respBody = (string) $response->getBody();
-
                 Yii::info("[HttpClient] POST {$url} {$status} {$respBody}");
                 Yii::info("[HttpClient] POST {$url} {$status} {$respBody}");
 
 
                 $json = json_decode($respBody, true);
                 $json = json_decode($respBody, true);

+ 197 - 0
common/components/delivery/platform/shunfeng/Auth.php

@@ -6,5 +6,202 @@ use Yii;
 
 
 class Auth
 class Auth
 {
 {
+    // 授权端点
+    const AUTHORIZE_URL = 'https://openic.sf-express.com/artascope/cx/receipt/getpage/product/artascope/page/storeBinding';
+    const TOKEN_URL = '';
 
 
+    protected $appKey;//appId
+    protected $appSecret;
+    protected $redirectUri;
+    protected $isSandbox;
+
+    /**
+     * 初始化授权类
+     * 根据环境获取配置信息
+     */
+    public function __construct()
+    {
+        $isProduction = getenv('YII_ENV') == 'production';
+
+        if ($isProduction) {
+            $this->appKey = '1741530942';
+            $this->appSecret = '07b3f83d19a4a9f89322976c936faf92';
+        } else {
+            $this->appKey = '1741530942';
+            $this->appSecret = '07b3f83d19a4a9f89322976c936faf92';
+        }
+
+        // 根据环境设置沙箱标志
+        $this->isSandbox = !$isProduction;
+    }
+
+    /**
+     * 设置重定向URI
+     * 需要进行URLEncode编码
+     *
+     * @param string $redirectUri 重定向地址
+     */
+    public function setRedirectUri($redirectUri)
+    {
+        $this->redirectUri = $redirectUri;
+        return $this;
+    }
+
+    /**
+     * 生成授权URL
+     * 用户需要通过浏览器访问此URL进行授权
+     *
+     * @param string $redirectUri 重定向地址(回调地址)
+     * @return string 授权URL
+     */
+    public function generateAuthUrl($mainId)
+    {
+        $params = [
+            'dev_id' => $this->appKey,
+            'out_shop_id' => $mainId,
+        ];
+
+        // 构建URL
+        $queryString = http_build_query($params);
+        return self::AUTHORIZE_URL . '?' . $queryString;
+    }
+
+    /**
+     * 获取AccessToken
+     * 使用授权码换取令牌
+     *
+     * 文档:/oauth/token (GET请求)
+     * 入参:grant_type, client_id, code, isSandbox
+     * 出参:access_token, refresh_token, expires_in
+     *
+     * @param string $code 授权码(来自授权页面重定向)
+     * @return array 返回格式:['success' => true/false, 'access_token' => '', 'refresh_token' => '', 'expires_in' => 0, 'error' => '']
+     */
+    public function getAccessToken($code)
+    {
+        $params = [
+            'grant_type' => '',
+            'client_id' => $this->appKey,
+            'code' => $code,
+            'isSandbox' => $this->isSandbox ? 'true' : 'false',
+        ];
+
+        $response = HttpClient::get(self::TOKEN_URL, $params);
+
+        Yii::info("[HuoLalaAuthGetToken] Response: " . json_encode($response));
+
+        return $this->parseResponse($response);
+    }
+
+    /**
+     * 刷新AccessToken
+     * 使用刷新令牌获取新的AccessToken
+     *
+     * 文档:/oauth/token (GET请求)
+     * 入参:grant_type, client_id, refresh_token, isSandbox
+     * 出参:access_token, refresh_token, expires_in
+     *
+     * @param string $refreshToken 刷新令牌
+     * @return array 返回格式:['success' => true/false, 'access_token' => '', 'refresh_token' => '', 'expires_in' => 0, 'error' => '']
+     */
+    public function refreshAccessToken($refreshToken)
+    {
+        $params = [
+            'grant_type' => '',
+            'client_id' => $this->appKey,
+            'refresh_token' => $refreshToken,
+            'isSandbox' => $this->isSandbox ? 'true' : 'false',
+        ];
+        $response = HttpClient::get(self::TOKEN_URL, $params);
+        Yii::info("[HuoLalaAuthRefreshToken] Response: " . json_encode($response));
+        return $this->parseResponse($response);
+    }
+
+    /**
+     * 解析API响应
+     *
+     * 货拉拉 API 返回格式:
+     * {
+     *     "ret": 0,
+     *     "msg": "success",
+     *     "data": {
+     *         "auth_mobile": "17168665598",
+     *         "access_token": "a3207b6b5ad04249ad1dbf6a9824dbea",
+     *         "refresh_token": "4ccbbab0e9e443159c518d1d10741ad",
+     *         "auth_end_time": "2020-06-07 15:35:08"
+     *     }
+     * }
+     *
+     * @param array $response HTTP响应
+     * @return array 标准化的响应格式
+     */
+    protected function parseResponse($response)
+    {
+        // 检查 ret 字段(0 表示成功)
+        if (!isset($response['ret'])) {
+            return [
+                'success' => false,
+                'error' => $response['msg'] ?? '响应格式错误,缺少 ret 字段',
+            ];
+        }
+
+        // 如果 ret 不等于 0,表示请求失败
+        if ($response['ret'] != 0) {
+            return [
+                'success' => false,
+                'error' => $response['msg'] ?? 'API返回异常',
+                'ret' => $response['ret'],
+            ];
+        }
+
+        // 提取 data 字段中的数据
+        $data = $response['data'] ?? [];
+
+        // 检查是否包含必需的token信息
+        if (empty($data['access_token'])) {
+            return [
+                'success' => false,
+                'error' => $response['msg'] ?? '未获取到 access_token',
+            ];
+        }
+
+        // 成功响应
+        return [
+            'success' => true,
+            'access_token' => $data['access_token'] ?? '',
+            'refresh_token' => $data['refresh_token'] ?? '',
+            'auth_mobile' => $data['auth_mobile'] ?? '',
+            'auth_end_time' => $data['auth_end_time'] ?? '',
+        ];
+    }
+
+    /**
+     * 获取AppKey
+     *
+     * @return string
+     */
+    public function getAppKey()
+    {
+        return $this->appKey;
+    }
+
+    /**
+     * 获取AppSecret
+     *
+     * @return string
+     */
+    public function getAppSecret()
+    {
+        return $this->appSecret;
+    }
+
+    /**
+     * 获取是否为沙箱环境
+     *
+     * @return bool
+     */
+    public function isSandbox()
+    {
+        return $this->isSandbox;
+    }
 }
 }

+ 79 - 2
common/components/delivery/services/DispatchService.php

@@ -7,7 +7,7 @@ use bizGhs\express\classes\DeliveryAuthTokenClass;
 use bizGhs\order\classes\OrderClass;
 use bizGhs\order\classes\OrderClass;
 use bizGhs\order\classes\OrderItemClass;
 use bizGhs\order\classes\OrderItemClass;
 use common\components\delivery\helpers\HttpClient;
 use common\components\delivery\helpers\HttpClient;
-use common\components\delivery\services\adapter\{ ShansongAdapter,  HuolalaAdapter, FengniaoAdapter}; // MeituanAdapter, DadaAdapter, SFAdapter, UUAdapter,
+use common\components\delivery\services\adapter\{ ShansongAdapter,  HuolalaAdapter, FengniaoAdapter, ShunfengAdapter}; // MeituanAdapter, DadaAdapter, SFAdapter, UUAdapter,
 use common\components\delivery\models\{DeliveryOrder, DeliveryAccount};
 use common\components\delivery\models\{DeliveryOrder, DeliveryAccount};
 use Yii;
 use Yii;
 
 
@@ -38,6 +38,9 @@ class DispatchService
                         $adapter->setMerchantId(14594092); // TODO: 确认是否需要使用动态的商户ID
                         $adapter->setMerchantId(14594092); // TODO: 确认是否需要使用动态的商户ID
                         $this->adapters['fengniao'] = $adapter;
                         $this->adapters['fengniao'] = $adapter;
                         break;
                         break;
+                    case 'shunfeng':
+                        $this->adapters['shunfeng'] = new ShunfengAdapter($pt['accessToken'], $pt['shopId']);
+                        break;
                 }
                 }
             }
             }
         } else {
         } else {
@@ -55,6 +58,9 @@ class DispatchService
                     $adapter->setMerchantId(14594092); // TODO: 确认是否需要使用动态的商户ID
                     $adapter->setMerchantId(14594092); // TODO: 确认是否需要使用动态的商户ID
                     $this->adapters['fengniao'] = $adapter;
                     $this->adapters['fengniao'] = $adapter;
                     break;
                     break;
+                case 'shunfeng':
+                    $this->adapters['shunfeng'] = new ShunfengAdapter($authPlatform['accessToken'], $authPlatform['shopId']);
+                    break;
             }
             }
             $this->platformName = $platform;
             $this->platformName = $platform;
         }
         }
@@ -74,7 +80,7 @@ class DispatchService
     public function createOrder($order, $shopId, $params)
     public function createOrder($order, $shopId, $params)
     {
     {
         $order = $order->toArray();
         $order = $order->toArray();
-        $shop = ShopClass::getById($shopId);
+        $shop = ShopClass::getById($shopId, false, 'id,merchantName,fullAddress,lat,long,city,dist,floor,mobile,merchantName,mobile');
 
 
         $adapter = $this->getAdapter();
         $adapter = $this->getAdapter();
         $orderData = $adapter->formatOrderData($order, $shop, $params);
         $orderData = $adapter->formatOrderData($order, $shop, $params);
@@ -184,6 +190,15 @@ class DispatchService
             }
             }
         }
         }
 
 
+        // 准备顺丰订单数据
+        if (isset($this->adapters['shunfeng'])) {
+            try {
+                $preparedData['shunfeng'] = $this->prepareShunfengData($order, $shop);
+            } catch (\Exception $e) {
+                Yii::warning("Shunfeng 数据准备失败: {$e->getMessage()}");
+            }
+        }
+
         return $preparedData;
         return $preparedData;
     }
     }
 
 
@@ -350,6 +365,68 @@ class DispatchService
         ];
         ];
     }
     }
 
 
+    /**
+     * 准备顺丰订单数据
+     */
+    private function prepareShunfengData($order, $shop)
+    {
+        // 获取订单商品信息
+        $itemInfos = OrderItemClass::getAllByCondition(['orderSn' => $order['orderSn']], null, 'id, name, unitPrice, num, unitWeight');
+        
+        // 计算商品总重量(单位:克)
+        $totalWeight = 0;
+        foreach ($itemInfos as $item) {
+            $totalWeight += (int)($item['unitWeight'] * $item['num']) * 1000;
+        }
+        
+        // 如果没有商品重量,使用订单重量
+        if ($totalWeight == 0 && isset($order['weight'])) {
+            $totalWeight = (int)$order['weight'] * 1000;
+        }
+        
+        // 默认重量为1000克(1公斤)
+        if ($totalWeight == 0) {
+            $totalWeight = 1000;
+        }
+
+        return [
+            'platform' => 'shunfeng',
+            'user_lng' => (string)$order['long'],                    // 用户地址经度
+            'user_lat' => (string)$order['lat'],                     // 用户地址纬度
+            'user_address' => $order['fullAddress'],                 // 用户详细地址
+            'weight' => $totalWeight,                                // 物品重量(单位:克)
+            'product_type' => 4,                                     // 物品类型(4:鲜花绿植)
+            'push_time' => time(),                                   // 推单时间(秒级时间戳)
+            'shop' => [                                              // 发货店铺信息
+                'shop_name' => $shop['merchantName'],                    // 店铺名称
+                'shop_address' => $shop['address'],                  // 店铺地址
+                'shop_lng' => (string)$shop['long'],                 // 店铺经度
+                'shop_lat' => (string)$shop['lat'],                  // 店铺纬度
+                'shop_phone' => $shop['mobile'],                     // 店铺联系电话
+            ],
+            //-------------------------- 非必填 -------------------------------
+            'city_name' => $order['city'],                          // 发单城市
+            //'order_source' => '6',                                   // 订单接入来源(6:京东秒送,可自定义)
+            'source_origin_order_id' => $order['orderSn'],          // 商流订单号
+            'total_price' => (int)($order['actPrice'] * 100),       // 用户订单总金额(单位:分)
+            'is_appoint' => 0,                                       // 是否预约单(0:非预约单)
+            'appoint_type' => 0,                                     // TODO 预约单类型(0:立即单)
+            'expect_time' => 0,                                      // TODO 用户期望送达时间
+            //'expect_pickup_time' => 0,                               // TODO 用户期望上门时间 -- appoint_type=2时需必传,秒级时间戳
+            'shop_expect_time' => 0,                                // TODO 商家期望送达时间 -- 格式为:{timestamp},
+            'lbs_type' => 2,                                         // 坐标类型(2:高德坐标)
+            'is_insured' => 0,                                       // 是否保价(0:非保价)
+            'is_person_direct' => 0,                                 // 是否专人直送(0:否)
+            'vehicle' => 0,                                          // 配送交通工具(0:否)
+            //'four_wheeler_type' => 0,                                // 车型 -- 只有vehicle = 2,⻋型字段才有效。
+            'declared_value' => 0,                                   // 保价金额(单位:分)
+            'gratuity_fee' => 0,                                     // 订单小费(单位:分)
+            'rider_pick_method' => 1,                                // 物流流向(1:从门店取件送至用户)
+            'return_flag' => 511,                                    // 返回字段控制标志位(511:全部返回)
+            'multi_pickup_info' => [],                               // 多点取货信息(暂不使用)
+        ];
+    }
+
     /**
     /**
      * 并发获取各平台报价(核心实现)
      * 并发获取各平台报价(核心实现)
      * 
      * 

+ 10 - 0
common/components/delivery/services/adapter/HuolalaAdapter.php

@@ -17,6 +17,10 @@ class HuolalaAdapter implements Adapter
     protected $appKey;
     protected $appKey;
     protected $accessToken;
     protected $accessToken;
     protected $apiVersion = '1.0';
     protected $apiVersion = '1.0';
+    /**
+     * @var string 订单消息回调地址
+     */
+    protected $notifyUrl;
 
 
     public function __construct($accessToken)
     public function __construct($accessToken)
     {
     {
@@ -25,18 +29,21 @@ class HuolalaAdapter implements Adapter
                 'base_url' => 'https://openapi.huolala.cn/v1',
                 'base_url' => 'https://openapi.huolala.cn/v1',
                 'app_key' => '8S2uZQrWwM1mkG2Cj576PsMiNIe25UNT',
                 'app_key' => '8S2uZQrWwM1mkG2Cj576PsMiNIe25UNT',
                 'secret' => 'olzor07UgmUWaagahNGQAKQKXRtvhK8j',
                 'secret' => 'olzor07UgmUWaagahNGQAKQKXRtvhK8j',
+                'notify_url' => 'https://api.shop.wixhb.com/delivery/huolala-callback',
             ];
             ];
         } else {
         } else {
             $cfg = [
             $cfg = [
                 'base_url' => 'https://openapi-pre.huolala.cn/v1',
                 'base_url' => 'https://openapi-pre.huolala.cn/v1',
                 'app_key' => '8S2uZQrWwM1mkG2Cj576PsMiNIe25UNT',
                 'app_key' => '8S2uZQrWwM1mkG2Cj576PsMiNIe25UNT',
                 'secret' => 'olzor07UgmUWaagahNGQAKQKXRtvhK8j',
                 'secret' => 'olzor07UgmUWaagahNGQAKQKXRtvhK8j',
+                'notify_url' => 'https://api.shop.hzghd.com/delivery/huolala-callback',
             ];
             ];
         }
         }
         $this->baseUrl = $cfg['base_url'];
         $this->baseUrl = $cfg['base_url'];
         $this->appSecret = $cfg['secret'];
         $this->appSecret = $cfg['secret'];
         $this->appKey = $cfg['app_key'];
         $this->appKey = $cfg['app_key'];
         $this->accessToken = $accessToken;
         $this->accessToken = $accessToken;
+        $this->notifyUrl = $cfg['notify_url'];
     }
     }
 
 
     /**
     /**
@@ -284,6 +291,9 @@ class HuolalaAdapter implements Adapter
             'commodity_info' => $params['commodity_info'] //商品信息(直接透传u-price-multiCalculate返回的commodity_item字段)
             'commodity_info' => $params['commodity_info'] //商品信息(直接透传u-price-multiCalculate返回的commodity_item字段)
         ];
         ];
 
 
+        // 添加订单消息回调地址
+        $apiData['notify_url'] = $this->notifyUrl;
+
         return $apiData;
         return $apiData;
     }
     }
 
 

+ 284 - 0
common/components/delivery/services/adapter/ShunfengAdapter.php

@@ -2,20 +2,304 @@
 namespace common\components\delivery\services\adapter;
 namespace common\components\delivery\services\adapter;
 
 
 
 
+use bizGhs\order\classes\OrderItemClass;
+use common\components\delivery\helpers\HttpClient;
+use Yii;
+
 class ShunfengAdapter
 class ShunfengAdapter
 {
 {
+    protected $baseUrl;
+    protected $appId;
+    protected $appSecret;
+    protected $merchantId;
+    protected $shopId;
+    protected $accessToken;
+    protected $apiVersion = '1.0';
+
+    public function __construct($accessToken = '', $shopId = 0)
+    {
+        // 根据环境设置基础URL
+        if (getenv('YII_ENV') == 'production') {
+            $this->baseUrl = 'https://openic.sf-express.com/open/api/external/';
+            $this->appId = 1741530942;
+            $this->appSecret = '07b3f83d19a4a9f89322976c936faf92';
+            $this->shopId = $shopId;
+        } else {
+            $this->baseUrl = 'https://openic.sf-express.com/open/api/external/';
+            $this->appId = 1741530942;
+            $this->appSecret = '07b3f83d19a4a9f89322976c936faf92';
+            $this->shopId = $shopId;
+        }
+
+        // 配置信息(从Auth类获取)
+        $this->accessToken = $accessToken;
+    }
+
     public function cityList()
     public function cityList()
     {
     {
         
         
     }
     }
 
 
+    /**
+     * 格式化订单数据为顺丰同城API所需格式
+     * 
+     * @param array $order 订单信息
+     * @param array $shop 店铺信息
+     * @param array $params 额外参数
+     * @return array 格式化后的订单数据
+     */
+    public function formatOrderData($order, $shop, $params)
+    {
+        // 获取订单商品信息
+        $itemInfos = OrderItemClass::getAllByCondition(
+            ['orderSn' => $order['orderSn']], 
+            null, 
+            'id, name, unitPrice, num, unitWeight'
+        );
+
+        // 计算商品总重量(单位:克)
+        $totalWeight = 0;
+        $productNum = 0; // 物品个数
+        $productTypeNum = 0; // 物品种类个数
+        $productDetail = []; // 物品详情
+
+        foreach ($itemInfos as $item) {
+            // 累加重量(转换为克)
+            $itemWeight = (float)($item['unitWeight'] ?? 0) * (int)($item['num'] ?? 1) * 1000;
+            $totalWeight += (int)$itemWeight;
+
+            // 累加商品数量和种类
+            $productNum += (int)($item['num'] ?? 1);
+            $productTypeNum++;
+
+            // 构建商品详情
+            $productDetail[] = [
+                'product_name' => $item['name'] ?? '鲜花',
+                'product_num' => (int)($item['num'] ?? 1),
+                'product_price' => (int)(($item['unitPrice'] ?? 0) * 100), // 转换为分
+            ];
+        }
+
+        // 如果没有商品信息,使用订单重量
+        if ($totalWeight == 0 && isset($order['weight'])) {
+            $totalWeight = (int)($order['weight'] * 1000);
+        }
+
+        // 默认重量为1000克(1公斤)
+        if ($totalWeight == 0) {
+            $totalWeight = 1000;
+        }
+
+        // 如果没有商品详情,添加默认商品
+        if (empty($productDetail)) {
+            $productDetail[] = [
+                'product_name' => '鲜花',
+                'product_num' => 1,
+            ];
+            $productNum = 1;
+            $productTypeNum = 1;
+        }
+
+        // 获取订单时间
+        $orderTime = isset($order['addTime']) ? strtotime($order['addTime']) : time();
+
+        // 构建必填参数
+        $data = [
+            // ==================== 必填字段 ====================
+            'shop_order_id' => $order['orderSn'],
+            'order_ver' => time(), // 订单版本号	使用相同的商家订单号,同城侧会幂等返回,可以使用此参数+1控制生成新的同城订单
+            'order_source' => $params['order_source'] ?? '花店系统', // 订单接入来源
+            'order_time' => $orderTime, // 用户下单时间(秒级时间戳)
+            'push_time' => time(), // 推单时间(秒级时间戳)
+            'version' => 19, // API版本号
+
+            // 收货人信息
+            'receive' => [
+                'user_name' => $order['customName'],
+                'user_phone' => $order['customMobile'],
+                'user_address' => $order['fullAddress'],
+                'user_lng' => (string)($order['long']),
+                'user_lat' => (string)($order['lat']),
+                'city_name' => $order['city'] ?? '', // 发单城市,用于校验是否跨城
+            ],
+
+            // 发货店铺信息
+            'shop' => [
+                'shop_name' => $shop['merchantName'] ?? $shop['shopName'] ?? '',
+                'shop_address' => $shop['address'] ?? '',
+                'shop_lng' => (string)($shop['long'] ?? ''),
+                'shop_lat' => (string)($shop['lat'] ?? ''),
+                'shop_phone' => $shop['mobile'] ?? '',
+            ],
+
+            // 订单详情
+            'order_detail' => [
+                'total_price' => (int)(($order['actPrice'] ?? 0) * 100), // 订单总金额(单位:分)
+                'product_type' => 14, // 物品类型:14-鲜花
+                'weight_gram' => $totalWeight, // 物品重量(单位:克)
+                'product_num' => $productNum, // 物品个数
+                'product_type_num' => $productTypeNum, // 物品种类个数
+                'product_detail' => $productDetail, // 物品详情
+            ],
+
+            // ==================== 可选字段 ====================
+            'lbs_type' => 2, // 坐标类型(2:高德坐标)
+            // 'is_appoint' => 0, // 是否预约单(0:非预约单)  // TODO
+            'is_insured' => 0, // 是否保价(0:非保价)
+            'is_person_direct' => 0, // 是否专人直送(0:否)
+            'vehicle' => 0, // 配送交通工具(0:否,1:电动车,2:小轿车)
+            'declared_value' => 0, // 保价金额(单位:分)
+            'gratuity_fee' => 0, // 订单小费(单位:分)
+            'rider_pick_method' => 1, // 物流流向(1:从门店取件送至用户)
+            'return_flag' => 511, // 返回字段控制标志位(511:全部返回)
+            'payment_method' => 1, // 支付方式(1:立即支付,2:到付)
+        ];
+
+        return $data;
+    }
+
     public function createOrder($data)
     public function createOrder($data)
     {
     {
+        $resp = $this->httpRequest('createorder', $data);
 
 
+        // 处理响应
+        if (isset($resp['error_code']) && $resp['error_code'] == 0 && isset($resp['result'])) {
+            // business_data 是 JSON 字符串,需要解析
+            $respData = is_string($resp['result']) ? json_decode($resp['result'], true) : $resp['result'];
+
+            if (isset($respData['sf_order_id'])) {
+                return [
+                    'code' => 0,
+                    'platform' => 'shunfeng',
+                    'data' => [
+                        'order_id' => $respData['sf_order_id'],
+                        'fee' => $respData['real_pay_money'],
+                        'distance' => $respData['delivery_distance_meter'],
+                    ]
+                ];
+            }
+        }
+
+        return [
+            'code' => (int)($resp['code'] ?? -1),
+            'msg' => $resp['msg'] ?? 'Unknown error',
+            'data' => null
+        ];
     }
     }
 
 
     public function getPrice($data)
     public function getPrice($data)
     {
     {
+        $requestData = [
+            "user_lng" => "116.339392",
+            "user_lat" => "40.002349",
+            "user_address" => "北京市海淀区国泰大厦",
+            "weight" => 1000,
+            "product_type" => 4,
+            "push_time" => 1639558442
+        ];
+        $method = 'precreateorder';
+        $ret = $this->httpRequest($method, $requestData);
+
+    }
+
+    /**
+     * 处理运费报价响应
+     *
+     * @param array $resp API 响应
+     * @return array|null 解析后的报价结果
+     */
+    public function processPriceResponse($resp)
+    {
+        if (isset($resp['error_code']) && $resp['error_code'] == 0 && isset($resp['result'])) {
+            //return $resp['result'];
+//            return [
+//                'platform' => 'fengniao',
+//                'distance' => (int)($respData['distance'] ?? 0),
+//                'time' => (int)($respData['time'] ?? 0),
+//                'goods_infos' => $goodsInfos,
+//            ];
+
+            $result = $resp['result'];
+            return [
+                'platform' => 'shunfeng',
+                'total_price' => $result['total_price'], // 配送费总额
+                'distance' => $result['delivery_distance_meter'], //配送距离
+                'total_pay_money' => $result['total_pay_money'], //总支付费用(优惠前),当return_flag中包含32时返回,单位分
+                'real_pay_money' => $result['real_pay_money'], //实际支付金额,当return_flag中包含64时返回,单位分(实际支付金额=总支付费用-优惠券总金额)
+                // 还有其它返回值没加进来,TODO
+            ];
+        }
+        return null;
+    }
+
+    /**
+     * 构建运费报价请求信息(供并发请求使用)
+     *
+     * @param array $data 报价参数
+     * @param string $orderTime 配送时间
+     * @return array|null 包含 url, payload, headers, timeout 的请求信息
+     */
+    public function buildPriceRequest($data, $orderTime)
+    {
+        unset($data['platform']);
+        // 根据 $orderTime ,调整 $data 中的部分参数
+
+
+        // 构建签名用的完整payload
+        $payload = [
+            'dev_id' => $this->appId,
+            'shop_id' => $this->shopId,
+            'shop_type' => 1, //1:顺丰店铺ID 2:接入方店铺ID
+        ];
+
+        $payload = array_merge($payload, $data);
+
+        // 计算签名(SignHelper 会自动递归过滤空值)
+        $sign = $this->generateSignature($payload);
+
+        return [
+            'url' => $this->baseUrl . 'precreateorder?sign=' .  $sign,
+            'data' => $payload,
+            'headers' => ["Content-Type" => "application/json"],
+            'timeout' => 5.0,
+        ];
+    }
+
+    /**
+     * 构建API请求参数
+     *
+     * 根据蜂鸟API规范构建完整的请求参数,包括签名
+     *
+     * @param string $method API方法名
+     * @param array $businessData 业务参数
+     * @return array 完整的请求参数
+     */
+    protected function httpRequest($method, $businessData = [])
+    {
+        // 构建参数
+        $payload = [
+            'dev_id' => $this->appId,
+            'shop_id' => $this->shopId,
+            'shop_type' => 1, //1:顺丰店铺ID 2:接入方店铺ID
+            //-------------------------------------------------
+        ];
+        $payload = array_merge($payload, $businessData);
+
+        // 计算签名
+        $signature = $this->generateSignature($payload);
+        $url = $this->baseUrl . $method . '?sign=' .  $signature;
 
 
+        $resp = HttpClient::post($url, $payload, [
+            'Content-Type' => 'application/json',
+        ]);
+        return $resp;
+    }
+
+    public function generateSignature($params)
+    {
+        $post_data = json_encode($params);
+        $sign_char = $post_data . "&{$this->appId}&{$this->appSecret}";
+        $sign      = base64_encode(MD5($sign_char)); // 注:md5出来的结果是32位小写16进制字符串,$sign 的最终结果末尾包含等号=
+        return $sign;
     }
     }
 }
 }