专注小程序 / APP 开发,用代码改变生活

获取方案
石家庄分站 主站首页 北京分站 石家庄分站 唐山分站 保定分站 廊坊分站 沧州分站 郑州分站

拼团小程序开发落地方案

小程序开发 2026-09-22 17 阅读 0 点赞 原创

为什么选择PHP做拼团小程序后端

拼团小程序的核心逻辑是“多人成团、限时优惠、库存扣减”,对后端的要求集中在事务一致性、并发控制和定时任务上。PHP生态成熟,配合MySQL和Redis足以支撑中小型拼团业务。本文以ThinkPHP 6 + MySQL + Redis为例,给出一套可直接落地的开发方案。

一、数据库设计

拼团业务至少需要四张核心表,设计时要预留扩展字段,避免后期频繁改表。

CREATE TABLE `goods` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(100) NOT NULL,
  `price` decimal(10,2) NOT NULL,
  `group_price` decimal(10,2) NOT NULL,
  `group_size` tinyint unsigned NOT NULL DEFAULT 2,
  `stock` int unsigned NOT NULL DEFAULT 0,
  `group_expire` int unsigned NOT NULL DEFAULT 86400,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `groupon` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `goods_id` int unsigned NOT NULL,
  `leader_uid` int unsigned NOT NULL,
  `need_num` tinyint unsigned NOT NULL,
  `join_num` tinyint unsigned NOT NULL DEFAULT 0,
  `status` tinyint NOT NULL DEFAULT 0 COMMENT '0进行中 1成功 2失败',
  `expire_time` int unsigned NOT NULL,
  `create_time` int unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_goods_status` (`goods_id`,`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `groupon_member` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `groupon_id` int unsigned NOT NULL,
  `uid` int unsigned NOT NULL,
  `order_id` int unsigned NOT NULL,
  `is_leader` tinyint NOT NULL DEFAULT 0,
  `create_time` int unsigned NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_group_uid` (`groupon_id`,`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `order` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `order_no` varchar(32) NOT NULL,
  `uid` int unsigned NOT NULL,
  `goods_id` int unsigned NOT NULL,
  `groupon_id` int unsigned NOT NULL DEFAULT 0,
  `amount` decimal(10,2) NOT NULL,
  `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已退款',
  `pay_time` int unsigned NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

其中groupon_member表的唯一索引uk_group_uid是防止同一用户重复参团的第一道防线。

二、核心流程与接口划分

1. 开团

  • 校验商品库存与限购
  • 创建groupon记录,leader_uid为当前用户
  • 创建订单并写入groupon_member
  • 返回拼团ID和订单号,拉起微信支付

2. 参团

  • 根据拼团ID查询团状态,必须是进行中且未过期
  • 校验用户是否已参团
  • 库存预扣,创建订单和成员记录,join_num加一
  • 若join_num等于need_num,触发成团逻辑

3. 成团与失败

成团在支付回调中判断,失败则依靠定时任务扫描过期团,统一退款。

三、并发控制的关键代码

拼团最怕超卖和重复参团,推荐用Redis原子操作加MySQL唯一索引双重保险。

public function joinGroup($grouponId, $uid)
{
    $redis = Redis::connection();
    $lockKey = 'groupon:lock:' . $grouponId;
    if (!$redis->set($lockKey, 1, ['nx', 'ex' => 5])) {
        throw new Exception('操作频繁,请重试');
    }
    Db::startTrans();
    try {
        $groupon = Db::name('groupon')->lock(true)->find($grouponId);
        if (!$groupon || $groupon['status'] != 0) {
            throw new Exception('拼团不存在或已结束');
        }
        if ($groupon['expire_time'] < time()) {
            throw new Exception('拼团已过期');
        }
        $exists = Db::name('groupon_member')
            ->where(['groupon_id' => $grouponId, 'uid' => $uid])
            ->count();
        if ($exists) {
            throw new Exception('您已参与该拼团');
        }
        $goods = Db::name('goods')->lock(true)->find($groupon['goods_id']);
        if ($goods['stock'] <= 0) {
            throw new Exception('库存不足');
        }
        Db::name('goods')->where('id', $goods['id'])->dec('stock')->update();
        $orderNo = date('YmdHis') . mt_rand(1000, 9999);
        $orderId = Db::name('order')->insertGetId([
            'order_no'   => $orderNo,
            'uid'        => $uid,
            'goods_id'   => $goods['id'],
            'groupon_id' => $grouponId,
            'amount'     => $goods['group_price'],
            'status'     => 0,
        ]);
        Db::name('groupon_member')->insert([
            'groupon_id' => $grouponId,
            'uid'        => $uid,
            'order_id'   => $orderId,
            'is_leader'  => 0,
            'create_time'=> time(),
        ]);
        $newNum = $groupon['join_num'] + 1;
        Db::name('groupon')->where('id', $grouponId)->update(['join_num' => $newNum]);
        if ($newNum >= $groupon['need_num']) {
            Db::name('groupon')->where('id', $grouponId)->update(['status' => 1]);
        }
        Db::commit();
        return ['order_no' => $orderNo, 'order_id' => $orderId];
    } catch (Exception $e) {
        Db::rollback();
        throw $e;
    } finally {
        $redis->del($lockKey);
    }
}

注意两点:一是lock(true)使用行锁,务必在事务内;二是Redis锁只做入口削峰,真正的数据一致性靠数据库事务和唯一索引。

四、定时任务处理过期团

用ThinkPHP的命令行工具写一个定时脚本,每分钟扫描一次。

php think groupon:expire
public function handle()
{
    $list = Db::name('groupon')
        ->where('status', 0)
        ->where('expire_time', '<', time())
        ->limit(100)
        ->select();
    foreach ($list as $item) {
        Db::startTrans();
        try {
            Db::name('groupon')->where('id', $item['id'])->update(['status' => 2]);
            $orders = Db::name('order')
                ->where('groupon_id', $item['id'])
                ->where('status', 1)
                ->select();
            foreach ($orders as $order) {
                $this->refund($order);
                Db::name('order')->where('id', $order['id'])->update(['status' => 2]);
            }
            Db::name('goods')->where('id', $item['goods_id'])->inc('stock', count($orders))->update();
            Db::commit();
        } catch (Exception $e) {
            Db::rollback();
            Log::error('拼团过期处理失败:' . $e->getMessage());
        }
    }
}

五、上线前必须检查的几点

  • 微信支付回调必须做签名校验和幂等处理,防止重复成团
  • 库存扣减建议在支付成功后再真正扣,下单时只做预占
  • 拼团分享路径要带上groupon_id,方便好友直接参团
  • 退款接口要记录退款单号,避免重复退款
  • 用Redis缓存商品和进行中的团列表,减少数据库压力

整体方案不复杂,难点在于把并发、事务和定时补偿这三块做扎实。PHP配合MySQL和Redis完全能撑起日订单几千到几万的拼团业务,先把核心链路跑通,再考虑分库分表和消息队列。

秒杀小程序开发实战:场景与落地方案同城配送小程序开发避坑指南

评论 (0)

还没有评论,快来抢沙发吧~

好想法,值得被认真交付

从小程序、APP 到全栈网站,一站式把想法变成可落地的产品

微信咨询

微信扫码,直接沟通需求