本文目录导读:

这是一个非常典型且很有价值的电商或会员系统需求,一个健壮的优惠券系统不仅仅是“生成一个码”,它需要处理生成、发放、核销、过期、阈值(满减/折扣)以及防超发等核心逻辑。
下面从数据库设计、核心功能逻辑、防并发处理以及代码示例四个维度,为你提供一个PHP可落地的完整方案。
核心数据库设计
你需要至少两张核心表:优惠券定义表(券模板)和用户持有表(券实例)。
-- 1. 优惠券定义表:定义一张券长什么样
CREATE TABLE `coupon_define` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '优惠券名称',
`type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '类型:1=满减券 2=折扣券 3=无门槛/代金券',
`value` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '减免金额(元) 或 折扣率(如0.85表示85折)',
`condition_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '满减门槛金额,0表示无门槛',
`total_count` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '发行总量(0表示不限)',
`received_count` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '已领取数(控制超发)',
`start_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '可领取开始时间',
`end_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '可领取截止时间',
`valid_days` smallint(5) UNSIGNED NOT NULL DEFAULT 0 COMMENT '领取后有效期(天数),0表示使用固定截止日',
`status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '状态:0=未发布 1=已发布 2=已失效',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. 用户持有的优惠券实例:用户领到的一张具体券
CREATE TABLE `coupon_user` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` int(11) UNSIGNED NOT NULL COMMENT '用户ID',
`coupon_define_id` int(11) UNSIGNED NOT NULL COMMENT '关联的券定义ID',
`code` varchar(32) NOT NULL DEFAULT '' COMMENT '优惠券唯一码(可用于展示和验证)',
`status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '状态:0=未使用 1=已使用 2=已过期 3=已冻结',
`used_at` datetime DEFAULT NULL COMMENT '使用时间',
`order_id` int(11) UNSIGNED DEFAULT NULL COMMENT '用在哪笔订单ID',
`start_time` datetime NOT NULL COMMENT '有效期起始',
`end_time` datetime NOT NULL COMMENT '有效期截止',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_status` (`user_id`, `status`),
UNIQUE KEY `uniq_code` (`code`) -- 防重复核销
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
设计要点:
received_count一定要加锁控制。- 在
coupon_user中存储start_time/end_time,是实际生效的时间,可以是固定日期,也可以按领取时间+有效天数动态生成。
核心业务流程
用户领取优惠券
- 校验:领取时间
start_time <= now() < end_time;发行总量received_count < total_count;每人限领(建议用单独的字段或查coupon_user记录判断)。 - 生成:生成唯一码(
uuid/uniqid + rand/ 雪花ID / 防重随机码),目的是防止被猜出或重复。 - 扣减:原子操作
UPDATE coupon_define SET received_count = received_count + 1 WHERE id = ? AND received_count < total_count - 写入:插入
coupon_user记录,并计算失效时间(valid_days > 0则start_time = now(), end_time = DATE_ADD(now(), INTERVAL valid_days DAY);否则使用定义表中的固定时间段)。
订单结算/核销优惠券
- 选择优惠券:前端展示用户可用的券(未使用 + 在有效期 + 订单金额 >=
condition_amount)。 - 适用校验:后端接收
user_id, coupon_user_id, order_amount。- 校验券状态为“未使用”。
- 校验券归属用户。
- 校验
order_amount >= condition_amount。 - 校验有效期。
- 计算优惠金额:根据
type计算(满减直接减value,折扣则order_amount * (1 - value),可设封顶)。 - 锁定/预占:建议将
coupon_user.status = 3(冻结),防止重复提交。 - 下单成功后:更新
status = 1(已使用),used_at,order_id。
过期处理
- 方案A(推荐):查询时实时判断,在查询用户优惠券列表时,用
end_time < NOW()来判断是否过期,不依赖定时任务,更可靠。 - 方案B:定时任务(Cron)每小时更新
status=2,适合数据量超大、需要查询性能极致的场景。
高并发与防超发(关键)
优惠券抢购容易遇到并发问题,PHP + MySQL 下最常见的处理方式:
使用 UPDATE ... WHERE ... 加行锁(最常用且可靠):
<?php
try {
$db->beginTransaction();
// 1. 锁定并扣减库存(行锁)
$sql = "UPDATE coupon_define SET received_count = received_count + 1
WHERE id = ? AND total_count > received_count AND status = 1
AND start_time <= NOW() AND end_time > NOW()";
$stmt = $db->prepare($sql);
$stmt->execute([$couponDefineId]);
$affected = $stmt->rowCount();
if ($affected <= 0) {
$db->rollBack();
throw new \Exception('优惠券已领完或不在领取期间');
}
// 2. 生成唯一券码(建议用足够随机的字符串避免恶意碰撞)
$code = bin2hex(random_bytes(8)); // 生成16位十六进制字符串
// 3. 计算用户这张券的有效期
$now = date('Y-m-d H:i:s');
$expireDate = date('Y-m-d H:i:s', strtotime('+30 days')); // 假设有效期为30天
// 4. 写入用户领券记录
$sql = "INSERT INTO coupon_user (user_id, coupon_define_id, code, status, start_time, end_time, created_at)
VALUES (?, ?, ?, 0, ?, ?, ?)";
$stmt = $db->prepare($sql);
$stmt->execute([$userId, $couponDefineId, $code, $now, $expireDate, $now]);
$db->commit();
return ['coupon_user_id' => $db->lastInsertId(), 'code' => $code];
} catch (\Exception $e) {
$db->rollBack();
throw $e;
}
注意:不要使用
SELECT ... FOR UPDATE+ 应用层判断,除非你确信事务隔离级别并自行处理死锁。UPDATE直接在WHERE中加条件是最简单高效的。
PHP代码示例:核销优惠券
<?php
class CouponService
{
/**
* 核销优惠券
* @param int $userId
* @param int $couponUserId
* @param float $orderAmount
* @param int $orderId
* @return float 优惠金额
* @throws \Exception
*/
public function useCoupon(int $userId, int $couponUserId, float $orderAmount, int $orderId): float
{
$db = \DB::connection();
try {
$db->beginTransaction();
// 1. 锁定用户持有的券记录(行锁)
$sql = "SELECT * FROM coupon_user WHERE id = ? AND status = 0 FOR UPDATE";
$couponUser = $db->selectOne($sql, [$couponUserId]);
if (!$couponUser) {
throw new \Exception('优惠券不存在或已使用');
}
if ($couponUser->user_id !== $userId) {
throw new \Exception('优惠券不属于当前用户');
}
if ($couponUser->end_time < date('Y-m-d H:i:s')) {
throw new \Exception('优惠券已过期');
}
// 2. 查询券定义
$couponDefine = $db->selectOne(
"SELECT * FROM coupon_define WHERE id = ?",
[$couponUser->coupon_define_id]
);
if (!$couponDefine || $couponDefine->status !== 1) {
throw new \Exception('优惠券活动无效');
}
// 3. 计算优惠金额
$discountAmount = 0;
if ($orderAmount < $couponDefine->condition_amount) {
throw new \Exception('订单金额不满足使用条件');
}
switch ($couponDefine->type) {
case 1: // 满减
$discountAmount = min($couponDefine->value, $orderAmount);
break;
case 2: // 折扣
$discountAmount = $orderAmount * (1 - $couponDefine->value);
// 可以设定最高减免金额
break;
case 3: // 无门槛
$discountAmount = min($couponDefine->value, $orderAmount);
break;
default:
throw new \Exception('优惠券类型错误');
}
// 4. 标记为已使用
$db->update(
"UPDATE coupon_user SET status = 1, used_at = NOW(), order_id = ? WHERE id = ? AND status = 0",
[$orderId, $couponUserId]
);
$db->commit();
return round($discountAmount, 2);
} catch (\Exception $e) {
$db->rollBack();
throw $e;
}
}
}
进阶优化点
- 唯一码防撞:对于批量生成或安全要求高,可以使用
hashids或uniqid + crc32组合。 - 用户领券频控:在领取接口加 Redis 计数器,防止脚本刷券:
$redis->incr('coupon:limit:user_' . $userId, 1, 3600)。 - 分表分库:用户券量级上千万时,考虑按
user_id哈希分表。 - 异步核销:高并发下单场景,可以将“用券”操作放入消息队列(RabbitMQ / Redis list),后端异步更新状态。
- 缓存:用户券列表可以缓存到 Redis,但状态变更(过期、使用)需要及时同步清理。
- 表结构:券定义 + 用户券,区分模板与实例。
- 核心逻辑:领取时用
UPDATE ... WHERE received_count < total_count保证不超发;使用事务 +FOR UPDATE保证状态一致。 - 计算优惠:按类型处理,注意折扣不能倒贴。
- 并发处理:数据库行锁是 PHP 项目的首选,简单且稳定。
按照这个方案,可以支撑绝大多数中小型项目的优惠券系统,代码示例可以直接用于 Laravel、ThinkPHP 或原生 PHP。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播