自习室座位管理系统是近年来在高校和共享办公场景中快速普及的数字化解决方案。传统自习室普遍存在占座、座位利用率低、管理混乱等问题。我们团队开发的这套基于Node.js+Vue.js的全栈系统,通过扫码签到、预约锁定、使用时长统计等功能模块,实现了座位资源的智能化分配。
这个系统最核心要解决三个痛点:
技术选型方面,后端采用Node.js主要考虑其异步I/O特性适合高并发的预约请求处理,配合Express框架可以快速构建RESTful API。前端选择Vue.js因其渐进式特性和丰富的UI组件库,能够快速开发响应式管理界面。
整个系统采用前后端分离架构:
核心表结构包括:
sql复制CREATE TABLE `seat` (
`id` int NOT NULL AUTO_INCREMENT,
`zone` varchar(10) NOT NULL COMMENT '区域编号',
`number` varchar(10) NOT NULL COMMENT '座位编号',
`status` tinyint NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_zone_number` (`zone`,`number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
采用RESTful风格设计API,主要端点包括:
前端通过WebSocket建立长连接,当座位状态变化时,后端推送广播消息:
javascript复制// WebSocket服务端
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', (message) => {
// 处理座位状态变更
broadcast(JSON.stringify(updateSeatStatus(message)));
});
});
function broadcast(data) {
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
}
使用数据库事务保证并发预约的原子性:
javascript复制async function createBooking(userId, seatId, startTime, endTime) {
const connection = await mysql.getConnection();
try {
await connection.beginTransaction();
// 检查座位是否可用
const [rows] = await connection.query(
'SELECT status FROM seat WHERE id = ? FOR UPDATE',
[seatId]
);
if (rows[0].status !== 0) {
throw new Error('座位已被占用');
}
// 插入预约记录
await connection.query(
'INSERT INTO booking SET ?',
{ user_id: userId, seat_id: seatId, start_time: startTime, end_time: endTime }
);
// 更新座位状态
await connection.query(
'UPDATE seat SET status = 1 WHERE id = ?',
[seatId]
);
await connection.commit();
return true;
} catch (err) {
await connection.rollback();
throw err;
} finally {
connection.release();
}
}
javascript复制// 二维码生成(前端)
import QRCode from 'qrcode';
const generateQrCode = (seatId) => {
return QRCode.toDataURL(`seat:${seatId}`);
};
// 签到验证(后端)
app.post('/api/checkin', async (req, res) => {
const { seatId, userId } = req.body;
const [booking] = await pool.query(
`SELECT * FROM booking
WHERE seat_id = ? AND user_id = ?
AND start_time <= NOW()
AND end_time >= NOW()`,
[seatId, userId]
);
if (!booking.length) {
return res.status(400).json({ error: '无效的签到请求' });
}
// 更新实际使用时间
await pool.query(
`UPDATE booking SET actual_start = NOW()
WHERE id = ?`,
[booking[0].id]
);
res.json({ success: true });
});
使用SVG动态渲染座位图,不同状态显示不同颜色:
vue复制<template>
<div class="seat-map">
<svg :viewBox="`0 0 ${width} ${height}`">
<rect
v-for="seat in seats"
:key="seat.id"
:x="seat.x"
:y="seat.y"
width="30"
height="30"
:fill="getSeatColor(seat.status)"
@click="handleSeatClick(seat)"
/>
</svg>
</div>
</template>
<script>
export default {
methods: {
getSeatColor(status) {
return {
0: '#4CAF50', // 空闲-绿色
1: '#F44336', // 使用中-红色
2: '#FFC107' // 维修中-黄色
}[status];
}
}
}
</script>
使用ECharts实现使用率统计:
javascript复制async function generateUsageReport() {
const [dailyData] = await pool.query(`
SELECT
DATE(start_time) AS date,
COUNT(*) AS total,
SUM(TIMESTAMPDIFF(MINUTE, actual_start, end_time)) AS minutes
FROM booking
WHERE actual_start IS NOT NULL
GROUP BY DATE(start_time)
ORDER BY date DESC
LIMIT 7
`);
return {
dates: dailyData.map(item => item.date),
counts: dailyData.map(item => item.total),
minutes: dailyData.map(item => item.minutes)
};
}
bash复制# PM2启动命令
pm2 start ecosystem.config.js --env production
# ecosystem.config.js示例
module.exports = {
apps: [{
name: 'seat-booking',
script: 'app.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production'
}
}]
}
对高频访问的座位状态数据使用Redis缓存:
javascript复制const redis = require('redis');
const client = redis.createClient();
// 获取座位状态(先查缓存)
async function getSeatStatus(seatId) {
const cacheKey = `seat:${seatId}`;
return new Promise((resolve, reject) => {
client.get(cacheKey, async (err, reply) => {
if (reply) {
resolve(JSON.parse(reply));
} else {
const [seat] = await pool.query('SELECT * FROM seat WHERE id = ?', [seatId]);
client.setex(cacheKey, 60, JSON.stringify(seat[0])); // 缓存60秒
resolve(seat[0]);
}
});
});
}
javascript复制// 预约频率限制中间件
const rateLimit = require('express-rate-limit');
const bookingLimiter = rateLimit({
windowMs: 30 * 60 * 1000, // 30分钟
max: 3,
message: '操作过于频繁,请稍后再试'
});
app.post('/api/bookings', bookingLimiter, bookingController.create);
javascript复制// 使用mysql2的预处理语句
const [rows] = await pool.execute(
'SELECT * FROM user WHERE id = ? AND status = ?',
[userId, 1]
);
使用Flexbox+媒体查询实现多端适配:
css复制/* 座位列表响应式布局 */
.seat-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
@media (max-width: 768px) {
.seat-list {
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
}
}
通过uni-app跨平台方案输出小程序版本:
javascript复制// 小程序端签到逻辑
uni.scanCode({
success: (res) => {
const seatId = res.result.split(':')[1];
uni.request({
url: 'https://api.example.com/checkin',
method: 'POST',
data: { seatId, userId },
success: (res) => {
uni.showToast({ title: '签到成功' });
}
});
}
});
在某高校图书馆的部署数据:
性能优化前后对比:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 数据库查询耗时 | 350ms | 80ms |
| 内存占用 | 1.2GB | 600MB |
| 最大并发量 | 80 | 150 |
关键优化措施:
基于用户历史数据推荐合适座位:
javascript复制function recommendSeats(userId) {
// 获取用户历史偏好
const history = await getUserHistory(userId);
// 计算推荐分数
const seats = await getAvailableSeats();
return seats.map(seat => {
let score = 0;
// 距离偏好区域的距离分
score += calculateDistanceScore(seat, history.preferredZones);
// 设备需求匹配分
if (seat.hasOutlet && history.needOutlet) {
score += 20;
}
return { ...seat, score };
}).sort((a, b) => b.score - a.score);
}
可选集成方案:
python复制# Python人脸识别示例
import cv2
def recognize_face(image):
face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray, 1.3, 5)
return len(faces) > 0
在三个月的开发周期中,我们遇到了几个关键挑战:
高并发下的座位状态同步问题
移动端扫码识别率低
预约规则复杂性管理
对于想要开发类似系统的开发者,我的建议是:
系统未来可能的改进方向: