为什么选择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:expirepublic 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)
还没有评论,快来抢沙发吧~