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

Merge branch 'zhongqi-delivery' of http://git.huaml.com/zhh/huahuibao into zhongqi-delivery

shish 8 месяцев назад
Родитель
Сommit
8a50eebfd5

+ 234 - 0
REFACTOR_SUMMARY.md

@@ -0,0 +1,234 @@
+# 配送报价逻辑重构总结
+
+## 重构日期
+2024-12-04
+
+## 重构目标
+将分散在多个 Controller 中的配送报价逻辑封装到统一的工具类中,提高代码复用性和可维护性。
+
+## 重构内容
+
+### 1. 新增文件
+
+#### ✅ `common/components/delivery/helpers/DeliveryQuoteUtil.php`
+- **位置**:`common/components/delivery/helpers/`
+- **类型**:工具类(Util)
+- **职责**:统一处理跑腿平台报价、免费配送规则等逻辑
+
+**核心方法**:
+```php
+DeliveryQuoteUtil::getDeliveryQuote($params)
+```
+
+**功能特性**:
+- ✅ 自动计算商品总重量
+- ✅ 调用跑腿平台获取报价
+- ✅ 自动重试机制
+- ✅ 应用免费配送规则
+- ✅ 统一的异常处理
+- ✅ 详细的日志记录
+
+#### ✅ `common/components/delivery/helpers/DeliveryQuoteUtil_USAGE.md`
+- **位置**:`common/components/delivery/helpers/`
+- **类型**:使用文档
+- **内容**:详细的使用说明、参数说明、示例代码
+
+### 2. 修改文件
+
+#### ✅ `app-hd/controllers/PurchaseController.php`
+
+**修改位置**:第 573-660 行
+
+**修改前**(84 行代码):
+```php
+// 复杂的内联逻辑:
+// - 计算重量
+// - 构建订单数据
+// - 调用 DispatchService
+// - 格式化报价
+// - 重试逻辑
+// - 免费配送规则判断
+```
+
+**修改后**(30 行代码):
+```php
+// 简洁的工具类调用
+try {
+    $quoteResult = DeliveryQuoteUtil::getDeliveryQuote([
+        'productList' => $productList,
+        'deliveryPlatform' => $post['deliveryPlatform'],
+        'ghsInfo' => $ghsInfo,
+        'custom' => $custom,
+        'order' => [
+            'orderSn' => $orderSn,
+            'itemTotalAmount' => $post['itemTotalAmount'] ?? 0,
+            'remark' => $post['remark'] ?? '',
+        ],
+        'mainId' => $this->mainId,
+        'productCount' => $productCount,
+    ]);
+    
+    $sendCost = $quoteResult['sendCost'];
+    $sendDistance = $quoteResult['sendDistance'];
+} catch (\Exception $e) {
+    util::fail($e->getMessage());
+}
+```
+
+**代码减少**:54 行(减少 64%)
+
+## 重构优势
+
+### 1. 代码复用性 ⬆️
+- 原来:每个需要配送报价的地方都要复制 84 行代码
+- 现在:只需调用一个方法,传入参数即可
+
+### 2. 可维护性 ⬆️
+- 原来:修改逻辑需要在多个文件中同步修改
+- 现在:只需修改 `DeliveryQuoteUtil.php` 一个文件
+
+### 3. 可测试性 ⬆️
+- 原来:逻辑嵌入在 Controller 中,难以单独测试
+- 现在:独立的工具类方法,便于编写单元测试
+
+### 4. 代码可读性 ⬆️
+- 原来:84 行复杂逻辑,需要仔细阅读才能理解
+- 现在:方法名清晰表达意图,参数结构化
+
+### 5. 错误处理 ⬆️
+- 原来:错误处理分散在多处
+- 现在:统一的异常处理机制
+
+## 待迁移位置
+
+以下位置可以使用新的工具类替换现有逻辑:
+
+### 1. ✅ 已完成
+- `app-hd/controllers/PurchaseController.php` 第 577-660 行
+
+### 2. 🔲 待迁移
+- `app-mall/controllers/OrderController.php` 第 374 行附近
+- 其他需要调用配送报价的地方(可通过搜索 `DispatchService` 和 `getAllPlatformPrice` 找到)
+
+## 使用指南
+
+### 快速开始
+
+1. **引入命名空间**
+```php
+use common\components\delivery\helpers\DeliveryQuoteUtil;
+```
+
+2. **调用方法**
+```php
+try {
+    $quoteResult = DeliveryQuoteUtil::getDeliveryQuote([
+        'productList' => $productList,
+        'deliveryPlatform' => 'shansong',
+        'ghsInfo' => ['mainId' => xxx, 'shopId' => xxx],
+        'custom' => $customObject,
+        'order' => ['orderSn' => xxx, 'itemTotalAmount' => xxx, 'remark' => xxx],
+        'mainId' => $this->mainId,
+        'productCount' => $count,
+    ]);
+    
+    $sendCost = $quoteResult['sendCost'];
+    $sendDistance = $quoteResult['sendDistance'];
+} catch (\Exception $e) {
+    util::fail($e->getMessage());
+}
+```
+
+### 详细文档
+参见:`common/components/delivery/helpers/DeliveryQuoteUtil_USAGE.md`
+
+## 技术细节
+
+### 封装的逻辑
+
+1. **重量计算**
+   - 遍历商品列表
+   - 使用 `bcmul` 和 `bcadd` 精确计算
+
+2. **订单数据构建**
+   - 从客户信息对象提取字段
+   - 构建标准化的订单数据结构
+
+3. **平台报价调用**
+   - 初始化 `DispatchService`
+   - 调用 `getAllPlatformPrice` 获取报价
+   - 格式化报价结果
+
+4. **重试机制**
+   - 第一次失败后等待 2 秒
+   - 自动重试一次
+
+5. **免费配送规则**
+   - 读取店铺扩展配置
+   - 判断基础免费距离
+   - 判断条件免费配送规则
+
+6. **异常处理**
+   - 参数验证
+   - 业务逻辑异常捕获
+   - 统一的错误消息
+
+### 设计原则
+
+- **单一职责**:只负责配送报价相关逻辑
+- **开闭原则**:对扩展开放,对修改关闭
+- **依赖倒置**:依赖抽象(接口)而非具体实现
+- **接口隔离**:提供简洁的公共接口
+
+## 测试建议
+
+### 单元测试
+```php
+// 测试正常报价
+testGetDeliveryQuote_Success()
+
+// 测试免费配送规则
+testGetDeliveryQuote_FreeDelivery()
+
+// 测试参数验证
+testGetDeliveryQuote_InvalidParams()
+
+// 测试重试机制
+testGetDeliveryQuote_Retry()
+```
+
+### 集成测试
+- 测试与 `DispatchService` 的集成
+- 测试与 `ShopClass`、`ShopExtClass` 的集成
+- 测试真实平台报价接口调用
+
+## 性能影响
+
+- ✅ 无性能损失:封装不增加额外的性能开销
+- ✅ 代码更简洁:减少代码量,提高执行效率
+- ✅ 重试机制:已有的重试逻辑保持不变
+
+## 向后兼容性
+
+- ✅ 完全兼容:不影响现有功能
+- ✅ 渐进式迁移:可以逐步替换旧代码
+- ✅ 旧代码仍可用:未迁移的代码仍可正常运行
+
+## 下一步计划
+
+1. 迁移 `app-mall/controllers/OrderController.php` 中的配送报价逻辑
+2. 搜索其他使用 `DispatchService::getAllPlatformPrice` 的地方并迁移
+3. 编写单元测试
+4. 添加性能监控和日志分析
+
+## 注意事项
+
+1. 确保传入的参数完整且正确
+2. 使用 `try-catch` 捕获异常
+3. 检查返回结果中的 `sendCost` 和 `sendDistance`
+4. 注意单位:`sendCost` 是元,`sendDistance` 是米
+
+## 联系方式
+
+如有问题或建议,请联系开发团队。
+

