|
|
@@ -0,0 +1,268 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace bizHd\seckill\classes;
|
|
|
+
|
|
|
+use bizHd\base\classes\BaseClass;
|
|
|
+use bizHd\goods\classes\GoodsClass;
|
|
|
+use bizHd\homePageConfig\classes\HomePageConfigClass;
|
|
|
+use bizHd\homePageConfig\classes\HomePageModuleClass;
|
|
|
+use common\components\util;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 秒杀活动批次与商品版本业务类
|
|
|
+ * 供 hd 后台保存/读取、mall 首页展示与下单核价使用
|
|
|
+ * 替代原先仅写 Redis 的单例配置,按起止时间批次化持久化到 MySQL
|
|
|
+ */
|
|
|
+class SeckillActivityClass extends BaseClass
|
|
|
+{
|
|
|
+ public static $baseFile = '\bizHd\seckill\models\SeckillActivity';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 读取当前门店「生效中」的秒杀配置,返回结构与历史 Redis getSeckill 兼容
|
|
|
+ *
|
|
|
+ * @param int $mainId
|
|
|
+ * @param bool $refreshStock 是否回查真实库存
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ public static function getSeckill($mainId, $refreshStock = true)
|
|
|
+ {
|
|
|
+ $mainId = intval($mainId);
|
|
|
+ if ($mainId <= 0) {
|
|
|
+ util::fail('无效门店');
|
|
|
+ }
|
|
|
+
|
|
|
+ $enabled = HomePageConfigClass::getModuleEnabled($mainId, 'seckill');
|
|
|
+ $activity = self::getCurrentActivity($mainId);
|
|
|
+ if (empty($activity)) {
|
|
|
+ // 无 MySQL 批次时回退 Redis,兼容尚未迁移的旧数据
|
|
|
+ return HomePageModuleClass::getSeckillFromRedis($mainId, $refreshStock);
|
|
|
+ }
|
|
|
+
|
|
|
+ $goodsRows = SeckillGoodsClass::getActiveGoodsByActivityId(intval($activity['id']), $mainId, $refreshStock);
|
|
|
+ $base = [
|
|
|
+ 'activityId' => intval($activity['id']),
|
|
|
+ 'title' => strval($activity['title'] ?? ''),
|
|
|
+ 'subtitle' => strval($activity['subtitle'] ?? ''),
|
|
|
+ 'showCountdown' => !empty($activity['showCountdown']) ? 1 : 0,
|
|
|
+ 'expand' => !empty($activity['expand']) ? 1 : 0,
|
|
|
+ 'startTime' => intval($activity['startTime'] ?? 0),
|
|
|
+ 'endTime' => intval($activity['endTime'] ?? 0),
|
|
|
+ 'desc' => strval($activity['desc'] ?? ''),
|
|
|
+ ];
|
|
|
+ $layout = HomePageModuleClass::resolveActivityLayout(count($goodsRows), $base['expand']);
|
|
|
+ $base['enabled'] = $enabled;
|
|
|
+ $base['goods'] = $goodsRows;
|
|
|
+ $base['layoutCols'] = $layout['layoutCols'];
|
|
|
+ $base['displayCount'] = $layout['displayCount'];
|
|
|
+ $base['status'] = HomePageModuleClass::calcActivityStatus($base['startTime'], $base['endTime'], $enabled);
|
|
|
+ return $base;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 保存秒杀配置:按时间重叠复用/新建活动批次,商品按关键字段变化新建版本
|
|
|
+ *
|
|
|
+ * @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('无效门店');
|
|
|
+ }
|
|
|
+ if (!is_array($base)) {
|
|
|
+ util::fail('参数错误');
|
|
|
+ }
|
|
|
+
|
|
|
+ $validatedGoods = self::validateSeckillGoods($mainId, is_array($goods) ? $goods : []);
|
|
|
+ $activity = self::resolveOrCreateActivity($mainId, $base);
|
|
|
+ $activityId = intval($activity['id'] ?? ($activity->id ?? 0));
|
|
|
+ if ($activityId <= 0) {
|
|
|
+ util::fail('活动保存失败');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新活动基础信息(同一批次内修改标题/说明等)
|
|
|
+ self::updateById($activityId, [
|
|
|
+ 'title' => $base['title'],
|
|
|
+ 'subtitle' => $base['subtitle'],
|
|
|
+ 'showCountdown' => !empty($base['showCountdown']) ? 1 : 0,
|
|
|
+ 'expand' => !empty($base['expand']) ? 1 : 0,
|
|
|
+ 'startTime' => $base['startTime'],
|
|
|
+ 'endTime' => $base['endTime'],
|
|
|
+ 'desc' => strval($base['desc'] ?? ''),
|
|
|
+ 'status' => 1,
|
|
|
+ 'delStatus' => 0,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ SeckillGoodsClass::syncActivityGoods($mainId, $activityId, $validatedGoods);
|
|
|
+ HomePageConfigClass::updateModuleEnabled($mainId, 'seckill', $enabled);
|
|
|
+
|
|
|
+ // 同步写 Redis,保证旧读路径与预览链路仍可用
|
|
|
+ $syncGoods = SeckillGoodsClass::getActiveGoodsByActivityId($activityId, $mainId, false);
|
|
|
+ $persistGoods = [];
|
|
|
+ foreach ($syncGoods as $row) {
|
|
|
+ $persistGoods[] = [
|
|
|
+ 'id' => intval($row['id']),
|
|
|
+ 'goodsId' => intval($row['goodsId']),
|
|
|
+ 'price' => floatval($row['price']),
|
|
|
+ 'stock' => intval($row['stock']),
|
|
|
+ 'limit' => intval($row['limit']),
|
|
|
+ 'status' => intval($row['status']),
|
|
|
+ 'name' => strval($row['name']),
|
|
|
+ 'cover' => strval($row['cover']),
|
|
|
+ 'originPrice' => floatval($row['originPrice']),
|
|
|
+ 'specName' => strval($row['specName'] ?? ''),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ HomePageModuleClass::setJson($mainId, HomePageModuleClass::REDIS_SECKILL, array_merge([
|
|
|
+ 'title' => strval($base['title'] ?? ''),
|
|
|
+ 'subtitle' => strval($base['subtitle'] ?? ''),
|
|
|
+ 'showCountdown' => !empty($base['showCountdown']) ? 1 : 0,
|
|
|
+ 'expand' => !empty($base['expand']) ? 1 : 0,
|
|
|
+ 'startTime' => intval($base['startTime'] ?? 0),
|
|
|
+ 'endTime' => intval($base['endTime'] ?? 0),
|
|
|
+ 'desc' => strval($base['desc'] ?? ''),
|
|
|
+ ], ['goods' => $persistGoods]));
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 取当前应展示的活动:优先进行中,其次未开始最近一场,再取最近结束的一场
|
|
|
+ *
|
|
|
+ * @param int $mainId
|
|
|
+ * @return array|null
|
|
|
+ */
|
|
|
+ public static function getCurrentActivity($mainId)
|
|
|
+ {
|
|
|
+ $mainId = intval($mainId);
|
|
|
+ $now = time();
|
|
|
+ $list = self::getAllByCondition([
|
|
|
+ 'mainId' => $mainId,
|
|
|
+ 'delStatus' => 0,
|
|
|
+ 'status' => 1,
|
|
|
+ ], 'startTime DESC', '*', null);
|
|
|
+ if (empty($list)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $running = null;
|
|
|
+ $upcoming = null;
|
|
|
+ $latestEnded = null;
|
|
|
+ foreach ($list as $row) {
|
|
|
+ $start = intval($row['startTime'] ?? 0);
|
|
|
+ $end = intval($row['endTime'] ?? 0);
|
|
|
+ if ($start <= $now && $now <= $end) {
|
|
|
+ $running = $row;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ if ($start > $now) {
|
|
|
+ if ($upcoming === null || $start < intval($upcoming['startTime'])) {
|
|
|
+ $upcoming = $row;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if ($end < $now) {
|
|
|
+ if ($latestEnded === null || $end > intval($latestEnded['endTime'])) {
|
|
|
+ $latestEnded = $row;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return $running ?: ($upcoming ?: $latestEnded);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按起止时间重叠复用历史活动,否则新建批次
|
|
|
+ *
|
|
|
+ * @param int $mainId
|
|
|
+ * @param array $base
|
|
|
+ * @return array|\yii\db\ActiveRecord
|
|
|
+ */
|
|
|
+ public static function resolveOrCreateActivity($mainId, $base)
|
|
|
+ {
|
|
|
+ $mainId = intval($mainId);
|
|
|
+ $startTime = intval($base['startTime'] ?? 0);
|
|
|
+ $endTime = intval($base['endTime'] ?? 0);
|
|
|
+ if ($startTime <= 0 || $endTime <= $startTime) {
|
|
|
+ util::fail('活动时间无效');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 时间区间有重叠即复用:startA < endB AND endA > startB
|
|
|
+ $existed = self::getAllByCondition([
|
|
|
+ 'mainId' => $mainId,
|
|
|
+ 'delStatus' => 0,
|
|
|
+ 'status' => 1,
|
|
|
+ 'startTime<' => $endTime,
|
|
|
+ 'endTime>' => $startTime,
|
|
|
+ ], 'id DESC', '*', null);
|
|
|
+ if (!empty($existed[0])) {
|
|
|
+ $row = $existed[0];
|
|
|
+ // 重叠时沿用历史起止时间,不因小改动拆分批次
|
|
|
+ return $row;
|
|
|
+ }
|
|
|
+
|
|
|
+ return self::add([
|
|
|
+ 'mainId' => $mainId,
|
|
|
+ 'title' => strval($base['title'] ?? ''),
|
|
|
+ 'subtitle' => strval($base['subtitle'] ?? ''),
|
|
|
+ 'showCountdown' => !empty($base['showCountdown']) ? 1 : 0,
|
|
|
+ 'expand' => !empty($base['expand']) ? 1 : 0,
|
|
|
+ 'startTime' => $startTime,
|
|
|
+ 'endTime' => $endTime,
|
|
|
+ 'desc' => strval($base['desc'] ?? ''),
|
|
|
+ 'status' => 1,
|
|
|
+ 'delStatus' => 0,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 业务校验秒杀商品(归属/库存),与历史 validateSeckillGoods 对齐并保留 id/specName
|
|
|
+ *
|
|
|
+ * @param int $mainId
|
|
|
+ * @param array $rawList
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ public static function validateSeckillGoods($mainId, $rawList)
|
|
|
+ {
|
|
|
+ $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);
|
|
|
+ $goods = GoodsClass::getById($goodsId, true);
|
|
|
+ if (empty($goods) || intval($goods->mainId ?? 0) !== intval($mainId)) {
|
|
|
+ util::fail("第{$no}个秒杀商品无效");
|
|
|
+ }
|
|
|
+ $realStock = intval($goods->stock ?? 0);
|
|
|
+ if ($stock > $realStock) {
|
|
|
+ util::fail("第{$no}个秒杀库存不能超过商品实际库存({$realStock})");
|
|
|
+ }
|
|
|
+ $status = !empty($item['status']) ? 1 : 0;
|
|
|
+ if ($realStock < $stock) {
|
|
|
+ $status = 0;
|
|
|
+ }
|
|
|
+ $name = $goods->masterId > 0 ? $goods->name . '(' . $goods->specName . ')' : $goods->name;
|
|
|
+ $list[] = [
|
|
|
+ 'id' => intval($item['id'] ?? 0),
|
|
|
+ 'goodsId' => $goodsId,
|
|
|
+ 'price' => round($price, 2),
|
|
|
+ 'stock' => $stock,
|
|
|
+ 'limit' => $limit,
|
|
|
+ 'status' => $status,
|
|
|
+ 'name' => $name,
|
|
|
+ 'cover' => strval($goods->shortCover ?? ($goods->cover ?? ($item['cover'] ?? ''))),
|
|
|
+ 'originPrice' => floatval($goods->price ?? ($item['originPrice'] ?? 0)),
|
|
|
+ 'specName' => strval($goods->specName ?? ($item['specName'] ?? '')),
|
|
|
+ 'realStock' => $realStock,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ return $list;
|
|
|
+ }
|
|
|
+}
|