自习室预约
微信小程序 + Node 后端:座位图选座、次卡扣次、管理员开卡充值,后端已上线运行
FEATURES
可视化选座
一楼/二楼切换、按日期选座,座位状态区分空座、已预约、月卡占座与已选
扣次锁座事务
单一 SQLite 事务内先查日预约与月卡占用冲突,再复查卡内余次,最后插入预约并扣次
卡券与购买
次数卡·10 次 / 30 次与月卡·30 天三档,购买后进入我的卡券,预约时自动扣次
管理员端
营收概览、营业时间配置、用户列表与发卡、会员卡充值、座位占用与释放、卡种增删改、操作日志
33 个后端接口
用户侧 16 个 + 管理端 17 个,10 张表的数据模型,21 个座位启动时自动播种
后端已上线
PM2 托管 + nginx 反代,线上接口实测返回真实数据(营业时间、座位布局、卡种)
TECH
- ▸微信小程序原生(11 个页面)+ Node/Express/better-sqlite3 后端(645 行)
- ▸10 张表:users / user_sessions / card_products / user_cards / orders / seats / bookings / month_occupancy / admin_logs / settings
- ▸bookings(seat_id, book_date) 唯一索引兜底重复预约,冲突返回 409 SEAT_TAKEN
- ▸PM2 进程 study-room-server + nginx 反代 /study/ 路径,与同域其它服务共存
项目概览
自习室的座位预约系统,顾客端与管理员端一体。
顾客端:微信登录后用「一楼 / 二楼」切换楼层,按日期在座位图上点选空座,确认预约时从次卡扣减次数;「我的」页有卡券、预约与消费记录,管理员还会多一个入口。
管理员端:营收与座位占度概览、营业时间配置、用户列表与发卡、会员卡充值、座位手动占用/释放、按日预约查询与取消、卡种增删改、操作日志。
并发下的座位安全
预约是整个项目里唯一有真正并发风险的地方——两个人可能同时选中同一个座位。实现方式是把这些操作全部收进一个 SQLite 事务:
先在事务内查这个「座位 + 日期」是否已有预约,再查是否被月卡长期占用,两者命中就抛 SEAT_TAKEN;然后复查次卡是否处于有效状态且余次足够,不足抛 NO_CARD;两步都过了才插入预约记录并扣减次数。数据库层再加一道 bookings(seat_id, book_date) 唯一索引兜底,冲突统一返回 409。
better-sqlite3 的同步事务保证了这个过程的原子性,不需要额外的锁。
部署
后端用 PM2 托管,nginx 把 /study/ 路径反代到本地服务,与同域的作品集站、另一个小程序后端共用一台服务器。
线上接口是活的:营业时间返回 08:30 - 23:30,座位布局返回真实座位,卡种返回次数卡·10 次 ¥10 / 30 次 ¥25 / 月卡三档。
真实的边界
这个后端虽然在线运行,但还没有面向真实用户开放,原因有三个:
- 登录是模拟的:
.env里的WX_APPID与WX_APPSECRET是空的,登录逻辑走 mock 分支,所有用户会共用一个账号。 - 支付是模拟的:购买页顶部明确写着「当前为模拟支付,正式上线将接入微信支付」,订单支付方式记为
simulate,没有调用微信支付。 - 前端还没切生产:
config.js里的apiBase仍指向127.0.0.1:3001,生产地址只写在注释里。
另外文档有个不一致处:README 说服务跑在 3002 端口,nginx 配置里写的是 3001。
SCREENSHOTS

一楼 / 二楼切换,营业时间 08:30 - 23:30,按日期选择天数。座位图区分空座、已预约、月卡占座与已选四种状态,底部提示「已选 1 天 · 扣 1 次」后确认预约。

三档卡券:次数卡·10 次 ¥10、次数卡·30 次 ¥25、月卡·30 天 ¥99。页面顶部明确标注「当前为模拟支付,正式上线将接入微信支付」。

个人页展示待预约 / 次数卡 / 月卡 / 累计消费四项统计,入口包括我的预约与消费、我的卡券、管理员入口与退出登录。