做计算机毕业设计选了教室预约系统这个题目,我看到越来越多同学往 Spring Boot 加微信小程序这个组合上靠。原因其实很直白:Spring Boot 在后端领域属于简历上写出来不丢人的技术栈,微信小程序又天然覆盖了移动端使用场景,评委现场演示的时候掏出手机扫码就能跑起来,展示效果比纯网页端强太多。这篇内容我打算把整个项目的核心链路完整拆一遍,从需求怎么落到数据库表,到预约冲突检测怎么做,再到小程序端页面和接口怎么对接,最后把我实际开发中踩过的坑和排查思路全部交代清楚。项目难度对本科生来说属于中等偏上,但刚好卡在能完整做完、又能讲清楚原理的范围里。
1. 项目整体设计与技术选型
1.1 需求拆解:一个教室预约系统到底在管什么
很多同学拿到这种题目上来就建表写接口,这是最容易翻车的做法。教室预约系统表面看就是"学生选教室、管理员审批",但真正落地要考虑的细节远不止这些。
先把核心角色理清楚。系统里至少有学生和管理员两类人,部分学校可能还有老师角色,比如导师要预约研讨室带组会。学生端的核心诉求是能按时间、校区、教室容量、设备条件筛选可用教室,提交预约申请,查看审批状态,如果需要还能取消预约。管理员端的诉求是维护教室基础信息、处理预约申请、查看某间教室某天的时间占用情况。这些诉求落到功能模块上,就能拆出用户认证、教室管理、预约管理、审核管理等几个清晰的模块。
我在这里多说一句,做毕设最忌讳的就是盲目堆功能。比如看到网上模板里有"意见反馈"“个人公告”这种模块就顺手加进去,结果答辩时老师问"这个模块的意义是什么"就答不上来。教室预约系统的核心业务闭环只有一个:预约申请——冲突检测——审批——使用记录。所有功能都要围绕这个闭环展开,其他都是支线。
1.2 技术方案选型的核心逻辑
后端选 Spring Boot,这个没什么争议。生态成熟、上手快、网上资料多,哪怕遇到问题搜报错信息也能快速找到解决方案。Spring Boot 自动配置机制帮我们省掉了大量 XML 配置,内嵌 Tomcat 也让部署变得非常轻量,本地开发一个 main 方法跑起来就能调试。
访问层框架我建议用 MyBatis-Plus,理由很简单:单表 CRUD 可以完全不用手写 SQL,内置的分页插件和条件构造器能省不少事。对于预约冲突检测这种稍微复杂一点的查询,自己动手写 SQL 也完全可控。有些教程喜欢推荐 Spring Data JPA,但说实话在毕设答辩场景下,MyBatis-Plus 这种"SQL 半自动"的模式更容易向老师解释清楚查询逻辑,JPA 的自动代理机制有时候反而会让老师觉得你对自己的代码不够熟悉。
小程序端我用的是原生开发,而不是 Uniapp 或者 Taro。原生小程序虽然在多端复用上不如跨端框架,但有两个好处:第一,微信开发者工具对原生代码的错误提示最准确,遇到问题好排查;第二,答辩时老师可能会现场改代码、提问题,原生框架的 Page、Component、WXML 这套体系更好解释。如果你后续想接小程序支付、蓝牙这类硬件能力,原生框架的资料也最全。
为什么不把后端拆成微服务、不上 Redis 缓存、不做消息队列?这个问题我几乎每次答辩模拟都会被人问。不是技术越复杂越好,而是要在合适的技术深度上把核心业务做扎实。单体应用加一个 MySQL 数据库,对于日均预约量几百次的场景绰绰有余。如果非要用 Redis 做教室状态缓存,那你必须能解释清楚缓存和数据库的一致性怎么保证,这反而给自己挖坑。
1.3 系统整体功能清单
在动工之前,最好先把功能清单列出来,做成表格贴在电脑前面。我整理了自己项目里的功能划分,大家可以直接参考:
- 用户模块:微信授权登录、用户角色识别(学生/管理员)、个人信息维护
- 教室模块:教室信息列表、按教学楼筛选、按容量/设备筛选、教室详情查看
- 预约模块:按日期查看教室空闲情况、提交预约申请、我的预约记录、取消预约
- 审核模块(管理员):待审核列表、通过/拒绝操作、按日期查看所有预约
2. 数据库设计与预约核心逻辑
2.1 数据库表结构设计
数据库设计是整篇项目能不能讲清楚的关键。很多同学表建得随意,后期写接口发现字段不够用、查起来费劲,又回头改表结构,改代码,非常痛苦。我这里的经验是:先把核心业务的三张表设计清楚,再考虑扩展表。
第一张是用户表user,字段包括id、openid、nickname、avatar_url、role、create_time。这里有一个关键点:openid是微信用户的唯一标识,不能暴露给前端任何业务接口,只在登录时维护用户身份映射关系使用。如果后续要扩展教师角色,加一个teacher_no字段就行。
第二张是教室表classroom,字段包括id、name、building、floor、capacity、has_projector、has_air_conditioner、status、description。这里的status是教室的可预约状态,1 表示开放预约,0 表示维护中。很多同学会忽略教室的开关状态,结果管理员把教室锁了,学生还能提交预约,这就是状态设计没做好。
第三张是预约表appointment,这是整个系统的核心表。字段包括id、user_id、classroom_id、appointment_date、start_time、end_time、status、reason、audit_remark、create_time。重点说status字段,我设计的是 0 待审核、1 已通过、2 已拒绝、3 已取消。为什么把状态设计成数字而不是字符串?因为数字在索引查询和条件判断上效率更高,而且代码里用常量类统一管理状态值,可读性也不会差。
这三张表的关系也很直观:用户和预约是一对多,教室和预约是一对多。预约表通过user_id和classroom_id两个外键关联用户表和教室表。如果后续要支持"暂时离开保留座位"这种计时场景,再单独建一张扩展表就行,核心表尽量保持简单。条件允许的话,可以给预约表加一个复合索引,比如(classroom_id, appointment_date, start_time, end_time),预约查询的时间复杂度会明显下降。
2.2 时间段冲突检测的核心算法
这个部分是整个系统的灵魂,也是答辩时老师最喜欢追问的地方。假设某个教室 3 月 20 日已经有一笔预约是 10:00 到 11:30,这时候又来了一个预约申请是 11:00 到 12:00,系统应该怎么判断出冲突?
先画一个时间轴就清楚了。已有预约的时间区间是[10:00, 11:30),新预约的时间区间是[11:00, 12:00),这两个区间在 11:00 到 11:30 这一段重叠了,所以有冲突。写成数学表达式就是:新预约的开始时间必须小于已有预约的结束时间,同时新预约的结束时间必须大于已有预约的开始时间,也就是不等式new_start < existing_end AND new_end > existing_start。
这个判断逻辑在 SQL 里写起来非常简洁,伪代码如下:
SELECT COUNT(*) FROM appointment WHERE classroom_id = #{classroomId} AND appointment_date = #{date} AND status IN (0, 1) AND start_time < #{newEndTime} AND end_time > #{newStartTime}为什么status要包含 0 和 1?因为待审核的预约虽然还没通过,但它已经暂时占用了这个时间段,如果不把它算进去,就会出现两个待审核申请挂在同一时间段上,管理员审批时还得人为判断先后顺序,业务逻辑就乱了。
有几个边界情况需要特别注意。第一,新预约结束时间和已有开始时间完全相等,比如新预约是 9:00 到 10:00,已有预约是 10:00 到 11:30,这种情况不冲突,因为前一场在 10:00 整结束,后一场 10:00 整开始,用上面的不等式判断就是10:00 < 10:00不成立,判定无冲突,正确。第二,预约表单里要限制开始时间必须早于结束时间,这个校验前端要做、后端接口也要做,千万别只做一端。
2.3 预约状态流转与业务边界
预约状态不能只是建表时随手填的数字,整个生命周期要想清楚。学生提交预约后状态是待审核,管理员同意后变成已通过,学生可以在已通过且未开始时取消,取消后变成已取消。如果管理员拒绝,状态变成已拒绝。这里有一个容易忽略的边界:学生是否可以在待审核状态下取消申请?我项目里允许,因为学生可能填错了时间,管理员还没处理,这时候学生主动取消对整体流程更友好。
状态流转还有一个时间条件是"是否已逾期"。我项目里的处理方式是:前端在"我的预约"列表里展示所有记录,但操作按钮根据当前时间和预约时间的对比来决定是否可用。比如预约时间已经过去的记录,取消按钮直接置灰;预约时间在未来但状态为已通过的记录,取消按钮正常可用。这个判断虽然也可以在后端做,但为了减少接口请求次数,我在前端做了一层展示控制,后端接口里还是会再做一次二次校验,防止有人直接调接口绕过前端。
3. 后端接口与关键实现
3.1 微信登录与用户身份关联
小程序端用户首次进入系统时,前端调用wx.login()获取临时登录凭证code,把这个code传到后端,后端再调用微信官方接口jscode2session换取用户的openid和session_key。这一步很多同学会踩坑,因为微信接口返回的openid是用户在当前小程序下的唯一标识,但它不是直接把用户信息给你的,需要后端把这个openid存储到自己的用户表里,维护用户和微信身份的映射关系。
获取到openid后,后端先查用户表,如果查不到记录,就自动注册一个新用户,并分配默认角色,然后生成一个自定义登录态返回给前端。这个自定义登录态我用的是 JWT,原因是无状态、不需要在服务端存 session,小程序端每次请求把 token 放在请求头里,后端通过拦截器统一解析用户身份。JWT 的 key 不要写死在代码里,放到application.yml配置文件中,用环境变量覆盖生产配置。
我实际项目中登录接口的完整流程是:接收前端传来的code,向后端配置文件中配好的appid和secret发起请求获取openid,根据openid查询或创建用户,生成 JWT 返回给前端,前端存入本地 storage,之后所有请求统一携带。这里有一个扩展点:如果后续要绑定手机号、完善真实姓名和学号,可以在这个接口的基础上扩展一个信息完善步骤。
3.2 教室查询与预约接口的实现细节
教室列表接口看起来简单,就是把教室表按条件查出来返回给前端,但实际上有两个细节值得展开。
第一个细节是"当天是否已被约满"的展示。前端教室列表页需要一个字段来标识每间教室当天的可预约状态,如果前端单独对每间教室查一次接口,教室数量多的时候就会产生大量请求。我的做法是:后端教室列表接口接收一个可选参数queryDate,如果传了就统计当天每间教室的预约占用情况,一次性返回给前端。SQL 上可以先用子查询统计预约数,再和教室表做关联查询。
第二个细节是图片和富文本信息的处理。教室可能有配图,通常推荐把图片存到本地静态目录或对象存储,数据库里只存图片 URL。我用的是在 Spring Boot 里配置静态资源映射,把本地上传目录映射到/images/**路径,小程序端直接通过域名加路径访问图片。
预约提交接口是整个系统最重要的接口,处理流程分五步:
- 从 JWT 中解析出用户 ID,判断用户是否存在且角色是否为允许预约的角色;
- 参数校验:日期不能是过去日期,开始时间早于结束时间,不能超出教室开放时间段;
- 检查教室状态是否为开放预约状态;
- 执行时间段冲突检测查询,如果返回数量大于 0,直接返回"该时间段已被预约"的错误信息;
- 插入预约记录,初始状态为待审核。
这个流程看起来简单,但里面有一个并发问题我后面会单独展开讲。
3.3 预约取消、管理员审核接口的设计
取消预约接口要做三步操作:查询预约记录是否存在、校验预约时间是否在当前时间之后、更新状态为已取消。这里有业务边界,如果预约时间已经开始了,是不允许取消的,这时候应该提示用户"该时间段已开始,如需调整请联系管理员"。管理员审核接口相对简单,接收预约 ID 和审核结果,把状态改成已通过或已拒绝,如果是已拒绝,还可以附带审核备注。
管理员查看待审核列表时,我建议关联查询教室名称和学生昵称一起返回,因为前端列表展示需要显示"谁预约了哪个教室",如果只返回 ID,前端就要用Promise.all多个请求去查详情,性能太差了。这里可以用 MyBatis 的关联查询或者写一个 VO 类来承接联合查询结果。
3.4 统一返回结果与全局异常处理
写接口的时候最容易出现代码风格不一致的问题,有的地方返回 Map,有的地方返回 JSONObject,前端接起来很痛苦。我从一开始就定义了统一的返回结果类Result<T>,里面有三个核心字段:code表示业务状态码,200 表示成功,400 表示参数错误,401 表示未登录,403 表示权限不足,500 表示服务器内部异常;message表示错误提示或说明;data表示业务数据。
全局异常处理用 Spring 的@RestControllerAdvice注解实现。我自己定义了一个业务异常类BizException,所有业务校验不通过的地方都手动抛出这个异常,异常处理器统一捕获后返回对应的错误码和提示信息。这样前端就不用每次在fail回调里解析各种不同的错误结构了,直接读message字段弹 toast 提示就行。
4. 小程序端页面与交互实现
4.1 小程序端整体架构设计
小程序端我按四个 tab 页来设计:首页展示教室列表、预约页是完整的预约流程入口、我的预约展示当前用户所有预约记录、个人中心展示用户信息和退出登录等功能。每个 tab 对应的页面目录下再拆分子组件,比如教室列表页可以把教室卡片抽成一个独立组件,多个页面复用。
小程序根目录下有一个app.js,我在这里面封装了请求统一逻辑,也就是说每个页面不用自己管理 wx.request 的完整参数。核心逻辑是:每次请求前从 storage 里取出 token,加上请求头;拦截到状态码为 401 的响应时,跳转到登录页并清理本地缓存。这个方法非常推荐大家用,代码量不大但能救很多命。页面里只需要调用封装的request.get('/api/classroom/list', params)就能完成请求和响应处理。
4.2 教室列表页的多条件筛选实现
教室列表页是整个小程序端最核心的页面。我的方案是顶部放筛选条件区,下面放教室卡片列表。筛选项包括教学楼、日期、容量区间和是否有投影仪。日期选择器用小程序原生的picker组件,mode="date",容量输入用input组件加数字键盘限制,教学楼和投影仪是多个radio或switch。
这页有一个细节值得特别说明:筛选条件变更后要不要自动重新请求接口?我的做法是点击"查询"按钮才触发请求,而不是每次都自动请求。原因是在真机上频繁请求会明显卡顿,而且教室列表数据量不大,用户筛选完点击查询的交互链路比"边输入边搜索"更稳定。如果你确实想做类似实时搜索的效果,至少也要加一个防抖函数,300 毫秒之内只触发最后一次请求。
教室卡片我设计了几个核心展示字段:教室名称、教学楼和楼层、容量、设备标签,以及当前日期的预约状态。预约状态用不同颜色标识,绿色表示可选、灰色表示约满、黄色表示部分时段可约。部分时段可约这个状态需要后端返回每个时间段的具体信息,前端用表格或时间轴组件展示。
4.3 预约表单与时间选择
从教室列表点击"预约"按钮进入预约页,这时候教室 ID 已经通过页面参数带过来了。预约表单包括日期、开始时间、结束时间、预约用途说明四个字段。日期用picker选择,开始时间和结束时间不能只用picker,因为可选时间要在教室开放时间段范围内,同时结束时间必须晚于开始时间。
我实现的是一个两列联动选择器:选了开始时间后,结束时间选择器会自动过滤掉早于开始时间的选项。这个小交互看起来很基础,但能让用户直观地感受到"系统在帮我管理规则的边界"。如果开始时间恰好在午饭时间、而选择器里没有合适的结束时间,为了减少用户输入成本,我还加了一个"快速选择下午第一节课"的便捷按钮。
4.4 我的预约列表与状态处理
“我的预约”页面按时间倒序展示当前用户的所有预约记录,每条记录展示教室名称、日期、时间段、状态标签和操作按钮。状态标签我这里做了统一映射,后端返回的是数字 0 到 3,前端根据数字转换成对应的中文和颜色。0 显示橙色"待审核",1 显示绿色"已通过",2 显示红色"已拒绝",3 显示灰色"已取消"。
操作按钮的处理逻辑我前面提过,要根据时间和状态来判断是否展示。待审核状态展示"取消申请"按钮;已通过且当前时间早于预约开始时间,展示"取消预约"按钮;其他情况不展示操作按钮。这样做界面干净,用户也不会误操作。
5. 常见问题与排查技巧实录
5.1 开发调试期的三个高频问题
第一个高频问题:小程序真机预览时,请求后端接口提示"不在以下 request 合法域名列表中"。这是因为微信小程序对请求域名有白名单限制。开发阶段可以在微信开发者工具的"详情-本地设置"里勾选"不校验合法域名",但真机预览时需要在 mp 后台把后端域名加到 request 合法域名列表中。如果你没有域名,也可以用内网穿透工具把本地后端映射成一个临时域名,这样真机也能正常调试。
第二个高频问题:代码里明明写了console.log,但控制台就是看不到输出。这种情况先确认是普通 JS 错误还是 API 返回异常。如果是 API 异常,错误信息通常在 Network 面板里而不是 Console 面板;如果是 JS 语法错误,Console 面板一定会有红色报错。另外要特别注意wx.request的 success 回调,这里不是所有的 HTTP 状态码都会走 success,500 状态码会走 fail,所以调试时要先看 Network 面板的响应体内容。
第三个高频问题:日期显示成"Issue: invalid time value"。这个通常是因为后端返回的时间字段格式是"2025-03-20T10:00:00"而前端直接用new Date()解析后格式化出错。解决方案是后端在返回前就格式化好,用LocalDateTime配合@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss"),前端拿到字符串直接展示,不在前端做二次转换。
5.2 时间相关的一串深坑
我做这个项目时在时间上踩的坑最多,今天集中写出来。
第一个坑:前后端时长类型不一致。后端实体类是LocalTime类型,前端传过来的是字符串"09:00",如果不做处理,Spring 会解析失败。解决办法是给字段加@JsonFormat(pattern = "HH:mm"),或者在接收参数上用@RequestParam接收字符串后手动解析成LocalTime。
第二个坑:数据库存的是datetime还是time。我之前见过有同学把时间段字段存成datetime,这样会导致同一间教室在数据库里有多条记录,虽然能判断冲突,但查起来很费劲,而且数据冗余严重。预约时间段应该存成"日期 + 开始时间 + 结束时间"三个字段,分别用 date、time、time 三种类型。
第三个坑:时区问题。如果服务器设置了 UTC 时区,而本地是东八区,数据库时间字段就会差 8 个小时。排查现象是前端展示的时间总是比实际时间早或晚 8 小时。解决方案是在数据库连接 URL 上加serverTimezone=Asia/Shanghai参数,同时服务端 JVM 启动时也指定-Duser.timezone=Asia/Shanghai,最好双管齐下。
5.3 并发预约导致的数据一致性问题
如果两个学生同时提交同一间教室同一时间段的预约申请,理论上两个请求都通过了冲突检测,然后都插入了预约记录,数据库里就会出现两条冲突的数据。要解决这个问题,有几个方案,从简单到复杂排列:
第一个方案是最简单粗暴的:在插入预约记录时,用数据库的唯一索引把冲突时间锁住。你可以建一个唯一索引,索引列为classroom_id + appointment_date + start_time,这样同一间教室同一天相同开始时间的预约只能成功插入一条。这个方案的问题是必须保证结束时间一致,如果都是按整节课来约,这个方案完全够用,但如果有半节课的预约粒度,就锁不住了。
第二个方案:用乐观锁。在预约表加一个version字段,插入前先查询一次,确认没有冲突,再执行插入操作。乐观锁的问题是,如果冲突检测和插入是两步操作,高并发下仍然有间隙,需要配合事务和唯一索引才能彻底解决。
第三个方案也是我在项目里采用的方案:在冲突检测的 SQL 上直接加FOR UPDATE锁。把教室记录锁住,这样同时进入的第二个请求会等待第一个请求的事务提交后再执行冲突检测,自然就能看到第一条记录,从而判定冲突。这个方案对教室预约这种低频场景完全够用,代码也容易解释清楚。以下是核心代码逻辑,我用事务加行锁来实现。
注意:
FOR UPDATE行锁必须放在事务里才会生效,而且是查询完再插入的整个过程都在同一个事务中。如果只给查询加锁、插入在事务外,锁就失去意义了。
@Transactional(rollbackFor = Exception.class) public AppointmentResult createAppointment(AppointmentCreateDTO dto, Long userId) { // 锁定教室记录,避免并发预约 Classroom classroom = classroomMapper.selectByIdForUpdate(dto.getClassroomId()); if (classroom == null || classroom.getStatus() != 1) { throw new BizException("教室不存在或不可预约"); } // 执行冲突检测 int conflictCount = appointmentMapper.countConflicting( dto.getClassroomId(), dto.getAppointmentDate(), dto.getStartTime(), dto.getEndTime()); if (conflictCount > 0) { throw new BizException("该时间段已被预约,请选择其他时间"); } // 插入预约记录 Appointment appointment = new Appointment(); appointment.setUserId(userId); appointment.setClassroomId(dto.getClassroomId()); appointment.setAppointmentDate(dto.getAppointmentDate()); appointment.setStartTime(dto.getStartTime()); appointment.setEndTime(dto.getEndTime()); appointment.setReason(dto.getReason()); appointment.setStatus(AppointmentStatus.PENDING); appointmentMapper.insert(appointment); return new AppointmentResult(appointment.getId()); }实际项目里我最后是"事务 + 行锁"和唯一索引双保险,两个都上。唯一索引不是为了代替行锁,而是防止程序中确实出现 bug 时数据库能兜底,不至于产生脏数据。如果老师的追问到了这个层面,你也把两个方案的边界说清楚,基本就是满分回答了。
5.4 小程序端真机的表现与兼容性
真机调试时我发现一个有意思的问题:模拟器上滑动非常流畅,但真机上教室列表页出现明显的卡顿。排查下来不是请求慢,而是列表里的图片太多,真机渲染压力大。解决方案是图片懒加载,小程序image组件自带lazy-load属性,加上以后情况明显改善。
另外picker组件的日期范围默认从 1970 年开始,会给用户一种"什么都能选"的错觉。我做了限制,日期选择器的起始日期是今天,结束日期是未来 30 天,这个范围已经覆盖了绝大多数预约需求。如果你希望更灵活,可以把最大值设置成教务系统里整个学期的最后一天。
6. 一个完整的预约流程走查示例
写到这里,我拿一条完整的预约流程把前面所有模块串起来走一遍,这样大家能体会到代码模块之间是怎么配合的,也方便答辩时讲业务。比如现在有一名学生小李,想预约 3 月 20 日 18:00 到 20:00 的研讨室。
小李打开小程序,首次进入弹出微信授权,前端调wx.login拿到code,传给后端接口换成openid,后端查库发现没有这个用户,自动创建一条用户记录,角色默认是学生,然后返回 JWT。小李进入首页,选择日期 3 月 20 日,后端返回当天所有教室的预约占用情况,他看到 A 楼 201 研讨室这一天的 18:00 到 20:00 是空闲的,点击预约,进入预约页面,选中开始和结束时间,填写用途"小组讨论",提交。
后端接收入口先校验参数,然后锁住教室记录,执行冲突检测。如果这个时段已经被张三预约了,这里会直接返回错误,小李会看到"该时间段已被预约"的提示。如果没冲突,系统插入一条状态为待审核的预约记录。管理员在小程序管理端看到待审核列表,确认研讨室届时没有其他安排,点击通过。小李在"我的预约"里看到状态变为已通过。
到了 3 月 20 日晚上,小李按时间去研讨室,他也可以提前在小程序里点击"取消预约",取消后这个时间段就释放了,其他同学可以重新预约。整个流程到这里是一个完整闭环,数据库里的数据变化是:用户表多了一名新用户、教室表数据没有变化、预约表新增了一条记录且状态由 0 变到 1 或 3。
实际开发中,管理员端的页面我建议用网页后台来承载,因为管理端的操作频率低、数据结构复杂,用小程序反而不好用。我当时是在同一个 Spring Boot 项目里同时开发了小程序端接口和管理员网页接口,前端用了一个非常轻量的 Bootstrap 加原生 HTML 页面,没有引入额外框架,这样既省时间,答辩时还能说清管理端的技术选型。
7. 几个值得注意的细节和优化思路
教室预约系统做完主体功能之后,还有几个小细节可以让项目层次感提升不少。第一个是教室详情页增加本周占用情况总览,用时间轴加色块的样式展示周一到周日每个时间段被哪些预约占用,这个对管理员特别友好,一眼就能看出某间教室的高峰时段。实现方式不复杂,后端按教室 ID 查询一周内的所有预约记录,前端做一个按星期分组的简单渲染。
第二个是预约原因字段的规范化。如果不做任何限制,学生会填各种奇怪的用途,管理员审核时看着头疼。我在前端做了预设选项,比如"课程研讨""自习备考""项目组会""社团活动",同时留一个"其他"选项可以手动输入。这样既方便学生选择,也方便管理员快速理解预约意图。
第三个是消息触达。预约状态变化时,如果小程序订阅消息权限满足条件,可以推送一条预约审核结果给用户。微信小程序的订阅消息是一次性的,需要用户主动订阅,这在小程序端的用户交互里可以加一个"开启审核通知"的开关。这一块在毕设答辩中属于加分项,体现了对用户体验的考虑。
第四个是数据看板的思路。管理员后台可以加一个简单统计接口,统计今天预约数量、本周预约数量、教室使用率排名等。不需要做得很花哨,一个接口、一个表格就能让项目在业务完整度上高出不少。
我个人在实际操作中的体会是,这个项目想做到 80 分,核心业务闭环跑通就行;想做到 90 分以上,需要在并发控制、状态流转、异常处理这些细节上多花心思。有很多细节是靠"把用户当真人"来驱动的,你多问自己几次"如果我是管理员,看到这条数据会怎么想",很多设计决策就不会走偏。