Эх сурвалжийг харах

refactor(home-page-config): 抽离首页配置表单校验

- 将首页模块、导航、轮播、活动和商品专区保存参数迁入 Form 模型统一校验
- 控制器改为加载校验表单后调用类层,类层保留门店归属、库存和 Redis 写入等业务校验
- 商城端更多商品列表增加分页查询 Form,限制模块和分页参数范围
API: 首页配置保存接口保持原路由,参数校验错误由 Form 统一返回;商城端更多商品列表继续接收 moduleKey/page/pageSize
shizhongqi 1 долоо хоног өмнө
parent
commit
28444aff3c

+ 64 - 22
app-hd/controllers/HomePageConfigController.php

@@ -10,6 +10,14 @@ use common\components\dirUtil;
 use common\components\oss;
 use common\components\stringUtil;
 use common\components\util;
+use hd\models\homePageConfig\DeleteBannerImgForm;
+use hd\models\homePageConfig\SaveBannerForm;
+use hd\models\homePageConfig\SaveGoodsSectionForm;
+use hd\models\homePageConfig\SaveGroupBuyForm;
+use hd\models\homePageConfig\SaveModulesForm;
+use hd\models\homePageConfig\SaveNavGridForm;
+use hd\models\homePageConfig\SaveSeckillForm;
+use hd\models\homePageConfig\SaveTopNavForm;
 use Yii;
 
 /**
@@ -41,8 +49,9 @@ class HomePageConfigController extends BaseController
      */
     public function actionSaveModules()
     {
-        $items = Yii::$app->request->post('items', []);
-        HomePageConfigClass::saveModules($this->mainId, $items);
+        $form = new SaveModulesForm();
+        $form->loadAndValidate();
+        HomePageConfigClass::saveModules($this->mainId, $form->getItems());
         util::complete('保存成功');
     }
 
@@ -61,8 +70,11 @@ class HomePageConfigController extends BaseController
      */
     public function actionSaveTopNav()
     {
-        $post = Yii::$app->request->post();
-        HomePageConfigClass::saveTopNav($this->mainId, $post);
+        $form = new SaveTopNavForm();
+        $data = Yii::$app->request->post();
+        $form->load($data ?: ['enabled' => 0], '');
+        $form->validateForm();
+        HomePageConfigClass::saveTopNav($this->mainId, $form->getPayload(), $form->getEnabled());
         util::complete('保存成功');
     }
 
@@ -80,8 +92,11 @@ class HomePageConfigController extends BaseController
      */
     public function actionSaveBanner()
     {
-        $post = Yii::$app->request->post();
-        HomePageConfigClass::saveBanner($this->mainId, $post);
+        $form = new SaveBannerForm();
+        $data = Yii::$app->request->post();
+        $form->load($data ?: ['enabled' => 0], '');
+        $form->validateForm();
+        HomePageConfigClass::saveBanner($this->mainId, $form->getPayload(), $form->getEnabled());
         util::complete('保存成功');
     }
 
@@ -134,16 +149,9 @@ class HomePageConfigController extends BaseController
      */
     public function actionDeleteBannerImg()
     {
-        $filePath = Yii::$app->request->post('filePath', '');
-        $filePath = ltrim(strval($filePath), '/');
-        if ($filePath === '') {
-            util::fail('参数错误');
-        }
-        $prefix = HomePageConfigClass::BANNER_OSS_ROOT . '/' . intval($this->mainId) . '/' . intval($this->shopId) . '/';
-        if (strpos($filePath, $prefix) !== 0) {
-            util::fail('无权删除该图片');
-        }
-        HomePageConfigClass::deleteBannerOssImages([$filePath]);
+        $form = new DeleteBannerImgForm();
+        $form->loadAndValidate();
+        HomePageConfigClass::deleteBannerOssImages([$form->getFilePath()]);
         util::complete('删除成功');
     }
 
@@ -155,7 +163,11 @@ class HomePageConfigController extends BaseController
 
     public function actionSaveNavGrid()
     {
-        HomePageModuleClass::saveNavGrid($this->mainId, Yii::$app->request->post());
+        $form = new SaveNavGridForm();
+        $data = Yii::$app->request->post();
+        $form->load($data ?: ['enabled' => 0], '');
+        $form->validateForm();
+        HomePageModuleClass::saveNavGrid($this->mainId, $form->getPayload(), $form->getEnabled());
         util::complete('保存成功');
     }
 
@@ -175,7 +187,16 @@ class HomePageConfigController extends BaseController
 
     public function actionSaveSeckill()
     {
-        HomePageModuleClass::saveSeckill($this->mainId, Yii::$app->request->post());
+        $form = new SaveSeckillForm();
+        $data = Yii::$app->request->post();
+        $form->load($data ?: ['enabled' => 0], '');
+        $form->validateForm();
+        HomePageModuleClass::saveSeckill(
+            $this->mainId,
+            $form->getBasePayload(),
+            $form->getGoods(),
+            $form->getEnabled()
+        );
         util::complete('保存成功');
     }
 
@@ -187,7 +208,16 @@ class HomePageConfigController extends BaseController
 
     public function actionSaveGroupBuy()
     {
-        HomePageModuleClass::saveGroupBuy($this->mainId, Yii::$app->request->post());
+        $form = new SaveGroupBuyForm();
+        $data = Yii::$app->request->post();
+        $form->load($data ?: ['enabled' => 0], '');
+        $form->validateForm();
+        HomePageModuleClass::saveGroupBuy(
+            $this->mainId,
+            $form->getBasePayload(),
+            $form->getGoods(),
+            $form->getEnabled()
+        );
         util::complete('保存成功');
     }
 
@@ -199,7 +229,11 @@ class HomePageConfigController extends BaseController
 
     public function actionSaveHot()
     {
-        HomePageModuleClass::saveGoodsSection($this->mainId, 'hot', Yii::$app->request->post());
+        $form = new SaveGoodsSectionForm();
+        $data = Yii::$app->request->post();
+        $form->load($data ?: ['enabled' => 0], '');
+        $form->validateForm();
+        HomePageModuleClass::saveGoodsSection($this->mainId, 'hot', $form->getPayload(), $form->getEnabled());
         util::complete('保存成功');
     }
 
@@ -211,7 +245,11 @@ class HomePageConfigController extends BaseController
 
     public function actionSaveNew()
     {
-        HomePageModuleClass::saveGoodsSection($this->mainId, 'new', Yii::$app->request->post());
+        $form = new SaveGoodsSectionForm();
+        $data = Yii::$app->request->post();
+        $form->load($data ?: ['enabled' => 0], '');
+        $form->validateForm();
+        HomePageModuleClass::saveGoodsSection($this->mainId, 'new', $form->getPayload(), $form->getEnabled());
         util::complete('保存成功');
     }
 
@@ -223,7 +261,11 @@ class HomePageConfigController extends BaseController
 
     public function actionSavePullGoods()
     {
-        HomePageModuleClass::saveGoodsSection($this->mainId, 'pullGoods', Yii::$app->request->post());
+        $form = new SaveGoodsSectionForm();
+        $data = Yii::$app->request->post();
+        $form->load($data ?: ['enabled' => 0], '');
+        $form->validateForm();
+        HomePageModuleClass::saveGoodsSection($this->mainId, 'pullGoods', $form->getPayload(), $form->getEnabled());
         util::complete('保存成功');
     }
 