+ 22 - 79
app-hd/controllers/PurchaseController.php

@@ -16,6 +16,7 @@ use bizGhs\product\classes\ProductClass;
 use bizHd\wx\classes\WxOpenClass;
 use common\components\dateUtil;
 use common\components\delivery\services\DispatchService;
+use common\components\delivery\util\DeliveryQuoteUtil;
 use common\components\dict;
 use common\components\imgUtil;
 use common\components\noticeUtil;
@@ -575,89 +576,31 @@ class PurchaseController extends BaseController
                 $sendCost = 0;
                 $sendDistance = 0;
                 if ($sendType == dict::getDict('sendType', 'thirdSend')) {
-                    $weight = 0;
-                    foreach ($productList as $itemData) {
-                        $bigNum = $itemData['bigNum'] ?? 0;
-                        $thisWeight = $itemData['weight'] ?? 0;
-                        $currentWeight = bcmul($thisWeight, $bigNum, 2);
-                        $weight = bcadd($currentWeight, $weight, 2);
-                    }
-                    // 使用平台估价接口获取费用与距离 -- 弃用
-                    //$result = PurchaseClass::getDistanceFee($this->shop, $ghsShopInfo, $weight, $custom);
-                    //$sendCost = $result['fee'] ?? 0;
-                    //$sendDistance = $result['distance'] ?? 0;
-
-                    $platform = $post['deliveryPlatform'];
-                    $ghsShopId = $ghsInfo['shopId'];
                     // 生成随机订单号
                     $prefix = 'XSD_CS-' . $this->mainId . '-';
                     $orderSn = $prefix . round(microtime(true) * 1000);
 
-                    //构建出 Order 数据
-                    $order = [
-                        'orderSn' => $orderSn,
-                        'customName' => $custom->name,
-                        'customMobile' => $custom->mobile,
-                        'fullAddress' => $custom->fullAddress,
-                        'floor' => $custom->floor,
-                        'dist' => $custom->dist,
-                        'lat' => $custom->lat,
-                        'long' => $custom->long,
-                        'address' => $custom->address, //'toAddress' => $order['address'],
-                        'city' => $custom->city,
-                        'weight' => $weight,
-                        'remark' => $post['remark'] ?? '',
-                        'prePrice' => $post['itemTotalAmount'] ?? 0, // 花材总价
-                        'actPrice' => $post['itemTotalAmount'] ?? 0,
-                    ];
-                    $ghsShop = ShopClass::getById($ghsShopId);
-                    $orderTime = date('Y-m-d H:i:s');
-                    $ds = new DispatchService($ghsInfo['mainId'], $platform);
-                    $platformQuotes = $ds->getAllPlatformPrice($order, $ghsShop, $orderTime);
-                    if(isset($platformQuotes['error'])){
-                        Yii::error($platformQuotes['error']);
-                        noticeUtil::push($platform . "-跑腿平台估价接口获取费用与距离失败:mainId=" . $this->mainId);
-                        util::fail($platform . "-跑腿平台估价接口获取费用与距离失败:mainId=" . $this->mainId);
-                    } else {
-                        // 格式化各平台报价为前端展示格式
-                        $deliveryList = $ds->formatPlatformQuotesForDisplay($platformQuotes);
-                        if(is_array($deliveryList) && empty($deliveryList)){ // 重试一次
-                            sleep(2);
-                            $platformQuotes = $ds->getAllPlatformPrice($order, $ghsShop, $orderTime);
-                        }
-
-                        $deliveryList = $ds->formatPlatformQuotesForDisplay($platformQuotes);
-                        if(is_array($deliveryList) && !empty($deliveryList)){
-                            $result = $deliveryList[0];
-                            $sendCost = ($result['price'] ?? 0)/100;
-                            $sendDistance = $result['distance'] ?? 0;
-
-                            //获取花材的免费配送设置
-                            $hcFreeKm = $shopExt['hcFreeKm'] * 1000;
-                            $hcMapString = $shopExt['hcMap'] ?? '';
-                            if (!empty($hcMapString)) {
-                                $hcMap = json_decode($hcMapString, true);
-                            }
-
-                            // 判断是否免跑腿费
-                            if($sendDistance > $hcFreeKm){
-                                if(count($hcMap)>0){
-                                    foreach($hcMap as $rule){
-                                        if($productCount >= $rule['num'] && $post['itemTotalAmount'] >= $rule['price'] && $sendDistance <= $rule['distance']*1000){
-                                            $sendCost = 0;
-                                            noticeUtil::push("ghsMainId={$ghsInfo['mainId']}, orderSn={$orderSn}, 免跑腿费,满足条件:".json_encode($rule));
-                                            break;
-                                        }
-                                    }
-                                }
-                            }else{
-                                $sendCost = 0;
-                            }
-                        }else{
-                            noticeUtil::push("ghsMainId={$ghsInfo['mainId']}, orderSn={$orderSn}  请求 {$platform} 平台报价失败。");
-                            Yii::error("ghsMainId={$ghsInfo['mainId']}, orderSn={$orderSn}  请求" . $platform . "平台报价失败。");
-                            util::fail("ghsMainId={$ghsInfo['mainId']}, orderSn={$orderSn}  请求 {$platform} 平台报价失败。");
-                        }
+                    // 使用 DeliveryQuoteUtil 获取配送报价
+                    try {
+                        $quoteResult = DeliveryQuoteUtil::getDeliveryQuote([
+                            'productList' => $productList,
+                            'deliveryPlatform' => $post['deliveryPlatform'],
+                            'deliveryBracketContent' => $post['deliveryBracketContent'], // 平台多报价识别id
+                            'ghsInfo' => $ghsInfo,
+                            'custom' => $custom,
+                            'order' => [
+                                'orderSn' => $orderSn,
+                                'itemTotalAmount' => $post['itemTotalAmount'] ?? 0,
+                                'remark' => $post['remark'] ?? '',
+                            ],
+                            'mainId' => $this->mainId,
+                            'productCount' => $productCount,
+                        ]);
+                        
+                        $sendCost = $quoteResult['sendCost'];
+                        $sendDistance = $quoteResult['sendDistance'];
+                    } catch (\Exception $e) {
+                        util::fail($e->getMessage());
                     }
                 }
                 $post['sendCost'] = $sendCost;

+ 5 - 1
app-mall/controllers/BaseController.php

@@ -64,13 +64,17 @@ class BaseController extends PublicController
                 if (!empty($hdId)) {
                     $hd = HdClass::getById($hdId, true);
                     if (!empty($hd)) {
-                        if (!empty($this->userId) && $hd->userId == $this->userId && $shopId == $hd->shopId) {
+                        if (!empty($this->userId) && $hd->userId == $this->userId && $shopId == $hd->shopId) { // 用户和店铺匹配校验
                             $this->hd = $hd;
                             $this->hdId = $hdId;
                             $customId = $hd->customId ?? 0;
                             $custom = CustomClass::getById($customId, true);
                             $this->custom = $custom;
                             $this->customId = $customId;
+                        } else { // 用户和店铺不匹配,清空店铺信息,防止后续使用(防止用户恶意使用店铺信息)
+                            $this->shop = null;
+                            $this->main = null;
+                            $this->mainId = 0;
                         }
                     }
                 }

+ 0 - 12
app-mall/controllers/CustomController.php

@@ -1,12 +0,0 @@
-<?php
-
-namespace mall\controllers;
-
-use bizHd\custom\classes\CustomClass;
-use common\components\httpUtil;
-use common\components\util;
-
-class CustomController extends BaseController
-{
-
-}

+ 98 - 0
app-mall/controllers/DeliveryController.php

@@ -0,0 +1,98 @@
+<?php
+namespace mall\controllers;
+
+use bizMall\shop\classes\ShopClass;
+use common\components\delivery\services\DispatchService;
+use common\components\util;
+use Yii;
+use yii\helpers\ArrayHelper;
+
+class DeliveryController extends BaseController
+{
+    // 终端客户向花店买花材 -- 获取多个平台报价
+    public function actionAllDeliveryQuotes()
+    {
+        $post = Yii::$app->request->post();
+        $orderTime = date('Y-m-d H:i:s');
+        // --------- $ghsShopId ----------        
+        $ghsShopId = $this->shopId;
+        if (empty($ghsShopId)) {
+            util::fail('店铺不存在');
+        }
+        $user = $this->user;
+        if (empty($user)) {
+            util::fail('用户不存在');
+        }
+        $mainId = $this->mainId;
+        if (empty($mainId)) {
+            util::fail('商户不存在');
+        }
+        $weight = $post['weight'] ?? 1;
+        $buyType = $post['buyType'] ?? 'huaCai';
+
+        $order = $this->generateOrderData($buyType, $user, $post);
+        $ghsShop = ShopClass::getById($ghsShopId);
+        $ds = new DispatchService($mainId);
+        $platformQuotes = $ds->getAllPlatformPrice($order, $ghsShop, $orderTime);
+        if(isset($platformQuotes['error'])){
+            util::fail($platformQuotes['error']);
+        }
+
+        $deliveryList = [];
+        // 格式化各平台报价为前端展示格式
+        $deliveryList = $ds->formatPlatformQuotesForDisplay($platformQuotes);
+        // 按 price 从低到高排序
+        ArrayHelper::multisort($deliveryList, 'price', SORT_ASC);
+
+        $ret['deliveryList'] = $deliveryList;
+        util::success($ret, "success");
+    }
+
+    private function generateOrderData($buyType, $user, $post)
+    {
+        // 生成随机订单号
+        $prefix = 'XSD_CS-' . $this->mainId . '-';
+        $orderSn = $prefix . round(microtime(true) * 1000);
+
+        //构建出 Order 数据
+        if($buyType == 'huaCai'){//花材
+            $order = [
+                'orderSn' => $orderSn,
+                'customName' => $user['name'],
+                'customMobile' => $user['mobile'],
+                'fullAddress' => $user['fullAddress'],
+                'floor' => $user['floor'],
+                'dist' => $user['dist'],
+                'lat' => $user['lat'],
+                'long' => $user['long'],
+                'address' => $user['address'], //'toAddress' => $order['address'],
+                'city' => $user['city'],
+                'weight' => $post['weight'] ?? 1,
+                'remark' => $post['remark'] ?? '',
+                'prePrice' => floatval($post['totalPrice'] ?? 0),
+                'actPrice' => floatval($post['totalPrice'] ?? 0),
+            ];
+        }elseif($buyType == 'huaShu'){//花束
+            $order = [
+                'orderSn' => $orderSn,
+                'customName' => $post['receiveUserName'],
+                'customMobile' => $post['receiveMobile'],
+                'fullAddress' => $post['address'],
+                'floor' => $post['floor'] ?? '',
+                'dist' => $post['dist'] ?? '',
+                'lat' => $post['region']['latitude'],// TODO
+                'long' => $post['region']['longitude'],// TODO
+                'address' => $post['address'], //'toAddress' => $order['address'],
+                'city' => $post['city'],
+                'weight' => $post['weight'] ?? 1,
+                'remark' => $post['remark'] ?? '',
+                'prePrice' => floatval($post['totalPrice'] ?? 0),
+                'actPrice' => floatval($post['totalPrice'] ?? 0),
+            ];
+        }else{
+            util::fail('不存在此订单类型');
+        }
+
+        return $order;
+    }
+}

+ 7 - 1
app-mall/controllers/ExpressController.php

@@ -37,6 +37,12 @@ class ExpressController extends BaseController
     // 获取字典中的 hasMap
     public function actionGetHasMap()
     {
-        util::success(['hasMap' => dict::getDict('hasMap')]);
+        // 补充店铺信息
+        $shop = [];
+        $intraCityRet = \biz\shop\classes\ShopClass::hasIntraCity($this->shop);
+        $shop['openIntraCity'] = $intraCityRet['openIntraCity'];
+        $shop['hcFreeKm'] = $intraCityRet['hcFreeKm'];
+        $shop['hcMap'] = $intraCityRet['hcMap'];
+        util::success(['hasMap' => dict::getDict('hasMap'), 'shop'=>$shop]);
     }
 }

+ 179 - 106
app-mall/controllers/OrderController.php

@@ -19,6 +19,7 @@ use bizMall\saas\services\RegionService;
 use bizMall\shop\classes\ShopExtClass;
 use bizMall\user\services\UserService;
 use common\components\dateUtil;
+use common\components\delivery\util\DeliveryQuoteUtil;
 use common\components\dict;
 use common\components\imgUtil;
 use common\components\noticeUtil;
@@ -334,12 +335,12 @@ class OrderController extends BaseController
                 }
                 $originalLng = doubleval($post['long']);
                 $originalLat = doubleval($post['lat']);
