ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue疫苗预约系统设计与实现:防超卖与状态管理实战

SpringBoot+Vue疫苗预约系统设计与实现:防超卖与状态管理实战 1. 项目概述与核心需求拆解从大约一年前开始陆陆续续有读者私信我疫苗预约类的系统怎么做毕设可不可以选这个说实话这个选题在近两年确实很火原因也简单——它天然自带一套完整业务链路用户端要登录、要查疫苗、要预约、要取消管理端要发布疫苗信息、要维护批次库存、要审批预约记录、要汇总接种数据。这套链路刚好把Web开发里最常见的技术点全部串联起来了用来练手、做毕业设计、写进简历都非常合适。我这次复现的项目标题很明确“基于SpringBootVue的疫苗发布和接种预约系统管理系统设计与实现”配套技术栈是Java、MySQL、MyBatis。好多人看到这个标题第一反应是“又一个CRUD管理系统”但真正动手做的时候会发现CRUD只是最表层的东西。疫苗预约系统要比普通的图书管理系统、学生信息管理系统复杂得多最大的难点在于预约资格校验、批次库存扣减、状态流转控制这三块。尤其是“用户抢到某个批次的预约号同时得保证这个批次没有被约超”这个并发问题一旦处理不好系统上线就会出事故。先说这个系统到底解决什么问题。站在接种者角度以前的流程是想打疫苗不知道哪儿有苗、不知道什么时间能打、不知道有没有库存只能一趟一趟跑社区医院问。站在接种点角度每天要接无数电话查询还得手工登记预约人信息Excel表记到后面根本没法查谁约过谁没约过。这套系统的核心价值就是把线下的人工排队、电话预约、手工登记搬到线上管理员后台发布疫苗批次和可预约时间段用户在微信小程序或Web端实时查看可预约的疫苗种类、剩余名额一键预约并收到结果通知后台自动生成预约记录和接种档案。务虚的话不说了直接拆需求。这类系统按照使用角色分至少要有三类用户系统管理员、接种点工作人员也可以和管理员合并、普通接种者。管理员负责疫苗信息的增删改查、批次库存设置、公告发布、接种数据汇总普通用户负责注册登录、浏览疫苗、提交预约、取消预约、查看自己的接种记录。如果按功能模块分常见拆法是一套前台预约门户一套后台管理面板前台管“看和约”后台管“发和审”中间靠数据库表和接口做数据串联。1.1 功能模块提炼前台与后台的分工前台用户端我实际做下来觉得至少需要这几个页面首页展示最新疫苗公告和热门疫苗列表疫苗列表页支持按疫苗名称、适用年龄段、接种点筛选疫苗详情页展示疫苗介绍、禁忌症说明、剩余名额、可预约时间段预约提交页选择接种人、选择时间段、确认预约个人中心展示我的预约记录、我的接种档案、待办提醒。被拒约的还能看到拒绝原因这点很多家常项目不做但真实业务里非常关键。后台管理端功能划分可以做成工作台今日预约量、待处理预约数、库存预警、疫苗管理疫苗种类维护、批次管理、库存调节、预约管理预约审核、接种状态确认、超时取消、用户管理接种者档案、账号启停、公告管理内容发布与下架、数据统计按周/月维度出接种量报表。上面的“批次管理”一般是新手最容易忽略的——疫苗数据必须带批号、生产日期、有效期不能只存一个疫苗名字否则后台的库存控制和预警逻辑根本没法做。2. 技术选型解析为什么就是这一套组合很多读者问过我为什么这类毕设项目基本都是SpringBootVueMySQLMyBatis能不能换成别的能但从学习成本和成熟度角度这套组合在当下的Web全栈开发里仍然是最稳、资料最多、最容易找到参考实现的方案。SpringBoot负责整个后端服务。它的价值不在于让你写出更牛的Java代码而在于帮你把之前SSH时代那堆繁琐的配置全部干掉。以前搭一个SpringMVC项目要写web.xml、Spring配置文件、MyBatis配置文件、数据源配置、事务配置全部串起来至少大半天。SpringBoot用自动配置把这一切收敛了你只需要在application.yml里写几行数据源参数加一个SpringBootApplication注解一个能跑起来的Tomcat内嵌服务就出来了。对于做管理系统这类以CRUD为底座的业务SpringBoot的开发效率几乎是碾压式的。Vue负责前端页面渲染。选Vue而不是React有一个很实在的理由Vue的上手曲线更平缓模板语法非常直观适合后端思维习惯。你写过Java的人去看Vue的template会觉得很亲切因为它像HTML而JSX的写法对很多Java选手反而不适应。项目里我用的Vue2配合Element UI虽然Vue3已经稳定很多年但考虑到毕设答辩时老师最容易理解的是Vue2的选项式API写法data、methods、computed三板斧清清楚楚代码读起来没有任何心智负担。如果你已经熟练了用Vue3Element Plus也没问题接口和业务逻辑完全可以照搬。MySQL承担数据持久化MyBatis负责数据访问层操作。MyBatis为什么在中小型管理系统里比JPA更受欢迎因为管理系统里的SQL往往带有明显的手工优化痕迹——联表查询、子查询、条件动态拼接、统计聚合MyBatis的XML文件可以让你精确控制每一条SQL排查慢查询也方便把SQL复制到Navicat里一执行问题一眼就能定位。而JPA的自动SQL在复杂查询时会绕很多圈子性能调优的时候你会特别痛苦。2.1 版本选择与初始环境准备我这次实际用的版本组合贴出来给你们做参考JDK 1.8、SpringBoot 2.7.x、MyBatis 1.3.2、MySQL 5.78.0也可以驱动注意换成mysql-connector-java对应的新版本、Vue 2.6.x、Element UI 2.15.x。之所以SpringBoot卡在2.7而不是3.x是因为3.x要求JDK 17起步而且很多老版本的集成组件对3.x的兼容性还没完全跟上。对于学习和对毕业设计而言成熟稳定远比版本新重要。开发工具方面后端用IDEA前端用VSCode数据库客户端用Navicat。初始化流程分四步走第一步创建数据库vaccine_system默认字符集utf8mb4排序规则utf8mb4_general_ci这是为了支持中文和特殊符号存储第二步新建SpringBoot项目pom.xml引入Web、MyBatis、MySQL驱动、Lombok这4个基本依赖第三步配置application.yml的数据源、MyBatis驼峰映射、端口号和上下文路径后面联调会用到第四步前端用vue-cli脚手架创建Vue项目安装axios、element-ui、vue-router、vuex这四个库。跑通了这四步整个项目的骨架就算立起来了。注意MySQL驱动依赖的groupId在5.1.46之后发生了变化如果是SpringBoot 2.1以上版本直接依赖默认的mysql-connector-java即可如果坐标报错换成新坐标com.mysql:mysql-connector-j也是标准做法。3. 数据库设计与核心表结构拆解打完地基就要开始设计承重墙——数据库表。这套系统的所有业务逻辑最终都跑在表关系上表设计得差后面接口写得再漂亮也白搭。我见过太多人先写代码再补表结果用户表里没有手机号疫苗表里没有批号临时加字段加到怀疑人生。数据库设计一定要先做哪怕用纸画个关系图也比直接建表强。这套系统我最终的物理表结构是8张表用户表sys_user、疫苗信息表vaccine_info、疫苗批次表vaccine_batch、预约记录表appointment_record、接种记录表vaccination_record、公告表announcement、公告阅读记录表announcement_read_log、操作日志表operation_log。如果要做更细致的管理可以再加一张管理员角色表但一般情况下用sys_user里的role字段区分角色就够了角色字段我建议用Integer类型1代表管理员0代表普通用户千万别用字符串“admin”存着累赘查询也慢。下面把这8张表里的核心表拿出来逐一拆解讲清楚字段设计意图。3.1 疫苗信息表与批次表一对多关系vaccine_info表设计字段时我踩过一次坑最初只设计了vaccine_name、manufacturer、vaccine_type这三个字段结果发布会后读者问我“适用年龄呢”“剂次呢”“间隔天数呢”都是很实际的业务字段。最终的字段清单包含id、vaccine_name、manufacturer生产厂家、vaccine_type类型比如灭活疫苗/重组蛋白疫苗、applicable_age适用年龄段这里用字符串存比如“3岁以上”、dosage_count接种剂次常见为1或2、interval_days两剂间隔天数、contraindication禁忌症描述、description疫苗详细介绍、cover_image封面图片URL、status上下架状态1上架0下架、create_time、update_time。vaccine_batch表是这套系统的灵魂它解决的是“同一个疫苗多个批次库存怎么管”的问题。字段包括id、vaccine_id关联疫苗信息表、batch_no批号这个必须有疫苗行业靠批号追溯、produce_date生产日期、expire_date有效期至、total_stock批次总库存、remain_stock剩余库存、create_time。为什么要单独拆一张批次表因为同一款疫苗可能是不同日期生产的有效期不一样如果混在一起只存一个总库存用户预约到的批次可能已经过期这属于严重业务事故。实操心得remain_stock这个字段不能直接用total_stock减预约数然后实时算出来因为预约里有取消、有拒绝、有超时释放状态链路复杂查询时临时计算很容易出错。正确做法是在批次表里冗余一个remain_stock字段预约成功就减一取消预约或超时释放就加一查询直接读这个字段性能好且逻辑清晰。3.2 预约记录表状态机是核心appointment_record表承载的是整个系统的业务主流程。字段设计如下id、user_id预约人、vaccine_id预约的疫苗、batch_id预约的疫苗批次、appointment_date预约接种日期、time_slot时间段比如“上午9:00-11:00”、status预约状态、reject_reason拒绝原因、create_time、update_time。这里的status字段是整个业务逻辑的关键我使用的取值有6个状态值含义触发场景0待审核用户提交预约后管理员未处理1已确认管理员审核通过等待用户到点接种2已接种用户到接种点完成接种3已取消用户主动取消4已拒绝管理员审核不通过多用于资料不符5已过期预约日期已过且未接种系统自动置为过期预约记录表加一个batch_id字段是我在第二次重构时补上的一开始只存vaccine_id结果做数据统计时根本算不出某个批次的接种率批次库存与预约记录对不上账。加上batch_id之后后台统计每个批次的预约人数、完成人数就变成一条简单SQL的事。另外建议再加一个appointment_no的编号字段格式类似“YYMMDD四位随机码”方便人工查询时快速定位记录。vaccination_record表则是接种完成后的信息固化包括接种人姓名、身份证号、联系电话、疫苗名称、批号、接种日期、接种点默认系统管理员所属接种机构、接种医生签名、备注信息。这张表的设计核心是“档案感”做毕设时如果能把接种档案展示页面做得像模像样答辩时能加分不少。4. 后端核心模块实现预约业务中的并发与状态控制后端代码的组织方式是经典的Controller-Service-Mapper三层。Controller层只做参数接收和数据封装Service层承载业务逻辑Mapper层放SQL操作。下面挑几个最有技术含量的点展开说这块的理解程度直接决定系统的代码质量。4.1 预约提交的防超卖设计先演示一个反面写法这是我在开发早期犯过的错误Transactional public void makeAppointment(AppointmentRequestDTO dto) { // 查询批次剩余库存 VaccineBatch batch vaccineBatchMapper.selectById(dto.getBatchId()); if (batch.getRemainStock() 0) { // 插入预约记录 appointmentMapper.insert(dto); // 扣减库存 vaccineBatchMapper.decreaseStock(dto.getBatchId(), 1); } }这段代码在单用户测试时完全没问题但只要用JMeter并发跑50个请求最后生成的预约记录数量一定大于批次剩余库存预约超卖。原因很简单查询剩余库存和扣减库存这两个操作不是一个原子操作在高并发下多个请求同时读到remain_stock1同时通过if判断然后全部执行insert和扣减库存就变成负数了。解决这个问题的核心思路有两套。第一套是对扣减库存的SQL做条件更新也就是把数量判断下沉到数据库层update iddecreaseStock UPDATE vaccine_batch SET remain_stock remain_stock - 1 WHERE id #{batchId} AND remain_stock 0 /update然后在Java代码里检查受影响的行数等于1说明扣减成功等于0说明库存不足直接抛业务异常终止流程。因为UPDATE语句在MySQL的默认隔离级别下是行级锁同一时刻只有一条UPDATE能成功所以不会超卖。这套方案胜在简单可靠不需要引入Redis和分布式锁非常适合这个量级的系统。第二套方案是在批次表上增加一个version字段做乐观锁UPDATE时带上version条件每次更新version加一。这套方案在冲突频繁的场景下会大量重试配合重试机制代码复杂度明显上升。我的建议是管理系统这个并发量方案一完全够用别给自己找麻烦。重要提示防超卖逻辑必须和insert预约记录放在同一个事务里。如果扣减库存成功了但后面insert预约记录失败事务会回滚库存也会回滚到扣减之前的状态。没有事务保护的话会出现“库存扣了但预约记录没了”的脏数据。4.2 状态流转的服务端校验用户发起取消预约操作时Service层的校验是当前状态必须等于1已确认或0待审核已接种、已取消、已拒绝的状态不允许取消。这个判断不能漏因为前端只要把按钮隐藏掉就能挡住一般用户但懂技术的用户用Postman直接调接口就能绕过前端操作。这种对状态的校验一定是在服务端做的前置条件校验也是同样的道理——用户提交预约时后端必须校验批次是否上架、用户是否已经预约过同批次、所选的预约日期是否在批次有效期之内。只有前置校验全通过才进入防超卖的扣减逻辑。4.3 定时任务做预约状态补偿预约系统有一个必须处理的场景用户约了今天上午的号结果没来这个预约状态就永远卡在“已确认”。所以需要一个定时任务每过一段时间把所有appointment_date小于当前日期且status1的记录批量更新为status5已过期同时让对应的批次库存加一。这在SpringBoot里用Scheduled注解就能实现Component public class AppointmentExpireTask { Resource private AppointmentMapper appointmentMapper; Resource private VaccineBatchMapper vaccineBatchMapper; Scheduled(cron 0 0 2 * * ?) public void expireAppointments() { ListAppointmentRecord expiredList appointmentMapper .selectExpiredRecords(new Date(), 1); for (AppointmentRecord record : expiredList) { appointmentMapper.updateStatus(record.getId(), 5); vaccineBatchMapper.increaseStock(record.getBatchId(), 1); } } }这里有个细节cron表达式我写的是凌晨两点执行因为那是访问量最低的时间段。另外必须分两条SQL去操作先查过期记录再逐条更新不能用一条UPDATE直接改状态因为你还得让库存加回来两件事必须绑定在一起做。如果一个预约记录过期了但库存不释放那些库存就会永久锁死用户看到的永远是“名额已满”。4.4 MyBatis XML中动态SQL的实用片段管理后台的预约管理列表必然需要一套组合筛选条件按状态筛、按日期范围筛、按疫苗名称模糊筛。如果这些条件都写在Java代码里拼接SQL字符串既不安全又难维护。MyBatis的 标签在这里是神器。我的mapper里这样写的select idselectAppointmentList parameterTypemap resultMapAppointmentRecordResultMap SELECT ar.*, vi.vaccine_name, ub.real_name FROM appointment_record ar LEFT JOIN vaccine_info vi ON ar.vaccine_id vi.id LEFT JOIN sys_user ub ON ar.user_id ub.id where if teststatus ! null AND ar.status #{status} /if if testvaccineName ! null and vaccineName ! AND vi.vaccine_name LIKE CONCAT(%, #{vaccineName}, %) /if if teststartDate ! null AND ar.appointment_date gt; #{startDate} /if if testendDate ! null AND ar.appointment_date lt; #{endDate} /if /where ORDER BY ar.create_time DESC /select注意 标签的用法它会自动处理第一个条件前面的AND关键字这样即使status参数为空、只有vaccineName有值生成的SQL也不会因为多一个AND导致语法错误。这个细节对新手来说特别容易踩坑我线下教学时几乎每次都有人在这里报错。提示MyBatis的resultMap需要把id、batch_id这类下划线字段与javaBean的驼峰属性映射对应如果你不想写resultMap可以在application.yml里设置map-underscore-to-camel-case: trueMyBatis会自动将SQL列名下划线转驼峰代码能省一大截。5. 前端Vue实现从登录到预约的全链路交互前端这块我的整体设计思路是用Vue Router管理路由用Vuex管理登录状态和用户信息用axios封装统一请求入口页面UI全部使用Element UI组件库。接下来分模块讲具体实现。5.1 项目结构与路由拦截Vue项目的src目录我习惯这样组织src ├── api # 所有接口请求定义 │ ├── user.js # 用户模块接口 │ ├── vaccine.js # 疫苗模块接口 │ └── appointment.js # 预约模块接口 ├── assets # 静态资源 ├── components # 公共组件 │ ├── HeaderNav.vue # 顶部导航栏 │ └── PageFooter.vue # 页面底部 ├── router │ └── index.js # 路由配置 ├── store │ └── index.js # Vuex状态管理 ├── utils │ └── request.js # axios实例封装 ├── views │ ├── admin # 后台管理页面 │ ├── user # 用户端页面 │ └── Login.vue # 登录页面 └── App.vueapi目录和views目录对齐是这套结构的核心思想——一个模块的接口定义和页面视图放在同一层找起来非常顺手不会出现十几个页面堆在views里、接口全怼在一个文件里的混乱状态。路由拦截是前端鉴权的核心。在router/index.js里注册一个全局前置守卫每次跳转路由前检查Vuex里的token没有token一律踢回登录页。同时用meta.requiresAdmin标记管理员路由普通用户即使登录了也不能访问管理后台。router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }); } else if (to.meta.requiresAdmin store.state.user.role ! 1) { next(/); } else { next(); } });5.2 axios请求封装与登录态保持axios封装的目的是统一处理BASE_URL、请求头、响应拦截、统一错误提示。我在utils/request.js里的典型写法是这样的import axios from axios; import { Message } from element-ui; import router from ../router; const service axios.create({ baseURL: process.env.VUE_APP_BASE_API || http://localhost:8080/api, timeout: 10000 }); service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer token; } return config; }); service.interceptors.response.use( response { const res response.data; if (res.code 200) { return res; } 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(error.message || 网络异常); return Promise.reject(error); } ); export default service;这里有一个容易迷惑的地方如果后端接口统一返回JSON格式{code: 200, data: ..., message: ...}那么前端在response拦截器里已经把这个外壳拆掉了业务代码里拿到的直接就是data部分。团队开发时这个统一封装的价值特别大不会有十个人写十种请求习惯。5.3 前端防重复提交与场景化体验优化预约提交页面里我做了一个很实用的细节点击“提交预约”按钮后先把按钮置灰再调用接口只有请求返回且处理完成才恢复可点击状态。为什么因为后端已经做了防超卖处理用户A连续点击两次时虽然在秒级之间请求是串行执行的但第二次请求会触发“该时段已无余量”的提示体验上就会非常糟糕。既然后端已经把守了最后防线前端也应该在交互上防止用户半路开小差。另外用户点击疫苗详情页里“立即预约”按钮时如果这个批次已经剩余0个名额按钮直接就不可点了这一点我建议前端要做后端接口返回的remainStock字段就是要在这个时候用上的。管理者视角上后台预约管理页面建议用标签页把待审核、已确认、已接种分成三个tab这样比全部堆在一个table里再用状态下拉去筛体验会好很多——操作人员处理后不需要回头找刚才那条记录在哪。6. 管理员后台与公告发布模块的实现要点后台管理系统是疫苗预约项目的另一个重头戏这个部分用来展示管理员对整个系统的掌控能力。我按实际业务动作把后台拆成几块来讲。6.1 疫苗发布与启停“疫苗发布”指的就是疫苗信息的上架操作。后台疫苗管理页面点“新增疫苗”弹出表单字段有疫苗名称、生产厂家、疫苗类型、适用年龄、接种剂次、间隔天数、禁忌症、详情描述、封面图片URL。前端表单提交到后端接口 /vaccine/saveService层判断是新增还是更新如果疫苗已经在运行中的预约流程中下架status置0可以但不能物理删除否则历史预约记录的外键关联就断了。批次入库与修改库存是这里经常被问到的一个批次的总库存和剩余库存后台管理员能不能直接改我的建议是初始设置时可以改一旦有预约记录关联到这个批次系统就不允许再调整total_stock库存调整只能通过“增加库存”这个动作来实现即直接对remain_stock做增量更新。这样能保证数据可追溯、操作清晰同时也避免后期库存对不上账时无从排查。批次库存低到一定阈值时系统在工作台上给出醒目的“预警”标识代码逻辑很直白遍历批次列表remain_stock/total_stock小于0.2或者绝对数量小于10即为预警状态。6.2 预约审核与接种状态确认预约审核这个功能在我的设计里是管理员的核心操作入口。待审核tab里展示所有状态为0的预约记录操作按钮有“通过”和“拒绝”两个。点“通过”时前端弹出确认框确认后调后端接口状态更新为1点“拒绝”时弹出对话框要求填写拒绝原因这个原因不能留空拒绝后用户端个人中心会展示管理员填写的拒绝原因。用户被拒绝的那一刻批次剩余库存要加一这步非常重要很多新手做审核时忘记加库存导致“越审库存越少实际约的人却不多”的假象用户反馈说约不到号管理员一脸懵。代码实现时审核拒绝和库存回补必须写在同一个事务里。“确认接种”按钮是当用户到线下接种点后管理员把状态为1的预约记录更新为2同时插入一条接种记录。我做的接种记录保存里需要录入的是实际接种人姓名、身份证号、接种日期、医生签名。这三条信息是医护人员线下核对后填写的系统会在提交成功后通过预约记录里的batch_id自动关联出疫苗名称和批号。这样一套操作下来线上预约的闭环就完整了。6.3 数据统计与图表展示数据统计模块是后台设计的一个加分项。如果只用Table展示所有预约记录老师看到这个页面会觉得只是做了一个“记录表”没有“管理系统”的Feel。我建议加上三个统计卡片和一个柱状图今日预约量不含取消和拒绝、待处理预约数量、累计接种人数柱状图展示近14天的预约趋势。图表用ECharts前端安装echarts依赖把后端统计接口返回的数据数组渲染进去即可代码量不大但视觉冲击力强很多。const chartDom this.$refs.chartContainer; const myChart echarts.init(chartDom); myChart.setOption({ title: { text: 近14天预约趋势, left: center }, tooltip: { trigger: axis }, xAxis: { type: category, data: this.dateList }, yAxis: { type: value }, series: [{ name: 预约量, type: bar, data: this.countList, barWidth: 20, itemStyle: { color: #409EFF } }] });后端对应提供一个查询接口接收查询天数N返回一个包含日期字符串和预约数量字典结构的List。SQL可以用GROUP BY appointment_date来实现需要注意日期格式在MySQL中用DATE_FORMAT函数处理成‘yyyy-MM-dd’字符串省得前端再转换。7. 系统部署与完整运行指南附踩坑实录这一节是给要亲自把整个项目跑起来的读者准备的从环境准备到部署走一遍完整流程。我会把过程中最容易出的、且网上很难搜到标准答案的几个问题一并说透。7.1 本地联调的全套流程第一步启动MySQL服务新建数据库vaccine_system用Navicat运行项目里的sql脚本建表并插入初始数据。初始数据至少要有一个管理员账号角色1、一个测试用户角色0、3种疫苗信息、若干批次库存。没有初始数据登录成功后面临的是一片空白你根本没法验证业务逻辑的完整性。第二步启动后端项目。IDEA里直接运行Application主类看到控制台打印Tomcat started on port(s): 8080说明后端挂起成功。再用POSTMAN测一下登录接口确认能返回token排除数据库连接和账号配置的问题。第三步烦请先把后端配置里面的数据库密码改成你本地实际的MySQL密码这一步漏掉的话启动直接报Access denied for user会堵在第一个路口。第四步启动前端。命令行进入vue目录执行npm install安装依赖。这一步在国内网络上非常容易出现node-sass编译失败、chromedriver下载卡死的问题。我的建议是先执行命令npm config set registry https://registry.npmmirror.com把npm源切到国内镜像源再执行npm install成功率能提升一个档次。安装完成后npm run serve启动开发服务器默认端口8081。第五步联调。前端axios实例里的baseURL必须指向http://localhost:8080/api如果前端改动后不生效先确认是不是没重启开发服务器或者看看浏览器控制台的网络请求里显示的实际请求URL到底是什么。需要特别提示后端接口统一加前缀/api的话可以配置SpringBoot的context-path为“/api”那么Controller里的路径就不用每个都写/api了前后端开发时都以这个前缀为基准沟通成本最低。7.2 前后端分离后跨域问题的标准解法前后端端口不同8080后端、8081前端必然出现跨域问题。跨域问题不解决前端发出的请求在浏览器控制台会报CORS error而且状态码显示为200但前端拿不到数据。我先说结论最简便的解法是在后端加一个CORS配置类。Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.setAllowCredentials(true); config.addAllowedMethod(*); config.addAllowedHeader(*); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }这一套配置做完所有接口都允许跨域访问。本地开发时可以用这种方法因为不涉及线上安全问题但如果部署到生产环境建议把allowedOriginPattern改成实际的前端域名避免任何人从任意网站上直接请求你的后端接口。这是我亲身经历的一次线下指导场景里反复出现的坑顺手写下来提醒大家。7.3 从开发环境到上线部署本地跑通之后很多读者想知道怎么把项目部署到服务器上。后端部署最简单的方式打包成可执行jar包。在IDEA右侧Maven工具栏运行package命令在target目录下会生成vaccine-server.jar然后用命令行启动java -jar vaccine-server.jar --spring.profiles.activeprod vaccine.log 21 为了让生产环境和开发环境配置分离项目里要维护两套配置文件application-dev.yml本地数据库地址、日志级别DEBUG和application-prod.yml服务器数据库地址、日志级别INFO。通过启动参数切换环境这是SpringBoot多环境配置的标准姿势。前端部署则需要先执行npm run build生成dist静态目录。把dist目录里的文件扔到Nginx的html目录下然后配置Nginx反向代理把/api开头的请求转发到后端8080端口server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }那个try_files配置别删这是前端路由用history模式时必须加的一条否则页面刷新时会直接报404。Vue Router默认用的hash模式URL带#号不会遇到这个问题但看着不够正式所以我建议后台上线时用history模式配Nginx这套。8. 常见问题与排查技巧实录最后整理一份我在这类项目开发和指导过程中遇到过的高频问题按排查难度排序每个问题都配上排查思路帮你少走几个小时的弯路。问题一启动报错“Invalid bound statement (not found)”这个错误几乎每个用MyBatis的新手都会遇到。原因一般是Mapper接口和XML文件没有绑定上。排查路径第一检查Mapper接口和XML文件是否在同一个包路径下多数项目约定Mapper接口在mapper包XML也在mapper包或resources/mapper目录下第二检查application.yml里的mapper-locations配置是否写对了通配符比如classpath:mapper/.xml第三检查XML中的namespace是否与接口全限定名一致。最常见的坑是XML文件放到了resources根目录而mapper-locations默认路径是classpath:mapper/**/*.xml自然找不到。问题二前端请求能到达后端但返回的JSON在控制台打印是undefined这通常不是前后端故障而是数据结构的key对不上。很多后端同学用Map结构返回数据时前端习惯于用点语法访问但Java的HashMap默认序列化为JSON对象时如果字段命名是驼峰比如appointmentDate前端却写成了appointment_date就会拿不到。解决方法统一后端返回实体DTO时字段命名统一为驼峰前端封装数据模型时严格对齐必要时后端用JsonFormat、JsonProperty注解显式指定序列化规则。问题三用户同时提交两个预约发现两个都成功了这个问题的根源一定在防超卖设计上大概率是库存判断和扣减没有做原子性操作。回到第4.1节看实现重点是确认那条UPDATE SQL里带了AND remain_stock 0并且判断了受影响行数。另外注意Service类上是否加了Transactional保证扣库存和插入记录在同一事务。顺序上先扣库存再插记录逻辑能对先插记录再扣库存一旦扣库存失败事务回滚会把插入的记录也回滚掉这种写法是否正确取决于你接口设计的异常处理策略。问题四预约状态已经变了但前端页面展示没有刷新前端拿到响应后可以重新拉取详情接口不要依赖本地缓存的数据。用Vuex管理全局状态的话数据更新后要commit对应的mutation否则响应式会失效。如果发现Vue组件的data字段改了但页面没更新检查是不是直接用下标修改了数组元素Vue2的响应式系统对数组下标的直接操作是监听不到的要改成this.appointmentList.splice(index, 1, newItem)这是Vue2的一个经典坑。问题五打包后前端访问静态资源全部404常见原因是Nginx没有正确配置location /和try_files或者前端构建时没有指定publicPath。如果部署在子路径下需要在vue.config.js里配置publicPath为相对路径或具体子路径构建后的资源引用才能正确加载。最后再分享一个小技巧。整个项目完成后记得做一次全流程冒烟测试用普通用户注册一个账号发布一条疫苗预约一个批次管理员审核通过用户取消一个预约管理员确认接种最后打开数据统计页面看数字变化。这一套流程走下来系统80%的核心业务逻辑都验证过了答辩、演示、写进简历都能拿得出手。有读者问我“为什么我按教程写了代码但老师看不出工作量”我的答案是功能链路闭合不等于不复杂只要业务闭环完整、代码组织清晰、核心难点有自己的思考项目的说服力自然就出来了。
返回列表