上次因为一个会员制医疗预约系统的毕业设计,我在实验室连续肝了三个星期。说实话,这类题目看起来平平无奇,做起来却能把 Spring Boot 全家桶里最常碰的东西全部摸一遍:REST API、Spring Security、JWT、Caffeine 缓存、WebSocket 推送、定时任务释放号源,甚至还有 OpenFeign 和 QueryDSL 的版本兼容问题。如果你也正在纠结“Spring Boot 会员制医疗预约服务管理信息系统”这种题目该怎么架构、怎么写、怎么避免做成一个纯增删改查的玩具项目,这篇就把我实际开发时的完整思路、取舍逻辑和踩过的坑一起拆给你看。
先说结论:这系统绝对不是一个简单的预约登记表。真正的难点在于“会员制”三个字——它牵扯到等级权益、积分折扣、认证授权,再加上医疗预约特有的号源排班、防并发抢号、状态机流转,整个项目做下来,技术深度和业务复杂度都足够支撑一篇漂亮的毕业设计说明。这篇文章我只讲策略和关键代码片段,不会把全部源码贴出来,但看完你应该能亲手搭一个能跑、能答辩、能讲清楚原理的系统。
1. 项目定位与需求拆解
1.1 会员制医疗预约到底在解决什么问题
很多同学拿到题目后第一反应就是做三张表:用户表、医生表、预约表,然后写一堆 CRUD 页面。但仔细想一想,普通预约和会员制预约的区别在哪里?普通预约是“谁都能约,约完拉倒”,会员制预约则多了一层用户运营:会员是充过值、办过卡或者有专属权益的群体,系统必须能识别会员身份,给他不同的折扣、优先预约权、积分回馈,还要记录会员的消费和积分流水。抛开医疗场景,你会发现这和外卖会员、视频网站会员的逻辑是相通的,只是换到了医疗领域。
再往深一层讲,医疗预约场景有很强的行业特性。医生排班不是随便填的,一个科室一周可能只有几个半天门诊,一个时段只能放 10 个号;患者取消预约后号源要能立刻释放,否则就浪费了资源;医生临时停诊要能批量通知已预约用户。这些需求如果不在设计阶段想清楚,后期改起来会非常痛苦。我一开始也天真地以为预约表只要存个开始时间、结束时间和用户 ID 就行,后来被导师追问了几个业务场景,才发现漏了一大块。
所以我把核心需求拆成三条主线。第一条是会员服务体系:会员等级、折扣规则、积分累计、余额账户。第二条是预约调度体系:排班生成、号源释放、预约状态流转。第三条是数据运营体系:日志记录、统计报表、消息通知。这三条线支撑起了整个系统的骨架,也决定了后端要拆成哪几个模块。
1.2 用一个项目练会 Spring Boot 的分层设计
这类题目的价值不只是“能跑”,而是让你把 Spring Boot 的分层架构真正落到代码里。Controller 只负责参数校验和响应封装,Service 层承载业务规则,Repository 层做数据访问,领域对象里存放业务状态变化逻辑。我见过很多同学把判断逻辑全写在 Controller 里,一个方法写了一百多行 if else,后面加一个会员场景就崩了。
我的做法是严格执行“Service 层事务控制 + 领域状态机”的模式。比如预约这个动作,Controller 拿到请求后只调用appointmentService.reserve(),真正把“检查号源数量、检查会员等级、扣减库存、生成预约单”这件事放在一个@Transactional方法里完成。这样既保证数据一致,也方便我写单元测试的时候直接 mock Service 接口,不必去启动整个 Web 容器。
另外注意,Spring Boot 的包结构一定要按模块切,不要按技术层切。我见过有人把entity、mapper、controller各建一个包,结果所有业务混在一起,很难维护。我习惯按业务域建包:member、schedule、appointment、notification,每个包内部再分controller、service、repository、domain。这样项目一多,你会发现自己查找和定位问题快得多。
1.3 功能模块梳理与页面角色划分
系统分两端:用户端和管理端。用户端面向会员,提供注册登录、会员卡购买、科室与医生查询、预约挂号、预约记录、积分明细等能力。管理端面向医院运营人员,负责科室管理、医生排班、号源维护、会员审核、订单退款、停诊通知等。这个划分决定了权限模型:普通会员只能操作自己的数据,管理员可以管理基础数据,超管还能看统计报表。
千万别一上来就急着写页面。我建议先把所有 URL 和角色权限列一张表,比如/api/member/**只有 MEMBER 角色能访问,/api/admin/schedule/**只有 ADMIN 角色能访问。这样做的好处是后面接 Spring Security 时,你只需要对着表配 URL 规则就行,不会漏掉接口。我自己吃过亏,一开始没列权限清单,后来写安全配置时反复改了好几轮。
2. 技术选型与核心设计思路
2.1 Spring Boot 版本、JDK 与依赖管理的选择
我用的 Spring Boot 3.2.5,搭配 JDK 17。说实话,现在再开新项目就别选 JDK 8 了,Spring Boot 3 对虚拟线程、Records、Switch 表达式这些新特性支持得更好,而且很多依赖的新版本已经不再兼容旧 JDK。如果你在学校机房装的是 JDK 8,那就选 Spring Boot 2.7.x,两者写起来差别不大,但要注意 Spring Security 5 和 6 的配置差异,后面我会单独说。
依赖管理我强烈建议用 Maven 而不是 Gradle。不是说 Gradle 不好,而是毕业设计场景下网上能抄的、能搜到的 Spring Boot 项目绝大多数是 Maven 工程,遇到问题更容易找答案。pom.xml里尽量选官方spring-boot-starter-parent作为父工程,这样可以统一管理依赖版本,避免自己手动维护一堆版本号。比如你引入spring-boot-starter-web、spring-boot-starter-data-jpa、spring-boot-starter-security,这三个 starter 的版本都会被父工程锁住,冲突的概率小很多。
第一次启动 Spring Boot 项目其实是最简单的一关。用 IDEA 的 Spring Initializr 直接勾选需要的依赖生成工程,或者去 start.spring.io 上把项目下载下来,双击mvn spring-boot:run就能起来。但很多人会卡在端口占用、数据库连接失败这类环境问题上,所以我习惯在application.yml里先把日志级别调到debug,启动报错时能看到具体是哪个 Bean 没注入、哪个 URL 连不上,而不是对着一个无头无尾的报错干瞪眼。
2.2 数据库表设计:排班、号源、预约单怎么解耦
数据库设计是整个项目的根,根烂了,代码写得再花也没用。医疗预约系统最少要有这些表:member(会员)、member_level(会员等级)、department(科室)、doctor(医生)、schedule(排班)、appointment(预约单)、payment_order(支付订单)、member_credit_log(积分流水)、schedule_cancel_notice(停诊通知)。其中最容易设计错的就是排班和号源的关系。
我先说一个常见错误:直接在schedule表里放一个remaining_count字段,每次预约就UPDATE schedule SET remaining_count = remaining_count - 1 WHERE id = ?。这种做法在低并发下没问题,但一旦有两个人同时抢最后一个号,就可能导致超卖。我在正式号源表里为每个号源单独建一条记录,比如上午 10:00 到 10:30 这个时段放了 10 个号,就生成 10 条schedule_slot记录,每条记录有slot_status(AVAILABLE、LOCKED、BOOKED、CANCELLED)。预约时只是把一条 AVAILABLE 的记录改成 LOCKED,再用数据库唯一索引保证一个人在同一时段只能预约一次。
别怕数据量大。一个科室一天几十个号源,一个月才几千条数据,完全够用。而且这种设计在业务上有个大好处:可以精确记录每个号源是被谁预约的、什么时候锁定的、后来有没有被释放,排查问题时非常直观。
预约单表我单独建,不跟号源明细混在一起。预约单字段包括:appointment_no、member_id、slot_id、doctor_id、department_id、status(PENDING_PAYMENT、PAID、CANCELLED、COMPLETED、EXPIRED)、create_time、cancel_time、pay_time。这里最关键的是status字段要能完整表达用户旅程。用户提交预约如果没有立即支付,就先创建一条 PENDING_PAYMENT 的预约单,同时把号源锁住;支付成功后变成 PAID;医生标记就诊完成后变成 COMPLETED;超时未支付则由定时任务改为 CANCELLED 并释放号源。这样一张表的状态机就把整个流程串起来了。
2.3 认证与权限:会员制系统的地基
聊会员制,躲不开一个问题:怎么区分普通访客和会员?最简单的方案是给用户加一个member_level字段,但这只解决了“身份标识”,没有解决“为什么你能用这个接口”的授权问题。我建议直接上 Spring Security + JWT,登录成功后发一个 Token,前端每次请求带着 Token,后端从 Token 中解析出用户 ID 和角色。Spring Security 负责 URL 访问控制,JWT 负责无状态认证,两者配合很默契。
关于 Bean 注入,Spring Boot 3 里我全部用构造器注入,不再使用@Autowired字段注入。原因很简单:构造器注入让依赖关系显式化,写单元测试时直接new XxxService(memberRepository, appointmentRepository)就行了,不会出现 Spring 容器没起来就报空指针的尴尬。如果你在看旧教程时发现有人用@Resource或者@Autowired写在字段上,不是不行,只是可测性差。项目里我自定义了一个@CurrentMember注解,配合 Spring MVC 的HandlerMethodArgumentResolver,在 Controller 方法参数里直接拿到当前登录会员对象,省去每个接口都从 Token 里解析用户信息的重复代码。
权限配置要注意一个坑:Spring Security 6 和 5 的写法差别很大。Spring Boot 3 里配置类要继承SecurityFilterChain的SecurityFilterChainBean,WebSecurityConfigurerAdapter已经被移除了。我刚开始升级时用的还是 5 的写法,结果项目直接启动报错。Spring Security 6 的requestMatchers("/api/admin/**").hasRole("ADMIN")其实更直观,配起来不复杂。
3. 核心功能实现详解
3.1 防止号源超卖:乐观锁和 Redis 锁该怎么选
预约系统最怕的就是超卖:明明只剩一个号,两个人同时提交,结果两个人都显示预约成功。我在 2.2 里说了按号源明细加行锁是最稳的,但具体到代码实现,还要决定用悲观锁还是乐观锁。
悲观锁就是SELECT ... FOR UPDATE,事务开始后直接把这条号源记录锁住,另一个事务等待。这种方式写起来简单,适用并发量不高的系统。测试下来,一台机器上 100 个并发抢 50 个号,悲观锁能保证不超卖,但响应时间会随着冲突增加而变长。乐观锁则是在schedule_slot表加一个version字段,更新时执行UPDATE schedule_slot SET status='LOCKED', version=version+1 WHERE id=? AND version=?,影响行数为 0 就说明版本冲突,重试或者提示“号源已被抢完”。
我个人更推荐乐观锁,原因有两点。第一,医疗预约的并发量远没有电商秒杀那么夸张,乐观锁几乎不存在性能瓶颈;第二,乐观锁不会一直占用数据库连接,不容易引发连接池耗尽。我给号源表加了一个version字段,并且把UPDATE语句封装在 Repository 里,业务代码看起来非常清爽。注意不是说用了乐观锁就可以忽略事务,检查状态、更新状态、创建预约单这三个动作仍然要放在同一个事务里,只是不再需要长时间的悲观阻塞。
另外,如果你的部署环境中还有多实例,乐观锁的冲突概率会上升。那时候可以考虑把“锁定号源”这一步做成 Redis 分布式锁,用setNx带过期时间实现。但说实话,毕业设计阶段用乐观锁完全够用,老师问起来你也能把两种方案差异讲清楚,反而是加分项。
3.2 会员等级与折扣计算:策略模式实战
会员制系统一定会有不同等级的会员,比如普通会员 95 折、银卡会员 9 折、金卡会员 85 折。这个需求看似简单,最蠢的写法就是 Service 里写if (level.equals("SILVER")) { price *= 0.9 },一旦等级增加到五六个,这段代码会越来越烂。我用的是策略模式,把折扣计算拆成一个个策略类,再通过 Spring 注入到一个DiscountCalculator里。
具体做法是先建一个MemberDiscountStrategy接口,里面一个方法BigDecimal calculate(BigDecimal originalPrice, MemberLevel level),然后用@Component分别实现NormalStrategy、SilverStrategy、GoldStrategy。在MemberLevel枚举里加上一个strategyClass字段,计算的时候直接从 Spring 容器中按类型取对应的 Bean,这样新增等级时只需要新增一个策略类,不用改任何 Service。而且这里能顺便练到 Spring 的依赖注入、ApplicationContext.getBean()的高级用法,答辩时也是个亮点。
优惠计算还不只是折扣,还有积分抵扣。我的积分规则是:每消费 10 元累计 1 积分,100 积分抵 1 元。积分抵扣和折扣不能叠加,用户只能选一种,这个规则我放在OrderCalculator里做。真实的医疗预约可能没有那么复杂的营销玩法,但你把这种通用业务规则做出来以后,换一个行业照样能复用。
3.3 缓存设计:Caffeine 如何扛住高并发查医生排班
预约系统查询频次最高的接口是什么?不是预约,而是“查医生排班”。用户一进科室页面就会按日期翻排班,这种读多写少的场景最适合加缓存。我用的是 Caffeine,纯本地缓存,比 Redis 轻量得多,也不依赖外部服务。配合 Spring Cache,你只需要在查询排班的方法上加上@Cacheable(cacheNames="doctorSchedule", key="#doctorId + '-' + #date"),Caffeine 就会自动把返回结果缓存起来。
这里有一个容易踩的坑:本地缓存是 JVM 级的,多个实例的缓存不共享,而且缓存过期时如果大量请求同时打到数据库,就会造成缓存击穿。解决方式有两种,一是在查询方法里加synchronized或者用 Caffeine 的refreshAfterWrite做异步刷新,二是直接把一层 Redis 放在 Caffeine 前面。我的建议是毕业设计用 Caffeine 足够,只要把过期时间设短一点,比如 60 秒,然后留意一下热点数据的更新频率就行。如果是真实的互联网医疗平台,那肯定得再套一层 Redis,但技术本质是相通的。
缓存更新也要想清楚。医生调整排班后,对应医生的缓存必须立刻删除,否则用户看到的还是旧排班。我用 Spring Cache 的@CacheEvict在排班更新的接口上同步清理相关缓存。记住@CacheEvict默认是在方法执行成功后才删缓存,所以不用担心事务回滚的时候已经把缓存删了的问题。
3.4 日志与操作审计:用 AOP 记录关键行为
医疗系统的日志比普通系统要求高,因为要追溯谁在什么时候改了什么数据。Spring Boot 默认集成的是 Logback,application.yml里配置日志级别和输出格式就够了。但我还想记录“用户行为”级别的日志,比如会员修改了手机号、管理员停用了某个医生的排班。这种操作日志如果靠每个业务方法里手动写,会漏很多。
我的做法是定义一个@AuditLog注解,用 Spring AOP 切面在标注了该注解的方法执行后,把当前用户、方法名、请求参数、执行结果统一记录到operation_log表里。AOP 切面本身要小心一点:不要在切面里执行耗时的 SQL,日志写入用异步线程池。这里我用了@Async,配合自定义线程池跑日志写库,避免影响主业务流程。如果你只想简单记录访问日志,直接用 Logback 输出 JSON 格式到文件也行,反正答辩时你能讲清楚自己的设计目标就好。
日志不要光看输出,还得会查问题。我开发时把com.example.appointment这个包名的日志级别设成DEBUG,其他框架的日志保持INFO,这样排错时能看清 SQL 和关键参数,又不会被 Spring 的内核日志刷屏。
4. 订单状态机与消息通知
4.1 预约状态流转:一个状态机管住所有异常情况
预约单的状态如果只设计“已预约、已取消、已完成”,那跑不了多久你就会发现很多特殊情况处理不了:用户预约后一直没支付,号源被占着;医生停诊,用户不取消,号源永远释放不出来。我用了一个状态机,核心状态包括PENDING_PAYMENT、PAID、CANCELLED、COMPLETED、EXPIRED。
状态机怎么落地?不要满代码里写if (appointment.getStatus() != AppointmentStatus.PAID),而是把状态转移规则封装在Appointment领域对象里。比如cancel()方法内部先判断当前状态是否允许取消:PENDING_PAYMENT和PAID都可以取消,但COMPLETED就不能取消。字段更新通过 JPA 的乐观锁版本号控制,避免并发下状态被错误覆盖。
我还会在预约单表加一个cancel_type字段,区分是用户主动取消、超时未支付取消,还是医生停诊取消。这样后面做统计分析时,才能知道多少号源是因为患者原因浪费的,多少是因为医院调整造成的。这个细节看起来不起眼,却是答辩时能打动评审老师的业务深度。
4.2 WebSocket 与通知:医生停诊时怎么把消息推给会员
预约成功之后,用户当然希望收到提醒。我做的是一套组合通知:预约成功时发送系统站内信,医生停诊时通过 WebSocket 实时推送到前端弹窗。短信和邮件需要第三方服务商,毕业设计可以只做接口预留,但 WebSocket 这一块建议你亲身搭一遍,因为这是很多企业项目里很常见的需求。
Spring Boot 集成 WebSocket 其实不难,在pom.xml加spring-boot-starter-websocket,然后配置一个WebSocketConfigurer注册handshake拦截器和WebSocketHandler。如果你用的是 Spring Boot 3.2,yml配置里还能设置spring.websocket.port等参数,但通常默认端口就能工作。关键是前端连接的时候怎么带上用户身份,我用的方案是:用户登录后拿到 JWT,前端在 WebSocket 连接 URL 后面拼上 token,后端在拦截器里解析 token,把用户 ID 存入 WebSocketSession 的 attributes 里。
有一个很实际的坑:当管理员停诊并批量设置排班状态为取消时,后端要能查到所有受影响预约单关联的会员,然后推送通知。这个查询如果用for循环逐条查数据库会有点慢,我选择用 QueryDSL 把同一个医生、同一个日期、状态为PAID的预约单一次性查出来,再根据会员 ID 分组后推送。这里就遇到版本兼容问题了,Spring Boot 3.2 里的 QueryDSL 需要单独引入querydsl-jpa并配置QClass生成目录,我折腾了好一会才让它和jakarta.persistence顺利配合上。如果要省事,直接用 Spring Data JPA 的@Query写 JPQL 也能达到同样目的。
4.3 定时任务释放过期号源
过期未支付的预约单不能一直占用号源,我用了 Spring Boot 的@Scheduled定时任务,每 30 秒扫描一次,把所有status = PENDING_PAYMENT且创建时间超过 15 分钟的预约单批量改成EXPIRED,同时把对应的号源状态改回AVAILABLE。
这里有两个细节。第一是扫描的 SQL 一定要加索引,否则随着数据量增长,全表扫描会拖垮数据库。我是在create_time和status上建了联合索引。第二是定时任务要加分布式锁,避免多实例部署时两个节点同时处理同一批数据。我用的方案是ShedLock,但如果你只有一个实例,完全不用考虑这个。答辩时如果能主动提到“这个任务在多实例下会重复执行,我通过分布式锁解决了”,比只会跑通功能要强很多。
5. 实际开发中踩过的坑与排查技巧
5.1 Spring Security 的版本迁移坑
这是我这几天最痛的一段。Spring Boot 3 自带的 Spring Security 6,把原来继承WebSecurityConfigurerAdapter的老写法彻底抛弃了。我一开始在网上找的教程都是 Spring Boot 2 时代的,@Override configure(AuthenticationManagerBuilder auth)等等一大堆方法在新版本里根本不存在。最后翻官方文档才弄明白,现在只需要声明一个SecurityFilterChainBean,在里面用 lambda 配置规则。
常见的迁移问题还有两个。第一,antMatchers改成了requestMatchers,如果你从旧项目复制代码,编译阶段就会报错。第二,默认密码加密方式变了,Spring Security 6 的PasswordEncoder默认要求你显式声明一个 Bean,否则启动时可能报错。我最后使用的是BCryptPasswordEncoder,一次性替换掉原来明文存储的用户密码。这个坑几乎每个人都会踩,提前知道能省半天时间。
如果你的项目里的用户表不是 Spring Security 默认提供的内存用户,记得实现UserDetailsService,从数据库查出用户后返回一个UserDetails对象。别自作聪明地把密码存成明文,加密这件事不需要自己发明算法,用官方提的 BCrypt 就行了。
5.2 日志排查:Bean 注入失败的三种典型场景
Spring Boot 项目经常遇到NoSuchBeanDefinitionException,第一次遇到会慌,其实无非三种情况。第一,类上忘了加@Service或@Component,Spring 根本不知道要创建这个 Bean。第二,有多个实现类但没指定@Qualifier,自动装配时不知道该注入哪个。第三,依赖的 Configuration 类没有被扫描到,比如配置类放在了项目根包之外。
排查技巧是看启动日志里的Excluded和Condition evaluation,Spring Boot 启动时其实打印了非常多的诊断信息,只是很多人刚启动完就急着往后翻。我还习惯在 IDEA 里打开Services窗口的Spring Boot控制台,把所有 INFO 级别日志保留下来,搜关键词Bean或者类名,很快就能定位到缺失的注入点。
5.3 Caffeine 缓存命中和多环境配置
Caffeine 配置里面要注意maximumSize和expireAfterWrite不是设置得越大越好。如果内存紧张,一个缓存实例塞了几十万条数据,GC 压力会变大。我做压力测试时发现,排班数据量大概几千条,设置maximumSize=1000就够了。如果有些数据确实很热,可以单独建一个更大的缓存实例,而不是一股脑全放进去。
多环境配置文件也别忽略。开发环境、测试环境、生产环境的数据库地址、日志级别、缓存参数都不一样。我用的方案是application-dev.yml、application-prod.yml,然后在启动命令后面加--spring.profiles.active=dev。这套方法是 Spring Boot 的核心实践,毕业设计里用了会显得很规范。
5.4 把项目做“深”比做“多”更重要
最后想给你一个心态上的建议。这类题目往往要求学生做一个“信息系统”,很多同学会拼命堆功能,会员管理做了、预约做了、支付做了、问诊也做了,每个模块都是两张表一个列表,看起来屏幕很多,但答辩时一问核心逻辑就答不上来。我反而觉得,把“预约防超卖”这一个点做扎实,把乐观锁和事务传播机制讲清楚,远比塞进五个不成熟的功能更能体现能力。
做深一个点的具体方法可以是:设计一个压测方案,统计并发预约的成功率和响应时间;对比一下去掉缓存前后接口 QPS 的变化;写清楚表结构里每个唯一索引是用来防什么的。这些都是老师在论文里愿意看到的“工作量”,而且你确实做了实验,回答起来底气足。
6. 从零到部署:给新手的快速上手指南
6.1 28 天完成这个项目的节奏建议
如果你是自己一个人做毕业设计,建议别按瀑布流走,我的节奏是:第一周做需求梳理和数据库设计,把所有表字段都定下来,ER 图画清楚;第二周搭 Spring Boot 工程,实现会员注册登录和权限管理;第三周实现排班、预约、号源管理这三块核心业务;第四周补上缓存、消息通知、定时任务和报表,最后留两天部署和写文档。其中最容易拖进度的是权限管理和状态机设计,这两块一定不要临到头再想。
项目里不需要额外引入太重的微服务架构。很多同学一看热词里“Spring 和 Spring Boot 和 Spring 微服务有什么区别”就纠结,是不是要用微服务?我明确说,毕业设计这个体量,单体应用 + 模块化包结构已经足够。微服务带来的分布式事务和链路追踪会让你崩溃,在答辩里也解释不清。
6.2 部署方案:Docker 包一个镜像就够
部署这块我推荐用 Docker Compose,把 MySQL 8.0 和 Spring Boot 应用放在一个 compose 文件里,一条docker-compose up -d就能拉起整个环境。Spring Boot 3 的镜像可以用eclipse-temurin:17-jre作为基础镜像,打包成 jar 后运行。如果不想引入 Docker,直接在服务器上装 JDK 和 MySQL,用nohup java -jar xxx.jar --spring.profiles.active=prod &启动也完全没有问题。
这里提醒一句:application-prod.yml里的数据库密码别硬编码,我用环境变量注入。如果你提交到 GitHub,记得把该文件加入.gitignore,不然密码泄露之后很麻烦。虽然医疗预约项目不是真正的生产系统,但习惯从一开始就要养好。
6.3 答辩展示时怎么讲清楚设计与实现
技术架构可以画一张简单的分层图,从 Controller 到 Service 再到 Repository,说明每层的职责。核心业务就讲两个故事:一个用户登录后,是怎么查到排班、提交预约、锁定号源、生成预约单的;一个管理员停诊后,WebSocket 是怎么把通知推送到用户手机的。把这些故事串起来的时候,把自己做过的乐观锁、缓存、定时任务这些亮点穿插进去,整个思路就流畅了。
不要背源码,但要把关键流程的代码风格记熟,比如乐观锁的UPDATE ... WHERE version=?,以及状态机的switch分支。老师可能会问“你这个系统最多支持多少并发”,你可以回答“在单机 200 并发测试下预约接口 QPS 稳定在 50 左右,主要瓶颈在数据库更新,通过缓存和乐观锁可以继续扩展”,这种答案一听就是真正测过的。
做这个项目最大的收获,不是学会背几个 Spring Boot 注解,而是理解了什么叫“把一个业务约束用技术手段落地”。预约不超卖、状态不乱跳、会员等级能扩展,这些问题的答案都藏在数据库设计、状态机、事务和锁的细节里。我后来面试聊到这类项目时,面试官最感兴趣的也正是我踩过的那些并发坑和版本迁移问题。所以别怕遇到 bug,解决一个,你的项目就厚实一分。如果你现在刚开始这个题目,建议先把数据库表和状态流转图画出来,代码反而是最简单的那一步。