Browse Source

实现闪送、货拉拉、蜂鸟这三平台报价,开始创建真实订单

shizhongqi 9 months ago
parent
commit
291158372a

+ 191 - 36
app-ghs/controllers/DeliveryController.php

@@ -1,7 +1,10 @@
 <?php
 namespace ghs\controllers;
 
+use biz\shop\classes\ShopClass;
 use bizGhs\express\classes\ShansAuthTokenClass;
+use bizGhs\order\classes\OrderClass;
+use common\components\delivery\services\adapter\HuolalaAdapter;
 use common\components\util;
 use common\components\delivery\services\DispatchService;
 use Yii;
@@ -48,6 +51,28 @@ class DeliveryController extends BaseController
         header('Location: ' . $authUrl);
     }
 
+    public function actionDadaAuth()
+    {
+        $dadaAuth = new \common\components\delivery\platform\dada\Auth();
+        $ticket = $dadaAuth->getTicket();
+
+        $mainId = $this->mainId;
+        $redirectUrl = 'https://api.shop.hzghd.com/delivery/dada-auth-callback' . '?mainId=' . $mainId;
+        $shopNumber = '40d8391f9b05477b';
+        $state = 'huidiao_biaoshi';
+        $authUrl = $dadaAuth->generateAuthUrl($ticket, $state, $shopNumber);
+        header('Location: ' . $authUrl);
+    }
+
+    public function actionHuolalaVehicle()
+    {
+        $mainId = $this->mainId;
+        $authPlatforms = ShansAuthTokenClass::getByCondition(['user_id'=>$mainId, 'platform'=>'huolala']);
+        $huolalaAdapter = new HuolalaAdapter($authPlatforms['access_token']);
+        $cities = $huolalaAdapter->getCityVehicleList(1006);
+        return $this->asJson($cities);
+    }
+
     // 查询开通城市
     public function actionOpenCitiesLists()
     {
@@ -56,7 +81,26 @@ class DeliveryController extends BaseController
         $cities = $ds->openCitiesLists($platform);
 
         if ($platform == 'shansong') {
+            $newCitiesArr = [];
+
+            $citiesArr = $cities['data'];
+            foreach($citiesArr as $block) {
+                foreach ($block['cities'] as $city) {
+                    $newCitiesArr[$city['name']] = $city;
+                }
+            }
+
+            // 把newCitiesArr保存到 platform/huolala/cities.php 文件中
+            $citiesFile = Yii::getAlias('@common/components/delivery/platform/shansong/cities.php');
+            // 如果文件不存在,则创建文件
+            if (!file_exists($citiesFile)) {
+                file_put_contents($citiesFile, '<?php return []; ?>');
+            }
+            // 文件内容要包含命名空间
+            $content = '<?php return ' . var_export($newCitiesArr, true) . '; ?>';
+            file_put_contents($citiesFile, $content);
 
+            return $newCitiesArr;
         } else if ($platform == 'huolala') {
             $citiesArr = $cities['data']['city_list'];
             $newCitiesArr = [
@@ -86,51 +130,112 @@ class DeliveryController extends BaseController
         return $cities;
     }
 
+    public function actionAddress()
+    {
+        $post = Yii::$app->request->post();
+
+        $adminId = $this->adminId;
+        util::checkRepeatCommit($adminId, 4);
+
+        $orderId = intval($post['orderId']);
+        $order = OrderClass::getById($orderId, false, 'fullAddress');
+        $shop = ShopClass::getById($this->shopId, false, 'fullAddress');
+
+        $ret = [
+            "send_address" => $shop['fullAddress'],
+            "receive_address" => $order['fullAddress']
+        ];
+        util::success($ret, "success");
+    }
+
     // 获取多个平台报价
-    public function actionAllPlatformPrice()
+    public function actionAllDeliveryQuotes()
     {
+        $post = Yii::$app->request->post();
+
+        $adminId = $this->adminId;
+        util::checkRepeatCommit($adminId, 4);
+
+        $orderId = intval($post['orderId']);
+        $order = OrderClass::getById($orderId);
+        $shop = ShopClass::getById($this->shopId);
+
         $ds = new DispatchService($this->mainId);
+        $platforms = $ds->getBestPlatformByPrice($order, $shop);
+
+        $deliveryList = [];
+        foreach ($platforms['quotes'] as $item) {
+            switch ($item['platform']) {
+                case 'shansong':
+                    $deliveryList[] = [
+                        'name' => '闪送',
+                        'en_name' => 'shansong',
+                        'price' => $item['total_amount'],
+                        'distance' => $item['total_distance'],
+                        'type' => ''
+                    ];
+                    break;
+                case 'fengniao':
+                    // 循环遍历 goods_infos 数组,提取有效项的信息
+                    $validGoods = [];
+                    if (!empty($item['goods_infos']) && is_array($item['goods_infos'])) {
+                        foreach ($item['goods_infos'] as $good) {
+                            if (!empty($good['is_valid']) && $good['is_valid'] == 1) {
+                                $validGoods[] = [
+                                    'actual_delivery_amount_cent' => $good['actual_delivery_amount_cent'] ?? 0,
+                                    'service_goods_id' => $good['service_goods_id'] ?? ''
+                                ];
+                            }
+                        }
+                    }
+                    $deliveryList[] = [
+                        'name' => '蜂鸟',
+                        'en_name' => 'fengniao',
+                        'price' => !empty($validGoods) ? $validGoods[0]['actual_delivery_amount_cent'] : 0,
+                        'distance' => $item['distance'],
+                        'valid_goods' => $validGoods,  // 返回所有有效的商品信息
+                        'type' => ''
+                    ];
+                    break;
+                case 'huolala':
+                    $deliveryList[] = [
+                        'name' => '货拉拉',
+                        'en_name' => 'huolala',
+                        'price' => isset($item['price_list']) ? $item['price_list'][0]['price_conditions'][0]['total_price'] : 0,
+                        'distance' => isset($item['price_list']) ? $item['price_list'][0]['distance_info']['distance_total'] : 0,
+                        'type' => $item['vehicle_type'],
+                        'city_id' => $item['city_id'],
+                        'city_info_revision' => $item['city_info_revision'],
+                    ];
+                    break;
+            }
+        }
 
-        $orderData = [
-              'city_name' => '北京市',
-              'sender' => [
-                  'from_address' => '东升科技国际园',
-                  'from_address_detail' => '2层202',
-                  'from_sender_name' => '小闪',
-                  'from_mobile' => '13800000000',
-                  'from_latitude' => '40.047858',
-                  'from_longitude' => '116.378424',
-              ],
-              'receiver_list' => [
-                  [
-                      'order_no' => 'C1119A000013053981',
-                      'to_address' => '永泰庄地铁站',
-                      'to_address_detail' => '1楼',
-                      'to_receiver_name' => '小送',
-                      'to_mobile' => '13800000001',
-                      'to_latitude' => '40.043612',
-                      'to_longitude' => '116.361199',
-                      'good_type' => 5,           // 物品类型
-                      'weight' => 2,              // 物品重量(kg,整数)
-                      //'remarks' => '',            // 备注
-                  ]
-              ],
-              'appoint_type' => 0,              // 0: 立即单,1: 预约单
-              'appointment_date' => '',         // 预约时间 yyyy-MM-dd HH:mm
-              //'store_id' => 393549,             // 店铺ID
-              'travel_way' => 0,                // 指定交通工具,0: 不限交通方式
-              'delivery_type' => 1,             // 1: 帮我送,2: 帮我取
-              'expect_start_time' => null,      // 期望送达时间起始(毫秒级时间戳)
-              'expect_end_time' => null,        // 期望送达时间终止(毫秒级时间戳)
-        ];
-        $re = $ds->getBestPlatformByPrice($orderData);
-        return $this->asJson($re);
+        $ret['deliveryList'] = $deliveryList;
+        util::success($ret, "success");
     }
 
-    // 创建订单(发单)
+    // 创建订单(真实下单)
+    public function actionCreateOrder()
+    {
+        $post = Yii::$app->request->post();
+        $platform = $post['platform'];
+
+        // huolala 需要有以下参数
+        // 'city_id' => 下单城市ID (必需),
+        // 'city_info_revision' => 城市版本号 (必需) -- 下单城市版本号, 示例:296,从u-city-info接口获取
+        // 'order_vehicle_id' => 车型ID (必需),
+        // 'vehicle_std' => ['双排座'] 车型附加要求 (可选),
+        // 'order_time' => 用车时间戳(秒) (必需),
+
+        $ds = new DispatchService($this->mainId, $platform);
+        $order = [];
+        $platforms = $ds->createOrder($order);
+    }
 
     // 查询订单(查单)
 
+
     // 第三方回调入口
     /**
      * 回调接口
@@ -266,5 +371,55 @@ class DeliveryController extends BaseController
             }
         }
         Yii::info('蜂鸟授权回调:' . json_encode($callbackData, JSON_UNESCAPED_UNICODE), 'fengniao');
+
+        $mainId = $get['mainId'];
+        $code = $get['code'];
+        $merchantId = $get['merchant_id'];
+        $scope = $get['scope'];
+        $state = $get['state'];
+
+        // 获取AccessToken
+        $auth = new \common\components\delivery\platform\fengniao\Auth();
+        $result = $auth->getAccessToken($code, $merchantId);
+
+        if (!$result['success']) {
+            throw new \yii\web\HttpException(500, '获取授权失败:' . $result['error']);
+        }
+        $result = $result['data'];
+
+        // 保存授权信息到数据库
+        $data = [
+            'user_id' => $mainId,
+            'shop_id' => '',
+            'access_token' => $result['access_token'],
+            'refresh_token' => $result['refresh_token'],
+            'expires_at' => $result['expire_in'],
+            'auth_type' => 'all_store',
+            'platform' => 'fengniao'
+        ];
+
+        ShansAuthTokenClass::add($data);
+
+        return $this->asJson(['return_code' => 0, 'return_msg' => 'OK']);
+    }
+
+    /**
+     * 达达授权回调接口
+     */
+    public function actionDadaAuthCallback()
+    {
+        $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');
     }
 }

+ 145 - 0
app-ghs/controllers/DeliveryFengniaoController.php

@@ -0,0 +1,145 @@
+<?php
+namespace ghs\controllers;
+
+use bizGhs\express\classes\ShansAuthTokenClass;
+use common\components\util;
+use common\components\delivery\services\DispatchService;
+use Yii;
+use common\components\delivery\services\adapter\FengniaoAdapter;
+use ghs\controllers\BaseController;
+use common\components\delivery\platform\fengniao\Auth;
+
+/**
+ * 蜂鸟配送 API 测试控制器
+ * 
+ * ⚠️ 常见错误诊断:apiCode=B0112 -- 门店不存在
+ * 
+ * 可能的原因:
+ * 1. chain_store_id 填写错误或不存在 ← 最可能!
+ * 2. chain_store_id 所属的城市不在蜂鸟配送范围内
+ * 3. 收货地址超出该门店的配送范围
+ * 4. 收货坐标系与接口要求不匹配(建议使用高德地图坐标系 position_source=3)
+ * 5. 门店已被禁用或删除
+ * 
+ * 解决步骤:
+ * 1. 确认 chain_store_id = '14594092' 是否真实存在
+ * 2. 使用 actionCityList() 验证收货城市是否在蜂鸟配送范围内
+ * 3. 修改收货地址到该门店的配送范围内
+ * 4. 确保使用高德地图坐标系(position_source = 3)
+ * 5. 查看运行时日志(runtime/logs/app.log)了解更多错误细节
+ */
+class DeliveryFengniaoController extends BaseController
+{
+    private const TEST_SHOP_ID = '467788524';
+
+    public function actionGetToken()
+    {
+        $auth = new Auth();
+        $auth->setMerchantId('14594092');
+        $accessToken = $auth->getAccessToken('H9IbEc4t5lf4w96KJ7yjQT');
+        return $this->asJson($accessToken);
+    }
+
+    /**
+     * 获取蜂鸟配送覆盖的城市列表
+     * 用于诊断:收货地址所在城市是否在蜂鸟配送范围内
+     */
+    public function actionCityList()
+    {
+        $fap = new FengniaoAdapter('5e1577dc-487d-4f56-96e0-e4b3aba7326b');
+        $fap->setMerchantId('14594092');
+        
+        Yii::info("=== 获取城市列表 ===");
+        $re = $fap->cityList();
+        
+        Yii::info("=== 城市列表响应 ===");
+        Yii::info(json_encode($re, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
+        
+        return $this->asJson($re);
+    }
+
+    public function actionCreateOrder()
+    {
+        $order = $this->getValidCreateOrderData();
+        $fap = new FengniaoAdapter('5e1577dc-487d-4f56-96e0-e4b3aba7326b');
+        $fap->setMerchantId('14594092');
+        $fap->createOrder($order);
+    }
+
+    public function actionPreCreateOrder()
+    {
+        $order = $this->getValidCreateOrderData();
+        $fap = new FengniaoAdapter('5e1577dc-487d-4f56-96e0-e4b3aba7326b');
+        $fap->setMerchantId('14594092');
+        
+        // 添加调试日志
+        Yii::info("=== 预下单接口请求 ===");
+        Yii::info("门店ID: " . $order['chain_store_id']);
+        Yii::info("商户ID: 14594092");
+        Yii::info("收货地址: " . $order['receiver_address']);
+        Yii::info("收货坐标: {$order['receiver_latitude']}, {$order['receiver_longitude']}");
+        
+        $re = $fap->getPrice($order);
+        
+        Yii::info("=== 预下单接口响应 ===");
+        Yii::info(json_encode($re, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
+        
+        return $this->asJson($re);
+    }
+
+    public function actionPreCancelOrder()
+    {
+        $fap = new FengniaoAdapter('5e1577dc-487d-4f56-96e0-e4b3aba7326b');
+        $fap->setMerchantId('14594092');
+        $cancelOrder = [
+            "order_id" => '100000000191491340',
+            'order_cancel_role' => 1,
+            "order_cancel_code" => 6,
+            'order_cancel_other_reason' => '订单取消'
+        ];
+        $re = $fap->cancelOrder($cancelOrder);
+    }
+
+
+
+    /**
+     * 获取有效的创建订单数据
+     */
+    private function getValidCreateOrderData(): array
+    {
+        return [
+            'partner_order_code' => 'TEST_ORDER_' . time() . '_' . uniqid(),
+            'receiver_primary_phone' => '13800000000',
+            'receiver_name' => '张三',
+            'receiver_latitude' => 39.9042,
+            'receiver_longitude' => 116.4074,
+            'receiver_address' => '北京市朝阳区建国门外大街1号',
+            'position_source' => 3,
+            'goods_count' => 2,
+            'goods_weight' => 1.5,
+            'goods_total_amount_cent' => 5000,
+            'goods_actual_amount_cent' => 4500,
+            'goods_item_list' => [
+                [
+                    'item_id' => 'ITEM_001',
+                    'item_name' => '玫瑰花束',
+                    'item_amount_cent' => 3000,
+                    'item_actual_amount_cent' => 2700,
+                    'item_quantity' => 1,
+                    'item_size' => 2,
+                ],
+                [
+                    'item_id' => 'ITEM_002',
+                    'item_name' => '贺卡',
+                    'item_amount_cent' => 2000,
+                    'item_actual_amount_cent' => 1800,
+                    'item_quantity' => 1,
+                    'item_size' => 1,
+                ],
+            ],
+            'order_type' => 1,
+            'chain_store_id' => self::TEST_SHOP_ID,
+            'order_remark' => '请轻拿轻放',
+        ];
+    }
+}

+ 7 - 2
common/components/delivery/helpers/HttpClient.php

@@ -22,13 +22,18 @@ class HttpClient
             'Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8',
         ], $headers);
 
-        $body = http_build_query($data);
+        // 根据 Content-Type 决定请求体格式
+        if (strpos($headers['Content-Type'], 'application/json') !== false) {
+            $body = json_encode($data);
+        } else {
+            $body = http_build_query($data);
+        }
 
         for ($i = 0; $i <= $retry; $i++) {
             try {
                 $response = $client->post($url, [
                     'headers' => $headers,
-                    'body' => $body,
+                    'body' => $body
                 ]);
 
                 $status = $response->getStatusCode();

+ 415 - 0
common/components/delivery/platform/dada/Auth.php

@@ -4,7 +4,422 @@ namespace common\components\delivery\platform\dada;
 use common\components\delivery\helpers\HttpClient;
 use Yii;
 
+/**
+ * 达达(DaDa)开放平台授权认证类
+ * 
+ * 处理达达开放平台的授权流程,包括:
+ * - OAuth2.0 授权
+ * - 授权码兑换令牌
+ * - 令牌刷新
+ */
 class Auth
 {
+    // OAuth 参数
+    const GRANT_TYPE_AUTH_CODE = 'authorization_code';
+    const GRANT_TYPE_REFRESH = 'refresh_token';
+    
+    // 授权端点
+    const AUTHORIZE_URL_PROD = 'https://newopen.imdada.cn/';
+    const TOKEN_URL_PROD = '';
+    
+    const AUTHORIZE_URL_TEST = 'https://newopen.qa.imdada.cn/';
+    const TOKEN_URL_TEST = '';
+    
+    protected $appKey;
+    protected $appSecret;
+    protected $redirectUri;
+    protected $isSandbox;
+    protected $authorizeUrl;
+    protected $tokenUrl;
+    
+    /**
+     * 初始化授权类
+     * 根据环境获取配置信息
+     */
+    public function __construct()
+    {
+        $isProduction = getenv('YII_ENV') == 'dev';
+        
+        if ($isProduction) {
+            // 生产环境配置(需要替换为实际的生产环境凭证)
+            $this->appKey = 'dadaf2279d901a5ba40';
+            $this->appSecret = '828a03677a1c00a3a5b3c59209c4433d';
+            $this->redirectUri = 'https://api.shop.hzghd.com/delivery/dada-auth-callback';
+            $this->authorizeUrl = self::AUTHORIZE_URL_PROD;
+            $this->tokenUrl = self::TOKEN_URL_PROD;
+        } else {
+            // 测试环境配置
+            $this->appKey = 'dadaf2279d901a5ba40';
+            $this->appSecret = '828a03677a1c00a3a5b3c59209c4433d';
+            $this->redirectUri = 'https://api.shop.hzghd.com/delivery/dada-auth-callback';
+            $this->authorizeUrl = self::AUTHORIZE_URL_TEST;
+            $this->tokenUrl = self::TOKEN_URL_TEST;
+        }
+        
+        $this->isSandbox = !$isProduction;
+    }
 
+    /**
+     * 获取授权码
+     * 
+     * 调用达达 API 获取一次性授权码(ticket),后续授权流程需要此 ticket
+     * 接口地址:GET /third/party/ticket
+     * 
+     * 返回结果参数说明:
+     * - status: 响应状态
+     * - code: 响应编码
+     * - msg: 响应描述
+     * - result: ticket,一次性准入码
+     * - errorCode: 错误编码
+     * 
+     * @return string|null ticket 一次性授权码,获取失败返回 null
+     */
+    public function getTicket()
+    {
+        // 生成随机数
+        // $nonce = $this->generateNonce(15);
+        $nonce = 'VV7JK4BJXAUSYP8'; // TODO 写死,后期修改回去
+        
+        // 生成签名
+        $sign = $this->generateTicketSign($nonce);
+        
+        // 构建请求参数
+        $params = [
+            'appKey' => $this->appKey,
+            'nonce' => $nonce,
+            'sign' => strtoupper($sign),
+        ];
+        
+        // 发送 GET 请求
+        $url = $this->authorizeUrl . 'third/party/ticket';// . '?' . http_build_query($params);
+        
+        try {
+            $resp = HttpClient::get($url, $params); // $params
+            
+            // 检查响应状态,返回 ticket
+            if (isset($resp['result']) && !empty($resp['result'])) {
+                Yii::debug('成功获取授权码:' . $resp['result']);
+                return $resp['result'];
+            }
+            
+            Yii::warning('获取授权码失败:' . json_encode($resp));
+            return null;
+        } catch (\Exception $e) {
+            Yii::error('获取授权码异常:' . $e->getMessage());
+            return null;
+        }
+    }
+    
+    /**
+     * 生成获取授权码请求的签名
+     * 
+     * 签名算法:
+     * 1. 参与签名的参数按字典排序:appKey、appSecret、nonce
+     * 2. 拼接参数值
+     * 3. SHA1 加密
+     * 
+     * 示例:
+     * - appKey: dada6c68011157c5f63
+     * - appSecret: 828a03677a1c00a3a5b3c59209c4433d
+     * - nonce: RHU3RY4YR234238
+     * 排序后拼接:dada6c68011157c5f63828a03677a1c00a3a5b3c59209c4433dRHU3RY4YR234238
+     * SHA1(上述字符串)
+     * 
+     * @param string $nonce 随机数
+     * @return string SHA1 签名
+     */
+    private function generateTicketSign($nonce)
+    {
+        // 参与签名的参数
+        $signParams = [
+            'appKey' => $this->appKey,
+            'nonce' => $nonce,
+            'appSecret' => $this->appSecret,
+        ];
+        
+        // 第一步:按键排序
+        ksort($signParams);
+        // 第一步:按值排序且不改变key
+        //asort($signParams);
+        //sort($signParams, SORT_STRING);
+       
+        
+        // 第二步:拼接所有参数值
+        $signString = implode('', $signParams);
+        //$signString = $signString . $this->appSecret;
+        // 第二步:拼接 key 和 value
+//        $signString = '';
+//        foreach ($signParams as $key => $value) {
+//            $signString .= $key . $value;
+//        }
+//        $signString = $signString . 'appSecret' . $this->appSecret;
+        
+        // 第三步:SHA1 加密
+        $hex = sha1($signString);
+
+        $len = 36;
+        if ($len < strlen($hex)) {
+            $hex = substr($hex, 0, $len);
+        }
+        return $hex;
+    }
+
+    private function newGenerateTicketSign($nonce)
+    {
+        // 参与签名的参数
+        $signParams = [
+            'appKey' => $this->appKey,
+            'nonce' => $nonce,
+            'appSecret' => $this->appSecret,
+        ];
+        
+        // 第一步:将参与签名的参数按照键值(key)进行字典排序
+        ksort($signParams);
+        
+        // 第二步:将排序过后的参数,进行key和value字符串拼接
+        $signString = '';
+        foreach ($signParams as $key => $value) {
+            $signString .= $key . $value;
+        }
+        
+        // 第三步:将拼接后的字符串首尾加上app_secret秘钥,合成签名字符串
+        $finalSignString = $this->appSecret . $signString . $this->appSecret;
+        
+        // 第四步:对签名字符串进行MD5加密,生成32位的字符串
+        $sign = md5($finalSignString);
+        
+        // 第五步:将签名生成的32位字符串转换为大写
+        return strtoupper($sign);
+    }
+
+    /**
+     * 生成授权链接
+     * 
+     * 用户需要访问此链接进行授权,授权后会重定向到 redirectUri
+     * 根据达达文档,授权链接需要以下参数:
+     * - appKey: 应用 key
+     * - shopId: 三方门店编号(可选)
+     * - redirectUrl: 回调地址
+     * - state: 回调标识(用于 CSRF 防护)
+     * - nonce: 随机数
+     * - ticket: 渠道授权码(需要从获取渠道授权码接口获取)
+     * - sign: 签名字符串
+     * - resultType: 是否跳转(可选,0-默认结果页,1-跳转)
+     * 
+     * @param string $ticket 渠道授权码(从"获取渠道授权码"接口获取)
+     * @param string $state 应用程序定义的不透明值(用于 CSRF 防护)
+     * @param string $shopId 三方门店编号(可选)
+     * @param int $resultType 是否跳转(可选,0-默认结果页,1-跳转)
+     * @return string 授权链接
+     * @throws \Exception 当缺少必要参数时
+     */
+    public function generateAuthUrl($ticket, $state = '', $shopId = '', $resultType = 0)
+    {
+        if (empty($ticket)) {
+            throw new \Exception('ticket 参数不能为空,需要从"获取渠道授权码"接口获取');
+        }
+        
+        // 生成随机数 nonce
+        $nonce = $this->generateNonce();
+        
+        // 生成签名
+        $sign = $this->generateSign($ticket, $nonce, $shopId);
+        
+        // 构建授权链接参数
+        $params = [
+            'appKey' => $this->appKey,
+            'redirectUrl' => $this->redirectUri,
+            'state' => $state ?: uniqid(),
+            'nonce' => $nonce,
+            'ticket' => $ticket,
+            'sign' => $sign,
+        ];
+        
+        // 添加可选参数
+        if (!empty($shopId)) {
+            $params['shopId'] = $shopId;
+        }
+        
+        if ($resultType != 0) {
+            $params['resultType'] = $resultType;
+        }
+        
+        return $this->authorizeUrl . 'third/party/oauth' . '?' . http_build_query($params);
+    }
+    
+    /**
+     * 生成随机数(nonce)
+     * 
+     * @return string 8位随机字母数字组合
+     */
+    private function generateNonce($length = 8)
+    {
+        $characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+        $nonce = '';
+        for ($i = 0; $i < $length; $i++) {
+            $nonce .= $characters[rand(0, strlen($characters) - 1)];
+        }
+        return $nonce;
+    }
+    
+    /**
+     * 生成签名(sign)
+     * 
+     * 签名算法:
+     * 1. 参与签名的参数按字典顺序排列:appKey、nonce、ticket、shopId(如果有)
+     * 2. 按 key+value 拼接
+     * 3. 首尾加上 appSecret
+     * 4. MD5 加密
+     * 5. 转大写
+     * 
+     * @param string $ticket 渠道授权码
+     * @param string $nonce 随机数
+     * @param string $shopId 三方门店编号(可选)
+     * @return string 签名字符串(32位大写MD5)
+     */
+    private function generateSign($ticket, $nonce, $shopId = '')
+    {
+        // 参与签名的参数
+        $signParams = [
+            'appKey' => $this->appKey,
+            'nonce' => $nonce,
+            'ticket' => $ticket,
+        ];
+        
+        // 如果有shopId,也加入签名参数
+        if (!empty($shopId)) {
+            $signParams['shopId'] = $shopId;
+        }
+        
+        // 第一步:按键值字典排序
+        ksort($signParams);
+        
+        // 第二步:拼接 key 和 value
+        $signString = '';
+        foreach ($signParams as $key => $value) {
+            $signString .= $key . $value;
+        }
+        
+        // 第三步:首尾加上 appSecret
+        $finalSignString = $this->appSecret . $signString . $this->appSecret;
+        
+        // 第四步:MD5 加密
+        $sign = md5($finalSignString);
+        
+        // 第五步:转大写
+        return strtoupper($sign);
+    }
+
+
+    
+    /**
+     * 通过授权码获取访问令牌
+     * 
+     * 用户授权后,使用授权码兑换访问令牌
+     * 
+     * @param string $code 授权码(从授权回调中获取)
+     * @return array|null 令牌信息
+     *   [
+     *       'access_token' => '访问令牌',
+     *       'token_type' => 'Bearer',
+     *       'expires_in' => 令牌过期时间(秒),
+     *       'refresh_token' => '刷新令牌',
+     *       'scope' => '授权范围'
+     *   ]
+     */
+    public function getAccessToken($code)
+    {
+        if (empty($code)) {
+            return null;
+        }
+        
+        $params = [
+            'grant_type' => self::GRANT_TYPE_AUTH_CODE,
+            'code' => $code,
+            'client_id' => $this->appKey,
+            'client_secret' => $this->appSecret,
+            'redirect_uri' => $this->redirectUri,
+        ];
+        
+        $resp = HttpClient::post($this->tokenUrl, $params);
+        
+        // 检查响应是否成功
+        if (isset($resp['access_token'])) {
+            return [
+                'access_token' => $resp['access_token'],
+                'token_type' => $resp['token_type'] ?? 'Bearer',
+                'expires_in' => $resp['expires_in'] ?? 3600,
+                'refresh_token' => $resp['refresh_token'] ?? null,
+                'scope' => $resp['scope'] ?? '',
+                'create_time' => time(),
+            ];
+        }
+        
+        Yii::error('Failed to get access token: ' . json_encode($resp));
+        return null;
+    }
+    
+    /**
+     * 使用刷新令牌获取新的访问令牌
+     * 
+     * 当访问令牌过期时,使用刷新令牌获取新的访问令牌
+     * 
+     * @param string $refreshToken 刷新令牌
+     * @return array|null 新的令牌信息
+     */
+    public function refreshAccessToken($refreshToken)
+    {
+        if (empty($refreshToken)) {
+            return null;
+        }
+        
+        $params = [
+            'grant_type' => self::GRANT_TYPE_REFRESH,
+            'refresh_token' => $refreshToken,
+            'client_id' => $this->appKey,
+            'client_secret' => $this->appSecret,
+        ];
+        
+        $resp = HttpClient::post($this->tokenUrl, $params);
+        
+        // 检查响应是否成功
+        if (isset($resp['access_token'])) {
+            return [
+                'access_token' => $resp['access_token'],
+                'token_type' => $resp['token_type'] ?? 'Bearer',
+                'expires_in' => $resp['expires_in'] ?? 3600,
+                'refresh_token' => $resp['refresh_token'] ?? $refreshToken,
+                'scope' => $resp['scope'] ?? '',
+                'create_time' => time(),
+            ];
+        }
+        
+        Yii::error('Failed to refresh access token: ' . json_encode($resp));
+        return null;
+    }
+    
+    /**
+     * 检查访问令牌是否已过期
+     * 
+     * @param array $tokenInfo 令牌信息
+     *   [
+     *       'access_token' => '...',
+     *       'expires_in' => 3600,
+     *       'create_time' => 时间戳,
+     *   ]
+     * @return bool 是否已过期
+     */
+    public function isTokenExpired($tokenInfo)
+    {
+        if (empty($tokenInfo) || !is_array($tokenInfo)) {
+            return true;
+        }
+        
+        $createTime = $tokenInfo['create_time'] ?? 0;
+        $expiresIn = $tokenInfo['expires_in'] ?? 0;
+        $currentTime = time();
+        
+        // 提前 300 秒(5 分钟)进行刷新,避免临界情况
+        return ($createTime + $expiresIn - 300) <= $currentTime;
+    }
 }

+ 0 - 192
common/components/delivery/platform/fengniao/00_START_HERE.txt

@@ -1,192 +0,0 @@
-================================================================================
-                    峰鸟平台认证模块 - 快速导航
-                          2025-10-29 更新
-================================================================================
-
-📌 首次使用指南:
-
-1. 【快速入门】→ 读这个文件
-   - 5 分钟了解如何使用
-
-2. 【详细参考】→ QUICK_REFERENCE.md
-   - 快速速查表,所有关键 API
-
-3. 【完整文档】→ IMPLEMENTATION_GUIDE.md
-   - 深入了解签名算法、API 文档、错误处理
-
-4. 【修改说明】→ CHANGES_SUMMARY.md
-   - 了解从旧版本的所有变更内容
-
-5. 【项目概览】→ README.md
-   - 完整的项目文档和文件说明
-
-================================================================================
-
-🚀 快速开始 - 5 行代码获取 Token:
-
-    use common\components\delivery\platform\fengniao\Auth;
-    
-    $auth = new Auth();
-    $result = $auth->getAccessToken('授权码', '商户ID');
-    
-    echo $result['data']['access_token'];  // 获得 Token!
-
-================================================================================
-
-📋 修改概览:
-
-✅ 新增功能:
-   • SHA-256 签名算法(自动生成)
-   • 完整的商户 ID 支持
-   • 毫秒级时间戳处理
-   • 返回更多信息(expire_in 等)
-
-⚠️  破坏性变更:
-   • 请求方式从 GET 改为 POST
-   • 返回值从扁平化改为嵌套格式
-   • 需要提供商户 ID 和时间戳
-   • 参数结构完全不同
-
-✅ 旧方法仍保留:
-   • getAppSecret()
-   • isSandbox()
-
-================================================================================
-
-📁 文件结构:
-
-    Auth.php                    ← 主认证类(关键文件)
-    README.md                   ← 完整项目文档
-    QUICK_REFERENCE.md         ← 快速参考(推荐!)
-    IMPLEMENTATION_GUIDE.md    ← 详细实现指南
-    CHANGES_SUMMARY.md         ← 详细修改说明
-    cities.php                 ← 城市列表数据
-    00_START_HERE.txt          ← 本文件
-
-================================================================================
-
-🔑 核心方法:
-
-    $auth->getAccessToken($code, $merchantId)
-    $auth->refreshAccessToken($refreshToken, $merchantId)
-    $auth->setMerchantId($merchantId)
-    $auth->getAppId()
-    $auth->isSandbox()
-
-================================================================================
-
-🔐 环境配置:
-
-    沙箱环境:export YII_ENV=dev         (默认)
-    正式环境:export YII_ENV=production
-
-    API URLs:
-    - 获取 Token: https://open-anubis.ele.me/anubis-webapi/openapi/token
-    - 刷新 Token: https://open-anubis.ele.me/anubis-webapi/openapi/refreshToken
-
-================================================================================
-
-❌ 常见问题解决:
-
-    Q: 商户ID从哪里获取?
-    A: 从峰鸟授权回调中获得
-
-    Q: 签名失败?
-    A: 检查日志中的 "Sign Before" 字符串和 appSecret
-
-    Q: 怎样验证签名生成?
-    A: 查看日志输出或使用在线 SHA-256 工具验证
-
-================================================================================
-
-📚 相关文档推荐阅读顺序:
-
-    1️⃣  QUICK_REFERENCE.md      (10 分钟) - 快速掌握 API
-    2️⃣  README.md               (15 分钟) - 了解项目全景
-    3️⃣  IMPLEMENTATION_GUIDE.md (20 分钟) - 深入理解细节
-    4️⃣  CHANGES_SUMMARY.md      (10 分钟) - 理解版本升级
-
-================================================================================
-
-✅ 检查清单(迁移旧版本时):
-
-    [ ] 修改代码中所有调用方式
-    [ ] 更新返回值访问方式(data 嵌套)
-    [ ] 添加商户 ID 参数
-    [ ] 测试沙箱环境
-    [ ] 查看日志验证签名
-    [ ] 部署前充分测试
-
-================================================================================
-
-🔗 关键 API 示例:
-
-    获取 Token:
-    ─────────────────────────────────────────────
-    $result = $auth->getAccessToken($code, '6665');
-    if ($result['success']) {
-        $token = $result['data']['access_token'];
-        $expireTime = $result['data']['expire_in'];  // 秒数
-    }
-
-    刷新 Token:
-    ─────────────────────────────────────────────
-    $result = $auth->refreshAccessToken($refreshToken, '6665');
-    if ($result['success']) {
-        $newToken = $result['data']['access_token'];
-    }
-
-    错误处理:
-    ─────────────────────────────────────────────
-    if (!$result['success']) {
-        $error = $result['error'];
-        Yii::error("认证失败: {$error}", 'fengniao');
-    }
-
-================================================================================
-
-📊 签名算法(自动处理):
-
-    流程:参数排序 → 字典序拼接 → 追加 appSecret → SHA-256 加密
-
-    示例:
-    app_id=222&code=4444&grant_type=authorization_code&merchant_id=333&timestamp=1719297100558
-    ↓
-    111app_id=222&code=4444&grant_type=authorization_code&merchant_id=333&timestamp=1719297100558
-    ↓
-    06abc54c633f1636e3d03cc6c6e36a113529db6fb4f2e0b4c64535c6baa69a15
-
-================================================================================
-
-💡 最佳实践:
-
-    1. 将 appId/appSecret 移到配置文件
-    2. 实现 Token 缓存机制
-    3. 详细记录 API 错误日志
-    4. 正式部署前在沙箱测试
-    5. 监控 Token 过期时间
-
-================================================================================
-
-❓ 需要帮助?
-
-    1. 查看 QUICK_REFERENCE.md 找快速答案
-    2. 查看 IMPLEMENTATION_GUIDE.md 获取详细信息
-    3. 检查日志:tail -f @app/runtime/logs/app.log | grep FengniaAuth
-    4. 参考峰鸟官方 API 文档
-
-================================================================================
-
-📌 重要提示:
-
-    • 所有请求/响应都需要签名(自动处理)
-    • 时间戳必须是毫秒级(自动生成)
-    • Token 有效期默认一年
-    • 刷新接口 10 分钟内只会真正刷新一次
-    • 沙箱环境 code 参数可以为空
-
-================================================================================
-
-最后更新: 2025-10-29
-作者: 开发团队
-

+ 27 - 12
common/components/delivery/platform/fengniao/Auth.php

@@ -3,6 +3,8 @@ namespace common\components\delivery\platform\fengniao;
 
 use common\components\delivery\helpers\HttpClient;
 use Yii;
+use yii\helpers\Json;
+use linslin\yii2\curl;
 
 class Auth
 {
@@ -32,8 +34,8 @@ class Auth
         $isProduction = getenv('YII_ENV') == 'production';
 
         // 配置(需要替换为真实的appId和appSecret)
-        $this->appId = '6587209115185920913';
-        $this->appSecret = '3f935d5f-bf65-467e-a61c-72cfb1d53960';
+        $this->appId = '6587209115185920913';// 3659064244254722812  --  6587209115185920913
+        $this->appSecret = '3f935d5f-bf65-467e-a61c-72cfb1d53960'; // dce31e3b-32da-45ed-8353-f2f041b5288f   --   3f935d5f-bf65-467e-a61c-72cfb1d53960
         $this->merchantId = ''; // 需要从配置中获取或外部设置
 
         // 根据环境设置沙箱标志
@@ -81,10 +83,10 @@ class Auth
      */
     protected function generateSignature(array $params)
     {
-        // Step 1: 过滤空值
-        $params = array_filter($params, function ($v) {
-            return $v !== null && $v !== '';
-        });
+        // Step 1: 过滤空值  --  不能清除,不然 code 就被排除了
+//        $params = array_filter($params, function ($v) {
+//            return $v !== null && $v !== '';
+//        });
 
         // Step 2: 按字典序排序
         ksort($params);
@@ -93,6 +95,9 @@ class Auth
         $paramStr = '';
         $first = true;
         foreach ($params as $key => $value) {
+//            if ($value == "") {
+//                $value = "''";
+//            }
             if ($first) {
                 $paramStr = "{$key}={$value}";
                 $first = false;
@@ -103,12 +108,11 @@ class Auth
 
         // Step 4: 拼接 appSecret
         $signBefore = $this->appSecret . $paramStr;
-
+        //$signBefore = '1111app_id=222&code=4444&grant_type=authorization_code&merchant_id=333&timestamp=1719297100558';
         // Step 5: 使用SHA-256加密
         $signature = hash('sha256', $signBefore);
 
         Yii::info("[FengniaAuth] Sign Before: {$signBefore}, Signature: {$signature}");
-
         return $signature;
     }
 
@@ -157,8 +161,7 @@ class Auth
             ];
         }
 
-        $timestamp = (string)(time() * 1000); // 毫秒级时间戳
-
+        $timestamp = (int)(microtime(true) * 1000); // 毫秒级时间戳
         $params = [
             'grant_type' => self::GRANT_TYPE_AUTH_CODE,
             'code' => $code,
@@ -167,17 +170,26 @@ class Auth
             'timestamp' => $timestamp,
         ];
 
-        // 生成签名
+        // 沙箱环境不传code
+        if (getenv('YII_ENV') !== 'production') {
+            //unset($params['code']);
+            //$params['code'] = "";
+        }
+
+            // 生成签名
         $signature = $this->generateSignature($params);
         $params['signature'] = $signature;
 
         $url = $this->getTokenUrl();
-
         // 发送POST请求
         $response = HttpClient::post($url, $params, [
             'Content-Type' => 'application/json',
         ]);
 
+//        $curl = new curl\Curl();
+//        $response = $curl->setOption(CURLOPT_POSTFIELDS, Json::encode($params))->post($url);
+//        $response = Json::decode($response);
+
         Yii::info("[FengniaAuth] GetAccessToken Response: " . json_encode($response));
 
         return $this->parseResponse($response);
@@ -276,6 +288,9 @@ class Auth
 
         // 提取 business_data 字段中的数据
         $businessData = $response['business_data'] ?? [];
+        if (is_string($businessData)) {
+            $businessData = json_decode($businessData, true);
+        }
 
         // 检查是否包含必需的token信息
         if (empty($businessData['access_token'])) {

+ 0 - 353
common/components/delivery/platform/fengniao/CHANGES_SUMMARY.md

@@ -1,353 +0,0 @@
-# 峰鸟平台认证类 (Fengniao Auth) 修改总结
-
-## 修改日期
-2025-10-29
-
-## 修改概述
-根据峰鸟开放平台官方 API 文档,对 `Auth.php` 认证类进行了全面升级和完善,实现了正确的 **SHA-256 签名算法**和规范的 **OAuth2 Token 管理流程**。
-
----
-
-## 主要修改点
-
-### 1. 常量定义更新
-
-#### 移除的常量
-```php
-❌ const RESPONSE_TYPE = '';
-❌ const GRANT_TYPE_AUTH_CODE = '';
-❌ const GRANT_TYPE_REFRESH = '';
-❌ const AUTHORIZE_URL = '';
-❌ const TOKEN_URL = '';
-```
-
-#### 新增/修改的常量
-```php
-✅ const GRANT_TYPE_AUTH_CODE = 'authorization_code';      // 授权类型:授权码
-✅ const GRANT_TYPE_REFRESH = 'refresh_token';             // 授权类型:刷新令牌
-
-// 正式环境 URL
-✅ const TOKEN_URL_PRODUCTION = 'https://open-anubis.ele.me/anubis-webapi/openapi/token';
-✅ const REFRESH_TOKEN_URL_PRODUCTION = 'https://open-anubis.ele.me/anubis-webapi/openapi/refreshToken';
-
-// 沙箱环境 URL
-✅ const TOKEN_URL_SANDBOX = 'https://exam-anubis.ele.me/anubis-webapi/openapi/token';
-✅ const REFRESH_TOKEN_URL_SANDBOX = 'https://exam-anubis.ele.me/anubis-webapi/openapi/refreshToken';
-```
-
-### 2. 属性变更
-
-#### 移除的属性
-```php
-❌ protected $redirectUri;    // 重定向 URI(不再需要)
-```
-
-#### 新增的属性
-```php
-✅ protected $merchantId;     // 商户 ID(新增,签名必需)
-```
-
-### 3. 方法修改
-
-#### 移除的方法
-```php
-❌ setRedirectUri()           // 设置重定向 URI
-❌ generateAuthUrl()          // 生成授权 URL(不需要)
-```
-
-#### 新增的方法
-
-**✅ `generateSignature(array $params): string`**
-- 生成符合峰鸟 API 要求的 SHA-256 签名
-- 实现流程:
-  1. 过滤空值参数
-  2. 按字典序排序参数
-  3. 拼接成 `key=value&key=value` 格式
-  4. 在前面拼接 `appSecret`
-  5. 使用 SHA-256 进行加密
-- 返回 16 进制签名字符串
-
-**✅ `getTokenUrl(): string`**
-- 根据当前环境(沙箱/正式)返回获取 Token 的 URL
-
-**✅ `getRefreshTokenUrl(): string`**
-- 根据当前环境(沙箱/正式)返回刷新 Token 的 URL
-
-**✅ `setMerchantId(string $merchantId): self`**
-- 设置商户 ID(可链式调用)
-- 用于后续签名和请求
-
-**✅ `getMerchantId(): string`**
-- 获取当前设置的商户 ID
-
-**✅ `getAppId(): string`**
-- 获取应用 ID(原方法名为 `getAppKey()`)
-
-#### 修改的方法
-
-**`getAccessToken()` 方法**
-
-| 方面 | 旧版本 | 新版本 |
-|------|--------|--------|
-| 请求方式 | GET | POST |
-| 请求 URL | `self::TOKEN_URL` (未定义) | `$this->getTokenUrl()` |
-| 参数 | `grant_type, client_id, code, isSandbox` | `grant_type, code, app_id, merchant_id, timestamp, signature` |
-| 签名 | 无 | ✅ 自动生成 SHA-256 签名 |
-| 商户 ID | 不支持 | ✅ 可选参数,支持 |
-| 时间戳 | 不需要 | ✅ 毫秒级时间戳 |
-
-**`refreshAccessToken()` 方法**
-
-| 方面 | 旧版本 | 新版本 |
-|------|--------|--------|
-| 请求方式 | GET | POST |
-| 请求 URL | `self::TOKEN_URL` (未定义) | `$this->getRefreshTokenUrl()` |
-| 参数 | `grant_type, client_id, refresh_token, isSandbox` | `grant_type, app_id, merchant_id, timestamp, refresh_token, signature` |
-| 签名 | 无 | ✅ 自动生成 SHA-256 签名 |
-| 商户 ID | 不支持 | ✅ 可选参数,支持 |
-
-**`parseResponse()` 方法**
-
-| 方面 | 旧版本 | 新版本 |
-|------|--------|--------|
-| 返回字段检查 | 检查 `ret` 字段 | 检查 `code` 字段 |
-| 成功判断 | `ret == 0` | `code == '200'` |
-| 数据提取 | `response['data']` | `response['business_data']` |
-| 返回格式 | 扁平化 | 使用 `data` 嵌套对象 |
-| 支持的字段 | `access_token, refresh_token, auth_mobile, auth_end_time` | `access_token, refresh_token, app_id, merchant_id, expire_in, re_expire_in` |
-
-#### 原方法保留
-
-**`getAppSecret(): string`**
-- 保持不变
-
-**`isSandbox(): bool`**
-- 保持不变
-
----
-
-## 签名算法详解
-
-### 签名流程
-
-```
-Step 1: 准备参数
-params = [
-    'grant_type' => 'authorization_code',
-    'code' => 'xxx',
-    'app_id' => '123',
-    'merchant_id' => '456',
-    'timestamp' => '1719297100558'
-]
-
-Step 2: 过滤空值
-// 移除值为空的参数
-
-Step 3: 字典序排序
-sorted = [
-    'app_id' => '123',
-    'code' => 'xxx',
-    'grant_type' => 'authorization_code',
-    'merchant_id' => '456',
-    'timestamp' => '1719297100558'
-]
-
-Step 4: 拼接成字符串
-paramStr = "app_id=123&code=xxx&grant_type=authorization_code&merchant_id=456&timestamp=1719297100558"
-
-Step 5: 拼接 appSecret
-signBefore = "app_secret_value" + paramStr
-           = "app_secret_valueapp_id=123&code=xxx&grant_type=authorization_code&merchant_id=456&timestamp=1719297100558"
-
-Step 6: SHA-256 加密
-signature = hash('sha256', signBefore)
-         = "06abc54c633f1636e3d03cc6c6e36a113529db6fb4f2e0b4c64535c6baa69a15"
-```
-
-### 代码实现
-
-```php
-protected function generateSignature(array $params)
-{
-    // 过滤空值
-    $params = array_filter($params, function ($v) {
-        return $v !== null && $v !== '';
-    });
-    
-    // 字典序排序
-    ksort($params);
-    
-    // 拼接字符串
-    $paramStr = '';
-    $first = true;
-    foreach ($params as $key => $value) {
-        if ($first) {
-            $paramStr = "{$key}={$value}";
-            $first = false;
-        } else {
-            $paramStr .= "&{$key}={$value}";
-        }
-    }
-    
-    // 拼接 appSecret
-    $signBefore = $this->appSecret . $paramStr;
-    
-    // SHA-256 加密
-    $signature = hash('sha256', $signBefore);
-    
-    return $signature;
-}
-```
-
----
-
-## API 接口变更
-
-### 获取 Token 接口
-
-**请求参数顺序变更:**
-
-| 参数 | 旧版本 | 新版本 |
-|------|--------|--------|
-| grant_type | ✅ | ✅ (authorization_code) |
-| client_id | ✅ | ❌ |
-| code | ✅ | ✅ |
-| app_id | ❌ | ✅ (新增) |
-| merchant_id | ❌ | ✅ (新增) |
-| timestamp | ❌ | ✅ (新增) |
-| signature | ❌ | ✅ (新增) |
-| isSandbox | ✅ | ❌ (移除,改为自动检测) |
-
-**返回值格式变更:**
-
-旧版本:
-```php
-[
-    'success' => true,
-    'access_token' => 'token',
-    'refresh_token' => 'refresh_token',
-    'auth_mobile' => '',
-    'auth_end_time' => ''
-]
-```
-
-新版本:
-```php
-[
-    'success' => true,
-    'data' => [
-        'access_token' => 'token',
-        'refresh_token' => 'refresh_token',
-        'app_id' => 'app_id',
-        'merchant_id' => 'merchant_id',
-        'expire_in' => 31536000,
-        're_expire_in' => 31536000
-    ]
-]
-```
-
----
-
-## 兼容性说明
-
-### ❌ 破坏性变更
-- API 接口 URL 从未定义状态变为完整的正式/沙箱环境 URL
-- 请求方式从 GET 改为 POST
-- 请求参数结构完全变更
-- 返回值格式变更(添加了 `data` 嵌套对象)
-- 需要提供 `merchant_id` 和 `timestamp`
-
-### ✅ 保留的方法
-- `getAppSecret()`
-- `isSandbox()`
-
-### 迁移指南
-
-**旧代码:**
-```php
-$auth = new Auth();
-$result = $auth->getAccessToken($code);
-if ($result['success']) {
-    $token = $result['access_token'];
-}
-```
-
-**新代码:**
-```php
-$auth = new Auth();
-$auth->setMerchantId('6665');  // 需要设置商户 ID
-$result = $auth->getAccessToken($code, '6665');  // 或在调用时传入
-if ($result['success']) {
-    $token = $result['data']['access_token'];  // 数据在 'data' 中
-}
-```
-
----
-
-## 测试建议
-
-### 1. 单元测试项目
-
-- [ ] 签名生成是否正确
-- [ ] 参数按字典序排序是否正确
-- [ ] SHA-256 加密结果是否正确
-- [ ] 获取 Token 是否成功
-- [ ] 刷新 Token 是否成功
-- [ ] 错误响应处理是否正确
-- [ ] 沙箱环境和正式环境 URL 切换是否正确
-
-### 2. 集成测试
-
-- [ ] 与峰鸟沙箱环境的集成测试
-- [ ] 错误码处理
-- [ ] 超时和重试机制
-- [ ] 日志记录
-
-### 3. 验证签名
-
-可以使用以下在线工具验证 SHA-256 签名:
-- https://www.fileformat.info/tool/hash.htm
-- https://emn178.github.io/online-tools/sha256.html
-
----
-
-## 日志输出示例
-
-```log
-[2025-10-29 10:15:30] Info: [FengniaAuth] Sign Before: app_secret_valueapp_id=123&code=xxx&grant_type=authorization_code&merchant_id=456&timestamp=1719297100558, Signature: 06abc54c633f1636e3d03cc6c6e36a113529db6fb4f2e0b4c64535c6baa69a15
-[2025-10-29 10:15:31] Info: [FengniaAuth] GetAccessToken Response: {"sign":"","code":"200","msg":"success","business_data":{"app_id":"123","merchant_id":"456","access_token":"token_value","refresh_token":"refresh_token_value","expire_in":"31536000","re_expire_in":"31536000"}}
-```
-
----
-
-## 相关文件
-
-### 已修改
-- ✅ `common/components/delivery/platform/fengniao/Auth.php`
-
-### 已创建
-- ✅ `common/components/delivery/platform/fengniao/IMPLEMENTATION_GUIDE.md`
-- ✅ `common/components/delivery/platform/fengniao/CHANGES_SUMMARY.md` (本文件)
-
-### 相关但未修改
-- `common/components/delivery/helpers/HttpClient.php` (已支持 POST 请求)
-- `common/components/delivery/helpers/SignHelper.php` (通用签名工具,可选使用)
-
----
-
-## 后续优化建议
-
-1. **配置外部化**:将 `appId` 和 `appSecret` 移到配置文件中,而不是硬编码
-2. **缓存 Token**:实现 Token 缓存机制,避免频繁重新获取
-3. **返回值签名验证**:实现峰鸟 API 返回值的签名验证
-4. **业务数据签名**:支持业务数据的签名生成(用于主动请求业务接口)
-5. **单元测试**:编写完整的单元测试覆盖所有方法
-6. **错误码映射**:创建详细的错误码到错误信息的映射表
-
----
-
-## 联系方式
-
-如有问题或建议,请参考以下资源:
-- 项目文档:`common/components/delivery/platform/fengniao/IMPLEMENTATION_GUIDE.md`
-- 峰鸟开放平台官方文档

+ 0 - 270
common/components/delivery/platform/fengniao/IMPLEMENTATION_GUIDE.md

@@ -1,270 +0,0 @@
-# 峰鸟平台认证实现指南
-
-## 概述
-
-根据峰鸟开放平台的官方 API 文档,本指南详细说明了认证类的使用方法和签名算法的实现细节。
-
-## 1. 签名算法
-
-峰鸟平台采用 **SHA-256** 摘要签名算法进行安全认证。所有请求和响应都需要使用此算法进行签名验证。
-
-### 1.1 签名步骤
-
-1. **准备待签名的参数**
-   - 收集所有需要参与签名的参数
-   - 过滤掉值为空的参数
-
-2. **按字典序排序**
-   - 将参数按照字母顺序从小到大排序
-   - 例:`app_id=222&code=4444&grant_type=authorization_code&merchant_id=333&timestamp=1719297100558`
-
-3. **拼接 appSecret**
-   - 在排序后的字符串前面拼接 appSecret
-   - 例:`signBefore = appSecret + sortedParam`
-
-4. **SHA-256 加密**
-   - 使用 SHA-256 算法对拼接后的字符串进行加密
-   - 返回 16 进制字符串结果
-
-### 1.2 签名示例
-
-```
-参数信息:
-- app_secret = 111
-- app_id = 222
-- merchant_id = 333
-- code = 4444
-- grant_type = authorization_code
-- timestamp = 1719297100558
-
-Step 1: 按字典序排列参数
-app_id=222&code=4444&grant_type=authorization_code&merchant_id=333&timestamp=1719297100558
-
-Step 2: 拼接 appSecret
-111app_id=222&code=4444&grant_type=authorization_code&merchant_id=333&timestamp=1719297100558
-
-Step 3: SHA-256 加密
-06abc54c633f1636e3d03cc6c6e36a113529db6fb4f2e0b4c64535c6baa69a15
-```
-
-## 2. API 接口
-
-### 2.1 获取 Token
-
-**接口地址:**
-- 正式环境:`https://open-anubis.ele.me/anubis-webapi/openapi/token`
-- 沙箱环境:`https://exam-anubis.ele.me/anubis-webapi/openapi/token`
-
-**请求参数:**
-
-| 参数名 | 类型 | 必须 | 说明 | 备注 |
-|--------|------|------|------|------|
-| grant_type | string | 是 | 授权模式 | 固定值:`authorization_code` |
-| code | string | 是 | 授权码 | 有效期24小时,沙箱环境可填空 |
-| app_id | string | 是 | 应用ID | |
-| merchant_id | string | 是 | 商户ID | |
-| timestamp | int | 是 | 当前请求时间戳 | 单位:毫秒 |
-| signature | string | 是 | 签名 | 根据签名规则生成 |
-
-**请求示例:**
-
-```json
-{
-    "grant_type": "authorization_code",
-    "code": "u6nNE8O78HxEaNF7OjJzvv",
-    "app_id": "5a09bfcb-3bee-4e56-8486-3af6201ebf12",
-    "merchant_id": "6665",
-    "signature": "c2de762812d1f71ea49471f074c9df80c4ebd15902ba811b3bef1a8d2adb74f6",
-    "timestamp": "1610097357000"
-}
-```
-
-**返回值:**
-
-```json
-{
-    "sign": "",
-    "code": "200",
-    "msg": "success",
-    "business_data": {
-        "app_id": "5a09bfcb-3bee-4e56-8486-3af6201ebf12",
-        "merchant_id": "6665",
-        "access_token": "token_value",
-        "refresh_token": "refresh_token_value",
-        "expire_in": "31536000",
-        "re_expire_in": "31536000"
-    }
-}
-```
-
-### 2.2 刷新 Token
-
-**接口地址:**
-- 正式环境:`https://open-anubis.ele.me/anubis-webapi/openapi/refreshToken`
-- 沙箱环境:`https://exam-anubis.ele.me/anubis-webapi/openapi/refreshToken`
-
-**请求参数:**
-
-| 参数名 | 类型 | 必须 | 说明 | 备注 |
-|--------|------|------|------|------|
-| grant_type | string | 是 | 授权模式 | 固定值:`refresh_token` |
-| app_id | string | 是 | 应用ID | |
-| merchant_id | string | 是 | 商户ID | |
-| timestamp | int | 是 | 当前请求时间戳 | 单位:毫秒 |
-| refresh_token | string | 是 | 刷新令牌 | |
-| signature | string | 是 | 签名 | 根据签名规则生成 |
-
-**请求示例:**
-
-```json
-{
-    "grant_type": "refresh_token",
-    "refresh_token": "dbde9396-1175-4ddc-b607-2d03263f267a",
-    "app_id": "5a09bfcb-3bee-4e56-8486-3af6201ebf12",
-    "merchant_id": "6665",
-    "signature": "e6afc4f5e547909f518681a2378d08955f25b305225d74dfc88a00a08892cc58",
-    "timestamp": "1610097357000"
-}
-```
-
-**返回值:**
-
-```json
-{
-    "sign": "",
-    "code": "200",
-    "msg": "success",
-    "business_data": {
-        "app_id": "5a09bfcb-3bee-4e56-8486-3af6201ebf12",
-        "merchant_id": "6665",
-        "access_token": "new_token_value",
-        "refresh_token": "new_refresh_token_value",
-        "expire_in": "31536000",
-        "re_expire_in": "31536000"
-    }
-}
-```
-
-## 3. 使用示例
-
-### 3.1 基本初始化
-
-```php
-use common\components\delivery\platform\fengniao\Auth;
-
-// 创建认证实例
-$auth = new Auth();
-
-// 设置商户ID(可选)
-$auth->setMerchantId('6665');
-```
-
-### 3.2 获取 Token
-
-```php
-$code = 'u6nNE8O78HxEaNF7OjJzvv'; // 授权码来自授权回调
-$merchantId = '6665'; // 商户ID
-
-$result = $auth->getAccessToken($code, $merchantId);
-
-if ($result['success']) {
-    $data = $result['data'];
-    echo "Access Token: " . $data['access_token'] . "\n";
-    echo "Refresh Token: " . $data['refresh_token'] . "\n";
-    echo "过期时间: " . $data['expire_in'] . " 秒\n";
-} else {
-    echo "获取Token失败: " . $result['error'] . "\n";
-}
-```
-
-### 3.3 刷新 Token
-
-```php
-$refreshToken = 'dbde9396-1175-4ddc-b607-2d03263f267a';
-$merchantId = '6665';
-
-$result = $auth->refreshAccessToken($refreshToken, $merchantId);
-
-if ($result['success']) {
-    $data = $result['data'];
-    echo "新的 Access Token: " . $data['access_token'] . "\n";
-    echo "新的 Refresh Token: " . $data['refresh_token'] . "\n";
-} else {
-    echo "刷新Token失败: " . $result['error'] . "\n";
-}
-```
-
-## 4. 环境配置
-
-### 4.1 正式环境 vs 沙箱环境
-
-- **正式环境**:当 `YII_ENV` 环境变量设置为 `production` 时使用
-- **沙箱环境**:其他情况下默认使用沙箱环境
-
-### 4.2 配置项
-
-在 `Auth` 类构造函数中需要配置以下项:
-
-```php
-$this->appId = '6587209115185920913';        // 应用ID
-$this->appSecret = '3f935d5f-bf65-467e-a61c-72cfb1d53960'; // 应用Secret
-$this->merchantId = '';                       // 商户ID(需要从配置或外部设置)
-```
-
-## 5. 错误处理
-
-所有方法返回的响应格式如下:
-
-**成功响应:**
-```php
-[
-    'success' => true,
-    'data' => [
-        'access_token' => 'token_value',
-        'refresh_token' => 'refresh_token_value',
-        'app_id' => 'app_id',
-        'merchant_id' => 'merchant_id',
-        'expire_in' => 31536000,
-        're_expire_in' => 31536000,
-    ]
-]
-```
-
-**失败响应:**
-```php
-[
-    'success' => false,
-    'error' => 'Error message',
-    'code' => 'error_code'  // 可选
-]
-```
-
-## 6. 注意事项
-
-1. **时间戳**:使用毫秒级时间戳(`time() * 1000`)
-2. **签名验证**:所有请求都必须包含正确的签名
-3. **Token 有效期**:默认 Token 有效期为一年
-4. **刷新限制**:刷新 Token 接口 10 分钟内多次调用,只有第一次会刷新 token
-5. **沙箱测试**:沙箱环境获取 Token 时 code 参数可填空
-6. **SSL 证书**:确保生产环境已正确配置 SSL 证书
-
-## 7. 日志记录
-
-认证类会自动记录以下信息到 Yii 日志:
-
-- 签名生成过程(包括签名前字符串和最终签名)
-- 获取/刷新 Token 的 API 响应
-
-可通过查看 Yii 日志文件 `@app/runtime/logs/app.log` 查看详细信息。
-
-## 8. 技术参考
-
-### 类文件
-- `common/components/delivery/platform/fengniao/Auth.php` - 认证类
-
-### 辅助类
-- `common/components/delivery/helpers/HttpClient.php` - HTTP 客户端(用于发送请求)
-- `common/components/delivery/helpers/SignHelper.php` - 通用签名工具
-
-### 相关文档
-- 峰鸟开放平台官方文档:需参考官方 API 文档

+ 0 - 309
common/components/delivery/platform/fengniao/QUICK_REFERENCE.md

@@ -1,309 +0,0 @@
-# 峰鸟认证类快速参考
-
-## 核心改动概览
-
-| 项目 | 旧版本 | 新版本 |
-|------|--------|--------|
-| 签名算法 | ❌ 无 | ✅ SHA-256 |
-| 请求方式 | GET | POST |
-| 商户ID支持 | ❌ 否 | ✅ 是 |
-| 返回值格式 | 扁平化 | 嵌套(data 对象) |
-| Token URL | 未定义 | 完整 URL |
-
----
-
-## 使用方式对比
-
-### 1. 获取 Token
-
-#### ❌ 旧方式
-```php
-$auth = new Auth();
-$result = $auth->getAccessToken($code);
-
-// 返回值:
-// [
-//     'success' => true,
-//     'access_token' => 'token_value',
-//     'refresh_token' => 'refresh_value'
-// ]
-```
-
-#### ✅ 新方式
-```php
-$auth = new Auth();
-$result = $auth->getAccessToken($code, '6665'); // 传入商户ID
-
-if ($result['success']) {
-    $token = $result['data']['access_token'];
-    $refreshToken = $result['data']['refresh_token'];
-    $expireTime = $result['data']['expire_in'];  // 秒数
-}
-```
-
-**新方式的优势:**
-- ✅ 自动生成并附加 SHA-256 签名
-- ✅ 使用毫秒级时间戳
-- ✅ 包含更多返回信息(expire_in, re_expire_in)
-- ✅ 商户 ID 灵活传入
-
-### 2. 刷新 Token
-
-#### ❌ 旧方式
-```php
-$auth = new Auth();
-$result = $auth->refreshAccessToken($refreshToken);
-```
-
-#### ✅ 新方式
-```php
-$auth = new Auth();
-$result = $auth->refreshAccessToken($refreshToken, '6665');
-
-if ($result['success']) {
-    $newToken = $result['data']['access_token'];
-    $newRefreshToken = $result['data']['refresh_token'];
-}
-```
-
-### 3. 初始化方式
-
-#### ✅ 方式 1:先设置商户ID
-```php
-$auth = new Auth();
-$auth->setMerchantId('6665');
-$result = $auth->getAccessToken($code);
-```
-
-#### ✅ 方式 2:调用时传入商户ID
-```php
-$auth = new Auth();
-$result = $auth->getAccessToken($code, '6665');
-```
-
-#### ✅ 方式 3:链式调用
-```php
-$result = (new Auth())
-    ->setMerchantId('6665')
-    ->getAccessToken($code);
-```
-
----
-
-## 请求参数
-
-### 获取 Token 请求参数
-
-| 参数 | 类型 | 说明 |
-|------|------|------|
-| grant_type | string | `authorization_code` (自动) |
-| code | string | 授权码(必须) |
-| app_id | string | 应用ID(自动) |
-| merchant_id | string | 商户ID(必须) |
-| timestamp | int | 毫秒时间戳(自动) |
-| signature | string | SHA-256签名(自动) |
-
-**示例请求(自动生成):**
-```json
-{
-    "grant_type": "authorization_code",
-    "code": "u6nNE8O78HxEaNF7OjJzvv",
-    "app_id": "6587209115185920913",
-    "merchant_id": "6665",
-    "timestamp": "1719297100558",
-    "signature": "06abc54c633f1636e3d03cc6c6e36a113529db6fb4f2e0b4c64535c6baa69a15"
-}
-```
-
-### 刷新 Token 请求参数
-
-| 参数 | 类型 | 说明 |
-|------|------|------|
-| grant_type | string | `refresh_token` (自动) |
-| app_id | string | 应用ID(自动) |
-| merchant_id | string | 商户ID(必须) |
-| timestamp | int | 毫秒时间戳(自动) |
-| refresh_token | string | 刷新令牌(必须) |
-| signature | string | SHA-256签名(自动) |
-
----
-
-## 返回值格式
-
-### 成功响应
-
-```php
-[
-    'success' => true,
-    'data' => [
-        'access_token' => 'token_value',           // 访问令牌
-        'refresh_token' => 'refresh_value',        // 刷新令牌
-        'app_id' => '6587209115185920913',        // 应用ID
-        'merchant_id' => '6665',                  // 商户ID
-        'expire_in' => 31536000,                  // 秒数(一般是1年)
-        're_expire_in' => 31536000                // 刷新令牌过期时间
-    ]
-]
-```
-
-### 失败响应
-
-```php
-[
-    'success' => false,
-    'error' => '商户ID未设置',  // 错误信息
-    'code' => '400'              // 可选:错误码
-]
-```
-
----
-
-## 错误处理
-
-```php
-$auth = new Auth();
-$result = $auth->getAccessToken($code, $merchantId);
-
-if (!$result['success']) {
-    // 处理错误
-    $error = $result['error'];  // 获取错误信息
-    $code = $result['code'] ?? null;  // 获取可能的错误码
-    
-    // 记录日志
-    Yii::error("获取Token失败: {$error}", 'fengniao');
-    
-    // 返回错误响应
-    return ['success' => false, 'msg' => $error];
-}
-
-// 成功处理
-$data = $result['data'];
-$accessToken = $data['access_token'];
-```
-
----
-
-## 签名算法(自动处理)
-
-通常不需要手动调用,但如需了解细节:
-
-```php
-// 内部处理流程(已自动完成)
-
-// 1. 准备参数
-$params = [
-    'grant_type' => 'authorization_code',
-    'code' => 'xxx',
-    'app_id' => '123',
-    'merchant_id' => '456',
-    'timestamp' => '1719297100558'
-];
-
-// 2. 字典序排序 & 拼接
-// app_id=123&code=xxx&grant_type=authorization_code&merchant_id=456&timestamp=1719297100558
-
-// 3. 拼接 appSecret
-// app_secret_valueapp_id=123&code=xxx&...
-
-// 4. SHA-256 加密
-// 06abc54c633f1636e3d03cc6c6e36a113529db6fb4f2e0b4c64535c6baa69a15
-```
-
----
-
-## 常见 API 端点
-
-### 获取 Token
-
-- **正式环境**: `https://open-anubis.ele.me/anubis-webapi/openapi/token`
-- **沙箱环境**: `https://exam-anubis.ele.me/anubis-webapi/openapi/token`
-- **自动切换**: 根据 `YII_ENV` 环境变量
-
-### 刷新 Token
-
-- **正式环境**: `https://open-anubis.ele.me/anubis-webapi/openapi/refreshToken`
-- **沙箱环境**: `https://exam-anubis.ele.me/anubis-webapi/openapi/refreshToken`
-- **自动切换**: 根据 `YII_ENV` 环境变量
-
----
-
-## 环境配置
-
-```bash
-# 沙箱环境(默认)
-export YII_ENV=dev
-
-# 正式环境
-export YII_ENV=production
-```
-
----
-
-## 常用方法速览
-
-| 方法 | 返回值 | 说明 |
-|------|--------|------|
-| `getAccessToken($code, $merchantId)` | array | 获取访问令牌 |
-| `refreshAccessToken($token, $merchantId)` | array | 刷新访问令牌 |
-| `setMerchantId($id)` | self | 设置商户ID(链式) |
-| `getMerchantId()` | string | 获取商户ID |
-| `getAppId()` | string | 获取应用ID |
-| `getAppSecret()` | string | 获取应用密钥 |
-| `isSandbox()` | bool | 是否沙箱环境 |
-
----
-
-## 日志查看
-
-查看签名和 API 响应日志:
-
-```bash
-tail -f @app/runtime/logs/app.log | grep "FengniaAuth"
-```
-
-日志示例:
-```
-[FengniaAuth] Sign Before: app_secret_valueapp_id=123&code=xxx&...
-[FengniaAuth] Signature: 06abc54c633f1636e3d03cc6c6e36a113529db6fb4f2e0b4c64535c6baa69a15
-[FengniaAuth] GetAccessToken Response: {"code":"200","business_data":{...}}
-```
-
----
-
-## 迁移检查清单
-
-- [ ] 修改代码:从 `$result['access_token']` 改为 `$result['data']['access_token']`
-- [ ] 修改代码:所有调用添加商户ID参数
-- [ ] 测试:在沙箱环境测试获取Token
-- [ ] 测试:在沙箱环境测试刷新Token
-- [ ] 测试:验证错误处理逻辑
-- [ ] 配置:更新 appId 和 appSecret
-- [ ] 日志:检查日志输出是否正确
-- [ ] 部署:部署到正式环境前做充分测试
-
----
-
-## 常见问题
-
-### Q: 商户ID从哪里获取?
-A: 商户ID从峰鸟授权回调中获得,或从峰鸟平台后台查看。
-
-### Q: 时间戳为什么要用毫秒?
-A: 峰鸟 API 要求毫秒级时间戳,PHP 中使用 `time() * 1000`。
-
-### Q: 签名失败怎么办?
-A: 检查日志中的 "Sign Before" 字符串,确认参数排序和拼接是否正确。
-
-### Q: Token 过期怎么办?
-A: 使用 `refreshAccessToken()` 刷新,注意 10 分钟内多次调用只会刷新一次。
-
-### Q: 如何切换沙箱/正式环境?
-A: 设置 `YII_ENV` 环境变量,无需修改代码。
-
----
-
-## 相关文档
-
-- 📖 详细文档: `IMPLEMENTATION_GUIDE.md`
-- 📋 修改说明: `CHANGES_SUMMARY.md`
-- 🔗 峰鸟官方文档: 需参考官方 API 文档

+ 0 - 413
common/components/delivery/platform/fengniao/README.md

@@ -1,413 +0,0 @@
-# 峰鸟开放平台认证模块
-
-## 📋 项目概述
-
-本模块实现了峰鸟开放平台 (Fengniao OpenAPI) 的完整认证流程,包括:
-
-- ✅ **OAuth2 授权码流程** - 获取访问令牌
-- ✅ **Token 刷新机制** - 使用刷新令牌获取新 Token
-- ✅ **SHA-256 签名算法** - 符合峰鸟 API 规范
-- ✅ **自动环境切换** - 沙箱/正式环境自动选择
-- ✅ **完整错误处理** - 详细的错误信息和日志记录
-
----
-
-## 📁 文件结构
-
-```
-fengniao/
-├── Auth.php                      # 主认证类
-├── cities.php                    # 城市列表数据
-├── README.md                     # 本文件
-├── QUICK_REFERENCE.md           # 快速参考指南
-├── IMPLEMENTATION_GUIDE.md      # 详细实现指南
-└── CHANGES_SUMMARY.md           # 修改总结
-```
-
----
-
-## 🚀 快速开始
-
-### 基础使用
-
-```php
-use common\components\delivery\platform\fengniao\Auth;
-
-// 1. 创建认证实例
-$auth = new Auth();
-
-// 2. 获取 Token(使用授权码)
-$code = 'u6nNE8O78HxEaNF7OjJzvv';  // 从授权回调获取
-$merchantId = '6665';               // 商户ID
-
-$result = $auth->getAccessToken($code, $merchantId);
-
-// 3. 检查结果
-if ($result['success']) {
-    $token = $result['data']['access_token'];
-    $refreshToken = $result['data']['refresh_token'];
-    echo "Token 获取成功!";
-} else {
-    echo "错误: " . $result['error'];
-}
-```
-
-### 刷新 Token
-
-```php
-$refreshToken = 'dbde9396-1175-4ddc-b607-2d03263f267a';
-$merchantId = '6665';
-
-$result = $auth->refreshAccessToken($refreshToken, $merchantId);
-
-if ($result['success']) {
-    $newToken = $result['data']['access_token'];
-    echo "Token 已刷新!";
-}
-```
-
----
-
-## 📚 文档导航
-
-### 新手入门
-- 👉 **[快速参考](QUICK_REFERENCE.md)** - 5分钟快速上手
-- 📖 **[详细指南](IMPLEMENTATION_GUIDE.md)** - 完整 API 文档
-
-### 开发者参考
-- 📋 **[修改总结](CHANGES_SUMMARY.md)** - 了解从旧版本的变更
-- 🔍 **[Auth.php](Auth.php)** - 源代码(含完整注释)
-
----
-
-## 🔑 核心功能
-
-### 1. 获取 Token
-
-**方法**: `getAccessToken(string $code, ?string $merchantId = null): array`
-
-获取峰鸟授权令牌,使用授权码换取访问令牌。
-
-**参数:**
-- `$code` (string) - 授权码,有效期 24 小时
-- `$merchantId` (string, 可选) - 商户ID
-
-**返回:**
-```php
-[
-    'success' => true,
-    'data' => [
-        'access_token' => 'token_value',
-        'refresh_token' => 'refresh_value',
-        'app_id' => '应用ID',
-        'merchant_id' => '商户ID',
-        'expire_in' => 31536000,        // 秒数
-        're_expire_in' => 31536000
-    ]
-]
-```
-
-### 2. 刷新 Token
-
-**方法**: `refreshAccessToken(string $refreshToken, ?string $merchantId = null): array`
-
-使用刷新令牌获取新的访问令牌。
-
-**参数:**
-- `$refreshToken` (string) - 刷新令牌
-- `$merchantId` (string, 可选) - 商户ID
-
-**返回:** 同上(成功返回新 Token)
-
-### 3. 商户 ID 管理
-
-**设置商户 ID:**
-```php
-$auth->setMerchantId('6665');
-```
-
-**获取商户 ID:**
-```php
-$merchantId = $auth->getMerchantId();
-```
-
-### 4. 其他 getter 方法
-
-```php
-$auth->getAppId();      // 获取应用ID
-$auth->getAppSecret();  // 获取应用密钥
-$auth->isSandbox();     // 是否沙箱环境
-```
-
----
-
-## 🔐 签名算法
-
-### 概述
-
-峰鸟 API 采用 **SHA-256 摘要算法**进行请求签名。所有请求和响应都需要验证签名完整性。
-
-### 签名流程(自动处理)
-
-类内部自动执行以下步骤:
-
-1. **参数收集** - 收集待签名的参数
-2. **过滤空值** - 移除值为空的参数
-3. **字典序排序** - 按 key 名称字母顺序排序
-4. **字符串拼接** - 拼接成 `key1=value1&key2=value2` 格式
-5. **追加密钥** - 在字符串前追加 `appSecret`
-6. **SHA-256 加密** - 使用 SHA-256 算法加密
-7. **16进制转换** - 返回 16 进制字符串结果
-
-### 签名示例
-
-```
-原始参数:
-{
-    app_id: "222",
-    merchant_id: "333",
-    code: "4444",
-    grant_type: "authorization_code",
-    timestamp: "1719297100558"
-}
-
-Step 1: 字典序排序
-app_id=222&code=4444&grant_type=authorization_code&merchant_id=333&timestamp=1719297100558
-
-Step 2: 拼接 appSecret(假设 appSecret = "111")
-111app_id=222&code=4444&grant_type=authorization_code&merchant_id=333&timestamp=1719297100558
-
-Step 3: SHA-256 加密
-06abc54c633f1636e3d03cc6c6e36a113529db6fb4f2e0b4c64535c6baa69a15
-```
-
----
-
-## 🌍 环境配置
-
-### 自动环境切换
-
-根据环境变量 `YII_ENV` 自动选择正式/沙箱环境:
-
-| 环境变量 | 使用环境 | 说明 |
-|---------|---------|------|
-| `production` | 正式环境 | 生产系统 |
-| 其他 | 沙箱环境 | 开发/测试 |
-
-### 配置示例
-
-**沙箱环境(默认):**
-```bash
-export YII_ENV=dev
-```
-
-**正式环境:**
-```bash
-export YII_ENV=production
-```
-
-### 配置 appId 和 appSecret
-
-在 `Auth.php` 构造函数中修改:
-
-```php
-public function __construct()
-{
-    // ... 其他代码 ...
-    
-    // 配置应用凭证(需要替换为真实值)
-    $this->appId = '6587209115185920913';
-    $this->appSecret = '3f935d5f-bf65-467e-a61c-72cfb1d53960';
-}
-```
-
-**建议:** 将凭证移到配置文件中:
-
-```php
-$this->appId = Yii::$app->params['fengniao']['app_id'];
-$this->appSecret = Yii::$app->params['fengniao']['app_secret'];
-```
-
----
-
-## 🔗 API 端点
-
-### 获取 Token
-
-| 环境 | URL |
-|------|-----|
-| 正式 | `https://open-anubis.ele.me/anubis-webapi/openapi/token` |
-| 沙箱 | `https://exam-anubis.ele.me/anubis-webapi/openapi/token` |
-
-### 刷新 Token
-
-| 环境 | URL |
-|------|-----|
-| 正式 | `https://open-anubis.ele.me/anubis-webapi/openapi/refreshToken` |
-| 沙箱 | `https://exam-anubis.ele.me/anubis-webapi/openapi/refreshToken` |
-
----
-
-## 🔍 日志记录
-
-类自动记录以下信息到 Yii 日志:
-
-### 签名日志
-```log
-[FengniaAuth] Sign Before: app_secret_valueapp_id=123&code=xxx&..., Signature: 06abc54c633f1636e3d03cc6c6e36a113529db6fb4f2e0b4c64535c6baa69a15
-```
-
-### API 响应日志
-```log
-[FengniaAuth] GetAccessToken Response: {"code":"200","msg":"success","business_data":{...}}
-[FengniaAuth] RefreshAccessToken Response: {"code":"200","msg":"success","business_data":{...}}
-```
-
-### 查看日志
-
-```bash
-# 查看所有 Fengniao Auth 相关日志
-tail -f @app/runtime/logs/app.log | grep "FengniaAuth"
-```
-
----
-
-## ❌ 错误处理
-
-### 失败响应格式
-
-```php
-[
-    'success' => false,
-    'error' => 'Error message',  // 详细错误信息
-    'code' => 'error_code'        // 可选:错误码
-]
-```
-
-### 常见错误
-
-| 错误 | 原因 | 解决方案 |
-|------|------|---------|
-| 商户ID未设置 | 未提供商户ID | 调用 `setMerchantId()` 或在方法中传入 |
-| 未获取到 access_token | API 返回为空 | 检查授权码是否有效、商户ID是否正确 |
-| 响应格式错误 | API 返回异常 | 检查网络连接、API 状态、签名是否正确 |
-| 签名验证失败 | 签名错误 | 检查 appSecret 是否正确、参数排序是否正确 |
-
-### 错误处理最佳实践
-
-```php
-$auth = new Auth();
-$result = $auth->getAccessToken($code, $merchantId);
-
-if (!$result['success']) {
-    // 获取错误信息
-    $error = $result['error'];
-    
-    // 记录日志用于调试
-    Yii::error("峰鸟认证失败: {$error}", 'fengniao');
-    
-    // 返回用户友好的错误信息
-    return ['success' => false, 'msg' => '授权失败,请重试'];
-}
-
-// 成功处理
-$data = $result['data'];
-$accessToken = $data['access_token'];
-```
-
----
-
-## ⚙️ 配置要求
-
-### PHP 版本
-- PHP >= 7.1
-
-### 依赖
-- Yii2 框架
-- GuzzleHttp(HTTP 客户端,项目已包含)
-
-### 网络要求
-- 能访问峰鸟 API 服务器(正式/沙箱环境)
-- 支持 HTTPS 连接
-
----
-
-## 📝 实现细节
-
-### 方法列表
-
-| 方法 | 权限 | 返回类型 | 说明 |
-|------|------|---------|------|
-| `__construct()` | public | void | 初始化认证类 |
-| `getAccessToken()` | public | array | 获取访问令牌 |
-| `refreshAccessToken()` | public | array | 刷新访问令牌 |
-| `setMerchantId()` | public | self | 设置商户ID(链式) |
-| `getMerchantId()` | public | string | 获取商户ID |
-| `getAppId()` | public | string | 获取应用ID |
-| `getAppSecret()` | public | string | 获取应用密钥 |
-| `isSandbox()` | public | bool | 检查沙箱环境 |
-| `generateSignature()` | protected | string | 生成签名(内部) |
-| `getTokenUrl()` | protected | string | 获取 Token URL(内部) |
-| `getRefreshTokenUrl()` | protected | string | 获取刷新 URL(内部) |
-| `parseResponse()` | protected | array | 解析响应(内部) |
-
----
-
-## 🔄 版本更新
-
-### v2.0 (当前版本)
-- ✅ 实现 SHA-256 签名算法
-- ✅ 支持 POST 请求
-- ✅ 完整的商户ID支持
-- ✅ 毫秒级时间戳
-- ✅ 详细的文档和示例
-
-### v1.0 (历史版本)
-- ❌ 无签名支持
-- ❌ 使用 GET 请求
-- ❌ 参数不完整
-
----
-
-## 📞 支持与问题
-
-### 常见问题
-
-**Q: 如何切换沙箱/正式环境?**
-A: 设置 `YII_ENV` 环境变量即可,代码无需修改。
-
-**Q: Token 的有效期是多长?**
-A: 默认一年(`expire_in` 字段),具体值由峰鸟 API 返回。
-
-**Q: 签名失败怎么办?**
-A: 检查日志中的 "Sign Before" 字符串,确保参数排序和 appSecret 正确。
-
-**Q: 10分钟内调用刷新接口多次会怎样?**
-A: 峰鸟 API 规定只会刷新一次,其他调用返回相同的 token。
-
-### 联系方式
-
-- 📖 详细文档:见本目录中的 `*.md` 文件
-- 🐛 报告问题:检查日志并参考文档
-- 📝 官方文档:参考峰鸟开放平台官方 API 文档
-
----
-
-## 📄 许可证
-
-本代码遵循项目整体的许可证。
-
----
-
-## 🎯 后续优化计划
-
-- [ ] 添加 Token 缓存机制
-- [ ] 实现响应签名验证
-- [ ] 业务数据签名支持
-- [ ] 单元测试覆盖
-- [ ] 错误码完整映射表
-
----
-
-**最后更新**: 2025-10-29  
-**维护者**: 开发团队

+ 1 - 1
common/components/delivery/platform/huolala/cities.php

@@ -1,5 +1,5 @@
 <?php namespace common\components\delivery\platform\huolala; return array (
-  'expire' => 1761688800,
+  'expire' => 1761948000,
   '广州' => 
   array (
     'city_id' => 1001,

+ 2034 - 0
common/components/delivery/platform/shansong/cities.php

@@ -0,0 +1,2034 @@
+<?php return array (
+  '澳门' => 
+  array (
+    'index' => 'A',
+    'name' => '澳门',
+    'id' => 82,
+    'fakeCities' => NULL,
+    'code' => 82,
+  ),
+  '测试程式' => 
+  array (
+    'index' => 'A',
+    'name' => '测试程式',
+    'id' => 1102,
+    'fakeCities' => NULL,
+    'code' => 1102,
+  ),
+  '鞍山市' => 
+  array (
+    'index' => 'A',
+    'name' => '鞍山市',
+    'id' => 2103,
+    'fakeCities' => NULL,
+    'code' => 2103,
+  ),
+  '安庆市' => 
+  array (
+    'index' => 'A',
+    'name' => '安庆市',
+    'id' => 3408,
+    'fakeCities' => NULL,
+    'code' => 3408,
+  ),
+  '安阳市' => 
+  array (
+    'index' => 'A',
+    'name' => '安阳市',
+    'id' => 4105,
+    'fakeCities' => NULL,
+    'code' => 4105,
+  ),
+  '北京市' => 
+  array (
+    'index' => 'B',
+    'name' => '北京市',
+    'id' => 1101,
+    'fakeCities' => 
+    array (
+      0 => 
+      array (
+        'id' => 131024,
+        'name' => '香河县',
+      ),
+      1 => 
+      array (
+        'id' => 1201,
+        'name' => '天津市',
+      ),
+    ),
+    'code' => 1101,
+  ),
+  '保定市' => 
+  array (
+    'index' => 'B',
+    'name' => '保定市',
+    'id' => 1306,
+    'fakeCities' => 
+    array (
+      0 => 
+      array (
+        'id' => 1301,
+        'name' => '石家庄市',
+      ),
+    ),
+    'code' => 1306,
+  ),
+  '包头市' => 
+  array (
+    'index' => 'B',
+    'name' => '包头市',
+    'id' => 1502,
+    'fakeCities' => NULL,
+    'code' => 1502,
+  ),
+  '本溪市' => 
+  array (
+    'index' => 'B',
+    'name' => '本溪市',
+    'id' => 2105,
+    'fakeCities' => NULL,
+    'code' => 2105,
+  ),
+  '蚌埠市' => 
+  array (
+    'index' => 'B',
+    'name' => '蚌埠市',
+    'id' => 3403,
+    'fakeCities' => NULL,
+    'code' => 3403,
+  ),
+  '亳州市' => 
+  array (
+    'index' => 'B',
+    'name' => '亳州市',
+    'id' => 3416,
+    'fakeCities' => NULL,
+    'code' => 3416,
+  ),
+  '滨州市' => 
+  array (
+    'index' => 'B',
+    'name' => '滨州市',
+    'id' => 3716,
+    'fakeCities' => NULL,
+    'code' => 3716,
+  ),
+  '北海市' => 
+  array (
+    'index' => 'B',
+    'name' => '北海市',
+    'id' => 4505,
+    'fakeCities' => NULL,
+    'code' => 4505,
+  ),
+  '宝鸡市' => 
+  array (
+    'index' => 'B',
+    'name' => '宝鸡市',
+    'id' => 6103,
+    'fakeCities' => NULL,
+    'code' => 6103,
+  ),
+  '毕节市' => 
+  array (
+    'index' => 'B',
+    'name' => '毕节市',
+    'id' => 522401,
+    'fakeCities' => NULL,
+    'code' => 5205,
+  ),
+  '承德市' => 
+  array (
+    'index' => 'C',
+    'name' => '承德市',
+    'id' => 1308,
+    'fakeCities' => NULL,
+    'code' => 1308,
+  ),
+  '沧州市' => 
+  array (
+    'index' => 'C',
+    'name' => '沧州市',
+    'id' => 1309,
+    'fakeCities' => NULL,
+    'code' => 1309,
+  ),
+  '长治市' => 
+  array (
+    'index' => 'C',
+    'name' => '长治市',
+    'id' => 1404,
+    'fakeCities' => NULL,
+    'code' => 1404,
+  ),
+  '赤峰市' => 
+  array (
+    'index' => 'C',
+    'name' => '赤峰市',
+    'id' => 1504,
+    'fakeCities' => NULL,
+    'code' => 1504,
+  ),
+  '朝阳市' => 
+  array (
+    'index' => 'C',
+    'name' => '朝阳市',
+    'id' => 2113,
+    'fakeCities' => NULL,
+    'code' => 2113,
+  ),
+  '长春市' => 
+  array (
+    'index' => 'C',
+    'name' => '长春市',
+    'id' => 2201,
+    'fakeCities' => NULL,
+    'code' => 2201,
+  ),
+  '常州市' => 
+  array (
+    'index' => 'C',
+    'name' => '常州市',
+    'id' => 3204,
+    'fakeCities' => NULL,
+    'code' => 3204,
+  ),
+  '滁州市' => 
+  array (
+    'index' => 'C',
+    'name' => '滁州市',
+    'id' => 3411,
+    'fakeCities' => NULL,
+    'code' => 3411,
+  ),
+  '长沙市' => 
+  array (
+    'index' => 'C',
+    'name' => '长沙市',
+    'id' => 4301,
+    'fakeCities' => NULL,
+    'code' => 4301,
+  ),
+  '常德市' => 
+  array (
+    'index' => 'C',
+    'name' => '常德市',
+    'id' => 4307,
+    'fakeCities' => NULL,
+    'code' => 4307,
+  ),
+  '郴州市' => 
+  array (
+    'index' => 'C',
+    'name' => '郴州市',
+    'id' => 4310,
+    'fakeCities' => NULL,
+    'code' => 4310,
+  ),
+  '潮州市' => 
+  array (
+    'index' => 'C',
+    'name' => '潮州市',
+    'id' => 4451,
+    'fakeCities' => NULL,
+    'code' => 4451,
+  ),
+  '重庆市' => 
+  array (
+    'index' => 'C',
+    'name' => '重庆市',
+    'id' => 5001,
+    'fakeCities' => NULL,
+    'code' => 5001,
+  ),
+  '成都市' => 
+  array (
+    'index' => 'C',
+    'name' => '成都市',
+    'id' => 5101,
+    'fakeCities' => NULL,
+    'code' => 5101,
+  ),
+  '常熟市' => 
+  array (
+    'index' => 'C',
+    'name' => '常熟市',
+    'id' => 320581,
+    'fakeCities' => NULL,
+    'code' => 320581,
+  ),
+  '大同市' => 
+  array (
+    'index' => 'D',
+    'name' => '大同市',
+    'id' => 1402,
+    'fakeCities' => NULL,
+    'code' => 1402,
+  ),
+  '大连市' => 
+  array (
+    'index' => 'D',
+    'name' => '大连市',
+    'id' => 2102,
+    'fakeCities' => NULL,
+    'code' => 2102,
+  ),
+  '丹东市' => 
+  array (
+    'index' => 'D',
+    'name' => '丹东市',
+    'id' => 2106,
+    'fakeCities' => NULL,
+    'code' => 2106,
+  ),
+  '大庆市' => 
+  array (
+    'index' => 'D',
+    'name' => '大庆市',
+    'id' => 2306,
+    'fakeCities' => NULL,
+    'code' => 2306,
+  ),
+  '东营市' => 
+  array (
+    'index' => 'D',
+    'name' => '东营市',
+    'id' => 3705,
+    'fakeCities' => NULL,
+    'code' => 3705,
+  ),
+  '德州市' => 
+  array (
+    'index' => 'D',
+    'name' => '德州市',
+    'id' => 3714,
+    'fakeCities' => NULL,
+    'code' => 3714,
+  ),
+  '东莞市' => 
+  array (
+    'index' => 'D',
+    'name' => '东莞市',
+    'id' => 4419,
+    'fakeCities' => NULL,
+    'code' => 4419,
+  ),
+  '德阳市' => 
+  array (
+    'index' => 'D',
+    'name' => '德阳市',
+    'id' => 5106,
+    'fakeCities' => NULL,
+    'code' => 5106,
+  ),
+  '达州市' => 
+  array (
+    'index' => 'D',
+    'name' => '达州市',
+    'id' => 5117,
+    'fakeCities' => NULL,
+    'code' => 5117,
+  ),
+  '定州市' => 
+  array (
+    'index' => 'D',
+    'name' => '定州市',
+    'id' => 130682,
+    'fakeCities' => 
+    array (
+      0 => 
+      array (
+        'id' => 1305,
+        'name' => '邢台市',
+      ),
+    ),
+    'code' => 130682,
+  ),
+  '都匀市' => 
+  array (
+    'index' => 'D',
+    'name' => '都匀市',
+    'id' => 522701,
+    'fakeCities' => NULL,
+    'code' => 522701,
+  ),
+  '大理市' => 
+  array (
+    'index' => 'D',
+    'name' => '大理市',
+    'id' => 532901,
+    'fakeCities' => NULL,
+    'code' => 532901,
+  ),
+  '鄂尔多斯市' => 
+  array (
+    'index' => 'E',
+    'name' => '鄂尔多斯市',
+    'id' => 1506,
+    'fakeCities' => NULL,
+    'code' => 1506,
+  ),
+  '恩施土家族苗族自治州' => 
+  array (
+    'index' => 'E',
+    'name' => '恩施土家族苗族自治州',
+    'id' => 4228,
+    'fakeCities' => NULL,
+    'code' => 4228,
+  ),
+  '恩施市' => 
+  array (
+    'index' => 'E',
+    'name' => '恩施市',
+    'id' => 422801,
+    'fakeCities' => NULL,
+    'code' => 422801,
+  ),
+  '抚顺市' => 
+  array (
+    'index' => 'F',
+    'name' => '抚顺市',
+    'id' => 2104,
+    'fakeCities' => NULL,
+    'code' => 2104,
+  ),
+  '阜阳市' => 
+  array (
+    'index' => 'F',
+    'name' => '阜阳市',
+    'id' => 3412,
+    'fakeCities' => NULL,
+    'code' => 3412,
+  ),
+  '福州市' => 
+  array (
+    'index' => 'F',
+    'name' => '福州市',
+    'id' => 3501,
+    'fakeCities' => NULL,
+    'code' => 3501,
+  ),
+  '抚州市' => 
+  array (
+    'index' => 'F',
+    'name' => '抚州市',
+    'id' => 3610,
+    'fakeCities' => NULL,
+    'code' => 3610,
+  ),
+  '佛山市' => 
+  array (
+    'index' => 'F',
+    'name' => '佛山市',
+    'id' => 4406,
+    'fakeCities' => 
+    array (
+      0 => 
+      array (
+        'id' => 4401,
+        'name' => '广州市',
+      ),
+    ),
+    'code' => 4406,
+  ),
+  '赣州市' => 
+  array (
+    'index' => 'G',
+    'name' => '赣州市',
+    'id' => 3607,
+    'fakeCities' => NULL,
+    'code' => 3607,
+  ),
+  '广州市' => 
+  array (
+    'index' => 'G',
+    'name' => '广州市',
+    'id' => 4401,
+    'fakeCities' => 
+    array (
+      0 => 
+      array (
+        'id' => 4406,
+        'name' => '佛山市',
+      ),
+    ),
+    'code' => 4401,
+  ),
+  '桂林市' => 
+  array (
+    'index' => 'G',
+    'name' => '桂林市',
+    'id' => 4503,
+    'fakeCities' => NULL,
+    'code' => 4503,
+  ),
+  '贵港市' => 
+  array (
+    'index' => 'G',
+    'name' => '贵港市',
+    'id' => 4508,
+    'fakeCities' => NULL,
+    'code' => 4508,
+  ),
+  '贵阳市' => 
+  array (
+    'index' => 'G',
+    'name' => '贵阳市',
+    'id' => 5201,
+    'fakeCities' => NULL,
+    'code' => 5201,
+  ),
+  '邯郸市' => 
+  array (
+    'index' => 'H',
+    'name' => '邯郸市',
+    'id' => 1304,
+    'fakeCities' => NULL,
+    'code' => 1304,
+  ),
+  '衡水市' => 
+  array (
+    'index' => 'H',
+    'name' => '衡水市',
+    'id' => 1311,
+    'fakeCities' => NULL,
+    'code' => 1311,
+  ),
+  '呼和浩特市' => 
+  array (
+    'index' => 'H',
+    'name' => '呼和浩特市',
+    'id' => 1501,
+    'fakeCities' => NULL,
+    'code' => 1501,
+  ),
+  '葫芦岛市' => 
+  array (
+    'index' => 'H',
+    'name' => '葫芦岛市',
+    'id' => 2114,
+    'fakeCities' => NULL,
+    'code' => 2114,
+  ),
+  '哈尔滨市' => 
+  array (
+    'index' => 'H',
+    'name' => '哈尔滨市',
+    'id' => 2301,
+    'fakeCities' => NULL,
+    'code' => 2301,
+  ),
+  '淮安市' => 
+  array (
+    'index' => 'H',
+    'name' => '淮安市',
+    'id' => 3208,
+    'fakeCities' => NULL,
+    'code' => 3208,
+  ),
+  '杭州市' => 
+  array (
+    'index' => 'H',
+    'name' => '杭州市',
+    'id' => 3301,
+    'fakeCities' => 
+    array (
+      0 => 
+      array (
+        'id' => 3304,
+        'name' => '嘉兴市',
+      ),
+      1 => 
+      array (
+        'id' => 3306,
+        'name' => '绍兴市',
+      ),
+      2 => 
+      array (
+        'id' => 3302,
+        'name' => '宁波市',
+      ),
+    ),
+    'code' => 3301,
+  ),
+  '湖州市' => 
+  array (
+    'index' => 'H',
+    'name' => '湖州市',
+    'id' => 3305,
+    'fakeCities' => NULL,
+    'code' => 3305,
+  ),
+  '合肥市' => 
+  array (
+    'index' => 'H',
+    'name' => '合肥市',
+    'id' => 3401,
+    'fakeCities' => NULL,
+    'code' => 3401,
+  ),
+  '淮南市' => 
+  array (
+    'index' => 'H',
+    'name' => '淮南市',
+    'id' => 3404,
+    'fakeCities' => NULL,
+    'code' => 3404,
+  ),
+  '淮北市' => 
+  array (
+    'index' => 'H',
+    'name' => '淮北市',
+    'id' => 3406,
+    'fakeCities' => NULL,
+    'code' => 3406,
+  ),
+  '菏泽市' => 
+  array (
+    'index' => 'H',
+    'name' => '菏泽市',
+    'id' => 3717,
+    'fakeCities' => NULL,
+    'code' => 3717,
+  ),
+  '鹤壁市' => 
+  array (
+    'index' => 'H',
+    'name' => '鹤壁市',
+    'id' => 4106,
+    'fakeCities' => NULL,
+    'code' => 4106,
+  ),
+  '黄石市' => 
+  array (
+    'index' => 'H',
+    'name' => '黄石市',
+    'id' => 4202,
+    'fakeCities' => NULL,
+    'code' => 4202,
+  ),
+  '黄冈市' => 
+  array (
+    'index' => 'H',
+    'name' => '黄冈市',
+    'id' => 4211,
+    'fakeCities' => NULL,
+    'code' => 4211,
+  ),
+  '衡阳市' => 
+  array (
+    'index' => 'H',
+    'name' => '衡阳市',
+    'id' => 4304,
+    'fakeCities' => NULL,
+    'code' => 4304,
+  ),
+  '怀化市' => 
+  array (
+    'index' => 'H',
+    'name' => '怀化市',
+    'id' => 4312,
+    'fakeCities' => NULL,
+    'code' => 4312,
+  ),
+  '惠州市' => 
+  array (
+    'index' => 'H',
+    'name' => '惠州市',
+    'id' => 4413,
+    'fakeCities' => NULL,
+    'code' => 4413,
+  ),
+  '河源市' => 
+  array (
+    'index' => 'H',
+    'name' => '河源市',
+    'id' => 4416,
+    'fakeCities' => NULL,
+    'code' => 4416,
+  ),
+  '贺州市' => 
+  array (
+    'index' => 'H',
+    'name' => '贺州市',
+    'id' => 4511,
+    'fakeCities' => NULL,
+    'code' => 4511,
+  ),
+  '海口市' => 
+  array (
+    'index' => 'H',
+    'name' => '海口市',
+    'id' => 4601,
+    'fakeCities' => NULL,
+    'code' => 4601,
+  ),
+  '汉中市' => 
+  array (
+    'index' => 'H',
+    'name' => '汉中市',
+    'id' => 6107,
+    'fakeCities' => NULL,
+    'code' => 6107,
+  ),
+  '晋城市' => 
+  array (
+    'index' => 'J',
+    'name' => '晋城市',
+    'id' => 1405,
+    'fakeCities' => NULL,
+    'code' => 1405,
+  ),
+  '晋中市' => 
+  array (
+    'index' => 'J',
+    'name' => '晋中市',
+    'id' => 1407,
+    'fakeCities' => NULL,
+    'code' => 1407,
+  ),
+  '锦州市' => 
+  array (
+    'index' => 'J',
+    'name' => '锦州市',
+    'id' => 2107,
+    'fakeCities' => NULL,
+    'code' => 2107,
+  ),
+  '吉林市' => 
+  array (
+    'index' => 'J',
+    'name' => '吉林市',
+    'id' => 2202,
+    'fakeCities' => NULL,
+    'code' => 2202,
+  ),
+  '佳木斯市' => 
+  array (
+    'index' => 'J',
+    'name' => '佳木斯市',
+    'id' => 2308,
+    'fakeCities' => NULL,
+    'code' => 2308,
+  ),
+  '嘉兴市' => 
+  array (
+    'index' => 'J',
+    'name' => '嘉兴市',
+    'id' => 3304,
+    'fakeCities' => 
+    array (
+      0 => 
+      array (
+        'id' => 3301,
+        'name' => '杭州市',
+      ),
+      1 => 
+      array (
+        'id' => 3306,
+        'name' => '绍兴市',
+      ),
+      2 => 
+      array (
+        'id' => 3302,
+        'name' => '宁波市',
+      ),
+    ),
+    'code' => 3304,
+  ),
+  '金华市' => 
+  array (
+    'index' => 'J',
+    'name' => '金华市',
+    'id' => 3307,
+    'fakeCities' => NULL,
+    'code' => 3307,
+  ),
+  '九江市' => 
+  array (
+    'index' => 'J',
+    'name' => '九江市',
+    'id' => 3604,
+    'fakeCities' => NULL,
+    'code' => 3604,
+  ),
+  '吉安市' => 
+  array (
+    'index' => 'J',
+    'name' => '吉安市',
+    'id' => 3608,
+    'fakeCities' => NULL,
+    'code' => 3608,
+  ),
+  '济南市' => 
+  array (
+    'index' => 'J',
+    'name' => '济南市',
+    'id' => 3701,
+    'fakeCities' => NULL,
+    'code' => 3701,
+  ),
+  '济宁市' => 
+  array (
+    'index' => 'J',
+    'name' => '济宁市',
+    'id' => 3708,
+    'fakeCities' => NULL,
+    'code' => 3708,
+  ),
+  '焦作市' => 
+  array (
+    'index' => 'J',
+    'name' => '焦作市',
+    'id' => 4108,
+    'fakeCities' => NULL,
+    'code' => 4108,
+  ),
+  '荆门市' => 
+  array (
+    'index' => 'J',
+    'name' => '荆门市',
+    'id' => 4208,
+    'fakeCities' => NULL,
+    'code' => 4208,
+  ),
+  '荆州市' => 
+  array (
+    'index' => 'J',
+    'name' => '荆州市',
+    'id' => 4210,
+    'fakeCities' => NULL,
+    'code' => 4210,
+  ),
+  '江门市' => 
+  array (
+    'index' => 'J',
+    'name' => '江门市',
+    'id' => 4407,
+    'fakeCities' => NULL,
+    'code' => 4407,
+  ),
+  '揭阳市' => 
+  array (
+    'index' => 'J',
+    'name' => '揭阳市',
+    'id' => 4452,
+    'fakeCities' => NULL,
+    'code' => 4452,
+  ),
+  '开封市' => 
+  array (
+    'index' => 'K',
+    'name' => '开封市',
+    'id' => 4102,
+    'fakeCities' => NULL,
+    'code' => 4102,
+  ),
+  '昆明市' => 
+  array (
+    'index' => 'K',
+    'name' => '昆明市',
+    'id' => 5301,
+    'fakeCities' => NULL,
+    'code' => 5301,
+  ),
+  '克拉玛依市' => 
+  array (
+    'index' => 'K',
+    'name' => '克拉玛依市',
+    'id' => 6502,
+    'fakeCities' => NULL,
+    'code' => 6502,
+  ),
+  '昆山市' => 
+  array (
+    'index' => 'K',
+    'name' => '昆山市',
+    'id' => 320583,
+    'fakeCities' => 
+    array (
+      0 => 
+      array (
+        'id' => 3205,
+        'name' => '苏州市',
+      ),
+    ),
+    'code' => 320583,
+  ),
+  '凯里市' => 
+  array (
+    'index' => 'K',
+    'name' => '凯里市',
+    'id' => 522601,
+    'fakeCities' => NULL,
+    'code' => 522601,
+  ),
+  '廊坊市' => 
+  array (
+    'index' => 'L',
+    'name' => '廊坊市',
+    'id' => 1310,
+    'fakeCities' => NULL,
+    'code' => 1310,
+  ),
+  '临汾市' => 
+  array (
+    'index' => 'L',
+    'name' => '临汾市',
+    'id' => 1410,
+    'fakeCities' => NULL,
+    'code' => 1410,
+  ),
+  '辽阳市' => 
+  array (
+    'index' => 'L',
+    'name' => '辽阳市',
+    'id' => 2110,
+    'fakeCities' => NULL,
+    'code' => 2110,
+  ),
+  '连云港市' => 
+  array (
+    'index' => 'L',
+    'name' => '连云港市',
+    'id' => 3207,
+    'fakeCities' => NULL,
+    'code' => 3207,
+  ),
+  '丽水市' => 
+  array (
+    'index' => 'L',
+    'name' => '丽水市',
+    'id' => 3311,
+    'fakeCities' => NULL,
+    'code' => 3311,
+  ),
+  '六安市' => 
+  array (
+    'index' => 'L',
+    'name' => '六安市',
+    'id' => 3415,
+    'fakeCities' => NULL,
+    'code' => 3415,
+  ),
+  '龙岩市' => 
+  array (
+    'index' => 'L',
+    'name' => '龙岩市',
+    'id' => 3508,
+    'fakeCities' => NULL,
+    'code' => 3508,
+  ),
+  '临沂市' => 
+  array (
+    'index' => 'L',
+    'name' => '临沂市',
+    'id' => 3713,
+    'fakeCities' => NULL,
+    'code' => 3713,
+  ),
+  '聊城市' => 
+  array (
+    'index' => 'L',
+    'name' => '聊城市',
+    'id' => 3715,
+    'fakeCities' => NULL,
+    'code' => 3715,
+  ),
+  '洛阳市' => 
+  array (
+    'index' => 'L',
+    'name' => '洛阳市',
+    'id' => 4103,
+    'fakeCities' => NULL,
+    'code' => 4103,
+  ),
+  '娄底市' => 
+  array (
+    'index' => 'L',
+    'name' => '娄底市',
+    'id' => 4313,
+    'fakeCities' => NULL,
+    'code' => 4313,
+  ),
+  '柳州市' => 
+  array (
+    'index' => 'L',
+    'name' => '柳州市',
+    'id' => 4502,
+    'fakeCities' => NULL,
+    'code' => 4502,
+  ),
+  '泸州市' => 
+  array (
+    'index' => 'L',
+    'name' => '泸州市',
+    'id' => 5105,
+    'fakeCities' => NULL,
+    'code' => 5105,
+  ),
+  '乐山市' => 
+  array (
+    'index' => 'L',
+    'name' => '乐山市',
+    'id' => 5111,
+    'fakeCities' => NULL,
+    'code' => 5111,
+  ),
+  '六盘水市' => 
+  array (
+    'index' => 'L',
+    'name' => '六盘水市',
+    'id' => 5202,
+    'fakeCities' => NULL,
+    'code' => 5202,
+  ),
+  '拉萨市' => 
+  array (
+    'index' => 'L',
+    'name' => '拉萨市',
+    'id' => 5401,
+    'fakeCities' => NULL,
+    'code' => 5401,
+  ),
+  '兰州市' => 
+  array (
+    'index' => 'L',
+    'name' => '兰州市',
+    'id' => 6201,
+    'fakeCities' => NULL,
+    'code' => 6201,
+  ),
+  '醴陵市' => 
+  array (
+    'index' => 'L',
+    'name' => '醴陵市',
+    'id' => 430281,
+    'fakeCities' => NULL,
+    'code' => 430281,
+  ),
+  '牡丹江市' => 
+  array (
+    'index' => 'M',
+    'name' => '牡丹江市',
+    'id' => 2310,
+    'fakeCities' => NULL,
+    'code' => 2310,
+  ),
+  '马鞍山市' => 
+  array (
+    'index' => 'M',
+    'name' => '马鞍山市',
+    'id' => 3405,
+    'fakeCities' => NULL,
+    'code' => 3405,
+  ),
+  '茂名市' => 
+  array (
+    'index' => 'M',
+    'name' => '茂名市',
+    'id' => 4409,
+    'fakeCities' => NULL,
+    'code' => 4409,
+  ),
+  '梅州市' => 
+  array (
+    'index' => 'M',
+    'name' => '梅州市',
+    'id' => 4414,
+    'fakeCities' => NULL,
+    'code' => 4414,
+  ),
+  '绵阳市' => 
+  array (
+    'index' => 'M',
+    'name' => '绵阳市',
+    'id' => 5107,
+    'fakeCities' => NULL,
+    'code' => 5107,
+  ),
+  '眉山市' => 
+  array (
+    'index' => 'M',
+    'name' => '眉山市',
+    'id' => 5114,
+    'fakeCities' => NULL,
+    'code' => 5114,
+  ),
+  '南京市' => 
+  array (
+    'index' => 'N',
+    'name' => '南京市',
+    'id' => 3201,
+    'fakeCities' => NULL,
+    'code' => 3201,
+  ),
+  '南通市' => 
+  array (
+    'index' => 'N',
+    'name' => '南通市',
+    'id' => 3206,
+    'fakeCities' => NULL,
+    'code' => 3206,
+  ),
+  '宁波市' => 
+  array (
+    'index' => 'N',
+    'name' => '宁波市',
+    'id' => 3302,
+    'fakeCities' => 
+    array (
+      0 => 
+      array (
+        'id' => 3301,
+        'name' => '杭州市',
+      ),
+      1 => 
+      array (
+        'id' => 3304,
+        'name' => '嘉兴市',
+      ),
+      2 => 
+      array (
+        'id' => 3306,
+        'name' => '绍兴市',
+      ),
+    ),
+    'code' => 3302,
+  ),
+  '南平市' => 
+  array (
+    'index' => 'N',
+    'name' => '南平市',
+    'id' => 3507,
+    'fakeCities' => NULL,
+    'code' => 3507,
+  ),
+  '宁德市' => 
+  array (
+    'index' => 'N',
+    'name' => '宁德市',
+    'id' => 3509,
+    'fakeCities' => NULL,
+    'code' => 3509,
+  ),
+  '南昌市' => 
+  array (
+    'index' => 'N',
+    'name' => '南昌市',
+    'id' => 3601,
+    'fakeCities' => NULL,
+    'code' => 3601,
+  ),
+  '南阳市' => 
+  array (
+    'index' => 'N',
+    'name' => '南阳市',
+    'id' => 4113,
+    'fakeCities' => NULL,
+    'code' => 4113,
+  ),
+  '南宁市' => 
+  array (
+    'index' => 'N',
+    'name' => '南宁市',
+    'id' => 4501,
+    'fakeCities' => NULL,
+    'code' => 4501,
+  ),
+  '内江市' => 
+  array (
+    'index' => 'N',
+    'name' => '内江市',
+    'id' => 5110,
+    'fakeCities' => NULL,
+    'code' => 5110,
+  ),
+  '南充市' => 
+  array (
+    'index' => 'N',
+    'name' => '南充市',
+    'id' => 5113,
+    'fakeCities' => NULL,
+    'code' => 5113,
+  ),
+  '盘锦市' => 
+  array (
+    'index' => 'P',
+    'name' => '盘锦市',
+    'id' => 2111,
+    'fakeCities' => NULL,
+    'code' => 2111,
+  ),
+  '莆田市' => 
+  array (
+    'index' => 'P',
+    'name' => '莆田市',
+    'id' => 3503,
+    'fakeCities' => NULL,
+    'code' => 3503,
+  ),
+  '平顶山市' => 
+  array (
+    'index' => 'P',
+    'name' => '平顶山市',
+    'id' => 4104,
+    'fakeCities' => NULL,
+    'code' => 4104,
+  ),
+  '濮阳市' => 
+  array (
+    'index' => 'P',
+    'name' => '濮阳市',
+    'id' => 4109,
+    'fakeCities' => NULL,
+    'code' => 4109,
+  ),
+  '秦皇岛市' => 
+  array (
+    'index' => 'Q',
+    'name' => '秦皇岛市',
+    'id' => 1303,
+    'fakeCities' => NULL,
+    'code' => 1303,
+  ),
+  '齐齐哈尔市' => 
+  array (
+    'index' => 'Q',
+    'name' => '齐齐哈尔市',
+    'id' => 2302,
+    'fakeCities' => NULL,
+    'code' => 2302,
+  ),
+  '衢州市' => 
+  array (
+    'index' => 'Q',
+    'name' => '衢州市',
+    'id' => 3308,
+    'fakeCities' => NULL,
+    'code' => 3308,
+  ),
+  '泉州市' => 
+  array (
+    'index' => 'Q',
+    'name' => '泉州市',
+    'id' => 3505,
+    'fakeCities' => NULL,
+    'code' => 3505,
+  ),
+  '青岛市' => 
+  array (
+    'index' => 'Q',
+    'name' => '青岛市',
+    'id' => 3702,
+    'fakeCities' => NULL,
+    'code' => 3702,
+  ),
+  '清远市' => 
+  array (
+    'index' => 'Q',
+    'name' => '清远市',
+    'id' => 4418,
+    'fakeCities' => NULL,
+    'code' => 4418,
+  ),
+  '钦州市' => 
+  array (
+    'index' => 'Q',
+    'name' => '钦州市',
+    'id' => 4507,
+    'fakeCities' => NULL,
+    'code' => 4507,
+  ),
+  '曲靖市' => 
+  array (
+    'index' => 'Q',
+    'name' => '曲靖市',
+    'id' => 5303,
+    'fakeCities' => NULL,
+    'code' => 5303,
+  ),
+  '日照市' => 
+  array (
+    'index' => 'R',
+    'name' => '日照市',
+    'id' => 3711,
+    'fakeCities' => NULL,
+    'code' => 3711,
+  ),
+  '石家庄市' => 
+  array (
+    'index' => 'S',
+    'name' => '石家庄市',
+    'id' => 1301,
+    'fakeCities' => 
+    array (
+      0 => 
+      array (
+        'id' => 1306,
+        'name' => '保定市',
+      ),
+    ),
+    'code' => 1301,
+  ),
+  '沈阳市' => 
+  array (
+    'index' => 'S',
+    'name' => '沈阳市',
+    'id' => 2101,
+    'fakeCities' => NULL,
+    'code' => 2101,
+  ),
+  '四平市' => 
+  array (
+    'index' => 'S',
+    'name' => '四平市',
+    'id' => 2203,
+    'fakeCities' => NULL,
+    'code' => 2203,
+  ),
+  '松原市' => 
+  array (
+    'index' => 'S',
+    'name' => '松原市',
+    'id' => 2207,
+    'fakeCities' => NULL,
+    'code' => 2207,
+  ),
+  '绥化市' => 
+  array (
+    'index' => 'S',
+    'name' => '绥化市',
+    'id' => 2312,
+    'fakeCities' => NULL,
+    'code' => 2312,
+  ),
+  '上海市' => 
+  array (
+    'index' => 'S',
+    'name' => '上海市',
+    'id' => 3101,
+    'fakeCities' => NULL,
+    'code' => 3101,
+  ),
+  '苏州市' => 
+  array (
+    'index' => 'S',
+    'name' => '苏州市',
+    'id' => 3205,
+    'fakeCities' => 
+    array (
+      0 => 
+      array (
+        'id' => 320583,
+        'name' => '昆山市',
+      ),
+    ),
+    'code' => 3205,
+  ),
+  '宿迁市' => 
+  array (
+    'index' => 'S',
+    'name' => '宿迁市',
+    'id' => 3213,
+    'fakeCities' => NULL,
+    'code' => 3213,
+  ),
+  '绍兴市' => 
+  array (
+    'index' => 'S',
+    'name' => '绍兴市',
+    'id' => 3306,
+    'fakeCities' => 
+    array (
+      0 => 
+      array (
+        'id' => 3301,
+        'name' => '杭州市',
+      ),
+      1 => 
+      array (
+        'id' => 3304,
+        'name' => '嘉兴市',
+      ),
+      2 => 
+      array (
+        'id' => 3302,
+        'name' => '宁波市',
+      ),
+    ),
+    'code' => 3306,
+  ),
+  '宿州市' => 
+  array (
+    'index' => 'S',
+    'name' => '宿州市',
+    'id' => 3413,
+    'fakeCities' => NULL,
+    'code' => 3413,
+  ),
+  '三明市' => 
+  array (
+    'index' => 'S',
+    'name' => '三明市',
+    'id' => 3504,
+    'fakeCities' => NULL,
+    'code' => 3504,
+  ),
+  '上饶市' => 
+  array (
+    'index' => 'S',
+    'name' => '上饶市',
+    'id' => 3611,
+    'fakeCities' => NULL,
+    'code' => 3611,
+  ),
+  '商丘市' => 
+  array (
+    'index' => 'S',
+    'name' => '商丘市',
+    'id' => 4114,
+    'fakeCities' => NULL,
+    'code' => 4114,
+  ),
+  '十堰市' => 
+  array (
+    'index' => 'S',
+    'name' => '十堰市',
+    'id' => 4203,
+    'fakeCities' => NULL,
+    'code' => 4203,
+  ),
+  '邵阳市' => 
+  array (
+    'index' => 'S',
+    'name' => '邵阳市',
+    'id' => 4305,
+    'fakeCities' => NULL,
+    'code' => 4305,
+  ),
+  '韶关市' => 
+  array (
+    'index' => 'S',
+    'name' => '韶关市',
+    'id' => 4402,
+    'fakeCities' => NULL,
+    'code' => 4402,
+  ),
+  '深圳市' => 
+  array (
+    'index' => 'S',
+    'name' => '深圳市',
+    'id' => 4403,
+    'fakeCities' => NULL,
+    'code' => 4403,
+  ),
+  '汕头市' => 
+  array (
+    'index' => 'S',
+    'name' => '汕头市',
+    'id' => 4405,
+    'fakeCities' => NULL,
+    'code' => 4405,
+  ),
+  '汕尾市' => 
+  array (
+    'index' => 'S',
+    'name' => '汕尾市',
+    'id' => 4415,
+    'fakeCities' => NULL,
+    'code' => 4415,
+  ),
+  '三亚市' => 
+  array (
+    'index' => 'S',
+    'name' => '三亚市',
+    'id' => 4602,
+    'fakeCities' => NULL,
+    'code' => 4602,
+  ),
+  '遂宁市' => 
+  array (
+    'index' => 'S',
+    'name' => '遂宁市',
+    'id' => 5109,
+    'fakeCities' => NULL,
+    'code' => 5109,
+  ),
+  '天津市' => 
+  array (
+    'index' => 'T',
+    'name' => '天津市',
+    'id' => 1201,
+    'fakeCities' => 
+    array (
+      0 => 
+      array (
+        'id' => 1101,
+        'name' => '北京市',
+      ),
+      1 => 
+      array (
+        'id' => 131024,
+        'name' => '香河县',
+      ),
+    ),
+    'code' => 1201,
+  ),
+  '唐山市' => 
+  array (
+    'index' => 'T',
+    'name' => '唐山市',
+    'id' => 1302,
+    'fakeCities' => NULL,
+    'code' => 1302,
+  ),
+  '太原市' => 
+  array (
+    'index' => 'T',
+    'name' => '太原市',
+    'id' => 1401,
+    'fakeCities' => NULL,
+    'code' => 1401,
+  ),
+  '通辽市' => 
+  array (
+    'index' => 'T',
+    'name' => '通辽市',
+    'id' => 1505,
+    'fakeCities' => NULL,
+    'code' => 1505,
+  ),
+  '铁岭市' => 
+  array (
+    'index' => 'T',
+    'name' => '铁岭市',
+    'id' => 2112,
+    'fakeCities' => NULL,
+    'code' => 2112,
+  ),
+  '通化市' => 
+  array (
+    'index' => 'T',
+    'name' => '通化市',
+    'id' => 2205,
+    'fakeCities' => NULL,
+    'code' => 2205,
+  ),
+  '泰州市' => 
+  array (
+    'index' => 'T',
+    'name' => '泰州市',
+    'id' => 3212,
+    'fakeCities' => NULL,
+    'code' => 3212,
+  ),
+  '台州市' => 
+  array (
+    'index' => 'T',
+    'name' => '台州市',
+    'id' => 3310,
+    'fakeCities' => NULL,
+    'code' => 3310,
+  ),
+  '泰安市' => 
+  array (
+    'index' => 'T',
+    'name' => '泰安市',
+    'id' => 3709,
+    'fakeCities' => NULL,
+    'code' => 3709,
+  ),
+  '铜仁市' => 
+  array (
+    'index' => 'T',
+    'name' => '铜仁市',
+    'id' => 522201,
+    'fakeCities' => NULL,
+    'code' => 5206,
+  ),
+  '无锡市' => 
+  array (
+    'index' => 'W',
+    'name' => '无锡市',
+    'id' => 3202,
+    'fakeCities' => NULL,
+    'code' => 3202,
+  ),
+  '温州市' => 
+  array (
+    'index' => 'W',
+    'name' => '温州市',
+    'id' => 3303,
+    'fakeCities' => NULL,
+    'code' => 3303,
+  ),
+  '芜湖市' => 
+  array (
+    'index' => 'W',
+    'name' => '芜湖市',
+    'id' => 3402,
+    'fakeCities' => NULL,
+    'code' => 3402,
+  ),
+  '潍坊市' => 
+  array (
+    'index' => 'W',
+    'name' => '潍坊市',
+    'id' => 3707,
+    'fakeCities' => NULL,
+    'code' => 3707,
+  ),
+  '威海市' => 
+  array (
+    'index' => 'W',
+    'name' => '威海市',
+    'id' => 3710,
+    'fakeCities' => NULL,
+    'code' => 3710,
+  ),
+  '武汉市' => 
+  array (
+    'index' => 'W',
+    'name' => '武汉市',
+    'id' => 4201,
+    'fakeCities' => NULL,
+    'code' => 4201,
+  ),
+  '梧州市' => 
+  array (
+    'index' => 'W',
+    'name' => '梧州市',
+    'id' => 4504,
+    'fakeCities' => NULL,
+    'code' => 4504,
+  ),
+  '渭南市' => 
+  array (
+    'index' => 'W',
+    'name' => '渭南市',
+    'id' => 6105,
+    'fakeCities' => NULL,
+    'code' => 6105,
+  ),
+  '乌鲁木齐市' => 
+  array (
+    'index' => 'W',
+    'name' => '乌鲁木齐市',
+    'id' => 6501,
+    'fakeCities' => NULL,
+    'code' => 6501,
+  ),
+  '邢台市' => 
+  array (
+    'index' => 'X',
+    'name' => '邢台市',
+    'id' => 1305,
+    'fakeCities' => 
+    array (
+      0 => 
+      array (
+        'id' => 130682,
+        'name' => '定州市',
+      ),
+    ),
+    'code' => 1305,
+  ),
+  '徐州市' => 
+  array (
+    'index' => 'X',
+    'name' => '徐州市',
+    'id' => 3203,
+    'fakeCities' => NULL,
+    'code' => 3203,
+  ),
+  '厦门市' => 
+  array (
+    'index' => 'X',
+    'name' => '厦门市',
+    'id' => 3502,
+    'fakeCities' => NULL,
+    'code' => 3502,
+  ),
+  '新乡市' => 
+  array (
+    'index' => 'X',
+    'name' => '新乡市',
+    'id' => 4107,
+    'fakeCities' => NULL,
+    'code' => 4107,
+  ),
+  '许昌市' => 
+  array (
+    'index' => 'X',
+    'name' => '许昌市',
+    'id' => 4110,
+    'fakeCities' => NULL,
+    'code' => 4110,
+  ),
+  '信阳市' => 
+  array (
+    'index' => 'X',
+    'name' => '信阳市',
+    'id' => 4115,
+    'fakeCities' => NULL,
+    'code' => 4115,
+  ),
+  '孝感市' => 
+  array (
+    'index' => 'X',
+    'name' => '孝感市',
+    'id' => 4209,
+    'fakeCities' => NULL,
+    'code' => 4209,
+  ),
+  '咸宁市' => 
+  array (
+    'index' => 'X',
+    'name' => '咸宁市',
+    'id' => 4212,
+    'fakeCities' => NULL,
+    'code' => 4212,
+  ),
+  '湘潭市' => 
+  array (
+    'index' => 'X',
+    'name' => '湘潭市',
+    'id' => 4303,
+    'fakeCities' => NULL,
+    'code' => 4303,
+  ),
+  '西安市' => 
+  array (
+    'index' => 'X',
+    'name' => '西安市',
+    'id' => 6101,
+    'fakeCities' => NULL,
+    'code' => 6101,
+  ),
+  '咸阳市' => 
+  array (
+    'index' => 'X',
+    'name' => '咸阳市',
+    'id' => 6104,
+    'fakeCities' => NULL,
+    'code' => 6104,
+  ),
+  '西宁市' => 
+  array (
+    'index' => 'X',
+    'name' => '西宁市',
+    'id' => 6301,
+    'fakeCities' => NULL,
+    'code' => 6301,
+  ),
+  '香河县' => 
+  array (
+    'index' => 'X',
+    'name' => '香河县',
+    'id' => 131024,
+    'fakeCities' => 
+    array (
+      0 => 
+      array (
+        'id' => 1101,
+        'name' => '北京市',
+      ),
+      1 => 
+      array (
+        'id' => 1201,
+        'name' => '天津市',
+      ),
+    ),
+    'code' => 131024,
+  ),
+  '襄阳市' => 
+  array (
+    'index' => 'X',
+    'name' => '襄阳市',
+    'id' => 420607,
+    'fakeCities' => NULL,
+    'code' => 4206,
+  ),
+  '运城市' => 
+  array (
+    'index' => 'Y',
+    'name' => '运城市',
+    'id' => 1408,
+    'fakeCities' => NULL,
+    'code' => 1408,
+  ),
+  '营口市' => 
+  array (
+    'index' => 'Y',
+    'name' => '营口市',
+    'id' => 2108,
+    'fakeCities' => NULL,
+    'code' => 2108,
+  ),
+  '盐城市' => 
+  array (
+    'index' => 'Y',
+    'name' => '盐城市',
+    'id' => 3209,
+    'fakeCities' => NULL,
+    'code' => 3209,
+  ),
+  '扬州市' => 
+  array (
+    'index' => 'Y',
+    'name' => '扬州市',
+    'id' => 3210,
+    'fakeCities' => NULL,
+    'code' => 3210,
+  ),
+  '宜春市' => 
+  array (
+    'index' => 'Y',
+    'name' => '宜春市',
+    'id' => 3609,
+    'fakeCities' => NULL,
+    'code' => 3609,
+  ),
+  '烟台市' => 
+  array (
+    'index' => 'Y',
+    'name' => '烟台市',
+    'id' => 3706,
+    'fakeCities' => NULL,
+    'code' => 3706,
+  ),
+  '宜昌市' => 
+  array (
+    'index' => 'Y',
+    'name' => '宜昌市',
+    'id' => 4205,
+    'fakeCities' => NULL,
+    'code' => 4205,
+  ),
+  '岳阳市' => 
+  array (
+    'index' => 'Y',
+    'name' => '岳阳市',
+    'id' => 4306,
+    'fakeCities' => NULL,
+    'code' => 4306,
+  ),
+  '永州市' => 
+  array (
+    'index' => 'Y',
+    'name' => '永州市',
+    'id' => 4311,
+    'fakeCities' => NULL,
+    'code' => 4311,
+  ),
+  '阳江市' => 
+  array (
+    'index' => 'Y',
+    'name' => '阳江市',
+    'id' => 4417,
+    'fakeCities' => NULL,
+    'code' => 4417,
+  ),
+  '云浮市' => 
+  array (
+    'index' => 'Y',
+    'name' => '云浮市',
+    'id' => 4453,
+    'fakeCities' => NULL,
+    'code' => 4453,
+  ),
+  '玉林市' => 
+  array (
+    'index' => 'Y',
+    'name' => '玉林市',
+    'id' => 4509,
+    'fakeCities' => NULL,
+    'code' => 4509,
+  ),
+  '宜宾市' => 
+  array (
+    'index' => 'Y',
+    'name' => '宜宾市',
+    'id' => 5115,
+    'fakeCities' => NULL,
+    'code' => 5115,
+  ),
+  '玉溪市' => 
+  array (
+    'index' => 'Y',
+    'name' => '玉溪市',
+    'id' => 5304,
+    'fakeCities' => NULL,
+    'code' => 5304,
+  ),
+  '榆林市' => 
+  array (
+    'index' => 'Y',
+    'name' => '榆林市',
+    'id' => 6108,
+    'fakeCities' => NULL,
+    'code' => 6108,
+  ),
+  '银川市' => 
+  array (
+    'index' => 'Y',
+    'name' => '银川市',
+    'id' => 6401,
+    'fakeCities' => NULL,
+    'code' => 6401,
+  ),
+  '伊犁哈萨克自治州' => 
+  array (
+    'index' => 'Y',
+    'name' => '伊犁哈萨克自治州',
+    'id' => 6540,
+    'fakeCities' => NULL,
+    'code' => 6540,
+  ),
+  '义乌市' => 
+  array (
+    'index' => 'Y',
+    'name' => '义乌市',
+    'id' => 330782,
+    'fakeCities' => NULL,
+    'code' => 330782,
+  ),
+  '伊宁市' => 
+  array (
+    'index' => 'Y',
+    'name' => '伊宁市',
+    'id' => 654002,
+    'fakeCities' => NULL,
+    'code' => 654002,
+  ),
+  '张家口市' => 
+  array (
+    'index' => 'Z',
+    'name' => '张家口市',
+    'id' => 1307,
+    'fakeCities' => NULL,
+    'code' => 1307,
+  ),
+  '镇江市' => 
+  array (
+    'index' => 'Z',
+    'name' => '镇江市',
+    'id' => 3211,
+    'fakeCities' => NULL,
+    'code' => 3211,
+  ),
+  '漳州市' => 
+  array (
+    'index' => 'Z',
+    'name' => '漳州市',
+    'id' => 3506,
+    'fakeCities' => NULL,
+    'code' => 3506,
+  ),
+  '淄博市' => 
+  array (
+    'index' => 'Z',
+    'name' => '淄博市',
+    'id' => 3703,
+    'fakeCities' => NULL,
+    'code' => 3703,
+  ),
+  '枣庄市' => 
+  array (
+    'index' => 'Z',
+    'name' => '枣庄市',
+    'id' => 3704,
+    'fakeCities' => NULL,
+    'code' => 3704,
+  ),
+  '郑州市' => 
+  array (
+    'index' => 'Z',
+    'name' => '郑州市',
+    'id' => 4101,
+    'fakeCities' => NULL,
+    'code' => 4101,
+  ),
+  '周口市' => 
+  array (
+    'index' => 'Z',
+    'name' => '周口市',
+    'id' => 4116,
+    'fakeCities' => NULL,
+    'code' => 4116,
+  ),
+  '驻马店市' => 
+  array (
+    'index' => 'Z',
+    'name' => '驻马店市',
+    'id' => 4117,
+    'fakeCities' => NULL,
+    'code' => 4117,
+  ),
+  '株洲市' => 
+  array (
+    'index' => 'Z',
+    'name' => '株洲市',
+    'id' => 4302,
+    'fakeCities' => NULL,
+    'code' => 4302,
+  ),
+  '珠海市' => 
+  array (
+    'index' => 'Z',
+    'name' => '珠海市',
+    'id' => 4404,
+    'fakeCities' => NULL,
+    'code' => 4404,
+  ),
+  '湛江市' => 
+  array (
+    'index' => 'Z',
+    'name' => '湛江市',
+    'id' => 4408,
+    'fakeCities' => NULL,
+    'code' => 4408,
+  ),
+  '肇庆市' => 
+  array (
+    'index' => 'Z',
+    'name' => '肇庆市',
+    'id' => 4412,
+    'fakeCities' => NULL,
+    'code' => 4412,
+  ),
+  '中山市' => 
+  array (
+    'index' => 'Z',
+    'name' => '中山市',
+    'id' => 4420,
+    'fakeCities' => NULL,
+    'code' => 4420,
+  ),
+  '自贡市' => 
+  array (
+    'index' => 'Z',
+    'name' => '自贡市',
+    'id' => 5103,
+    'fakeCities' => NULL,
+    'code' => 5103,
+  ),
+  '遵义市' => 
+  array (
+    'index' => 'Z',
+    'name' => '遵义市',
+    'id' => 5203,
+    'fakeCities' => NULL,
+    'code' => 5203,
+  ),
+  '昭通市' => 
+  array (
+    'index' => 'Z',
+    'name' => '昭通市',
+    'id' => 5306,
+    'fakeCities' => NULL,
+    'code' => 5306,
+  ),
+); ?>

+ 182 - 68
common/components/delivery/services/DispatchService.php

@@ -15,19 +15,39 @@ use Yii;
 class DispatchService
 {
     protected $adapters;
+    protected $platformName = '';
 
-    public function __construct($mainId)
+    public function __construct($mainId, $platform='')
     {
-        $authPlatforms = ShansAuthTokenClass::getAllByCondition(['user_id'=>$mainId]);
-        foreach($authPlatforms as $pt) {
-            switch ($pt['platform']) {
+        if ($platform == '') {
+            $authPlatforms = ShansAuthTokenClass::getAllByCondition(['user_id'=>$mainId]);
+            foreach($authPlatforms as $pt) {
+                switch ($pt['platform']) {
+                    case 'shansong':
+                        $this->adapters['shansong'] = new ShansongAdapter($pt['access_token']);
+                        break;
+                    case 'huolala':
+                        $this->adapters['huolala'] = new HuolalaAdapter($pt['access_token']);
+                        break;
+                    case 'fengniao':
+                        $this->adapters['fengniao'] = new FengniaoAdapter($pt['access_token']);
+                        break;
+                }
+            }
+        } else {
+            $authPlatform = ShansAuthTokenClass::getByCondition(['user_id'=>$mainId, 'platform'=>$platform]);
+            switch ($platform) {
                 case 'shansong':
-                    $this->adapters['shansong'] = new ShansongAdapter($pt['access_token']);
+                    $this->adapters['shansong'] = new ShansongAdapter($authPlatform['access_token']);
                     break;
                 case 'huolala':
-                    $this->adapters['huolala'] = new HuolalaAdapter($pt['access_token']);
+                    $this->adapters['huolala'] = new HuolalaAdapter($authPlatform['access_token']);
+                    break;
+                case 'fengniao':
+                    $this->adapters['fengniao'] = new FengniaoAdapter($authPlatform['access_token']);
                     break;
             }
+            $this->platformName = $platform;
         }
 
         //$this->adapters = [
@@ -42,99 +62,193 @@ class DispatchService
     /**
      * 发单调度
      */
-    public function createOrder($mainId, $orderData)
+    public function createOrder($order)
     {
-        $merchantAccount = DeliveryAccount::where('mainId', $mainId)
-            ->where('is_active', 1)
-            ->first();
-
-        // 优先使用商户自配
-        if ($merchantAccount) {
-            $adapter = $this->adapters[$merchantAccount->platform];
-            return $adapter->createOrder($merchantAccount, $orderData);
-        }
-
-        // 否则使用平台优惠价策略调度
-        $platform = $this->getBestPlatform($orderData);
+        $platform = $this->getPlatform();
         $adapter = $this->adapters[$platform];
+        $orderData = $adapter->formatOrderData($order);
         return $adapter->createOrder($orderData);
     }
 
-    protected function getBestPlatform($orderData)
+    protected function setPlatform()
+    {
+
+    }
+
+    protected function getPlatform()
     {
-        // 简化策略:根据距离、重量、历史价格动态选择
-        $candidates = ['shansong']; //'meituan', 'dada', 'sf',  'uu'
-        return $candidates[array_rand($candidates)];
+        return $this->adapters[$this->platformName];
     }
 
-    public function getBestPlatformByPrice($orderData)
+    public function getBestPlatformByPrice($order, $shop)
     {
         $results = [];
         $accessToken = '';
+        $orderData = [];
 
-        $saveOrderData = $orderData;
         foreach ($this->adapters as $name => $adapter) {
             if ($name == 'huolala') {
-                $tempOrder = [
-                    'city_id' => 1006,                        // 上海
-                    'order_vehicle_id' => 6117,               // 车型ID
-                    'city_info_revision' => 3743,             // 城市版本号
-                    'order_time' => time() + 600,             // 10分钟后用车
-                    
-                    // 多个地址:发货地 → 经停点1 → 经停点2 → 收货地
+                $formatCity = rtrim($order['city'], '市');
+
+                $cities = include Yii::getAlias('@common/components/delivery/platform/huolala/cities.php');
+                if (isset($cities[$formatCity])) {
+                    $cityId = $cities[$formatCity]['city_id'];
+                } else {
+                    Yii::error('城市编码表中没有找到城市: ' . $formatCity);
+                    throw new \Exception('城市编码表中没有找到城市: ' . $formatCity);
+                }
+                $cityVehicleList = $this->adapters['huolala']->getCityVehicleList($cityId);
+                $cityInfoRevision = $cityVehicleList['city_info_revision'];
+
+                // 获取 "跑腿"
+                $vehicleList = $cityVehicleList['vehicle_list'];
+                $selectVehicle = [];
+                $vehicleType = '';
+                foreach($vehicleList as $vehicle) {
+                    if ($vehicle['vehicle_name'] == '跑腿') {
+                        $selectVehicle = $vehicle;
+                        $vehicleType = '跑腿';
+                        break;
+                    }
+                }
+
+                $erLunVehicle = [];
+                $mianBaoVehicle = [];
+                if (empty($selectVehicle)) {
+                    foreach($vehicleList as $vehicle) {
+                        if ($vehicle['vehicle_name'] == '二轮车') {
+                            $erLunVehicle = $vehicle;
+                        }
+                        if ($vehicle['vehicle_name'] == '微面' || $vehicle['vehicle_name'] == '小面车' || $vehicle['vehicle_name'] == '小面包车') {
+                            $mianBaoVehicle = $vehicle;
+                            $vehicleType = $vehicle['vehicle_name'];
+                        }
+                    }
+                }
+
+                $orderVehicleId = $selectVehicle != [] ? $selectVehicle['order_vehicle_id'] : ($erLunVehicle != [] ? $erLunVehicle['order_vehicle_id'] : $mianBaoVehicle != [] ? $mianBaoVehicle['order_vehicle_id'] : 0);
+                if ($orderVehicleId == 0) {
+                    new \Exception('没有找到可选车型');
+                }
+               
+                // 需要实现通过城市编码表查询 city_id、order_vehicle_id、city_info_revision
+                $orderData = [
+                    'city_id' => $cityId,                       // 根据 $order['city'] 查询城市ID
+                    'order_vehicle_id' => $orderVehicleId,      // 根据业务规则确定车型ID
+                    'city_info_revision' => $cityInfoRevision,  // 根据城市信息查询版本号
+                    'order_time' => time() + 600,               // 10分钟后用车
+
+                    // 寄件地址 → 收货地址
                     'addr_info' => [
                         [
-                            'name' => '万达影城(五角场万达广场店)',
-                            'addr' => '上海市杨浦区国宾路58号万达广场3层',
-                            'city_id' => 1006,
-                            'city_name' => '上海',
-                            'district_name' => '杨浦区',
-                            'house_number' => '万达广场3层',
-                            'contacts_name' => 'wendy',
-                            'contacts_phone_no' => '13027251129',
-                            'lat_lon' => ['lat' => 31.30105, 'lon' => 121.513353],
+                            'name' => $shop['shopName'],
+                            'addr' => $shop['province'] . $shop['city'] . $shop['dist'] . $shop['address'],
+                            'city_id' => $cityId,              // 与上面的 city_id 保持一致
+                            'city_name' => $shop['city'],
+                            'district_name' => $shop['dist'],
+                            'house_number' => $shop['floor'],
+                            'contacts_name' => $shop['mobile'],
+                            'contacts_phone_no' => $shop['mobile'],
+                            'lat_lon' => ['lat' => (float)$shop['lat'], 'lon' => (float)$shop['long']],
                         ],
                         [
-                            'name' => '万达广场(上海松江店)',
-                            'addr' => '上海市-松江区-广富林路658号',
-                            'city_id' => 1006,
-                            'city_name' => '上海',
-                            'district_name' => '松江区',
-                            'house_number' => '',
-                            'contacts_name' => 'wendy',
-                            'contacts_phone_no' => '15839679352',
-                            'lat_lon' => ['lat' => 31.057754389551, 'lon' => 121.24308776893],
-                        ],
-                        [
-                            'name' => '万达广场(上海江桥店)',
-                            'addr' => '上海市-嘉定区-金沙江西路1051弄',
-                            'city_id' => 1006,
-                            'city_name' => '上海',
-                            'district_name' => '嘉定区',
-                            'house_number' => '',
-                            'contacts_name' => 'wendy',
-                            'contacts_phone_no' => '18514587355',
-                            'lat_lon' => ['lat' => 31.239945240482, 'lon' => 121.32368743374],
+                            'name' => $order['customName'],
+                            'addr' => $order['fullAddress'],
+                            'city_id' => $cityId,              // 与上面的 city_id 保持一致
+                            'city_name' => $order['city'],
+                            'district_name' => $order['dist'],
+                            'house_number' => $order['floor'],
+                            'contacts_name' => $order['customName'],
+                            'contacts_phone_no' => $order['customMobile'],
+                            'lat_lon' => ['lat' => (float)$order['lat'], 'lon' => (float)$order['long']],
                         ]
                     ],
-                    
+
                     // 车型要求
                     'vehicle_std' => ['全拆座', '面包车'],
-                    
+
                     // 城市额外需求
                     'spec_req' => [3, 4, 5, 6, 7, 8],
-                    
+
                     // 可选:优惠券、开票、服务类型等
                     'coupon_id' => 123456,
                     'invoice_type' => 1,
                     'order_service_type' => 1,
                 ];
-                $orderData = $tempOrder;
-            } else {
-                $orderData = $saveOrderData;
+            } else if ($name == 'fengniao') {
+                $adapter->setMerchantId(14594092); // TODO: 确认是否需要使用动态的商户ID
+                // TODO: 需要实现 goods_item_list 的商品信息补充
+                $orderData = [
+                    'partner_order_code' => $order['orderSn'],
+                    'receiver_primary_phone' => $order['customMobile'],
+                    'receiver_name' => $order['customName'],
+                    'receiver_latitude' => (float)$order['lat'],
+                    'receiver_longitude' => (float)$order['long'],
+                    'receiver_address' => $order['fullAddress'],
+                    'position_source' => 3,             // 高德地图
+                    'goods_count' => 1,                 // TODO: 根据订单商品数量确定
+                    'goods_weight' => (float)$order['weight'],
+                    'goods_total_amount_cent' => (int)($order['prePrice'] * 100),
+                    'goods_actual_amount_cent' => (int)($order['actPrice'] * 100),
+                    'goods_item_list' => [
+                        // TODO: 补充实际的商品信息
+                        // [
+                        //     'item_id' => '',
+                        //     'item_name' => '',
+                        //     'item_amount_cent' => 0,
+                        //     'item_actual_amount_cent' => 0,
+                        //     'item_quantity' => 0,
+                        //     'item_size' => 0,
+                        // ],
+                    ],
+                    'order_type' => 1,
+                    'chain_store_id' => 467788524,      // TODO: 后续修改为动态的门店号
+                    'order_remark' => $order['remark'],
+                ];
+            } else if($name == 'shansong') {
+                $orderData = [
+                    'city_name' => $shop['city'],
+                    'sender' => [
+                        'from_address' => $shop['address'],
+                        'from_address_detail' => $shop['floor'],
+                        'from_sender_name' => $shop['shopName'],
+                        'from_mobile' => $shop['mobile'],
+                        'from_latitude' => $shop['lat'],
+                        'from_longitude' => $shop['long'],
+                    ],
+                    'receiver_list' => [
+                        [
+                            'order_no' => $order['orderSn'],
+                            'to_address' => $order['address'],
+                            'to_address_detail' => $order['floor'],
+                            'to_receiver_name' => $order['customName'],
+                            'to_mobile' => $order['customMobile'],
+                            'to_latitude' => $order['lat'],
+                            'to_longitude' => $order['long'],
+                            'good_type' => 7,               // 鲜花
+                            'weight' => (int)$order['weight'],  // 物品重量(kg,整数)
+                            'remarks' => $order['remark'],
+                        ]
+                    ],
+                    'appoint_type' => 0,                // 0: 立即单,1: 预约单
+                    'appointment_date' => '',           // 预约时间 yyyy-MM-dd HH:mm
+                    'travel_way' => 0,                  // 指定交通工具,0: 不限交通方式
+                    'delivery_type' => 1,               // 1: 帮我送,2: 帮我取
+                    'expect_start_time' => null,        // 期望送达时间起始(毫秒级时间戳)
+                    'expect_end_time' => null,          // 期望送达时间终止(毫秒级时间戳)
+                ];
             }
+
             try {
                 $quote = $adapter->getPrice($orderData, $accessToken);
+
+
+                if ($name == 'huolala') {
+                    $quote['city_id'] = $cityId;
+                    $quote['city_info_revision'] = $cityInfoRevision;
+                    $quote['vehicle_type'] = $vehicleType;
+                }
+
                 if ($quote) {
                     $results[] = $quote;
                 }
@@ -155,7 +269,7 @@ class DispatchService
         // 返回所有报价供前端展示
         return [
             'quotes' => $results,
-            'best' => $results[0],
+            //'best' => $results[0],
         ];
     }
 

+ 330 - 2
common/components/delivery/services/adapter/Dada.php

@@ -1,21 +1,349 @@
 <?php
 namespace common\components\delivery\services\adapter;
 
+use common\components\delivery\helpers\HttpClient;
+use common\components\delivery\helpers\SignHelper;
+use Yii;
 
-class Dada
+/**
+ * 达达(DaDa)配送平台适配器
+ * 
+ * 基于达达开放平台 API 接口规范进行封装
+ * API 文档参考:达达开放平台 - 接口调用协议与签名算法
+ */
+class Dada implements Adapter
 {
-    public function cityList()
+    // API 配置常量
+    const API_VERSION = '1.0';
+    const FORMAT = 'json';
+    const RESPONSE_TYPE = 'code';
+    
+    // 测试环境和生产环境基础 URL
+    const TEST_BASE_URL = 'https://newopen.qa.imdada.cn';
+    const PROD_BASE_URL = 'https://newopen.imdada.cn';
+
+    protected $baseUrl;
+    protected $appKey;
+    protected $appSecret;
+    protected $sourceId;
+    protected $isSandbox;
+
+    /**
+     * 初始化达达适配器
+     * 根据运行环境加载对应的配置信息
+     */
+    public function __construct()
     {
+        $isProduction = getenv('YII_ENV') == 'production';
+        
+        if ($isProduction) {
+            // 生产环境配置(需要替换为实际的生产环境凭证)
+            $this->baseUrl = self::PROD_BASE_URL;
+            $this->appKey = 'dadaf2279d901a5ba40';
+            $this->appSecret = '828a03677a1c00a3a5b3c59209c4433d';
+            $this->sourceId = '499021274';
+        } else {
+            // 测试环境配置(需要替换为实际的测试环境凭证)
+            $this->baseUrl = self::TEST_BASE_URL;
+            $this->appKey = 'dadaf2279d901a5ba40';
+            $this->appSecret = '828a03677a1c00a3a5b3c59209c4433d';
+            $this->sourceId = '499021274';
+        }
+        
+        $this->isSandbox = !$isProduction;
+    }
 
+    /**
+     * 获取已开通城市列表
+     * 
+     * 通过此接口可获取到达达配送业务所有已开通的城市列表信息
+     * 
+     * @return array 包含城市列表的响应数据
+     *   示例结构:
+     *   [
+     *       'status' => 'success',
+     *       'code' => 0,
+     *       'msg' => '成功',
+     *       'result' => [
+     *           'cityList' => [
+     *               [
+     *                   'cityCode' => '110101',
+     *                   'cityName' => '东城区',
+     *                   'provinceName' => '北京市',
+     *               ],
+     *               // ... 更多城市
+     *           ]
+     *       ]
+     *   ]
+     */
+    public function cityList()
+    {
+        // 业务参数为空时使用空字符串
+        $body = '';
+        
+        // 构建完整的 API 请求参数
+        $payload = $this->buildRequestPayload('cityList', $body);
+        
+        // 发送 POST 请求
+        $resp = HttpClient::post("{$this->baseUrl}/cityList", $payload);
+        
+        return $resp;
     }
 
+    /**
+     * 创建订单(指派订单给配送员)
+     * 
+     * 根据达达官方文档实现下单功能
+     * 
+     * @param array $data 订单数据,结构参考:
+     *   [
+     *       'order_id' => '订单编号' (必需),           // 商户系统内部订单编号
+     *       'shop_no' => '门店编号' (必需),           // 门店编号
+     *       'transporter_id' => 配送员ID (必需),      // 指定配送员ID(可选,为空则由系统自动分配)
+     *       'receiver_name' => '收货人名称' (可选),
+     *       'receiver_address' => '收货地址' (可选),
+     *       'receiver_phone' => '收货人电话' (可选),
+     *       'receiver_lng' => 经度 (可选),
+     *       'receiver_lat' => 纬度 (可选),
+     *       'cargo_price' => 货物价格(分) (可选),
+     *       'tips' => 小费(分) (可选),
+     *       'distance' => 距离(米) (可选),
+     *       'prepare_time' => 商品准备时间 (可选),    // 单位:秒
+     *       'remark' => '备注' (可选),
+     *       'callback' => '回调地址' (可选),
+     *   ]
+     * @return array API 响应结果
+     */
     public function createOrder($data)
     {
+        // 验证必需字段
+        if (empty($data['order_id']) || empty($data['shop_no'])) {
+            return [
+                'code' => -1,
+                'msg' => '缺少必需参数: order_id 和 shop_no'
+            ];
+        }
+
+        // 构建业务参数(按达达 API 规范)
+        $apiData = [
+            'order_id' => $data['order_id'],
+            'shop_no' => $data['shop_no'],
+        ];
+
+        // 如果指定了配送员ID,添加到请求
+        if (isset($data['transporter_id'])) {
+            $apiData['transporter_id'] = (int)$data['transporter_id'];
+        }
+
+        // 可选参数
+        if (!empty($data['receiver_name'])) {
+            $apiData['receiver_name'] = $data['receiver_name'];
+        }
+        if (!empty($data['receiver_address'])) {
+            $apiData['receiver_address'] = $data['receiver_address'];
+        }
+        if (!empty($data['receiver_phone'])) {
+            $apiData['receiver_phone'] = $data['receiver_phone'];
+        }
+        if (isset($data['receiver_lng']) && $data['receiver_lng'] !== '') {
+            $apiData['receiver_lng'] = (double)$data['receiver_lng'];
+        }
+        if (isset($data['receiver_lat']) && $data['receiver_lat'] !== '') {
+            $apiData['receiver_lat'] = (double)$data['receiver_lat'];
+        }
+        if (isset($data['cargo_price'])) {
+            $apiData['cargo_price'] = (int)$data['cargo_price'];
+        }
+        if (isset($data['tips'])) {
+            $apiData['tips'] = (int)$data['tips'];
+        }
+        if (isset($data['distance'])) {
+            $apiData['distance'] = (int)$data['distance'];
+        }
+        if (isset($data['prepare_time'])) {
+            $apiData['prepare_time'] = (int)$data['prepare_time'];
+        }
+        if (!empty($data['remark'])) {
+            $apiData['remark'] = $data['remark'];
+        }
+        if (!empty($data['callback'])) {
+            $apiData['callback'] = $data['callback'];
+        }
 
+        // 将业务参数转换为 JSON 字符串
+        $body = json_encode($apiData, JSON_UNESCAPED_UNICODE);
+
+        // 构建完整的 API 请求参数
+        $payload = $this->buildRequestPayload('orderAddRequest', $body);
+
+        // 发送 POST 请求
+        $resp = HttpClient::post("{$this->baseUrl}/orderAddRequest", $payload);
+
+        return $resp;
     }
 
+    /**
+     * 获取订单运费报价
+     * 
+     * 根据达达官方文档实现订单运费估价功能
+     * 
+     * @param array $data 运费估价参数,结构参考:
+     *   [
+     *       'shop_no' => '门店编号' (必需),
+     *       'origin_id' => '原始订单ID' (必需),       // 商户侧订单编号
+     *       'receiver_address' => '收货地址' (必需),
+     *       'receiver_phone' => '收货人电话' (必需),
+     *       'receiver_lng' => 经度 (必需),
+     *       'receiver_lat' => 纬度 (必需),
+     *       'cargo_price' => 货物价格(分) (可选),    // 商品价格,单位为分
+     *       'distance' => 距离(米) (可选),
+     *       'prepare_time' => 商品准备时间 (可选),   // 单位:秒
+     *   ]
+     * @return array|null 运费报价结果
+     */
     public function getPrice($data)
     {
+        // 验证必需字段
+        if (empty($data['shop_no']) || empty($data['origin_id']) || 
+            empty($data['receiver_address']) || empty($data['receiver_phone']) ||
+            !isset($data['receiver_lng']) || !isset($data['receiver_lat'])) {
+            return [
+                'code' => -1,
+                'msg' => '缺少必需参数'
+            ];
+        }
+
+        // 构建业务参数(按达达 API 规范)
+        $apiData = [
+            'shop_no' => $data['shop_no'],
+            'origin_id' => $data['origin_id'],
+            'receiver_address' => $data['receiver_address'],
+            'receiver_phone' => $data['receiver_phone'],
+            'receiver_lng' => (double)$data['receiver_lng'],
+            'receiver_lat' => (double)$data['receiver_lat'],
+        ];
+
+        // 可选参数
+        if (isset($data['cargo_price'])) {
+            $apiData['cargo_price'] = (int)$data['cargo_price'];
+        }
+        if (isset($data['distance'])) {
+            $apiData['distance'] = (int)$data['distance'];
+        }
+        if (isset($data['prepare_time'])) {
+            $apiData['prepare_time'] = (int)$data['prepare_time'];
+        }
+
+        // 将业务参数转换为 JSON 字符串
+        $body = json_encode($apiData, JSON_UNESCAPED_UNICODE);
+
+        // 构建完整的 API 请求参数
+        $payload = $this->buildRequestPayload('queryDeliverFee', $body);
+
+        // 发送 POST 请求
+        $resp = HttpClient::post("{$this->baseUrl}/queryDeliverFee", $payload);
+
+        // 处理响应
+        if (isset($resp['status']) && $resp['status'] === 'success' && $resp['code'] == 0) {
+            $result = $resp['result'] ?? [];
+            return [
+                'code' => 0,
+                'platform' => 'dada',
+                'deliver_fee' => $result['deliver_fee'] ?? 0,           // 配送费(分)
+                'distance' => $result['distance'] ?? 0,                 // 距离(米)
+                'duration' => $result['duration'] ?? 0,                 // 配送时间(分钟)
+                'tips' => $result['tips'] ?? 0,                         // 小费(分)
+            ];
+        }
+
+        return [
+            'code' => $resp['code'] ?? -1,
+            'msg' => $resp['msg'] ?? 'Unknown error',
+            'data' => null
+        ];
+    }
+
+    /**
+     * 构建达达 API 请求参数
+     * 
+     * 根据达达 API 规范构建完整的请求参数,包括签名
+     * 参数说明详见接口协议文档
+     * 
+     * @param string $apiMethod 接口方法名(不包括基础 URL)
+     * @param string $body 业务参数 JSON 字符串
+     * @return array 完整的 POST 请求参数
+     */
+    protected function buildRequestPayload($apiMethod, $body = '')
+    {
+        // 获取当前时间戳(单位:秒)
+        $timestamp = (string)time();
 
+        // 构建待签名的参数(不包括 signature)
+        $params = [
+            'app_key' => $this->appKey,
+            'v' => self::API_VERSION,
+            'format' => self::FORMAT,
+            'source_id' => $this->sourceId,
+            'timestamp' => $timestamp,
+            'body' => $body,
+        ];
+
+        // 计算签名(按达达 API 规范)
+        $signature = $this->generateSignature($params);
+
+        // 加入签名到请求参数
+        $params['signature'] = $signature;
+
+        return $params;
+    }
+
+    /**
+     * 生成达达 API 签名
+     * 
+     * 达达签名算法:
+     * 1. 将所有参数(除 signature 外)按 key 升序排列
+     * 2. 拼接为 key1value1key2value2...keyNvalueN 的形式
+     * 3. 在字符串后面加上 app_secret
+     * 4. 对结果进行 MD5 加密,得到 32 位大写字符串
+     * 
+     * @param array $params 待签名参数(不包括 signature)
+     * @return string 签名值(32 位大写)
+     */
+    protected function generateSignature($params)
+    {
+        // 按 key 升序排列参数
+        ksort($params);
+
+        // 拼接参数字符串
+        $str = '';
+        foreach ($params as $key => $value) {
+            // 跳过空值
+            if ($value === '' || $value === null) {
+                continue;
+            }
+            $str .= $key . $value;
+        }
+
+        // 加上 app_secret
+        $str .= $this->appSecret;
+
+        // MD5 加密,返回 32 位大写字符串
+        return strtoupper(md5($str));
+    }
+
+    /**
+     * 生成随机数字符串(备用方法,如需要使用)
+     * 
+     * @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;
     }
 }

+ 18 - 12
common/components/delivery/services/adapter/FengniaoAdapter.php

@@ -541,7 +541,6 @@ class FengniaoAdapter implements Adapter
 
         // 构建请求参数
         $payload = $this->buildRequestPayload('preCreateOrder', $businessData);
-
         // 发送请求
         $url = $this->baseUrl . '/preCreateOrder';
         $resp = HttpClient::post($url, $payload, [
@@ -1487,28 +1486,35 @@ class FengniaoAdapter implements Adapter
      * @param array $params 待签名参数(不包括signature)
      * @return string 签名值
      */
-    protected function generateSignature($params)
+    protected function generateSignature(array $params)
     {
-        // 过滤空值
-        $params = array_filter($params, function ($v) {
-            return $v !== null && $v !== '';
-        });
+        // Step 1: 过滤空值  --  不能清除,不然测试环境 code 就被排除了
+//        $params = array_filter($params, function ($v) {
+//            return $v !== null && $v !== '';
+//        });
 
-        // 按字典序排序
+        // Step 2: 按字典序排序
         ksort($params);
 
-        // 拼接成字符串
+        // Step 3: 拼接成字符串
         $paramStr = '';
+        $first = true;
         foreach ($params as $key => $value) {
-            $paramStr .= "{$key}{$value}";
+            if ($first) {
+                $paramStr = "{$key}={$value}";
+                $first = false;
+            } else {
+                $paramStr .= "&{$key}={$value}";
+            }
         }
 
-        // 前面加上 appSecret,然后使用 SHA-256 加密
+        // Step 4: 拼接 appSecret
         $signBefore = $this->appSecret . $paramStr;
+        //$signBefore = '1111app_id=222&code=4444&grant_type=authorization_code&merchant_id=333&timestamp=1719297100558';
+        // Step 5: 使用SHA-256加密
         $signature = hash('sha256', $signBefore);
 
-        Yii::info("[FengniaoAdapter] Sign Before: {$signBefore}, Signature: {$signature}");
-
+        Yii::info("[FengniaAuth] Sign Before: {$signBefore}, Signature: {$signature}");
         return $signature;
     }
 }

+ 309 - 249
common/components/delivery/services/adapter/FengniaoAdapterTest.php

@@ -7,7 +7,13 @@ use common\components\delivery\services\adapter\FengniaoAdapter;
 /**
  * 蜂鸟适配器单元测试
  * 
- * 测试覆盖所有公开方法的业务逻辑、参数验证和响应处理
+ * 支持两种测试模式:
+ * 1. 模拟模式 - 使用虚假数据进行快速单元测试
+ * 2. 真实模式 - 向蜂鸟沙箱/正式环境发送真实请求
+ * 
+ * 使用方式:
+ * - 单元测试(模拟模式):phpunit common/tests/unit/components/delivery/services/adapter/FengniaoAdapterTest.php
+ * - 集成测试(真实模式):phpunit common/tests/unit/components/delivery/services/adapter/FengniaoAdapterTest.php --use-real-api=true
  */
 class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
 {
@@ -17,15 +23,47 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
     private $adapter;
 
     /**
-     * @var \PHPUnit\Framework\MockObject\MockObject
+     * 是否使用真实API
+     */
+    private $useRealApi = false;
+
+    /**
+     * 测试用商户ID
+     */
+    private const TEST_MERCHANT_ID = '204653075';
+
+    /**
+     * 测试用访问令牌(沙箱环境)
      */
-    private $httpClientMock;
+    private const TEST_ACCESS_TOKEN = '';  // 需要配置真实的访问令牌
 
     protected function setUp(): void
     {
+        // 从命令行参数或环境变量检查是否使用真实API
+        $this->useRealApi = getenv('USE_REAL_API') === 'true' || 
+                           (isset($_SERVER['USE_REAL_API']) && $_SERVER['USE_REAL_API'] === 'true');
+
         // 初始化适配器
-        $this->adapter = new FengniaoAdapter('test-access-token');
-        $this->adapter->setMerchantId('test-merchant-id');
+        $this->adapter = new FengniaoAdapter(self::TEST_ACCESS_TOKEN);
+        $this->adapter->setMerchantId(self::TEST_MERCHANT_ID);
+    }
+
+    /**
+     * 发送真实请求或返回模拟响应
+     * 
+     * @param callable $realApiCall 真实API调用的回调函数
+     * @param array $mockResponse 模拟响应数据
+     * @return array 响应数据
+     */
+    private function executeApiCall(callable $realApiCall, array $mockResponse = null)
+    {
+        if ($this->useRealApi) {
+            // 真实API调用
+            return $realApiCall();
+        } else {
+            // 模拟模式:返回预定义的响应
+            return $mockResponse ?? [];
+        }
     }
 
     /**
@@ -33,12 +71,11 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
      */
 
     /**
-     * 测试城市列表查询成功
+     * 测试城市列表查询
      * @test
      */
-    public function testCityListSuccess()
+    public function testCityList()
     {
-        // 模拟蜂鸟API的成功响应
         $mockResponse = [
             'code' => '200',
             'msg' => 'success',
@@ -52,51 +89,24 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
             'sign' => 'mock-signature'
         ];
 
-        $this->assertEquals('200', $mockResponse['code']);
-        $this->assertEquals('success', $mockResponse['msg']);
-    }
-
-    /**
-     * ============= createOrder() 方法测试 =============
-     */
-
-    /**
-     * 测试创建订单成功
-     * @test
-     */
-    public function testCreateOrderSuccess()
-    {
-        $orderData = $this->getValidCreateOrderData();
-        
-        // 模拟蜂鸟API的成功响应
-        $mockResponse = [
-            'code' => '200',
-            'msg' => 'success',
-            'business_data' => json_encode([
-                'order_id' => '300000211323129144'
-            ]),
-            'sign' => 'mock-signature'
-        ];
-
-        $this->assertEquals('200', $mockResponse['code']);
-        $respData = json_decode($mockResponse['business_data'], true);
-        $this->assertNotEmpty($respData['order_id']);
-    }
-
-    /**
-     * 测试创建订单参数缺失
-     * @test
-     */
-    public function testCreateOrderMissingParameters()
-    {
-        // 缺少必填参数的订单数据
-        $incompleteData = [
-            'partner_order_code' => 'TEST_ORDER_001',
-            // 缺少其他必填参数
-        ];
-
-        // 验证参数检查
-        $this->assertEmpty($incompleteData['receiver_name'] ?? null);
+        $result = $this->executeApiCall(
+            function() { return $this->adapter->cityList(); },
+            $mockResponse
+        );
+
+        if ($this->useRealApi) {
+            // 真实API返回结果验证
+            $this->assertIsArray($result);
+            if (isset($result['code'])) {
+                $this->assertNotEmpty($result['code']);
+            }
+        } else {
+            // 模拟响应验证
+            $this->assertEquals('200', $result['code']);
+            $this->assertEquals('success', $result['msg']);
+            $respData = json_decode($result['business_data'], true);
+            $this->assertNotEmpty($respData['cities']);
+        }
     }
 
     /**
@@ -104,14 +114,13 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
      */
 
     /**
-     * 测试获取运费报价成功
+     * 测试获取运费报价
      * @test
      */
-    public function testGetPriceSuccess()
+    public function testGetPrice()
     {
         $priceData = $this->getValidGetPriceData();
         
-        // 模拟蜂鸟API的成功响应
         $mockResponse = [
             'code' => '200',
             'msg' => 'success',
@@ -137,9 +146,70 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
             'sign' => 'mock-signature'
         ];
 
-        $this->assertEquals('200', $mockResponse['code']);
-        $respData = json_decode($mockResponse['business_data'], true);
-        $this->assertNotEmpty($respData['goods_infos']);
+        $result = $this->executeApiCall(
+            function() use ($priceData) { return $this->adapter->getPrice($priceData); },
+            $mockResponse
+        );
+
+        if ($this->useRealApi) {
+            // 真实API返回结果验证
+            $this->assertIsArray($result);
+            echo "\n[真实API响应] getPrice: " . json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+            if (isset($result['code']) && $result['code'] == '200') {
+                $respData = json_decode($result['business_data'], true);
+                $this->assertIsArray($respData);
+                if (isset($respData['goods_infos'])) {
+                    $this->assertIsArray($respData['goods_infos']);
+                }
+            }
+        } else {
+            // 模拟响应验证
+            $this->assertEquals('200', $result['code']);
+            $respData = json_decode($result['business_data'], true);
+            $this->assertNotEmpty($respData['goods_infos']);
+        }
+    }
+
+    /**
+     * ============= createOrder() 方法测试 =============
+     */
+
+    /**
+     * 测试创建订单
+     * @test
+     */
+    public function testCreateOrder()
+    {
+        $orderData = $this->getValidCreateOrderData();
+        
+        $mockResponse = [
+            'code' => '200',
+            'msg' => 'success',
+            'business_data' => json_encode([
+                'order_id' => '300000211323129144'
+            ]),
+            'sign' => 'mock-signature'
+        ];
+
+        $result = $this->executeApiCall(
+            function() use ($orderData) { return $this->adapter->createOrder($orderData); },
+            $mockResponse
+        );
+
+        if ($this->useRealApi) {
+            // 真实API返回结果验证
+            $this->assertIsArray($result);
+            echo "\n[真实API响应] createOrder: " . json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+            if (isset($result['code']) && $result['code'] == '200') {
+                $respData = json_decode($result['business_data'], true);
+                $this->assertNotEmpty($respData['order_id'] ?? null);
+            }
+        } else {
+            // 模拟响应验证
+            $this->assertEquals('200', $result['code']);
+            $respData = json_decode($result['business_data'], true);
+            $this->assertNotEmpty($respData['order_id']);
+        }
     }
 
     /**
@@ -147,10 +217,10 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
      */
 
     /**
-     * 测试获取取消原因列表成功
+     * 测试获取取消原因列表
      * @test
      */
-    public function testGetCancelReasonListSuccess()
+    public function testGetCancelReasonList()
     {
         $mockResponse = [
             'code' => '200',
@@ -166,22 +236,34 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
             'sign' => 'mock-signature'
         ];
 
-        $this->assertEquals('200', $mockResponse['code']);
-        $respData = json_decode($mockResponse['business_data'], true);
-        $this->assertCount(4, $respData['cancel_reason_list']);
-    }
+        // 使用真实订单ID或合作订单号
+        $queryData = [
+            // 二选一:order_id 或 partner_order_code
+            // 'order_id' => '300000219758073736',
+            'partner_order_code' => 'TEST_ORDER_' . time(),
+        ];
 
-    /**
-     * 测试获取取消原因列表 - 参数验证
-     * @test
-     */
-    public function testGetCancelReasonListParameterValidation()
-    {
-        // 订单标识必填一个
-        $invalidData = [];
-        
-        $this->assertEmpty($invalidData['order_id'] ?? null);
-        $this->assertEmpty($invalidData['partner_order_code'] ?? null);
+        $result = $this->executeApiCall(
+            function() use ($queryData) { return $this->adapter->getCancelReasonList($queryData); },
+            $mockResponse
+        );
+
+        if ($this->useRealApi) {
+            // 真实API返回结果验证
+            $this->assertIsArray($result);
+            echo "\n[真实API响应] getCancelReasonList: " . json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+            if (isset($result['code']) && $result['code'] == '200') {
+                $respData = json_decode($result['business_data'], true);
+                if (isset($respData['cancel_reason_list'])) {
+                    $this->assertIsArray($respData['cancel_reason_list']);
+                }
+            }
+        } else {
+            // 模拟响应验证
+            $this->assertEquals('200', $result['code']);
+            $respData = json_decode($result['business_data'], true);
+            $this->assertCount(4, $respData['cancel_reason_list']);
+        }
     }
 
     /**
@@ -189,10 +271,10 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
      */
 
     /**
-     * 测试订单取消成功
+     * 测试订单取消
      * @test
      */
-    public function testCancelOrderSuccess()
+    public function testCancelOrder()
     {
         $cancelData = $this->getValidCancelOrderData();
         
@@ -206,41 +288,27 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
             'sign' => 'mock-signature'
         ];
 
-        $this->assertEquals('200', $mockResponse['code']);
-        $respData = json_decode($mockResponse['business_data'], true);
-        $this->assertTrue($respData['result']);
-    }
-
-    /**
-     * 测试订单取消 - 商户取消
-     * @test
-     */
-    public function testCancelOrderByMerchant()
-    {
-        $cancelData = [
-            'order_id' => '300000219758073736',
-            'order_cancel_role' => 1,  // 商户取消
-            'order_cancel_code' => 32,
-            'order_cancel_other_reason' => '库存不足',
-        ];
-
-        $this->assertEquals(1, $cancelData['order_cancel_role']);
-    }
-
-    /**
-     * 测试订单取消 - 用户取消
-     * @test
-     */
-    public function testCancelOrderByUser()
-    {
-        $cancelData = [
-            'order_id' => '300000219758073736',
-            'order_cancel_role' => 2,  // 用户取消
-            'order_cancel_code' => 9,
-            'order_cancel_other_reason' => '临时不想要了',
-        ];
-
-        $this->assertEquals(2, $cancelData['order_cancel_role']);
+        $result = $this->executeApiCall(
+            function() use ($cancelData) { return $this->adapter->cancelOrder($cancelData); },
+            $mockResponse
+        );
+
+        if ($this->useRealApi) {
+            // 真实API返回结果验证
+            $this->assertIsArray($result);
+            echo "\n[真实API响应] cancelOrder: " . json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+            if (isset($result['code']) && $result['code'] == '200') {
+                $respData = json_decode($result['business_data'], true);
+                if (isset($respData['result'])) {
+                    $this->assertIsBool($respData['result']);
+                }
+            }
+        } else {
+            // 模拟响应验证
+            $this->assertEquals('200', $result['code']);
+            $respData = json_decode($result['business_data'], true);
+            $this->assertTrue($respData['result']);
+        }
     }
 
     /**
@@ -248,10 +316,10 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
      */
 
     /**
-     * 测试预取消订单成功
+     * 测试预取消订单
      * @test
      */
-    public function testPreCancelOrderSuccess()
+    public function testPreCancelOrder()
     {
         $mockResponse = [
             'code' => '200',
@@ -262,9 +330,27 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
             'sign' => 'mock-signature'
         ];
 
-        $this->assertEquals('200', $mockResponse['code']);
-        $respData = json_decode($mockResponse['business_data'], true);
-        $this->assertEquals(0, $respData['actual_cancel_cost_cent']);
+        $queryData = [
+            'order_id' => '300000219758073736',
+            'order_cancel_role' => 1,
+            'order_cancel_code' => 32,
+        ];
+
+        $result = $this->executeApiCall(
+            function() use ($queryData) { return $this->adapter->preCancelOrder($queryData); },
+            $mockResponse
+        );
+
+        if ($this->useRealApi) {
+            // 真实API返回结果验证
+            $this->assertIsArray($result);
+            echo "\n[真实API响应] preCancelOrder: " . json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+        } else {
+            // 模拟响应验证
+            $this->assertEquals('200', $result['code']);
+            $respData = json_decode($result['business_data'], true);
+            $this->assertEquals(0, $respData['actual_cancel_cost_cent']);
+        }
     }
 
     /**
@@ -272,10 +358,10 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
      */
 
     /**
-     * 测试添加小费成功
+     * 测试添加小费
      * @test
      */
-    public function testAddTipSuccess()
+    public function testAddTip()
     {
         $tipData = [
             'order_id' => '300000215893560503',
@@ -292,31 +378,21 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
             'sign' => 'mock-signature'
         ];
 
-        $this->assertEquals('200', $mockResponse['code']);
-        $respData = json_decode($mockResponse['business_data'], true);
-        $this->assertTrue($respData['result']);
-    }
-
-    /**
-     * 测试添加小费 - 幂等性
-     * @test
-     */
-    public function testAddTipIdempotency()
-    {
-        // 使用相同的 third_index_id 进行重复请求
-        $tipData1 = [
-            'order_id' => '300000215893560503',
-            'third_index_id' => 1,
-            'add_tip_amount_cent' => 200,
-        ];
-
-        $tipData2 = [
-            'order_id' => '300000215893560503',
-            'third_index_id' => 1,  // 相同的 ID
-            'add_tip_amount_cent' => 200,
-        ];
-
-        $this->assertEquals($tipData1['third_index_id'], $tipData2['third_index_id']);
+        $result = $this->executeApiCall(
+            function() use ($tipData) { return $this->adapter->addTip($tipData); },
+            $mockResponse
+        );
+
+        if ($this->useRealApi) {
+            // 真实API返回结果验证
+            $this->assertIsArray($result);
+            echo "\n[真实API响应] addTip: " . json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+        } else {
+            // 模拟响应验证
+            $this->assertEquals('200', $result['code']);
+            $respData = json_decode($result['business_data'], true);
+            $this->assertTrue($respData['result']);
+        }
     }
 
     /**
@@ -324,10 +400,10 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
      */
 
     /**
-     * 测试获取订单详情成功
+     * 测试获取订单详情
      * @test
      */
-    public function testGetOrderDetailSuccess()
+    public function testGetOrderDetail()
     {
         $mockResponse = [
             'code' => '200',
@@ -344,39 +420,35 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
                 'order_actual_amount_cent' => 950,
                 'order_tip_amount_cent' => 200,
                 'order_distance' => 5000,
-                'price_detail' => [
-                    'start_price_cent' => 500,
-                    'distance_price_cent' => 450,
-                ]
             ]),
             'sign' => 'mock-signature'
         ];
 
-        $this->assertEquals('200', $mockResponse['code']);
-        $respData = json_decode($mockResponse['business_data'], true);
-        $this->assertEquals(2, $respData['order_status']);
-        $this->assertNotEmpty($respData['carrier_driver_name']);
-    }
-
-    /**
-     * 测试获取订单详情 - 查询不同订单状态
-     * @test
-     */
-    public function testGetOrderDetailMultipleStatus()
-    {
-        $statusList = [
-            0 => '订单生成',
-            1 => '运单生成成功',
-            20 => '骑手接单',
-            80 => '骑手到店',
-            2 => '配送中',
-            3 => '已完成',
-            4 => '已取消',
-            5 => '配送异常',
+        $queryData = [
+            'order_id' => '300000211323129144',
         ];
 
-        foreach ($statusList as $status => $desc) {
-            $this->assertNotEmpty($desc);
+        $result = $this->executeApiCall(
+            function() use ($queryData) { return $this->adapter->getOrderDetail($queryData); },
+            $mockResponse
+        );
+
+        if ($this->useRealApi) {
+            // 真实API返回结果验证
+            $this->assertIsArray($result);
+            echo "\n[真实API响应] getOrderDetail: " . json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+            if (isset($result['code']) && $result['code'] == '200') {
+                $respData = json_decode($result['business_data'], true);
+                $this->assertIsArray($respData);
+                if (isset($respData['order_status'])) {
+                    $this->assertIsInt((int)$respData['order_status']);
+                }
+            }
+        } else {
+            // 模拟响应验证
+            $this->assertEquals('200', $result['code']);
+            $respData = json_decode($result['business_data'], true);
+            $this->assertEquals(2, $respData['order_status']);
         }
     }
 
@@ -385,10 +457,10 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
      */
 
     /**
-     * 测试获取骑手信息成功
+     * 测试获取骑手信息
      * @test
      */
-    public function testGetKnightInfoSuccess()
+    public function testGetKnightInfo()
     {
         $mockResponse = [
             'code' => '200',
@@ -403,36 +475,33 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
             'sign' => 'mock-signature'
         ];
 
-        $this->assertEquals('200', $mockResponse['code']);
-        $respData = json_decode($mockResponse['business_data'], true);
-        $this->assertNotEmpty($respData['carrier_driver_name']);
-        $this->assertNotEmpty($respData['carrier_driver_latitude']);
-    }
-
-    /**
-     * 测试获取骑手信息 - 返回高德坐标系
-     * @test
-     */
-    public function testGetKnightInfoAMapCoordinate()
-    {
-        $mockResponse = [
-            'code' => '200',
-            'msg' => 'success',
-            'business_data' => json_encode([
-                'carrier_driver_latitude' => '39.9042',   // 高德纬度
-                'carrier_driver_longitude' => '116.4074', // 高德经度
-            ]),
+        $queryData = [
+            'order_id' => '300000215893560503',
         ];
 
-        $respData = json_decode($mockResponse['business_data'], true);
-        // 验证坐标格式(高德地图坐标范围)
-        $lat = (float)$respData['carrier_driver_latitude'];
-        $lon = (float)$respData['carrier_driver_longitude'];
-        
-        $this->assertGreaterThanOrEqual(-90, $lat);
-        $this->assertLessThanOrEqual(90, $lat);
-        $this->assertGreaterThanOrEqual(-180, $lon);
-        $this->assertLessThanOrEqual(180, $lon);
+        $result = $this->executeApiCall(
+            function() use ($queryData) { return $this->adapter->getKnightInfo($queryData); },
+            $mockResponse
+        );
+
+        if ($this->useRealApi) {
+            // 真实API返回结果验证
+            $this->assertIsArray($result);
+            echo "\n[真实API响应] getKnightInfo: " . json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+            if (isset($result['code']) && $result['code'] == '200') {
+                $respData = json_decode($result['business_data'], true);
+                if (isset($respData['carrier_driver_latitude'])) {
+                    $lat = (float)$respData['carrier_driver_latitude'];
+                    $this->assertGreaterThanOrEqual(-90, $lat);
+                    $this->assertLessThanOrEqual(90, $lat);
+                }
+            }
+        } else {
+            // 模拟响应验证
+            $this->assertEquals('200', $result['code']);
+            $respData = json_decode($result['business_data'], true);
+            $this->assertNotEmpty($respData['carrier_driver_name']);
+        }
     }
 
     /**
@@ -445,7 +514,7 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
     private function getValidCreateOrderData(): array
     {
         return [
-            'partner_order_code' => 'TEST_ORDER_' . time(),
+            'partner_order_code' => 'TEST_ORDER_' . time() . '_' . uniqid(),
             'receiver_primary_phone' => '13800000000',
             'receiver_name' => '张三',
             'receiver_latitude' => 39.9042,
@@ -475,7 +544,7 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
                 ],
             ],
             'order_type' => 1,
-            'chain_store_id' => 204653075,
+            'chain_store_id' => self::TEST_MERCHANT_ID,
             'order_remark' => '请轻拿轻放',
         ];
     }
@@ -486,8 +555,8 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
     private function getValidGetPriceData(): array
     {
         return [
-            'partner_order_code' => 'TEST_PRICE_' . time(),
-            'chain_store_id' => 204653075,
+            'partner_order_code' => 'TEST_PRICE_' . time() . '_' . uniqid(),
+            'chain_store_id' => self::TEST_MERCHANT_ID,
             'position_source' => 3,
             'receiver_address' => '北京市朝阳区建国门外大街1号',
             'receiver_latitude' => 39.9042,
@@ -529,55 +598,46 @@ class FengniaoAdapterTest extends \PHPUnit\Framework\TestCase
      * 测试API错误响应处理
      * @test
      */
-    public function testApiErrorResponse()
+    public function testApiErrorResponseHandling()
     {
-        $errorResponse = [
-            'code' => '400',
-            'msg' => 'Invalid parameters',
-            'business_data' => null,
-        ];
-
-        $this->assertNotEquals('200', $errorResponse['code']);
-        $this->assertNull($errorResponse['business_data']);
-    }
-
-    /**
-     * 测试业务数据JSON解析失败
-     * @test
-     */
-    public function testBusinessDataJsonParseFail()
-    {
-        $invalidResponse = [
-            'code' => '200',
-            'msg' => 'success',
-            'business_data' => 'invalid json data',
-        ];
-
-        $respData = json_decode($invalidResponse['business_data'], true);
-        $this->assertNull($respData);  // 解析失败返回 null
+        if ($this->useRealApi) {
+            // 真实模式:发送错误的参数
+            $invalidData = [
+                'partner_order_code' => '',  // 空的订单号
+                'receiver_name' => '',       // 空的收货人名称
+            ];
+            
+            $result = $this->adapter->createOrder($invalidData);
+            
+            // 应该返回错误响应
+            $this->assertIsArray($result);
+            echo "\n[真实API响应] 错误处理测试: " . json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+        } else {
+            // 模拟模式:简单的响应验证
+            $errorResponse = [
+                'code' => '400',
+                'msg' => 'Invalid parameters',
+                'business_data' => null,
+            ];
+            
+            $this->assertNotEquals('200', $errorResponse['code']);
+            $this->assertNull($errorResponse['business_data']);
+        }
     }
 
     /**
-     * ============= 必填参数验证测试 =============
+     * ============= 集成测试帮助方法 =============
      */
 
     /**
-     * 测试订单标识参数验证(二选一)
-     * @test
+     * 输出测试提示信息
      */
-    public function testOrderIdentifierParameterValidation()
+    protected function tearDown(): void
     {
-        // 只有 order_id
-        $data1 = ['order_id' => '300000219758073736'];
-        $this->assertNotEmpty($data1['order_id']);
-
-        // 只有 partner_order_code
-        $data2 = ['partner_order_code' => 'ORDER_2024_001'];
-        $this->assertNotEmpty($data2['partner_order_code']);
-
-        // 两个都缺失(无效)
-        $data3 = [];
-        $this->assertEmpty($data3['order_id'] ?? null);
-        $this->assertEmpty($data3['partner_order_code'] ?? null);
+        if ($this->useRealApi) {
+            echo "\n========================================";
+            echo "\n✓ 真实API测试已执行";
+            echo "\n========================================\n";
+        }
     }
 }

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

@@ -73,6 +73,162 @@ class HuolalaAdapter implements Adapter
         return $resp;
     }
 
+    /**
+     * 获取城市可选车型信息
+     * 
+     * 通过此接口可获取城市可选车型信息,包括车型详情、计价规则、车型附加要求和额外需求等
+     * 
+     * @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)
+    {
+        // 参数验证
+        if (empty($cityId) || !is_numeric($cityId)) {
+            return null;
+        }
+
+        // 构建请求参数
+        $payload = $this->buildRequestPayload('u-city-info', ['city_id' => (int)$cityId]);
+        
+        // 发送请求
+        $resp = HttpClient::post($this->baseUrl, $payload);
+        
+        // 处理响应
+        if (isset($resp['ret']) && $resp['ret'] == 0 && isset($resp['data'])) {
+            $data = $resp['data'];
+            
+            // 提取车型列表
+            $vehicleList = [];
+            if (!empty($data['vehicle_list']) && is_array($data['vehicle_list'])) {
+                foreach ($data['vehicle_list'] as $vehicle) {
+                    // 处理超里程价格分段
+                    $exceedSegmentPrice = [];
+                    if (!empty($vehicle['price_text_item']['exceed_segment_price']) 
+                        && is_array($vehicle['price_text_item']['exceed_segment_price'])) {
+                        foreach ($vehicle['price_text_item']['exceed_segment_price'] as $segment) {
+                            $exceedSegmentPrice[] = [
+                                'start_exdistancekm' => (int)($segment['start_exdistancekm'] ?? 0),
+                                'end_exdistancekm' => (int)($segment['end_exdistancekm'] ?? 0),
+                                'price_extra_fen' => (int)($segment['price_extra_fen'] ?? 0),
+                            ];
+                        }
+                    }
+                    
+                    // 处理车型附加要求
+                    $vehicleStdItem = [];
+                    if (!empty($vehicle['vehicle_std_item']) && is_array($vehicle['vehicle_std_item'])) {
+                        foreach ($vehicle['vehicle_std_item'] as $std) {
+                            $vehicleStdItem[] = [
+                                'name' => $std['name'] ?? '',
+                                'desc' => $std['desc'] ?? '',
+                                'price_fen' => (int)($std['price_fen'] ?? 0),
+                            ];
+                        }
+                    }
+                    
+                    $vehicleList[] = [
+                        'order_vehicle_id' => (int)($vehicle['order_vehicle_id'] ?? 0),
+                        'vehicle_name' => $vehicle['vehicle_name'] ?? '',
+                        'img_url_high_light' => $vehicle['img_url_high_light'] ?? '',
+                        'vehicle_volume' => $vehicle['vehicle_volume'] ?? '',
+                        'vehicle_weight' => $vehicle['vehicle_weight'] ?? '',
+                        'vehicle_size' => $vehicle['vehicle_size'] ?? '',
+                        'standard_order_vehicle_id' => (int)($vehicle['standard_order_vehicle_id'] ?? 0),
+                        'text_desc' => $vehicle['text_desc'] ?? '',
+                        'vehicle_attr' => (int)($vehicle['vehicle_attr'] ?? 0),
+                        'price_text_item' => [
+                            'base_distancekm' => (int)($vehicle['price_text_item']['base_distancekm'] ?? 0),
+                            'base_price_fen' => (int)($vehicle['price_text_item']['base_price_fen'] ?? 0),
+                            'exceed_segment_price' => $exceedSegmentPrice,
+                        ],
+                        'vehicle_std_item' => $vehicleStdItem,
+                    ];
+                }
+            }
+            
+            // 处理城市额外需求列表
+            $specReqItem = [];
+            if (!empty($data['spec_req_item']) && is_array($data['spec_req_item'])) {
+                foreach ($data['spec_req_item'] as $specReq) {
+                    $specReqItem[] = [
+                        'type' => (int)($specReq['type'] ?? 0),
+                        'name' => $specReq['name'] ?? '',
+                        'desc' => $specReq['desc'] ?? '',
+                        'price_type' => (int)($specReq['price_type'] ?? 0),
+                        'price_value_fen' => (int)($specReq['price_value_fen'] ?? 0),
+                    ];
+                }
+            }
+            
+            return [
+                'city_id' => (int)($data['city_id'] ?? 0),
+                'name' => $data['name'] ?? '',
+                'name_en' => $data['name_en'] ?? '',
+                'city_info_revision' => (int)($data['city_info_revision'] ?? 0),
+                'vehicle_list' => $vehicleList,
+                'spec_req_item' => $specReqItem,
+            ];
+        }
+        
+        return null;
+    }
+
+    public function formatOrderData($order)
+    {
+
+    }
+
     /**
      * 创建订单(需用户授权)
      * 

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

@@ -185,16 +185,17 @@ class ShansongAdapter implements Adapter
             $respData = $resp['data'];
             return [
                 'platform' => 'shansong',
-                'order_number' => $respData['orderNumber'] ?? '',
-                'total_distance' => $respData['totalDistance'] ?? 0,
-                'total_weight' => $respData['totalWeight'] ?? 0,
-                'total_amount' => $respData['totalAmount'] ?? 0,              // 订单总金额(分)
+                'order_number' => $respData['orderNumber'] ?? '',               // 闪送订单号(有效期30分钟)
+                'total_distance' => $respData['totalDistance'] ?? 0,            // 总距离,单位:米
+                'total_weight' => $respData['totalWeight'] ?? 0,                // 总重量,单位:kg
+                'total_amount' => $respData['totalAmount'] ?? 0,               // 订单总金额(分)
                 'coupon_save_fee' => $respData['couponSaveFee'] ?? 0,          // 优惠额度(分)
                 'total_fee_after_save' => $respData['totalFeeAfterSave'] ?? 0, // 实际支付费用(分)
                 'fee_info_list' => $respData['feeInfoList'] ?? [],             // 费用明细
                 'interest_dto_list' => $respData['interestDtoList'] ?? [],     // 增值服务明细
-                'estimate_grab_second' => $respData['estimateGrabSecond'] ?? -1,
-                'estimate_receive_second' => $respData['estimateReceiveSecond'] ?? -1,
+                // 弃用字段
+                'estimate_grab_second' => $respData['estimateGrabSecond'] ?? -1, // 预计接单时长,字段弃用
+                'estimate_receive_second' => $respData['estimateReceiveSecond'] ?? -1, // 预计完单时长,字段弃用
             ];
         }