-                $city = $post['city'] ?? '';
-                $dist = $post['dist'] ?? '';
-                $address = $post['address'] ?? '';
-                $showAddress = $post['showAddress'] ?? '';
-                $userAddress = $city . $dist . $address . "({$showAddress})";
-                $main = $this->main;
+                // $city = $post['city'] ?? '';
+                // $dist = $post['dist'] ?? '';
+                // $address = $post['address'] ?? '';
+                // $showAddress = $post['showAddress'] ?? '';
+                // $userAddress = $city . $dist . $address . "({$showAddress})";
+                // $main = $this->main;
                 $shop = $this->shop;
                 $shopLat = $shop->lat ?? '';
                 $shopLong = $shop->long ?? '';
@@ -347,29 +348,61 @@ class OrderController extends BaseController
                     util::fail('商家门店地址定位未设置,编号263');
                 }
 
-                $wxStoreId = $main->wxStoreId ?? 0;
-                if (empty($wxStoreId)) {
-                    util::fail('商家还没有开通跑腿功能');
+            //    $wxStoreId = $main->wxStoreId ?? 0;
+            //    if (empty($wxStoreId)) {
+            //        util::fail('商家还没有开通跑腿功能');
+            //    }
+            //    $orderData = [
+            //        'wx_store_id' => $wxStoreId,
+            //        'user_name' => $customName,
+            //        'user_phone' => $customMobile,
+            //        'user_lng' => $originalLng,
+            //        'user_lat' => $originalLat,
+            //        'user_address' => $userAddress,
+            //        'cargo' => $cargo,
+            //    ];
+            //    //多处有用到这个方法,需修改请同步修改,搜索关键词 intra_city_express_pre_add
+            //    $result = IntraCityExpress::preAddOrder($orderData);
+            //    $errCode = $result['errcode'] ?? 1;
+            //    if ($errCode != 0) {
+            //        $errMsg = $result['errmsg'] ?? '';
+            //        util::fail('获取运费错误:' . $errMsg);
+            //    }
+            //    $fee = $result['est_fee'] ?? 0;
+            //    $distance = $result['distance'] ?? 0;
+            //    $sendCost = bcdiv($fee, 100, 2);
+
+                $distance = 0;
+                $sendCost = 0;
+
+                $prefix = 'XSD_CS-' . $this->mainId . '-';
+                $orderSn = $prefix . round(microtime(true) * 1000);
+
+                $ghsInfo = ['mainId'=>$this->mainId, 'shopId'=>$this->shopId];
+
+                // 这儿要调用跑腿接口计算运费和距离
+                try {
+                    $quoteResult = DeliveryQuoteUtil::getDeliveryQuote([
+                        'productList' => $productList,
+                        'deliveryPlatform' => $post['deliveryPlatform'],
+                        'deliveryBracketContent' => $post['deliveryBracketContent'], // 平台多报价识别id
+                        'ghsInfo' => $ghsInfo,
+                        'custom' => $this->user, // $custom 数据有问题,要是 user
+                        'order' => [
+                            'orderSn' => $orderSn,
+                            'itemTotalAmount' => $post['itemTotalAmount'] ?? 0,
+                            'remark' => $post['remark'] ?? '',
+                        ],
+                        'mainId' => $this->mainId,
+                        'productCount' => $totalNum,
+                    ]);
+                    
+                    $sendCost = $quoteResult['sendCost'];
+                    $distance = $quoteResult['sendDistance'];
+                } catch (\Exception $e) {
+                    util::fail($e->getMessage());
                 }
-                $orderData = [
-                    'wx_store_id' => $wxStoreId,
-                    'user_name' => $customName,
-                    'user_phone' => $customMobile,
-                    'user_lng' => $originalLng,
-                    'user_lat' => $originalLat,
-                    'user_address' => $userAddress,
-                    'cargo' => $cargo,
-                ];
-                //多处有用到这个方法,需修改请同步修改,搜索关键词 intra_city_express_pre_add
-                $result = IntraCityExpress::preAddOrder($orderData);
-                $errCode = $result['errcode'] ?? 1;
-                if ($errCode != 0) {
-                    $errMsg = $result['errmsg'] ?? '';
-                    util::fail('获取运费错误:' . $errMsg);
-                }
-                $fee = $result['est_fee'] ?? 0;
-                $distance = $result['distance'] ?? 0;
-                $sendCost = bcdiv($fee, 100, 2);
+
                 $post['sendCost'] = $sendCost;
                 $post['sendDistance'] = $distance;
                 noticeUtil::push("零售订单,获取运费:{$sendCost} 重量:{$totalWeight} 距离:{$distance} 姓名:{$customName} 手机号 {$customMobile} 经纬 {$originalLng} {$originalLat} 金额:{$modifyPrice} 数量:{$totalNum}", '15280215347');
@@ -492,86 +525,126 @@ class OrderController extends BaseController
             $sendCost = 0;
 
             if ($sendType == dict::getDict('sendType', 'thirdSend')) {
-                if (dict::getDict('hasMap') == 0) {
-                    util::fail('不能使用跑腿,请选其它方式');
-                }
-                $lat = $post['lat'] ?? '';
-                $lng = $post['long'] ?? '';
-                if ((empty($lat) || empty($lng))) { // 当 hasMap 为1时,经纬数据是必要的
-                    util::fail('请填写正确地址');
-                }
-                $address = $post['address'] ?? '';
-                $floor = $post['floor'] ?? '';
-                $fullAddress = $address . $floor;
-                $post['fullAddress'] = $fullAddress;
-
-                $shopLat = $shop->lat ?? '';
-                $shopLong = $shop->long ?? '';
-                if (empty($shopLat) || empty($shopLong)) {
-                    util::fail('商家门店地址定位未设置,编号8966');
-                }
-
-                $weight = $goodsInfo->weight ?? 0;
-                if ($weight <= 0) {
-                    util::fail('商品没有重量');
-                }
-                $totalWeight = bcmul($weight, $goodsNum, 2);
-                $cargoName = '鲜花'; // 商品名称
-                $cargo = [
-                    'cargo_name' => $cargoName, // 商品名称
-                    'cargo_weight' => intval($totalWeight * 1000), // 单位:克 -- 把千克转换为克 ---- 重量(用户未知)-- 大概
-                    'cargo_price' => intval($goodsPrice * 100), // 单位:分 -- 把元转换为分
-                    'cargo_type' => IntraCityExpress::GOODS_TYPE_FLOWER,
-                    'cargo_num' => $goodsNum
+                // if (dict::getDict('hasMap') == 0) {
+                //     util::fail('不能使用跑腿,请选其它方式');
+                // }
+                // $lat = $post['lat'] ?? '';
+                // $lng = $post['long'] ?? '';
+                // if ((empty($lat) || empty($lng))) { // 当 hasMap 为1时,经纬数据是必要的
+                //     util::fail('请填写正确地址');
+                // }
+                // $address = $post['address'] ?? '';
+                // $floor = $post['floor'] ?? '';
+                // $fullAddress = $address . $floor;
+                // $post['fullAddress'] = $fullAddress;
+
+                // $shopLat = $shop->lat ?? '';
+                // $shopLong = $shop->long ?? '';
+                // if (empty($shopLat) || empty($shopLong)) {
+                //     util::fail('商家门店地址定位未设置,编号8966');
+                // }
+
+                // $weight = $goodsInfo->weight ?? 0;
+                // if ($weight <= 0) {
+                //     util::fail('商品没有重量');
+                // }
+                // $totalWeight = bcmul($weight, $goodsNum, 2);
+                // $cargoName = '鲜花'; // 商品名称
+                // $cargo = [
+                //     'cargo_name' => $cargoName, // 商品名称
+                //     'cargo_weight' => intval($totalWeight * 1000), // 单位:克 -- 把千克转换为克 ---- 重量(用户未知)-- 大概
+                //     'cargo_price' => intval($goodsPrice * 100), // 单位:分 -- 把元转换为分
+                //     'cargo_type' => IntraCityExpress::GOODS_TYPE_FLOWER,
+                //     'cargo_num' => $goodsNum
+                // ];
+                // $customName = $custom->name;
+                // $customMobile = $custom->mobile;
+                // if (empty($post['long']) || empty($post['lat'])) {
+                //     util::fail('位置错误');
+                // }
+                // $originalLng = doubleval($post['long']);
+                // $originalLat = doubleval($post['lat']);
+                // $city = $post['city'] ?? '';
+                // $dist = $post['dist'] ?? '';
+                // $address = $post['address'] ?? '';
+                // $showAddress = $post['showAddress'] ?? '';
+                // $userAddress = $city . $dist . $address . "({$showAddress})";
+                // $main = $this->main;
+
+                // $shopLat = $shop->lat ?? '';
+                // $shopLong = $shop->long ?? '';
+                // if (empty($shopLat) || empty($shopLong)) {
+                //     util::fail('商家门店地址定位未设置,编号6880');
+                // }
+
+                // $wxStoreId = $main->wxStoreId ?? 0;
+                // if (empty($wxStoreId)) {
+                //     util::fail('商家还没有开通跑腿功能');
+                // }
+                // $orderData = [
+                //     'wx_store_id' => $wxStoreId,
+                //     'user_name' => $customName,
+                //     'user_phone' => $customMobile,
+                //     'user_lng' => $originalLng,
+                //     'user_lat' => $originalLat,
+                //     'user_address' => $userAddress,
+                //     'cargo' => $cargo,
+                // ];
+                // //多处有用到这个方法,需修改请同步修改,搜索关键词 intra_city_express_pre_add
+                // $result = IntraCityExpress::preAddOrder($orderData);
+                // $errCode = $result['errcode'] ?? 1;
+                // if ($errCode != 0) {
+                //     $errMsg = $result['errmsg'] ?? '';
+                //     util::fail('获取运费错误:' . $errMsg);
+                // }
+                // $fee = $result['est_fee'] ?? 0;
+                // $sendDistance = $result['distance'] ?? 0;
+                // $sendCost = bcdiv($fee, 100, 2);
+                // //免运费的不收运费
+                // if ($goodsInfo->freightType == 1) {
+                //     $sendCost = 0;
+                // }
+                // noticeUtil::push("零售订单!获取运费:{$sendCost} 重量:{$totalWeight} 距离:{$sendDistance} 姓名:{$customName} 手机号 {$customMobile} 经纬 {$originalLng} {$originalLat} 金额:{$goodsPrice} 数量:{$goodsNum}", '15280215347');
+
+                $cutomInfo = [
+                    'name' => $post['receiveUserName'],
+                    'mobile' => $post['receiveMobile'],
+                    'fullAddress' => $post['address'],
+                    'floor' => $post['floor'],
+                    'dist' => $post[''] ?? '',
+                    'lat' => $post['lat'],
+                    'long' => $post['long'],
+                    'address' => $post['address'],
+                    'city' => $post['city'],
                 ];
-                $customName = $custom->name;
-                $customMobile = $custom->mobile;
-                if (empty($post['long']) || empty($post['lat'])) {
-                    util::fail('位置错误');
-                }
-                $originalLng = doubleval($post['long']);
-                $originalLat = doubleval($post['lat']);
-                $city = $post['city'] ?? '';
-                $dist = $post['dist'] ?? '';
-                $address = $post['address'] ?? '';
-                $showAddress = $post['showAddress'] ?? '';
-                $userAddress = $city . $dist . $address . "({$showAddress})";
-                $main = $this->main;
-
-                $shopLat = $shop->lat ?? '';
-                $shopLong = $shop->long ?? '';
-                if (empty($shopLat) || empty($shopLong)) {
-                    util::fail('商家门店地址定位未设置,编号6880');
-                }
-
-                $wxStoreId = $main->wxStoreId ?? 0;
-                if (empty($wxStoreId)) {
-                    util::fail('商家还没有开通跑腿功能');
-                }
-                $orderData = [
-                    'wx_store_id' => $wxStoreId,
-                    'user_name' => $customName,
-                    'user_phone' => $customMobile,
-                    'user_lng' => $originalLng,
-                    'user_lat' => $originalLat,
-                    'user_address' => $userAddress,
-                    'cargo' => $cargo,
-                ];
-                //多处有用到这个方法,需修改请同步修改,搜索关键词 intra_city_express_pre_add
-                $result = IntraCityExpress::preAddOrder($orderData);
-                $errCode = $result['errcode'] ?? 1;
-                if ($errCode != 0) {
-                    $errMsg = $result['errmsg'] ?? '';
-                    util::fail('获取运费错误:' . $errMsg);
-                }
-                $fee = $result['est_fee'] ?? 0;
-                $sendDistance = $result['distance'] ?? 0;
-                $sendCost = bcdiv($fee, 100, 2);
-                //免运费的不收运费
-                if ($goodsInfo->freightType == 1) {
-                    $sendCost = 0;
+                $cutomInfo = (object)$cutomInfo;
+
+                // 生成随机订单号
+                $prefix = 'XSD_CS-' . $this->mainId . '-';
+                $orderSn = $prefix . round(microtime(true) * 1000);
+
+                // 使用 DeliveryQuoteUtil 获取配送报价
+                try {
+                    $quoteResult = DeliveryQuoteUtil::getDeliveryQuote([
+                        'productList' => [['bigNum' => $goodsNum, 'weight' => $goodsInfo->weight]],
+                        'deliveryPlatform' => $post['deliveryPlatform'],
+                        'deliveryBracketContent' => $post['deliveryBracketContent'], // 平台多报价识别id
+                        'ghsInfo' => ['mainId' => $mainId, 'shopId' => $this->shopId],
+                        'custom' => $cutomInfo,
+                        'order' => [
+                            'orderSn' => $orderSn,
+                            'itemTotalAmount' => $post['itemTotalAmount'] ?? 0,
+                            'remark' => $post['remark'] ?? '',
+                        ],
+                        'mainId' => $this->mainId,
+                        'productCount' => $goodsNum,
+                    ]);
+                    
+                    $sendCost = $quoteResult['sendCost'];
+                    $sendDistance = $quoteResult['sendDistance'];
+                } catch (\Exception $e) {
+                    util::fail($e->getMessage());
                 }
-                noticeUtil::push("零售订单!获取运费:{$sendCost} 重量:{$totalWeight} 距离:{$sendDistance} 姓名:{$customName} 手机号 {$customMobile} 经纬 {$originalLng} {$originalLat} 金额:{$goodsPrice} 数量:{$goodsNum}", '15280215347');
             }
             $post['sendDistance'] = $sendDistance;
             $post['sendCost'] = $sendCost;

+ 12 - 3
app-mall/controllers/UserController.php

@@ -2,12 +2,14 @@
 
 namespace mall\controllers;
 
+use biz\shop\classes\ShopExtClass;
 use bizHd\custom\classes\CustomClass;
 use bizHd\wx\classes\WxOpenClass;
 use bizMall\message\services\SmsService;
 use bizMall\user\classes\UserClass;
 use bizMall\user\services\UserAssetService;
 use bizMall\user\services\UserService;
+use biz\shop\classes\ShopClass;
 use common\components\dict;
 use common\components\imgUtil;
 use common\components\jwt;
@@ -159,9 +161,16 @@ class UserController extends BaseController
             $user['shortAvatar'] = $shortAvatar;
         }
 
-        $hasMap = dict::getDict('hasMap');
-        $user['hasMap'] = $hasMap;
-        util::success(['info' => $user]);
+        // 补充店铺信息
+        $shop = [];
+        if(!empty($this->shop)){
+            $intraCityRet = ShopClass::hasIntraCity($this->shop);
+            $shop['openIntraCity'] = $intraCityRet['openIntraCity'];
+            $shop['hcFreeKm'] = $intraCityRet['hcFreeKm'];
+            $shop['hcMap'] = $intraCityRet['hcMap'];
+        }
+        
+        util::success(['info' => $user, 'shop' => $shop]);
     }
 
     //使用账号登录 ssh 20220920

+ 2 - 2
biz/shop/classes/ShopClass.php

@@ -29,7 +29,7 @@ class ShopClass extends BaseClass
         $long = $shop->long ?? '';
         $address = $shop->address ?? '';
         $hasMap = dict::getDict('hasMap');
-        $ext = ShopExtClass::getByCondition(['shopId' => $shop->id], true, null, 'id,shopId,thirdSendFee');
+        $ext = ShopExtClass::getByCondition(['shopId' => $shop->id], true, null, 'id,shopId,thirdSendFee,hcFreeKm, hcMap');
         $openIntraCity = 0;
         //有地图功能
         if ($hasMap == 1) {
@@ -40,7 +40,7 @@ class ShopClass extends BaseClass
                 }
             }
         }
