Skip to content

团购系统

MineShop 的团购系统支持多人成团、阶梯价格、限时限量等玩法,自动处理开团、成团、失败等状态流转。

🎯 系统架构

┌─────────────────────────────────────────────────────────────┐
│                      团购活动 (GroupBuy)                      │
│              • 商品信息、团购价格、成团人数                      │
│              • 活动时间、库存数量、状态                          │
├─────────────────────────────────────────────────────────────┤
│     团1 (Group)      │     团2 (Group)      │     ...       │
│   团长: 用户A        │   团长: 用户B         │               │
│   状态: 拼团中       │   状态: 已成团        │               │
│   人数: 2/3         │   人数: 3/3          │               │
├─────────────────────────────────────────────────────────────┤
│  参团记录1  │  参团记录2  │  参团记录3  │  参团记录4  │  ...  │
└─────────────────────────────────────────────────────────────┘

✨ 核心特性

1. 活动管理

字段说明示例
title活动标题限时团购 iPhone 15
product_id关联商品商品 ID
original_price原价5999
group_price团购价4999
min_people最少成团人数2
max_people最多成团人数10
group_time_limit成团时限(小时)24
total_quantity活动库存100

2. 团购实体

php
// GroupBuyEntity.php
final class GroupBuyEntity
{
    // 判断是否可以参团
    public function canJoin(): bool
    {
        if (!$this->isEnabled) {
            return false;
        }
        if ($this->status !== 'active') {
            return false;
        }
        if ($this->soldQuantity >= $this->totalQuantity) {
            return false;
        }
        $timeVo = new ActivityTimeVo($this->startTime, $this->endTime);
        return $timeVo->isActive();
    }

    // 30分钟编辑锁定
    public function isWithinCacheWarmupPeriod(): bool
    {
        $startTime = Carbon::parse($this->startTime);
        $now = Carbon::now();
        
        if ($startTime->lte($now)) {
            return false;
        }
        
        return $startTime->diffInMinutes($now) <= 30;
    }
}

3. 缓存预热

php
// GroupBuyCacheService.php
public function warmStock(int $groupBuyId): void
{
    $model = $this->repository->findById($groupBuyId);
    if (!$model) {
        return;
    }
    
    // 计算剩余库存
    $remaining = max(0, $model->total_quantity - $model->sold_quantity);
    $skuId = (int) $model->sku_id;
    
    // 写入 Redis Hash
    $hashKey = sprintf('stock:%d', $groupBuyId);
    $this->cache->hMset($hashKey, [(string) $skuId => (string) $remaining]);
}

🔄 状态流转

活动状态

┌──────────┐    开始时间到达    ┌──────────┐    结束时间到达    ┌──────────┐
│  pending │ ───────────────→ │  active  │ ───────────────→ │  ended   │
│  待开始   │                  │  进行中   │                  │  已结束   │
└──────────┘                  └──────────┘                  └──────────┘

                                   │ 库存售罄

                              ┌──────────┐
                              │ sold_out │
                              │  已售罄   │
                              └──────────┘

团状态

┌──────────┐    人数达标    ┌──────────┐
│ grouping │ ───────────→ │ success  │
│  拼团中   │              │  已成团   │
└──────────┘              └──────────┘

      │ 超时未成团

┌──────────┐
│  failed  │
│  已失败   │
└──────────┘

🕐 定时任务

php
// GroupBuyActivityStatusCrontab.php
#[Crontab(
    name: 'group-buy-activity-status',
    rule: '*/10 * * * *',  // 每10分钟执行
    callback: 'execute'
)]
class GroupBuyActivityStatusCrontab
{
    public function execute(): void
    {
        // 1. 处理待开始的活动(30分钟内)
        $this->processPendingActivities();
        
        // 2. 处理已过期的活动
        $this->processExpiredActivities();
    }

    private function processPendingActivities(): void
    {
        $activities = $this->repository->findPendingActivitiesWithinMinutes(30);
        
        foreach ($activities as $activity) {
            $startTime = Carbon::parse($activity->start_time);
            
            if ($startTime->lte(Carbon::now())) {
                // 立即激活
                $this->groupBuyService->start($activity->id);
                $this->cacheService->warmStock($activity->id);
            } else {
                // 延迟 Job
                $delaySeconds = $startTime->diffInSeconds(Carbon::now());
                $this->driverFactory->get('default')->push(
                    new GroupBuyStartJob($activity->id), 
                    $delaySeconds
                );
            }
        }
    }
}

📦 缓存结构

Redis Key 结构:

groupbuy:stock:{groupBuyId}    # 库存数据 (Hash, field=skuId, value=剩余库存)
groupbuy:group:{groupId}       # 团信息 (String/JSON)
groupbuy:members:{groupId}     # 团成员 (Set)

💻 API 接口

后台管理

接口方法说明
/admin/group-buyGET活动列表
/admin/group-buyPOST创建活动
/admin/group-buy/{id}PUT更新活动
/admin/group-buy/{id}DELETE删除活动
/admin/group-buy/{id}/togglePOST切换状态

前端 API

接口方法说明
/api/group-buy/listGET获取活动列表
/api/group-buy/{id}GET活动详情
/api/group-buy/groupsGET获取可参与的团
/api/group-buy/create-groupPOST开团
/api/group-buy/join-groupPOST参团

🛡️ 业务规则

开团规则

  1. 活动必须处于 active 状态
  2. 活动库存充足
  3. 用户未超过限购数量

参团规则

  1. 团必须处于 grouping 状态
  2. 团人数未满
  3. 用户未参与过该团
  4. 活动库存充足

成团规则

  1. 团人数达到 min_people
  2. 未超过成团时限

失败处理

  1. 超时未成团自动标记失败
  2. 退还用户支付金额
  3. 回滚库存

📊 数据统计

  • 活动销售额
  • 成团率分析
  • 用户参与度
  • 热门商品排行

⚠️ 注意事项

  1. 库存预热: 活动开始前 30 分钟自动预热
  2. 编辑限制: 预热期内禁止编辑活动
  3. 超时处理: 定时任务检查超时团并处理
  4. 退款流程: 失败团需触发退款流程

📚 相关文档

基于 Apache-2.0 许可发布 | 感谢 MineAdmin 提供的优秀基础框架