SpringBoot自习室预约系统实战:高并发防超卖与黑名单机制
接到这个需求的时候我第一反应是这不就是“图书馆占座克星”吗。高校的自习室资源就那么点一到期末考试季就上演“书在人不在”的占座大战甚至还有人用一瓶水占一个下午的位置。管理老师头疼学生也窝火。基于SpringBoot实现一套自习室预约系统核心要解决的就是座位资源分配的问题把“抢座”变成“约座”让每一张椅子在哪个时间段属于谁都清清楚楚。这篇文章我打算换个写法不搞那种从零到一逐行敲代码的保姆级教程而是一个项目做完之后的完整复盘。聊清楚整体方案、核心设计思路、并发场景怎么处理、黑名单机制怎么落地再把我实际操作中踩过的坑原原本本说出来。适合正在做毕业设计的朋友、接校园信息化项目的团队以及想通过一个完整案例把SpringBoot实战串起来的开发者。1. 需求梳理和技术选型先想清楚这系统到底要管什么很多初学SpringBoot的朋友容易一上来就建工程、写Controller结果写到一半发现业务逻辑乱了。我接到这个项目的第一件事其实是坐下来把需求一条条列清楚把“系统边界”划出来。1.1 核心需求拆解自习室预约要解决的四个问题自习室预约系统表面上是个CRUD但真要落地它其实要回答四个核心问题第一座位资源怎么建模。一个自习室里有几十上百个座位每个座位有位置编号、是否靠窗、是否靠插座、是否属于安静区等属性。这些属性会影响用户的选择偏好也会影响管理员后续的统计和分析。所以座位不能简单当成一个字符串来存它需要有自己的实体和状态。第二预约流程怎么设计。用户是当天预约还是提前几天预约预约之后需不需要签到如果人没来座位什么时候释放这些问题直接决定了业务表的字段设计和状态机的流转。我见过不少新手把预约表设计成只有“预约中”和“已完成”两个状态结果用户取消、超时未签到这些情况完全没法处理。第三高并发怎么应对。热门自习室在晚上七点开放第二天预约时瞬间可能涌进来几百上千个请求。如果按照普通的数据库操作来处理很容易出现“座位超卖”——两个人同时约到了同一个座位。这是整个系统里最考验技术功底的地方。第四违约怎么治理。预约了不来、签到后中途离开不签退这些行为不治理的话系统很快就失去公信力。所以需要一套信用分或黑名单机制把违约用户暂时关进“小黑屋”。想清楚了这四个问题再去选技术和设计表结构思路就清晰了。1.2 为什么选SpringBoot MyBatis-Plus MySQL这套组合技术选型没有绝对的“最好”只有“最合适”。我这次选的是SpringBoot 2.7 MyBatis-Plus MySQL 8.0 Redis前端配了一个Vue3的管理后台。这套组合在校园类管理系统中属于非常成熟的方案教程多、社区活跃、出了问题很容易搜到答案。SpringBoot在这个项目里解决的最大痛点是“配置地狱”。你回想一下SSM时代光是一个Spring的XML配置文件就能写几百行各种Bean定义、事务配置、数据源配置新人光是搞定环境就要一周。SpringBoot通过自动装配把这些全都约定好了我只需要在application.yml里写几行核心参数就能把项目跑起来。这并不是说SpringBoot比SSM“高级”而是它在“快速交付”这件事上确实省了大量时间。MyBatis-Plus则是把“单表操作”的重复劳动降到了最低。座位表、预约表这种简单的增删改查直接用BaseMapper提供的方法就能完成不需要写一堆XML映射文件。更实用的是它的分页插件和条件构造器查询今天所有空闲座位、分页展示预约记录几行代码就能搞定。Redis在这个项目里扮演的角色很重。它不只是用来存验证码和Token更重要的是承担了座位状态缓存和分布式锁这两个关键任务。关于这部分我会在第三章详细展开。1.3 项目结构规划分包方式决定你后续开发效率项目结构看着是小事但一个清晰的分包方案能让你少走很多弯路。我是这样设计的com.library.reservation ├── config -- 配置类Redis、MyBatis-Plus、WebMvc、定时任务 ├── controller -- 接口层用户端、管理员端分开 ├── service -- 业务层重点所有业务逻辑都在这里 ├── mapper -- 数据访问层 ├── entity -- 实体类数据库表对应 ├── dto -- 前端交互数据对象 ├── vo -- 视图返回对象 ├── common -- 通用类统一返回结果、状态码、异常处理 ├── utils -- 工具类JWT、日期处理、座位编号生成等 ├── task -- 定时任务释放超时座位、清理过期违约记录 └── enums -- 枚举预约状态、座位状态、用户角色等这里面最想提醒大家的是controller层一定不要写业务逻辑。你可能会觉得“我就查一下数据直接在controller里写完算了”但如果后面业务越来越复杂这种写法会让代码膨胀得特别快。我在这个项目中踩过一次教训最初把“取消预约”的代码直接写在controller里后来要加“取消后是否需要扣除信用分”的需求时不得不把所有调用点一个个翻出来改。后来统一收敛到service层再改逻辑就只需要动一个地方了。2. 数据库设计一张好表胜过十次后期“打补丁”数据库是业务的地基这一块我花的时间最多。设计得好的表后面写业务代码会非常顺畅设计得烂的话各种字段不够用、状态对不上、查询奇慢的问题都会冒出来。我直接把这套系统的表结构核心部分拿出来讲讲你可以直接参考。2.1 核心表结构用户、座位、预约三张表的字段设计第一张是用户表。我这里不只是存用户名和密码还额外设计了角色、学号/工号、信用分这几个关键字段。CREATE TABLE tb_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(255) NOT NULL COMMENT BCrypt加密后的密码, real_name VARCHAR(50) COMMENT 真实姓名, student_no VARCHAR(20) COMMENT 学号/工号, role TINYINT DEFAULT 1 COMMENT 角色 1-普通用户 2-管理员, credit_score INT DEFAULT 100 COMMENT 信用分初始100, status TINYINT DEFAULT 1 COMMENT 账号状态 1-正常 0-禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_student_no (student_no) ) COMMENT 用户表;第二张是座位表。这里有一个细节我单独设计了一个seat_status字段来标记“当前状态”而不是通过预约表临时计算。虽然这意味着多了一个需要维护的字段但在查询“哪些座位是空闲的”这个高频操作上直接查一个索引字段远比联表查预约表快得多。CREATE TABLE tb_seat ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_id BIGINT NOT NULL COMMENT 自习室ID, seat_no VARCHAR(20) NOT NULL COMMENT 座位编号如A-01, seat_status TINYINT DEFAULT 0 COMMENT 座位状态 0-空闲 1-已预约 2-使用中 3-维护中, is_window TINYINT DEFAULT 0 COMMENT 是否靠窗 1-是 0-否, has_power TINYINT DEFAULT 0 COMMENT 是否有插座 1-是 0-否, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_room_seat (room_id, seat_no) ) COMMENT 座位表;第三张是预约表这是整个系统的核心。我设计了预约开始时间、预约结束时间、实际签到时间、实际签退时间、状态、违约标记等字段。状态我用了TINYINT而不是字符串因为字符串的存储空间更大而且要写枚举去维护它。CREATE TABLE tb_reservation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 用户ID, seat_id BIGINT NOT NULL COMMENT 座位ID, reserve_date DATE NOT NULL COMMENT 预约日期, start_time TIME NOT NULL COMMENT 预约开始时间, end_time TIME NOT NULL COMMENT 预约结束时间, checkin_time DATETIME DEFAULT NULL COMMENT 实际签到时间, checkout_time DATETIME DEFAULT NULL COMMENT 实际签退时间, status TINYINT DEFAULT 1 COMMENT 状态 1-已预约(待签到) 2-已签到(使用中) 3-已完成 4-已取消 5-超时未签到 6-违约, is_violation TINYINT DEFAULT 0 COMMENT 是否违约 1-是 0-否, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_date (user_id, reserve_date), INDEX idx_seat_time (seat_id, start_time, reserve_date), INDEX idx_status_time (status, reserve_date, start_time) ) COMMENT 预约表;这里要多说一句那个复合索引。一开始我只给预约表加了单列索引等数据量到了几万条之后查询某个座位在某个时间段是否被占用变得特别慢。后来按照“等值查询在前范围查询在后”的原则建了(status, reserve_date, start_time)这个联合索引查询性能明显改善。做系统设计时提前考虑索引比事后优化要省力很多。2.2 状态机设计预约状态流转的完整闭环预约系统的核心难点之一就是状态的流转要严谨。我画过一张状态流转图文字版描述给大家用户提交预约生成记录状态为“已预约(1)”。到达预约开始时间前30分钟到开始后15分钟内用户可签到状态变为“已签到(2)”。使用完毕后用户主动签退状态变为“已完成(3)”。如果用户想要取消且距离预约开始时间超过2小时状态变为“已取消(4)”座位立即释放。如果到了开始时间后15分钟用户仍未签到系统自动置为“超时未签到(5)”座位释放用户记一次违约。如果是“已签到”状态但用户在预约结束时间前就离开了且没有签退由管理员标记为“违约(6)”。这套状态机是经过和图书馆老师反复确认后定的。最关键的是“可取消”和“超时释放”的时间窗口定得太宽会有人反复预约占着位置定得太窄用户体验又很差。我这边最终定的方案是预约开始前2小时可免费取消预约开始后15分钟未签到自动释放。2.3 管理员端的统计需求一张视图搞定日报数据管理员除了管座位还特别想看每天的使用率、预约人数、违约人数。这不用单独写复杂的统计代码我直接在MySQL里建了一张统计视图CREATE VIEW v_daily_reservation AS SELECT reserve_date, COUNT(*) AS total_count, SUM(CASE WHEN status 3 THEN 1 ELSE 0 END) AS completed_count, SUM(CASE WHEN status 5 THEN 1 ELSE 0 END) AS timeout_count, SUM(CASE WHEN is_violation 1 THEN 1 ELSE 0 END) AS violation_count, COUNT(DISTINCT user_id) AS active_users FROM tb_reservation GROUP BY reserve_date;这样管理员端的“今日概况”就只需要查这一张视图轻松很多。不过要注意视图对于复杂聚合还是有性能压力的数据量超过百万级之后建议改成夜间的统计任务写入汇总表。3. 预约功能的核心实现并发场景下的“防超卖”方案预约系统最核心、也最容易被问到的一个问题是两个人同时提交了同一个座位的预约怎么办如果直接把查询和插入分开做就一定会出现并发问题。这也是面试官非常喜欢问的一道题——“你怎么防止座位超卖”3.1 为什么不建议只用select然后再insert最简单直观的做法是先查一下这个座位这个时段是否已经被预约如果没有就插入一条预约记录。但这里有一个经典的竞态条件用户A和用户B同时发起预约请求两个请求同时执行select查询发现座位都是空闲的然后同时执行insert。结果就是同一张座位在同一个时间段被预约了两次。有的同学会说“我可以给这段代码加个synchronized”。这在单机部署下确实有效但一旦系统部署到多台服务器上前面有负载均衡synchronized就锁不住了——两张服务器各跑各的线程根本不互斥。而且就算单机用synchronized锁住整个方法性能也会很差因为所有请求都在排队等同一把锁。3.2 数据库唯一约束兜底这是我的第一道防线也是最底层的一道防线。我给预约表加了一个联合唯一索引ALTER TABLE tb_reservation ADD UNIQUE KEY uk_user_slot (seat_id, reserve_date, start_time);这意味着同一天同一张座位的同一个开始时间在数据库层面只允许一条记录存在。有了这个约束即使业务代码出现并发问题数据库也会直接报错而不是产生脏数据。不过光有唯一索引还不够。如果系统真的并发请求很高数据库会抛DuplicateKeyException虽然数据一致性保住了但用户会看到一个五千错误页面这体验肯定不行。所以还需要在业务层面再做一层控制。3.3 Redis分布式锁把并发挡在业务代码之外我采用的方式是Redis分布式锁 数据库唯一约束双保险。为什么选Redis因为Redis是单线程模型setnxset if not exists这类操作天然是原子的多个服务实例同时执行也不会有竞态。这里我封装了一个简单的工具类核心逻辑就是利用Redis的setnx和过期时间public boolean tryLock(String key, String requestId, long expireTime) { // 利用setnx expire 组合保证原子性 Boolean result redisTemplate.opsForValue() .setIfAbsent(key, requestId, expireTime, TimeUnit.SECONDS); return Boolean.TRUE.equals(result); }在预约座位的核心业务方法中我先用座位ID和时间段拼接一个分布式锁的keyString lockKey seat:lock: seatId : reserveDate : startTime; boolean locked lockService.tryLock(lockKey, userId.toString(), 10); if (!locked) { throw new BizException(该座位该时段正在被其他用户预约请稍后重试); } try { // 核心预约逻辑 } finally { lockService.unlock(lockKey, userId.toString()); }这个方案做下来并发测试的结果就非常稳定了。我用JMeter模拟了50个线程同时抢同一个座位数据库最终只插入了1条预约记录其余49个请求全部返回“手慢了座位被抢走啦”的提示。3.4 Redis缓存座位状态查询性能优化的实践除了防并发Redis还承担了一个特别重要的作用座位状态缓存。用户在挑选座位时前端会频繁请求“某自习室的座位占用情况”。如果每一次请求都去MySQL里查数据库压力会非常大。我的方案是座位状态先放到Redis里定时或异步同步回MySQL。具体实现思路Key: seat:status:{roomId} Hash结构: fieldseatId, value座位状态用户查询时直接查Redis的Hash只有管理员修改座位比如设置为维护中时才同步更新缓存和数据库。预约成功后Redis中的座位状态立即更新为“已预约”同时发送一条异步消息去更新数据库保证最终一致性。这里要特别注意一个坑Redis缓存和数据库之间的一致性维护。我的策略是“先更新数据库再删除缓存”。因为更新数据库成功后如果删缓存失败最多就是下次查询发现缓存里面有旧数据可以通过设置短期过期时间来自动修正。如果反着来“先删缓存再更新数据库”一旦更新数据库失败缓存里就什么都没有了所有请求全部穿透到数据库数据库直接就垮了。4. 签到、超时释放和黑名单这些“不起眼”的逻辑才是体验关键预约功能做完了之后很多新手以为项目就结束了。但我在实际测试过程中发现真正影响用户体验和自习室秩序的其实是签到、超时释放、违约处理这些细节逻辑。这几块没做好用户根本没法信任这个系统。4.1 签到机制二维码地理围栏签到的方式我参考了现在很多图书馆系统的做法生成一个动态二维码用户在自习室门口的扫码设备上扫一下完成入座签到。二维码里加密存储预约记录ID和一个时间戳后端校验时间戳是否在签到时间窗口内。为什么不用纯定位因为大多数校园室内定位不准GPS漂移很严重。但是为了做一个兜底我会在签到接口里做一个“地理位置校验”——前端调用地图SDK获取用户当前位置后端判断该位置距离自习室是否超过500米。超过就直接拒绝签到。签到完成时做两件事// 更新预约状态为已签到 reservation.setStatus(2); reservation.setCheckinTime(new Date()); reservationService.updateById(reservation); // 更新座位状态为使用中 seat.setSeatStatus(2); seatService.updateById(seat);顺带清理Redis中的座位缓存让其他用户看到这个座位已被占用。这里有个很容易被忽略的细节签到的接口一定要做“幂等处理”。二维码可能因为网络问题被重复扫描或者用户手抖点了两次签到如果每次请求都执行更新虽然最终状态还是“已签到”但会出现“重复签退”或“重复更新耗时”这类小问题。我的做法是在签到接口入口处用分布式锁锁住预约ID第二次进来直接返回成功。4.2 定时任务超时未签到的自动释放这是整个系统里“自动化”程度最高的一个模块。用户预约了早上8:00-10:00的座位如果到8:15还没有签到系统就应该自动释放该座位并把一次违约记到用户头上。我基于SpringBoot自带的Scheduled定时任务实现每1分钟执行一次扫描Scheduled(cron 0 * * * * ?) public void releaseTimeoutReservations() { // 查询所有状态为“已预约”且开始时间已超过15分钟的记录 ListReservation timeoutList reservationMapper.selectList( new LambdaQueryWrapperReservation() .eq(Reservation::getStatus, 1) .lt(Reservation::getStartTime, DateUtil.minutesAgo(15)) .le(Reservation::getReserveDate, DateUtil.today()) ); for (Reservation r : timeoutList) { // 更新预约状态为超时未签到 // 更新座位状态为空闲 // 扣减用户信用分 } }这里要提醒大家一个分布式部署场景下的坑如果这个服务被部署到多台服务器上Scheduled任务会在每台机器上同时执行导致同一个预约被重复处理。生产环境我建议大家引入xxl-job这类分布式任务调度平台或者至少在任务执行逻辑里加一个分布式锁保证同一时刻只有一个实例在跑。还有一点时间判断不要用数据库的NOW()尽量用应用服务器的时间统一计算。因为如果数据库服务器和应用服务器不在同一台机器上时钟不一致会导致漏判或误判。4.3 信用分与黑名单让规则真正“硬”起来系统上线之前我和管理员老师讨论了很久违约了到底怎么处理光提示没用得有实际约束力。最后定了这套规则用户初始信用分100分。超时未签到一次扣20分。预约后取消不扣分鼓励大家不来的话主动取消。签退超时或中途离场未签退被管理员标记扣10分。信用分低于60分时禁止预约未来7天的座位相当于进入黑名单。信用分低于40分时永久限制预约需要找管理员申诉恢复。我通过一个信用分变更记录表来追踪每次扣分的原因确保用户可以查询自己的扣分明细避免了“莫名其妙被拉黑”的投诉。同时在黑名单判定时用定时任务每天凌晨检查一次所有用户的信用分统一更新预约权限状态而不是每次预约请求都实时计算这样可以减少不必要的性能开销。4.4 主动签退与“爽约释放”的异步消息通知用户签退时系统除了更新状态还会触发一个异步消息通知如果该座位在接下来的一段时间有其他人预约就提前给下一位用户推送“座位已释放请准备入座”。这个我是用Spring的事件机制实现的核心是Async和EventListener两个注解。发一个预约状态变更事件监听器异步处理通知逻辑这样不影响主流程的响应速度。GitHub上很多人用消息队列比如RabbitMQ来做这件事但对于这个小项目Spring自带的事件机制完全够用不需要为了引入MQ而引入MQ。5. 部署上线与常见问题排查那些文档里不会写的实战教训代码写完之后真正的考验才刚刚开始。部署到服务器上、真实用户跑起来之后各种奇怪的问题就会冒出来。我接下来说的这些问题都是我实际踩过的坑可能需要花上几天才能排查出来希望你们不用再走一遍。5.1 IDEA创建SpringBoot项目遇到的版本陷阱现在大家练手基本都是用IDEA来创建SpringBoot项目但最近Spring官方对初始化向导做了调整默认生成的SpringBoot版本可能会比较高。如果你用的是JDK 1.8环境大概率会遇到启动直接报错的情况提示类似“Unsupported class file major version”或“无法加载主类”这类问题。我遇到过最典型的一个场景是IDEA默认识别到机器上装了JDK 21生成项目时SpringBoot版本选到了3.x。SpringBoot 3.x最低要求JDK 17如果你的目标是做毕业设计或者在企业里维护老项目绝大多数机器装的还是JDK 1.8。这种情况下我建议直接在pom.xml里把SpringBoot版本改回2.7.x然后在Project Structure里把SDK切回1.8同时确认Maven的Java Compiler版本也改成了1.8。版本不是越高越好适合生产环境才是硬道理。5.2 Linux服务器部署Java环境与项目打包的踩坑记录本地开发环境一切正常一部署到Linux服务器就出各种幺蛾子。最常见的是这几个第一是Java环境变量问题。很多人习惯直接在/etc/profile里写JAVA_HOME但有些发行版默认安装的是OpenJDK的头文件路径导致命令行里java能找到Tomcat却找不到。稳妥的做法是设置完环境变量之后执行source /etc/profile并且用echo $JAVA_HOME确认路径真实存在。第二是打包问题。前后端分离的项目中前端同学构建出dist目录后我需要手动把静态文件复制到SpringBoot的resources/static目录下再执行mvn clean package。这个过程很容易出现“前端改了新的接口后端打的包还是旧的”。我在项目里干脆写了一个脚本构建前端后自动复制到后端再打包彻底杜绝人工操作的疏漏。第三是MySQL时区问题。SpringBoot连接MySQL时如果application.yml里的连接串没有配置serverTimezone在高版本MySQL/驱动下经常会出现“The server time zone value is unrecognized”的报错。我在连接串里加了serverTimezoneAsia/Shanghai解决。哪怕你本机的MySQL没报错生产环境的机器大概率会出这个问题建议一开始就加上。5.3 数据库连接池参数调优让系统扛得住抢座高峰预约系统是有明显的流量尖峰的比如每周日晚八点开放下一周座位那一瞬间的并发量可能是平时的几十倍。如果数据库连接池配置不对服务直接假死都时有发生。我用的HikariCP是SpringBoot默认的连接池配置经验如下spring: datasource: hikari: maximum-pool-size: 30 minimum-idle: 10 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000这里有几个参数要说一下。maximum-pool-size不建议盲目设置得很大很多新手以为连接池越大越好其实连接数过多反而会增加数据库的线程切换开销甚至把数据库拖垮。一般一个MySQL实例的并发能力在几百左右应用层的连接池重点在于“快速获取连接”而不是长时间持有连接。另外max-lifetime一定要小于MySQL的wait_timeout否则连接会被MySQL服务端提前关闭而应用还不知道拿到一个已失效的连接直接报错。5.4 前端联调阶段经常出现的跨域和接口问题这个项目前端用的Vue开发环境下它默认跑在8080端口后端在8081跨域问题就会冒出来。最省事的方案是后端加一个全局CORS配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这个配置在开发阶段很方便但上线后建议还是用Nginx做反向代理把前后端域名统一起来。因为生产环境开通带凭据的跨域请求如果配置不当会有潜在的CSRF风险Nginx统一入口能把这个面收敛住。另外一个联调时的高频问题就是前端传参格式和后端的DTO对不上。比如前端传了一个JSON数组后端却用RequestParam去接那肯定接不到。我的统一规范是GET请求用请求参数POST请求用RequestBody接收JSON对象。这样可以在controller层就减少很多误解。5.5 线上问题速查表直接抄作业的排查手册我最后整理了一份自己在实际排查问题时使用的速查表希望对你有用。现象可能原因排查方式项目启动时报端口被占用其他进程占用了8080端口用netstat -ano查看占用端口的PID然后结束进程或在配置里换端口登录后访问接口一直401JWT密钥配置不一致或Token过期检查JWT的签名密钥是否一致确认token是否在有效期之内座位状态显示与数据库不一致Redis缓存未及时更新检查“先更新数据库再删除缓存”的顺序确认异步更新是否执行预约提交后页面一直转圈分布式锁超时时间太短或业务处理慢调大锁过期时间或检查业务代码中是否存在慢SQL拖慢接口每日定时任务执行了多次项目在多实例部署且未加分布式锁引入xxl-job或在任务逻辑中加Redis锁签到接口偶现异常超时二维码校验逻辑中调用了慢查询给预约记录表的主键索引检查一下确认是否走了全表扫描5.6 这套系统后续还能怎么扩展如果你做完这个基础版本还觉得不过瘾有几个方向可以继续深入。一个是引入消息队列把预约通知、签到通知、违约通知全部异步化提高系统的整体吞吐量。一个是用Netty或WebSocket做实时座位状态推送。现在用户预约之后要一直刷新页面才能看到座位是否被别人占了体验并不好。如果服务端能主动推送座位变更的WebSocket消息系统会专业很多。一个是引入监控体系。用Spring Boot Actuator暴露健康检查接口配合Prometheus和Grafana做一个简单的监控面板看看JVM内存、接口耗时、数据库连接池占用情况。这一步做完整个项目的“工程化”味道就出来了写在简历上也更有亮点。最后再多说一句实际体会做这类管理系统技术永远是第二位的第一位是你对业务场景的理解深度。如果当初我没有去自习室实地坐了一下午观察用户的真实行为我根本不会想到设计“2小时前可免费取消”这个规则也不会想到信用分恢复需要“连续守约一周才能加回来”这种细节。技术方案在网上搜一搜到处都是但能把业务细节打磨到让人用着舒服才是真正拉开差距的地方。