最近在做一个医院挂号就诊系统的练习项目,技术栈选了SpringBoot+Vue+MyBatis+MySQL这一套前后端分离的组合。做完整个过程,从业务梳理、表结构设计到前后端联调、打包部署,踩了不少坑,也总结了一些心得。这篇文就围绕这个项目完整走一遍,把核心的设计思路、代码实现、部署步骤和一个一个踩过的坑都写清楚。如果你正准备做类似的JavaWeb实战项目,或者刚接触前后端分离开发,这篇内容可以直接拿来参考,很多细节我都是试过错之后才整理出来的。
1. 项目概览与业务拆解
1.1 挂号就诊系统的核心业务逻辑
挂号就诊系统,表面上看就是“挂号”两个字,但真正落地成代码,业务链路比想象中长不少。我先把这个系统的角色和核心流程拆清楚,因为后续的所有表结构、接口设计、前端页面都是围绕这套业务逻辑展开的。
这个项目里我确定了三种核心角色:患者(普通用户)、医生、管理员。患者端完成的主要动作是:注册登录、浏览科室和医生排班、选择科室和医生完成挂号、查看自己的挂号记录、就诊时查看医生开具的诊断和处方。医生端负责的是:查看自己被挂号的患者列表、给患者填写诊断结果、开具处方(药品和用量)、处理患者的复诊需求。管理员则负责基础数据的维护:科室管理、医生信息管理、排班管理、号源池管理。
核心业务流程可以概括为这么一条链路:患者选择科室 → 查看该科室下的医生排班信息 → 选择某个时间段进行挂号 → 系统锁定号源并生成挂号记录 → 医生在后台看到挂自己号的患者列表 → 患者到诊后医生填写诊断和处方 → 患者查看最终就诊结果。这条链路中,挂号环节是最容易出并发问题的位置,我后面会专门讲这里的设计。
1.2 技术选型为什么是这套组合
技术栈选择上,我做的是前后端分离架构,后端用 SpringBoot 2.7.x,持久层用 MyBatis,前端用 Vue 2 + Element UI,数据库用 MySQL 5.7,构建工具分别用 Maven 和 npm。这套组合在当前JavaWeb实战项目里算是比较“标准”的一套,适合学习也适合直接落地。
选 SpringBoot 的原因很简单:它解决了传统SSM项目里大量繁琐的 XML 配置,内嵌Tomcat让部署也变得轻量,一个 jar 包就能跑起来。MyBatis 相比 JPA 来说,SQL 是手写的,虽然多了一些工作量,但对于这种有明显查询场景的业务系统(比如按科室查医生、按日期查排班、按状态查挂号记录),手写 SQL 反而更容易控制性能和逻辑。Vue 负责前端页面的渲染和交互,Element UI 提供了一套现成的组件库,表格、表单、弹窗这些直接拿来用,开发效率高很多。
MySQL 5.7 的选择没有什么特别之处,就是稳定、资料多、坑少。这套组合意味着什么?意味着你在网上随便搜一个问题,都能找到答案,这也是学习型项目选技术栈时一个很重要的考量——不要选太冷门的东西,否则出了问题都没地方问。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 后端工程结构与核心实现
2.1 SpringBoot 项目分包与分层设计
拿到项目之后第一步我把后端工程分成了 controller、service、mapper、entity、config、common 这几个包,标准的三层架构。我实际项目的分包结构大致是这样的:
code复制com.hospital.registration
├── controller # 接口层,接收前端请求
├── service # 业务逻辑层
│ └── impl # 业务实现
├── mapper # MyBatis 数据访问层
├── entity # 数据库实体类
├── dto # 数据传输对象,用于接口出入参
├── config # 配置类,比如跨域、拦截器
├── common # 公共类,Result响应封装、异常处理、工具类
└── utils # 通用工具
分层的好处是,controller 只做参数接收和结果返回,不写业务逻辑;service 专注业务流程的编排;mapper 只负责 SQL 的执行。这样分工清晰,出了问题定位也快。我见过不少项目把所有代码堆在 controller 里面,一个类写两三千行,后期根本维护不动。这个项目规模不大,但是分层的习惯一定要从一开始就养成。
回到请求链路上来:前端发起一个 HTTP 请求 → SpringBoot 的 Controller 接收参数 → 调用 Service 层处理业务 → Service 调用 Mapper 访问数据库 → 结果逐层返回。这个链路是前后端分离模式下最基础的调用模型。
2.2 统一响应格式与异常处理
前后端分离开发中,接口返回格式的统一是一件必须提前定好的事情。如果每个接口返回的数据结构都不一样,前端对接时就要为每个接口写单独的处理逻辑,那画面太美不敢看。我这里封装了一个统一的 Result 响应体。
这个响应体包含三个核心字段:code(状态码)、message(提示信息)、data(业务数据)。code 为 200 表示成功,其他值表示各种异常情况。这样前端只需要在 Axios 的响应拦截器里统一判断 code 是否为 200,就可以决定是正常渲染还是弹出错误提示,省去了一大堆重复代码。
java复制public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("操作成功");
result.setData(data);
return result;
}
public static <T> Result<T> error(String message) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMessage(message);
return result;
}
}
结合全局异常处理器,业务代码里只需要专注正常的业务逻辑,异常统一抛出,由全局异常处理器来兜底。比如挂号时发现号源已满,直接抛一个 BusinessException,前端就能收到对应的错误提示,不用在每一层都去 try-catch。我实际用下来,这个模式对开发效率的提升非常明显,前端也特别省心。
2.3 数据库表结构设计要点
表结构设计是这类系统的核心,设计得好不好直接关系到底层代码的复杂程度。我建了这么几张表:用户表(sys_user)、科室表(department)、医生表(doctor)、排班表(schedule)、挂号单表(registration)、诊断记录表(diagnosis)、药品表(drug)、处方表(prescription)和处方明细表(prescription_item)。
其中,用户表里用一个字段区分角色类型,我是用 role_type(1患者 2医生 3管理员),没有单独建角色关联表,因为系统角色比较简单,没必要引入 RBAC 的复杂度。医生表单独建,通过 user_id 关联到用户表,这样医生也有登录账号,同时可以维护职称、简介、所属科室等专业信息。
排班表是最关键的一张表,它决定了号源是否存在。字段上我设计了 doctor_id(医生)、schedule_date(排班日期)、time_slot(时间段:上午/下午)、total_count(总号量)、booked_count(已挂数量)、status(排班状态)。每次挂号操作的时候,实际上做的是对 booked_count 的更新操作,这也是后面并发控制的关键。挂号单表记录的是每一次具体的挂号行为,包含患者信息、医生信息、排班信息、挂号的创建时间以及当前状态(待就诊/已完成/已取消)。
我在设计字段时特别注意了一点:状态类字段一律用 tinyint 存数字,比如挂号状态 0表示待就诊,1表示已完成,2表示已取消,而不是直接存字符串。这样既节省空间,也方便后端用数字判断逻辑。前端展示的时候再通过字典映射显示对应的中文文本。
2.4 MyBatis 映射与动态 SQL 实践
MyBatis 在这个项目里承担所有数据访问工作。我用的是 XML 映射文件的方式来写 SQL,而不是注解。原因很简单,XML 方案下动态 SQL 的编写能力更强,排班查询这种带多个可选条件的场景用动态 SQL 非常顺手。
举个例子,医生排班列表查询往往要同时支持按科室、按医生姓名、按日期来过滤,如果这些条件都写死,就得写好几个 Mapper 方法。实际工作中我写了一个动态 SQL 让它智能拼接条件,一个方法搞定所有场景。核心逻辑如下:
xml复制<select id="selectScheduleList" resultType="com.hospital.registration.entity.Schedule">
select s.*, d.name as doctor_name, dep.name as department_name
from schedule s
left join doctor d on s.doctor_id = d.id
left join department dep on d.department_id = dep.id
<where>
<if test="departmentId != null and departmentId != ''">
and d.department_id = #{departmentId}
</if>
<if test="doctorName != null and doctorName != ''">
and d.name like concat('%', #{doctorName}, '%')
</if>
<if test="scheduleDate != null and scheduleDate != ''">
and s.schedule_date = #{scheduleDate}
</if>
</where>
order by s.schedule_date desc
</select>
这里 MySQL 已经默认开启了下划线到驼峰的自动映射(map-underscore-to-camel-case),所以数据库字段 schedule_date 可以直接映射到实体类的 scheduleDate 属性,不用在 resultMap 里写一遍字段对照。这个配置非常重要,否则你会发现查出来的对象一堆属性是 null,还找不到原因。很多新手在这里卡住很久。
2.5 挂号核心接口的并发控制
挂号这个动作看起来只是简单地插入一条挂号记录,但实际考虑到并发场景就复杂了。比如某个医生上午的号源总共是30个,已经有28个人挂了,这时候同时有5个患者一起点击挂号,如果代码不做控制,就有可能出现超挂的情况——创建了挂号记录,但号源其实已经超出容量了。
我采用的方案是,挂号操作包在一个事务里,并且使用 MyBatis 的 update 语句配合乐观锁的思路来处理号源扣减。核心步骤是这样的:先更新排班表号源,要求当前排班的已挂数量小于总号量,这个更新 SQL 加上了 booked_count < total_count 这个条件,更新成功后返回的影响行数为 1,说明号源抢占成功;如果影响行数为 0,说明号源已经被抢完了,直接抛出异常。
java复制@Transactional(rollbackFor = Exception.class)
public Registration register(RegisterDTO dto) {
// 1. 扣减号源:只有当前剩余号源大于0时才允许更新
int rows = scheduleMapper.decrementBookedCount(dto.getScheduleId());
if (rows == 0) {
throw new BusinessException("当前号源已被挂完,请选择其他时间段");
}
// 2. 创建挂号单记录
Registration registration = new Registration();
registration.setPatientId(dto.getPatientId());
registration.setScheduleId(dto.getScheduleId());
registration.setDoctorId(dto.getDoctorId());
registration.setStatus(0);
registrationMapper.insert(registration);
return registration;
}
update 语句加条件判断,这个方式在数据量不大、并发量可控的场景下是非常可靠且简单直接的方案。比先 select 再 update 的流程少了一个竞态窗口,也避免了分布式锁的复杂度。这个设计思路在整个项目里属于核心亮点,面试的时候也经常被问到,推荐各位在类似场景里直接用这种写法。
3. 前端 Vue 工程从零到联调
3.1 Vue 工程初始化与环境准备
前端我用 Vue CLI 3.x 脚手架来创建的项目。在真正执行创建命令之前,需要先把 Node.js 环境搞定。Node.js 的版本需要注意一下,Vue CLI 3 和 4 在 Node 10 以上都能跑,但如果你的 Node 版本太新,比如 17 以上,一些老项目的依赖安装可能会因为 OpenSSL 的兼容性报错,常见的报错内容是 digital envelope routines::unsupported。解决办法是降低 Node 大版本,或者在 package.json 的 scripts 里加一句 NODE_OPTIONS=--openssl-legacy-provider。
Node 环境确认无误后,创建项目就是两条命令的事:
bash复制npm install -g @vue/cli
vue create hospital-front
创建时我选择了手动配置,勾选了 Router 和 Vuex。UI 组件库选了 Element UI,通过 npm install element-ui 安装后在 main.js 里全局引入。这套组合是 Vue 2 生态里最经典的一套,上手快,组件全,遇到问题资料也多。
3.2 项目目录与核心模块划分
Vue 工程创建好之后,我按照业务模块对 src 目录做了划分。views 目录下按角色拆分成 patient 视图和 doctor 视图,再放一个 admin 视图给管理员用。components 目录放公共组件,比如科室选择器、排班表格这类可以被多个页面复用的部分。router 目录里做路由配置,同时通过路由守卫做登录权限控制。
路由设计上,我区分了不需要登录就能访问的页面(登录页、注册页)和需要登录才能访问的页面(挂号页、个人中心、医生工作台等)。这个控制是通过 Vue Router 的全局前置守卫来实现的,每次路由跳转前检查 localStorage 里是否存有 token,没有就跳转到登录页。
javascript复制router.beforeEach((to, from, next) => {
const [token](https://taotoken.net?utm_source=hardware) = localStorage.getItem('token');
if (to.path === '/login' || to.path === '/register') {
next();
} else if (!token) {
next('/login');
} else {
next();
}
});
这样的好处是所有需要登录才能看的页面,逻辑都在一处控制,新增页面时只要在路由表里配置好 meta 字段,权限就自动生效了,不需要每个页面里单独判断登录状态。这个方案是我做过的前端项目里比较常用也比较稳的写法,代码量小,维护也简单。
3.3 Axios 封装与跨域联调
前端所有请求我都通过 axios 来发。但是每个页面里单独调 axios 会写大量重复代码,所以我在 src/utils/request.js 里做了一层封装,统一配置 baseURL、请求超时时间、请求拦截器和响应拦截器。
baseURL 我设置成了 /api,配合后端项目的 context-path 一起使用。这个设计是为了解决开发环境下跨域问题的。开发时前端跑在 8080 端口,后端跑在 8081 端口,浏览器直接请求会产生跨域问题。我在 vue.config.js 里配置了 devServer 的代理,把所有以 /api 开头的请求转发到后端的 8081,这样浏览器看到的请求是同源的,跨域问题就消失了。
javascript复制devServer: {
proxy: {
'/api': {
target: 'http://localhost:8081',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
}
这里有个细节要注意,如果前端请求路径是 /api/doctor/list,代理转发到后端时会把 /api 前缀去掉,变成 /doctor/list,所以后端接口的 RequestMapping 里不需要带 /api 前缀。这个约定要是没弄明白,就会出现“前端明明配好了代理,后端也启动了,但请求就是 404”的尴尬情况。
响应拦截器里,我统一处理后端返回的 Result 结构。当 code 不是 200 时,直接弹出 Message 提示并 reject 这个 Promise,业务页面里就不用在每个请求后面都去判断错误分支了,只处理成功的逻辑就行。这个方案实际体验非常好,代码干净,前端代码量减少了很多。
3.4 核心页面的交互设计与数据流
前端几个核心页面里,挂号页面算是最复杂的。它的交互流程是这样的:页面加载后先拉取科室列表,点击某个科室后展示该科室下的医生列表,点击医生后查询该医生未来7天的排班信息,选择具体某一天的某个时间段后确认挂号。
这个页面里我用了一个两级联动的效果,科室列表和医生列表通过 Element UI 的表格展示,点击行的时候动态切换。排班信息用卡片式布局展示,卡片上直接显示日期、时间段和剩余号源。剩余号源为0的卡片需要置灰禁用,这块逻辑要跟后端返回的 bookedCount 和 totalCount 做计算判断,不能只看 isFull 这种字段——因为用户在页面上停留的时间内号源可能已经被其他人挂完了,只有提交时才做最终校验。这也是前后端双重校验的一个典型实践,前端管体验,后端管安全。
就诊记录页面相对简单,就是一个列表加一个详情弹窗。列表展示了挂号时间、科室、医生、状态等信息,详情弹窗里展示本次就诊的完整信息,包括医生的诊断内容和处方明细。这类页面逻辑不复杂,但信息字段多,建议把表格列定义好,字段和数据之间的对应关系写清楚,方便后续调整。
4. 完整部署流程与环境配置
4.1 数据库初始化与数据准备
部署的第一步是准备好数据库。我在项目里附带了初始化 SQL 脚本,里面建好了所有表结构,并且插入了一些测试数据:科室(比如内科、外科、妇科)、医生(每个科室分配2到3人)、药品、以及未来一周的排班数据。
导入时如果用的是命令行方式,需要注意脚本的编码问题。我一开始用 Windows 命令行导入老出现中文乱码,后来改用 source 命令导入且把连接字符集设置成 utf8mb4 后问题就解决了。MySQL 5.7 的默认字符集是 latin1,如果建表时没有显式指定字符集,存中文就全是问号。所以我所有表的建表语句里都显式加了 DEFAULT CHARSET=utf8mb4。
分配测试数据时有个小技巧:排班数据的日期不要写死,而是用当前日期动态生成。我是写了一条存储过程来自动生成未来7天的排班记录,这样不管哪一天导入这个项目,都能保证有排班数据可以演示。否则写死日期的话,过几天你再打开项目,看到的全是没有号源的可悲状态。
4.2 后端打包与配置文件调整
后端工程打包之前,先要确认 application.yml 里的数据库连接配置。我通常会把数据库的地址、账号、密码从代码里抽出来,放到 application-prod.yml 里,用 spring.profiles.active 来切换开发和生产的配置。这样开发环境连本地的库,生产环境连服务器的库,互不影响。
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/hospital_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 你的数据库密码
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.hospital.registration.entity
configuration:
map-underscore-to-camel-case: true
连接地址里有两个参数特别关键,一个是 characterEncoding=utf8,一个是 serverTimezone=Asia/Shanghai。第一个保证中文正确处理,第二个解决 MySQL 驱动 8.x 版本对时区的强校验。刚开始用 MySQL 8.0 的驱动连接数据库时,如果 URL 里没加 serverTimezone,启动直接就报时区错误,这个问题操作时一定会遇到。
配置没问题后,执行 Maven 打包命令就能生成可运行的 jar 包:
bash复制mvn clean package -DskipTests
打出来的 jar 在 target 目录下,名字是 hospital-registration-0.0.1-SNAPSHOT.jar 这样。后面部署时直接把这个 jar 扔到服务器上执行 java -jar 就行,不需要再装 Tomcat 或者其他应用服务器。Spring Boot 的内嵌容器已经把这一步简化了。
4.3 前端构建与部署方式
前端构建执行 npm run build,打包完会在 dist 目录下生成静态文件,里面是 index.html、css 文件夹、js 文件夹和图片等静态资源。这些文件本身不依赖任何后端环境,只需要一个静态服务器或者 Nginx 就能跑起来。
对于前后端分离的项目,推荐部署方式是用 Nginx 托管前端静态文件,同时配置一个反向代理,把 /api 前缀的请求转发到后端服务。Nginx 配置里核心的两个块是这样的:
nginx复制server {
listen 80;
server_name your-domain.com;
location / {
root /usr/share/nginx/html/hospital-front;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:8081/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
这里同样需要注意 pathRewrite 的语义。Nginx 配置里 proxy_pass http://127.0.0.1:8081/ 末尾带一个斜杠,那么转发时会自动去掉 /api 前缀;如果不带斜杠,就会保留完整路径,转到了后端一个不存在的接口地址上。这个细节踩过坑了会比较敏感,建议配置时多核对一次。
4.4 服务器环境需要装什么
服务器上我们最终需要的东西其实很少:一个 JDK(8或11都行)、一个 Nginx(托管前端)、MySQL(跑数据库)。不需要装 Maven 和 Node,因为前端构建和后端打包都可以在本地或者其他构建机上完成,服务器只需要运行制品就行。
实际操作时如果用的 CentOS 7 服务器,JDK 安装用 yum install java-1.8.0-openjdk 就能搞定。MySQL 需要先配置 MySQL 官方仓库再安装,也可以直接用 Docker 跑一个 MySQL 容器,看个人习惯。我自己的偏好是能用 Docker 跑的就用 Docker,隔离性好、卸载干净,不过在生产环境追求性能的话,直接装在宿主机上可能会更直接一些。
后端启动流程也比较简单,确认 MySQL 已经建库导表、配置文件里的连接的账号密码正确之后,一条命令就能跑起来:
bash复制nohup java -jar hospital-registration-0.0.1-SNAPSHOT.jar > app.log 2>&1 &
日志输出到 app.log 里,方便出现问题时排查。启动后可以先用 curl 访问一下后端的健康接口或者某个测试接口,确认服务正常响应后,再去配置 Nginx 的代理,确保后端服务先通、再连前端,这种排查顺序可以避免两个问题混在一起浪费精力。
5. 常见问题与排查技巧实录
5.1 跨域问题的三种表现与对应解法
前后端分离项目里,跨域问题几乎人人都会遇到。我这里结合实际经验把这些情况给大家理一理。如果你在浏览器控制台看到 “Access-Control-Allow-Origin” 相关的报错,说明后端没有正确处理跨域请求。最简单的解法是在 SpringBoot 里写一个配置类,实现 WebMvcConfigurer 接口,重写 addCorsMappings 方法,允许所有的来源、所有的方法和所有请求头。
如果用了网关或者代理,那情况可能有点差异。开发环境下用 vue.config.js 里的 devServer.proxy 就能解决,不需要在后端额外开放跨域。生产环境下用 Nginx 反向代理也同样解决。只有当直接用 IP 访问后端接口时,才必须用后端加 CORS 配置的方式。我项目的完整代码里后端是加了 CORS 配置的,这样不管是前后端分离直接访问还是代理访问,两层都能通。还有个小细节,如果同时配了 Spring Security 或拦截器,需要确保跨域配置的优先级在拦截器之前,否则请求在拦截器阶段就会被拦下来,返回的是 403 而不是正常的 CORS 响应头。
5.2 MyBatis 查询结果为空时的排查顺序
遇到 MyBatis 查询结果为 null 的时候,不要急着怀疑 SQL 写错了。先确认几个点:数据库里到底有没有数据;SQL 在数据库客户端里执行是否正常;实体类属性名和数据库字段名的映射关系是否一致;resultType 是否写对。
最常见的问题是字段映射不一致。数据库里字段叫 create_time,实体类属性叫 createTime,如果没开 camelCase 映射,查出来的时间就是 null。这个问题我前面也提到过,在 application.yml 里配置 map-underscore-to-camel-case: true 就能解决。还有一个易错点是关联查询的时候有同名字段,比如医生表和科室表都有 name 字段,那么 resultType 映射时后一个 name 会覆盖前一个 name,导致查出来的医生姓名是科室名称。解决办法是在 SQL 里给字段起别名,保证别名与实体类属性一一对应。
5.3 后端启动失败的高频原因
后端启动失败,大部分集中在数据源配置这一块。报错信息如果是 “Access denied for user”,说明数据库账号或密码不对,去检查数据库的连接配置即可。报错如果是 “Communications link failure”,一般是数据库端口写错、数据库服务没启动,或者防火墙拦截了连接请求。如果报错里包含 “Unknown database”,那就是你连接配置里写的库名和实际建的库名不一致,去服务器上 show databases 看一下真实存在的库名就行。
还有一个比较容易忽略的原因,就是 pom.xml 里 MyBatis 相关依赖的版本和 SpringBoot 版本不兼容。SpringBoot 2.x 对应的是 mybatis-spring-boot-starter 2.x 版本,如果你的 SpringBoot 升级到了 2.7 以上,但 starter 还在用 1.3.x,启动时可能遇到奇怪的报错,比如找不到 SqlSessionFactory。建议用官方 starter 时保持主版本一致,出了问题去查 Maven 依赖树也比排查业务代码迅速得多。
5.4 前端常见报错速查
前端和后端联调阶段,有几类报错非常典型。比如 404 错误,原因可能是路径写错了,也可能是代理配置没生效,还可能后端接口本来就不存在。我建议前端调试时先打开 Network 面板看实际请求发出的 URL,再对比后端 Controller 的 RequestMapping,这样定位速度会快很多,不用靠猜。
再比如 401 错误,那就是身份认证的问题。我项目里前端登录后会把 token 存到 localStorage,每次请求 Axios 拦截器会把 token 塞到请求头 Authorization 里,后端有一个拦截器校验这个 token,校验不过就返回 401。如果是新打开页面后第一次请求就报 401,大多是 token 过期或者没正确存入 localStorage;如果是多个请求同时报 401,大半是后端拦截器放行或者校验逻辑有问题。按这个思路去查基本都会很快找到原因。
5.5 部署到服务器后的几个坑
本地跑起来很顺畅,部署到服务器就各种问题,这个是常见现象。我遇到过最多的有三个。一是防火墙,服务器上 Nginx 端口(80)或者后端端口没放行,外网死活访问不通。排查方法是先在服务器本机 curl 试试能不能通,本机能通、外网不通,十有八九就是防火墙或者安全组规则的问题,逐项检查一下端口和规则就好。
二是服务器内存不足。SpringBoot 应用默认的 JVM 堆内存参数可能比较大,如果服务器只有 1G 或 2G 内存,启动很慢甚至直接报 OutOfMemoryError。解决办法是启动时带上 JVM 参数,限制堆内存的大小,比如 java -Xms256m -Xmx512m -jar xxx.jar,这样小内存机器也能正常运行。
三是服务器时间不对。如果服务器时区设置错了,Java 后端的日期时间相关逻辑会出现偏差,比如排班日期比实际日期少一天。解决方案就是在 Java 启动命令里带上参数 -Duser.timezone=Asia/Shanghai,同时在 MySQL 连接 URL 里设置 serverTimezone=Asia/Shanghai,双端保持一致才能避开这个坑。
6. 源码基础上还能怎么扩展
项目跑通之后,如果你想在现有代码基础上继续完善,我列几个值得尝试的扩展方向供你参考,这几个方向也是实际生产系统中经常会用到的能力。
第一个方向是引入 Redis 做缓存和分布式会话。现在挂号排班信息每次都是直接查数据库,数据量小的时候没问题,但如果排班数据量很大,可以把热点排班数据缓存到 Redis 里,减少 MySQL 的压力。另外 Redis 也可以用来实现简单的分布式锁,替代我上面说的乐观锁方案,在并发量非常大的时候会更稳。
第二个方向是引入 Spring Security 或者 JWT 的完整认证授权方案。目前这个项目登录用的是 Token 拦截器做校验,通过拦截器验证每个请求的 token 来确认身份信息,但还没有实现角色级别的精细权限控制。如果引入 Spring Security 做角色管理,就能让不同角色访问不同接口的权限控制标准化,代码复用性和可维护性都会提升不少。
第三个方向是加一个预约提醒或者就诊状态通知的功能。挂号成功后可以通过邮件或者短信通知用户,就诊前一天发送提醒,这个功能在实际业务中价值很大。具体来说可以在后端加一个定时任务,通过 Spring 的 @Scheduled 注解实现,每天扫描一次第二天的排班数据,给挂了号的患者发送提醒。
第四个方向是做一个管理后台的数据统计页面。比如统计各科室每天的挂号量、医生的工作量、热门科室排行等,用图表的方式展示。这里前端可以用 ECharts 来画图,后端只需要提供相应的统计查询接口。这类功能很能体现一个项目的完整度,也适合写进简历里。
从我的实际经验来看,项目能跑通只是第一步,你自己动手去改一两个功能、修一两个 bug,进步会非常明显。代码里的每一处设计,如果你能讲清楚当时为什么这么写,你的项目在面试时就能变成真正加分的东西,而不仅仅是简历上的一行字。我当初做这个项目修 bug 花的时间比写代码还多,但正是那些排查的过程,让我对这套技术栈的掌握深入了一个层次。
