本文目录导读:

实现一个“助力砍价”功能,核心在于管理每个用户的砍价进度(已砍金额/剩余金额)以及防止刷单(每个用户只能帮同一个人砍一次)。
以下是一个基于 PHP + MySQL 的经典实现方案,包含数据库设计、核心逻辑和关键代码示例。
核心业务逻辑拆解
- 发起砍价:用户A发起一个砍价活动,系统生成一个活动记录,包含商品原价、底价、当前价格、活动有效期。
- 好友助力(砍一刀):用户B(从未帮A砍过)点击链接,系统随机生成一个砍价金额(需在合理范围内,如0.01~10元)。
- 更新价格:将用户A的当前价格减去砍掉的金额。
- 记录日志:记录“谁在什么时间帮谁砍了多少”,用于防重和查询。
- 判断成功:如果当前价格 <= 底价,则标记为“砍价成功”,用户可以下单。
数据库表设计
表1:砍价活动表 bargain_activity
| 字段 | 类型 | 说明 |
| :--- | :--- | :--- |
| id | int (PK) | 自增ID |
| user_id | int | 发起人用户ID |
| product_id | int | 商品ID |
| original_price | decimal(10,2) | 原价 |
| floor_price | decimal(10,2) | 底价(最低能砍到多少) |
| current_price | decimal(10,2) | 当前价格(初始=原价) |
| status | tinyint | 0=进行中, 1=成功, 2=已过期 |
| created_at | datetime | 发起时间 |
| expire_time | datetime | 过期时间(通常24或48小时) |
表2:砍价助力记录表 bargain_help
| 字段 | 类型 | 说明 |
| :--- | :--- | :--- |
| id | int (PK) | 自增ID |
| activity_id | int | 关联的活动ID |
| helper_user_id | int | 帮忙砍价的人 |
| cut_amount | decimal(10,2) | 本次砍掉的金额 |
| created_at | datetime | 砍价时间 |
关键约束:在 bargain_help 表上建立 UNIQUE KEY(activity_id, helper_user_id),确保同一个用户不能帮同一个活动重复砍价。
PHP 核心代码实现
以下是一个典型的“执行砍价”接口的逻辑,重点在于原子性操作和并发控制(防止超卖)。
1 核心函数:执行砍价
<?php
/**
* 执行砍价操作
* @param int $activityId 活动ID
* @param int $helperUserId 帮忙砍价的用户ID
* @return array ['code' => 0|1, 'msg' => '...', 'cut_amount' => 0.00]
*/
function doBargainHelp($activityId, $helperUserId) {
// 1. 查询活动信息(加锁或使用乐观锁)
$activity = getActivityById($activityId);
if (!$activity) {
return ['code' => 0, 'msg' => '活动不存在'];
}
// 检查活动是否进行中或过期
if ($activity['status'] != 0) {
return ['code' => 0, 'msg' => '活动已结束'];
}
if (strtotime($activity['expire_time']) < time()) {
// 更新状态为过期
updateActivityStatus($activityId, 2);
return ['code' => 0, 'msg' => '活动已过期'];
}
// 2. 检查是否重复助力
if (checkAlreadyHelped($activityId, $helperUserId)) {
return ['code' => 0, 'msg' => '您已经砍过一刀了'];
}
// 3. 计算本次砍价金额
$cutAmount = calculateCutAmount($activity['current_price'], $activity['floor_price']);
// 4. 新价格
$newPrice = $activity['current_price'] - $cutAmount;
if ($newPrice < $activity['floor_price']) {
// 确保不会砍到底价以下,调整本次砍价金额
$cutAmount = $activity['current_price'] - $activity['floor_price'];
$newPrice = $activity['floor_price'];
}
// 5. 原子性更新(防止并发)—— 使用事务 + 行锁
$db->beginTransaction();
try {
// 重新读取并加锁(SELECT ... FOR UPDATE)
$lockedActivity = getActivityForUpdate($activityId);
if (!$lockedActivity || $lockedActivity['status'] != 0) {
throw new Exception('活动状态异常');
}
if ($lockedActivity['current_price'] != $activity['current_price']) {
// 价格已被其他请求修改,重新计算
// 这里简单抛错,实际可以递归重试
throw new Exception('并发冲突,请重试');
}
// 插入助力记录
$insertData = [
'activity_id' => $activityId,
'helper_user_id' => $helperUserId,
'cut_amount' => $cutAmount,
'created_at' => date('Y-m-d H:i:s')
];
$db->insert('bargain_help', $insertData);
// 更新活动当前价格
$updateData = ['current_price' => $newPrice];
if ($newPrice <= $activity['floor_price']) {
$updateData['status'] = 1; // 砍价成功
}
$db->update('bargain_activity', $updateData, ['id' => $activityId]);
$db->commit();
return ['code' => 1, 'msg' => '砍价成功', 'cut_amount' => $cutAmount, 'current_price' => $newPrice];
} catch (Exception $e) {
$db->rollBack();
// 记录日志:$e->getMessage()
return ['code' => 0, 'msg' => '系统繁忙,请稍后重试'];
}
}
/**
* 计算砍价金额(可自定义策略)
* 规则:越接近底价,砍价越少(防止过快成功)
*/
function calculateCutAmount($currentPrice, $floorPrice) {
$maxCut = $currentPrice * 0.3; // 最多砍掉当前价格的30%
$minCut = 0.01;
// 保证至少砍到下一个档位,且不低于底价
$available = $currentPrice - $floorPrice;
// 如果剩余金额很少,直接全部砍掉
if ($available <= 0.01) {
return $available;
}
// 随机生成 0.01 到 min($maxCut, $available) 之间的金额
$cut = mt_rand(1, min(intval($maxCut * 100), intval($available * 100))) / 100;
return $cut;
}
说明:
getActivityForUpdate:使用 MySQL 的SELECT ... FOR UPDATE对当前行加行级锁,防止并发情况下砍价金额计算错误。checkAlreadyHelped:直接查询bargain_help表,利用唯一索引判断。
2 前沿优化:使用 Redis 提升性能(可选)
如果并发量很大,可以先走 Redis:
- 将活动信息缓存到 Redis Hash。
- 使用 Lua 脚本 或 Redis 分布式锁 实现原子减库存操作。
- 最后异步写入数据库。
3 安全与风控要点
- 每个用户限制:利用数据库唯一索引强制拦截“同一个用户给同一个活动多次助力”。
- IP / 设备限制:短时间内大量同IP或同设备ID的助力请求,可以触发限流或验证码。
- 金额合理性:砍价金额不能为负,不能超过剩余可砍金额。
- 活动期限:必须在服务端校验
expire_time,不要只依赖前端。 - 防机器人:建议在助力前加入图形验证码或滑块验证。
前端交互示例(简化版 AJAX)
// 点击“帮ta砍一刀”
function helpBargain(activityId) {
$.ajax({
url: '/api/bargain/help',
type: 'POST',
data: { activity_id: activityId },
success: function(res) {
if (res.code == 1) {
alert('恭喜!您帮好友砍掉了 ' + res.data.cut_amount + ' 元!');
// 更新页面上的进度条、剩余金额
updateBargainProgress(res.data.current_price, res.data.floor_price);
} else {
alert(res.msg);
}
},
error: function() {
alert('网络异常,请稍后重试');
}
});
}
总结流程
- 用户A发起砍价 → 插入
bargain_activity记录。 - 用户A分享链接(包含
activity_id)。 - 用户B点击链接 → PHP 接收请求 → 执行
doBargainHelp()。 - 检查活动有效性 + 防重复 → 生成随机金额 → 事务更新数据库。
- 返回结果给前端 → 刷新进度。
这个方案可以满足绝大多数中小型项目的砍价需求,核心就是 防重(唯一索引) + 防并发(行锁/乐观锁) + 金额策略(递减规律)。