首页 / 资讯中心 / 文章详情

疫苗预约系统实战:高并发库存一致性与权限设计

疫苗预约系统实战:高并发库存一致性与权限设计 ★ FEATURED ARTICLE
1. 项目概述与整体设计思路做企业级应用和做课程设计完全是两码事这个疫苗发布和接种预约系统就是典型的“看着简单做起来全是细节”的项目。表面上是管理疫苗信息、用户在线预约、后台审核接种记录但真正往企业级靠的时候你要面对的是高并发预约、库存一致性、状态流转控制、权限分级这些硬骨头。我最初接到这个项目的时候第一反应不是急着建表和写接口而是先想清楚一个事情这个系统的核心业务到底是谁在用、怎么用。这套系统的使用方其实有三类角色系统管理员负责维护疫苗品类、发布接种计划、审核数据医护人员或接种点工作人员需要查看预约队列、录入接种结果普通用户则在微信端或Web端完成实名注册、查看可预约疫苗、选择时间段、获取预约凭证。三类角色的操作路径完全不同对数据的读写需求也完全不同这就要求系统在架构上必须把权限和业务边界拆干净。技术选型上项目采用了SpringBoot Vue MyBatis MySQL这套组合。这个选型在今天看来可能不算“最前沿”但它是目前企业级Java后端最稳妥、人才储备最充足、踩坑成本最低的组合之一。SpringBoot负责提供成熟的依赖管理和自动化配置让团队不用花大量时间去搭建基础框架MyBatis作为持久层框架胜在SQL可控性强业务复杂的查询可以手写SQL来优化这对预约系统这种需要频繁联表查询、条件筛选的业务特别友好MySQL则承载核心业务数据配合合理索引和事务隔离级别完全能支撑中小型城市级别的疫苗接种预约量。为什么不用MyBatis-Plus全自动CRUD也不是不行但在这个项目里预约逻辑、库存扣减、状态流转都涉及多表联动和条件更新手写SQL反而更容易控制细节排查问题也直观。至于前端为什么选Vue而不是React很大程度是因为Vue对后端开发者更友好模板语法接近HTML思维团队上手快生态里的Element UI组件库做后台管理界面是一把好手。需要说明的是项目是完整可运行的源码版本包含数据库初始化脚本、后端工程、前端工程和部署文档。我在这篇文章里会拆解整个系统的核心设计思路和实现细节包括数据库表结构、后端关键接口、前端页面逻辑、以及我实际部署过程中踩过的坑。2. 需求拆解与核心功能模块设计2.1 业务角色与权限边界先讲权限。企业级系统里权限设计不是简单的前端按钮显隐而是后端接口级别的硬隔离。我把整个系统的用户分为三类管理员ROLE_ADMIN、接种点工作人员ROLE_STAFF、普通用户ROLE_USER。三者权限边界如下角色核心权限典型操作管理员全局管理疫苗品类增删改查、接种点管理、公告发布、数据统计工作人员接种点运营查看本接种点预约列表、核销预约码、录入接种结果普通用户个人操作注册登录、查询疫苗、预约登记、查看个人接种记录权限控制用Spring Security JWT实现后端通过注解PreAuthorize(hasRole(ADMIN))在接口层做拦截。这里有一个很容易被新手忽略的点前端路由守卫只是用户体验层面的优化真正的安全边界必须在后端接口上。哪怕前端把某个按钮藏起来了别人直接调接口一样能操作所以后端权限校验必须是最后一道闸门永远不能信任前端传过来的任何角色信息。2.2 疫苗发布与库存管理疫苗发布是整个系统的上游数据源管理员在后台创建疫苗批次包含疫苗名称、生产企业、批次号、接种剂次第1针/第2针/加强针、适用年龄范围、禁忌症说明、库存数量、有效期等字段。一个批次对应一个具体的库存池预约时扣减的就是批次库存。这里有个业务细节要特别注意疫苗不同于普通商品同一款疫苗可能有多个批次不同批次的生产日期和有效期不一样。用户在前端看到的是“可预约疫苗列表”但后台实际库存必须精确到批次。所以我在设计时把vaccine疫苗品类和vaccine_batch疫苗批次拆成两张表品类表放通用信息批次表放库存和有效期预约单关联的是批次ID而不是品类ID。这样一来批次过期自动下架、批次库存预警、特定批次召回这些后续需求都能平滑扩展。库存管理上我用了一个简单但有效的策略数据库字段stock记录剩余可预约数量每次预约在事务内执行UPDATE vaccine_batch SET stock stock - 1 WHERE id ? AND stock 0利用数据库行锁天然解决并发超卖问题。这个方法比“先SELECT再UPDATE”的方式更安全——因为查询和更新不是原子操作并发场景下会出现多个请求同时读到stock1然后都执行更新把库存扣成负数。而把条件直接写在UPDATE语句里数据库会在行级别加锁只有一个事务能更新成功。2.3 预约流程与状态机设计预约是整个系统的核心流程也是最容易出并发问题的环节。我画了一条完整的状态流转链待支付仅限收费疫苗→ 已预约 → 已核销 → 已完成 ↘ 已取消用户主动取消或超时未接种考虑到疫苗预约的特性大多数情况下是免费接种所以简化后的流程是用户选择接种点和时间段 → 系统校验批次库存和预约名额 → 创建预约记录并扣减库存 → 生成预约二维码 → 用户到现场出示二维码 → 工作人员核销 → 录入接种信息。这里我设计了一个状态机字段appointment_status用整数表示状态值0-已取消1-待接种2-已核销3-已完成。所有状态变更都通过后端接口完成前端只负责展示不允许直接修改状态。一个容易踩坑的地方核销操作必须做幂等控制。如果工作人员手抖点了两次核销或者前端重复提交请求同一预约单就被核销两次可能导致同一个二维码被使用多次。我的解决办法是在核销接口加唯一约束判断核销前先检查当前状态是否为“待接种”如果不是直接返回错误。另外在预约单表上加了一个version字段做乐观锁更新时带上WHERE version ?防止并发更新导致状态覆盖。2.4 时间段预约与名额控制接种点每天能承接的接种人数不是无限的必须配置每日名额上限和分时段名额。我把时间段的粒度设置成半小时一个槽位例如9:00-9:30、9:30-10:00每个槽位可预约人数由接种点管理员自行配置。预约时后端先检查该时间段当前已预约人数是否小于上限再执行库存扣减。这两个动作必须放在同一个事务里确保数据一致。我写了一个带有SELECT ... FOR UPDATE的查询来判断名额虽然在高并发下会对该时间段的行加锁但实际效果很好——不会出现同一个时间段预约人数超上限的情况。在实际压测中这种设计在单机部署、1000并发左右的情况下表现稳定。如果未来要支撑更大流量可以在应用层加分布式锁如Redis或者在上游加一个MQ做削峰填谷但就现阶段项目的定位来说数据库乐观锁 条件更新已经够用。3. 数据库设计与核心表结构解析数据库设计决定了整个系统能走多远。这一节我把核心表的字段和设计思路都列出来这部分的经验可以直接迁移到其他预约类系统比如医院挂号、政务办事预约上。3.1 用户表与扩展信息用户表我拆成了两部分sys_user存放登录认证所需的基础账号信息user_profile存放业务扩展信息姓名、身份证号、联系电话、过敏史等。拆分的原因是登录信息和业务信息变更频率差别很大拆开之后可以独立优化索引、独立扩展字段。sys_user关键字段如下字段名类型说明idbigint主键自增usernamevarchar(50)登录名唯一索引passwordvarchar(100)BCrypt加密后的密码real_namevarchar(20)真实姓名phonevarchar(20)手机号rolevarchar(20)角色编码statustinyint账号状态0-禁用 1-正常create_timedatetime创建时间身份证号没有直接存在用户主表里而是放在user_profile里单独管理并且渲染到前端时做了脱敏处理只显示前6位和后4位。这个细节是考虑到数据合规要求虽然疫苗预约不属于严格意义上的金融医疗核心数据但养成脱敏习惯总没错。3.2 疫苗品类表与批次表这里要理解一个业务差异用户关心的是“打什么疫苗、什么品牌、几针”而系统库存管理关心的是“哪一批次的疫苗还有多少支”。所以两张表各司其职。vaccine表存放疫苗品类信息id、vaccine_name疫苗名称、manufacturer生产企业、vaccine_type疫苗类型、dosage_form剂型、age_min/age_max适用年龄范围、description描述、status上架/下架vaccine_batch表存放批次库存id、vaccine_id关联品类表、batch_no批次号、production_date生产日期、expire_date有效期至、stock剩余库存、total_stock初始库存、status0-可用 1-已用完 2-已过期批次过期检测我写了一个定时任务每天凌晨扫描一次expire_date把已过期的批次自动置为不可预约状态。这个任务用Spring的Scheduled注解就能实现配合cron 0 0 2 * * ?每天凌晨2点执行。3.3 预约单表与并发控制字段appointment表是整个系统的核心交易表字段设计直接影响后续统计和运维字段名类型说明idbigint主键order_novarchar(32)预约单号全局唯一user_idbigint用户IDbatch_idbigint疫苗批次IDpoint_idbigint接种点IDappoint_datedate预约日期time_slotvarchar(20)时间段如09:00-09:30statustinyint状态0-取消 1-待接种 2-已核销 3-已完成versionint乐观锁版本号cancel_reasonvarchar(200)取消原因create_timedatetime创建时间order_no我采用了“日期 随机数 自增ID”的组合方式例如AP202506150001。没有直接用UUID当订单号因为UUID太长且无序用户报给工作人员核销时不方便数据库索引效率也低。实际生成时用Redis或在Java代码里生成保证唯一即可。3.4 接种点表和核销记录表vaccination_point表管理线下接种点包括接种点名称、地址、联系电话、每日最大接待量、状态营业/停业。每个接种点可以配置多个时间段名额这部分我单独建了point_slot_config表存接种点ID、时间段、可预约人数上限、已预约人数。vaccination_record表记录实际接种结果由工作人员在核销后填写关联预约单ID、疫苗批次ID、接种医生、接种部位、接种时间、不良反应备注等。这张表和预约表是一对一关系但单独拆出来是因为查询频率不同——记录表主要用于事后追溯和统计报表和预约流程的实时读写分开能减少锁竞争。4. 后端SpringBoot MyBatis核心实现后端工程按标准的分层结构组织controller→service→mapper共用entity、dto、vo、common、config、security等包。这里重点讲几个核心实现都是能直接拿来用的。4.1 JWT认证与Spring Security整合登录认证用的是JWTJSON Web Token无状态、跨域友好。用户登录成功后后端签发一个有效期2小时的Token返回前端前端存储在本地并放到请求头的Authorization字段中。核心配置类继承WebSecurityConfigurerAdapter重写configure(HttpSecurity http)方法http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(/api/auth/**, /api/vaccine/list, /api/appointment/public/**).permitAll() .antMatchers(/api/admin/**).hasRole(ADMIN) .antMatchers(/api/staff/**).hasRole(STAFF) .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);注意几个细节CSRF必须关闭因为JWT方案不依赖Cookie会话CSRF防护意义不大且会干扰跨域请求设置了STATELESS会话策略服务端不存Session完全靠Token认证这样才能支持前后端分离部署。JWT过滤器里做的事很简单取出请求头中的Token → 解析用户信息 → 构造Authentication对象放入SecurityContext。注意解析失败时不要直接抛异常而是放行让后续逻辑自己处理避免给攻击者提供接口探测的信息。4.2 预约接口的事务控制与防超卖实现预约接口是整个系统的技术难点实现好不好直接决定系统能不能上生产环境。核心代码如下Transactional(rollbackFor Exception.class) public AppointmentDTO createAppointment(Long userId, Long batchId, Long pointId, String appointDate, String timeSlot) { // 1. 校验疫苗批次状态和库存 VaccineBatch batch vaccineBatchMapper.selectByIdForUpdate(batchId); if (batch null || batch.getStatus() ! 0 || batch.getStock() 0) { throw new BusinessException(该批次疫苗不可预约); } // 2. 校验时间段名额 PointSlotConfig slot pointSlotMapper.selectByPointAndTime(pointId, appointDate, timeSlot); if (slot null || slot.getBookedCount() slot.getMaxCount()) { throw new BusinessException(该时间段已约满); } // 3. 生成预约单号 String orderNo generateOrderNo(); // 4. 插入预约记录 Appointment appointment new Appointment(); appointment.setOrderNo(orderNo); appointment.setUserId(userId); appointment.setBatchId(batchId); appointment.setPointId(pointId); appointment.setAppointDate(appointDate); appointment.setTimeSlot(timeSlot); appointment.setStatus(1); appointment.setVersion(1); appointmentMapper.insert(appointment); // 5. 扣减批次库存条件更新防止超卖 int rows vaccineBatchMapper.decreaseStock(batchId, 1); if (rows 0) { throw new BusinessException(手慢了库存已被抢完); } // 6. 时间段已预约人数 1 pointSlotMapper.increaseBookedCount(slot.getId()); return buildDTO(appointment); }关键点是第5步的decreaseStockSQLUPDATE vaccine_batch SET stock stock - 1 WHERE id #{batchId} AND stock 0这条SQL利用数据库的行锁和条件更新在单条语句里完成了“检查库存 0”和“扣减1”两个操作天然避免了并发超卖。执行后返回受影响行数如果为0说明库存已经被扣完了直接抛异常回滚事务整个预约操作一并撤销。还有一个隐含问题等待时间段的并发预约。如果100个人同时预约同一个时间段的最后一个名额数据库会怎样pointSlotMapper.increaseBookedCount这条更新语句执行时会锁定对应行其他事务的更新会排队等待所以最终只有1个请求成功其余全部拿到bookedCount maxCount的校验异常。这就是数据库行锁的串行化效果在最坏情况下性能会有所下降但数据一致性绝对有保障。4.3 MyBatis多表查询与XML映射MyBatis的SQL写在XML文件里虽然多写几个标签但对复杂查询的可维护性提升很大。以“查询用户预约列表含疫苗信息和接种点信息”为例select idselectAppointmentDetail resultTypecom.example.vo.AppointmentDetailVO SELECT a.id, a.order_no, a.appoint_date, a.time_slot, a.status, a.create_time, v.vaccine_name, v.manufacturer, v.vaccine_type, vb.batch_no, p.point_name, p.address FROM appointment a INNER JOIN vaccine_batch vb ON a.batch_id vb.id INNER JOIN vaccine v ON vb.vaccine_id v.id INNER JOIN vaccination_point p ON a.point_id p.id WHERE a.user_id #{userId} ORDER BY a.create_time DESC /select三张表联查用INNER JOIN就够了因为预约单在创建时就已确保关联的批次和接种点存在。如果有些历史数据可能指向已删除的批次那就改用LEFT JOIN具体业务具体分析。这里我踩过一个坑resultType映射下划线字段到驼峰属性需要在配置里开启。在application.yml里加mybatis: configuration: map-underscore-to-camel-case: true开启之后数据库的order_no就能自动映射到Java属性的orderNo不需要在XML里逐个写resultMap。如果不开启查出来的对象里这些字段全是null排查半天才发现是映射问题。4.4 定时任务与数据统计系统需要两个定时任务批次过期检测前面提到过和每日预约数据汇总统计。都用Scheduled注解实现简单可靠Component public class VaccineBatchTask { Scheduled(cron 0 0 2 * * ?) public void expireBatchCheck() { ListVaccineBatch batchList vaccineBatchMapper.selectUnExpired(); LocalDate today LocalDate.now(); for (VaccineBatch batch : batchList) { if (batch.getExpireDate().isBefore(today)) { vaccineBatchMapper.updateStatus(batch.getId(), 2); log.info(批次 {} 已过期自动下架, batch.getBatchNo()); } } } }数据统计方面后台管理首页要展示“今日预约量”“本月接种量”“各接种点预约饱和度”等指标。这些数据如果实时查数据库虽然数据量不大问题也不大但更合理的做法是建一张daily_statistics汇总表定时任务每天凌晨把前一天的预约、核销数据聚合写入前端查询直接读统计表。这样既减轻主表查询压力也方便后续做趋势分析。5. 前端Vue实现与页面交互要点前端工程基于Vue 2 Element UI Axios Vue Router构建。为什么选Vue 2而不是Vue 3因为项目启动时Vue 3生态还不够成熟Element UI对Vue 2的支持最完善团队也最熟悉。如果是新项目Vue 3 Element Plus也是不错的选择但代码写法上会有差异需要团队统一评估。5.1 前端项目结构和路由设计前端目录结构如下src/ ├── api/ // 接口请求封装按模块拆分 │ ├── auth.js │ ├── vaccine.js │ ├── appointment.js │ └── user.js ├── assets/ // 静态资源 ├── components/ // 公共组件 ├── router/ // 路由配置 ├── store/ // Vuex状态管理 ├── views/ // 页面组件 │ ├── admin/ // 管理后台页面 │ ├── staff/ // 工作人员页面 │ └── user/ // 用户端页面 └── utils/ // 工具函数含axios实例封装路由配置采用动态路由 静态路由结合的方式。登录后根据角色动态添加对应权限路由避免用户直接访问无权限页面。比如普通用户登录后路由表里只有首页、疫苗列表、我的预约管理员登录后多出用户管理、疫苗管理、接种点管理等路由。5.2 Axios拦截器与Token刷新机制Axios实例封装是前端的核心基建我在utils/request.js里做了两层拦截请求拦截器从localStorage取出Token放到请求头Authorization: Bearer token这样后端就能识别用户身份。响应拦截器统一处理HTTP状态码。401表示Token失效我做了自动跳转登录页并清除本地用户信息的处理403表示无权限提示“当前账号无权操作”其他业务错误码后端在响应体里返回code字段统一弹Message提示。这里有一个很多新手容易忽略的点Token过期后的体验优化。如果用户正在填写预约表单填写到一半Token过期点击提交直接被踢回登录页体验非常糟糕。我的处理方式是在响应拦截器里判断401时把当前路由地址存到sessionStorage登录成功后跳回原页面。虽然不能解决表单数据丢失的问题但至少用户能快速回到原来的功能入口。5.3 预约页面的核心交互逻辑用户端的预约页面是整个前端最复杂的部分交互链路是选择疫苗 → 选择接种点 → 选择日期和时间段 → 确认预约。疫苗列表用卡片式展示每张卡片上显示疫苗名称、生产企业、剩余库存和“立即预约”按钮。库存为0或批次已过期的疫苗按钮置灰不可点击。这里的库存数据是通过/api/vaccine/list接口查询的实时数据不是页面写死的。选择接种点和时间段时我用了Element UI的DatePicker 自定义时间段卡片组件。日期选择器禁用了今天之前的日期时间段根据后端返回的point_slot_config动态渲染。已经约满的时间段显示“已约满”并禁止选中可预约的显示剩余名额。用户点击“确认预约”后前端先弹出确认框展示预约信息摘要确认后调用后端接口。后端返回成功时前端跳转到“预约成功”页面显示预约单号和预约二维码。二维码用qrcode前端库生成内容就是一个包含预约单号的链接工作人员扫码后跳转到核销页面。6. 部署上线与常见问题排查实战这一节是实践经验的总结。项目做完只是第一步真正把它部署到服务器上、跑起来不出问题才是企业级应用的目标。6.1 环境准备与前后端构建后端部署的服务器环境是LinuxCentOS 7.9需要提前安装好JDK 1.8SpringBoot 2.x推荐Java 8MySQL 5.7使用时区设置要特别注意后面讲Nginx 1.20用于前端静态资源托管和反向代理Maven 3.6用于构建后端jar包后端打包命令mvn clean package -DskipTests打包后在target/目录下生成vaccine-system-1.0.0.jar上传到服务器后使用nohup java -jar vaccine-system-1.0.0.jar --spring.profiles.activeprod /app/logs/vaccine.log 21 --spring.profiles.activeprod指定生产环境配置文件生产环境的数据源密码、Redis地址等敏感信息单独维护在application-prod.yml里和开发环境隔离。前端打包npm install npm run build生成的dist/目录通过Nginx托管。Nginx配置里要做两件事静态文件服务 反向代理。核心配置如下server { listen 80; server_name vaccine.example.com; # 前端静态资源 location / { root /app/vaccine-front/dist; index index.html; try_files $uri $uri/ /index.html; # 支持Vue Router的history模式 } # 后端API反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files那一行特别重要Vue Router如果使用history模式刷新页面时Nginx需要把所有路径都回退到index.html否则刷新二级页面会报404。6.2 部署过程中的常见错误与解决方案我把部署中遇到的高频问题整理成一张速查表这些问题基本都是新手容易卡住的地方问题现象可能原因解决方案数据库连接报Public Key Retrieval is not allowedMySQL 8.0默认认证插件和连接驱动不匹配JDBC连接串加allowPublicKeyRetrievaltrue或改用caching_sha2_password插件前端访问API报跨域错误前后端分离部署浏览器跨域拦截优先用Nginx反向代理解决Nginx层面同源或后端配置CORS过滤器中文乱码数据库字符集不是UTF-8建库时指定CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci并在JDBC连接串加characterEncodingutf-8打包时Maven下载依赖超时网络问题或镜像源不稳定在settings.xml配置阿里云Maven镜像定时任务不执行SpringBoot主类缺EnableScheduling主启动类加上该注解预约时一直报“时间段已约满”数据库时间字段类型与Java LocalDate映射错误在MyBatis配置里注册LocalDateTypeHandler或升级MyBatis版本这里重点讲一下MySQL 8.0的坑。MySQL 8默认使用caching_sha2_password认证插件而老版本的JDBC驱动如5.1.6以下不认识这个插件就会报错。解决方法是升级MySQL Connector/J到8.0版本同时在连接串上加两个参数jdbc:mysql://localhost:3306/vaccine_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltruecharacterEncodingutf8serverTimezone也必须显式指定否则半夜跑定时任务时日期可能差8小时导致批次过期判断错乱。6.3 性能优化与并发压测实录系统上线前我做了一轮并发压测用JMeter模拟300个用户同时抢同一批疫苗的预约名额。压测结果300个并发请求成功预约208个其余92个全部返回“库存不足”或“已约满”的业务提示无超卖现象无异常报错平均响应时间从初始的2.3秒下降到优化后的0.6秒。优化措施主要做了三个第一热点数据缓存。疫苗列表属于读多写少的数据用Redis缓存疫苗列表30秒减少数据库QPS。只有管理员上下架疫苗或批次变化时才主动刷新缓存。第二CDN加速静态资源。前端的JS、CSS打包文件放在CDN上Nginx只负责HTML入口和API代理静态资源加载速度提升明显。第三SQL索引优化。预约查询、时间段统计都建了联合索引。比如appointment表上建了(user_id, appoint_date)联合索引、(point_id, appoint_date, time_slot)联合索引配合EXPLAIN查看执行计划确保没有全表扫描。压测还暴露了一个问题单台服务器上数据库的行锁等待会导致部分请求超时。所以部署时我把数据库连接池参数调整了一下maximum-pool-size从默认10调整到50connection-timeout设为3000毫秒缓解了高并发下的连接池打满问题。6.4 数据备份与应急预案企业级应用最后必须考虑的数据安全。我配置了两层机制自动备份每天凌晨3点用mysqldump备份数据库备份文件保留最近7天脚本如下#!/bin/bash BACKUP_DIR/data/backup/mysql DATE$(date %Y%m%d%H%M) mysqldump -uroot -pYourPassword vaccine_db $BACKUP_DIR/vaccine_db_$DATE.sql find $BACKUP_DIR -mtime 7 -name *.sql -exec rm -f {} \;配合crontab每天执行。手工快照每次上线新版本前用云平台制作数据库快照万一导出数据时误操作或迁移脚本有问题还能回滚到上一个稳定版本。这个步骤看起来繁琐但真正出过事故的人都知道这是救命稻草。7. 项目源码结构与二次开发建议源码的目录结构直接影响接手人的上手速度。我梳理一遍当前项目的完整工程结构这部分可以让拿到源码的开发者少走弯路。前端工程根部vaccine-front/ ├── public/ ├── src/ │ ├── api/ │ ├── router/ │ ├── store/ │ ├── views/ │ └── main.js ├── package.json ├── vue.config.js └── .env.production后端工程根部vaccine-system/ ├── src/main/java/com/example/vaccine/ │ ├── controller/ // 接口层只做参数校验和返回 │ ├── service/ // 业务层事务控制核心 │ ├── mapper/ // MyBatis接口 │ ├── entity/ // 数据库实体 │ ├── dto/ // 接收前端参数对象 │ ├── vo/ // 返回前端视图对象 │ ├── common/ // 全局异常、统一返回、工具类 │ ├── config/ // 配置类Security、Redis、Swagger等 │ └── task/ // 定时任务 ├── src/main/resources/ │ ├── mapper/ // MyBatis XML文件 │ ├── application.yml │ └── application-prod.yml ├── pom.xml └── sql/ ├── schema.sql // 建表语句 └── data.sql // 初始化数据拿到源码后第一件事不是急着启动而是先跑通数据库初始化脚本。sql/目录下的schema.sql按依赖顺序建库建表data.sql里预置了管理员账号和几个测试疫苗批次。启动后端前修改application-dev.yml里的数据库连接配置确认数据库能连通再启动。二次开发建议方面我推荐几个方向接入消息推送预约成功和接种前一天给用户发短信或微信通知。可以在预约成功接口后接一个MQ发送方把消息丢进队列消费者负责调第三方短信接口避免预约接口被短信服务拖慢。引入Redis分布式锁目前防超卖依赖数据库行锁已经是单机版最稳的方案。如果部署多实例需要考虑分布式锁——用Redisson的RLock或Redis的SET NX实现锁的粒度是“批次ID 时间段”。增加报表可视化管理后台目前的数据统计是表格形式可以用ECharts把预约趋势、疫苗消耗、各接种点饱和度做成折线图、饼图、热力图让管理员一眼看清全局。对接疫苗接种电子档案如果后续要和本地疾控系统对接需要把vaccination_record表的数据通过接口推送出去。建议提前在记录表里预留external_id字段存第三方系统的对应ID方便对账和追溯。我自己做完这个项目最大的体会是疫苗预约系统的核心不是“预约”这个动作本身而是库存、名额、状态、权限这四件事的一致性和安全性。任何一个环节没想清楚上线后都会出事故。尤其是并发场景下的超卖问题很多新手把精力花在花哨的页面上等到部署到生产环境才发现库存对不上账那就非常被动了。如果你正打算做类似的预约系统医院挂号、场馆预约、政务办事这套架构完全可以作为底子来改。把疫苗批次换成医生排班或场地时段把接种记录换成业务流水核心的并发控制、状态流转、权限隔离思路都是通用的。最后再分享一个小技巧每个表的create_time和update_time字段一定要加并且建表时就用DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP自动维护。这个小习惯在排查数据问题时价值极大没有时间字段出问题你连什么时候写入的都不知道排查效率会大打折扣。
阅读完成 · 觉得有帮助?
咨询建站