+ 55 - 0
app-hd/models/homePageConfig/DeleteBannerImgForm.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace hd\models\homePageConfig;
+
+use bizHd\homePageConfig\classes\HomePageConfigClass;
+use hd\models\BaseForm;
+use Yii;
+
+/**
+ * 删除单张轮播图 OSS
+ */
+class DeleteBannerImgForm extends BaseForm
+{
+    public $filePath;
+
+    public function rules()
+    {
+        return [
+            ['filePath', 'required', 'message' => '参数错误'],
+            ['filePath', 'validateFilePath'],
+        ];
+    }
+
+    public function attributeLabels()
+    {
+        return [
+            'filePath' => '图片路径',
+        ];
+    }
+
+    public function validateFilePath($attribute)
+    {
+        $filePath = ltrim(strval($this->$attribute), '/');
+        $this->$attribute = $filePath;
+        if ($filePath === '') {
+            $this->addError($attribute, '参数错误');
+            return;
+        }
+        $controller = Yii::$app->controller;
+        $mainId = intval($controller->mainId ?? 0);
+        $shopId = intval($controller->shopId ?? 0);
+        $prefix = HomePageConfigClass::BANNER_OSS_ROOT . '/' . $mainId . '/' . $shopId . '/';
+        if (strpos($filePath, $prefix) !== 0) {
+            $this->addError($attribute, '无权删除该图片');
+        }
+    }
+
+    /**
+     * @return string
+     */
+    public function getFilePath()
+    {
+        return ltrim(strval($this->filePath), '/');
+    }
+}

+ 119 - 0
app-hd/models/homePageConfig/SaveBannerForm.php

@@ -0,0 +1,119 @@
+<?php
+
+namespace hd\models\homePageConfig;
+
+use bizHd\homePageConfig\classes\HomePageConfigClass;
+use hd\models\BaseForm;
+
+/**
+ * 保存首页轮播图配置
+ */
+class SaveBannerForm extends BaseForm
+{
+    public $enabled;
+    public $interval;
+    /** @var array|string */
+    public $list;
+
+    public function rules()
+    {
+        return [
+            ['enabled', 'default', 'value' => 0],
+            ['interval', 'default', 'value' => 3],
+            ['list', 'default', 'value' => []],
+            ['enabled', 'filter', 'filter' => function ($value) {
+                return !empty($value) ? 1 : 0;
+            }],
+            ['interval', 'integer', 'min' => 1, 'max' => 60,
+                'message' => '轮播间隔时间格式错误',
+                'tooSmall' => '轮播间隔时间至少1秒',
+                'tooBig' => '轮播间隔时间不能超过60秒',
+            ],
+            ['list', 'validateList'],
+        ];
+    }
+
+    public function attributeLabels()
+    {
+        return [
+            'enabled' => '开关',
+            'interval' => '轮播间隔',
+            'list' => '轮播图列表',
+        ];
+    }
+
+    public function validateList($attribute)
+    {
+        $rawList = $this->$attribute;
+        if (is_string($rawList)) {
+            $decoded = json_decode($rawList, true);
+            $rawList = is_array($decoded) ? $decoded : null;
+        }
+        if (!is_array($rawList)) {
+            $this->addError($attribute, '轮播图数据格式错误');
+            return;
+        }
+        if (count($rawList) > HomePageConfigClass::MAX_BANNER_COUNT) {
+            $this->addError($attribute, '最多可配置' . HomePageConfigClass::MAX_BANNER_COUNT . '张轮播图');
+            return;
+        }
+
+        $list = [];
+        $types = HomePageConfigClass::getBannerTypes();
+        $ossRoot = HomePageConfigClass::BANNER_OSS_ROOT . '/';
+        foreach ($rawList as $index => $item) {
+            if (!is_array($item)) {
+                $this->addError($attribute, '轮播图数据格式错误');
+                return;
+            }
+            $img = isset($item['img']) ? trim(strval($item['img'])) : '';
+            $type = isset($item['type']) ? intval($item['type']) : 0;
+            $value = isset($item['value']) ? trim(strval($item['value'])) : '';
+            $no = $index + 1;
+            if ($img === '' && $value === '') {
+                continue;
+            }
+            if ($img === '') {
+                $this->addError($attribute, "请上传轮播图{$no}的图片");
+                return;
+            }
+            if ($value === '') {
+                $this->addError($attribute, "请完善轮播图{$no}的关联内容");
+                return;
+            }
+            if (!in_array($type, $types, true)) {
+                $this->addError($attribute, "轮播图{$no}关联类型无效");
+                return;
+            }
+            if (strpos($img, $ossRoot) !== 0) {
+                $this->addError($attribute, "轮播图{$no}图片路径无效");
+                return;
+            }
+            $list[] = [
+                'img' => $img,
+                'type' => $type,
+                'value' => $value,
+            ];
+        }
+        $this->$attribute = $list;
+    }
+
+    /**
+     * @return array
+     */
+    public function getPayload()
+    {
+        return [
+            'interval' => intval($this->interval),
+            'list' => is_array($this->list) ? $this->list : [],
+        ];
+    }
+
+    /**
+     * @return int
+     */
+    public function getEnabled()
+    {
+        return !empty($this->enabled) ? 1 : 0;
+    }
+}

+ 77 - 0
app-hd/models/homePageConfig/SaveGoodsSectionForm.php

@@ -0,0 +1,77 @@
+<?php
+
+namespace hd\models\homePageConfig;
+
+use bizHd\homePageConfig\classes\HomePageModuleClass;
+use hd\models\BaseForm;
+
+/**
+ * 保存热门推荐 / 今日上新 / 下拉商品配置
+ */
+class SaveGoodsSectionForm extends BaseForm
+{
+    public $enabled;
+    public $name;
+    public $type;
+    public $value;
+    public $sort;
+
+    public function rules()
+    {
+        return [
+            ['enabled', 'default', 'value' => 0],
+            ['sort', 'default', 'value' => HomePageModuleClass::SORT_PRODUCT],
+            ['name', 'trim'],
+            ['value', 'trim'],
+            ['name', 'required', 'message' => '请输入首页展示名称'],
+            ['name', 'string', 'max' => 20, 'tooLong' => '展示名称不能超过20字'],
+            ['type', 'required', 'message' => '关联类型无效'],
+            ['type', 'in', 'range' => [
+                HomePageModuleClass::LINK_GOODS,
+                HomePageModuleClass::LINK_CATEGORY,
+                HomePageModuleClass::LINK_USE_CASE,
+            ], 'message' => '关联类型无效'],
+            ['value', 'required', 'message' => '请选择关联内容'],
+            ['sort', 'in', 'range' => [
+                HomePageModuleClass::SORT_PRODUCT,
+                HomePageModuleClass::SORT_SALES,
+                HomePageModuleClass::SORT_NEW,
+            ], 'message' => '排序规则无效'],
+            ['enabled', 'filter', 'filter' => function ($value) {
+                return !empty($value) ? 1 : 0;
+            }],
+        ];
+    }
+
+    public function attributeLabels()
+    {
+        return [
+            'enabled' => '开关',
+            'name' => '展示名称',
+            'type' => '关联类型',
+            'value' => '关联内容',
+            'sort' => '排序规则',
+        ];
+    }
+
+    /**
+     * @return array
+     */
+    public function getPayload()
+    {
+        return [
+            'name' => trim(strval($this->name)),
+            'type' => intval($this->type),
+            'value' => trim(strval($this->value)),
+            'sort' => intval($this->sort),
+        ];
+    }
+
+    /**
+     * @return int
+     */
+    public function getEnabled()
+    {
+        return !empty($this->enabled) ? 1 : 0;
+    }
+}