-        return ['openIntraCity' => $openIntraCity];
+        return ['openIntraCity' => $openIntraCity, 'hcFreeKm'=>$ext->hcFreeKm, 'hcMap'=>$ext->hcMap];
     }
 
     //营业状态

+ 3 - 0
common/components/delivery/services/DispatchService.php

@@ -726,6 +726,7 @@ class DispatchService
 
                                 $deliveryList[] = [
                                     'name' => '蜂鸟'. ' (' .$good['base_goods_id'] . ')',
+                                    'base_goods_id' => $good['base_goods_id'],
                                     'en_name' => 'fengniao',
                                     'price' => $good['actual_delivery_amount_cent'],
                                     'distance' => $item['distance'],
@@ -736,6 +737,7 @@ class DispatchService
                             } else {
                                 $deliveryList[] = [
                                     'name' => '蜂鸟'. ' (' .$good['base_goods_id'] . ')',
+                                    'base_goods_id' => $good['base_goods_id'],
                                     'en_name' => 'fengniao',
                                     'price' => 0,
                                     'distance' => $item['distance'],
@@ -753,6 +755,7 @@ class DispatchService
                         $vehicleType = $arr['vehicle_type'];
                         $deliveryList[] = [
                             'name' => '货拉拉' . ' (' . $vehicleType['_meta']['vehicle_type'] . ')',
+                            'vehicle_type' => $vehicleType['_meta']['vehicle_type'],
                             'en_name' => 'huolala',
                             'price' => $priceInfo['price_conditions'][0]['price_info']['total_price'],
                             'distance' => $priceInfo['distance_info']['distance_total'],

+ 1 - 1
common/components/delivery/services/adapter/Functions.php

@@ -15,6 +15,6 @@ Class Functions {
             return $order['bookMobile'];
         }
 
-        return false;
+        throw new \Exception('收件人手机号不存在');
     }
 }

+ 275 - 0
common/components/delivery/util/DeliveryQuoteUtil.php

@@ -0,0 +1,275 @@
+<?php
+namespace common\components\delivery\util;
+
+use biz\shop\classes\ShopClass;
+use biz\shop\classes\ShopExtClass;
+use common\components\delivery\services\DispatchService;
+use common\components\noticeUtil;
+use common\components\util;
+use Yii;
+
+/**
+ * 配送报价工具类
+ * 用于统一处理跑腿平台报价、免费配送规则等逻辑
+ */
+class DeliveryQuoteUtil
+{
+    /**
+     * 获取配送报价(含免费配送规则计算)
+     * 
+     * @param array $params 参数数组
+     *   - productList: 商品列表 [['bigNum' => 数量, 'weight' => 重量], ...]
+     *   - deliveryPlatform: 配送平台(shansong/huolala/fengniao等)
+     *   - ghsInfo: 供货商信息 ['mainId' => xxx, 'shopId' => xxx]
+     *   - custom: 客户信息对象(包含 name, mobile, fullAddress, lat, long 等)
+     *   - order: 订单基础信息 ['orderSn' => xxx, 'itemTotalAmount' => xxx, 'remark' => xxx]
+     *   - mainId: 当前操作者的 mainId
+     *   - productCount: 商品总数量(用于免费配送判断)
+     * 
+     * @return array 返回数组
+     *   - sendCost: 配送费用(元)
+     *   - sendDistance: 配送距离(米)
+     *   - deliveryList: 可选配送方式列表
+     *   - platformQuotes: 原始平台报价数据
+     * 
+     * @throws \Exception 当报价失败时抛出异常
+     */
+    public static function getDeliveryQuote($params)
+    {
+        // 1. 参数验证
+        self::validateParams($params);
+        
+        // 2. 计算商品总重量
+        $weight = self::calculateTotalWeight($params['productList']);
+        
+        // 3. 构建订单数据
+        $order = self::buildOrderData($params, $weight);
+        
+        // 4. 获取供货商店铺信息
+        $ghsShop = ShopClass::getById($params['ghsInfo']['shopId']);
+        if (empty($ghsShop)) {
+            throw new \Exception("供货商店铺信息不存在:shopId={$params['ghsInfo']['shopId']}");
+        }
+        
+        // 5. 调用跑腿平台获取报价
+        $orderTime = date('Y-m-d H:i:s');
+        $ds = new DispatchService($params['ghsInfo']['mainId'], $params['deliveryPlatform']);
+        $platformQuotes = $ds->getAllPlatformPrice($order, $ghsShop, $orderTime);
+        
+        // 6. 检查报价结果
+        if (isset($platformQuotes['error'])) {
+            $errorMsg = $platformQuotes['error'];
+            Yii::error($errorMsg);
+            noticeUtil::push("{$params['deliveryPlatform']}-跑腿平台估价接口获取费用与距离失败:mainId={$params['mainId']}");
+            throw new \Exception($errorMsg);
+        }
+        
+        // 7. 格式化平台报价
+        $deliveryList = $ds->formatPlatformQuotesForDisplay($platformQuotes);
+        
+        // 8. 重试机制(如果第一次失败)
+        if (is_array($deliveryList) && empty($deliveryList)) {
+            sleep(2);
+            $platformQuotes = $ds->getAllPlatformPrice($order, $ghsShop, $orderTime);
+            $deliveryList = $ds->formatPlatformQuotesForDisplay($platformQuotes);
+        }
+        
+        // 9. 检查是否有可用的配送方式
+        if (is_array($deliveryList) && empty($deliveryList)) {
+            $errorMsg = "ghsMainId={$params['ghsInfo']['mainId']}, orderSn={$order['orderSn']} 请求{$params['deliveryPlatform']}平台报价失败。";
+            noticeUtil::push($errorMsg);
+            Yii::error($errorMsg);
+            throw new \Exception($errorMsg);
+        }
+        
+        // 10. 获取报价结果
+        $result = null;
+        if(in_array($params['deliveryPlatform'], ['huolala', 'fengniao'])){
+            $bracketContent = $params['deliveryBracketContent'] ?? '';
+            $key = '';
+            foreach($deliveryList as $item){
+                if($params['deliveryPlatform'] == 'huolala'){
+                    $key = 'vehicle_type';
+                }
+                if($params['deliveryPlatform'] == 'fengniao'){
+                    $key = 'base_goods_id';
+                }
+                if($item[$key] == $bracketContent){
+                    $result = $item;
+                }
+            }
+            if($result === null){
+                throw new \Exception('没有找到对应的配送方式');
+            }
+        }else{
+            $result = $deliveryList[0];
+        }
+
+        $sendCost = ($result['price'] ?? 0) / 100;
+        $sendDistance = $result['distance'] ?? 0;
+        
+        // 11. 应用免费配送规则
+        $finalSendCost = self::applyFreeDeliveryRules(
+            $sendCost,
+            $sendDistance,
+            $params['ghsInfo']['shopId'],
+            $params['productCount'] ?? 0,
+            $params['order']['itemTotalAmount'] ?? 0,
+            $params['ghsInfo']['mainId'],
+            $order['orderSn']
+        );
+        
+        return [
+            'sendCost' => $finalSendCost,
+            'sendDistance' => $sendDistance,
+            'deliveryList' => $deliveryList,
+            'platformQuotes' => $platformQuotes,
+        ];
+    }
+    
+    /**
+     * 计算商品总重量
+     * 
+     * @param array $productList 商品列表
+     * @return float 总重量(公斤)
+     */
+    private static function calculateTotalWeight($productList)
+    {
+        $weight = 0;
+        foreach ($productList as $itemData) {
+            $bigNum = $itemData['bigNum'] ?? 0;
+            $thisWeight = $itemData['weight'] ?? 0;
+            $currentWeight = bcmul($thisWeight, $bigNum, 2);
+            $weight = bcadd($currentWeight, $weight, 2);
+        }
+        return $weight;
+    }
+    
+    /**
+     * 构建订单数据
+     * 
+     * @param array $params 参数数组
+     * @param float $weight 总重量
+     * @return array 订单数据
+     */
+    private static function buildOrderData($params, $weight)
+    {
+        $custom = $params['custom'];
+        $orderInfo = $params['order'];
+        
+        return [
+            'orderSn' => $orderInfo['orderSn'],
+            'customName' => $custom->name,
+            'customMobile' => $custom->mobile,
+            'fullAddress' => $custom->fullAddress,
+            'floor' => $custom->floor,
+            'dist' => $custom->dist,
+            'lat' => $custom->lat,
+            'long' => $custom->long,
+            'address' => $custom->address,
+            'city' => $custom->city,
+            'weight' => $weight,
+            'remark' => $orderInfo['remark'] ?? '',
+            'prePrice' => $orderInfo['itemTotalAmount'] ?? 0,
+            'actPrice' => $orderInfo['itemTotalAmount'] ?? 0,
+        ];
+    }
+    
+    /**
+     * 应用免费配送规则
+     * 
+     * @param float $sendCost 原始配送费用
+     * @param int $sendDistance 配送距离(米)
+     * @param int $shopId 店铺ID
+     * @param int $productCount 商品数量
+     * @param float $itemTotalAmount 商品总金额
+     * @param int $ghsMainId 供货商mainId
+     * @param string $orderSn 订单号
+     * @return float 最终配送费用
+     */
+    private static function applyFreeDeliveryRules(
+        $sendCost,
+        $sendDistance,
+        $shopId,
+        $productCount,
+        $itemTotalAmount,
+        $ghsMainId,
+        $orderSn
+    ) {
+        // 获取店铺扩展信息(免费配送设置)
+        $shopExt = ShopExtClass::getByCondition(['shopId' => $shopId], false, null, 'id,hcFreeKm,hcMap');
+        if (empty($shopExt)) {
+            return $sendCost;
+        }
+        
+        // 获取花材的免费配送设置
+        $hcFreeKm = ($shopExt['hcFreeKm'] ?? 0) * 1000; // 转换为米
+        $hcMapString = $shopExt['hcMap'] ?? '';
+        $hcMap = [];
+        
+        if (!empty($hcMapString)) {
+            $hcMap = json_decode($hcMapString, true);
+        }
+        
+        // 判断是否免跑腿费
+        if ($sendDistance > $hcFreeKm) {
+            // 超过免费距离,检查是否满足其他免费条件
+            if (count($hcMap) > 0) {
+                foreach ($hcMap as $rule) {
+                    $ruleNum = $rule['num'] ?? 0;
+                    $rulePrice = $rule['price'] ?? 0;
+                    $ruleDistance = ($rule['distance'] ?? 0) * 1000;
+                    
+                    if ($productCount >= $ruleNum 
+                        && $itemTotalAmount >= $rulePrice 
+                        && $sendDistance <= $ruleDistance
+                    ) {
+                        noticeUtil::push(
+                            "ghsMainId={$ghsMainId}, orderSn={$orderSn}, 免跑腿费,满足条件:" 
+                            . json_encode($rule)
+                        );
+                        return 0;
+                    }
+                }
+            }
+        } else {
+            // 在免费配送距离内
+            return 0;
+        }
+        
+        return $sendCost;
+    }
+    
+    /**
+     * 参数验证
+     * 
+     * @param array $params 参数数组
+     * @throws \Exception 当参数不合法时抛出异常
+     */
+    private static function validateParams($params)
+    {
+        $requiredFields = [
+            'productList', 
+            'deliveryPlatform', 
+            'ghsInfo', 
+            'custom', 
+            'order', 
+            'mainId'
+        ];
+        
+        foreach ($requiredFields as $field) {
+            if (!isset($params[$field])) {
+                throw new \Exception("缺少必要参数:{$field}");
+            }
+        }
+        
+        if (empty($params['productList']) || !is_array($params['productList'])) {
+            throw new \Exception("商品列表不能为空");
+        }
+        
+        if (empty($params['ghsInfo']['mainId']) || empty($params['ghsInfo']['shopId'])) {
+            throw new \Exception("供货商信息不完整");
+        }
+    }
+}
+

+ 210 - 0
common/components/delivery/util/DeliveryQuoteUtil_USAGE.md

@@ -0,0 +1,210 @@
+# DeliveryQuoteUtil 使用说明
+
+## 简介
+
+`DeliveryQuoteUtil` 是一个统一的配送报价工具类,用于处理跑腿平台报价、免费配送规则等逻辑。
+
+## 功能特性
+
+- ✅ 自动计算商品总重量
+- ✅ 调用跑腿平台获取报价(支持闪送、货拉拉、蜂鸟、顺丰、达达等)
+- ✅ 自动重试机制
+- ✅ 应用免费配送规则
+- ✅ 统一的异常处理
+- ✅ 详细的日志记录
+
+## 使用方法
+
+### 1. 引入命名空间
+
+```php
+use common\components\delivery\helpers\DeliveryQuoteUtil;
+```
+
+### 2. 调用方法
+
+```php
+try {
+    $quoteResult = DeliveryQuoteUtil::getDeliveryQuote([
+        'productList' => $productList,           // 商品列表
+        'deliveryPlatform' => $platform,         // 配送平台
+        'ghsInfo' => $ghsInfo,                   // 供货商信息
+        'custom' => $custom,                     // 客户信息对象
+        'order' => [                             // 订单信息
+            'orderSn' => $orderSn,
+            'itemTotalAmount' => $totalAmount,
+            'remark' => $remark,
+        ],
+        'mainId' => $this->mainId,               // 当前操作者 mainId
+        'productCount' => $productCount,         // 商品总数量
+    ]);
+    
+    // 获取结果
+    $sendCost = $quoteResult['sendCost'];           // 配送费用(元)
+    $sendDistance = $quoteResult['sendDistance'];   // 配送距离(米)
+    $deliveryList = $quoteResult['deliveryList'];   // 可选配送方式列表
+    
+} catch (\Exception $e) {
+    util::fail($e->getMessage());
+}
+```
+
+## 参数说明
+
+### 输入参数
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| productList | array | 是 | 商品列表,每项包含 `bigNum`(数量)和 `weight`(重量) |
+| deliveryPlatform | string | 是 | 配送平台:`shansong`/`huolala`/`fengniao`/`shunfeng`/`dada` |
+| ghsInfo | array | 是 | 供货商信息,包含 `mainId` 和 `shopId` |
+| custom | object | 是 | 客户信息对象,包含 `name`, `mobile`, `fullAddress`, `lat`, `long` 等 |
+| order | array | 是 | 订单信息,包含 `orderSn`, `itemTotalAmount`, `remark` |
+| mainId | int | 是 | 当前操作者的 mainId |
+| productCount | int | 否 | 商品总数量(用于免费配送规则判断) |
+
+### 返回结果
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| sendCost | float | 配送费用(元),已应用免费配送规则 |
+| sendDistance | int | 配送距离(米) |
+| deliveryList | array | 可选配送方式列表 |
+| platformQuotes | array | 原始平台报价数据 |
+
+## 使用示例
+
+### 示例 1:在 PurchaseController 中使用
+
+```php
+// 生成订单号
+$prefix = 'XSD_CS-' . $this->mainId . '-';
+$orderSn = $prefix . round(microtime(true) * 1000);
+
+// 获取配送报价
+try {
+    $quoteResult = DeliveryQuoteUtil::getDeliveryQuote([
+        'productList' => $productList,
+        'deliveryPlatform' => $post['deliveryPlatform'],
+        'ghsInfo' => $ghsInfo,
+        'custom' => $custom,
+        'order' => [
+            'orderSn' => $orderSn,
+            'itemTotalAmount' => $post['itemTotalAmount'] ?? 0,
+            'remark' => $post['remark'] ?? '',
+        ],
+        'mainId' => $this->mainId,
+        'productCount' => $productCount,
+    ]);
+    
+    $sendCost = $quoteResult['sendCost'];
+    $sendDistance = $quoteResult['sendDistance'];
+    
+} catch (\Exception $e) {
+    util::fail($e->getMessage());
+}
+```
+
+### 示例 2:在 OrderController 中使用
+
+```php
+// 准备商品列表
+$productList = [];
+foreach ($orderItems as $item) {
+    $productList[] = [
+        'bigNum' => $item['num'],
+        'weight' => $item['weight'],
+    ];
+}
+
+// 准备客户信息对象
+$custom = (object)[
+    'name' => $customName,
+    'mobile' => $customMobile,
+    'fullAddress' => $fullAddress,
+    'floor' => $floor,
+    'dist' => $dist,
+    'lat' => $lat,
+    'long' => $long,
+    'address' => $address,
+    'city' => $city,
+];
+
+// 获取配送报价
+try {
+    $quoteResult = DeliveryQuoteUtil::getDeliveryQuote([
+        'productList' => $productList,
+        'deliveryPlatform' => 'shansong',
+        'ghsInfo' => [
+            'mainId' => $shop->mainId,
+            'shopId' => $shop->id,
+        ],
+        'custom' => $custom,
+        'order' => [
+            'orderSn' => $orderSn,
+            'itemTotalAmount' => $totalAmount,
+            'remark' => $remark,
+        ],
+        'mainId' => $this->mainId,
+        'productCount' => $totalCount,
+    ]);
+    
+    $sendCost = $quoteResult['sendCost'];
+    $sendDistance = $quoteResult['sendDistance'];
+    
+} catch (\Exception $e) {
+    util::fail('获取配送报价失败:' . $e->getMessage());
+}
+```
+
+## 免费配送规则说明
+
+工具类会自动应用以下免费配送规则:
+
+1. **基础免费距离**:如果配送距离在店铺设置的 `hcFreeKm` 范围内,免收配送费
+2. **条件免费配送**:超过基础免费距离后,如果满足以下条件,仍可免费:
+   - 商品数量 >= 规则设定数量
+   - 商品总金额 >= 规则设定金额
+   - 配送距离 <= 规则设定距离
+
+这些规则从 `xh_shop_ext` 表的 `hcFreeKm` 和 `hcMap` 字段中读取。
+
+## 错误处理
+
+工具类会在以下情况抛出异常:
+
+- 缺少必要参数
+- 商品列表为空
+- 供货商信息不完整
+- 供货商店铺不存在
+- 平台报价接口调用失败
+- 没有可用的配送方式
+
+建议使用 `try-catch` 捕获异常并给用户友好的提示。
+
+## 日志记录
+
+工具类会自动记录以下日志:
+
+- 平台报价接口调用失败
+- 免费配送规则命中情况
+- 异常信息
+
+日志通过 `Yii::error()` 和 `noticeUtil::push()` 记录。
+
+## 注意事项
+
+1. 确保传入的 `custom` 对象包含完整的地址和经纬度信息
+2. `productList` 中每个商品必须包含 `bigNum` 和 `weight` 字段
+3. 配送平台名称必须是小写英文:`shansong`/`huolala`/`fengniao`/`shunfeng`/`dada`
+4. 工具类内置重试机制,第一次失败会自动重试一次
+5. 返回的 `sendCost` 单位是元,`sendDistance` 单位是米
+
+## 版本历史
+
+- **v1.0.0** (2024-12-04)
+  - 初始版本
+  - 支持闪送、货拉拉、蜂鸟、顺丰、达达平台
+  - 支持免费配送规则
+  - 支持自动重试机制
+