
简介这份资源是面向高校计算机相关专业学生与小程序开发初学者的毕业设计/课程设计实战项目主题为基于微信小程序的图书馆座位预约系统前后端代码与数据库脚本一并提供可帮助读者完成从选题到答辩的完整开发流程。压缩包共442个文件约4.04MB以Java后端源码、Vue前端页面、微信小程序wxml与wxss页面、JSON配置及PNG界面截图为主另含SQL数据库脚本、properties配置、md说明文档与少量jar依赖覆盖服务端、管理端与小程序端三部分。项目代码附有注释新手也能对照理解业务逻辑与接口调用。目前已有881人学习下载适合需要现成赛题方案、可运行源码与数据库结构的同学参考便于快速搭建环境、梳理座位预约与后台管理模块的实现思路。1. 图书馆座位预约小程序从“占座大战”到扫码即坐这套源码到底能跑通什么图书馆早上七点半开门七点门口已经排了三十多人门一开全冲进去占座然后人去吃早饭——这个场景在高校里年年上演。座位预约系统要解决的就是这件事把“先到先得”变成“先约先得”用规则替代体力。基于微信小程序的图书馆座位预约系统核心链路其实就四条学生端选座预约、扫码签到、暂离与释放、管理端配置座位和查看统计。这套毕业设计源码加数据库的组合适合两类人一是要交毕设、需要一套能跑起来、能讲清楚、能改得动的完整项目二是想学小程序全栈开发拿一个真实业务场景练手。它不复杂但麻雀虽小五脏俱全用户、座位、预约、签到、违约、统计六个模块一个不少。下面我按“怎么跑起来、怎么改、坑在哪”的顺序拆一遍。2. 技术选型与数据库设计为什么这套组合最适合毕设落地2.1 小程序端 后端服务 数据库的三层结构常见做法是微信小程序做前端后端用 Node.jsExpress 或 Koa或者 JavaSpring Boot数据库用 MySQL。为什么推荐这个组合因为毕设答辩时老师最常问的三个问题是数据存哪、接口怎么调、并发怎么办。MySQL 能答第一个RESTful 接口能答第二个加个乐观锁或 Redis 能答第三个。小程序端用原生 WXML WXSS JS 就够了不需要上 uni-app 或 Taro原生写法在答辩时更好讲清楚生命周期和页面栈。后端我一般会分成四层路由层接请求、控制层做参数校验、服务层写业务逻辑、数据层操作数据库。这样分层的好处是预约冲突判断、签到超时释放这些逻辑都集中在服务层改规则时不用翻遍所有文件。// 服务层预约核心逻辑Node.js Express 风格 async function createReservation(userId, seatId, startTime, endTime) { // 1. 检查该座位在时间段内是否已被预约 const conflict await db.query( SELECT id FROM reservation WHERE seat_id ? AND status IN (reserved,checked_in) AND start_time ? AND end_time ?, [seatId, endTime, startTime] ); if (conflict.length 0) { throw new Error(该座位在所选时段已被预约); } // 2. 检查用户是否有未完成的违约记录 const violation await db.query( SELECT COUNT(*) AS cnt FROM reservation WHERE user_id ? AND status violated AND created_at DATE_SUB(NOW(), INTERVAL 7 DAY), [userId] ); if (violation[0].cnt 3) { throw new Error(违约次数过多暂无法预约); } // 3. 写入预约记录 const result await db.query( INSERT INTO reservation (user_id, seat_id, start_time, end_time, status, created_at) VALUES (?, ?, ?, ?, reserved, NOW()), [userId, seatId, startTime, endTime] ); return result.insertId; }这段代码的关键在第一个查询用start_time endTime AND end_time startTime判断时间段重叠这是区间重叠的标准写法比逐分钟比对高效得多。第二个查询是违约惩罚逻辑7 天内违约 3 次就禁止预约这个阈值可以按学校规则调整。第三个写入操作没有加锁高并发下可能出问题后面避坑章节会讲怎么补。2.2 数据库表设计六张表撑起整个系统数据库设计是毕设的重头戏老师一定会看 ER 图。核心六张表用户表、座位表、楼层/区域表、预约表、签到记录表、违约记录表。下面给出关键字段和建表语句。-- 用户表区分学生和管理员 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) UNIQUE NOT NULL COMMENT 微信openid, student_no VARCHAR(20) UNIQUE COMMENT 学号, name VARCHAR(20) NOT NULL, role TINYINT DEFAULT 0 COMMENT 0学生 1管理员, credit_score INT DEFAULT 100 COMMENT 信用分, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 座位表每个座位属于某个区域 CREATE TABLE seat ( id INT PRIMARY KEY AUTO_INCREMENT, area_id INT NOT NULL, seat_no VARCHAR(10) NOT NULL COMMENT 座位编号如A-01, has_power TINYINT DEFAULT 0 COMMENT 是否有电源, status TINYINT DEFAULT 1 COMMENT 1可用 0维护中, UNIQUE KEY uk_area_seat (area_id, seat_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 预约表核心业务表 CREATE TABLE reservation ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, seat_id INT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status VARCHAR(20) DEFAULT reserved COMMENT reserved/checked_in/completed/cancelled/violated, checkin_time DATETIME DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_seat_time (seat_id, start_time, end_time), INDEX idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;索引设计是容易被忽略的点。idx_seat_time这个联合索引直接服务于冲突检测查询没有它预约表数据量上去后查询会明显变慢。utf8mb4字符集是为了支持昵称里的 emoji用utf8会报错。2.3 座位状态流转一张图讲清所有状态变化预约状态不是随便改的得有明确的状态机。reserved已预约→checked_in已签到→completed已完成reserved超时未签到 →violated违约reserved用户主动取消 →cancelled。管理端只能操作seat表的status字段可用/维护不能直接改预约状态。这个边界要在代码里卡死否则后期数据会乱。3. 小程序端核心页面实现从选座到签到的完整链路3.1 选座页面用 canvas 还是用 view 拼座位图座位图有两种画法canvas 绘制和 view 绝对定位。canvas 性能好但交互麻烦view 方案代码直观、点击事件好绑定座位数在 200 以内推荐 view 方案。每个座位是一个绝对定位的 view背景色表示状态绿色可选、灰色已占、蓝色自己约的、黄色维护中。// pages/seat/seat.js Page({ data: { seats: [], // 座位列表 selectedSeat: null, areaId: 1 }, onLoad(options) { this.setData({ areaId: options.areaId || 1 }); this.loadSeats(); }, async loadSeats() { const res await wx.request({ url: ${app.globalData.baseUrl}/api/seat/list, data: { areaId: this.data.areaId } }); // 给每个座位计算显示状态 const seats res.data.map(seat ({ ...seat, displayStatus: this.getDisplayStatus(seat) })); this.setData({ seats }); }, getDisplayStatus(seat) { if (seat.status 0) return maintenance; if (seat.reservedByMe) return mine; if (seat.isOccupied) return occupied; return available; }, onSeatTap(e) { const seat e.currentTarget.dataset.seat; if (seat.displayStatus ! available) return; this.setData({ selectedSeat: seat }); } });getDisplayStatus这个方法把后端返回的原始数据转成前端显示状态逻辑集中在一处改规则时只动这里。注意reservedByMe和isOccupied是后端在查询时根据当前用户和时间段计算好的前端不做业务判断这是前后端分离的基本原则。3.2 扫码签到wx.scanCode 的坑与签到时间窗口签到用wx.scanCode扫座位上的二维码二维码内容就是座位编号。签到有两个关键参数签到时间窗口和暂离时长。常见设置是预约开始后 15 分钟内可签到超过 15 分钟未签到自动释放并记违约签到后可以暂离一次暂离时长 30 分钟超时未回也记违约。// 扫码签到 async onScanCheckin() { const scanRes await wx.scanCode({ scanType: [qrCode] }); const seatNo scanRes.result; // 二维码内容为座位编号 const res await wx.request({ url: ${app.globalData.baseUrl}/api/reservation/checkin, method: POST, data: { seatNo: seatNo, reservationId: this.data.currentReservationId } }); if (res.data.code 0) { wx.showToast({ title: 签到成功, icon: success }); } else { wx.showToast({ title: res.data.msg, icon: none }); } }后端签到接口要做三件事校验预约是否存在且状态为reserved、校验当前时间是否在签到窗口内、更新状态为checked_in并写入checkin_time。这三步必须在一个事务里否则可能出现状态改了但签到时间没写进去的情况。3.3 我的预约列表分页加载与状态筛选预约列表要支持按状态筛选全部/进行中/已完成/已取消并且分页加载。小程序端用scroll-view或页面的onReachBottom做触底加载。// 分页加载预约列表 data: { list: [], page: 1, hasMore: true, status: all }, async loadList() { if (!this.data.hasMore) return; const res await wx.request({ url: ${app.globalData.baseUrl}/api/reservation/my, data: { page: this.data.page, size: 10, status: this.data.status } }); const newList this.data.list.concat(res.data.list); this.setData({ list: newList, page: this.data.page 1, hasMore: res.data.list.length 10 }); }, onReachBottom() { this.loadList(); }hasMore的判断用返回条数是否等于 pageSize简单可靠。状态筛选切换时要重置page和list否则会串数据。4. 管理端与定时任务座位配置、违约处理与自动释放4.1 管理端功能座位批量导入与区域配置管理端不需要做得多漂亮但功能要全。核心三个页面区域管理增删改查、座位管理单个添加/批量导入/维护状态切换、预约统计按天/周/月看使用率。批量导入座位用 Excel 解析前端上传文件后端用xlsx库解析后批量插入。// 后端批量导入座位 const XLSX require(xlsx); async function importSeats(filePath, areaId) { const workbook XLSX.readFile(filePath); const sheet workbook.Sheets[workbook.SheetNames[0]]; const rows XLSX.utils.sheet_to_json(sheet); const values rows.map(row [ areaId, row[座位编号], row[是否有电源] 是 ? 1 : 0, 1 ]); // 批量插入忽略重复 await db.query( INSERT IGNORE INTO seat (area_id, seat_no, has_power, status) VALUES ?, [values] ); }INSERT IGNORE配合唯一索引uk_area_seat重复的座位编号会自动跳过不会报错中断。Excel 模板要固定列名导入前先校验列名是否匹配不匹配直接返回错误提示。4.2 定时任务超时未签到自动释放与违约记录这是整个系统最容易被忽略但答辩时最容易被问的部分。用node-cron或setInterval每分钟跑一次扫描所有status reserved且start_time 15分钟 当前时间的预约将其状态改为violated同时写入违约记录表并扣信用分。// 定时任务每分钟检查超时未签到的预约 const cron require(node-cron); cron.schedule(* * * * *, async () { const now new Date(); // 查出所有超时未签到的预约 const expired await db.query( SELECT id, user_id FROM reservation WHERE status reserved AND DATE_ADD(start_time, INTERVAL 15 MINUTE) ?, [now] ); for (const item of expired) { await db.query( UPDATE reservation SET status violated WHERE id ?, [item.id] ); await db.query( INSERT INTO violation (user_id, reservation_id, reason, created_at) VALUES (?, ?, 超时未签到, NOW()), [item.user_id, item.id] ); await db.query( UPDATE user SET credit_score credit_score - 10 WHERE id ?, [item.user_id] ); } });这个任务要注意幂等性如果某次执行失败下次执行不能重复处理同一条记录。解决办法是在更新状态时加AND status reserved条件确保只有还是reserved状态的才被更新。4.3 统计报表用 SQL 聚合代替前端计算管理端要看座位使用率、高峰时段、违约率。这些数据全部用 SQL 聚合查询完成不要把原始数据拉到前端算。-- 查询某区域本周各时段使用率 SELECT HOUR(start_time) AS hour_slot, COUNT(*) AS reservation_count, COUNT(DISTINCT seat_id) AS seat_used FROM reservation WHERE area_id ? AND start_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) AND status IN (checked_in, completed) GROUP BY HOUR(start_time) ORDER BY hour_slot;这个查询按小时统计预约量COUNT(DISTINCT seat_id)得到实际使用的座位数两者结合就能算出使用率。注意只统计checked_in和completed状态violated和cancelled不算实际使用。5. 避坑与排查六个让毕设翻车的细节5.1 预约冲突并发下两个用户约到同一个座位现象压测时发现同一时间段同一座位出现两条reserved记录。原因冲突检测和插入之间有时间窗口两个请求同时通过检测。解决在reservation表上加唯一索引UNIQUE KEY uk_seat_time (seat_id, start_time)或者在插入时用SELECT ... FOR UPDATE锁行。更稳妥的做法是用 Redis 分布式锁但毕设环境用数据库唯一索引就够了。5.2 签到时间窗口服务器时间和手机时间不一致现象用户手机时间比服务器快 2 分钟在窗口边缘签到失败。原因签到校验用的是服务器时间用户看到的是手机时间。解决所有时间判断以服务器为准前端只做展示。签到接口返回明确的剩余时间提示比如“签到窗口还剩 3 分钟”。5.3 小程序登录态openid 获取失败导致白屏现象首次进入小程序wx.login返回的 code 换取 openid 失败页面卡在加载中。原因后端接口没配好或者appid和secret不匹配。解决后端换取 openid 的接口要加错误日志前端加超时和重试机制失败时显示“登录失败请退出重试”而不是一直转圈。5.4 数据库字符集昵称带 emoji 插入报错现象用户昵称里有 emoji插入user表时报Incorrect string value。原因数据库或表的字符集是utf8而不是utf8mb4。解决建表时统一用utf8mb4连接字符串也要加charsetutf8mb4。已经建好的表用ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4改。5.5 定时任务重复执行多实例部署下的双跑问题现象本地开发时定时任务正常部署到服务器后违约记录出现两条。原因如果后端起了多个实例每个实例都会跑定时任务。解决毕设环境一般单实例问题不大如果要多实例用 Redis 锁或数据库锁保证同一时间只有一个实例执行。简单做法是在任务开始时往数据库写一条锁记录执行完删除。5.6 二维码内容座位编号被篡改签到别人座位现象学生把 A-01 的二维码截图改成 A-02 的内容去签到。原因二维码只存了座位编号没有防篡改机制。解决二维码内容用seatNo 签名的形式签名用 HMAC 生成后端校验签名通过才允许签到。签名密钥存在后端前端不暴露。6. 从能跑到能答辩三个让老师眼前一亮的进阶技巧第一个技巧是加一个简单的压力测试脚本。用autocannon或ab对预约接口发 100 个并发请求看冲突检测是否生效、响应时间是否在可接受范围。把测试结果截图放进论文比空口说“系统支持高并发”有说服力得多。# 用 autocannon 压测预约接口 npx autocannon -c 100 -d 10 -m POST \ -H Content-Type: application/json \ -b {userId:1,seatId:5,startTime:2025-06-01 08:00:00,endTime:2025-06-01 10:00:00} \ http://localhost:3000/api/reservation/create-c 100是 100 个并发连接-d 10是持续 10 秒。重点看non-2xx responses和latency两个指标。如果出现大量 500 错误说明冲突检测没做好如果延迟超过 500ms说明索引没建对。第二个技巧是给预约表加一个“信用分”联动机制。初始 100 分违约扣 10 分签到加分 1 分上限 100信用分低于 60 分禁止预约。这个规则写进服务层答辩时讲“用信用体系引导用户行为”比单纯说“违约禁止预约”更有层次。第三个技巧是导出统计报表为 Excel。管理端加一个导出按钮后端用exceljs生成文件流返回。这个功能代码量不大但演示时很直观老师能看到“数据可导出、可分析”。// 导出预约统计为 Excel const ExcelJS require(exceljs); async function exportStats(res) { const workbook new ExcelJS.Workbook(); const sheet workbook.addWorksheet(预约统计); sheet.columns [ { header: 日期, key: date, width: 15 }, { header: 预约数, key: count, width: 10 }, { header: 签到数, key: checkin, width: 10 }, { header: 违约数, key: violation, width: 10 } ]; const data await db.query( SELECT DATE(created_at) AS date, COUNT(*) AS count, SUM(status checked_in) AS checkin, SUM(status violated) AS violation FROM reservation GROUP BY DATE(created_at) ); data.forEach(row sheet.addRow(row)); res.setHeader(Content-Type, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); res.setHeader(Content-Disposition, attachment; filenamestats.xlsx); await workbook.xlsx.write(res); res.end(); }这个导出功能的关键是Content-Disposition头没有它浏览器会直接打开文件而不是下载。SUM(status checked_in)是 MySQL 的布尔求和写法比CASE WHEN简洁。我自己的习惯是每次改完业务逻辑先跑一遍定时任务的手动触发版本确认违约记录和信用分扣减都对再去调前端。因为定时任务是后台跑的出了问题不容易发现手动触发一次能省很多排查时间。希望帮到你。本文还有配套的精品资源点击获取