+ 148 - 0
app-hd/models/homePageConfig/SaveGroupBuyForm.php

@@ -0,0 +1,148 @@
+<?php
+
+namespace hd\models\homePageConfig;
+
+use hd\models\BaseForm;
+
+/**
+ * 保存团购专区配置
+ */
+class SaveGroupBuyForm extends BaseForm
+{
+    public $enabled;
+    public $title;
+    public $subtitle;
+    public $showCountdown;
+    public $expand;
+    public $startTime;
+    public $endTime;
+    public $desc;
+    /** @var array|string */
+    public $goods;
+
+    public function rules()
+    {
+        return [
+            ['enabled', 'default', 'value' => 0],
+            ['showCountdown', 'default', 'value' => 0],
+            ['expand', 'default', 'value' => 0],
+            ['goods', 'default', 'value' => []],
+            [['title', 'subtitle', 'desc'], 'trim'],
+            ['title', 'required', 'message' => '请输入活动标题'],
+            ['title', 'string', 'max' => 10, 'tooLong' => '活动标题不能超过10字'],
+            ['subtitle', 'required', 'message' => '请输入活动副标题'],
+            ['subtitle', 'string', 'max' => 20, 'tooLong' => '活动副标题不能超过20字'],
+            ['desc', 'required', 'message' => '请输入活动说明'],
+            ['desc', 'string', 'max' => 500, 'tooLong' => '活动说明不能超过500字'],
+            [['startTime', 'endTime'], 'required', 'message' => '请选择活动时间'],
+            [['startTime', 'endTime'], 'integer', 'min' => 1, 'message' => '请选择活动时间', 'tooSmall' => '请选择活动时间'],
+            ['endTime', 'compare', 'compareAttribute' => 'startTime', 'operator' => '>', 'message' => '结束时间必须大于开始时间'],
+            [['enabled', 'showCountdown', 'expand'], 'filter', 'filter' => function ($value) {
+                return !empty($value) ? 1 : 0;
+            }],
+            ['goods', 'validateGoods'],
+        ];
+    }
+
+    public function attributeLabels()
+    {
+        return [
+            'enabled' => '开关',
+            'title' => '活动标题',
+            'subtitle' => '活动副标题',
+            'showCountdown' => '倒计时',
+            'expand' => '商品展开',
+            'startTime' => '开始时间',
+            'endTime' => '结束时间',
+            'desc' => '活动说明',
+            'goods' => '团购商品',
+        ];
+    }
+
+    public function validateGoods($attribute)
+    {
+        $rawList = $this->$attribute;
+        if (is_string($rawList)) {
+            $decoded = json_decode($rawList, true);
+            $rawList = is_array($decoded) ? $decoded : null;
+        }
+        if (!is_array($rawList)) {
+            $this->addError($attribute, '团购商品数据格式错误');
+            return;
+        }
+
+        $list = [];
+        foreach ($rawList as $index => $item) {
+            if (!is_array($item)) {
+                continue;
+            }
+            $no = $index + 1;
+            $goodsId = intval($item['goodsId'] ?? 0);
+            $price = floatval($item['price'] ?? 0);
+            $stock = intval($item['stock'] ?? 0);
+            $limit = intval($item['limit'] ?? 0);
+            $groupSize = intval($item['groupSize'] ?? 3);
+            if (!in_array($groupSize, [2, 3, 5], true)) {
+                $this->addError($attribute, "第{$no}个成团人数无效");
+                return;
+            }
+            if ($goodsId <= 0 || $price <= 0 || $stock <= 0 || $limit <= 0) {
+                $this->addError($attribute, "请完善第{$no}个团购商品");
+                return;
+            }
+            $virtualGroup = !empty($item['virtualGroup']) ? 1 : 0;
+            $virtualMinutes = intval($item['virtualMinutes'] ?? 0);
+            if ($virtualGroup && $virtualMinutes <= 0) {
+                $this->addError($attribute, "请填写第{$no}个虚拟成团时间");
+                return;
+            }
+            $list[] = [
+                'goodsId' => $goodsId,
+                'price' => $price,
+                'stock' => $stock,
+                'limit' => $limit,
+                'groupSize' => $groupSize,
+                'virtualGroup' => $virtualGroup,
+                'virtualMinutes' => $virtualMinutes,
+                'autoRefund' => !empty($item['autoRefund']) ? 1 : 0,
+                'status' => !empty($item['status']) ? 1 : 0,
+                'name' => strval($item['name'] ?? ''),
+                'cover' => strval($item['cover'] ?? ''),
+                'originPrice' => floatval($item['originPrice'] ?? 0),
+            ];
+        }
+        $this->$attribute = $list;
+    }
+
+    /**
+     * @return array
+     */
+    public function getBasePayload()
+    {
+        return [
+            'title' => trim(strval($this->title)),
+            'subtitle' => trim(strval($this->subtitle)),
+            'showCountdown' => !empty($this->showCountdown) ? 1 : 0,
+            'expand' => !empty($this->expand) ? 1 : 0,
+            'startTime' => intval($this->startTime),
+            'endTime' => intval($this->endTime),
+            'desc' => trim(strval($this->desc)),
+        ];
+    }
+
+    /**
+     * @return array
+     */
+    public function getGoods()
+    {
+        return is_array($this->goods) ? $this->goods : [];
+    }
+
+    /**
+     * @return int
+     */
+    public function getEnabled()
+    {
+        return !empty($this->enabled) ? 1 : 0;
+    }
+}

+ 83 - 0
app-hd/models/homePageConfig/SaveModulesForm.php

@@ -0,0 +1,83 @@
+<?php
+
+namespace hd\models\homePageConfig;
+
+use bizHd\homePageConfig\classes\HomePageConfigClass;
+use hd\models\BaseForm;
+
+/**
+ * 保存首页模块顺序与开关
+ */
+class SaveModulesForm extends BaseForm
+{
+    /** @var array */
+    public $items;
+
+    public function rules()
+    {
+        return [
+            ['items', 'required', 'message' => '请提交配置项'],
+            ['items', 'validateItems'],
+        ];
+    }
+
+    public function attributeLabels()
+    {
+        return [
+            'items' => '配置项',
+        ];
+    }
+
+    public function validateItems($attribute)
+    {
+        $items = $this->$attribute;
+        if (is_string($items)) {
+            $items = json_decode($items, true);
+        }
+        if (!is_array($items)) {
+            $this->addError($attribute, '配置项格式错误');
+            return;
+        }
+        $defaultKeys = HomePageConfigClass::DEFAULT_KEYS;
+        if (count($items) !== count($defaultKeys)) {
+            $this->addError($attribute, '配置项数量不正确');
+            return;
+        }
+
+        $result = [];
+        $used = [];
+        foreach ($items as $item) {
+            if (!is_array($item)) {
+                $this->addError($attribute, '配置项格式错误');
+                return;
+            }
+            $key = isset($item['key']) ? strval($item['key']) : '';
+            if ($key === '' || !in_array($key, $defaultKeys, true)) {
+                $this->addError($attribute, '存在无效配置项');
+                return;
+            }
+            if (isset($used[$key])) {
+                $this->addError($attribute, '配置项重复');
+                return;
+            }
+            $result[] = [
+                'key' => $key,
+                'enabled' => !empty($item['enabled']) ? 1 : 0,
+            ];
+            $used[$key] = true;
+        }
+        if (count($used) !== count($defaultKeys)) {
+            $this->addError($attribute, '配置项不完整');
+            return;
+        }
+        $this->$attribute = $result;
+    }
+
+    /**
+     * @return array
+     */
+    public function getItems()
+    {
+        return is_array($this->items) ? $this->items : [];
+    }
+}

+ 106 - 0
app-hd/models/homePageConfig/SaveNavGridForm.php

@@ -0,0 +1,106 @@
+<?php
+
+namespace hd\models\homePageConfig;
+
+use bizHd\homePageConfig\classes\HomePageModuleClass;
+use hd\models\BaseForm;
+
+/**
+ * 保存金刚区导航配置
+ */
+class SaveNavGridForm extends BaseForm
+{
+    public $enabled;
+    public $cols;
+    /** @var array|string */
+    public $list;
+
+    public function rules()
+    {
+        return [
+            ['enabled', 'default', 'value' => 0],
+            ['cols', 'default', 'value' => 5],
+            ['list', 'default', 'value' => []],
+            ['enabled', 'filter', 'filter' => function ($value) {
+                return !empty($value) ? 1 : 0;
+            }],
+            ['cols', 'in', 'range' => [4, 5], 'message' => '排列规则无效'],
+            ['list', 'validateList'],
+        ];
+    }
+
+    public function attributeLabels()
+    {
+        return [
+            'enabled' => '开关',
+            'cols' => '排列规则',
+            'list' => '导航列表',
+        ];
+    }
+
+    public function validateList($attribute)
+    {
+        $rawList = $this->$attribute;
+        if (is_string($rawList)) {
+            $decoded = json_decode($rawList, true);
+            $rawList = is_array($decoded) ? $decoded : null;
+        }
+        if (!is_array($rawList)) {
+            $this->addError($attribute, '导航数据格式错误');
+            return;
+        }
+        if (count($rawList) > HomePageModuleClass::MAX_NAV_COUNT) {
+            $this->addError($attribute, '最多可配置' . HomePageModuleClass::MAX_NAV_COUNT . '个导航');
+            return;
+        }
+
+        $list = [];
+        foreach ($rawList as $index => $item) {
+            if (!is_array($item)) {
+                continue;
+            }
+            $name = trim(strval($item['name'] ?? ''));
+            $icon = trim(strval($item['icon'] ?? ''));
+            $type = intval($item['type'] ?? 0);
+            $value = trim(strval($item['value'] ?? ''));
+            $no = $index + 1;
+            if ($name === '' || $icon === '' || $value === '') {
+                $this->addError($attribute, "请完善第{$no}个导航项");
+                return;
+            }
+            if (!in_array($type, [HomePageModuleClass::LINK_CATEGORY, HomePageModuleClass::LINK_USE_CASE], true)) {
+                $this->addError($attribute, "第{$no}个导航关联类型无效");
+                return;
+            }
+            $list[] = [
+                'id' => strval($item['id'] ?? ('nav_' . ($index + 1))),
+                'name' => mb_substr($name, 0, 20),
+                'icon' => $icon,
+                'iconType' => intval($item['iconType'] ?? 1) === 2 ? 2 : 1,
+                'enabled' => !empty($item['enabled']) ? 1 : 0,
+                'type' => $type,
+                'value' => $value,
+            ];
+        }
+        $this->$attribute = $list;
+    }
+
+    /**
+     * @return array
+     */
+    public function getPayload()
+    {
+        return [
+            'cols' => intval($this->cols),
+            'list' => is_array($this->list) ? $this->list : [],
+        ];
+    }
+
+    /**
+     * @return int
+     */
+    public function getEnabled()
+    {
+        return !empty($this->enabled) ? 1 : 0;
+    }
+}

+ 146 - 0
app-hd/models/homePageConfig/SaveSeckillForm.php

@@ -0,0 +1,146 @@
+<?php
+
+namespace hd\models\homePageConfig;
+
+use hd\models\BaseForm;
+
+/**
+ * 保存秒杀专区配置
+ */
+class SaveSeckillForm extends BaseForm
+{
+    public $enabled;
+    public $title;
+    public $subtitle;
+    public $showCountdown;
+    public $expand;
+    public $startTime;
+    public $endTime;
+    public $desc;
+    /** @var array|string */
+    public $goods;
+
+    public function rules()
+    {
+        return [
+            ['enabled', 'default', 'value' => 0],
+            ['showCountdown', 'default', 'value' => 0],
+            ['expand', 'default', 'value' => 0],
+            ['goods', 'default', 'value' => []],
+            [['title', 'subtitle', 'desc'], 'trim'],
+            ['title', 'required', 'message' => '请输入活动标题'],
+            ['title', 'string', 'max' => 10, 'tooLong' => '活动标题不能超过10字'],
+            ['subtitle', 'required', 'message' => '请输入活动副标题'],
+            ['subtitle', 'string', 'max' => 20, 'tooLong' => '活动副标题不能超过20字'],
+            ['desc', 'required', 'message' => '请输入活动说明'],
+            ['desc', 'string', 'max' => 500, 'tooLong' => '活动说明不能超过500字'],
+            [['startTime', 'endTime'], 'required', 'message' => '请选择活动时间'],
+            [['startTime', 'endTime'], 'integer', 'min' => 1, 'message' => '请选择活动时间', 'tooSmall' => '请选择活动时间'],
+            ['endTime', 'compare', 'compareAttribute' => 'startTime', 'operator' => '>', 'message' => '结束时间必须大于开始时间'],
+            [['enabled', 'showCountdown', 'expand'], 'filter', 'filter' => function ($value) {
+                return !empty($value) ? 1 : 0;
+            }],
+            ['goods', 'validateGoods'],
+        ];
+    }
+
+    public function attributeLabels()
+    {
+        return [
+            'enabled' => '开关',
+            'title' => '活动标题',
+            'subtitle' => '活动副标题',
+            'showCountdown' => '倒计时',
+            'expand' => '商品展开',
+            'startTime' => '开始时间',
+            'endTime' => '结束时间',
+            'desc' => '活动说明',
+            'goods' => '秒杀商品',
+        ];
+    }
+
+    public function validateGoods($attribute)
+    {
+        $rawList = $this->$attribute;
+        if (is_string($rawList)) {
+            $decoded = json_decode($rawList, true);
+            $rawList = is_array($decoded) ? $decoded : null;
+        }
+        if (!is_array($rawList)) {
+            $this->addError($attribute, '秒杀商品数据格式错误');
+            return;
+        }
+
+        $list = [];
+        foreach ($rawList as $index => $item) {
+            if (!is_array($item)) {
+                continue;
+            }
+            $no = $index + 1;
+            $goodsId = intval($item['goodsId'] ?? 0);
+            $price = floatval($item['price'] ?? 0);
+            $stock = intval($item['stock'] ?? 0);
+            $limit = intval($item['limit'] ?? 0);
+            if ($goodsId <= 0) {
+                $this->addError($attribute, "请选择第{$no}个秒杀商品");
+                return;
+            }
+            if ($price <= 0) {
+                $this->addError($attribute, "请填写第{$no}个秒杀价格");
+                return;
+            }
+            if ($stock <= 0) {
+                $this->addError($attribute, "请填写第{$no}个秒杀库存");
+                return;
+            }
+            if ($limit <= 0) {
+                $this->addError($attribute, "请填写第{$no}个单人限购");
+                return;
+            }
+            $list[] = [
+                'goodsId' => $goodsId,
+                'price' => $price,
+                'stock' => $stock,
+                'limit' => $limit,
+                'status' => !empty($item['status']) ? 1 : 0,
+                'name' => strval($item['name'] ?? ''),
+                'cover' => strval($item['cover'] ?? ''),
+                'originPrice' => floatval($item['originPrice'] ?? 0),
+            ];
+        }
+        $this->$attribute = $list;
+    }
+
+    /**
+     * 活动基础字段
+     * @return array
+     */
+    public function getBasePayload()
+    {
+        return [
+            'title' => trim(strval($this->title)),
+            'subtitle' => trim(strval($this->subtitle)),
+            'showCountdown' => !empty($this->showCountdown) ? 1 : 0,
+            'expand' => !empty($this->expand) ? 1 : 0,
+            'startTime' => intval($this->startTime),
+            'endTime' => intval($this->endTime),
+            'desc' => trim(strval($this->desc)),
+        ];
+    }
+
+    /**
+     * @return array
+     */
+    public function getGoods()
+    {
+        return is_array($this->goods) ? $this->goods : [];
+    }
+
+    /**
+     * @return int
+     */
+    public function getEnabled()
+    {
+        return !empty($this->enabled) ? 1 : 0;
+    }
+}

