1. 项目背景与核心需求
斯诺克台球馆作为休闲娱乐场所,其运营管理正面临数字化转型的迫切需求。传统的人工售票和电话预约方式存在效率低下、易出错、高峰期排队等问题。基于Node.js+Vue的购票系统设计,正是为了解决以下核心痛点:
- 实时库存管理:台球桌作为场馆核心资源,需要精确到小时粒度的动态库存控制
- 多终端适配:前台收银、移动端预约、自助终端需数据实时同步
- 价格策略灵活配置:支持时段定价、会员折扣、节假日溢价等复杂规则
- 数据可视化分析:营业数据实时呈现,辅助经营决策
我在实际考察北京某连锁台球品牌时发现,其周末高峰期因人工调度导致的台桌空置率高达15%,而采用智能调度系统后空置率降至3%以下,这正是技术赋能传统行业的典型案例。
2. 技术栈选型与架构设计
2.1 前后端分离架构
采用经典的B/S架构:
code复制[客户端] Vue3 + Vant UI + Axios
[网关层] Nginx 负载均衡
[服务层] Node.js + Express/Koa
[数据层] MongoDB/MySQL + Redis缓存
选择Node.js而非Java/PHP的核心考量:
- 高并发IO:购票系统的瞬时并发可达500+/秒,Node的非阻塞IO更胜任
- 开发效率:全栈JavaScript统一语言,减少上下文切换成本
- 生态丰富:NPM现有模块可直接复用,如:
node-schedule处理时段定价mongoose操作MongoDBjsonwebtoken实现JWT认证
2.2 数据库设计要点
以台球桌预定为例的核心表结构:
javascript复制// 台球桌模型
const TableSchema = new Schema({
tableId: { type: String, unique: true }, // 如"A区-01号"
type: { type: String }, // 斯诺克/美式/开伦
status: { type: Number }, // 0-空闲 1-使用中 2-维护中
priceRules: [{ // 时段定价规则
startTime: String, // "14:00"
endTime: String,
weekday: Number, // 0-6代表周日到周六
price: Number
}]
});
// 订单模型
const OrderSchema = new Schema({
userId: { type: ObjectId, ref: 'User' },
tableId: { type: String },
startTime: { type: Date }, // 预约开始时间
endTime: { type: Date },
actualFee: { type: Number } // 实付金额
});
关键技巧:使用MongoDB的TTL索引自动清理过期订单
javascript复制OrderSchema.index({ createTime: 1 }, { expireAfterSeconds: 1800 }); // 30分钟未支付自动取消
3. 核心功能实现细节
3.1 台桌状态实时同步
采用WebSocket+Redis发布订阅模式:
- 服务端维护WebSocket连接池
- 台桌状态变更时发布Redis消息
- 所有节点订阅消息并广播给客户端
javascript复制// WebSocket服务核心代码
const wss = new WebSocket.Server({ port: 8081 });
const redis = require("redis");
const sub = redis.createClient();
wss.on('connection', ws => {
// 订阅台桌状态频道
sub.subscribe('table-status');
ws.on('message', msg => {
// 处理客户端消息
});
});
// Redis消息转发
sub.on('message', (channel, msg) => {
wss.clients.forEach(client => {
client.send(msg);
});
});
3.2 高并发锁座逻辑
解决"超卖"问题的关键代码:
javascript复制async function reserveTable(tableId, timeSlot) {
const lockKey = `lock:${tableId}:${timeSlot}`;
// 获取Redis分布式锁
const locked = await redis.set(lockKey, '1', 'NX', 'EX', 5);
if (!locked) throw new Error('当前时段正在被其他用户预定');
try {
// 检查库存
const available = await checkInventory(tableId, timeSlot);
if (!available) throw new Error('台桌已订满');
// 创建预订单
const order = await createOrder(tableId, timeSlot);
// 扣减库存
await decreaseInventory(tableId, timeSlot);
return order;
} finally {
// 释放锁
await redis.del(lockKey);
}
}
4. 性能优化实践
4.1 前端渲染优化
针对台桌列表的虚拟滚动实现:
vue复制<template>
<div class="viewport" @scroll="handleScroll">
<div class="list" :style="{ height: totalHeight + 'px' }">
<div
v-for="item in visibleItems"
:key="item.id"
:style="{ transform: `translateY(${item.offset}px)` }"
>
<!-- 台桌项内容 -->
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
allItems: [], // 所有台桌数据
visibleItems: [], // 当前可视区域数据
itemHeight: 80, // 每项高度
bufferSize: 5 // 缓冲项数
}
},
computed: {
totalHeight() {
return this.allItems.length * this.itemHeight;
}
},
methods: {
handleScroll() {
const scrollTop = this.$el.scrollTop;
const startIdx = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.bufferSize);
const endIdx = Math.min(
this.allItems.length,
Math.ceil((scrollTop + this.$el.clientHeight) / this.itemHeight) + this.bufferSize
);
this.visibleItems = this.allItems
.slice(startIdx, endIdx)
.map((item, i) => ({
...item,
offset: (startIdx + i) * this.itemHeight
}));
}
}
}
</script>
4.2 后端缓存策略
三级缓存架构设计:
- 浏览器缓存:静态资源设置Cache-Control
- CDN缓存:配置边缘节点缓存策略
- 服务端缓存:
- Redis缓存热点数据(如台桌状态)
- 本地内存缓存配置信息(如价格规则)
javascript复制// 带缓存的台桌查询接口
router.get('/tables', async (req, res) => {
const cacheKey = 'tables:list';
let data = await redis.get(cacheKey);
if (!data) {
data = await Table.find().lean();
// 设置缓存,过期时间10分钟
await redis.setex(cacheKey, 600, JSON.stringify(data));
} else {
data = JSON.parse(data);
}
res.json(data);
});
5. 安全防护方案
5.1 防黄牛脚本措施
实现人机验证与限流:
javascript复制// 滑动验证码中间件
const slidingCaptcha = require('sliding-captcha');
app.post('/api/order',
slidingCaptcha.middleware(), // 验证滑动行为
rateLimit({ windowMs: 60*1000, max: 5 }), // 每分钟5次
async (req, res) => {
// 正常业务逻辑
}
);
5.2 支付安全加固
采用双重校验机制:
- 前端生成订单时获取临时令牌
- 支付回调时验证签名+令牌
javascript复制// 支付回调验证
router.post('/pay/notify', async (req, res) => {
const { orderId, sign, token } = req.body;
// 验证签名
const validSign = verifySign(sign, orderId);
if (!validSign) return res.status(403).send('Invalid sign');
// 验证令牌
const savedToken = await redis.get(`pay:token:${orderId}`);
if (token !== savedToken) return res.status(403).send('Invalid token');
// 更新订单状态
await updateOrderStatus(orderId, 'paid');
res.send('success');
});
6. 部署与监控
6.1 Docker容器化部署
典型的生产环境docker-compose.yml配置:
yaml复制version: '3'
services:
web:
image: node:16
working_dir: /app
volumes:
- ./:/app
ports:
- "3000:3000"
command: npm start
depends_on:
- redis
- mongo
redis:
image: redis:6
ports:
- "6379:6379"
volumes:
- redis_data:/data
mongo:
image: mongo:5
ports:
- "27017:27017"
volumes:
- mongo_data:/data/db
volumes:
redis_data:
mongo_data:
6.2 性能监控配置
使用PM2+ELK栈监控方案:
bash复制# 安装PM2监控
npm install pm2 -g
pm2 start server.js --name "ticket-system"
# 配置日志收集
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 30
在Kibana中关键监控指标:
- 接口响应时间P99 < 500ms
- WebSocket连接数波动监控
- 订单创建失败率告警阈值0.5%
7. 实际运营中的经验教训
时段定价的坑:初期直接按整点划分时段,导致用户故意订59分避开高价时段。改进方案:
javascript复制// 优化后的时段计算逻辑
function getTimeSlot(startTime) {
const minutes = startTime.getHours() * 60 + startTime.getMinutes();
// 按30分钟颗粒度划分时段
return Math.floor(minutes / 30);
}
移动端适配技巧:测试发现iOS Safari的Date解析与Android存在差异,统一使用moment.js处理时间:
javascript复制// 错误写法
new Date('2023-07-15 14:00');
// 正确写法
moment('2023-07-15 14:00', 'YYYY-MM-DD HH:mm').toDate();
这个项目让我深刻体会到,台球馆管理系统不是简单的CRUD应用,需要处理复杂的资源调度、实时状态同步和商业规则。建议后续可扩展会员积分体系、教练预约等功能模块
