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

Merge branch 'dev' of http://git.huaml.com/zhh/huahuibao into dev

shish 2 недель назад
Родитель
Сommit
c3e290ceaf

+ 56 - 0
app-hd/controllers/PsMethodController.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace hd\controllers;
+
+use bizHd\order\classes\PsMethodClass;
+use Yii;
+use common\components\util;
+
+/**
+ * 花店配送方式设置接口(xhPsMethod / xhPsExplain / xhPsReduceRule)
+ * 用途:hdApp 门店设置-配送方式页,与供货商 xhSh* 配置分离
+ */
+class PsMethodController extends BaseController
+{
+    /**
+     * 获取指定配送方式的配置
+     * GET style:0送货 1自取 2跑腿 3物流 4快递
+     */
+    public function actionGetConfig()
+    {
+        $style = Yii::$app->request->get('style', 0);
+        try {
+            $config = PsMethodClass::getConfig($this->shopId, $this->mainId, $style);
+            util::success($config);
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+
+    /**
+     * 获取所有配送方式的排序与别名(底部预览用)
+     */
+    public function actionGetSorts()
+    {
+        try {
+            $sorts = PsMethodClass::getSorts($this->mainId);
+            util::success($sorts);
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+
+    /**
+     * 保存指定配送方式配置
+     */
+    public function actionSaveConfig()
+    {
+        $post = Yii::$app->request->post();
+        try {
+            PsMethodClass::saveConfig($this->shopId, $this->mainId, $post);
+            util::complete('保存成功');
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+}

+ 4 - 1
app-hd/controllers/ShMethodController.php

@@ -7,9 +7,12 @@ use bizHd\ghs\classes\GhsClass;
 use Yii;
 use common\components\util;
 
+/**
+ * 花店配送方式接口(供货商 xhSh* 表,供进货结算等场景)
+ * 用途:门店向供货商下单时读取配送配置;设置页请走 PsMethodController
+ */
 class ShMethodController extends BaseController
 {
-
     /**
      * 获取供货商所有的5种配送方式配置
      * 如果数据库里没有,则取默认配置进行初始化

+ 90 - 0
app-hd/controllers/WlController.php

@@ -0,0 +1,90 @@
+<?php
+/**
+ * 花店物流选项管理
+ * 用途:配送方式-物流(style=3)关联的物流名称维护,与 ghs 端 WlClass 共用主账号维度数据
+ */
+
+namespace hd\controllers;
+
+use bizGhs\merchant\classes\WlClass;
+use Yii;
+use common\components\util;
+
+class WlController extends BaseController
+{
+    /** 获取全部物流(下拉用) */
+    public function actionGetAllWl()
+    {
+        $list = WlClass::getAllByCondition(['mainId' => $this->mainId, 'delStatus' => 0], 'inTurn DESC', 'id,name');
+        util::success(['list' => $list]);
+    }
+
+    /** 物流列表(分页) */
+    public function actionList()
+    {
+        $where = ['mainId' => $this->mainId, 'delStatus' => 0];
+        $list = WlClass::getWlList($where);
+        util::success($list);
+    }
+
+    /** 删除物流 */
+    public function actionDelWl()
+    {
+        $id = Yii::$app->request->get('id', 0);
+        $wl = WlClass::getById($id, true);
+        if (empty($wl)) {
+            util::fail('没有物流信息');
+        }
+        if ($wl->mainId != $this->mainId) {
+            util::fail('无法操作');
+        }
+        $wl->delStatus = 1;
+        $wl->save();
+        util::complete();
+    }
+
+    /** 添加物流 */
+    public function actionAdd()
+    {
+        $post = Yii::$app->request->post();
+        $post['mainId'] = $this->mainId ?? 0;
+        $name = trim($post['name'] ?? '');
+        if (empty($name)) {
+            util::fail('请填写名称');
+        }
+        $has = WlClass::getByCondition(['mainId' => $this->mainId, 'name' => $name], true);
+        if (!empty($has)) {
+            $has->delStatus = 0;
+            $has->save();
+        } else {
+            WlClass::add($post);
+        }
+        util::complete('添加成功');
+    }
+
+    /** 更新物流 */
+    public function actionUpdate()
+    {
+        $post = Yii::$app->request->post();
+        $name = trim($post['name'] ?? '');
+        $inTurn = $post['inTurn'] ?? 100;
+        $id = $post['id'] ?? 0;
+        $pre = WlClass::getById($id, true);
+        if (empty($pre)) {
+            util::fail('修改失败');
+        }
+        if ($pre->mainId != $this->mainId) {
+            util::fail('没有权限修改');
+        }
+        if ($name != $pre->name) {
+            $repeat = WlClass::getByCondition(['mainId' => $this->mainId, 'name' => $name], true);
+            if (!empty($repeat)) {
+                util::fail('名称已经存在');
+            }
+        }
+        $pre->name = $name;
+        $pre->inTurn = $inTurn;
+        $pre->save();
+        util::complete();
+    }
+}

+ 188 - 0
app-mall/controllers/UserAddressController.php

@@ -0,0 +1,188 @@
+<?php
+
+namespace mall\controllers;
+
+use common\components\util;
+use common\models\xhUserAddress;
+use Yii;
+
+class UserAddressController extends BaseController
+{
+    public $guestAccess = [];
+
+    // 获取地址列表
+    public function actionList()
+    {
+        $user = $this->user;
+        if (empty($user)) {
+            util::fail('请先登录');
+        }
+
+        $list = xhUserAddress::find()
+            ->where(['userId' => $user->id])
+            ->orderBy(['default' => SORT_DESC, 'id' => SORT_DESC])
+            ->asArray()
+            ->all();
+
+        // 格式化数据,兼容前端需要的字段
+        foreach ($list as &$item) {
+            $item['name'] = $item['name'] ?? '';
+            $item['phone'] = $item['phone'] ?? '';
+            $item['tag'] = $item['tag'] ?? '';
+        }
+
+        util::success(['list' => $list]);
+    }
+
+    // 获取地址详情
+    public function actionDetail()
+    {
+        $user = $this->user;
+        if (empty($user)) {
+            util::fail('请先登录');
+        }
+
+        $id = Yii::$app->request->get('id');
+        if (empty($id)) {
+            util::fail('参数错误');
+        }
+
+        $info = xhUserAddress::find()
+            ->where(['id' => $id, 'userId' => $user->id])
+            ->asArray()
+            ->one();
+
+        if (empty($info)) {
+            util::fail('地址不存在');
+        }
+
+        util::success(['info' => $info]);
+    }
+
+    // 创建地址
+    public function actionCreate()
+    {
+        $user = $this->user;
+        if (empty($user)) {
+            util::fail('请先登录');
+        }
+
+        $post = Yii::$app->request->post();
+        
+        $model = new xhUserAddress();
+        $model->userId = $user->id;
+        $model->name = $post['name'] ?? '';
+        $model->phone = $post['phone'] ?? '';
+        $model->tag = $post['tag'] ?? '';
+        $model->lat = $post['lat'] ?? '';
+        $model->long = $post['long'] ?? '';
+        $model->province = $post['province'] ?? '';
+        $model->city = $post['city'] ?? '';
+        $model->dist = $post['dist'] ?? '';
+        $model->address = $post['address'] ?? '';
+        $model->floor = $post['floor'] ?? '';
+        $model->fullAddress = $post['fullAddress'] ?? '';
+        $model->showAddress = $post['showAddress'] ?? '';
+        $model->default = $post['default'] ?? 0;
+        
+        if ($model->default == 1) {
+            xhUserAddress::updateAll(['default' => 0], ['userId' => $user->id]);
+        }
+
+        if ($model->save()) {
+            util::complete();
+        } else {
+            util::fail('保存失败');
+        }
+    }
+
+    // 更新地址
+    public function actionUpdate()
+    {
+        $user = $this->user;
+        if (empty($user)) {
+            util::fail('请先登录');
+        }
+
+        $post = Yii::$app->request->post();
+        $id = $post['id'] ?? 0;
+
+        $model = xhUserAddress::findOne(['id' => $id, 'userId' => $user->id]);
+        if (empty($model)) {
+            util::fail('地址不存在');
+        }
+
+        $model->name = $post['name'] ?? $model->name;
+        $model->phone = $post['phone'] ?? $model->phone;
+        $model->tag = $post['tag'] ?? $model->tag;
+        $model->lat = $post['lat'] ?? $model->lat;
+        $model->long = $post['long'] ?? $model->long;
+        $model->province = $post['province'] ?? $model->province;
+        $model->city = $post['city'] ?? $model->city;
+        $model->dist = $post['dist'] ?? $model->dist;
+        $model->address = $post['address'] ?? $model->address;
+        $model->floor = $post['floor'] ?? $model->floor;
+        $model->fullAddress = $post['fullAddress'] ?? $model->fullAddress;
+        $model->showAddress = $post['showAddress'] ?? $model->showAddress;
+        $model->default = $post['default'] ?? $model->default;
+
+        if ($model->default == 1) {
+            xhUserAddress::updateAll(['default' => 0], ['userId' => $user->id]);
+        }
+
+        if ($model->save()) {
+            util::complete();
+        } else {
+            util::fail('保存失败');
+        }
+    }
+
+    // 删除地址
+    public function actionDelete()
+    {
+        $user = $this->user;
+        if (empty($user)) {
+            util::fail('请先登录');
+        }
+
+        $post = Yii::$app->request->post();
+        $id = $post['id'] ?? 0;
+
+        $model = xhUserAddress::findOne(['id' => $id, 'userId' => $user->id]);
+        if (empty($model)) {
+            util::fail('地址不存在');
+        }
+
+        if ($model->delete()) {
+            util::complete();
+        } else {
+            util::fail('删除失败');
+        }
+    }
+
+    // 设为默认地址
+    public function actionSetDefault()
+    {
+        $user = $this->user;
+        if (empty($user)) {
+            util::fail('请先登录');
+        }
+
+        $post = Yii::$app->request->post();
+        $id = $post['id'] ?? 0;
+
+        $model = xhUserAddress::findOne(['id' => $id, 'userId' => $user->id]);
+        if (empty($model)) {
+            util::fail('地址不存在');
+        }
+
+        xhUserAddress::updateAll(['default' => 0], ['userId' => $user->id]);
+        $model->default = 1;
+        
+        if ($model->save()) {
+            util::complete();
+        } else {
+            util::fail('设置失败');
+        }
+    }
+}

+ 12 - 13
biz-hd/homePageConfig/classes/HomePageDisplayClass.php

@@ -153,7 +153,7 @@ class HomePageDisplayClass
         }
         $goods = self::filterActivityGoods($data['goods'] ?? []);
         $data['goodsTotal'] = count($goods);
-        $limit = HomePageModuleClass::resolveHomeDisplayLimit($data['displayCount'] ?? 0);
+        $limit = !empty($data['expand']) ? 0 : HomePageModuleClass::resolveHomeDisplayLimit($data['displayCount'] ?? 0);
         if ($limit > 0 && count($goods) > $limit) {
             $goods = array_slice($goods, 0, $limit);
         }
@@ -162,7 +162,7 @@ class HomePageDisplayClass
     }
 
     /**
-     * 格式化热门推荐/今日上新/下拉商品:关闭时清空商品;开启时按配置解析出真实商品列表
+     * 格式化热门推荐/今日上新/下拉商品:关闭时清空商品;开启时按真实商品数量自动推导列数与展示条数
      *
      * @param int $mainId
      * @param string $moduleKey hot|new|pullGoods
@@ -174,19 +174,18 @@ class HomePageDisplayClass
         if (empty($data['enabled'])) {
             $data['goods'] = [];
             $data['goodsTotal'] = 0;
+            $data['layoutCols'] = 1;
             return $data;
         }
-        $limit = HomePageModuleClass::resolveHomeDisplayLimit($data['displayCount'] ?? 0);
-        $paged = HomePageModuleClass::resolveDisplayGoodsPaged(
-            $mainId,
-            $data['type'],
-            $data['value'],
-            $data['sort'],
-            1,
-            $limit
-        );
-        $data['goods'] = $paged['list'];
-        $data['goodsTotal'] = $paged['total'];
+        $rows = HomePageModuleClass::matchGoodsRows($mainId, $data['type'], $data['value'], $data['sort']);
+        $total = count($rows);
+        $layout = HomePageModuleClass::resolveGoodsSectionLayout($total, $moduleKey);
+        $data['layoutCols'] = $layout['layoutCols'];
+        if ($layout['displayCount'] > 0 && $layout['displayCount'] < $total) {
+            $rows = array_slice($rows, 0, $layout['displayCount']);
+        }
+        $data['goods'] = HomePageModuleClass::formatGoodsRows($rows);
+        $data['goodsTotal'] = $total;
         return $data;
     }
 

+ 65 - 37
biz-hd/homePageConfig/classes/HomePageModuleClass.php

@@ -171,6 +171,10 @@ class HomePageModuleClass
         $data = self::normalizeActivityBase($saved);
         $data['enabled'] = HomePageConfigClass::getModuleEnabled($mainId, 'seckill');
         $data['goods'] = self::normalizeSeckillGoods($saved['goods'] ?? [], $mainId, $refreshStock);
+        // 按已添加商品数量 + 展开开关动态推导列数与展示数量(不依赖商家手动配置)
+        $layout = self::resolveActivityLayout(count($data['goods']), $data['expand']);
+        $data['layoutCols'] = $layout['layoutCols'];
+        $data['displayCount'] = $layout['displayCount'];
         $data['status'] = self::calcActivityStatus($data['startTime'], $data['endTime'], $data['enabled']);
         return $data;
     }
@@ -314,6 +318,10 @@ class HomePageModuleClass
         $data = self::normalizeActivityBase($saved);
         $data['enabled'] = HomePageConfigClass::getModuleEnabled($mainId, 'groupBuy');
         $data['goods'] = self::normalizeGroupBuyGoods($saved['goods'] ?? [], $mainId, $refreshStock);
+        // 按已添加商品数量 + 展开开关动态推导列数与展示数量(不依赖商家手动配置)
+        $layout = self::resolveActivityLayout(count($data['goods']), $data['expand']);
+        $data['layoutCols'] = $layout['layoutCols'];
+        $data['displayCount'] = $layout['displayCount'];
         $data['status'] = self::calcActivityStatus($data['startTime'], $data['endTime'], $data['enabled']);
         return $data;
     }
@@ -451,8 +459,6 @@ class HomePageModuleClass
             'type' => intval($saved['type'] ?? self::LINK_GOODS),
             'value' => strval($saved['value'] ?? ''),
             'sort' => intval($saved['sort'] ?? self::SORT_PRODUCT),
-            'layoutCols' => self::normalizeLayoutCols($saved['layoutCols'] ?? null),
-            'displayCount' => intval($saved['displayCount'] ?? 0),
         ];
     }
 
@@ -464,8 +470,7 @@ class HomePageModuleClass
         $type = intval($data['type'] ?? 0);
         $value = trim(strval($data['value'] ?? ''));
         $sort = intval($data['sort'] ?? self::SORT_PRODUCT);
-        $layoutCols = intval($data['layoutCols'] ?? self::DEFAULT_LAYOUT_COLS);
-        $displayCount = self::normalizeDisplayCount($data['displayCount'] ?? 0);
+        // layoutCols/displayCount 由读取时按真实商品数量自动推导,保存时不再接收商家手动配置
         if ($name === '') {
             util::fail('请输入首页展示名称');
         }
@@ -481,21 +486,41 @@ class HomePageModuleClass
         if (!in_array($sort, [self::SORT_PRODUCT, self::SORT_SALES, self::SORT_NEW], true)) {
             util::fail('排序规则无效');
         }
-        if (!in_array($layoutCols, self::LAYOUT_COLS_OPTIONS, true)) {
-            util::fail('排列规则无效');
-        }
         self::setJson($mainId, $suffix, [
             'name' => $name,
             'type' => $type,
             'value' => $value,
             'sort' => $sort,
-            'layoutCols' => $layoutCols,
-            'displayCount' => $displayCount,
         ]);
         HomePageConfigClass::updateModuleEnabled($mainId, $moduleKey, !empty($data['enabled']) ? 1 : 0);
         return true;
     }
 
+    /**
+     * 按真实商品总数与模块类型自动推导首页列数与展示数量
+     * - 1个商品:1排1列,展示全部
+     * - 2个商品:1排2列,展示全部
+     * - ≥3个:hot/new 按1排3列且只展示3个;pullGoods 按1排2列且展示全部
+     *
+     * @param int $goodsTotal 关联配置解析出的真实商品总数
+     * @param string $moduleKey hot|new|pullGoods
+     * @return array{layoutCols:int,displayCount:int}
+     */
+    public static function resolveGoodsSectionLayout($goodsTotal, $moduleKey)
+    {
+        $goodsTotal = intval($goodsTotal);
+        if ($goodsTotal <= 1) {
+            return ['layoutCols' => 1, 'displayCount' => 0];
+        }
+        if ($goodsTotal === 2) {
+            return ['layoutCols' => 2, 'displayCount' => 0];
+        }
+        if ($moduleKey === 'pullGoods') {
+            return ['layoutCols' => 2, 'displayCount' => 0];
+        }
+        return ['layoutCols' => 3, 'displayCount' => 3];
+    }
+
     /**
      * 规范化排列规则取值,非法/缺省时回退默认值
      */
@@ -553,8 +578,9 @@ class HomePageModuleClass
             // 商品类型:保留用户选择顺序
             $goodsIds = $ids;
         } elseif ($type === self::LINK_CATEGORY) {
+            // xhGoodsCategory 无 delStatus 字段;未删除/上架状态在下方按 xhGoods 过滤
             $rows = GoodsCategoryClass::getAllByCondition(
-                ['cId' => ['in', $ids], 'delStatus' => 0],
+                ['cId' => ['in', $ids]],
                 null,
                 'gId'
             );
@@ -667,26 +693,9 @@ class HomePageModuleClass
         ];
     }
 
-    /**
-     * 规范化首页商品展示数量:0=全部,1-10=具体数量
-     *
-     * @param mixed $value
-     * @return int
-     */
-    public static function normalizeDisplayCount($value)
-    {
-        if ($value === '' || $value === null || intval($value) === 0) {
-            return 0;
-        }
-        $count = intval($value);
-        if ($count < 1 || $count > 10) {
-            util::fail('商品展示数量范围为1-10,留空表示全部');
-        }
-        return $count;
-    }
-
     /**
      * 根据 displayCount 计算首页实际截断条数:1-10 用配置值,0(全部)回退默认上限
+     * 供秒杀/团购等活动模块使用
      *
      * @param mixed $displayCount
      * @return int
@@ -721,14 +730,38 @@ class HomePageModuleClass
             'title' => strval($saved['title'] ?? ''),
             'subtitle' => strval($saved['subtitle'] ?? ''),
             'showCountdown' => !empty($saved['showCountdown']) ? 1 : 0,
-            'displayCount' => intval($saved['displayCount'] ?? 0),
+            // 商品展开:开启后首页按1排1列展示全部商品,默认关闭
+            'expand' => !empty($saved['expand']) ? 1 : 0,
             'startTime' => intval($saved['startTime'] ?? 0),
             'endTime' => intval($saved['endTime'] ?? 0),
             'desc' => strval($saved['desc'] ?? ''),
-            'layoutCols' => self::normalizeLayoutCols($saved['layoutCols'] ?? null),
         ];
     }
 
+    /**
+     * 按已添加商品数量与「商品展开」开关自动推导首页列数与展示数量
+     * - expand 开启:1排1列,展示全部
+     * - 1个商品:1排1列;2个:1排2列;≥3个:1排3列且只展示3个
+     *
+     * @param int $goodsCount 已添加商品总数(含未上架)
+     * @param int $expand 商品展开开关 0/1
+     * @return array{layoutCols:int,displayCount:int}
+     */
+    public static function resolveActivityLayout($goodsCount, $expand)
+    {
+        if (!empty($expand)) {
+            return ['layoutCols' => 1, 'displayCount' => 0];
+        }
+        $goodsCount = intval($goodsCount);
+        if ($goodsCount <= 1) {
+            return ['layoutCols' => 1, 'displayCount' => 0];
+        }
+        if ($goodsCount === 2) {
+            return ['layoutCols' => 2, 'displayCount' => 0];
+        }
+        return ['layoutCols' => 3, 'displayCount' => 3];
+    }
+
     public static function validateActivityBase($data)
     {
         $title = trim(strval($data['title'] ?? ''));
@@ -736,8 +769,7 @@ class HomePageModuleClass
         $desc = trim(strval($data['desc'] ?? ''));
         $startTime = intval($data['startTime'] ?? 0);
         $endTime = intval($data['endTime'] ?? 0);
-        $layoutCols = intval($data['layoutCols'] ?? self::DEFAULT_LAYOUT_COLS);
-        $displayCount = self::normalizeDisplayCount($data['displayCount'] ?? 0);
+        // layoutCols/displayCount 由读取时按商品数量自动推导,保存时不再接收商家手动配置
         if ($title === '') {
             util::fail('请输入活动标题');
         }
@@ -762,18 +794,14 @@ class HomePageModuleClass
         if (mb_strlen($desc) > 500) {
             util::fail('活动说明不能超过500字');
         }
-        if (!in_array($layoutCols, self::LAYOUT_COLS_OPTIONS, true)) {
-            util::fail('排列规则无效');
-        }
         return [
             'title' => $title,
             'subtitle' => $subtitle,
             'showCountdown' => !empty($data['showCountdown']) ? 1 : 0,
-            'displayCount' => $displayCount,
+            'expand' => !empty($data['expand']) ? 1 : 0,
             'startTime' => $startTime,
             'endTime' => $endTime,
             'desc' => $desc,
-            'layoutCols' => $layoutCols,
         ];
     }
 

+ 15 - 0
biz-hd/order/classes/PsExplainClass.php

@@ -0,0 +1,15 @@
+<?php
+/**
+ * 用途:花店配送方式说明项业务逻辑类
+ * 谁用:花店系统
+ * 解决什么问题:处理 xhPsExplain 说明项的增删改查
+ */
+
+namespace bizHd\order\classes;
+
+use bizHd\base\classes\BaseClass;
+
+class PsExplainClass extends BaseClass
+{
+    public static $baseFile = '\bizHd\order\models\PsExplain';
+}

+ 195 - 0
biz-hd/order/classes/PsMethodClass.php

@@ -0,0 +1,195 @@
+<?php
+/**
+ * 用途:花店配送方式配置业务逻辑类
+ * 谁用:花店系统 hdApp 配送方式设置页
+ * 解决什么问题:读写 xhPsMethod / xhPsExplain / xhPsReduceRule,与供货商 xhSh* 分离
+ */
+
+namespace bizHd\order\classes;
+
+use bizHd\base\classes\BaseClass;
+use common\components\dict;
+use Yii;
+
+class PsMethodClass extends BaseClass
+{
+    public static $baseFile = '\bizHd\order\models\PsMethod';
+
+    /**
+     * 获取所有配送方式的排序和别名信息
+     * @param int $mainId 商户ID
+     * @return array
+     */
+    public static function getSorts($mainId)
+    {
+        $list = [];
+        $aliases = [
+            0 => '送货',
+            1 => '自取',
+            2 => '跑腿',
+            3 => '物流',
+            4 => '快递'
+        ];
+        for ($style = 0; $style <= 4; $style++) {
+            $config = self::getByCondition(['mainId' => $mainId, 'style' => $style]);
+            $sort = 0;
+            $alias = $aliases[$style];
+            $status = 1;
+            if (!empty($config)) {
+                $sort = isset($config['sort']) ? (int)$config['sort'] : 0;
+                $alias = !empty($config['name']) ? $config['name'] : $aliases[$style];
+                $status = isset($config['status']) ? (int)$config['status'] : 1;
+            }
+            $list[] = [
+                'style' => $style,
+                'name' => $alias,
+                'sort' => $sort,
+                'status' => $status
+            ];
+        }
+        return $list;
+    }
+
+    /**
+     * 获取指定配送方式的配置(不存在则按字典默认初始化)
+     * @param int $shopId 门店ID
+     * @param int $mainId 商户ID
+     * @param int $style 配送类型 0-4
+     * @return array
+     */
+    public static function getConfig($shopId, $mainId, $style)
+    {
+        // 复杂分支/关键逻辑:按 mainId + style 查 xhPsMethod,无记录则写入默认配置
+        $config = self::getByCondition(['mainId' => $mainId, 'style' => $style]);
+        if (empty($config)) {
+            $defaultConfig = dict::getDict('shMethod', $style);
+
+            $initData = array_merge([
+                'shopId' => $shopId,
+                'mainId' => $mainId,
+                'style' => $style,
+            ], $defaultConfig);
+
+            $config = self::add($initData, true);
+            $config = $config->toArray();
+        }
+
+        // 关联说明项 xhPsExplain
+        $config['explains'] = PsExplainClass::getAllByCondition(
+            ['methodId' => $config['id']],
+            'sort ASC, id ASC'
+        );
+
+        // 跑腿 style=2 时加载满减规则 xhPsReduceRule
+        if ($style == 2) {
+            $config['reduceRules'] = PsReduceRuleClass::getAllByCondition(
+                ['methodId' => $config['id']],
+                'sort ASC, id ASC'
+            );
+        } else {
+            $config['reduceRules'] = [];
+        }
+
+        return $config;
+    }
+
+    /**
+     * 保存指定配送方式配置及关联说明项、满减规则
+     * @param int $shopId 门店ID
+     * @param int $mainId 商户ID
+     * @param array $data 配置数据
+     * @return bool
+     * @throws \Exception
+     */
+    public static function saveConfig($shopId, $mainId, $data)
+    {
+        $id = $data['id'] ?? 0;
+        $style = $data['style'] ?? 0;
+
+        $config = null;
+        if ($id > 0) {
+            $config = self::getByCondition(['id' => $id, 'mainId' => $mainId], true);
+        }
+
+        if (empty($config)) {
+            $config = self::getByCondition(['mainId' => $mainId, 'style' => $style], true);
+        }
+
+        $saveData = [
+            'name' => $data['name'] ?? '',
+            'status' => isset($data['status']) ? (int)$data['status'] : 1,
+            'sort' => isset($data['sort']) ? (int)$data['sort'] : 0,
+            'minAmount' => isset($data['minAmount']) ? (float)$data['minAmount'] : 0.00,
+            'minNum' => isset($data['minNum']) ? (int)$data['minNum'] : 0,
+            'unMeet' => isset($data['unMeet']) ? (int)$data['unMeet'] : 0,
+            'unMeetFee' => isset($data['unMeetFee']) ? (float)$data['unMeetFee'] : 0.00,
+            'startKm' => isset($data['startKm']) ? (float)$data['startKm'] : 0.00,
+            'startPrice' => isset($data['startPrice']) ? (float)$data['startPrice'] : 0.00,
+            'perKmPrice' => isset($data['perKmPrice']) ? (float)$data['perKmPrice'] : 0.00,
+            'changeRate' => isset($data['changeRate']) ? (float)$data['changeRate'] : 0.00,
+        ];
+
+        $transaction = Yii::$app->db->beginTransaction();
+        try {
+            if (empty($config)) {
+                $saveData['shopId'] = $shopId;
+                $saveData['mainId'] = $mainId;
+                $saveData['style'] = $style;
+                $defaultConfig = dict::getDict('shMethod', $style);
+                if (!empty($defaultConfig['originName'])) {
+                    $saveData['originName'] = $defaultConfig['originName'];
+                }
+                $config = self::add($saveData, true);
+            } else {
+                self::updateByCondition(['id' => $config->id], $saveData);
+            }
+
+            $methodId = $config->id;
+
+            // 1. 保存说明项 xhPsExplain
+            PsExplainClass::deleteByCondition(['methodId' => $methodId]);
+            if (!empty($data['explains']) && is_array($data['explains'])) {
+                $explainRows = [];
+                foreach ($data['explains'] as $index => $exp) {
+                    if (empty($exp['explain'])) {
+                        continue;
+                    }
+                    $explainRows[] = [
+                        'methodId' => $methodId,
+                        'explain' => $exp['explain'],
+                        'color' => isset($exp['color']) ? (int)$exp['color'] : 1,
+                        'fontWeight' => isset($exp['fontWeight']) ? (int)$exp['fontWeight'] : 1,
+                        'sort' => isset($exp['sort']) ? (int)$exp['sort'] : $index,
+                    ];
+                }
+                if (!empty($explainRows)) {
+                    PsExplainClass::batchAdd($explainRows);
+                }
+            }
+
+            // 2. 保存跑腿满减规则 xhPsReduceRule
+            PsReduceRuleClass::deleteByCondition(['methodId' => $methodId]);
+            if ($style == 2 && !empty($data['reduceRules']) && is_array($data['reduceRules'])) {
+                $ruleRows = [];
+                foreach ($data['reduceRules'] as $index => $rule) {
+                    $ruleRows[] = [
+                        'methodId' => $methodId,
+                        'meetNum' => isset($rule['meetNum']) ? (int)$rule['meetNum'] : 0,
+                        'meetAmount' => isset($rule['meetAmount']) ? (float)$rule['meetAmount'] : 0.00,
+                        'freeKm' => isset($rule['freeKm']) ? (float)$rule['freeKm'] : 0.00,
+                        'sort' => isset($rule['sort']) ? (int)$rule['sort'] : $index,
+                    ];
+                }
+                if (!empty($ruleRows)) {
+                    PsReduceRuleClass::batchAdd($ruleRows);
+                }
+            }
+
+            $transaction->commit();
+            return true;
+        } catch (\Exception $e) {
+            $transaction->rollBack();
+            throw $e;
+        }
+    }
+}

+ 15 - 0
biz-hd/order/classes/PsReduceRuleClass.php

@@ -0,0 +1,15 @@
+<?php
+/**
+ * 用途:花店跑腿满减规则业务逻辑类
+ * 谁用:花店系统
+ * 解决什么问题:处理 xhPsReduceRule 满减规则的增删改查
+ */
+
+namespace bizHd\order\classes;
+
+use bizHd\base\classes\BaseClass;
+
+class PsReduceRuleClass extends BaseClass
+{
+    public static $baseFile = '\bizHd\order\models\PsReduceRule';
+}

+ 22 - 0
biz-hd/order/models/PsExplain.php

@@ -0,0 +1,22 @@
+<?php
+/**
+ * 用途:花店配送方式说明项模型
+ * 谁用:花店系统(hdApp 配送方式设置页)
+ * 解决什么问题:存储每种配送方式下的说明文字、颜色、加粗等配置(xhPsExplain)
+ */
+
+namespace bizHd\order\models;
+
+use bizHd\base\models\Base;
+
+class PsExplain extends Base
+{
+    /**
+     * 返回对应的数据库表名
+     * @return string
+     */
+    public static function tableName()
+    {
+        return 'xhPsExplain';
+    }
+}

+ 22 - 0
biz-hd/order/models/PsMethod.php

@@ -0,0 +1,22 @@
+<?php
+/**
+ * 用途:花店配送方式配置主表模型
+ * 谁用:花店系统(hdApp 配送方式设置页)
+ * 解决什么问题:存储送货、自取、跑腿、快递、物流等配送方式的基础配置(xhPsMethod)
+ */
+
+namespace bizHd\order\models;
+
+use bizHd\base\models\Base;
+
+class PsMethod extends Base
+{
+    /**
+     * 返回对应的数据库表名
+     * @return string
+     */
+    public static function tableName()
+    {
+        return 'xhPsMethod';
+    }
+}

+ 22 - 0
biz-hd/order/models/PsReduceRule.php

@@ -0,0 +1,22 @@
+<?php
+/**
+ * 用途:花店跑腿满减规则模型
+ * 谁用:花店系统(hdApp 配送方式设置页)
+ * 解决什么问题:存储跑腿配送方式下的满减/免运费规则(xhPsReduceRule)
+ */
+
+namespace bizHd\order\models;
+
+use bizHd\base\models\Base;
+
+class PsReduceRule extends Base
+{
+    /**
+     * 返回对应的数据库表名
+     * @return string
+     */
+    public static function tableName()
+    {
+        return 'xhPsReduceRule';
+    }
+}

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

@@ -1060,7 +1060,7 @@ class DispatchService
                 case 'didi':
                     if(!isset($item['estimateList'])){
                         Yii::error('didi 报价数据为空:' . json_encode($item));
-                        continue;
+                        break;
                     }
 
                     $serviceType = [0 => '拼送', 1 => '直送'];

+ 80 - 0
common/models/xhUserAddress.php

@@ -0,0 +1,80 @@
+<?php
+
+namespace common\models;
+
+use Yii;
+
+/**
+ * This is the model class for table "xhUserAddress".
+ *
+ * @property string $id
+ * @property int $userId 商家id/用户id
+ * @property string $name 收货人姓名
+ * @property string $phone 手机号
+ * @property string $tag 标签
+ * @property string $lat 纬度
+ * @property string $long 经度
+ * @property string $province 省
+ * @property string $city 市
+ * @property string $dist 区县
+ * @property string $address 街道地址
+ * @property string $floor 楼号门牌号
+ * @property string $fullAddress 完整地址
+ * @property string $showAddress 地图接口显示的完整地址
+ * @property int $default 是否默认地址
+ * @property string $addTime 添加时间
+ * @property string $updateTime
+ */
+class xhUserAddress extends \yii\db\ActiveRecord
+{
+    /**
+     * {@inheritdoc}
+     */
+    public static function tableName()
+    {
+        return 'xhUserAddress';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function rules()
+    {
+        return [
+            [['userId', 'default'], 'integer'],
+            [['addTime', 'updateTime'], 'safe'],
+            [['lat', 'long'], 'string', 'max' => 20],
+            [['name', 'phone', 'tag'], 'string', 'max' => 50],
+            [['province', 'city', 'dist'], 'string', 'max' => 100],
+            [['address', 'fullAddress'], 'string', 'max' => 500],
+            [['floor'], 'string', 'max' => 50],
+            [['showAddress'], 'string', 'max' => 900],
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function attributeLabels()
+    {
+        return [
+            'id' => 'ID',
+            'userId' => 'User ID',
+            'name' => 'Name',
+            'phone' => 'Phone',
+            'tag' => 'Tag',
+            'lat' => 'Lat',
+            'long' => 'Long',
+            'province' => 'Province',
+            'city' => 'City',
+            'dist' => 'Dist',
+            'address' => 'Address',
+            'floor' => 'Floor',
+            'fullAddress' => 'Full Address',
+            'showAddress' => 'Show Address',
+            'default' => 'Default',
+            'addTime' => 'Add Time',
+            'updateTime' => 'Update Time',
+        ];
+    }
+}

+ 78 - 0
sql/20260706_redesign.sql

@@ -0,0 +1,78 @@
+-- ouyang 2026-7-17 10:11:34
+ALTER TABLE xhHd
+    add COLUMN `homeRule` tinyint(4) NOT NULL DEFAULT '0' COMMENT '上门遵循规则 0遵循总设置 1遵循自定义' after `updateTime`;
+ALTER TABLE xhCustom
+    add COLUMN `homeRule` tinyint(4) NOT NULL DEFAULT '0' COMMENT '上门遵循规则 0遵循总设置 1遵循自定义' after `updateTime`;
+
+-- ouyang 2026-7-17 11:31:52 用户地址列表
+CREATE TABLE `xhUserAddress` (
+    `id` bigint(11) NOT NULL AUTO_INCREMENT,
+    `userId` int(11) NOT NULL DEFAULT '0' COMMENT '用户id',
+    `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '收货人',
+    `phone` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '手机号',
+    `lat` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '纬度',
+    `long` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '经度',
+    `province` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '省',
+    `city` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '市',
+    `dist` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '区县',
+    `address` varchar(500) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '街道地址',
+    `floor` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '楼号门牌号',
+    `fullAddress` varchar(500) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '完整地址',
+    `showAddress` varchar(900) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '地图接口显示的完整地址',
+    `default` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否默认地址',
+    `tag` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '标签',
+    `addTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
+    `updateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`) USING BTREE,
+    KEY `idx_userId` (`userId`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户地址列表';
+
+-- ouyang 2026-7-20 15:27:39
+CREATE TABLE `xhPsExplain` (
+   `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
+   `methodId` int(11) NOT NULL COMMENT '关联 PsMethod.id',
+   `explain` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '说明文字,最多50字',
+   `color` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '颜色:1黑 2红 3蓝',
+   `fontWeight` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '字重:1常规 2加粗',
+   `sort` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '显示顺序,越小越靠前',
+   `addTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+   PRIMARY KEY (`id`),
+   KEY `idx_method_sort` (`methodId`,`sort`)
+) ENGINE=InnoDB AUTO_INCREMENT=51 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='花店配送方式说明条目';
+
+
+CREATE TABLE `xhPsMethod` (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `shopId` int(11) NOT NULL DEFAULT '0' COMMENT '门店ID',
+  `mainId` int(11) NOT NULL DEFAULT '0' COMMENT 'mainId',
+  `style` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT 'sendType 0送货 1自取 2跑腿 3物流 4快递 transType 0德邦 1顺丰 2冷链 3航空 4送货 5自取',
+  `name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名称',
+  `originName` char(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '原名',
+  `status` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '状态:1启用 0禁用',
+  `sort` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT '显示顺序',
+  `minAmount` decimal(10,2) DEFAULT '0.00' COMMENT '最低消费金额,0表示没有要求',
+  `minNum` smallint(5) DEFAULT '0' COMMENT '最低消费数量,0表示没有要求',
+  `unMeet` tinyint(3) unsigned DEFAULT '0' COMMENT '不满条件,0收运费 1收包装费 2不能下单',
+  `unMeetFee` decimal(10,2) DEFAULT '0.00' COMMENT '运费(送货) / 包装费(快递)',
+  `startKm` decimal(8,2) DEFAULT '0.00' COMMENT '起步公里数',
+  `startPrice` decimal(10,2) DEFAULT '0.00' COMMENT '起步公里内价格(元)',
+  `perKmPrice` decimal(10,2) DEFAULT '0.00' COMMENT '超出起步每公里加价(元)',
+  `changeRate` decimal(6,2) DEFAULT '0.00' COMMENT '临时涨价百分比,负数为降价',
+  `addTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_main_type` (`mainId`,`style`)
+) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='花店送货方式配置表';
+
+
+CREATE TABLE `xhPsReduceRule` (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `methodId` int(11) NOT NULL COMMENT '关联 PsMethod.id',
+  `meetNum` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT '满数量',
+  `meetAmount` decimal(10,2) NOT NULL COMMENT '满金额',
+  `freeKm` decimal(8,2) NOT NULL COMMENT 'X公里内免配送费',
+  `sort` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT '规则排序,越小越靠前',
+  `addTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_method_sort` (`methodId`,`sort`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='花店送货方式,跑腿,花材,满减规则';

+ 7 - 4
sql/20260714_goods_use_case.sql

@@ -8,16 +8,19 @@ CREATE TABLE `xhGoodsUseCase` (
   `delStatus` tinyint(4) NOT NULL DEFAULT 0 COMMENT '删除状态',
   `addTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
   `updateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-  PRIMARY KEY (`id`), KEY `mainId` (`mainId`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+  PRIMARY KEY (`id`),
+  KEY `mainId` (`mainId`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='花束场景';
 
 CREATE TABLE `xhGoodsUseCaseRelation` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `mainId` int(11) NOT NULL DEFAULT 0,
   `goodsId` int(11) NOT NULL DEFAULT 0,
   `useCaseId` int(11) NOT NULL DEFAULT 0,
-  PRIMARY KEY (`id`), UNIQUE KEY `goods_use_case` (`goodsId`,`useCaseId`), KEY `mainId` (`mainId`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `goods_use_case` (`goodsId`,`useCaseId`),
+  KEY `mainId` (`mainId`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='花束与场景关联表';