+ 70 - 0
app-hd/models/homePageConfig/SaveTopNavForm.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace hd\models\homePageConfig;
+
+use bizHd\homePageConfig\classes\HomePageConfigClass;
+use hd\models\BaseForm;
+
+/**
+ * 保存顶部导航与搜索配置
+ */
+class SaveTopNavForm extends BaseForm
+{
+    public $enabled;
+    public $placeholder;
+    public $searchBtnColor;
+    public $searchFontColor;
+    public $customerServiceEnabled;
+
+    public function rules()
+    {
+        $defaults = HomePageConfigClass::getDefaultTopNav();
+        return [
+            ['enabled', 'default', 'value' => 0],
+            ['placeholder', 'default', 'value' => $defaults['placeholder']],
+            ['searchBtnColor', 'default', 'value' => $defaults['searchBtnColor']],
+            ['searchFontColor', 'default', 'value' => $defaults['searchFontColor']],
+            ['customerServiceEnabled', 'default', 'value' => 0],
+            ['placeholder', 'required', 'message' => '请输入搜索占位文案'],
+            ['placeholder', 'trim'],
+            ['placeholder', 'string', 'max' => 50, 'tooLong' => '搜索占位文案不能超过50字'],
+            [['searchBtnColor', 'searchFontColor'], 'string'],
+            [['enabled', 'customerServiceEnabled'], 'filter', 'filter' => function ($value) {
+                return !empty($value) ? 1 : 0;
+            }],
+        ];
+    }
+
+    public function attributeLabels()
+    {
+        return [
+            'enabled' => '开关',
+            'placeholder' => '搜索占位文案',
+            'searchBtnColor' => '搜索按钮颜色',
+            'searchFontColor' => '搜索字体颜色',
+            'customerServiceEnabled' => '客服开关',
+        ];
+    }
+
+    /**
+     * 写入 Redis 的样式字段(不含模块开关)
+     * @return array
+     */
+    public function getPayload()
+    {
+        return [
+            'placeholder' => trim(strval($this->placeholder)),
+            'searchBtnColor' => strval($this->searchBtnColor),
+            'searchFontColor' => strval($this->searchFontColor),
+            'customerServiceEnabled' => !empty($this->customerServiceEnabled) ? 1 : 0,
+        ];
+    }
+
+    /**
+     * @return int
+     */
+    public function getEnabled()
+    {
+        return !empty($this->enabled) ? 1 : 0;
+    }
+}

+ 6 - 8
app-mall/controllers/HomePageConfigController.php

@@ -6,7 +6,7 @@ use bizHd\homePageConfig\classes\HomePageConfigClass;
 use bizHd\homePageConfig\classes\HomePageDisplayClass;
 use bizHd\homePageConfig\classes\HomePageModuleClass;
 use common\components\util;
-use Yii;
+use mall\models\homePageConfig\GetSectionGoodsForm;
 
 /**
  * 商城端首页配置读取接口
@@ -77,15 +77,13 @@ class HomePageConfigController extends BaseController
      */
     public function actionGetSectionGoods()
     {
-        $get = Yii::$app->request->get();
-        $moduleKey = strval($get['moduleKey'] ?? '');
-        $page = intval($get['page'] ?? 1);
-        $pageSize = intval($get['pageSize'] ?? 10);
+        $form = new GetSectionGoodsForm();
+        $form->loadAndValidate();
         util::success(HomePageDisplayClass::getSectionGoodsPage(
             $this->requireMainId(),
-            $moduleKey,
-            $page,
-            $pageSize
+            $form->moduleKey,
+            (int)$form->page,
+            (int)$form->pageSize
         ));
     }
 

+ 66 - 0
app-mall/models/BaseForm.php

