资讯动态

校园洗衣预约系统实战:从订单状态机到AI排队预测

发布时间:2026/10/18 12:08:50 来源:尧图企业网站定制
1. 先想清楚校园洗衣预约到底在解决什么问题1.1 绝大多数预约系统只做了“占坑”没解决高峰我看了不少打着“校园洗衣预约”旗号的项目大部分其实就是一个标准的增删改查系统用户注册、设备列表、预约下单、管理员审核再加一个看起来很有排面的数据看板。功能看着完整但放到真实的宿舍楼场景里你会发现它只解决了“把坑占住”这件事完全没有解决“什么时候去洗不用等”这个最核心的痛点。校园洗衣场景和外面商圈的洗衣房不一样它有非常明显的高峰期和地理分散性。晚自习下课到熄灯前那一个多小时所有楼栋的洗衣机几乎同时满载队伍排到洗衣机门外都是常态而白天上课时段大部分机器又空着。用户需要的不是一个只能“选中时间下单”的系统而是能提前告诉他“现在去洗大概率要排队半小时建议21:40之后再来”的智能提醒。这个区别恰恰是AI模块在预约平台里真正的价值所在。所以我在设计这类项目时习惯先把问题拆成三层第一层是用预约机制解决“资源占用冲突”第二层是用状态流转解决“订单生命周期管理”第三层才是用历史数据解决“供需平衡的预测与引导”。很多项目只做到第一层就停了后面两层才是能让系统从“能用”变成“好用”的分水岭。1.2 技术栈取舍为什么是 Python Flask技术选型这件事我建议不要盲目跟风。做一个校园预约平台可选的方案不少我把常见路线的优劣势拉出来对比过技术方案上手门槛适合规模和AI结合的便利度踩坑成本Python Flask低小到中型极佳机器学习生态直接调用低Python Django中中到大型好但重了一点中Spring Boot高中到大型中等需要额外桥接高Node.js Express低到中中小型一般Python模型要单独起服务中我的选择理由很简单这个项目既要做Web业务又要跑预测模型和推荐算法。如果前后端各拿一套技术栈模型训练好之后还得单独封装一个推理服务对于小团队来说凭空多出一大截联调工作。Flask作为轻量Web框架可以直接把训练好的模型文件加载到进程里在路由里同步做推理整个链路短得多。Flask的另一大优势是灵活路由、中间件、蓝图都可以按需组织。你不用担心它“太简单所以撑不起项目”——预约平台的核心功能无非是接口、数据库事务、并发控制Flask配合缓存中间件完全能扛住给学生规模的日常并发绰绰有余。如果你确实要用异步任务或者更复杂的限流也有对应的扩展可以做不至于像Django那样框架约束太多改起来掣肘。1.3 系统角色与业务闭环在动手写代码之前我通常会先画清楚角色和状态流这里直接描述一遍完整闭环用户打开系统先选楼栋和洗衣房看到每个时段过去一周的平均排队指数。系统根据当前设备占用率和AI预测结果给用户推荐未来几个“低拥挤”时段。用户选定洗衣机和时段提交订单并完成支付或者使用信用额度锁定。到了预约时间用户到现场扫码启用机器状态变为“使用中”。使用完成状态变为“已完成”释放机器时段配额。期间用户可以选择取消或超时未启用自动释放设备故障时管理端可介入取消。这个链路听起来不复杂但每个环节背后都对应一个重要设计时段怎么切、订单状态怎么流转、并发冲突怎么防、预测结果从哪里来。下面把每一层都拆开讲。2. 数据模型怎么设计订单状态机比字段清单更值得先想2.1 核心表结构与字段解释很多同学建表的时候喜欢“想一步做一步”缺什么字段再加什么。预约类系统我建议先把四张核心表的结构一次性定清楚用户表、设备表、时段表、订单表。设备表不能只放机器名称和状态还要带楼栋、位置、类型洗衣机/烘干机、额定容量。因为真实场景里用户会关心“我这栋楼有没有大容量的桶”商家端也要能按楼栋维度看设备利用率。时段表的设计有讲究。我见过直接把用户选的“开始时间、结束时间”当字符串存的后面做冲突检测和统计的时候非常痛苦。更好的做法是拆分出date、start_time、end_time三个字段再配合一个时段唯一编码比如20240601-21:00-22:00这个编码会贯穿并发锁、订单表、推荐算法后面会反复用到。订单表是核心中的核心我的建议是至少要包含这些字段订单号、用户ID、设备ID、日期、开始时间、结束时间、状态、支付状态、金额、创建时间、支付流水号。其中“订单号”最好用日期随机串生成不要只靠自增主键因为后面接支付对账、客服查询都要用到业务单号。2.2 订单状态机预约流转最关键的规则比起字段清单我更愿意多花时间设计订单状态机。预约系统的所有边界情况——超时未支付、用户取消、设备故障、使用完成——本质上都是状态迁移问题。我常用的状态枚举是PENDING_PAY预约下单成功时段已锁定等待支付CONFIRMED支付成功或信用锁定生效预约确认USING用户已扫码启用设备进入洗涤流程FINISHED使用完成订单归档CANCELLED用户主动取消或超时释放REFUNDING支付退款中这里最容易忽略的规则有两条。第一PENDING_PAY状态下时段资源也要锁定否则用户下单不支付别人就约不上了。第二取消订单时不能只改订单状态必须同时释放时段码对应的锁资源否则会出现订单显示取消、但时段依然被占的“幽灵占用”。这两个细节在第五章的踩坑实录里还会展开。2.3 预约冲突检测的基础SQL在数据库层面做最基础的冲突检测SQL其实很直白。判断同一台机器、同一天、某个时间段是否已被预约核心逻辑是时间段重叠SELECT id FROM orders WHERE machine_id :machine_id AND booking_date :booking_date AND status IN (PENDING_PAY, CONFIRMED, USING) AND start_time :end_time AND end_time :start_time LIMIT 1;只要这条SQL能查出数据就说明时间段重叠应该提示用户换时段。这里很多新手会写反用了start_time :start_time AND end_time :end_time结果只能识别“完全包含”的情况把“部分重叠”全漏掉了。记住一句话两个时间段重叠的判断条件是新开始 旧结束 AND 新结束 旧开始。但必须说明这种SQL查询不是并发安全的元婴期。当两个请求同时执行这条语句时两个进程都可能查到“无重叠”然后同时插入订单造成超卖。真正的防线是靠索引约束、Redis分布式锁和数据库事务一起兜底这部分是第四章的重点。3. AI功能的落点排队预测、时段推荐与空闲提醒3.1 排队指数预测模型怎么训练AI模块是这个项目最容易“假大空”的地方。很多设计文档里写“引入深度学习模型预测洗衣需求”听起来很高大上实际上数据量和业务复杂度完全撑不起深度学习。我建议用更务实的方案把每个时段的历史订单量、平均排队时长当成预测目标用随机森林或者梯度提升树做回归效果已经非常够用而且解释性强、训练快、方便部署。特征可以从这几个维度构造星期几、是否周末校园洗衣需求的星期周期性非常明显小时段晚上九点和下午三点是截然不同的负载是否节假日寒暑假和考试周对洗衣需求影响极大当前时段设备总数设备多的话即使订单多也不一定排队历史平均排队时长一周内同一天同时段的均值是最强的基线特征训练代码示意import pandas as pd from sklearn.ensemble import RandomForestRegressor from sklearn.pipeline import Pipeline from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer # 从订单表聚合出训练集每行是一个时间段的统计 def build_training_set(order_df): feature_rows [] for (weekday, hour, is_holiday, room_id), group in order_df.groupby( [weekday, hour, is_holiday, room_id]): wait_mean group[wait_minutes].mean() order_cnt len(group) feature_rows.append({ weekday: weekday, hour: hour, is_holiday: is_holiday, room_id: room_id, machine_count: machine_count[room_id], order_cnt: order_cnt, wait_minutes: wait_mean, }) return pd.DataFrame(feature_rows) train_df build_training_set(load_order_data()) X train_df[[weekday, hour, is_holiday, room_id, machine_count, order_cnt]] y train_df[wait_minutes] model Pipeline(steps[ (pre, ColumnTransformer( transformers[(cat, OneHotEncoder(), [weekday, is_holiday, room_id])], remainderpassthrough)), (reg, RandomForestRegressor(n_estimators120, max_depth8, random_state42)), ]) model.fit(X, y) import joblib joblib.dump(model, model_wait_pred.joblib)第一次跑完建议打印一下特征重要性看看哪个特征对排队时长影响最大。我实测下来hour和is_holiday通常排在最前面说明洗衣需求的“时间聚集效应”确实很强。3.2 结合负载与个人习惯的时段推荐算法预测模型只能告诉你“这个时段大概率有多少人、要排多久”但推荐时段还需要结合当前实时负载和用户个人习惯。我的做法是设计一个评分函数对每个候选时段打分再排序取前三def score_slot(slot, user_id, device_count): wait predict_wait( weekdayslot.weekday, hourslot.hour, is_holidayslot.is_holiday, room_idslot.room_id, current_order_cntcurrent_order_cnt(slot), machine_countdevice_count, ) occupied_ratio occupied_slots(slot) / device_count habit_score user_habit_score(user_id, slot.hour) # 用户历史使用偏好的归一化分数 score 0.5 * (1 - occupied_ratio) - 0.3 * (wait / 120) 0.2 * habit_score return score这个公式的思路很直观负载越低分越高排队越久扣分越多用户平时习惯的时间段加一点偏好分。权重可以根据实际反馈调比如前期数据少时可以把habit_score的权重调低避免冷启动阶段推荐结果太偏。注意这里不能只推荐“排队最短”的时间那样会把所有人导流到同一个时段制造新的高峰。更稳的做法是在评分后做一次分散化处理比如同一小时内不同用户拿到的推荐时段做轻微扰动或者对已推荐过的人数做惩罚。这一点是推荐逻辑上线后才会暴露的坑提前讲了能帮你少走弯路。3.3 模型和Flask的低耦合集成方式AI模型和Flask集成最大的问题是“训练代码”和“运行代码”混在一起。我见过有人直接在路由函数里调用model.fit()每次请求都重新训练一次系统慢到没法看。正确做法是离线训练在线推理。训练好模型后用joblib.dump保存成文件Flask应用启动时统一加载一次from flask import Flask, jsonify, request import joblib import pandas as pd app Flask(__name__) model joblib.load(model_wait_pred.joblib) def predict_wait(weekday, hour, is_holiday, room_id, order_cnt, machine_count): df pd.DataFrame([[weekday, hour, is_holiday, room_id, order_cnt, machine_count]], columns[weekday, hour, is_holiday, room_id, order_cnt, machine_count]) return round(float(model.predict(df)[0]), 1) app.route(/api/slots/recommend, methods[GET]) def recommend_slots(): user_id request.args.get(user_id) slots build_candidate_slots() scored sorted([(score_slot(s, user_id, DEVICE_COUNT), s) for s in slots], keylambda x: x[0], reverseTrue) return jsonify(code0, data[s.to_dict() for _, s in scored[:3]])模型只加载一次后续推理是纯内存计算响应时间通常在几毫秒级别。如果后面数据量涨了可以把训练逻辑做成定时任务比如每天凌晨跑一次更新模型文件后让Flask进程重载一下即可。模型更新时需要注意一个细节Flask进程持有的是旧模型重载进程会造成短暂不可用。轻量方案是后台服务加一个版本号缓存推理时先检查模型文件版本变了就从内存替换。学生规模的系统完全可以简化成“每天定时重启一次应用”来达到同样的效果。4. 核心接口与并发防线从查设备到扫码洗衣4.1 用户端接口结构后端接口我习惯按业务模块拆蓝图预约相关的核心接口不外乎这几个GET /api/machines?room_idxx查设备列表返回设备ID、位置、状态GET /api/slots/available查空闲时段支持按设备维度筛选POST /api/order提交预约GET /api/order/list我的订单POST /api/order/cancel取消订单POST /api/order/activate扫码启用GET /api/slots/recommend获取AI推荐时段接口设计上有一个我要特别提醒的点不要把所有字段都扔给前端。比如设备列表里的内部维护状态、订单表里的支付流水号这些对用户端没有意义反而增加接口字段理解的负担。写接口之前先想清楚调用方是谁——这是区分“能用”和“好用”的关键。4.2 防超卖Redis锁 数据库兜底双保险这是整个系统技术含量最高的地方值得认真说。预约平台一旦上线最容易出现的事故就是同一台洗衣机同一个时段被两个人同时预约成功。单纯靠SQL查询无法避免竞争因为两个请求可能同时通过冲突检测。解法分两层。第一层是Redis分布式锁以“设备ID日期时段编码”作为锁的key保证同一时刻只有一个请求能进入下单流程import redis r redis.Redis(host127.0.0.1, port6379, db2) def try_lock(machine_id, slot_key): lock_key fbooking_lock:{machine_id}:{slot_key} ok r.set(lock_key, 1, nxTrue, ex8) return lock_key if ok else None def release_lock(lock_key): r.delete(lock_key)下单接口的完整流程app.route(/api/order, methods[POST]) def create_order(): data request.get_json() machine_id data[machine_id] booking_date data[booking_date] start_time data[start_time] end_time data[end_time] slot_key f{booking_date}:{start_time}-{end_time} lock_key try_lock(machine_id, slot_key) if not lock_key: return jsonify(code409, msg这个时段刚刚被别人抢了建议换个时间) try: if check_conflict(machine_id, booking_date, start_time, end_time): return jsonify(code409, msg预约冲突请重新选择时段) order create_order_record(...) db.session.commit() return jsonify(code0, dataorder.to_dict()) except Exception: db.session.rollback() return jsonify(code500, msg下单失败) finally: release_lock(lock_key)这里Redis锁的关键参数是ex8意思是最长持有8秒。为什么要设超时防止进程在锁持有期间崩溃锁变成死锁永远不释放。超时时间不要设太短要大于下单接口最坏的耗时也不要太长否则用户会一直拿到“被占用”的提示。第二层是数据库兜底。就算是Redis锁也可能因为网络分区、键过期等问题失效所以数据库层面不能裸奔。前面那个冲突检测SQL要配合唯一索引思维——在订单表上加一个联合字段比如machine_id booking_date slot_key的唯一索引一旦有重复插入数据库直接报错从底层杜绝超卖。这两层都做了才有底气应对并发测试。后面专门讲翻车场景时会给你一个压测脚本自己跑一下就明白了。4.3 扫码启用与超时释放预约完成只是业务的一部分。到了实际使用环节还有一个常见的边界问题用户预约了但没来机器该不该一直为他保留我的设计是预约确认后用户需要在预约开始时间前后一段缓冲期内扫码启用比如提前10分钟到延后15分钟超过时限订单自动标记为过期并释放时段。这样既给了用户一定的柔性空间又避免了资源被白白占用。扫码启用接口做的事很简单校验订单状态是CONFIRMED把状态改为USING同时把设备表状态改掉。这里建议用事务一次性提交两个变更否则会出现订单已经USING但设备还是空闲的脏状态。扫码用什么实现都可以接入硬件扫码枪也好、生成二维码由用户识别也好后端接口保持一致即可。4.4 商家端管理接口越简单越好校园洗衣服务的管理端不怎么需要花哨的看板最核心的是设备维护和订单处置。我的建议是保留这几个接口就够了设备列表与状态修改、异常订单取消与退款、按楼栋维度查看今日订单量和设备空闲率。不要把管理端的报表功能做太重。很多项目把大量精力放在“漂亮的数据可视化页面”上实际上真正会用管理端的人只关心两件事哪个设备坏了、哪个订单出问题了。在资源有限的前提下把订单状态机、并发控制、AI预测做好比堆图表有意义得多。5. 最容易翻车的三个真问题超卖、对账、模型冷启动5.1 并发超卖同一台洗衣机同一时段被抢两次这个场景我在压测时遇到过不止一次。你写完接口本地测试一个人预约没问题但用脚本模拟100个人同时抢同一个时段就会看到部分请求返回成功、实际订单却重复插入。原因就是没加锁或者只加了数据库层约束但没处理唯一索引冲突。我当时排查的路径是先看日志发现两个请求几乎同时通过了check_conflict查询说明冲突检测在并发下不成立再看数据库表确实没有唯一索引重复数据直接插进去了。修复方案就是前面写的双保险。压测脚本可以这样写import threading import requests results [] def submit_order(i, url, payload): resp requests.post(url, jsonpayload) results.append((i, resp.status_code, resp.json().get(code))) threads [] for i in range(100): t threading.Thread(targetsubmit_order, args(i, API_URL, PAYLOAD)) threads.append(t) t.start() for t in threads: t.join() success [r for r in results if r[2] 0] conflict [r for r in results if r[2] 409] print(f成功 {len(success)}冲突 {len(conflict)})成功数应该是1冲突数应该是99。只要成功数大于1就说明锁或者唯一约束没生效必须改完再压测。这个脚本建议一开始就写不要等上线出事了再补。5.2 支付回调和订单状态不一致做了支付功能之后最头疼的是回调问题。用户支付成功了但系统里订单还是PENDING_PAY或者支付平台回调延迟用户重复提交订单。我的经验是做好两件事。第一是幂等每个下单请求带上客户端生成的唯一请求号后端记录这个请求号重复请求直接返回已有订单不重新创建。第二是对账兜底支付成功判定不能只依赖回调还要有主动查询逻辑比如用户打开订单详情时主动向支付渠道确认一笔订单的支付状态如果回调漏了也能补上。状态机上还要加一个规则只有PENDING_PAY状态的订单才能被支付成功回调更新为CONFIRMED其他状态直接忽略回调。不然会出现用户已经取消、支付回调才到的诡异状态。5.3 AI冷启动没有历史数据怎么办这套预测模型的原料是历史订单数据可系统刚上线时根本没有任何历史数据会出现冷启动问题。我见过有人直接把预测模块砍了等到有数据再上这其实没有必要。冷启动阶段可以用规则模型代替机器学习模型没有数据时排队时长直接用“当前时段已占用设备数 / 总设备数”乘一个经验系数比如每台洗衣机的平均使用时长是35分钟负载80%以上就预测排队半小时。规则虽然粗糙但至少能给用户一个参考。上线跑两到四周后积累的数据已经足够训练初版模型再自动切换到机器学习模型即可。另一种思路是先用相邻宿舍楼的订单数据做迁移比如A楼数据少就先参考B楼同时段的负载情况。不过跨楼栋迁移会引入误差我建议只把它作为冷启动的过渡手段不要长期依赖。6. 部署与上线调优本地能跑只是第一步6.1 部署架构与进程管理本地用python app.py调试没问题但真实部署时千万别直接暴露Flask开发服务器。我常用的组合是nginx gunicorn。gunicorn负责多进程跑Flask应用nginx负责静态文件和反向代理。gunicorn启动命令可以这样gunicorn -w 4 -k gthread --threads 8 -b 127.0.0.1:8000 app:app-w 4表示4个worker进程每个进程里再开8个线程。为什么不用纯多进程或纯多线程因为GIL和内存模型决定了混合模式在IO密集型业务下性价比最高。预约系统大量操作是数据库查询、缓存读写、HTTP调用都是IO等待线程在这里很合适。要注意的是AI模型加载。每个worker进程启动时都会加载一次模型文件如果你的worker有4个内存里就有4份模型。这个数据量不大时无所谓但模型文件做到几百MB时就会很吃内存。建议把模型做小或者用共享内存方案学生规模一般不涉及到这一步但提前知道没有坏处。6.2 性能优化清单本地联调跑得飞起上线后数据一多就卡这类问题基本都出在数据库查询和静默全表扫描上。我的优化清单很简单订单表的machine_id booking_date start_time加联合索引这是最高频的查询路径设备列表查询加Redis缓存缓存时间30秒用户看到的设备状态允许几秒延迟所有列表接口做分页禁止一次把全量订单拉出来支付回调接口要做限流防止回调风暴把服务打挂还有一个经常被忽略的点日志。上线前把请求日志、预约冲突日志、AI预测日志分文件输出。排错的时候这三类日志能帮你快速定位问题。我甚至建议在下单接口里打一行结构化日志记录request_id user_id machine_id slot_key result后面统计超卖、分析用户行为都靠它。6.3 实测反馈驱动的调整系统部署上去之后真正的调整才刚开始。我在这里分享几个从真实反馈中总结出来的问题很有共性第一个是用户根本不看推荐时段只凭习惯直接选每天晚上九点。这说明预约习惯的培养需要时间推荐系统的引导位要做得更明显不能只是默默算好分数藏在深层页面。后来的做法是用户在点开设备详情页时直接把“推荐时间”插到时段列表的最前面并标注预计排队时间转化率明显提升。第二个是洗衣机的故障状态不可信。一开始设备状态靠管理员手动改经常出现设备坏了但状态还是空闲。后来加了一个规则订单连续三次在启动后短时间内被取消自动标记疑似故障提醒管理员查验。这个简单的启发式规则比让管理员每天巡检靠谱得多。第三个问题是预测模型刚上线时准确度并不理想尤其是节假日。原因也很简单节假日的历史样本太少。我把特征里is_holiday从二值改成距离最近节假日的天数效果提升很明显。没有花哨的模型改进只是换了个更合理的特征表达这说明特征工程的意义永远大于模型堆叠。6.4 按个人经验收个尾做这套系统的过程中我最大体会是预约类项目的难点根本不在AI而在并发和状态管理。AI只是锦上添花状态机和并发防线才是地基。顺序上一定要先把“同一时段不能被约两次”“取消订单必须释放时段”“支付状态要可对账”这三件事做扎实再来谈预测模型有多炫。另外一个经验是不要把训练代码塞进Flask进程里跑离线训练、在线推理的模式一定要守住否则数据一多接口和训练互相抢资源两边都做不好。AI功能上线前先用规则兜底收集两三周的数据再换机器学习模型这是最稳妥的节奏。最后分享一个小技巧。上线前在测试环境跑一遍并发脚本模拟100个用户同时抢同一个时段打印成功数和冲突数。这个脚本值得一直留着每次改订单相关的代码之后都跑一遍防止改坏了自己还不知道。我做系统半年多被这个脚本救回来至少两次。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