做这类疫苗发布和接种预约系统,我前前后后帮人看过不少套代码,自己也完整落地过一个。说实话,大部分毕设项目或者个人练习项目,把功能跑通不难,难的是把预约这种高并发场景下的坑真正处理好。这篇文章我就以springboot疫苗发布和接种预约系统为核心,把我从需求拆解、表结构设计、接口实现到并发控制踩过的坑,完整梳理一遍,希望能帮你少走点弯路。
1. 疫苗发布和预约系统,核心痛点到底在哪里
1.1 这类系统看起来简单,实际却没想象中容易
很多人一拿到题目,第一反应就是:不就是个CRUD吗?疫苗发布就是往表里插一条数据,预约就是往预约表里加一条记录。确实,如果只是做个管理后台,那真的没什么难度。但一旦考虑到真实使用场景——大量用户同时在某个时间段抢着预约疫苗——问题就完全不一样了。
我接触到的不少需求里,疫苗预约往往有很强的时间集中性。比如某个社区卫生服务中心放号,可能就固定早上10点整放出未来三天的名额。用户会卡着点刷新,一瞬间涌进来的请求量可能是平时的几十倍。这时候如果接口没有做并发控制,超卖(超发)就是必然发生的事。
另外,疫苗本身是有批次、有有效期、有库存概念的。某个批次的疫苗到货一批,录入系统后分批发布,每一批对应的可预约数量、可预约时间段都不同。用户预约成功之后,还要考虑取消预约,取消后名额要回补库存,这些环节在表结构设计上如果不提前想清楚,后期改起来会非常痛苦。
1.2 从模糊需求到具体功能模块的拆解
我习惯先把角色理清楚。这个系统最基础的角色一般有三个:
- 管理员:维护疫苗批次信息、发布放号、查看预约统计数据、处理异常预约。
- 普通用户:浏览疫苗信息、查看当前可预约的时间段、提交预约、取消预约。
- 系统层面:定时任务自动关闭过期预约、回补未接种的名额,记录操作日志。
从这些角色反推功能模块,系统至少需要包含:
- 用户注册登录(手机号+验证码或密码)
- 疫苗管理(疫苗类型、生产厂家、批号、有效期)
- 发布管理(选择疫苗批次,设置可预约时间段、开放数量、放号时间)
- 预约管理(提交预约、取消预约、查询我的预约)
- 通知管理(预约成功通知、放号提醒)
在动手写代码之前,把这些功能列表拉出来,和客户或者导师确认一遍,远比你直接建表写接口省时间。因为很多隐含需求是在这个环节才能浮出来的,比如"用户是否可以同时预约多个不同疫苗","预约记录在接种后是否需要留档"。这些不确定点,越早确认越好。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术栈和项目结构,我为什么这么选
2.1 Spring Boot版本、持久层框架和前端方案的搭配
这套项目我最终采用的组合是:
- Spring Boot 2.7.x(对应JDK 1.8,稳定为主)
- MyBatis-Plus 3.5.x(强调单表CRUD效率和代码生成能力)
- MySQL 8.0(InnoDB引擎,这是并发事务处理的基础)
- Redis 6.x(库存预扣、接口幂等、高频数据缓存)
- Vue 2 + Element UI(管理后台),用户端用轻量的Thymeleaf或者直接前后端分离
为什么要用Spring Boot 2.7而不是3.x?这里有个很实际的原因:很多高校的毕设环境和资料教程,围绕的还是Spring Boot 2.x生态,网上可查的资料多,出了问题好排查。而且jdk版本在1.8环境下,2.7是兼容性最好的选择。如果你是自己在企业做项目,可以考虑3.x,但就这个项目的定位而言,稳定、资料丰富、跑通顺畅才是第一位的。
持久层我选了MyBatis-Plus而不是原生MyBatis或者JPA。理由也很简单:这个系统的查询场景偏多,而且大部分是单表操作。MyBatis-Plus的LambdaQueryWrapper基本覆盖了90%的查询需求,分页插件一行配置就能用。真正复杂的那几条SQL,我再手写XML注入进去,灵活度和开发效率兼顾。
2.2 包结构划分,按业务域还是按技术层
这个细节很多人不在意,但我认为对后期维护影响很大。按技术层分包(controller、service、mapper)是经典做法,但项目一旦膨胀,controller包下几十个类,找起来是真难受。
我采用的是按业务域分包作为顶层组织,然后在每个业务域内再按技术层分:
code复制com.example.vaccine
├── common // 通用类:统一返回体、异常处理、工具类
├── config // 配置类:Redis、MybatisPlus、CORS
├── modules
│ ├── user // 用户模块:controller, service, mapper, entity
│ ├── vaccine // 疫苗模块
│ ├── release // 疫苗发布模块
│ └── appoint // 预约模块
├── quartz // 定时任务
└── Security // 登录鉴权相关
这样做的直观好处是,你在改"预约模块"的代码时,所有相关类都在同一个包路径下,IDE代码树一展开就很清晰,新人接手也不需要花时间找类。
3. 疫苗发布功能的设计与实现,状态机是关键
3.1 发布的状态流转,我建议你画一张状态表
疫苗发布不是单纯地往release表里插记录,它是有生命周期的。我理出来的状态流转如下:
| 当前状态 | 操作 | 下一状态 | 说明 |
|---|---|---|---|
| 草稿 | 提交发布 | 已发布 | 前台可看到放号信息 |
| 已发布 | 预约满员 | 已约满 | 自动触发,或用户查询时实时判断 |
| 已发布 | 手动截止 | 已截止 | 管理员下发通知停止预约 |
| 已约满 | 有人取消预约 | 已发布 | 名额回补后恢复 |
| 已截止 | 接种日结束 | 已结束 | 定时任务自动执行 |
状态字段我通常在表里用一个int类型存,配合写代码时定义的枚举类。注意一点:判断状态变化时,不要散落在各个service里乱set,最好收敛到一个专门的service方法,或者用状态机模式统一管理。我见过太多项目,状态字段在代码里被随意赋值,最后查"哪些发布是已截止"状态时怎么都查不准。
3.2 放号接口的实现与库存扣减
放号这个操作,核心就做三件事:
- 把发布的疫苗信息(疫苗id、批次、时间段、总名额)写入发布表
- 把可预约名额初始化一份到Redis,key设计成
vaccine:release:stock:{releaseId} - 触发一个通知,告诉关注该疫苗的用户"可以预约了"
前两步没什么好说的,关键在第二步。为什么名额要同步一份到Redis?因为预约时高频的扣减操作不能直接打MySQL。你想想,一个时间段就100个号,瞬间来了500个请求,每个请求先select库存再update库存,MySQL的锁竞争会把请求拖垮。把库存预热到Redis里,用Redis的原子操作(incr/decr)来扣减,每秒支撑几千个请求轻轻松松。
Redis扣减代码大概是这样的:
java复制public boolean deductStock(Long releaseId) {
String key = RedisKeyUtils.buildReleaseStockKey(releaseId);
Long remain = stringRedisTemplate.opsForValue().decrement(key);
if (remain != null && remain >= 0) {
return true;
}
// 超扣了,回补
stringRedisTemplate.opsForValue().increment(key);
return false;
}
这个逻辑本身不复杂,但要注意一个坑:如果扣减成功,但是后续写预约记录时MySQL出错了怎么办?我之前第一版就是这么写的,扣库存和写预约单之间没有做一致性兜底,结果出现过几次"名额扣了,但是预约记录没生成"的问题。
解决思路是加一个"补偿任务":定时扫描预约记录表,找出"已扣库存但预约数据不完整"的记录,让用户重新预约或者系统自动取消。更简单的做法是把扣库存放在本地事务的最后一个步骤,用数据库的乐观锁来兜底,但这会牺牲一点性能。我的选择是保性能放在Redis,同时数据一致性靠兜底任务保证,双保险。
3.3 疫苗批次信息的管理
疫苗批次——如果这块不做,发布就是无源之水。每个批次需要记录:
- 疫苗类型(科兴、生物、智飞等)
- 生产厂家
- 批号(国家药品追溯码,这个字段必须唯一)
- 生产日期、有效期
- 到货数量
- 适用年龄段说明
批号唯一性建议直接在建表时加unique索引,代码层面再用查询确认一次,双保险。不然同一批号的疫苗被重复录入,后续统计接种数据时全乱套了。
4. 接种预约模块,并发和一致性是重头戏
4.1 预约接口的整体流程
预约接口的流程我捋过好几版,最终的落地方案是这样的:
- 接收用户预约请求(releaseId、userId、接种日期)
- 校验token,确认用户登录状态
- Redis分布式锁:
lock:appoint:{userId},防止用户重复提交 - 校验发布状态:当前状态是否可预约
- Redis扣减库存
- 生成预约记录(insert预约表)
- 异步通知用户(如果接入了微信或者短信)
这个流程里有几个值得展开的点。
4.2 用户重复提交问题,除了分布式锁还要幂等
分布式锁解决的是短时间内重复点击的问题:
java复制public boolean tryLock(String userId, String releaseId) {
String lockKey = "lock:appoint:" + userId + ":" + releaseId;
Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1",
Duration.ofSeconds(10));
return Boolean.TRUE.equals(locked);
}
但是分布式锁只能保证一段时间内互斥,如果用户在锁过期之后又点了一下,还是会走到后续逻辑。所以我建议在预约记录表上加一个唯一约束,比如(user_id, release_id, appoint_date, vaccine_id),这样数据库层面兜底,重复插入直接抛DuplicateKeyException,我在代码里捕获这个异常然后返回"请勿重复提交"。
4.3 防止超卖的最后一道防线:数据库乐观锁
虽然库存扣减已经在Redis做了,但在极端场景下(比如Redis突然宕机),需要有数据库层面的兜底。我的做法是在release表加一个stock字段,每次预约成功时:
java复制int updated = releaseMapper.deductStock(releaseId);
if (updated == 0) {
throw new BusinessException("名额已满");
}
对应SQL:
xml复制<update id="deductStock">
UPDATE vaccine_release
SET stock = stock - 1
WHERE id = #{releaseId}
AND stock > 0
</update>
这样即使Redis挂了,最终到数据库层库存也不会变成负数。stock > 0这个条件就是乐观锁的变种,由MySQL的行锁保护,不会超发。
4.4 取消预约,回补库存别忘了解除冲突
取消预约的逻辑看起来简单——把预约记录状态改成已取消,再把库存加回去。但有两个细节容易被忽略:
- 取消操作也要加锁,防止用户"取消的同时又点预约",两个操作并发导致库存数量不对。
- 如果已经接种完成(状态是已接种/已完成),就不能取消了,这一步校验别漏。
回补库存时,Redis的increment操作也要注意加回后不能超过原始总量。理论上不会出现这个问题,但防御性编程总没错,加个上限判断也就几行代码的事。
5. 数据库表结构设计,五张核心表就够了
5.1 从用户到预约单,核心表一览
这套系统不算复杂。我最终沉淀下来,核心表就六张:
用户表(user)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| phone | varchar(20) | 登录手机号,唯一 |
| password | varchar(128) | 加密存储 |
| real_name | varchar(30) | 真实姓名 |
| id_card_no | varchar(30) | 身份证号 |
| role | tinyint | 1-普通用户 2-管理员 |
| status | tinyint | 0-禁用 1-正常 |
| create_time | datetime | 创建时间 |
密码存储这一块我强调一下:不要明文存,至少用BCrypt加密。Spring Security的BCryptPasswordEncoder直接就能用,或者MyBatis-Plus官网推荐的加密方案也成。数据泄露事故见得太多了,这点成本不能省。
疫苗信息表(vaccine)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| vaccine_name | varchar(50) | 疫苗名称 |
| manufacturer | varchar(50) | 生产厂家 |
| batch_no | varchar(50) | 批次号,唯一索引 |
| vaccine_type | tinyint | 疫苗类型 |
| valid_date | datetime | 有效期至 |
| total_count | int | 到货总数量 |
| description | text | 接种说明 |
| create_time | datetime | 录入时间 |
疫苗发布表(vaccine_release)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| vaccine_id | bigint | 关联疫苗表 |
| release_title | varchar(100) | 发布标题 |
| appoint_start_time | datetime | 可预约开始时间 |
| appoint_end_time | datetime | 可预约结束时间 |
| population | tinyint | 适用人群 |
| release_status | tinyint | 状态:草稿/已发布/已约满等 |
| stock | int | 剩余可预约数量 |
| total_stock | int | 总放号量 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 最后更新时间 |
appoint_start_time和appoint_end_time这个字段设计有讲究。不要只存一个"放号日期",要精确到时分秒,因为预约系统是按时间段来控制的。比如今天中午12点放号,晚上10点截止,这个窗口期才是真正在控制用户能"在哪段时间内完成预约操作"。
预约记录表(appointment_record)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 用户ID |
| release_id | bigint | 发布ID |
| vaccine_id | bigint | 疫苗ID |
| appoint_date | date | 预约接种日期 |
| appoint_time_slot | tinyint | 时间段(1-上午 2-下午) |
| status | tinyint | 1-已预约 2-已接种 3-已取消 4-爽约 |
| remark | varchar(200) | 备注 |
| create_time | datetime | 预约创建时间 |
| update_time | datetime | 更新时间 |
加唯一索引:uk_user_release(user_id, release_id, appoint_date, vaccine_id),防重复提交。
接种记录表(vaccine_record)(可选,但要提前考虑)
记录用户实际接种信息:接种点、接种医生、接种时间、疫苗批号、接种部位、不良反应。这张表在"已预约转已接种"流程中使用,管理后台人员扫条码或点击确认接种后写一条记录。
5.2 索引设计的几个容易踩的坑
这系统查询场景比较典型,我整理过几个高频SQL:
- 查看"当前可预约的发布列表":
SELECT * FROM vaccine_release WHERE release_status = 1 AND appoint_start_time < NOW() AND appoint_end_time > NOW() - 查看"我的预约记录":
SELECT * FROM appointment_record WHERE user_id = ? ORDER BY create_time DESC - 定时任务查"超过放号时间但仍然未满的发布":
SELECT * FROM vaccine_release WHERE release_status = 1 AND appoint_end_time < NOW()
对应索引建议:
- vaccine_release表:加
idx_status_time(release_status, appoint_end_time) - appointment_record表:加
idx_user_time(user_id, create_time) - appointment_record表:加
idx_release_status(release_id, status)
有人说,我这表数据量也就几万条,不加索引也一样快啊。这话在数据量小的时候没问题,但系统运营起来后,预约记录表增长很快,特别是通知记录、操作日志这些会爆炸性增长。提前建好索引,避免后期再改,是成本最低的方案。
6. 定时任务和缓存同步,系统的"隐形齿轮"
6.1 定时任务场景梳理
这套系统至少有四个场景适合用定时任务:
- 自动截止:过了可预约结束时间的发布,状态从"已发布"自动改为"已截止"。每天晚上跑一次,刷掉过期数据。
- 库存回补:用户取消了预约但取消状态没有及时同步到Redis。虽然正常流程已经回补了,但兜底任务确保万无一失。
- 数据统计:每天凌晨生成报表,比如某疫苗当天预约了多少人,实际接种了多少人。
- 释放未支付/超时未确认的预约名额:如果设计了"预约后需要在X小时内确认"的规则,这个就要用定时任务扫描。
6.2 Spring Boot里的定时任务非常简单,但要注意单线程问题
Spring Boot原生支持的@Scheduled注解确实方便,但默认是单线程执行的。如果你写了两个任务,一个跑得很久,另一个就会一直等。所以我在项目里会用@EnableAsync配合@Async标注耗时任务,或者直接用Quartz框架配置线程池。
我踩过一次坑:当时有个"超时未确认则取消预约"的任务,每5分钟执行一次,但执行开始时需要扫描全表并关联查询疫苗发布表,单次执行要跑40多秒。然后"自动截止"任务就被卡住不跑了。排查了很久才发现是线程池问题。
code复制tomcat线程池接请求,定时任务线程池是另一个,但是@Scheduled默认只有一个线程
解决方案很简单:配置一个TaskScheduler:
java复制@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5);
scheduler.setThreadNamePrefix("vaccine-scheduler-");
return scheduler;
}
从此定时任务之间不再互相干扰。
6.3 缓存同步策略,Redis和MySQL怎么保持一致
发布放号时,库存是写进Redis的。但管理后台修改发布信息时(比如把总量从100改成80),Redis里的数据就成了脏数据。这里我建议采用最粗暴也最有效的方案:
只要发布信息发生变更,直接删除对应的Redis库存key,让预约接口下一次去MySQL重新加载。
具体落地为:在release的update接口里,除了更新数据库,同时调用:
java复制stringRedisTemplate.delete(RedisKeyUtils.buildReleaseStockKey(releaseId));
这比"先删Redis再更新数据库"再多一个顺序问题好解决多了。删除key的操作是幂等的,不会产生脏数据,最多就是下一次预约时重新从MySQL加载而已,性能影响可以忽略不计。
7. 权限控制和接口安全,不能只做表面功夫
7.1 登录鉴权方案
这个系统有管理员、普通用户两种角色,权限控制是必要环节。我不建议用复杂的Spring Security + OAuth2全家桶配置,对这类轻量系统来说,用JWT(JSON Web Token)配合拦截器就够了。
实现上分三步:
- 登录接口校验手机号密码,成功后生成JWT,把用户ID、角色信息塞进token里返回给前端。
- 前端每次请求在Header里带
Authorization: Bearer token。 - 后端写一个拦截器(HandlerInterceptor)解析token,若合法则通过,并将用户信息放入ThreadLocal。
我封装了一个UserContext工具类,方便在Service层直接拿当前登录用户的信息,而不必把userId当参数传来传去。
java复制public class UserContext {
private static final ThreadLocal<Long> CURRENT_USER = new ThreadLocal<>();
public static void setUserId(Long userId) {
CURRENT_USER.set(userId);
}
public static Long getUserId() {
return CURRENT_USER.get();
}
public static void clear() {
CURRENT_USER.remove();
}
}
这个工具类配合拦截器,写预约接口的时候直接:
java复制Long userId = UserContext.getUserId();
清爽又安全。
7.2 管理端接口要防"越权"
我看过很多毕设项目,管理端的删除接口直接暴露成GET /admin/delete/{id},不带任何权限校验,这属于裸奔。我的建议是管理端接口单独加一个@RequireAdmin注解,在拦截器处统一校验角色。
java复制@Component
public class AdminAuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
UserSession user = (UserSession) request.getAttribute("user");
if (user == null || !"admin".equals(user.getRole())) {
throw new BizException(401, "无权限访问");
}
return true;
}
}
然后再给Spring MVC注册拦截器并指定路径:
java复制registry.addInterceptor(adminAuthInterceptor)
.addPathPatterns("/admin/**");
这样管理端的接口统一收到/admin/路径下,权限控制一目了然。用户端无法访问这些接口,很多隐患直接消除。
8. 踩坑实录:那些我真实遇到过的问题
8.1 第一版上线后预约高峰期接口大面积超时
当时的现象是:放号时间一到,前端大量请求打到预约接口,MySQL的CPU飙升到100%,整个系统几乎不可用。
排查过程:
- 先看日志,发现大量线程阻塞在数据库查询上。
- 再看慢SQL日志,发现一条高频SQL没走索引:
SELECT * FROM vaccine_release WHERE release_status = ?——release_status字段没有索引,全表扫描。 - 更严重的是,预约时有一个先查库存再扣库存的步骤,一个2000并发就能把数据库连接池打满。
修复:
- 给release_status加索引。
- 所有放号查询改成先查Redis,存在则返回,不存在再查MySQL并回填。
- 扣库存操作改为"只扣库存、异步写预约记录",预约记录和扣库存之间通过消息队列异步解耦。
这一步改动之后,5000并发实测稳定。
8.2 库存扣减超卖一次,原因是Redis挂了
那是一次运维事故。Redis服务OOM导致进程挂掉,因为我当时没有做Redis不可用时的熔断降级,预约接口直接报"库存异常"。恢复后复盘发现,事故期间有几个用户预约成功,但Redis扣减操作其实没有执行成功,产生了超卖。
后来我在代码里加了一个逻辑:如果检测到Redis不可用(捕获连接异常),直接降级为数据库乐观锁扣库存。虽然性能差一些,但至少不超卖。
伪代码如下:
java复制public boolean deductStock(Long releaseId, int totalStock) {
try {
return tryDeductStockByRedis(releaseId);
} catch (Exception e) {
log.warn("Redis扣库存失败,降级为数据库扣库存:{}", e.getMessage());
return deductionBySql(releaseId);
}
}
这个思路在项目落地上非常实用,特别是刚上线阶段,不稳定因素比功能bug更致命。
8.3 用户预约成功后收不到通知,一查是异步线程吞了异常
因为短信通知和微信模板消息我用的是@Async方法异步执行,但Async方法内部没有try-catch,一旦第三方接口报错,异常直接在异步线程里被吞掉,日志也没打。用户那边收不到通知,后台也不报错,直到有人反馈才发现。
从这以后,我在所有异步方法里第一行就加try-catch包裹,异常全部打日志。同步邮件、短信、微信通知这些外部调用,我统一封装成NotificationService,里面逐个try-catch,保证个别渠道异常不影响主流程。
9. 从毕设到可以实际部署,还需要做什么
9.1 部署环境和运维要点
如果是个人项目或毕设,我通常建议部署在一台2核4G的云主机上就足够。Java应用用java -jar启动,配置nohup后台运行,或者直接写个简单的systemd服务。MySQL和Redis都用Docker方式起,管理起来简单:
bash复制docker run -d --name mysql \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=yourpassword \
-v mysql-data:/var/lib/mysql \
mysql:8.0
docker run -d --name redis \
-p 6379:6379 \
redis:6.2
Java应用打包配置要注意,项目用了MyBatis-Plus,maven打包时默认会把XML文件排除掉。得在pom.xml里显式声明:
xml复制<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*.xml</include>
<include>**/*.yml</include>
</includes>
</resource>
</resources>
否则你会遇到一个经典报错:Invalid bound statement (not found),排查半天发现是XML没打进去。
9.2 拓展方向:如果想把项目做得更出彩
如果你是拿这个题做毕设,想拿高分或者想要面试时有项目亮点,我建议在下面几个方向选一个加分:
-
对接微信小程序:用户端做成微信小程序,管理员端保留Web后台。小程序端天然适合预约类应用,面试聊起来也可以谈到微信登录、模板消息推送,加分项很明显。
-
引入MQ异步削峰:在预约高峰期,请求先发到MQ队列,后端服务直接返回"排队中",再由消费者异步处理预约。这个架构更容易讲清楚"怎么扛高并发"。
-
数据可视化大屏:做一张实时数据看板展示当日预约人数、各疫苗预约占比、接种进度。技术上也不复杂,用ECharts定时拉取接口就行,但视觉上非常加分。
-
多级缓存:Redis一级缓存 + Caffeine本地缓存二级缓存,解决热点数据问题。面试时说到这个,基本可以聊10分钟不冷场。
10. 最后聊聊这套系统的核心心得
回到标题本身,springboot疫苗发布和接种预约系统这类项目,真正考验人的不是Spring Boot框架本身,而是以下几点:
- 对业务场景的理解:疫苗发布不是简单插入一条数据,它需要状态管理、批次关联、时间窗口控制。
- 对并发的敬畏:预约接口天然高并发,必须考虑超卖、重复提交、接口幂等。
- 对数据一致性的把控:Redis和MySQL之间的双写一致性、定时任务的补偿机制,这些是系统健壮性的关键。
我最初做第一版的时候,也觉得这就是个"管理系统",结果上线后各种问题接踵而至。后来慢慢总结出一套适合这类场景的开发方法论:先梳理状态流转,再设计表结构,然后把并发场景逐个攻破,最后才是写业务接口。顺序反了的话,返工的次数会让你崩溃。
如果你正准备开发这样一套系统,我的建议是不要上来就写代码。先把上面说的状态流转表理清楚,把每个人物的操作路径走一遍,把表结构设计出来。这几个工作做扎实了,后面的编码只是时间问题。这个思路不仅适用于疫苗预约,任何类似的预约类系统——挂号、场馆预约、考试报名——都复用得上。