@@ -0,0 +1,66 @@
+<?php
+namespace mall\models;
+
+use Yii;
+use yii\base\Model;
+use common\components\util;
+
+/**
+ * 管理员表单基类
+ */
+class BaseForm extends Model
+{
+    /**
+     * 验证表单数据并返回结果
+     *
+     * @return bool 验证是否通过
+     */
+    public function validateForm()
+    {
+        if (!$this->validate()) {
+            $errors = $this->getFirstErrors();
+            $message = reset($errors);
+            util::fail($message);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 自动加载并验证请求数据
+     *
+     * 自动检测请求类型(GET 或 POST),加载对应的数据并进行验证。
+     * 相比手动指定 $method 参数,使用 $request->isGet 和 $request->isPost
+     * 是 Yii 框架推荐的做法,更符合框架风格。
+     *
+     * 使用示例:
+     * - POST 请求自动加载:$form->loadAndValidate();
+     * - GET 请求自动加载:$form->loadAndValidate();
+     *
+     * @param string $formName 表单名称,用于嵌套数据
+     * @return bool 验证成功返回 true,失败自动输出错误响应
+     * @throws \yii\base\InvalidConfigException
+     */
+    public function loadAndValidate($formName = '')
+    {
+        $request = Yii::$app->request;
+
+        // ✨ 优化:使用 Yii 提供的方法检查请求类型
+        // 相比手动字符串比较 ($method === 'get'),这种方式更符合框架风格
+        if ($request->isGet) {
+            $data = $request->get();
+        } elseif ($request->isPost) {
+            $data = $request->post();
+        } else {
+            util::fail('不支持的请求方法');
+            return false;
+        }
+
+        if (!$this->load($data, '')) {
+            util::fail('数据加载失败');
+            return false;
+        }
+
+        return $this->validateForm();
+    }
+}

+ 43 - 0
app-mall/models/homePageConfig/GetSectionGoodsForm.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace mall\models\homePageConfig;
+
+use mall\models\BaseForm;
+
+/**
+ * 首页模块「更多」商品列表分页
+ */
+class GetSectionGoodsForm extends BaseForm
+{
+    public $moduleKey;
+    public $page;
+    public $pageSize;
+
+    /** 合法模块 key */
+    const MODULE_KEYS = ['seckill', 'groupBuy', 'hot', 'new', 'pullGoods'];
+
+    public function rules()
+    {
+        return [
+            ['moduleKey', 'required', 'message' => '请选择模块'],
+            ['moduleKey', 'in', 'range' => self::MODULE_KEYS, 'message' => '无效模块'],
+            ['page', 'default', 'value' => 1],
+            ['pageSize', 'default', 'value' => 10],
+            ['page', 'integer', 'min' => 1, 'message' => '页码格式错误', 'tooSmall' => '页码格式错误'],
+            ['pageSize', 'integer', 'min' => 1, 'max' => 100,
+                'message' => '每页数量格式错误',
+                'tooSmall' => '每页数量格式错误',
+                'tooBig' => '每页数量不能超过100',
+            ],
+        ];
+    }
+
+    public function attributeLabels()
+    {
+        return [
+            'moduleKey' => '模块',
+            'page' => '页码',
+            'pageSize' => '每页数量',
+        ];
+    }
+}

+ 21 - 101
biz-hd/homePageConfig/classes/HomePageConfigClass.php

@@ -179,6 +179,7 @@ class HomePageConfigClass
 
     /**
      * 保存模块顺序与开关(合并写入同一 Redis key)
+     * 请求形态校验由 SaveModulesForm 完成;$items 应为已规范化的 [{key,enabled}, ...]
      *
      * @param int $mainId
      * @param array $items [{key,enabled}, ...]
@@ -190,33 +191,13 @@ class HomePageConfigClass
         if ($mainId <= 0) {
             util::fail('无效门店');
         }
-        if (!is_array($items) || count($items) !== count(self::DEFAULT_KEYS)) {
-            util::fail('配置项数量不正确');
-        }
-
-        $result = [];
-        $used = [];
-        foreach ($items as $item) {
-            $key = isset($item['key']) ? strval($item['key']) : '';
-            if ($key === '' || !in_array($key, self::DEFAULT_KEYS, true)) {
-                util::fail('存在无效配置项');
-            }
-            if (isset($used[$key])) {
-                util::fail('配置项重复');
-            }
-            $result[] = [
-                'key' => $key,
-                'enabled' => !empty($item['enabled']) ? 1 : 0,
-            ];
-            $used[$key] = true;
-        }
-        if (count($used) !== count(self::DEFAULT_KEYS)) {
-            util::fail('配置项不完整');
+        if (!is_array($items)) {
+            util::fail('配置项格式错误');
         }
 
         Yii::$app->redis->executeCommand('SET', [
             self::getModulesKey($mainId),
-            json_encode($result, JSON_UNESCAPED_UNICODE),
+            json_encode(array_values($items), JSON_UNESCAPED_UNICODE),
         ]);
         return true;
     }
@@ -302,38 +283,23 @@ class HomePageConfigClass
 
     /**
      * 保存顶部导航与搜索配置(不含门店名称)
-     * 同时把模块开关写回 modules Redis,与门店首页配置列表保持同步
+     * 请求形态校验由 SaveTopNavForm 完成;$payload 为已规范化样式字段
      *
      * @param int $mainId
-     * @param array $data
+     * @param array $payload {placeholder,searchBtnColor,searchFontColor,customerServiceEnabled}
+     * @param int $enabled 模块开关
      * @return bool
      */
-    public static function saveTopNav($mainId, $data)
+    public static function saveTopNav($mainId, $payload, $enabled = 0)
     {
         $mainId = intval($mainId);
         if ($mainId <= 0) {
             util::fail('无效门店');
         }
-        if (!is_array($data)) {
+        if (!is_array($payload)) {
             util::fail('参数错误');
         }
 
-        $defaults = self::getDefaultTopNav();
-        $enabled = !empty($data['enabled']) ? 1 : 0;
-        // 详情 Redis 只存样式类字段;开关统一落 modules
-        $payload = [
-            'placeholder' => isset($data['placeholder']) ? trim(strval($data['placeholder'])) : $defaults['placeholder'],
-            'searchBtnColor' => isset($data['searchBtnColor']) ? strval($data['searchBtnColor']) : $defaults['searchBtnColor'],
-            'searchFontColor' => isset($data['searchFontColor']) ? strval($data['searchFontColor']) : $defaults['searchFontColor'],
-            'customerServiceEnabled' => !empty($data['customerServiceEnabled']) ? 1 : 0,
-        ];
-        if ($payload['placeholder'] === '') {
-            util::fail('请输入搜索占位文案');
-        }
-        if (mb_strlen($payload['placeholder']) > 50) {
-            util::fail('搜索占位文案不能超过50字');
-        }
-
         Yii::$app->redis->executeCommand('SET', [
             self::getTopNavKey($mainId),
             json_encode($payload, JSON_UNESCAPED_UNICODE),
@@ -406,73 +372,25 @@ class HomePageConfigClass
 
     /**
      * 保存首页轮播图配置,并同步模块开关;会删除本次被移除的 OSS 图片
+     * 请求形态校验由 SaveBannerForm 完成;$payload 为已规范化 {interval,list}
      *
      * @param int $mainId
-     * @param array $data
+     * @param array $payload {interval, list:[{img,type,value}]}
+     * @param int $enabled 模块开关
      * @return bool
      */
-    public static function saveBanner($mainId, $data)
+    public static function saveBanner($mainId, $payload, $enabled = 0)
     {
         $mainId = intval($mainId);
         if ($mainId <= 0) {
             util::fail('无效门店');
         }
-        if (!is_array($data)) {
+        if (!is_array($payload)) {
             util::fail('参数错误');
         }
 
-        $enabled = !empty($data['enabled']) ? 1 : 0;
-        $interval = isset($data['interval']) ? intval($data['interval']) : 3;
-        if ($interval < 1) {
-            util::fail('轮播间隔时间至少1秒');
-        }
-        if ($interval > 60) {
-            util::fail('轮播间隔时间不能超过60秒');
-        }
-
-        $rawList = isset($data['list']) ? $data['list'] : [];
-        if (is_string($rawList)) {
-            $decoded = json_decode($rawList, true);
-            $rawList = is_array($decoded) ? $decoded : [];
-        }
-        if (!is_array($rawList)) {
-            util::fail('轮播图数据格式错误');
-        }
-        if (count($rawList) > self::MAX_BANNER_COUNT) {
-            util::fail('最多可配置' . self::MAX_BANNER_COUNT . '张轮播图');
-        }
-
-        $list = [];
-        foreach ($rawList as $index => $item) {
-            if (!is_array($item)) {
-                util::fail('轮播图数据格式错误');
-            }
-            $img = isset($item['img']) ? trim(strval($item['img'])) : '';
-            $type = isset($item['type']) ? intval($item['type']) : 0;
-            $value = isset($item['value']) ? trim(strval($item['value'])) : '';
-            $no = $index + 1;
-            if ($img === '' && $value === '') {
-                continue;
-            }
-            if ($img === '') {
-                util::fail("请上传轮播图{$no}的图片");
-            }
-            if ($value === '') {
-                util::fail("请完善轮播图{$no}的关联内容");
-            }
-            if (!in_array($type, self::getBannerTypes(), true)) {
-                util::fail("轮播图{$no}关联类型无效");
-            }
-            // 仅允许本项目轮播图目录下的相对路径,防止误删其它资源
-            if (strpos($img, self::BANNER_OSS_ROOT . '/') !== 0) {
-                util::fail("轮播图{$no}图片路径无效");
-            }
-            $list[] = [
-                'img' => $img,
-                'type' => $type,
-                'value' => $value,
-            ];
-        }
+        $interval = isset($payload['interval']) ? intval($payload['interval']) : 3;
+        $list = isset($payload['list']) && is_array($payload['list']) ? $payload['list'] : [];
 
         // 对比旧配置,删除本次不再使用的 OSS 图片
         $old = self::getBanner($mainId);
@@ -484,7 +402,9 @@ class HomePageConfigClass
         }
         $newImgs = [];
         foreach ($list as $newItem) {
-            $newImgs[$newItem['img']] = true;
+            if (!empty($newItem['img'])) {
+                $newImgs[$newItem['img']] = true;
+            }
         }
         $removeImgs = [];
         foreach ($oldImgs as $imgPath => $flag) {
@@ -494,13 +414,13 @@ class HomePageConfigClass
         }
         self::deleteBannerOssImages($removeImgs);
 
-        $payload = [
+        $savePayload = [
             'interval' => $interval,
             'list' => $list,
         ];
         Yii::$app->redis->executeCommand('SET', [
             self::getBannerKey($mainId),
-            json_encode($payload, JSON_UNESCAPED_UNICODE),
+            json_encode($savePayload, JSON_UNESCAPED_UNICODE),
         ]);
         self::updateModuleEnabled($mainId, 'banner', $enabled);
         return true;

+ 2 - 3
biz-hd/homePageConfig/classes/HomePageDisplayClass.php

@@ -207,6 +207,7 @@ class HomePageDisplayClass
         if ($mainId <= 0) {
             util::fail('无效门店');
         }
+        // moduleKey / page / pageSize 形态校验由 GetSectionGoodsForm 完成
 
         $title = '';
         $list = [];
@@ -228,7 +229,7 @@ class HomePageDisplayClass
             $all = empty($data['enabled']) ? [] : self::filterActivityGoods($data['goods'] ?? []);
             $total = count($all);
             $list = array_slice($all, ($page - 1) * $pageSize, $pageSize);
-        } elseif (in_array($moduleKey, ['hot', 'new', 'pullGoods'], true)) {
+        } else {
             $data = HomePageModuleClass::getGoodsSection($mainId, $moduleKey);
             $title = strval($data['name'] ?? '');
             if (empty($data['enabled'])) {
@@ -246,8 +247,6 @@ class HomePageDisplayClass
                 $list = $paged['list'];
                 $total = $paged['total'];
             }
-        } else {
-            util::fail('无效模块');
         }
 
         $totalPage = $pageSize > 0 ? intval(ceil($total / $pageSize)) : 0;

+ 66 - 133
biz-hd/homePageConfig/classes/HomePageModuleClass.php

@@ -121,47 +121,25 @@ class HomePageModuleClass
         ];
     }
 
-    public static function saveNavGrid($mainId, $data)
+    /**
+     * 保存金刚区导航;请求形态校验由 SaveNavGridForm 完成
+     *
+     * @param int $mainId
+     * @param array $payload {cols, list}
+     * @param int $enabled
+     * @return bool
+     */
+    public static function saveNavGrid($mainId, $payload, $enabled = 0)
     {
         $mainId = intval($mainId);
         if ($mainId <= 0) {
             util::fail('无效门店');
         }
-        $enabled = !empty($data['enabled']) ? 1 : 0;
-        $cols = isset($data['cols']) ? intval($data['cols']) : 5;
-        if (!in_array($cols, [4, 5], true)) {
-            util::fail('排列规则无效');
-        }
-        $rawList = self::decodeList($data['list'] ?? []);
-        if (count($rawList) > self::MAX_NAV_COUNT) {
-            util::fail('最多可配置' . self::MAX_NAV_COUNT . '个导航');
-        }
-        $list = [];
-        foreach ($rawList as $index => $item) {
-            if (!is_array($item)) {
-                continue;
-            }
-            $name = trim(strval($item['name'] ?? ''));
-            $icon = trim(strval($item['icon'] ?? ''));
-            $type = intval($item['type'] ?? 0);
-            $value = trim(strval($item['value'] ?? ''));
-            $no = $index + 1;
-            if ($name === '' || $icon === '' || $value === '') {
-                util::fail("请完善第{$no}个导航项");
-            }
-            if (!in_array($type, [self::LINK_CATEGORY, self::LINK_USE_CASE], true)) {
-                util::fail("第{$no}个导航关联类型无效");
-            }
-            $list[] = [
-                'id' => strval($item['id'] ?? ('nav_' . ($index + 1))),
-                'name' => mb_substr($name, 0, 20),
-                'icon' => $icon,
-                'iconType' => intval($item['iconType'] ?? 1) === 2 ? 2 : 1,
-                'enabled' => !empty($item['enabled']) ? 1 : 0,
-                'type' => $type,
-                'value' => $value,
-            ];
+        if (!is_array($payload)) {
+            util::fail('参数错误');
         }
+        $cols = isset($payload['cols']) ? intval($payload['cols']) : 5;
+        $list = isset($payload['list']) && is_array($payload['list']) ? $payload['list'] : [];
         self::setJson($mainId, self::REDIS_NAV_GRID, ['cols' => $cols, 'list' => $list]);
         HomePageConfigClass::updateModuleEnabled($mainId, 'navGrid', $enabled);
         return true;
@@ -186,21 +164,33 @@ class HomePageModuleClass
         return $data;
     }
 
-    public static function saveSeckill($mainId, $data)
+    /**
+     * 保存秒杀专区;请求形态校验由 SaveSeckillForm 完成,此处做商品归属与库存业务校验
+     *
+     * @param int $mainId
+     * @param array $base 已校验的活动基础字段
+     * @param array $goods 已校验的商品列表(形态)
+     * @param int $enabled
+     * @return bool
+     */
+    public static function saveSeckill($mainId, $base, $goods = [], $enabled = 0)
     {
         $mainId = intval($mainId);
         if ($mainId <= 0) {
             util::fail('无效门店');
         }
-        $base = self::validateActivityBase($data);
-        $goods = self::validateSeckillGoods($mainId, self::decodeList($data['goods'] ?? []));
+        if (!is_array($base)) {
+            util::fail('参数错误');
+        }
+        $goods = self::validateSeckillGoods($mainId, is_array($goods) ? $goods : []);
         self::setJson($mainId, self::REDIS_SECKILL, array_merge($base, ['goods' => $goods]));
-        HomePageConfigClass::updateModuleEnabled($mainId, 'seckill', !empty($data['enabled']) ? 1 : 0);
+        HomePageConfigClass::updateModuleEnabled($mainId, 'seckill', $enabled);
         return true;
     }
 
     /**
-     * 校验并规范化秒杀商品;秒杀库存不得超过实际商品库存
+     * 业务校验并规范化秒杀商品:归属当前门店,秒杀库存不得超过实际商品库存
+     * 字段形态校验由 SaveSeckillForm 完成
      */
     public static function validateSeckillGoods($mainId, $rawList)
     {
@@ -214,18 +204,6 @@ class HomePageModuleClass
             $price = floatval($item['price'] ?? 0);
             $stock = intval($item['stock'] ?? 0);
             $limit = intval($item['limit'] ?? 0);
-            if ($goodsId <= 0) {
-                util::fail("请选择第{$no}个秒杀商品");
-            }
-            if ($price <= 0) {
-                util::fail("请填写第{$no}个秒杀价格");
-            }
-            if ($stock <= 0) {
-                util::fail("请填写第{$no}个秒杀库存");
-            }
-            if ($limit <= 0) {
-                util::fail("请填写第{$no}个单人限购");
-            }
             $goods = GoodsClass::getById($goodsId, true);
             if (empty($goods) || intval($goods->mainId ?? 0) !== intval($mainId)) {
                 util::fail("第{$no}个秒杀商品无效");
@@ -441,19 +419,34 @@ class HomePageModuleClass
         return $data;
     }
 
-    public static function saveGroupBuy($mainId, $data)
+    /**
+     * 保存团购专区;请求形态校验由 SaveGroupBuyForm 完成,此处做商品归属与库存业务校验
+     *
+     * @param int $mainId
+     * @param array $base 已校验的活动基础字段
+     * @param array $goods 已校验的商品列表(形态)
+     * @param int $enabled
+     * @return bool
+     */
+    public static function saveGroupBuy($mainId, $base, $goods = [], $enabled = 0)
     {
         $mainId = intval($mainId);
         if ($mainId <= 0) {
             util::fail('无效门店');
         }
-        $base = self::validateActivityBase($data);
-        $goods = self::validateGroupBuyGoods($mainId, self::decodeList($data['goods'] ?? []));
+        if (!is_array($base)) {
+            util::fail('参数错误');
+        }
+        $goods = self::validateGroupBuyGoods($mainId, is_array($goods) ? $goods : []);
         self::setJson($mainId, self::REDIS_GROUP_BUY, array_merge($base, ['goods' => $goods]));
-        HomePageConfigClass::updateModuleEnabled($mainId, 'groupBuy', !empty($data['enabled']) ? 1 : 0);
+        HomePageConfigClass::updateModuleEnabled($mainId, 'groupBuy', $enabled);
         return true;
     }
 
+    /**
+     * 业务校验并规范化团购商品:归属当前门店,库存不得超过实际商品库存
+     * 字段形态校验由 SaveGroupBuyForm 完成
+     */
     public static function validateGroupBuyGoods($mainId, $rawList)
     {
         $list = [];
@@ -467,12 +460,6 @@ class HomePageModuleClass
             $stock = intval($item['stock'] ?? 0);
             $limit = intval($item['limit'] ?? 0);
             $groupSize = intval($item['groupSize'] ?? 3);
-            if (!in_array($groupSize, [2, 3, 5], true)) {
-                util::fail("第{$no}个成团人数无效");
-            }
-            if ($goodsId <= 0 || $price <= 0 || $stock <= 0 || $limit <= 0) {
-                util::fail("请完善第{$no}个团购商品");
-            }
             $goods = GoodsClass::getById($goodsId, true);
             if (empty($goods) || intval($goods->mainId ?? 0) !== intval($mainId)) {
                 util::fail("第{$no}个团购商品无效");
@@ -483,9 +470,6 @@ class HomePageModuleClass
             }
             $virtualGroup = !empty($item['virtualGroup']) ? 1 : 0;
             $virtualMinutes = intval($item['virtualMinutes'] ?? 0);
-            if ($virtualGroup && $virtualMinutes <= 0) {
-                util::fail("请填写第{$no}个虚拟成团时间");
-            }
             $list[] = [
                 'goodsId' => $goodsId,
                 'price' => round($price, 2),
@@ -577,37 +561,29 @@ class HomePageModuleClass
         ];
     }
 
-    public static function saveGoodsSection($mainId, $moduleKey, $data)
+    /**
+     * 保存热门/上新/下拉商品;请求形态校验由 SaveGoodsSectionForm 完成
+     *
+     * @param int $mainId
+     * @param string $moduleKey hot|new|pullGoods
+     * @param array $payload {name,type,value,sort}
+     * @param int $enabled
+     * @return bool
+     */
+    public static function saveGoodsSection($mainId, $moduleKey, $payload, $enabled = 0)
     {
         $mainId = intval($mainId);
         $suffix = self::sectionSuffix($moduleKey);
-        $name = trim(strval($data['name'] ?? ''));
-        $type = intval($data['type'] ?? 0);
-        $value = trim(strval($data['value'] ?? ''));
-        $sort = intval($data['sort'] ?? self::SORT_PRODUCT);
-        // layoutCols/displayCount 由读取时按真实商品数量自动推导,保存时不再接收商家手动配置
-        if ($name === '') {
-            util::fail('请输入首页展示名称');
-        }
-        if (mb_strlen($name) > 20) {
-            util::fail('展示名称不能超过20字');
-        }
-        if (!in_array($type, [self::LINK_GOODS, self::LINK_CATEGORY, self::LINK_USE_CASE], true)) {
-            util::fail('关联类型无效');
-        }
-        if ($value === '') {
-            util::fail('请选择关联内容');
-        }
-        if (!in_array($sort, [self::SORT_PRODUCT, self::SORT_SALES, self::SORT_NEW], true)) {
-            util::fail('排序规则无效');
+        if (!is_array($payload)) {
+            util::fail('参数错误');
         }
         self::setJson($mainId, $suffix, [
-            'name' => $name,
-            'type' => $type,
-            'value' => $value,
-            'sort' => $sort,
+            'name' => strval($payload['name'] ?? ''),
+            'type' => intval($payload['type'] ?? 0),
+            'value' => strval($payload['value'] ?? ''),
+            'sort' => intval($payload['sort'] ?? self::SORT_PRODUCT),
         ]);
-        HomePageConfigClass::updateModuleEnabled($mainId, $moduleKey, !empty($data['enabled']) ? 1 : 0);
+        HomePageConfigClass::updateModuleEnabled($mainId, $moduleKey, $enabled);
         return true;
     }
 
@@ -877,49 +853,6 @@ class HomePageModuleClass
         return ['layoutCols' => 3, 'displayCount' => 3];
     }
 
-    public static function validateActivityBase($data)
-    {
-        $title = trim(strval($data['title'] ?? ''));
-        $subtitle = trim(strval($data['subtitle'] ?? ''));
-        $desc = trim(strval($data['desc'] ?? ''));
-        $startTime = intval($data['startTime'] ?? 0);
-        $endTime = intval($data['endTime'] ?? 0);
-        // layoutCols/displayCount 由读取时按商品数量自动推导,保存时不再接收商家手动配置
-        if ($title === '') {
-            util::fail('请输入活动标题');
-        }
-        if (mb_strlen($title) > 10) {
-            util::fail('活动标题不能超过10字');
-        }
-        if ($subtitle === '') {
-            util::fail('请输入活动副标题');
-        }
-        if (mb_strlen($subtitle) > 20) {
-            util::fail('活动副标题不能超过20字');
-        }
-        if ($startTime <= 0 || $endTime <= 0) {
-            util::fail('请选择活动时间');
-        }
-        if ($endTime <= $startTime) {
-            util::fail('结束时间必须大于开始时间');
-        }
-        if ($desc === '') {
-            util::fail('请输入活动说明');
-        }
-        if (mb_strlen($desc) > 500) {
-            util::fail('活动说明不能超过500字');
-        }
-        return [
-            'title' => $title,
-            'subtitle' => $subtitle,
-            'showCountdown' => !empty($data['showCountdown']) ? 1 : 0,
-            'expand' => !empty($data['expand']) ? 1 : 0,
-            'startTime' => $startTime,
-            'endTime' => $endTime,
-            'desc' => $desc,
-        ];
-    }
-
     /**
      * 活动状态:0未开始 1进行中 2已结束;模块关闭视为已结束展示用
      */