ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Vue疫苗预约管理系统:架构、防超卖与部署全解析

SpringBoot+Vue疫苗预约管理系统:架构、防超卖与部署全解析 做企业级项目最怕的不是技术难而是需求绕、状态乱、并发一上来就出幺蛾子。这套基于SpringBootVueMyBatisMySQL的疫苗发布与接种预约管理系统是我最近完整过了一遍源码的实战项目从疫苗建档、库存批次管理到用户注册、线上预约、现场核销再到接种记录归档整条业务闭环都有。本文不搞那种源码粘贴一遍的复读机式讲解我会把这套系统从架构设计、数据库建模、核心接口实现到前端交互、部署踩坑一层层拆开聊适合正在做毕业设计、想快速上手企业级管理系统开发或者准备拿SpringBootVue全家桶练手的朋友。看完整篇文章你不仅知道这套源码怎么跑起来更知道它为什么这么设计、每个模块背后解决的是什么痛点。1. 系统需求与整体模块拆解1.1 疫苗预约系统要解决的几个真实痛点先不说代码聊业务。疫苗发布和接种预约这类系统在企业和社区场景里其实小而杂——用户量不一定爆炸但业务状态多、流程长、还涉及卫生安全合规每一步记录都不能丢。这套源码把需求收敛成几个核心模块我梳理一下疫苗全生命周期管理从疫苗产品建档名称、生产企业、规格、剂次到批次入库批号、有效期、库存数量再到批次审核发布疫苗的状态全程可追溯。接种点与医生排班每个接种点有独立的库存配额医生/接种员账号按接种点归属前端按接种点展示可预约号源。用户预约与核销闭环用户注册登录后选择疫苗批次和接种点选择日期与时间段提交预约后锁定库存现场接种时通过核销码完成确认预约状态从已预约流转为已完成。接种记录与统计分析完成接种后生成接种记录后台可按疫苗、按接种点、按时间段统计接种人次这部分对管理者来说是最有价值的。这套系统的角色管理也很有代表性不是一刀切的单用户模型而是分了三种角色角色核心权限典型操作管理员Admin全量管理疫苗建档、批次发布、接种点管理、数据统计、账号管理接种点医生Doctor接种点维度操作查看本点预约名单、核销预约码、登记接种记录普通用户User个人操作注册登录、查询疫苗、在线预约、查看接种记录与电子凭证这种RBAC基于角色的权限访问控制设计是大量企业管理系统的通用骨架。把这套权限模型看懂了后面遇到类似项目你直接能复用。1.2 为什么这套源码值得完整跑一遍市面上SpringBootVue的练手项目不少但大多数是CRUD四件套——用户管理、文章管理、分类管理完事。这套源码的价值在于它覆盖了带状态的复杂业务流转疫苗批次状态待审核 - 已发布 - 已售罄 - 已下线预约单状态待支付/待确认 - 已预约 - 已核销 - 已取消 - 已过期库存变动的强一致操作每个预约动作都涉及库存扣减而且必须防止超卖这些状态流转和并发控制才是企业级项目的灵魂。做过真实系统的人都知道查询接口好写但状态机设计、事务边界划分、数据一致性保证才是真正考察功底的地方。把这份源码的流转逻辑吃透比你写十个增删改查Demo都有价值。2. 技术架构与工程结构精讲2.1 后端SpringBoot分层架构这套系统的后端没有用那种全塞Service里的粗暴写法而是标准的企业级三层架构。后端工程结构大致是这样src/main/java/com/xxx/vaccine/ ├── config/ # 全局配置类跨域、拦截器、MyBatis配置 ├── controller/ # 接口层只做参数接收和响应封装 ├── service/ # 业务层事务边界全部在这一层 ├── mapper/ # MyBatis数据访问接口 ├── entity/ # 数据库实体对象 ├── vo/ # 前端视图对象用于接口响应数据组装 ├── utils/ # 工具类JWT工具、日期处理、Result封装 └── VaccinationApplication.java # 启动类这个分层看着普通但有几个细节值得说道controller层很薄只做参数校验和调用service任何业务判断都不在controller里写。这样做的好处是接口层稳定后面加接口只加方法不会牵一发动全身。service层直接叫接口实现类的形式Service接口定义业务方法Impl实现具体逻辑。这个模式经常被吐槽过度设计但在这个项目里管理员只读疫苗信息和医生核销预约这类不同角色的差异化鉴权实际就靠service层的细分来完成。拆开写权限逻辑各归各家。vo层是刚需而非冗余比如预约详情接口前端需要展示疫苗名称、接种点地址、剩余库存、医生姓名。这些字段分散在三四张表里如果用entity直接返回要么暴露多余字段要么前端要连环调接口。vo层在这里就是给前端定做的数据结构这个意识很多新手项目里是没有的。启动类没做什么花活标准写法。但有两点生产环境里必须注意一是MapperScan要扫到mapper包路径二是数据源配置建议放application-prod.yml别把线上数据库密码写死在主配置里。2.2 前端Vue工程结构与组件设计前端用的是Vue 2 Element UI这组经典搭配。工程结构如下vue-admin/ ├── src/ │ ├── api/ # 接口请求封装按模块拆文件 │ ├── assets/ # 静态资源 │ ├── components/ # 公共组件UploadExcel、Pagination等 │ ├── router/ # 路由配置含动态路由和权限控制 │ ├── store/ # Vuex状态管理 │ ├── views/ # 页面视图 │ │ ├── login/ # 登录页 │ │ ├── vaccine/ # 疫苗管理、批次发布 │ │ ├── appointment/ # 预约页面、预约列表 │ │ ├── site/ # 接种点管理 │ │ ├── record/ # 接种记录与统计 │ │ └── user/ # 用户中心与电子凭证 │ ├── utils/ # 请求工具axios封装、鉴权工具 │ └── main.js说两个容易踩坑的地方第一axios封装一定不能省。这套源码里所有请求都走统一封装的request.js统一配置了baseURL比如/api请求拦截器里自动带上Authorization: Bearer token响应拦截器里统一处理Http状态码和后端业务错误码。比如后端返回401前端直接跳login页。没有这层封装每个页面都写一遍token处理和错误提示维护成本直接爆炸。第二路由守卫和动态权限是一对好搭档。源码里router.beforeEach做三件事判断有没有token、没有就跳登录页有token但访问的是登录页就跳首页再根据用户角色里的菜单权限生成动态路由并router.addRoutes。这个思路很关键因为不同角色看到的菜单和能访问的页面完全不一样。把权限控制放在路由层面做统一拦截页面组件内部就不用到处写v-ifrole admin了。2.3 技术选型的几个关键判断标准为什么这套源码选MyBatis而不是JPA说白了还是业务决定的。疫苗预约这个场景里预约列表要按用户、时间、状态、接种点多个维度组合查询MyBatis的if动态SQL处理这种多条件组合搜索非常顺手。库存扣减的UPDATE ... SET stock stock - 1 WHERE id ? AND stock 0这种原子操作用XML写出来语义清晰JPA的抽象反而绕。多表联查预约单关联查询疫苗、接种点信息直接用自定义SQL效率可控SQL优化手段索引调整、执行计划分析也都能直接用上。当然MyBatis的代价就是你要手写大量SQLresultMap映射繁琐。但在这个项目里联查的结果映射本来就是刚需手写反而让数据流向更透明。一句话总结CRUD简单的项目用JPA省事复杂联查和条件查询多的业务用MyBatis更稳。前端选Element UI而不是更潮的Ant Design Vue或Naive UI图的还是生态成熟和组件齐全。表格、表单、分页、日期选择器、对话框这些后台管理系统的高频组件Element UI开箱即用遇到问题搜一下基本都有答案对开发效率的提升非常实际。3. 数据库设计一张表一张表抠关键细节3.1 核心数据表全景这套系统的数据库叫vaccine_system核心表有9张左右。我按业务域分组列一下基础档案域user用户表含用户名、密码BCrypt加密存储、手机号、身份证号、角色类型、状态。vaccine疫苗产品表存疫苗名称、生产企业、批准文号、规格、剂次1/2/3、适用人群、禁忌、不良反应说明。vaccine_batch疫苗批次表存所属疫苗ID、批号、生产日期、有效期、入库数量、剩余数量、状态。vaccine_site接种点表存接种点名称、地址、联系电话、每日最大预约量。doctor_info医生/接种员表绑定user表和vaccine_site表。业务流转域appointment预约单表存预约用户ID、批次ID、接种点ID、预约日期、时间段、状态、核销码。inoculation_record接种记录表存预约单ID、实际接种日期、接种医生ID、批次ID、接种部位、不良反应记录。notice公告表存公告标题、内容、发布时间、发布状态。表之间的关系不复杂但业务约束值得注意。比如appointment表同时关联了user、vaccine_batch、vaccine_site三张表实际上就是谁、在哪个点、打哪批疫苗三个维度的交叉记录。在实际建表时这四张表之间要加外键索引否则列表查询的联查性能会很难看。3.2 状态字段设计用int还是varchar这个细节我重点说一下。预约单表里的状态字段源码采用的是int类型并且用常量类做映射0 已取消 1 已预约待接种 2 已核销已完成 3 已过期未按时接种为什么不用varchar存已预约、已完成这类语义化字符串两个原因性能int字段在索引查询和比较时比字符串快数据量大时差别明显。代码维护状态值在代码里是常量引用AppointmentStatusEnum改了展示文案只需改前端映射数据库不用动。如果存字符串一旦上游需求把已预约改成待接种全表数据都得update。当然int状态的代价是读代码时需要状态枚举对照表个人建议在项目里维护一份统一的枚举类把状态值、状态中文名、可流转到哪些状态都集中定义别散落在各个Service里。3.3 库存字段的防超卖处理疫苗批次表里的remaining_count是典型的高并发热点数据。这套源码的扣减SQL非常关键不是先查出来再在Java里判断而是直接一条原子SQLUPDATE vaccine_batch SET remaining_count remaining_count - 1 WHERE id #{batchId} AND remaining_count 0然后通过int rows mapper.updateXXX(...)的返回值判断是否更新成功。如果rows 0说明没库存了直接抛业务异常提示该批次疫苗已约满。这个设计思路叫乐观锁思想下的原子扣减避免了先查后改带来的并发超卖问题。很多人写扣库存先SELECT一次判断库存够不够再UPDATE在高并发下两个请求同时查到库存还剩1两个都通过判断结果两个人都约成功库存变-1。这个坑在真实项目里是致命的。源码直接放弃了Java层的库存判断把校验交给数据库的行锁和条件更新简单粗暴且绝对可靠。事务边界也很清楚创建预约单 扣减批次库存这两个操作放在同一个Transactional方法里要么一起成功要么一起回滚。如果扣库存成功但插入预约单失败事务回滚后库存也恢复不会出现库存没了但预约单不存在的脏数据。3.4 核销码的设计思路每个预约单生成时都会有唯一核销码现场接种时验证。源码里核销码用的是纯数字的6位码生成策略是用户ID 预约记录ID 随机因子做哈希后取6位。这里注意核销码不能只依赖随机数因为随机数有碰撞概率万一两个预约单生成同一个核销码现场核销就出大问题。源码里在appointment表给verify_code字段建了唯一索引插入时如果冲突就重新生成双保险避免碰撞。4. 后端核心功能实现与代码拆解4.1 基于JWT的登录认证与权限控制这套系统的登录认证用的是JWTJSON Web Token方案流程是用户提交用户名密码后端用BCryptPasswordEncoder.matches()校验密码。校验通过后用JwtUtils生成tokentoken的payload里放userId、username、role三个关键信息。设置token有效期比如2小时并把token返回给前端。前端后续请求在请求头带Authorization: Bearer token。后端通过拦截器HandlerInterceptor统一解析token校验合法性并将用户信息放入ThreadLocal或RequestContextHolder供后续业务使用。// 拦截器里的核心校验逻辑 String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); Claims claims JwtUtils.parseToken(token); // 校验通过后设置用户上下文 UserContext.set(claims.get(userId), claims.get(role)); } else { throw new BusinessException(未登录或登录已过期); }源码里针对不同角色做了接口级权限控制。方式很朴素但好用拦截器里维护一个接口白名单再配合自定义注解RequireRole标注在Controller方法上拦截器解析token里的角色和注解要求的角色比对不一致直接拒绝。这个实现思路比引入Spring Security全家桶轻量得多够用且好懂。我特别建议把JWT的密钥和有效期配置到application.yml里不要硬编码在Java类里。源码里就是这么做的换环境只需要改配置文件不用重新编译。4.2 预约流程的后端接口实现全解预约接口是这套系统的核心核心逻辑代码如下逻辑经过简化梳理Override Transactional(rollbackFor Exception.class) public void createAppointment(AppointmentCreateDTO dto) { // 1. 校验用户预约资格同一批次每人只能预约一次 int count appointmentMapper.countByUserAndBatch(dto.getUserId(), dto.getBatchId()); if (count 0) { throw new BusinessException(您已预约过该批次疫苗请勿重复预约); } // 2. 校验接种点当天是否已达预约上限 int dailyCount appointmentMapper.countBySiteAndDate(dto.getSiteId(), dto.getAppointmentDate()); if (dailyCount siteMapper.getById(dto.getSiteId()).getDailyLimit()) { throw new BusinessException(该接种点当天预约名额已满); } // 3. 原子扣减批次库存 VaccineBatch batch batchMapper.getByIdForUpdate(dto.getBatchId()); if (batch.getRemainingCount() 0) { throw new BusinessException(该批次疫苗已约满); } int rows batchMapper.decrStock(dto.getBatchId()); if (rows 0) { throw new BusinessException(该批次疫苗已约满请选择其他批次); } // 4. 校验批次状态必须是已发布 if (!2.equals(batch.getStatus())) { throw new BusinessException(该批次疫苗未发布或已下线); } // 5. 生成核销码并插入预约单 String verifyCode generateVerifyCode(dto.getUserId(), dto.getBatchId()); Appointment appointment new Appointment(); // ... 组装字段 appointmentMapper.insert(appointment); }这里有三个细节必须讲透第一个细节校验顺序。先校验用户资格和接种点日限最后才扣库存。为什么因为用户资格和日限的查询成本低先把低成本校验前置能拦截掉大量无效请求把真正需要走库存扣减的请求压缩到最小。这相当于在数据库行锁之前设置了几道闸口高并发场景下能显著降低锁竞争。第二个细节getByIdForUpdate和乐观扣减能不能混用源码里getByIdForUpdate用了悲观锁SELECT ... FOR UPDATE查询批次然后又用条件更新扣减。这个设计在低并发下其实有点冗余因为FOR UPDATE已经锁行后面的条件更新必然成功。但在高并发场景下FOR UPDATE会阻塞其他事务导致吞吐量下降。实际生产里我更推荐只保留UPDATE ... WHERE remaining_count 0的原子扣减初始化库存查询放到扣减成功后再做性能更好。这也算是我对这份源码的一个优化建议。第三个细节事务的rollbackFor必须显式声明。Spring事务默认只回滚运行时异常RuntimeException如果业务里抛的是受检异常Exception而不加rollbackFor Exception.class库存扣减成功了但预约单插入失败事务不会回滚库存就悄悄丢了。这种bug排查起来极其痛苦因为数据对不上账。4.3 核销接口的实现医生端核销预约码的逻辑相对简单但对数据一致性要求高核心代码如下Override Transactional(rollbackFor Exception.class) public void verifyAppointment(String verifyCode, Integer doctorId) { // 1. 根据核销码查询预约单 Appointment appointment appointmentMapper.getByVerifyCode(verifyCode); if (appointment null) { throw new BusinessException(核销码不存在); } // 2. 校验预约状态只有已预约状态才能核销 if (!AppointmentStatus.BOOKED.equals(appointment.getStatus())) { throw new BusinessException(该预约单状态不是待接种状态); } // 3. 校验医生归属的接种点和预约单中的接种点一致 DoctorInfo doctor doctorMapper.getByUserId(doctorId); if (!appointment.getSiteId().equals(doctor.getSiteId())) { throw new BusinessException(不能核销其他接种点的预约); } // 4. 更新状态并插入接种记录 appointmentMapper.updateStatus(appointment.getId(), AppointmentStatus.FINISHED); inoculationRecordMapper.insert(...); // 组装接种记录 }核销场景下的状态判断用了乐观的思路先查状态再更新状态。这里其实存在并发风险比如用户在医院现场让两个工作人员同时扫描同一个核销码理论上两次核销都读到已预约状态然后都更新成已完成就会生成两条接种记录。实际项目里要么给核销更新加条件UPDATE ... SET status 已完成 WHERE id ? AND status 已预约用更新行数判断要么直接给核销码加唯一约束配合分布式锁。源码里用了条件更新这个处理是到位的。4.4 MyBatis动态SQL的实战用法这套系统里MyBatis的动态SQL用得很典型。举个例子后台的预约列表查询支持按用户手机号、预约状态、接种点、日期范围做多条件筛选XML里的写法是这样select idselectAppointmentList resultTypecom.xxx.vaccine.vo.AppointmentVO SELECT a.id, a.appointment_date, a.appointment_time, a.status, v.vaccine_name, s.site_name, u.real_name, u.phone FROM appointment a LEFT JOIN vaccine_batch vb ON a.batch_id vb.id LEFT JOIN vaccine v ON vb.vaccine_id v.id LEFT JOIN vaccine_site s ON a.site_id s.id LEFT JOIN user u ON a.user_id u.id where if testphone ! null and phone ! AND u.phone LIKE CONCAT(%, #{phone}, %) /if if teststatus ! null AND a.status #{status} /if if testsiteId ! null AND a.site_id #{siteId} /if if teststartDate ! null AND a.appointment_date #{startDate} /if if testendDate ! null AND a.appointment_date lt; #{endDate} /if /where ORDER BY a.create_time DESC LIMIT #{pageNum}, #{pageSize} /selectwhere标签会自动处理第一个条件前的AND不会出现SQL语法错误这是MyBatis日常开发里必须掌握的核心知识点。另外注意LIKE CONCAT(%, #{phone}, %)的写法用CONCAT拼接而不是直接写%${phone}%后者存在SQL注入风险${}直接拼接字符串是MyBatis的大忌。分页这里没有引入PageHelper插件而是手写了LIMIT #{pageNum}, #{pageSize}。小项目够用数据量大之后再换成PageHelper或MyBatis-Plus的分页插件也不难。这种先跑起来再按需优化的思路在企业项目里反而比一步到位引入高级组件更务实。5. 前端Vue核心交互与页面实现5.1 用户端的疫苗查询与预约流程用户端的核心页面是疫苗列表 - 疫苗详情 - 选择接种点与时间 - 提交预约 - 查看电子凭证这条链路里有几个交互细节做得比较到位疫苗列表页用了卡片式布局而不是纯表格每个卡片展示疫苗名称、生产企业、剩余剂次、库存状态充足/紧张/约满。库存紧张判断是前端根据后端返回的remainingCount字段算的比如小于等于20就显示仅剩少量。这些状态判断放在前端的好处是交互反馈快不用每次刷新都调后端。预约流程页是核心交互页面分三步完成选择接种点从后端接口拉取当前批次疫苗可预约的接种点列表展示地址、电话、当天剩余名额。选择日期和时间段日期组件禁用了今天之前的日期时间段上午/下午/晚上根据接种点当天的预约配额动态控制是否可点击已满则置灰。确认预约前端展示预约信息的摘要疫苗、批次、接种点、时间用户确认后调/api/appointment/create接口成功后跳转预约成功页页面展示核销码和预约单号。这个三步交互把复杂表单拆分成了三个简单步骤每步用户只需要做一个决定转化率比一次性填写完整表单高得多。前端在每次下一步按钮点击时做一次轻量校验是否选中可用日期、是否选中时间段不给后端增加无谓的请求压力。5.2 管理员端的疫苗发布与数据看板管理员端疫苗管理的核心操作是建档 - 批次入库 - 批次发布。前端通过一个Tab页签切换三个子页面疫苗档案、批次列表、发布审核。批次列表页每行显示批号、有效期、入库量、剩余量、状态操作列根据状态动态渲染按钮——待审核状态显示通过和驳回已发布状态显示下线已下线状态显示重新发布。这样状态机驱动按钮显隐的写法可以避免用户执行非法操作比如对已发布的批次重复发布。数据看板是整个前端最有企业级感觉的部分。首页用ECharts做了三个图表折线图最近30天每日接种人次柱状图各接种点接种量对比饼图各疫苗品种预约占比这些图表数据来自后端的一个聚合统计接口后端用一条GROUP BY的SQL把原始数据查出来前端直接绑定渲染。图表配置不复杂但要注意ECharts实例在组件销毁时记得dispose否则频繁切换Tab会导致内存泄漏页面越来越卡。这个坑我早期经常踩后来成了习惯性的写进beforeDestroy生命周期里。5.3 前端接口调用与状态管理的协作模式这套系统的前端状态管理用的是Vuex但只存了三个全局信息token、userInfo、menus。其他数据都是页面组件内部data维护通过接口获取。这个设计非常务实——不是所有数据都要进Vuex全局状态只放多页面共享且变化不频繁的东西。如果你把预约列表、疫苗列表这种页面级数据也塞进Vuex你会发现state越来越臃肿代码越来越难调试。axios封装的响应拦截器里做了统一错误处理service.interceptors.response.use( response { const res response.data; if (res.code 200) { return res.data; } else if (res.code 401) { // token过期清除本地登录态跳转登录页 localStorage.removeItem(token); router.push(/login); return Promise.reject(new Error(未登录或登录已过期)); } else { Message.error(res.message || 请求失败); return Promise.reject(new Error(res.message)); } }, error { Message.error(网络异常请稍后重试); return Promise.reject(error); } );统一在这里处理的好处是每个具体调接口的页面代码非常干净只管成功后的数据渲染错误提示全部由拦截器兜底。如果某个接口有特殊的错误处理需求比如预约失败要跳转重新选时间页面代码里再单独catch即可。这种默认统一、例外单独的处理机制是降低前端代码重复度的关键。6. 本地环境搭建与项目部署实战6.1 环境准备清单跑这套源码之前先把环境准备好。我列一个清单软件版本要求说明JDK1.8推荐用JDK 8兼容性最好Maven3.6后端依赖管理Node.js14.x前端构建环境MySQL5.7建议8.0注意连接驱动版本Redis非必需这套源码暂未集成但预留接口Spring Boot版本注意一下不同版本对JDK要求不同。如果下载的源码里pom.xml写的是Spring Boot 2.7.x那JDK 8和JDK 11都能跑如果已经是Spring Boot 3.x就必须JDK 17了。启动报Unsupported class file major version这类错误十有八九就是这个不匹配导致的。MySQL这边建议提前把字符集设置为utf8mb4不然疫苗名称里如果有特殊字符比如®商标符号可能存不进去。数据库的初始脚本一般在源码的/sql目录下用Navicat或命令行执行source xxx.sql即可完成建库和初始数据导入。6.2 项目启动步骤后端前端后端启动流程# 1. 修改application.yml里的数据库连接信息 # spring.datasource.urljdbc:mysql://localhost:3306/vaccine_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai # spring.datasource.usernameroot # spring.datasource.password你的密码 # 2. 在项目根目录执行Maven打包 mvn clean package -DskipTests # 3. 启动jar包 java -jar target/vaccine-system-1.0.0.jar如果是在IDEA里跑直接右键VaccinationApplication.java选择Run即可。启动日志看到Started VaccinationApplication和Tomcat端口号默认8080就说明后端起来了。第一次启动建议先看日志里的SQL初始化情况确认建表成功再启动前端。前端启动流程# 1. 安装依赖第一次会比较慢建议用淘宝镜像 npm install --registryhttps://registry.npmmirror.com # 2. 开发环境启动 npm run dev如果后端端口不是8080需要改前端vue.config.js里的代理配置。我常用的devServer配置长这样devServer: { port: 9528, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, // 如果后端接口没有/api前缀去掉这段注释让代理自动去除前缀 // pathRewrite: { ^/api: } } } }开发环境下用proxy代理可以完美避开跨域问题。前端请求写/api/xxx浏览器看到的是同源请求代理把请求转发到后端8080端口。这个配置如果你自己起前后端项目80%的跨域问题都是这么解决的。6.3 生产部署的核心配置与超实用经验生产环境部署我推荐用Nginx托管前端静态文件反向代理转发后端接口。Nginx配置的核心部分server { listen 80; server_name your-domain.com; # 前端静态文件 location / { root /opt/vaccine-front/dist; index index.html; try_files $uri $uri/ /index.html; # 刷新页面时路由回退 } # 后端接口代理 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 $uri $uri/ /index.html这行是Vue路由history模式的标配没有它刷新一个/vaccine/list页面会直接404。生产环境下如果要https记得在Nginx里配证书并加上location /的80端口跳转。后端生产启动我建议用nohup配合日志输出nohup java -jar vaccine-system-1.0.0.jar --spring.profiles.activeprod /opt/vaccine-backend.log 21 日志文件会越来越大可以用Logrotate做日志轮转或者在图方便的情况下定期手动清理。新手经常忽略日志管理等出问题发现日志几百MB打不开就真的是历史事故了。7. 常见问题与排查技巧实录7.1 后端启动失败速查表报错信息/现象大概率原因解决方法Access denied for user rootlocalhost数据库账号密码错误检查application.yml里的username/passwordUnknown database vaccine_system数据库没有创建手动执行CREATE DATABASE vaccine_system CHARACTER SET utf8mb4Table xxx doesnt exist初始化脚本未执行检查sql脚本执行情况导入初始数据Port 8080 was already in use端口被占用改server.port或用lsof -i:8080查占用进程并killjava.sql.SQLException: No suitable driver缺少MySQL驱动检查pom.xml里mysql-connector-java依赖是否存在bean初始化报错: Address already in use同上端口占用有时是Redis也用了默认6379但Redis没装导致连接失败一个值得特别注意的点新版MySQL 8.0的驱动类名换了。MySQL 5.x用com.mysql.jdbc.DriverMySQL 8.0用com.mysql.cj.jdbc.Driver。如果你的数据库是8.0却配了老驱动类名启动直接报驱动加载失败。这类配置型问题排查不难但要细心。7.2 前端启动与联调问题前端开发中最常遇到的三个问题第一个npm install网络卡死或报404。原因多半是官方源慢或某个依赖版本被下架。解决办法是配置淘宝镜像源npm config set registry https://registry.npmmirror.com后再重装。如果还有个别包装不上直接删掉node_modules和package-lock.json重新npm install大概率能解决。第二个登录后页面空白F12看到401。这是token丢失或过期导致的。检查后端返回的token是否正确保存以及axios拦截器里的Authorization头名字和后端拦截器读取的名字是否一致。我见过最离谱的一次是后端读的是token前端传的却是Authorization前端还一直没报错。这种静默失败最坑一定要配合F12的Network面板看请求头。第三个接口返回的数据渲染不出来。这个优先看response.data的结构到底长什么样。很多时候前端拿的是res.data.data.vaccineList但后端返回的是res.data.list字段名对不上。建议在组件里先console.log(res)看一次结构再写渲染代码别凭猜。7.3 预约并发问题的真实场景还原我拿测试环境模拟过两个账号同时抢同一个批次疫苗的最后一个名额我简单归纳一下异常场景和应对策略正常场景两个请求都执行原子库存扣减SQL数据库行锁保证只有一个请求更新成功另一个获取的行数为0后端返回约满无脏数据。超时场景库存充足但预约单插入耗时较长事务长时间持有行锁第二个请求等待锁超时报Lock wait timeout exceeded。优化方案是避免在事务里做耗时操作比如发短信通知、调用外部接口这类操作放到事务提交后再执行。重复预约场景用户疯狂点提交预约按钮产生并发请求。前端要加提交中禁用按钮的防抖处理后端有唯一索引兜底。这套源码在两个层面都做了防护这点做得比较严谨。还有一个容易忽略的问题核销码生成逻辑里如果用了UUID然后截断成6位冲突概率会很高。源码的处理是取哈希后6位冲突重试实测在百万条预约量级下偶发冲突但能自动重试处理这个方案兼顾了可用性和碰撞降险。7.4 效率类问题排查系统用久了最典型的问题是列表接口越查越慢。我给你列一条排查路径先看接口耗时如果SELECT * FROM appointment LEFT JOIN ...这种全表扫描数据量上万后就明显变慢。用EXPLAIN看执行计划重点看type字段是不是ALL全表扫描。给外键字段加索引appointment.batch_id、appointment.user_id、appointment.site_id、appointment.status这些出现在WHERE和JOIN ON里的字段都值得建索引。如果订单量到了百万级可以考虑按时间分表或引入ES做搜索这是后话。这套源码的索引建得比较基础appointment表里对user_id和status建了单列索引联查性能在十万级数据量内够用。如果你要基于这个项目做大并发改造优先考虑引入Redis做疫苗库存的预扣减、预约列表查询的缓存数据库只做最终一致性落库。8. 写在最后的实操心得与扩展建议关于这套源码我最后分享几个实操中的真实感受第一状态机设计是这套系统最值得学习的部分。预约状态从已预约到已核销到已取消的流转每一步都有明确的触发条件核销接口、取消接口、定时任务扫过期单。很多新手做管理系统只关注能查能增能改忽略了状态流转的合法性和边界情况这套源码把状态约束写在了代码和SQL里这种把规则写进实现的习惯值得坚持。第二事务和并发安全是所有预约类系统的生命线。库存扣减的原子操作、状态更新的条件约束、事务回滚边界任何一个环节出错都会直接导致业务事故。你可以在自己项目里做一次高并发抢约的压测亲眼看到超卖是怎么发生的再对比这套源码的处理方式理解的深度完全不一样。第三前后端权限模型的协同是后台管理系统的通用模板。后端有拦截器和角色检查前端有路由守卫和菜单权限两层权限共同织网单靠前端隐藏按钮是不够的单靠后端校验又会牺牲交互体验。这套源码的权限设计不算复杂但对中小型管理系统来说足够清晰。这个项目后续扩展的空间也很大我简单列几个方向引入Redis把疫苗库存预热到Redis预约扣减走Redis的DECR命令异步同步到MySQL扛住高并发。接入消息队列核销成功后通过RabbitMQ/Kafka发送接种通知短信异步解耦避免同步阻塞事务。引入定时任务定时扫描过期未核销的预约单自动更新状态并回补库存。疫苗批次效期预警对临近有效期的批次做颜色预警和自动下线这是卫生领域非常实际的功能。如果你手头正在写类似的预约管理系统或者准备拿这个项目去面试讲项目建议把状态流转、防超卖设计、权限控制这三个点讲透这比背十道八股文都管用。这套源码的价值不在代码量而在于它把真实业务和企业级工程习惯揉在了一起顺着这个思路改造成你自己的项目你会收获比跑通一遍多得多的东西。
返回列表