Browse Source

1. 各平台账号余额查询
2. 货拉拉下订单时附带的 notify_url 引起的签名认证失败问题解决
3. 完成货拉拉订单状态回调处理

shizhongqi 9 tháng trước cách đây
mục cha
commit
1c5af5dc3d

+ 25 - 5
app-ghs/controllers/DeliveryController.php

@@ -44,7 +44,7 @@ class DeliveryController extends BaseController
                 'logo'=>$platformLogos[$d['platform']]['logo'],
                 'name'=>$platformLogos[$d['platform']]['name'],
                 'is_authorized' => true,
-                'balance' => 0,
+                'balance' => $d['balance']/100.0,
                 'access_type' => 'oauth'
             ];
             $platformList[] = $item;
@@ -466,7 +466,7 @@ class DeliveryController extends BaseController
             }
             $cancelCostCent = $res['data']['actual_cancel_cost_cent']; //取消实际需金额(单位:分)
         }
-        if($platform='shunfeng') {
+        if($platform == 'shunfeng') {
             //请求订单预取消接口,获取是否可取消。如果运单状态不允许取消,则直接返回
             $cancelParams = ['order_id'=>$orderId, 'order_cancel_code'=>$post['order_cancel_code']];
             $res = $ds->getAdapter()->preCancelOrder($cancelParams);
@@ -610,10 +610,30 @@ class DeliveryController extends BaseController
     {
         $post = Yii::$app->request->post();
         Yii::info('货拉拉回调接口post:' . json_encode($post, JSON_UNESCAPED_UNICODE), 'delivery');
+        
+        $action = $post['action'] ?? '';
+        if($action != 'order_update') {
+            Yii::error('当前接口只处理货拉拉订单状态变更事件');
+            return $this->asJson(['return_code' => 0, 'msg' => '当前接口只处理货拉拉订单状态变更事件']);
+        }
 
-        $get = Yii::$app->request->get();
-        Yii::info('货拉拉回调接口get:' . json_encode($get, JSON_UNESCAPED_UNICODE), 'delivery');
-        return $this->asJson(['return_code' => 0, 'return_msg' => 'OK']);
+        $param = $post;
+        unset($param['signature']);
+        $huolala = new \common\components\delivery\platform\huolala\Huolala();
+        $paramSign = $huolala->generateSignatureWithoutAppSecret($param); //注意:不用拼接secret,所有的参数都参与计算,包含空值。
+        if($paramSign == $post['signature']){
+            $cbh = new \common\components\delivery\platform\huolala\CallBackHandler();
+            $ret = $cbh->handler($post);
+            if($ret) {
+                return $this->asJson(['serial_no' => $post['serial_no']]);
+            } else {
+                Yii::error('货拉拉回调失败: ' . json_encode($post, JSON_UNESCAPED_UNICODE));
+                return $this->asJson(['return_code' => -1, 'return_msg' => 'failed']);
+            }
+        }else{
+            Yii::error('蜂鸟回调接口签名出错');
+            return $this->asJson(['return_code' => -1, 'return_msg' => 'signature error']);
+        }
     }
 
     /**

+ 64 - 0
app-ghs/controllers/DeliveryShopController.php

@@ -0,0 +1,64 @@
+<?php
+namespace ghs\controllers;
+
+use bizGhs\express\classes\DeliveryAuthTokenClass;
+use common\components\util;
+use common\components\delivery\services\ShopService;
+use Yii;
+
+/**
+ * 聚合配送 -- 门店相关接口
+ * Class DeliveryController
+ * @package ghs\controllers
+ */
+class DeliveryShopController extends BaseController
+{
+    public $guestAccess = [];
+
+//    public function actionList()
+//    {
+//        $platformList = [];
+//        $platformLogos = [
+//            'data' => ['name'=>'达达', 'logo'=>'📦'],
+//            'shunfeng' => ['name'=>'顺丰同城', 'logo'=>'🚚'],
+//            'meituan' => ['name'=>'美团', 'logo'=>'🍱'] ,
+//            'fengniao' => ['name'=>'蜂鸟', 'logo'=>'🍜'],
+//            'huolala' => ['name'=>'货拉拉', 'logo'=>'🚛'],
+//            'shansong' => ['name'=>'闪送', 'logo'=>'⚡'],
+//            'didi' => ['name'=>'滴滴', 'logo'=>'🚗'],
+//			'uu' => ['name'=>'UU跑腿', 'logo'=>'🛵']
+//        ];
+//        $mainId = $this->mainId;
+//        $allDeliveries = DeliveryAuthTokenClass::getAllByCondition(['mainId'=>$mainId]);
+//        foreach($allDeliveries as $d){
+//            $item = [
+//                'id'=>$d['platform'],
+//                'logo'=>$platformLogos[$d['platform']]['logo'],
+//                'name'=>$platformLogos[$d['platform']]['name'],
+//                'is_authorized' => true,
+//                'balance' => $d['balance']/100.0,
+//                'access_type' => 'oauth'
+//            ];
+//            $platformList[] = $item;
+//        }
+//        util::success(['platformList'=>$platformList]);
+//    }
+
+    public function actionBalance()
+    {
+        $get = Yii::$app->request->get();
+        $platform = $get['platform'];
+
+        $ShopService = new ShopService($this->mainId, $platform);
+        $balance = $ShopService->getBalance($platform);
+        
+
+        $deliveryAuthToken = DeliveryAuthTokenClass::getByCondition(['platform'=>$platform, 'mainId'=>$this->mainId], true);
+        if(!$deliveryAuthToken){
+            util::fail('配送平台未授权');
+        }
+        $deliveryAuthToken->balance = $balance;
+        $deliveryAuthToken->save();
+        util::success(['balance'=>$balance]);
+    }
+}

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

@@ -159,7 +159,6 @@ class HttpClient
         // 构建 Guzzle 请求配置
         $requestConfigs = [];
         $platformNames = [];
-        $platforms = [];
 
         foreach ($requests as $platform => $config) {
             $url = $config['url'] ?? '';

+ 105 - 0
common/components/delivery/platform/huolala/CallBackHandler.php

@@ -0,0 +1,105 @@
+<?php
+namespace common\components\delivery\platform\huolala;
+
+use bizGhs\express\classes\GhsDeliveryOrderClass;
+use bizGhs\order\classes\OrderClass;
+use Yii;
+
+class CallBackHandler
+{
+    /**
+     * 货拉拉状态回调入口
+     *
+     * @param array $data
+     * @return bool
+     */
+    public function handler($data)
+    {
+        try {
+            Yii::info('货拉拉回调 - 原始数据: ' . json_encode($data, JSON_UNESCAPED_UNICODE), 'huolala-callback');
+
+            if (empty($data) || empty($data['data']) || !is_array($data['data'])) {
+                Yii::error('货拉拉回调参数异常: ' . json_encode($data, JSON_UNESCAPED_UNICODE), 'huolala-callback');
+                return false;
+            }
+
+            $bizData = $data['data'];
+            $eventType = $bizData['event_type'] ?? '';
+            $orderDisplayId = $bizData['order_display_id'] ?? '';
+
+            if ($eventType === '' || $orderDisplayId === '') {
+                Yii::error('货拉拉回调缺少必要字段: ' . json_encode($bizData, JSON_UNESCAPED_UNICODE), 'huolala-callback');
+                return false;
+            }
+
+            $orderStatus = $this->mapEventTypeToOrderStatus($eventType);
+            if ($orderStatus === null) {
+                Yii::warning(sprintf('货拉拉事件未配置状态映射: event_type=%s, order_display_id=%s', $eventType, $orderDisplayId), 'huolala-callback');
+                return true;
+            }
+
+            $deliveryOrder = GhsDeliveryOrderClass::getByCondition([
+                'deliveryId' => 'huolala',
+                'orderId' => $orderDisplayId,
+            ], true);
+
+            if (!$deliveryOrder) {
+                Yii::error('货拉拉配送订单不存在: order_display_id=' . $orderDisplayId, 'huolala-callback');
+                return false;
+            }
+
+            $deliveryOrder->orderStatus = $orderStatus;
+            if (!$deliveryOrder->save()) {
+                Yii::error('更新配送订单状态失败: ' . json_encode($deliveryOrder->getErrors(), JSON_UNESCAPED_UNICODE), 'huolala-callback');
+                return false;
+            }
+
+            $order = OrderClass::getById($deliveryOrder->ghsOrderId, true, 'id,sendStatus');
+            if ($order) {
+                $order->sendStatus = $orderStatus;
+                if (!$order->save()) {
+                    Yii::warning('更新主订单配送状态失败: ' . json_encode($order->getErrors(), JSON_UNESCAPED_UNICODE), 'huolala-callback');
+                }
+            } else {
+                Yii::warning('未找到主订单: ghsOrderId=' . $deliveryOrder->ghsOrderId, 'huolala-callback');
+            }
+
+            Yii::info(sprintf('货拉拉配送状态更新成功: order_display_id=%s, event_type=%s, orderStatus=%s', $orderDisplayId, $eventType, $orderStatus), 'huolala-callback');
+            return true;
+        } catch (\Throwable $e) {
+            Yii::error('货拉拉回调处理异常: ' . $e->getMessage() . ' | Trace: ' . $e->getTraceAsString(), 'huolala-callback');
+            return false;
+        }
+    }
+
+    /**
+     * 事件到系统配送状态的映射
+     *
+     * @param string $eventType
+     * @return int|null
+     */
+    protected function mapEventTypeToOrderStatus($eventType)
+    {
+        $map = [
+            'order_cancel' => -1,
+            'pickup' => 20,
+            'driver_reject' => 2,
+            'order_complete' => 5,
+            'set_order_abnormal' => -2,
+            'loaded' => 4,
+            'auto_confirm_bill_pay' => 5,
+            'no_load_cancel' => -1,
+            'no_complete_cancel' => -1,
+            'loading' => 3,
+            'unloading' => 4,
+            'unload_finish' => 4,
+            'order_auto_complete' => 5,
+            'order_timeout' => -1,
+            'prepay_ok' => 2,
+            'rear_pay_ok' => 5,
+            'cash_received' => 5,
+        ];
+
+        return $map[$eventType] ?? null;
+    }
+}

+ 128 - 0
common/components/delivery/platform/huolala/Huolala.php

@@ -0,0 +1,128 @@
+<?php
+
+
+namespace common\components\delivery\platform\huolala;
+
+
+use common\components\delivery\helpers\SignHelper;
+
+class Huolala
+{
+    protected $baseUrl;
+    protected $appSecret;
+    protected $appKey;
+    protected $accessToken;
+    protected $apiVersion = '1.0';
+    /**
+     * @var string 订单消息回调地址
+     */
+    protected $notifyUrl;
+
+    public function __construct($accessToken='')
+    {
+        if (getenv('YII_ENV') == 'production') {
+            $cfg = [
+                'base_url' => 'https://openapi.huolala.cn/v1',
+                'app_key' => '8S2uZQrWwM1mkG2Cj576PsMiNIe25UNT',
+                'secret' => 'olzor07UgmUWaagahNGQAKQKXRtvhK8j',
+                'notify_url' => 'https://api.shop.wixhb.com/delivery/huolala-callback',
+            ];
+        } else {
+            $cfg = [
+                'base_url' => 'https://openapi-pre.huolala.cn/v1',
+                'app_key' => '8S2uZQrWwM1mkG2Cj576PsMiNIe25UNT',
+                'secret' => 'olzor07UgmUWaagahNGQAKQKXRtvhK8j',
+                'notify_url' => 'https://api.shop.hzghd.com/delivery/huolala-callback',
+            ];
+        }
+        $this->baseUrl = $cfg['base_url'];
+        $this->appSecret = $cfg['secret'];
+        $this->appKey = $cfg['app_key'];
+        $this->accessToken = $accessToken;
+        $this->notifyUrl = $cfg['notify_url'];
+    }
+
+    /**
+     * 构建API请求参数
+     * 
+     * 根据货拉拉API规范构建完整的请求参数,包括签名
+     * 
+     * @param string $apiMethod API方法名
+     * @param array $apiData 业务参数
+     * @param string|null $accessToken 用户授权令牌(可选)
+     * @return array 完整的请求参数
+     */
+    protected function buildRequestPayload($apiMethod, $apiData = [], $accessToken = null)
+    {
+        // 生成随机数(nonce_str)
+        $nonceStr = $this->generateNonceStr();
+
+        // 构建系统级参数
+        $payload = [
+            'api_method' => $apiMethod,
+            'api_version' => $this->apiVersion,
+            'app_key' => $this->appKey,
+            'timestamp' => time(),
+            'nonce_str' => $nonceStr,
+        ];
+
+        // 如果提供了访问令牌,加入参数
+        if (!empty($accessToken)) {
+            $payload['access_token'] = $accessToken;
+        }
+
+        // 业务数据作为 JSON 字符串
+        // 注意:如果不加 JSON_UNESCAPED_SLASHES,PHP 会把 / 编码成 \/
+        //      而很多第三方(包括货拉拉)在服务端是按未转义的 / 参与签名的,
+        //      一旦字段里有 URL(如 notify_url),双方签名串就会不一致。
+        //      因此这里显式关闭斜杠转义,保证本地和对端采用一致的 JSON 形式参与签名。
+        $payload['api_data'] = json_encode($apiData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+
+        // 计算签名
+        $signature = $this->generateSignature($payload);
+        $payload['signature'] = $signature;
+
+        return $payload;
+    }
+
+    /**
+     * 生成签名
+     *
+     * 复用 SignHelper 的通用签名算法
+     * 货拉拉要求32位小写MD5签名,而 SignHelper 返回大写,所以需要转为小写
+     *
+     * @param array $params 待签名参数(不包括signature)
+     * @return string 签名值(32位小写)
+     */
+    protected function generateSignature($params)
+    {
+
+        // 使用 SignHelper 的通用签名算法
+        $sign = SignHelper::makeSign($params, $this->appSecret);
+
+        // 货拉拉 API 要求32位小写,转换为小写
+        return strtolower($sign);
+    }
+
+    public function generateSignatureWithoutAppSecret($params)
+    {
+        $sign = SignHelper::makeSign($params, '');
+        return strtolower($sign);
+    }
+
+    /**
+     * 生成随机数字符串
+     *
+     * @param int $length 长度
+     * @return string 随机字符串
+     */
+    protected function generateNonceStr($length = 16)
+    {
+        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+        $str = '';
+        for ($i = 0; $i < $length; $i++) {
+            $str .= $chars[random_int(0, strlen($chars) - 1)];
+        }
+        return $str;
+    }
+}

+ 61 - 0
common/components/delivery/platform/huolala/Shop.php

@@ -0,0 +1,61 @@
+<?php
+namespace common\components\delivery\platform\huolala;
+
+use common\components\delivery\helpers\HttpClient;
+
+/**
+ * 货拉拉商户钱包相关接口
+ * 
+ * 用于查询商户账户余额、子账户信息等钱包相关的操作
+ */
+class Shop extends Huolala
+{
+    /**
+     * 查询钱包余额
+     * 
+     * 通过此接口可以获取商户账户总余额、账户状态、子账户信息等详细的钱包信息
+     * 
+     * 文档参考:u-wallet-account API
+     * 
+     * @param string|null $ip 客户端IP地址(可选参数)
+     * @return array 包含以下字段的响应数据:
+     *     - user_id: 账户编号
+     *     - account_status: 账户状态 (5-审批中, 10-正常, 20-锁定, 25-资金不可出, 30-不可入)
+     *     - total_amount: 账户总余额
+     *     - sub_account_list: 子账户信息列表
+     *       - total_amount: 总余额
+     *       - avl_amount: 可用余额
+     *       - freeze_amount: 冻结金额
+     *       - sub_account_type: 子账户类型 (1-本金, 2-不可提现, 3-赠金, 4-红包)
+     *       - sub_account_status: 账户状态
+     */
+    public function queryWalletBalance($ip = null)
+    {
+        // 构建请求数据
+        $apiData = [];
+        if (!empty($ip)) {
+            $apiData['ip'] = $ip;
+        }
+
+        // 构建请求参数(包含access_token进行用户授权)
+        $payload = $this->buildRequestPayload('u-wallet-account', $apiData, $this->accessToken);
+
+        // 发送请求
+        $resp = HttpClient::post($this->baseUrl, $payload);
+
+        // 检查API响应状态
+        if (isset($resp['ret']) && $resp['ret'] == 0) {
+            return [
+                'success' => true,
+                'balance' => $resp['data']['total_amount'],
+                'message' => '查询余额成功',
+            ];
+        }
+
+        return [
+            'success' => false,
+            'balance' => 0,
+            'message' => '查询余额失败',
+        ];
+    }
+}

+ 315 - 0
common/components/delivery/platform/shansong/Shop.php

@@ -1,11 +1,326 @@
 <?php
 namespace common\components\delivery\platform\shansong;
 
+use common\components\delivery\helpers\HttpClient;
+use common\components\delivery\helpers\SignHelper;
+use Yii;
+
 /**
  * Class Shop
  * 门店接口实现
  */
 class Shop extends Shansong
 {
+    /**
+     * 查询账号额度
+     * 
+     * 接口文档:/openapi/developer/v5/getUserAccount
+     * 
+     * 返回账户余额,单位为分
+     * 
+     * @return array 返回格式:['success' => true/false, 'balance' => 0, 'message' => '']
+     *               balance: 账户余额,单位为分
+     */
+    public function getUserAccount()
+    {
+        $url = $this->baseUrl . '/openapi/developer/v5/getUserAccount';
+        
+        // 获取毫秒级时间戳
+        $timestamp = (string) (int) (microtime(true) * 1000);
+        
+        // 构建签名参数(不包括 sign)
+        $params = [
+            'clientId' => $this->clientId,
+            'accessToken' => $this->accessToken,
+            'timestamp' => $timestamp,
+        ];
+        
+        // 计算签名(闪送使用 MD5 签名)
+        $sign = SignHelper::makeSign(
+            $params, 
+            $this->appSecret, 
+            'md5', 
+            true, 
+            'shansong'
+        );
+        $params['sign'] = $sign;
+        
+        // 发送请求
+        $response = HttpClient::post($url, $params);
+        Yii::info("[ShansongUserAccount] Response: " . json_encode($response));
+        
+        // 检查API响应状态
+        if (isset($response['status']) && $response['status'] == 200) {
+            return [
+                'success' => true,
+                'balance' => $response['data']['balance'],
+                'message' => '查询余额成功',
+            ];
+        }
+        
+        return [
+            'success' => false,
+            'balance' => 0,
+            'message' => '查询余额失败',
+        ];
+    }
+
+    /**
+     * 新增店铺
+     * 
+     * 接口文档:/openapi/merchants/v5/storeOperation
+     * 接口说明:新增店铺接口不支持并发调用,一个请求返回结果后才能进行下一次请求
+     * 
+     * @param array $data 店铺信息,包括:
+     *        - storeName: 店铺名称 (必传, String(80))
+     *        - cityName: 城市名称 (必传, String(20))
+     *        - address: 店铺地址 (必传, String(80))
+     *        - addressDetail: 店铺详细地址 (必传, String(80))
+     *        - latitude: 店铺纬度-百度坐标系 (必传, String(20))
+     *        - longitude: 店铺经度-百度坐标系 (必传, String(20))
+     *        - phone: 店铺联系人手机号/座机 (必传, String(30))
+     *        - goodType: 店铺业务类型 (必传, Integer(8), 枚举: 1文件、3数码、5蛋糕、6餐饮、7鲜花、9汽配、10其他、12母婴、13医药健康、15商超、16水果)
+     * 
+     * @return array 返回格式:['success' => true/false, 'storeId' => 0, 'message' => '']
+     *               storeId: 新增店铺ID (只在success=true时有效)
+     */
+    public function addShop($data)
+    {
+        $url = $this->baseUrl . '/openapi/merchants/v5/storeOperation';
+        
+        // 获取毫秒级时间戳
+        $timestamp = (int) (microtime(true) * 1000);
+        
+        // 补充操作类型:1表示新增
+        $data['operationType'] = 1;
+        
+        // 构建完整payload
+        $payload = [
+            'clientId' => $this->clientId,
+            'shopId' => $this->accessToken, // 使用 accessToken 作为 shopId
+            'timestamp' => $timestamp,
+            'data' => json_encode($data, JSON_UNESCAPED_UNICODE),
+        ];
+        
+        // 计算签名
+        $sign = SignHelper::makeSign($payload, $this->appSecret, 'md5', true, 'shansong');
+        $payload['sign'] = $sign;
+        
+        // 发送请求
+        $response = HttpClient::post($url, $payload);
+        Yii::info("[ShansongAddShop] Response: " . json_encode($response));
+        
+        // 检查API响应状态
+        if (isset($response['status']) && $response['status'] == 200 && isset($response['data'])) {
+            return [
+                'success' => true,
+                'storeId' => $response['data'],
+                'message' => $response['msg'] ?? '店铺创建成功',
+            ];
+        }
+        
+        return [
+            'success' => false,
+            'storeId' => 0,
+            'message' => $response['msg'] ?? '店铺创建失败',
+        ];
+    }
+
+    /**
+     * 批量新增店铺
+     * 
+     * 接口文档:/openapi/merchants/v5/addStores
+     * 接口说明:每次最多批量新增10个店铺
+     * 
+     * @param array $storeList 店铺列表数组,每个元素包括:
+     *        - thirdStoreId: 第三方店铺ID (非必传, String(50))
+     *        - storeName: 店铺名称 (必传, String(80))
+     *        - cityName: 城市名称 (必传, String(20))
+     *        - address: 店铺地址 (必传, String(80))
+     *        - addressDetail: 店铺详细地址 (必传, String(80))
+     *        - latitude: 店铺纬度-百度坐标系 (必传, String(20))
+     *        - longitude: 店铺经度-百度坐标系 (必传, String(20))
+     *        - phone: 店铺联系人手机号/座机 (必传, String(30))
+     *        - goodType: 店铺业务类型 (必传, Integer(8))
+     * 
+     * @return array 返回格式:['success' => true/false, 'failList' => [], 'successList' => []]
+     *               failList: 失败列表,每项包含 dto 和 reason
+     *               successList: 成功列表,每项包含 dto 和 storeId
+     */
+    public function batchAddShops($storeList)
+    {
+        $url = $this->baseUrl . '/openapi/merchants/v5/addStores';
+        
+        // 获取毫秒级时间戳
+        $timestamp = (int) (microtime(true) * 1000);
+        
+        // 构建完整payload
+        $payload = [
+            'clientId' => $this->clientId,
+            'shopId' => $this->accessToken, // 使用 accessToken 作为 shopId
+            'timestamp' => $timestamp,
+            'data' => json_encode($storeList, JSON_UNESCAPED_UNICODE),
+        ];
+        
+        // 计算签名
+        $sign = SignHelper::makeSign($payload, $this->appSecret, 'md5', true, 'shansong');
+        $payload['sign'] = $sign;
+        
+        // 发送请求
+        $response = HttpClient::post($url, $payload);
+        Yii::info("[ShansongBatchAddShops] Response: " . json_encode($response));
+        
+        // 检查API响应状态
+        if (isset($response['status']) && $response['status'] == 200 && isset($response['data'])) {
+            $respData = $response['data'];
+            return [
+                'success' => true,
+                'failList' => $respData['failList'] ?? [],
+                'successList' => $respData['successList'] ?? [],
+            ];
+        }
+        
+        return [
+            'success' => false,
+            'failList' => [],
+            'successList' => [],
+        ];
+    }
+
+    /**
+     * 修改店铺
+     * 
+     * 接口文档:/openapi/merchants/v5/storeOperation
+     * 
+     * @param array $data 店铺信息,包括:
+     *        - storeId: 店铺ID (必传, Long(20))
+     *        - storeName: 店铺名称 (必传, String(80))
+     *        - cityName: 城市名称 (必传, String(20))
+     *        - address: 店铺地址 (必传, String(80))
+     *        - addressDetail: 店铺详细地址 (必传, String(80))
+     *        - latitude: 店铺纬度-百度坐标系 (必传, String(20))
+     *        - longitude: 店铺经度-百度坐标系 (必传, String(20))
+     *        - phone: 店铺联系人手机号/座机 (必传, String(30))
+     *        - goodType: 店铺业务类型 (必传, Integer(8))
+     * 
+     * @return array 返回格式:['success' => true/false, 'storeId' => 0, 'message' => '']
+     *               storeId: 修改后的店铺ID (只在success=true时有效)
+     */
+    public function updateShop($data)
+    {
+        $url = $this->baseUrl . '/openapi/merchants/v5/storeOperation';
+        
+        // 获取毫秒级时间戳
+        $timestamp = (int) (microtime(true) * 1000);
+        
+        // 补充操作类型:2表示修改
+        $data['operationType'] = 2;
+        
+        // 构建完整payload
+        $payload = [
+            'clientId' => $this->clientId,
+            'shopId' => $this->accessToken, // 使用 accessToken 作为 shopId
+            'timestamp' => $timestamp,
+            'data' => json_encode($data, JSON_UNESCAPED_UNICODE),
+        ];
+        
+        // 计算签名
+        $sign = SignHelper::makeSign($payload, $this->appSecret, 'md5', true, 'shansong');
+        $payload['sign'] = $sign;
+        
+        // 发送请求
+        $response = HttpClient::post($url, $payload);
+        Yii::info("[ShansongUpdateShop] Response: " . json_encode($response));
+        
+        // 检查API响应状态
+        if (isset($response['status']) && $response['status'] == 200 && isset($response['data'])) {
+            return [
+                'success' => true,
+                'storeId' => $response['data'],
+                'message' => $response['msg'] ?? '更新店铺成功',
+            ];
+        }
+        
+        return [
+            'success' => false,
+            'storeId' => 0,
+            'message' => $response['msg'] ?? '更新店铺失败',
+        ];
+    }
+
+    /**
+     * 分页查询商户店铺
+     * 
+     * 接口文档:/openapi/merchants/v5/queryAllStores
+     * 接口说明:分页查询商户创建店铺,供订单计费使用。仅状态为审核通过的店铺可计费使用
+     * 
+     * @param array $params 查询参数,包括:
+     *        - pageNo: 页码 (必传, Integer(8), 最小值为1)
+     *        - pageSize: 每页数量 (必传, Integer(8), 最小值为1, 最大值为100)
+     *        - storeName: 店铺名称 (非必传, String(20), 模糊搜索)
+     * 
+     * @return array 返回格式:['success' => true/false, 'data' => [], 'message' => '']
+     *               data: 店铺列表数组,每项包含:
+     *                   - storeId: 店铺ID
+     *                   - storeName: 店铺名称
+     *                   - cityId: 城市ID
+     *                   - cityName: 城市名称
+     *                   - contactsMobile: 店铺联系人手机号
+     *                   - address: 店铺地址
+     *                   - addressDetail: 店铺地址详情
+     *                   - latitude: 纬度
+     *                   - longitude: 经度
+     *                   - goodType: 店铺类型标签
+     *                   - goodName: 店铺类型标签名称
+     *                   - status: 店铺状态 (0-禁用、1-待审核、2-驳回、3-审核通过)
+     *                   - totalNum: 店铺总条数
+     */
+    public function queryShops($params)
+    {
+        $url = $this->baseUrl . '/openapi/merchants/v5/queryAllStores';
+        
+        // 获取毫秒级时间戳
+        $timestamp = (int) (microtime(true) * 1000);
+        
+        // 验证必传参数
+        if (!isset($params['pageNo']) || !isset($params['pageSize'])) {
+            return [
+                'success' => false,
+                'data' => [],
+                'message' => '缺少必传参数:pageNo 或 pageSize',
+            ];
+        }
+        
+        // 构建完整payload
+        $payload = [
+            'clientId' => $this->clientId,
+            'shopId' => $this->accessToken, // 使用 accessToken 作为 shopId
+            'timestamp' => $timestamp,
+            'data' => json_encode($params, JSON_UNESCAPED_UNICODE),
+        ];
+        
+        // 计算签名
+        $sign = SignHelper::makeSign($payload, $this->appSecret, 'md5', true, 'shansong');
+        $payload['sign'] = $sign;
+        
+        // 发送请求
+        $response = HttpClient::post($url, $payload);
+        Yii::info("[ShansongQueryShops] Response: " . json_encode($response));
+        
+        // 检查API响应状态
+        if (isset($response['status']) && $response['status'] == 200 && isset($response['data'])) {
+            return [
+                'success' => true,
+                'data' => $response['data'],
+                'message' => '查询店铺成功',
+            ];
+        }
+        
+        return [
+            'success' => false,
+            'data' => [],
+            'message' => $response['msg'] ?? '查询店铺失败',
+        ];
+    }
 
 }

+ 45 - 3
common/components/delivery/platform/shunfeng/Shop.php

@@ -1,6 +1,4 @@
 <?php
-
-
 namespace common\components\delivery\platform\shunfeng;
 
 /**
@@ -9,5 +7,49 @@ namespace common\components\delivery\platform\shunfeng;
  */
 class Shop extends Shunfeng
 {
-
+    /**
+     * 获取店铺账户余额
+     * 
+     * 获取当前店铺储值卡/预付费余额
+     * 
+     * 文档参考:https://openic.sf-express.com/open/api/external/getshopaccountbalance
+     * 
+     * @return array 返回格式:['success' => true/false, 'balance' => 0, 'reserved_amount' => 0, 
+     *                            'withdraw_amount' => 0, 'supplier_balance' => 0, 'message' => '']
+     *               balance: 店铺账户余额,单位:分
+     *               reserved_amount: 预留余额,单位:分
+     *               withdraw_amount: 实际可提现余额,单位:分
+     *               supplier_balance: 商家账户余额,单位:分(仅当传入为链店铺ID时返回)
+     */
+    public function getShopAccountBalance()
+    {
+        // 构建请求参数
+        $businessData = [
+            'push_time' => time(),
+        ];
+        
+        // 调用httpRequest方法发送请求
+        $response = $this->httpRequest('getshopaccountbalance', $businessData);
+        
+        // 检查API响应
+        if (isset($response['error_code']) && $response['error_code'] == 0) {
+            return [
+                'success' => true,
+                'balance' => $response['result']['balance'],
+                'reserved_amount' => $response['result']['reserved_amount'],
+                'withdraw_amount' => $response['result']['withdraw_amount'],
+                'supplier_balance' => $response['result']['supplier_balance'],
+                'message' => '查询余额成功',
+            ];
+        }
+        
+        return [
+            'success' => false,
+            'balance' => 0,
+            'reserved_amount' => 0,
+            'withdraw_amount' => 0,
+            'supplier_balance' => 0,
+            'message' => $response['error_msg'] ?? '查询余额失败',
+        ];
+    }
 }

+ 0 - 4
common/components/delivery/services/AccountBindService.php

@@ -1,4 +0,0 @@
-<?php
-namespace common\components\delivery\services;
-
-//商户账号绑定管理

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

@@ -6,8 +6,7 @@ use biz\shop\classes\ShopClass;
 use bizGhs\express\classes\DeliveryAuthTokenClass;
 use bizGhs\order\classes\OrderItemClass;
 use common\components\delivery\helpers\HttpClient;
-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\services\adapter\{ ShansongAdapter,  HuolalaAdapter, FengniaoAdapter, ShunfengAdapter}; // MeituanAdapter, DadaAdapter, UUAdapter,
 use Yii;
 
 /**

+ 95 - 0
common/components/delivery/services/ShopService.php

@@ -0,0 +1,95 @@
+<?php
+namespace common\components\delivery\services;
+
+//商户账号绑定管理
+use bizGhs\express\classes\DeliveryAuthTokenClass;
+use common\components\delivery\platform\fengniao\Shop as FengniaoShop;
+use common\components\delivery\platform\huolala\Shop as HuolalaShop;
+use common\components\delivery\platform\shansong\Shop as ShansongShop;
+use common\components\delivery\platform\shunfeng\Shop as ShunfengShop;
+
+/**
+ * 聚合调度逻辑(平台选择/优先级)
+ * Class DispatchService
+ * @package App\Services
+ */
+class ShopService
+{
+    protected $adapters;
+    protected $platformName = '';
+
+    public function __construct($mainId, $platform = '')
+    {
+        if($platform == '') {
+            $authPlatforms = DeliveryAuthTokenClass::getAllByCondition(['mainId' => $mainId]);
+            foreach($authPlatforms as $pt) {
+                switch($pt['platform']) {
+                    case 'shansong':
+                        $this->adapters['shansong'] = new ShansongShop($pt['accessToken']);
+                        break;
+                    case 'huolala':
+                        $this->adapters['huolala'] = new HuolalaShop($pt['accessToken']);
+                        break;
+                    case 'fengniao':
+                        $adapter = new FengniaoShop($pt['accessToken']);
+                        $adapter->setMerchantId(14594092); // TODO: 确认是否需要使用动态的商户ID
+                        $this->adapters['fengniao'] = $adapter;
+                        break;
+                    case 'shunfeng':
+                        $this->adapters['shunfeng'] = new ShunfengShop($pt['accessToken'], $pt['shopId']);
+                        break;
+                }
+            }
+        } else {
+            $authPlatform = DeliveryAuthTokenClass::getByCondition(['mainId' => $mainId, 'platform' => $platform]);
+            switch($platform) {
+                case 'shansong':
+                    $this->adapters['shansong'] = new ShansongShop($authPlatform['accessToken']);
+                    break;
+                case 'huolala':
+                    $this->adapters['huolala'] = new HuolalaShop($authPlatform['accessToken']);
+                    break;
+                case 'fengniao':
+                    //$this->adapters['fengniao'] = new FengniaoAdapter($authPlatform['accessToken']);
+                    $adapter = new FengniaoShop($authPlatform['accessToken']);
+                    $adapter->setMerchantId(14594092); // TODO: 确认是否需要使用动态的商户ID
+                    $this->adapters['fengniao'] = $adapter;
+                    break;
+                case 'shunfeng':
+                    $this->adapters['shunfeng'] = new ShunfengShop($authPlatform['accessToken'], $authPlatform['shopId']);
+                    break;
+            }
+            $this->platformName = $platform;
+        }
+    }
+
+    public function getAdapter()
+    {
+        return $this->adapters[$this->platformName];
+    }
+
+    public function getBalance($platform)
+    {
+        $ret = [];
+        switch($platform) {
+            case 'shansong':
+                $adapter = $this->adapters[$platform];
+                $ret = $adapter->getUserAccount();
+                break;
+            case 'huolala':
+                $adapter = $this->adapters[$platform];
+                $ret = $adapter->queryWalletBalance();
+                break;
+            case 'fengniao':
+                $ret['success'] = false;
+                $ret['balance'] = 0;
+                $ret['message'] = '蜂鸟平台不支持查询余额';
+                break;
+            case 'shunfeng':
+                $adapter = $this->adapters[$platform];
+                $ret = $adapter->getShopAccountBalance();  
+                break;
+        }
+        return $ret['balance'];
+    }
+}

+ 1 - 0
common/components/delivery/services/adapter/Adapter.php

@@ -8,5 +8,6 @@ interface Adapter
 {
     public function cityList();
     public function createOrder($order);
+    //public function selectOrder($data);
     public function buildPriceRequest($data, $orderTime);
 }

+ 1 - 76
common/components/delivery/services/adapter/FengniaoAdapter.php

@@ -582,6 +582,7 @@ class FengniaoAdapter extends Fengniao implements Adapter
     }
 
     /**
+     * @deprecated
      * 获取运费报价(预下单接口)
      * 
      * 根据蜂鸟官方文档实现报价功能,返回所有可选的商品列表
@@ -1095,82 +1096,6 @@ class FengniaoAdapter extends Fengniao implements Adapter
      *       // 注意:order_id 和 partner_order_code 必填一个
      *   ]
      * @return array API响应结果
-     *   成功:[
-     *       'code' => 0,
-     *       'data' => [
-     *           // 基本信息
-     *           'order_id' => '订单ID',
-     *           'partner_order_code' => '外部订单号',
-     *           'tracking_id' => 'trackingId',
-     *           'order_status' => '订单状态',
-     *           'serial_number' => '商家订单流水号',
-     *           'remark' => '备注',
-     *           
-     *           // 配送员信息
-     *           'carrier_driver_id' => '配送员ID',
-     *           'carrier_driver_name' => '配送员姓名',
-     *           'carrier_driver_phone' => '配送员电话',
-     *           'temperature' => '配送员体温',
-     *           
-     *           // 金额信息(单位:分)
-     *           'order_total_amount_cent' => '原始配送费',
-     *           'order_actual_amount_cent' => '实际配送费',
-     *           'order_tip_amount_cent' => '小费总金额',
-     *           'overtime_compensation_cost_cent' => '时效赔付',
-     *           
-     *           // 配送信息
-     *           'order_distance' => '配送距离(米)',
-     *           'estimate_arrive_time' => '预计送达时间(毫秒)',
-     *           'if_can_add_tip' => '是否支持添加调度费',
-     *           'fetch_code' => '取货码',
-     *           'write_off_code' => '收货码',
-     *           'cancel_code' => '退货码',
-     *           
-     *           // 价格明细
-     *           'price_detail' => [
-     *               'start_price_cent' => '起送价',
-     *               'distance_price_cent' => '距离加价',
-     *               'weight_price_cent' => '重量加价',
-     *               'pressure_surcharge_cent' => '运力紧张加价',
-     *               'time_period_surcharge_cent' => '时段加价',
-     *               'river_crossing_surcharge_cent' => '跨江单加价',
-     *               'category_surcharge_cent' => '品类加价',
-     *               'temporary_surcharge_cent' => '临时加价',
-     *               'order_price_surcharge_cent' => '客单价加价',
-     *           ],
-     *           
-     *           // 事件日志
-     *           'event_log_details' => [
-     *               [
-     *                   'order_status' => '订单状态码',
-     *                   'occur_time' => '发生时间(毫秒)',
-     *                   'carrier_driver_name' => '骑手姓名',
-     *                   'carrier_driver_phone' => '骑手电话',
-     *               ],
-     *           ],
-     *           
-     *           // 异常信息
-     *           'abnormal_code' => '运单异常原因code',
-     *           'abnormal_desc' => '运单异常原因描述',
-     *           
-     *           // 索赔信息
-     *           'claim_id' => '索赔ID',
-     *           'claim_status' => '索赔状态(1待处理,2成功,3失败)',
-     *           'claim_status_desc' => '索赔失败原因',
-     *           'claim_reason_desc' => '索赔原因描述',
-     *           
-     *           // 投诉信息
-     *           'complaint_id' => '投诉编号',
-     *           'complaint_status' => '投诉状态(1待处理,2成功,3失败)',
-     *           'complaint_response_desc' => '投诉失败原因',
-     *           'complaint_reason_desc' => '投诉原因描述',
-     *           
-     *           // 其他
-     *           'delivery_fetch_photos' => '骑手取货照片地址列表',
-     *           'weather_info' => '天气信息',
-     *       ]
-     *   ]
-     *   失败:['code' => 错误码, 'msg' => 错误信息, 'data' => null]
      */
     //public function getOrderDetail($data = [])
     public function selectOrder($data)

+ 56 - 215
common/components/delivery/services/adapter/HuolalaAdapter.php

@@ -2,7 +2,7 @@
 namespace common\components\delivery\services\adapter;
 
 use common\components\delivery\helpers\HttpClient;
-use common\components\delivery\helpers\SignHelper;
+use common\components\delivery\platform\huolala\Huolala;
 
 /**
  * 货拉拉适配器
@@ -10,66 +10,14 @@ use common\components\delivery\helpers\SignHelper;
  * 基于货拉拉开放平台API进行封装
  * 文档参考:货拉拉API调用协议与签名算法
  */
-class HuolalaAdapter implements Adapter
+class HuolalaAdapter extends Huolala implements Adapter
 {
-    protected $baseUrl;
-    protected $appSecret;
-    protected $appKey;
-    protected $accessToken;
-    protected $apiVersion = '1.0';
-    /**
-     * @var string 订单消息回调地址
-     */
-    protected $notifyUrl;
-
-    public function __construct($accessToken)
-    {
-        if (getenv('YII_ENV') == 'production') {
-            $cfg = [
-                'base_url' => 'https://openapi.huolala.cn/v1',
-                'app_key' => '8S2uZQrWwM1mkG2Cj576PsMiNIe25UNT',
-                'secret' => 'olzor07UgmUWaagahNGQAKQKXRtvhK8j',
-                'notify_url' => 'https://api.shop.wixhb.com/delivery/huolala-callback',
-            ];
-        } else {
-            $cfg = [
-                'base_url' => 'https://openapi-pre.huolala.cn/v1',
-                'app_key' => '8S2uZQrWwM1mkG2Cj576PsMiNIe25UNT',
-                'secret' => 'olzor07UgmUWaagahNGQAKQKXRtvhK8j',
-                'notify_url' => 'https://api.shop.hzghd.com/delivery/huolala-callback',
-            ];
-        }
-        $this->baseUrl = $cfg['base_url'];
-        $this->appSecret = $cfg['secret'];
-        $this->appKey = $cfg['app_key'];
-        $this->accessToken = $accessToken;
-        $this->notifyUrl = $cfg['notify_url'];
-    }
-
     /**
      * 获取已开通城市列表
      * 
      * 通过此接口可获取到货拉拉货运业务所有已开通的城市列表信息
      * 
      * @return array 包含城市列表的响应数据
-     *   [
-     *       'ret' => 0,
-     *       'msg' => 'Success',
-     *       'data' => [
-     *           'expire' => 1640728800,  // 城市信息缓存刷新时间
-     *           'city_list' => [
-     *               [
-     *                   'city_id' => 1001,
-     *                   'name' => '广州',
-     *                   'name_en' => 'guangzhou',
-     *                   'is_hot' => 1,
-     *                   'lat_lon' => ['lat' => 23.129061, 'lon' => 113.26435],
-     *                   'adcode' => '440100',
-     *               ],
-     *               // ... 更多城市
-     *           ]
-     *       ]
-     *   ]
      */
     public function cityList()
     {
@@ -87,56 +35,6 @@ class HuolalaAdapter implements Adapter
      * 
      * @param int $cityId 城市ID
      * @return array|null 包含城市车型信息的响应数据
-     *   [
-     *       'city_id' => 城市ID,
-     *       'name' => 城市名(中文),
-     *       'name_en' => 城市名(英文),
-     *       'city_info_revision' => 城市版本号,
-     *       'vehicle_list' => [
-     *           [
-     *               'order_vehicle_id' => 订单车型ID,
-     *               'vehicle_name' => 订单车型名称,
-     *               'img_url_high_light' => 订单车型图片,
-     *               'vehicle_volume' => 订单车型体积(示例:2.8方),
-     *               'vehicle_weight' => 订单车型载重(示例:500公斤),
-     *               'vehicle_size' => 订单车型尺寸(示例:2.1*1.7*1.6),
-     *               'standard_order_vehicle_id' => 国标车型id,
-     *               'text_desc' => 车型说明,
-     *               'vehicle_attr' => 大小车属性(1-大车,0-小车),
-     *               'price_text_item' => [
-     *                   'base_distancekm' => 起步公里数(km),
-     *                   'base_price_fen' => 起步价格(分),
-     *                   'exceed_segment_price' => [
-     *                       [
-     *                           'start_exdistancekm' => 区间起始公里数(km),
-     *                           'end_exdistancekm' => 区间结束公里数(km),
-     *                           'price_extra_fen' => 区间内每公里收费(分),
-     *                       ],
-     *                       // ... 更多区间
-     *                   ]
-     *               ],
-     *               'vehicle_std_item' => [
-     *                   [
-     *                       'name' => 附加要求名称,
-     *                       'desc' => 附加要求描述,
-     *                       'price_fen' => 附加要求收费(分),
-     *                   ],
-     *                   // ... 更多附加要求
-     *               ],
-     *           ],
-     *           // ... 更多车型
-     *       ],
-     *       'spec_req_item' => [
-     *           [
-     *               'type' => 额外需求ID,
-     *               'name' => 额外需求名称,
-     *               'desc' => 额外需求描述,
-     *               'price_type' => 计价类型(1-固定值计价, 2-百分比计价, 3-司机商议, 4-免费),
-     *               'price_value_fen' => 价格值(分),
-     *           ],
-     *           // ... 更多额外需求
-     *       ]
-     *   ]
      */
     public function getCityVehicleList($cityId)
     {
@@ -292,7 +190,7 @@ class HuolalaAdapter implements Adapter
         ];
 
         // 添加订单消息回调地址
-        $apiData['notify_url'] = urlencode($this->notifyUrl); // 要进行 URL 转码,不然会引起签名验证出错
+        $apiData['notify_url'] = $this->notifyUrl;
 
         return $apiData;
     }
@@ -302,46 +200,7 @@ class HuolalaAdapter implements Adapter
      * 
      * 根据货拉拉官方文档实现下单功能
      * 
-     * @param array $data 订单数据,结构参考:
-     *   [
-     *       'city_id' => 下单城市ID (必需),
-     *       'city_info_revision' => 城市版本号 (必需),
-     *       'order_vehicle_id' => 车型ID (必需),
-     *       'vehicle_std' => ['双排座'] 车型附加要求 (可选),
-     *       'order_time' => 用车时间戳(秒) (必需),
-     *       'contact_name' => 订单联系人 (必需),
-     *       'contact_phone_no' => 订单联系人手机 (必需),
-     *       'addr_info' => 地址信息数组 (必需)
-     *           [
-     *               [
-     *                   'name' => 地址名称,
-     *                   'addr' => 地址详情,
-     *                   'city_id' => 城市ID,
-     *                   'city_name' => 城市名称,
-     *                   'district_name' => 区(县),
-     *                   'house_number' => 楼层及门牌号,
-     *                   'contacts_name' => 联系人,
-     *                   'contacts_phone_no' => 联系方式,
-     *                   'lat_lon' => ['lat' => 纬度, 'lon' => 经度]
-     *               ],
-     *               // ... 更多地址(最多10个)
-     *           ],
-     *       'total_price_fen' => 订单总价(分) (必需),
-     *       'pay_type' => 支付方式 4-账户余额, 0-三方支付 (必需),
-     *       'price_calculate_id' => 计价ID (必需),
-     *       'price_item_encryption' => 估价加密数据 (必需),
-     *       'vehicle_attr' => 大小车属性 (必需),
-     *       'remark' => 订单备注 (可选),
-     *       'spec_req' => [2,5] 城市额外需求 (可选),
-     *       'out_user_id' => 下单人ID (可选),
-     *       'ip' => 下单人IP (可选),
-     *       'notify_url' => 消息回调地址 (可选),
-     *       'coupon_id' => 优惠券ID (可选),
-     *       'invoice_type' => 开票类型 (可选),
-     *       'order_service_type' => 服务类型 (可选),
-     *       'themis_data' => 风控数据 (可选),
-     *       'commodity_info' => 商品信息 (可选),
-     *   ]
+     * @param array $data 订单数据
      * @return array API响应结果
      */
     public function createOrder($order)
@@ -504,7 +363,7 @@ class HuolalaAdapter implements Adapter
 
     /**
      * 查询订单详情
-     * @param $orderUuid  货拉拉订单号 -- 文档上写的是 order_display_id,实则要传 order_uuid
+     * @param string $orderUuid  货拉拉订单号 -- 文档上写的是 order_display_id,实则要传 order_uuid
      * @return array
      */
     public function selectOrder($orderUuid)
@@ -620,6 +479,7 @@ class HuolalaAdapter implements Adapter
     }
 
     /**
+     * @deprecated
      * 获取运费报价(多品类)- 需用户授权
      * 
      * 根据货拉拉官方文档 u-price-multiCalculate 实现多品类估价功能
@@ -627,11 +487,11 @@ class HuolalaAdapter implements Adapter
      * @param array $data 估价参数
      * @param string $orderTime 配送时间
      * @return array|null 包含多个计价方案的报价结果
-     * @deprecated 使用 buildPriceRequests 和 processPriceResponse 替代
+     * @deprecated 使用 buildPriceRequest 和 processPriceResponse 替代
      */
     public function getPrice($data, $orderTime)
     {
-        $requests = $this->buildPriceRequests($data, $orderTime);
+        $requests = $this->buildPriceRequest($data, $orderTime);
         $priceList = [];
         
         foreach ($requests as $requestInfo) {
@@ -646,79 +506,60 @@ class HuolalaAdapter implements Adapter
     }
 
     /**
-     * 构建API请求参数
+     * 获取可选取消原因
      * 
-     * 根据货拉拉API规范构建完整的请求参数,包括签名
+     * 通过此接口可获取可选择的取消原因,包括主原因和子原因
      * 
-     * @param string $apiMethod API方法名
-     * @param array $apiData 业务参数
-     * @param string|null $accessToken 用户授权令牌(可选)
-     * @return array 完整的请求参数
+     * @param array $data 业务参数(当前接口无必需参数,可传空数组)
+     * @return array|null 包含取消原因列表的响应数据
      */
-    protected function buildRequestPayload($apiMethod, $apiData = [], $accessToken = null)
+    public function getCancelReasonList($data)
     {
-        // 生成随机数(nonce_str
-        $nonceStr = $this->generateNonceStr();
+        // 构建请求参数(无业务数据
+        $payload = $this->buildRequestPayload('u-order-cancelReason', []);
         
-        // 生成时间戳
-        $timestamp = time();
-
-        // 构建系统级参数
-        $payload = [
-            'api_method' => $apiMethod,
-            'api_version' => $this->apiVersion,
-            'app_key' => $this->appKey,
-            'timestamp' => $timestamp,
-            'nonce_str' => $nonceStr,
-        ];
-
-        // 如果提供了访问令牌,加入参数
-        if (!empty($accessToken)) {
-            $payload['access_token'] = $accessToken;
-        }
-
-        // 业务数据作为JSON字符串
-        $payload['api_data'] = json_encode($apiData, JSON_UNESCAPED_UNICODE);
-
-        // 计算签名
-        $signature = $this->generateSignature($payload);
-        $payload['signature'] = $signature;
-
-        return $payload;
-    }
-
-    /**
-     * 生成签名
-     * 
-     * 复用 SignHelper 的通用签名算法
-     * 货拉拉要求32位小写MD5签名,而 SignHelper 返回大写,所以需要转为小写
-     * 
-     * @param array $params 待签名参数(不包括signature)
-     * @return string 签名值(32位小写)
-     */
-    protected function generateSignature($params)
-    {
-
-        // 使用 SignHelper 的通用签名算法
-        $sign = SignHelper::makeSign($params, $this->appSecret);
+        // 发送请求
+        $resp = HttpClient::post($this->baseUrl, $payload);
         
-        // 货拉拉 API 要求32位小写,转换为小写
-        return strtolower($sign);
-    }
-
-    /**
-     * 生成随机数字符串
-     * 
-     * @param int $length 长度
-     * @return string 随机字符串
-     */
-    protected function generateNonceStr($length = 16)
-    {
-        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
-        $str = '';
-        for ($i = 0; $i < $length; $i++) {
-            $str .= $chars[random_int(0, strlen($chars) - 1)];
+        // 处理响应
+        if (isset($resp['ret']) && $resp['ret'] == 0 && isset($resp['data'])) {
+            $respData = $resp['data'];
+            
+            // 提取取消原因列表
+            $reasonList = [];
+            if (!empty($respData['reason']) && is_array($respData['reason'])) {
+                foreach ($respData['reason'] as $reason) {
+                    // 处理子原因
+                    $subReasons = [];
+                    if (!empty($reason['sub_reason']) && is_array($reason['sub_reason'])) {
+                        foreach ($reason['sub_reason'] as $subReason) {
+                            $subReasons[] = [
+                                'id' => (int)($subReason['id'] ?? 0),
+                                'sub_name' => $subReason['sub_name'] ?? '',
+                            ];
+                        }
+                    }
+                    
+                    $reasonList[] = [
+                        'id' => $reason['id'],
+                        //'name' => $reason['name'] ?? '',
+                        'name' => $subReasons,
+                    ];
+                }
+            }
+            
+            return [
+                'code' => 0,
+                'data' => [
+                    'reason' => $reasonList,
+                ]
+            ];
         }
-        return $str;
+        
+        return [
+            'code' => $resp['ret'] ?? -1,
+            'msg' => $resp['msg'] ?? 'Unknown error',
+            'data' => null
+        ];
     }
 }

+ 7 - 25
common/components/delivery/services/adapter/ShansongAdapter.php

@@ -193,6 +193,7 @@ class ShansongAdapter extends Shansong implements Adapter
     }
 
     /**
+     * @deprecated
      * * 下单询价 - 订单计费接口
      * 根据闪送官方文档 /openapi/developer/v5/orderCalculate
      *
@@ -208,6 +209,12 @@ class ShansongAdapter extends Shansong implements Adapter
         return $this->processPriceResponse($resp);
     }
 
+    /**
+     * 查询订单详情
+     * @param $orderId
+     * @param $thirdOrderNo
+     * @return array|null
+     */
     public function selectOrder($orderId, $thirdOrderNo)
     {
         // 构建请求体
@@ -315,29 +322,4 @@ class ShansongAdapter extends Shansong implements Adapter
         return $filtered;
     }
 
-    /**
-     * 递归对数组进行排序(包括嵌套数组)
-     * 确保 JSON 编码时键的顺序一致
-     *
-     * @param array $arr 需要排序的数组
-     * @return array 排序后的数组
-     */
-    private function sortArrayRecursive($arr)
-    {
-        if (!is_array($arr)) {
-            return $arr;
-        }
-
-        // 对当前数组进行排序
-        ksort($arr);
-
-        // 递归处理嵌套数组
-        foreach ($arr as $key => $value) {
-            if (is_array($value)) {
-                $arr[$key] = $this->sortArrayRecursive($value);
-            }
-        }
-
-        return $arr;
-    }
 }

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

@@ -311,6 +311,18 @@ class ShunfengAdapter extends Shunfeng implements Adapter
         ];
     }
 
+    /**
+     * 查询订单状态
+     */
+    public function selectOrder($data)
+    {
+
+    }
+
+    /**
+     * @deprecated
+     * @param $data
+     */
     public function getPrice($data)
     {
 //        $requestData = [