尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot+Vue全栈实现自习室预约管理系统:从需求到部署

SpringBoot+Vue全栈实现自习室预约管理系统:从需求到部署 自习室预约这套题说实话已经被做成“毕业设计常青树”了。但凡带点管理系统的课设、毕设十有八九绕不开这个选题。我这次做的就是一个基于SpringBootVue的MVC架构自习室管理和预约系统技术栈落在JavaMySQLMyBatis上属于典型的全栈分离项目。整个系统拆成用户端和管理端两套界面用户在手机上或者浏览器里能看到自习室列表、楼层分区、座位占用情况选个座位预约一段时间管理员在后台维护自习室、座位、预约规则还能看统计报表。这篇文章我就按自己做这个项目的完整过程来写从需求拆解、技术选型、数据库设计到后端接口、前端交互、部署运行再到我实际踩过的坑都会交代清楚。适合正在做课设/毕设、想复现一个完整前后端分离项目的同学参考也适合想入门SpringBootVue全栈开发的人照着搭一套练手。源码层面我会把关键代码片段贴出来不是那种“只给目录不给细节”的分享。1. 项目到底要解决什么问题1.1 自习室预约的核心痛点学校或者社会自习室的座位资源看起来是“公共的”但实际运营起来全是问题。最常见的场景就是考研季、期末周座位严重不够用第二天一早门口排队抢座甚至有人拿书占座几天不来。反过来非高峰期大量座位空着资源闲置。自习室管理员也很头疼纸质登记本翻起来麻烦谁占着座位、什么时候走完全靠人肉盯。这个系统的核心价值就是把“座位状态数字化”。每一张座位在系统里都有明确状态空闲、已预约、已签到、暂离、禁用。用户预约之前一眼看清哪些座位能用预约之后到现场签到入座离开时释放座位。管理员不用再跑现场后台就能看到实时占用率还能设置每个自习室的开放时段、预约时长上限。本质上这就是一个带时间维度的资源管理系统和会议室预约、健身房场地预约的逻辑是相通的。1.2 功能拆解与用户角色设计用户角色我分成三类普通学生用户、管理员、超级管理员。没有做“教师”角色因为自习室场景里教师通常只参与审批或者设备管理没必要为了凑功能硬加。普通用户侧的关键功能注册登录、个人信息维护查看自习室列表按楼栋、区域筛选查看每个自习室的座位图区分不同座位状态预约座位选择日期和起止时间签到入座、暂离、释放座位查看自己的预约记录、违规记录管理员侧的关键功能自习室管理新增、编辑、启停用座位管理批量生成座位、设置座位类型靠窗/普通/带电源、禁用某个座位预约规则管理设置可预约天数、单次最长时长、签到时限预约记录查询与统计导出表格用户管理封禁、解封、重置密码开发展示中最容易被问的就是“这个系统有什么亮点”。我当时给出来的方案是“座位状态实时同步预约冲突兜底”。也就是用户在选座时看到的座位状态是准实时的提交预约时后端再做一次防冲突校验避免两个人同时锁到同一个座位。2. 技术选型与架构设计思路2.1 为什么是SpringBootVue这套组合先说后端。SpringBoot在这个场景里几乎是无可争议的选择原因很简单内嵌Tomcat不需要额外部署Web容器自动配置把大量样板代码省掉生态成熟社区资料多遇到问题基本都能搜到答案。相比之下如果再用传统的SSHSpringStrutsHibernate那一套光是写各种XML配置就能劝退一堆初学者。我用的版本组合比较稳SpringBoot 2.7.x JDK 1.8 MySQL 5.7/8.0 MyBatis 2.x。为什么要锁这个版本因为SpringBoot 3.x强制要求JDK 17而且部分老项目用的MyBatis、PageHelper插件在3.x下需要额外适配如果你照着网上的教程抄作业大概率会踩版本不一致的坑。所以我的建议是除非你有明确理由否则毕设/课设别追新版本。前端选Vue就更好理解了。Vue 2.x Element UI是当前中文社区里资料最多、最稳定的组合Vue 3 Element Plus虽然新但有些组件用法变动比较大网上的老旧教程容易误导。我这次用的是Vue 2.6 Element UI 2.15配合vue-router和axios足够支撑这种后台管理用户选座的场景。2.2 MVC分层在后端怎么落地标题里特意提到MVC这个必须解释清楚。MVC本身是一个设计思想不是一套固定框架。我后端的代码结构就是按照Controller、Service、MapperDAO三个层次来的Model用实体类和VO来表现。典型的调用链路是这样的浏览器发起请求到Controller层Controller负责参数接收、简单校验然后调用Service层Service层处理业务逻辑比如校验预约冲突、计算座位状态Service调用Mapper接口Mapper对应XML里的SQLMySQL返回结果Mapper映射成实体类经过Service组装成VO最后由Controller返回JSON给前端这样的分层好处很明显至少有三点各层职责单一代码可读性好。别人拿到项目不需要翻完整套代码只看Controller就知道系统提供了哪些接口。Service层独立后业务规则可以单独做单元测试。比如预约冲突检测这种核心逻辑不依赖Controller和前端就能跑通。后期替换技术方案更方便。比如把MyBatis换成MyBatis-Plus或者把MySQL换成PostgreSQL底层影响可以被控制在Mapper层。实体类这块我做了一个区分数据库表对应的叫Entity接口返回前端的叫VO前端提交参数用DTO。之前的项目吃了不区分的亏查了一个用户信息顺便把密码hash也返回了前端虽然前端不显示但这事很不专业。2.3 前端Vue项目的目录组织Vue项目不是简单的“一个页面一个组件”就完了。我按功能模块拆分成view页面级组件和component可复用组件两大类。src/apiaxios请求封装按模块拆分成user.js、seat.js、reservation.jssrc/router路由配置文件区分需要登录的页面和不需要登录的页面src/storeVuex存放用户登录信息、当前选中的自习室状态src/views页面级组件比如Login.vue、SeatMap.vue、Admin/ReservationManage.vuesrc/components通用组件比如座位格子SeatItem.vue、分页组件封装的PaginationTable.vue实际开发中很多初学者会把所有逻辑全部塞进一个页面组件几百行代码堆在一个Vue文件里后面想改一个选座逻辑都要翻半天。我这次强行把座位图抽成了独立组件状态由父页面传入交互通过事件回调这样座位图组件可以单独测试也能复用到管理端的座位预览功能里。3. 数据库设计预约系统的命门3.1 核心表结构设计数据库设计是这种系统里最考验功力的部分。表设计不好后面写业务代码就是连环坑。我规划了这几张核心表sys_user用户表字段包括id、username、password、real_name、role、status、create_timestudy_room自习室表字段包括id、name、location、floor、open_time、close_time、status、capacityseat座位表字段包括id、room_id、seat_no、type、position_x、position_y、statusreservation预约记录表字段包括id、user_id、seat_id、room_id、reserve_date、start_time、end_time、statusviolation_record违规记录表记录超时未签到、超时未释放等情况用户表里role字段我直接用的字符串USER、ADMIN、SUPER_ADMIN。也有人用int类型再在代码里映射枚举我个人觉得字符串更直观查数据库的时候一眼能看懂也不容易被数字映射搞糊涂。座位表里的position_x和position_y是干嘛的这是为了前端渲染座位图准备的。每张座位在自习室的平面图里有一个坐标前端拿到数据后可以直接按坐标渲染到对应位置比后端拼接好HTML再返回强多了。当然纯粹做简单系统也可以让前端写死布局但那样教室里临时加把椅子就得改代码。3.2 预约时间冲突的处理思路预约系统的核心难点不是CRUD而是“同一座位同一时间段不能被两个人预约”。这个冲突检测我一开始想用纯SQL来做后来发现反而复杂干脆在Service层加锁校验。逻辑是用户提交预约时后端根据seat_id和reserve_date去查已经存在的预约记录只要新预约的时间段和已有记录存在重叠就拒绝。SQL大概是这样的select idselectConflict resultTypejava.lang.Integer SELECT COUNT(*) FROM reservation WHERE seat_id #{seatId} AND reserve_date #{reserveDate} AND status IN (BOOKED, CHECKED_IN, AWAY) AND NOT ( #{newEndTime} lt; start_time OR #{newStartTime} gt; end_time ) /select这里的时间重叠判断就是两个区间的交集判断新预约开始时间不早于已有记录的结束时间或者新预约结束时间不晚于已有记录的开始时间才算不冲突。反过来只要两个时间段存在任何交叉区域就说明有人占用了。只做查询校验还不够。两个请求同时提交理论上都能通过校验然后都插入成功这就产生了并发问题。解决办法一个是给座位加行锁用SELECT ... FOR UPDATE把座位记录锁住再校验另一个是对reservation表加唯一索引配合状态字段。我最终采用的是“事务座位行锁”的方案这也是商用系统中比较稳妥的做法。3.3 索引设计索引不是用来炫技的而是要命中实际查询场景。这个系统里查询频率最高的场景就两个用户查询自己某一时间段的预约记录按座位查询某个日期的预约占用情况所以我在reservation表上建了两个复合索引idx_user_time (user_id, reserve_date, start_time)idx_seat_time (seat_id, reserve_date, start_time)建索引的理由很直接预约记录表会越来越大没有索引的话WHERE user_id ? AND reserve_date BETWEEN ? AND ?这种查询就是全表扫描数据量到几万条之后响应时间会明显变差。加了复合索引后查询可以走索引快速定位排序也顺手能利用上索引。再强调一个细节不要给每个字段单独建索引那样不但不加速反而拖慢写入速度。MySQL的索引机制决定了复合索引的字段顺序也有讲究最常用的等值条件字段放前面范围条件字段放后面。4. 后端核心实现与关键细节4.1 登录认证与用户上下文登录这块我用的方案是JWTJSON Web Token。跟Session那套比JWT最大的好处是无状态后端不用存储会话记录横向扩展的时候不用做会话同步。对毕设项目来说前端拿到token后存到localStorage里每次请求在axios拦截器里带上Authorization头就行。具体实现上我用JWT生成token时把user_id作为payload核心字段。用户每次请求受保护接口时后端拦截器解析token拿到user_id后去redis或者直接查数据库取用户信息放入ThreadLocal里作为本次请求的用户上下文。注意我在这里没有用Spring Security因为对自习室预约这种场景来说Spring Security的学习成本和配置成本有点重。自己写一个HandlerInterceptor 自定义注解RequireRole代码量不大逻辑还更好懂。自定义注解是这样一个思路Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { String value() default USER; }在拦截器中根据Controller方法上的注解判断当前用户是否拥有对应角色。管理端接口标记RequireRole(ADMIN)用户端接口标记RequireRole(USER)超级管理员两个角色都能访问。4.2 预约与取消的核心业务逻辑预约的核心逻辑我在Service里用事务包裹起来大致流程是这样的校验用户是否有预约权限是否已被封禁、是否已经约了同一时段锁定座位记录SELECT ... FOR UPDATE查询冲突预约判断时间段是否重叠插入一条预约记录状态为BOOKED更新座位状态为OCCUPIED为什么要先锁座位再查冲突因为如果不锁并发场景下两个人同时查到“无冲突”然后都插入成功就出现了超卖问题。锁座位虽然牺牲了一点并发量但对于自习室这种场景完全没有压力单把椅子一天最多也就被约几次没有效能瓶颈。取消预约的规则有个细节距离预约开始时间不足30分钟不允许用户在线取消必须到现场找管理员操作。这个规则在管理端可以配置但默认值我是直接写在Service层常量里。为了这个规则我在事务里能直接判断当前时间是否满足条件很快也不用额外设计提醒表。流程是删除或标记预约记录为CANCELLED同时释放座位状态。我选择的是保留预约记录、把状态改成CANCELLED而不是物理删除。这样保留历史数据后面做统计分析比如“哪些座位最受欢迎”才有数据可用。4.3 MyBatis动态SQL与缓存配置MyBatis在这个项目里承担所有数据库访问工作。我坦白说一开始我也纠结要不要用MyBatis-Plus因为它真的能省掉大量单表CRUD代码。后来考虑到很多课设答辩的老师要求“使用MyBatis实现”所以我保留了原生MyBatis的XML写法把多表查询和动态SQL都写清楚了。动态SQL在预约记录查询里特别好用。管理端查询预约记录时筛选条件可能是“按日期查”“按自习室查”“按用户姓名查”也可能全部不选查全量。这种场景用动态SQL最合适select idselectReservationPage resultTypecom.example.entity.Reservation SELECT r.*, u.username, s.seat_no FROM reservation r LEFT JOIN sys_user u ON r.user_id u.id LEFT JOIN seat s ON r.seat_id s.id where if testroomId ! null AND r.room_id #{roomId} /if if testreserveDate ! null AND r.reserve_date #{reserveDate} /if if teststatus ! null and status ! AND r.status #{status} /if /where ORDER BY r.reserve_date DESC, r.start_time DESC /selectwhere标签会自动处理“第一个条件前面的AND会被去掉”的问题不用自己手动拼接字符串避免SQL注入的同时代码也干净很多。MyBatis的二级缓存我也聊一下。默认情况下MyBatis的二级缓存是关闭的我没有在全局开启而是只对seat表的Mapper开了二级缓存。原因很简单预约记录和用户表的实时性要求高缓存命中率低而且一旦数据变更还要考虑缓存刷新容易脏读。座位表相对稳定状态变更频率不高开启二级缓存后可以少打几次数据库。不过要注意现在很多项目直接用Spring Cache Redis做缓存MyBatis的二级缓存用得少了因为多级缓存的一致性维护成本偏高。4.4 管理端功能实现管理端的核心功能是自习室和座位管理我用了一个比较省事的思路座位批量生成。一个自习室可能有几十上百个座位一个个人工录入不现实。我的方案是在新增自习室时让管理员输入“行数”和“列数”系统自动生成座位编号例如A01、A02B01、B02并按照坐标规则写入seat表。这样既省了管理员的工作量又保证了前端座位图的位置数据是完整的。座位批量生成的核心代码大致是for (int row 1; row rowCount; row) { for (int col 1; col colCount; col) { Seat seat new Seat(); seat.setRoomId(roomId); seat.setSeatNo(String.format(%s%02d, (char)(A row - 1), col)); seat.setPositionX(col); seat.setPositionY(row); seat.setStatus(FREE); seatMapper.insert(seat); } }管理端还有一个实用功能是今日统计。首页展示“今日预约数”“当前在场人数”“自习室占用率”。占用率的计算方式是当前已占用座位数 / 总座位数查询时先分组查每个自习室的座位总数再查当前状态为OCCUPIED的座位数两个数据在Service层合并成统计VO返回前端。5. 前端页面与交互实现5.1 Vue路由与页面结构前端路由用的是Vue Router。路由设计上我分了两层不需要登录就能访问的和必须登录才能访问的。不需要登录的路由/login登录页/register注册页必须登录的路由放在/layout父路由的children里因为这一部分页面都有共同的顶部导航和侧边栏布局。{ path: /layout, component: Layout, redirect: /layout/home, children: [ { path: home, component: Home, meta: { title: 首页 } }, { path: rooms, component: RoomList, meta: { title: 自习室 } }, { path: seat-map, component: SeatMap, meta: { title: 选座 } }, { path: my-reservation, component: MyReservation, meta: { title: 我的预约 } } ] }管理端路由单独建了一份挂在/admin下面比如/admin/room-manage、/admin/seat-manage、/admin/reservation-manage。这里要注意的是前端路由的权限控制只是用户体验层面的真正的权限校验必须依赖后端接口。前端不能访问的页面只是看不到入口如果直接调后端接口没有权限后端必须返回403。路由守卫方面我在router.beforeEach里检查本地有没有tokenrouter.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next(/login); } else { next(); } });5.2 座位选座交互选座界面是整个前端最核心的交互页面我在设计上参考了影院选座、演唱会选座那一套一个平面图座位格子一个个平铺在上面用颜色区分状态。座位状态的配色我定的是空闲白色/浅灰色已预约黄色已签到绿色暂离橙色禁用深灰色打叉用户点击一个空闲座位后右侧弹出预约面板选择日期和起止时间点击确认后走预约接口。这个交互逻辑我封装在SeatMap.vue里数据来源是后端接口返回的座位列表和座位状态列表。这里有一个交互细节用户选中座位后要在前端本地标记“正在选中”状态但这个状态不能和后端实时同步等到真正提交时才返回给后端。所以为了防止两个用户同时选同一座位前端显示的状态只能作为参考后端提交时的冲突校验才是兜底。座位图渲染的模板大概是div v-forseat in seatList :keyseat.id classseat-item :classgetSeatClass(seat) :style{ left: (seat.positionX - 1) * seatSize px, top: (seat.positionY - 1) * seatSize px } clickhandleSeatClick(seat) {{ seat.seatNo }} /div座位尺寸我固定用44px×44px间距6px这样一张A4纸宽度的屏幕能放下十几个座位体验比较自然。如果自习室座位特别多还可以在右侧加一个缩小比例的迷你地图用于快速定位。5.3 Axios封装与权限控制axios不能每个组件里单独引一遍要统一封装。我在src/api/request.js里做了以下事情创建axios实例设置baseURL和timeoutbaseURL指向后端接口地址比如http://localhost:8080/api请求拦截器里从localStorage读取token放到请求头响应拦截器里统一处理HTTP状态码401跳转登录页403提示无权限500提示服务器错误把后端返回的业务状态码也做了统一处理比如code500的提示语统一用Element UI的Message组件展示这种封装的好处是后面如果后端接口地址变了只需要改一个文件如果后端的鉴权方式变了比如改为请求参数携带token也只需要改一个拦截器组件里的业务代码完全不用动。6. 从零到一完整部署运行步骤6.1 本地环境准备先说一下我本地的环境JDK 1.8如果你是JDK 17也可以跑SpringBoot 2.7但部分老插件可能报错Maven 3.6MySQL 5.7或8.0Node.js 14Vue 2项目不建议用Node 18会有兼容问题IDE后端用IDEA前端用VS Code环境这块我吃过亏尤其Node版本。Vue 2的老项目用Node 17跑npm install经常报Error: error:0308010C:digital envelope routines::unsupported这是因为新版Node的OpenSSL策略变了。解决办法要么把Node降级到14/16要么在package.json的启动脚本里加一句NODE_OPTIONS--openssl-legacy-provider。我更推荐前者降级到Node 14一了百了。6.2 数据库初始化数据库这块我用Navicat新建一个study_room_db数据库字符集选utf8mb4为什么不用utf8因为utf8在MySQL里是utf8mb3不支持emoji和一些特殊字符虽然系统里不太用emoji但保不准用户昵称里有特殊符号。然后直接执行init.sql脚本里面包含建表语句和初始数据。初始数据至少要有一个超级管理员账号admin/admin123两个自习室数据每个自习室对应的座位数据我直接用批量插入SQL写死了几条演示用的预约记录方便前端展示效果关于账号密码我在user表里存的是MD5加密后的值。明说一句MD5现在已经不够安全了真正商用至少要用BCrypt。我这里图省事用了MD5但代码里把加密算法抽成了工具类想替换成BCrypt很容易。6.3 后端启动后端项目导入IDEA后第一步修改application.yml里的数据库连接配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/study_room_db?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你的数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case这个配置一定要开启否则数据库字段seat_no映射到Java属性seatNo会失败查询结果全是null。这是个非常典型的新手问题排错能排半天。配置改完后先启动MySQL再运行StudyRoomApplication主类。后端启动后浏览器直接访问http://localhost:8080/api/health如果能返回正常结果说明后端已经跑起来了。6.4 前端启动前端项目打开终端按顺序执行npm install npm run devnpm install如果速度慢可以换成淘宝镜像源先执行npm config set registry https://registry.npmmirror.com再装。启动后开发服务器默认跑在http://localhost:8081我特意把端口改到8081避免和后端8080混淆。Vue开发服务器的端口可以在vue.config.js里配置module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这里用proxying而不是直接写死http://localhost:8080作为baseURL是为了避免开发阶段的跨域问题。浏览器访问8081端口请求通过Vue开发服务器转发到8080同源策略就不会拦截了。如果不用proxy要么后端配置CORS要么前端调接口时直接带完整地址但会被浏览器拦截所以这个配置很关键。7. 常见问题与排查经验7.1 端口占用与配置不一致启动后端时最常遇到的报错就是Web server failed to start. Port 8080 was already in use.端口被占用的原因五花八门可能是你自己之前启动过一次没关掉也可能是其他软件抢占了端口。排查方法Windows下用netstat -ano | findstr 8080查看占用进程PID用taskkill /PID 进程号 /F强制杀掉或者在application.yml里换一个端口但我更建议换端口而不是杀进程因为杀掉的进程有可能是别人正在用的服务。比如你机器上已经跑了一个Tomcat在8080你非要用8080最好还是改自己的项目端口。7.2 数据库连接失败排查数据库相关的报错几乎占了新手问题的一半。常见的几个Communications link failure这个报错十有八九是数据库没启动或者连接地址写错。先确认MySQL服务在系统服务列表里是“运行中”然后在命令行用mysql -u root -p直接连一下确认账号密码没问题。Unknown database study_room_db这个就是数据库没创建或者名字写错了。去Navicat里看看库名是不是和url里的一致注意大小写。The server time zone value Öйú±ê׼ʱ¼ä is unrecognized这个报错是因为MySQL时区设置问题在url上加serverTimezoneAsia/Shanghai就能解决。如果还报错就在MySQL的配置文件my.ini里加上default-time-zone 08:00。7.3 跨域问题我用了devServer的proxy方案之后开发阶段基本不会遇到跨域。但如果你把前端打包成静态文件在Nginx里部署或者直接打开dist/index.html跨域问题就会冒出来。本地开发时如果不用proxy就必须在后端加CORS配置。一个简单的做法是在Controller上或者全局配置类上加CrossOrigin或CorsFilter。CORS配置时要指定允许的来源、方法和请求头不能直接写*加允许所有请求头那是懒人配置不严谨。前端部署时更推荐Nginx反向代理的方式把/api/开头的请求转给后端服务。Nginx配置大致是location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }7.4 MyBatis相关坑MyBatis使用中我踩过最深刻的坑就是“SQL没有问题但查询结果全是null”。这个我在前面提过就是map-underscore-to-camel-case没开启。如果你不喜欢全局开这个配置也可以在XML里的resultMap里手动映射每一列但是那样的代码量翻倍不推荐。另外一个坑是参数传递问题。当Mapper接口方法有两个或以上参数时必须用Param注解标注参数名否则MyBatis不知道#{userId}对应哪个参数。我见过太多人写这样的代码然后报Parameter xxx not found的错误。还有MyBatis的if判断字符串比较时写成if teststatus BOOKED有时候不生效原因在于MyBatis解析OGNL表达式时单引号内的内容会被解析成字符而非字符串。正确写法是if teststatus BOOKED外层用单引号内层用双引号。这个真是很阴间的细节。7.5 预约并发冲突实战最后说一个真实遇到过的业务问题。系统上线模拟压力测试时两个用户同时点预约同一个座位结果两个都成功了。我排查后发现问题出在事务边界上我第一次把座位状态校验放到了事务外等事务内插入时才去更新座位中间丢了一个校验窗口。修正方案是在Service方法上加Transactional进入事务后先执行SELECT ... FOR UPDATE锁定该座位记录再执行冲突查询和插入操作。加上行锁之后模拟并发请求就不会再出现超卖场景了。这个问题的通用排查思路就是凡是涉及“资源唯一性”的业务座位、库存、优惠券都必须考虑并发。只有业务校验没有数据库锁或者唯一约束迟早出问题。8. 项目扩展与个人心得做完这套系统再回头去看我会觉得技术栈本身并不复杂真正花时间的其实是业务规则的设计和落地。预约时间冲突判断、签到时限、暂离时长限制每一个规则边界都需要想清楚否则上线之后运营人员会被各种边界case折磨。很多人以为做一个管理系统就是增删改查做完才发现增删改查只是最外面一层皮真正的核心在于“状态机怎么流转”“并发下如何保持一致”“用户体验怎么做得好”。如果你也正在做类似的系统我的建议是先把业务流程图画明白把每个状态之间的跳转条件和限制想清楚再动手写代码。数据库设计可以花一整天反复推敲这比代码写了一半再改表结构要划算得多。这个项目后续可以扩展的方向也很多。比如加入一个基于WebSocket的实时座位状态推送有人释放座位时其他在线用户立刻看到或者引入Redis缓存热点数据减轻数据库压力再或者把前端升级成Vue 3 TypeScript代码的可维护性会再上一个台阶。但这些都是增量优化核心骨架不变。最后说一个实际开发里的细节我在项目里对所有时间字段统一用了LocalDateTime而不是Date。这样在做时间段比较的时候比Date要顺手很多也避免了时区偏移导致的坑。如果你正在选型阶段一开始就统一用LocalDateTime能省掉后面不少麻烦。
返回列表