在O2O平台、物流配送、外卖服务等应用场景中,抢单功能已成为核心业务模块之一。本文将详细介绍如何使用UniApp作为前端框架,ThinkPHP作为后端API服务,实现一个高并发场景下的抢单系统。
-- 订单表
CREATE TABLE `orders` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_sn` varchar(20) NOT NULL COMMENT '订单编号',
`user_id` int(11) NOT NULL COMMENT '发布用户ID',
`title` varchar(255) NOT NULL COMMENT '订单标题',
`content` text COMMENT '订单详情',
`price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单金额',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0:待抢单,1:已抢单,2:已完成',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_status` (`status`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 抢单记录表
CREATE TABLE `order_grab` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_id` int(11) NOT NULL COMMENT '订单ID',
`grab_user_id` int(11) NOT NULL COMMENT '抢单用户ID',
`grab_time` datetime DEFAULT NULL COMMENT '抢单时间',
`status` tinyint(1) DEFAULT '0' COMMENT '抢单状态',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_id` (`order_id`), -- 唯一约束防止重复抢单
KEY `idx_grab_user_id` (`grab_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;<template>
<view class="container">
<view class="order-list">
<view v-for="item in orderList" :key="item.id" class="order-item">
<view class="order-info">
<text class="title">{{ item.title }}</text>
<text class="price">¥{{ item.price }}</text>
</view>
<view class="order-content">{{ item.content }}</view>
<button
v-if="item.status === 0"
class="grab-btn"
@click="grabOrder(item.id)"
:loading="grabLoading"
>抢单</button>
<text v-else class="status-text">
{{ item.status === 1 ? '已抢单' : '已完成' }}
</text>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
orderList: [],
grabLoading: false
}
},
onLoad() {
this.loadOrderList()
},
methods: {
// 加载订单列表
async loadOrderList() {
try {
const res = await this.$api.get('/order/list', {
status: 0 // 只获取待抢单的订单
})
this.orderList = res.data
} catch (error) {
uni.showToast({
title: '加载失败',
icon: 'none'
})
}
},
// 抢单操作
async grabOrder(orderId) {
this.grabLoading = true
try {
const res = await this.$api.post('/order/grab', {
order_id: orderId
})
if (res.code === 200) {
uni.showToast({
title: '抢单成功',
icon: 'success'
})
// 刷新列表
this.loadOrderList()
} else {
uni.showToast({
title: res.msg || '抢单失败',
icon: 'none'
})
}
} catch (error) {
uni.showToast({
title: '网络错误',
icon: 'none'
})
} finally {
this.grabLoading = false
}
}
}
}
</script><?php
namespace app\controller;
use app\BaseController;
use think\facade\Db;
use think\facade\Cache;
class Order extends BaseController
{
/**
* 抢单接口
*/
public function grab()
{
$orderId = $this->request->post('order_id');
$userId = $this->getUserId(); // 获取当前用户ID
if (empty($orderId)) {
return json(['code' => 400, 'msg' => '参数错误']);
}
// 使用Redis锁防止并发抢单
$lockKey = 'order_grab_lock:' . $orderId;
$lock = Cache::store('redis')->set($lockKey, 1, ['nx', 'ex' => 5]);
if (!$lock) {
return json(['code' => 500, 'msg' => '操作太频繁,请稍后重试']);
}
try {
// 开启事务
Db::startTrans();
// 检查订单是否存在且可抢
$order = Db::name('orders')
->where('id', $orderId)
->lock(true)
->find();
if (!$order) {
throw new \Exception('订单不存在');
}
if ($order['status'] != 0) {
throw new \Exception('订单已被抢');
}
// 检查用户是否已抢过该订单
$existGrab = Db::name('order_grab')
->where('order_id', $orderId)
->where('grab_user_id', $userId)
->find();
if ($existGrab) {
throw new \Exception('您已抢过该订单');
}
// 更新订单状态
Db::name('orders')
->where('id', $orderId)
->update([
'status' => 1,
'update_time' => date('Y-m-d H:i:s')
]);
// 创建抢单记录
Db::name('order_grab')
->insert([
'order_id' => $orderId,
'grab_user_id' => $userId,
'grab_time' => date('Y-m-d H:i:s'),
'status' => 1
]);
// 提交事务
Db::commit();
return json(['code' => 200, 'msg' => '抢单成功']);
} catch (\Exception $e) {
// 回滚事务
Db::rollback();
return json(['code' => 500, 'msg' => $e->getMessage()]);
} finally {
// 释放锁
Cache::store('redis')->delete($lockKey);
}
}
/**
* 获取订单列表
*/
public function list()
{
$status = $this->request->get('status', 0);
$page = $this->request->get('page', 1);
$limit = $this->request->get('limit', 10);
$where = [];
if ($status !== '') {
$where[] = ['status', '=', $status];
}
$list = Db::name('orders')
->where($where)
->page($page, $limit)
->order('create_time', 'desc')
->select();
return json(['code' => 200, 'data' => $list]);
}
/**
* 获取当前用户ID
*/
private function getUserId()
{
// 实际项目中根据Token获取用户ID
// 这里简化为直接返回
return 1;
}
}在高并发场景下,抢单功能需要特别注意以下几个问题:
// 使用版本号实现乐观锁
$order = Db::name('orders')
->where('id', $orderId)
->where('status', 0)
->find();
if ($order) {
$update = Db::name('orders')
->where('id', $orderId)
->where('version', $order['version']) // 检查版本号
->update([
'status' => 1,
'version' => $order['version'] + 1, // 版本号+1
'update_time' => date('Y-m-d H:i:s')
]);
if (!$update) {
throw new \Exception('抢单失败,请重试');
}
}对于极高并发场景,可以使用Redis队列缓解数据库压力:
// 将抢单请求放入队列
public function grabQueue()
{
$orderId = $this->request->post('order_id');
$userId = $this->getUserId();
$data = [
'order_id' => $orderId,
'user_id' => $userId,
'time' => time()
];
// 将抢单请求加入队列
Cache::store('redis')->lPush('order_grab_queue', json_encode($data));
return json(['code' => 200, 'msg' => '抢单请求已提交,请等待结果']);
}
// 使用定时任务处理队列
public function processGrabQueue()
{
// 每次处理10个请求
for ($i = 0; $i < 10; $i++) {
$data = Cache::store('redis')->rPop('order_grab_queue');
if (!$data) break;
$data = json_decode($data, true);
// 处理抢单逻辑
$this->doGrab($data['order_id'], $data['user_id']);
}
}使用Redis实现简单限流:
// 检查用户抢单频率
public function checkRateLimit($userId)
{
$key = 'grab_rate_limit:' . $userId;
$limit = 10; // 10次/分钟
$current = Cache::store('redis')->get($key);
if ($current && $current >= $limit) {
return false;
}
Cache::store('redis')->multi();
Cache::store('redis')->incr($key);
Cache::store('redis')->expire($key, 60); // 60秒过期
Cache::store('redis')->exec();
return true;
}本文介绍了基于UniApp和ThinkPHP实现抢单功能的完整方案,包括:
实际项目中还需要考虑更多的细节,如用户认证、权限控制、异常处理、日志记录等。抢单功能作为核心业务,需要保证数据的一致性和系统的高可用性,合理使用数据库事务、锁机制和缓存技术是关键。
希望本文能为需要实现类似功能的开发者提供参考和思路。