毕业设计选题那会儿,我几乎把管理系统类的题目翻了个遍:图书管理、宿舍管理、药店管理……最后敲定做“社区医院管理系统”,用的是 SpringBoot + Vue + MySQL 这套最经典的组合。原因很简单:社区医院的业务流程不算复杂,但恰好能把登录鉴权、CRUD、多表关联查询、事务处理、数据统计、权限控制这些毕业设计高频考点全部串起来,工作量适中,又能讲出东西。
这套系统最终覆盖了科室管理、医生排班、患者建档、预约挂号、门诊接诊、处方收费、统计报表等核心模块,后端用 SpringBoot + MyBatis-Plus 提供接口,前端用 Vue + Element 完成页面,数据库用 MySQL 8.0 存储业务数据,还配套了完整的 SQL 脚本、毕设论文和部署文档。如果你正好拿到了这套源码,或者准备仿照它做一套自己的系统,这篇从业务拆解、数据库设计、核心代码思路、本地环境搭建、打包上线的实操记录和经验建议,都能帮你少走不少弯路。
1. 项目整体设计与业务拆解
1.1 业务背景与角色划分
先聊业务背景。社区医院和综合医院不一样,科室规模小、就医流程轻,服务对象里中老年人占比高,所以系统设计要“够用就好”,别整一堆复杂又用不上的功能。医疗流程的核心闭环是:建档 → 挂号 → 就诊 → 开方 → 收费,这套系统就围绕这条完整链路来设计。
角色分成三类,每类都有自己的操作面和管理边界:
| 角色 | 核心操作 | 数据权限 |
|---|---|---|
| 管理员 | 维护科室、医生、公告,查看系统统计数据 | 全系统可见 |
| 医生 | 查看排班和号源、处理预约、接诊写病历开处方 | 仅本人相关数据 |
| 患者 / 前台 | 注册登录、建档、预约挂号、缴费、查看就诊记录 | 仅本人数据 |
为什么不把用户表、医生表、患者表合成一张?这个我放到数据库设计章节细说,先把业务边界理清。三个角色的权限边界清晰之后,后端拦截器、接口设计、前端菜单路由的工作量都会直线下降。特别是“医生只能看到自己的患者”,这类过滤条件在 SQL 层就做掉,比在代码里到处写 if 判断要稳得多。
1.2 功能模块拆解与开发优先级
拿到源码先别急着跑起来,我建议你把功能模块先摊开看一遍,心里有个优先级排序。这个项目按业务域可以分成 8 个模块:
| 模块 | 子功能 | 服务角色 | 建议优先级 |
|---|---|---|---|
| 系统登录注册 | JWT 登录、注册、登出、密码加密 | 全部 | P0 |
| 基础数据管理 | 科室管理、医生管理、公告维护 | 管理员 | P0 |
| 患者管理 | 建档、列表查询、编辑、删除 | 管理员/前台 | P0 |
| 排班与号源 | 医生排班、每日号源数量设置 | 管理员/医生 | P0 |
| 预约挂号 | 号源查询、在线预约、取消挂号 | 患者/前台 | P0 |
| 门诊接诊 | 病历记录、诊断、处方开单 | 医生 | P1 |
| 收费结算 | 处方费用计算、收费记录 | 前台/收费员 | P1 |
| 统计报表 | 日就诊量、科室占比、医生工作量 | 管理员 | P2 |
从开发顺序上,我强烈建议按 P0 → P1 → P2 走,先打通“登录 + 基础数据 + 预约挂号”这条主线,再接诊和收费,最后做统计。很多同学卡在毕设进度上,就是因为一上来就啃报表,结果主流程还没通。主流程通了,哪怕统计模块做得简单点,答辩时也能把业务逻辑讲完整。
2. 技术选型、系统架构与数据库设计
2.1 为什么是 SpringBoot + Vue + MySQL
这套组合不是随便选的,每个组件都对应一个明确的问题。SpringBoot 解决了后端开发效率的问题,内嵌 Tomcat、自动装配、起步依赖,写几个类就能把 Web 服务跑起来,对比早期 SSM 要写一堆 XML 配置,工作量少一个量级;Vue 解决了前端交互的问题,组件化 + 响应式数据,开发体验好,而且单页应用切换路由非常顺滑,整个系统的页面流转像桌面软件一样自然;MySQL 则是最稳妥的数据层选择,事务支持可靠,网上资料和问题解决方案到处都是,遇到报错基本一搜就有答案。
可能有同学会问:为什么不选 SSM 传统模式、不选 Spring Cloud 微服务?对毕业设计而言,微服务就是给自己挖坑,拆服务、服务注册、链路追踪这些复杂度和项目体量完全不匹配。社区医院系统业务量级就是一台服务器、一个数据库的事,单体应用加上清晰的分层结构,已经是性价比极高的方案了。答辩时你讲清楚“为什么不用更重的架构”,反而是加分项。
2.2 系统架构与请求流转
整个系统采用前后端分离架构,数据流向大概是这样的:浏览器里的 Vue 页面通过 Axios 发起 HTTP 请求,带上 JWT Token,请求先打到 Nginx,Nginx 把 /api 开头的请求转发到后端 SpringBoot 服务;后端经过统一鉴权拦截器校验 Token,放行后到达 Controller,Controller 调用 Service 处理业务逻辑,Service 通过 MyBatis-Plus 操作 MySQL,数据结果再一层层返回。前端拿到 JSON 数据后更新组件状态,页面自动刷新。
这里有一个设计要点:前端并不是直接拼接 HTML,而是用 Vue Router 管理页面路由,每个路由对应一个组件文件。比如 /patient/list 对应患者管理页面,/doctor/schedule 对应排班页面。这种设计的好处是页面切换不需要整页刷新,用户体验接近于原生应用,而且组件可以复用,后期加功能时能减少很多重复代码。
2.3 数据库核心表结构与关键设计
数据库设计是整个系统的地基,先把核心表结构搞清楚,后面写代码才有方向。这个项目核心表大致有 9 张:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| sys_user | 登录账号 | id, username, password, real_name, role, status |
| department | 科室 | id, dept_name, intro |
| doctor | 医生扩展信息 | id, user_id, department_id, title, reg_fee |
| patient | 患者信息 | id, user_id, name, id_card, phone, medical_history |
| schedule | 医生排班/号源 | id, doctor_id, work_date, period, total, remain |
| registration | 挂号记录 | id, patient_id, doctor_id, schedule_id, visit_date, status, fee, order_no |
| medical_record | 病历诊断 | id, registration_id, patient_id, diagnosis, advice, create_time |
| prescription | 处方 | id, record_id, drug_name, dosage, quantity, unit_price |
| charge | 收费记录 | id, registration_id, total_amount, pay_method, pay_time |
关于用户表和医生表、患者表分开的原因:sys_user 只负责登录鉴权,存账号密码状态;doctor 和 patient 表存业务信息,通过 user_id 关联。这样设计的好处是登录逻辑和业务数据解耦,比如以后要加一个“护士”角色,只需要在 sys_user 里加 role,不需要动医生表和患者表的结构。
还有几个容易踩坑的细节。第一,挂号表里的 order_no 要保证唯一,建议用时间戳 + 随机数或者数据库序列生成,不要用自增主键直接暴露给用户。第二,schedule 表的 remain 字段是关键,医生的每日号源数量靠它控制,预约挂号时要对 remain 做原子扣减,这个我在第 3 章专门讲。第三,所有业务表尽量加上 create_time、update_time、deleted 字段,做逻辑删除而不是物理删除,一方面便于追溯数据,另一方面也符合医疗类系统对数据留存的要求。
3. 核心功能模块的实现思路与关键代码
3.1 登录鉴权与权限控制
登录鉴权我选的是 JWT,理由很直接:前后端分离架构下,JWT 无状态、不占用服务端 Session 内存、跨域友好,后端只需要在拦截器里校验签名,就能确定请求来自哪个用户、什么角色。整个流程是:用户提交用户名密码,后端校验通过后用 userId 和 role 生成 Token 返回前端,前端把 Token 存到 localStorage,之后每个请求在 Axios 请求拦截器里自动带上 Authorization 头,后端拦截器解析并放行。
核心配置类大致是这样的写法:
@Configuration public class WebConfig implements WebMvcConfigurer { @Resource private JwtInterceptor jwtInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login", "/api/auth/register"); } }@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equals(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (StringUtils.isBlank(token) || !JwtUtil.verify(token)) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } Claims claims = JwtUtil.parse(token); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } }前端路由守卫配套使用,没有 Token 的用户强制跳回登录页:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else { next() } })做这个模块时有一个心得:拦截器里只做“这个 Token 是否合法”的校验,不要在里面写业务逻辑。“管理员才能访问的接口”用什么样的方式控制?我建议在需要管理员权限的接口上加自定义注解 @RequireRole("ADMIN"),由另一个拦截器或 AOP 切面处理。如果所有权限判断都堆在登录拦截器里,代码会越来越难维护。
3.2 分页查询与条件筛选
管理后台最常用的功能就是列表查询,患者列表、挂号记录列表、医生列表全是这种形态。这个项目用的是 MyBatis-Plus,分页查询非常省事,先注入分页插件,再用 LambdaQueryWrapper 构造条件。
Mapper 层不用写 SQL,Service 里直接这样查:
public Result<Page<Registration>> getRegistrationPage(int page, int size, Long patientId, String status) { Page<Registration> p = new Page<>(page, size); LambdaQueryWrapper<Registration> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(patientId != null, Registration::getPatientId, patientId) .eq(StringUtils.hasText(status), Registration::getStatus, status) .orderByDesc(Registration::getCreateTime); Page<Registration> result = registrationMapper.selectPage(p, wrapper); return Result.success(result); }注意 wrapper 里每个条件都用“参数不为空才拼接”的写法,这样前端传不传筛选条件都能正常工作。前端表格用 Element 的 el-table + el-pagination,数据从接口返回的 records 字段取,total 字段控制分页总数。
这里有个容易被忽略的点:分页一定要配合条件筛选的“总和”。很多同学只分页不查询,导致筛选后第 2 页还是老数据。前后端联动时,el-pagination 的 current-page 和 page-size 改变事件,都必须重新携带筛选参数请求接口,否则就会出现“筛选结果明明只有 3 条,页面上还有 20 条数据”的尴尬情况。
3.3 预约挂号与库存控制
预约挂号是本项目业务含金量最高的模块,核心难点是号源并发控制。你可以把每天的号源数量理解成电影院座位——医生一天放出 30 个号,患者每约到一个,可约数量就少一个,两个人同时预约同一个号不能都成功。
最稳妥的实现方式是利用数据库的行锁和原子更新操作。先查 schedule 表确认还有号,再执行条件更新:
UPDATE schedule SET remain = remain - 1 WHERE id = #{scheduleId} AND remain > 0这条 SQL 是关键:remain > 0 作为条件放进 WHERE,数据库会锁住这行记录,两个并发请求同时过来时,只有一个能匹配到 remain > 0 更新成功,另一个影响行数为 0,业务层拿到 0 就知道号源已满,直接抛出“号源不足”异常。同时给整个挂号方法加 @Transactional 事务注解,确保扣减号源和新增挂号记录要么都成功,要么都回滚:
@Transactional(rollbackFor = Exception.class) public String register(Long patientId, Long scheduleId, Long doctorId) { Schedule schedule = scheduleMapper.selectById(scheduleId); if (schedule == null || schedule.getRemain() <= 0) { throw new BizException("号源不足,请选择其他时段"); } // 重复挂号校验:同一天同一医生同一患者只能挂一次 long count = registrationMapper.selectCount( new LambdaQueryWrapper<Registration>() .eq(Registration::getPatientId, patientId) .eq(Registration::getScheduleId, scheduleId) .eq(Registration::getStatus, "NORMAL")); if (count > 0) { throw new BizException("请勿重复挂号"); } int rows = scheduleMapper.decreaseRemain(scheduleId); if (rows == 0) { throw new BizException("号源已被抢完,请选择其他医生"); } Registration registration = new Registration(); registration.setOrderNo(generateOrderNo()); registration.setPatientId(patientId); registration.setDoctorId(doctorId); registration.setScheduleId(scheduleId); registration.setStatus("NORMAL"); registration.setFee(schedule.getRegFee()); registrationMapper.insert(registration); return registration.getOrderNo(); }事务加在这个方法上,少了任何一个判断,都可能出现“号扣了但记录没生成”或者“记录生成了但号没扣”的数据不一致问题。实测中并发压测 100 个线程同时抢 10 个号,用这条 UPDATE 配合事务,最终挂号记录数一定是 10,remain 也一定扣到 0,不会出现超卖。
3.4 统计报表与可视化
统计报表我放在 P2,是因为它有锦上添花的性质,但也别做得太单薄。管理员首页展示三项核心指标:每日就诊量趋势、各科室挂号占比、各医生接诊数量排行。后端数据用 SQL 聚合一次查出来,前端用 ECharts 画折线图和饼图。
SQL 聚合的典型写法:
SELECT DATE_FORMAT(visit_date, '%Y-%m-%d') AS day, COUNT(*) AS cnt FROM registration WHERE status = 'HAS_VISIT' AND visit_date BETWEEN #{start} AND #{end} GROUP BY day ORDER BY day注意 DATE_FORMAT 的日期格式化,后端接收的 start 和 end 是 yyyy-MM-dd 字符串,visit_date 字段是 datetime 类型,用 BETWEEN 查一天的边界时容易出问题。更稳的做法是查完当天零点之后的数据,比如 visit_date >= '2024-06-01 00:00:00' AND visit_date < '2024-06-02 00:00:00',用左闭右开区间,避免丢数据。
前端 ECharts 的关键配置和 Vue 组件生命周期绑定,数据在 mounted 钩子里请求,拿到后 setOption。如果你用 Vue 3,要注意图表实例和数据更新的响应式关系,建议在 watch 里重新 setOption。很多同学遇到的“图表数据不刷新”问题,十有八九就是只给组件传了新 props,没有手动更新图表配置。
4. 本地开发环境搭建与项目启动
4.1 后端工程初始化
先解决环境版本问题。我强烈建议 JDK 用 8 或 11,SpringBoot 用 2.7.x,这是目前毕业设计圈最稳的搭配。SpringBoot 3.x 把 javax 换成了 jakarta,改包名倒是小事,关键是很多网上的旧教程、旧第三方库不兼容,排查问题时会非常痛苦。你拿到源码后会想保持版本一致。
创建后端工程有两种方式:IDEA 内置 Spring Initializr 新建,或者直接用 Maven 命令创建。核心依赖就这几个:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>application.yml 里的数据源配置,有些参数缺了会报错,我把踩过坑的都标出来:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/community_hospital?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 hikari: maximum-pool-size: 10 minimum-idle: 5 connection-timeout: 30000 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0URL 里 serverTimezone=Asia/Shanghai 解决数据库时间差 8 小时的问题;useSSL=false 避免 MySQL 8 的 SSL 握手警告;allowPublicKeyRetrieval=true 解决 MySQL 8 默认 caching_sha2_password 认证方式下 IDE 连接报 Public Key Retrieval is not allowed 的报错。这三行配置,都是血泪经验。
4.2 前端工程搭建
前端我用的是 Vue 2.7 + Element UI,或者 Vue 3 + Element Plus 都可以。如果源码是基于 Vue 2 写的,建议老老实实跟着源码版本走,不要自己升级到 Vue 3,否则组件库、路由、状态管理全要换一套写法,工作量直接翻倍。
创建前端工程的经典步骤:
npm install -g @vue/cli vue create hospital-frontend cd hospital-frontend npm install vue-router@3 axios element-ui echartsVue 2 的项目用 vue-router@3,Vue 3 的用 vue-router@4,这个版本匹配非常关键。我见过不少同学把 vue-router 默认装成 4.x,然后在 Vue 2 项目里用 new VueRouter 直接报错,其实就是版本兼容问题。
前端目录结构建议这样组织:
src/ ├── api/ # 接口请求封装 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # 状态管理 ├── views/ # 页面组件 ├── utils/ # 工具函数 └── App.vue ├── main.jsAxios 封装放在 utils/request.js 里,统一配置 baseURL 和请求拦截器,这样每个页面不需要重复写 Authorization 头:
import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = token } return config }) export default request4.3 数据库初始化与导入
MySQL 安装这里不多展开,但要记住三个关键点:字符集选 utf8mb4、密码设置好记一点、端口默认 3306 不要改。安装完成后,用命令行或者 Navicat / DBeaver 连接数据库,新建库 community_hospital,字符集和排序规则选 utf8mb4 / utf8mb4_general_ci。
然后把项目里自带的 hospital.sql 导入。命令行导入方式最快:
mysql -uroot -p community_hospital < hospital.sql如果 sql 文件路径里有中文或者空格,命令行解析可能会出问题,建议把文件放到纯英文路径下。导入后先不要急着启动项目,先手动查一下几个关键表有没有数据,比如 sys_user 表里管理员账号是否存在、字段值是否完整。这一步能提前排查 90% 的“登录报错”问题。
4.4 联调、跨域与启动顺序
启动顺序有讲究,一定是先数据库、再后端、最后前端。后端启动后可以用浏览器直接访问接口测试,比如 http://localhost:8080/api/auth/login 用 POST 工具提交一条测试数据,确认接口返回正常再启动前端。
前后端联调时,跨域是最常见的拦路虎。本地开发时有两种解法:第一种是后端开启全局跨域配置,第二种是前端用 devServer 代理转发。实际项目里我更推荐前端代理方案,因为生产环境部署时也是 Nginx 做代理,前后端配置思路一致。
vue.config.js 里的代理配置:
module.exports = { devServer: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }这样前端开发服务器在 3000 端口,页面里请求 /api/login 时,会被代理到 http://localhost:8080/api/login。注意 target 后面不要加路径,changeOrigin 设置为 true,否则 Host 头不对可能导致 session 或 Token 解析出问题。
5. 打包部署与论文配套
5.1 后端打包与服务器运行
本地跑起来只是第一步,毕设验收或者给导师演示时,通常需要在一台独立的服务器上把整个系统跑起来。后端打包非常简单,Maven 一个命令搞定:
mvn clean package -DskipTests打包后的 jar 文件在 target 目录下,比如 hospital-backend-0.0.1-SNAPSHOT.jar。在服务器上运行,先确认服务器 JDK 版本和本地一致,然后:
java -jar hospital-backend-0.0.1-SNAPSHOT.jar前台运行的问题是关掉终端进程就没了,需要用后台方式运行:
nohup java -jar hospital-backend-0.0.1-SNAPSHOT.jar > app.log 2>&1 &日志重定向到 app.log,方便排查问题。记得放行服务器安全组里的 8080 端口,或者配置 iptables/firewalld 规则。这里有个小经验:如果你用的是云服务器,控制台的安全组规则和系统内部的防火墙都得检查,很多同学部署后外网访问不了,排查了一圈发现是安全组没放行。
5.2 前端构建与 Nginx 配置
前端打包分散到 build 命令:
npm run build构建结果输出到 dist 目录,这个目录整体上传到服务器的 /usr/share/nginx/html 下,或者任何 Nginx 能访问的路径。Nginx 最简配置如下:
server { listen 80; server_name localhost; root /usr/share/nginx/html/dist; index index.html; location / { 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; } }location / 里那行 try_files 是必须的。因为 Vue 是单页应用,前端路由是 history 模式,用户直接访问 /patient/list 这个地址时,服务器上并没有这个物理文件,如果不加 try_files,Nginx 会返回 404。加上 try_files $uri $uri/ /index.html 后,所有前端路由都会回退到 index.html,由 Vue 接管页面渲染。
如果部署后发现静态资源白屏,还有可能是 publicPath 的问题。vue.config.js 里建议设成相对路径:
module.exports = { publicPath: './', outputDir: 'dist' }否则打包后的 JS/CSS 路径是绝对路径 /js/app.js,部署在二级目录时就会 404。
5.3 论文与答辩材料的高效产出套路
毕设论文是这个项目里让很多人头疼的部分,其实框架固定,按下面章节结构写就能拿下:摘要、绪论(背景/意义/国内外现状)、需求分析、系统设计(架构设计/功能设计/数据库设计)、系统实现(每个模块截图加代码说明)、系统测试(功能测试/性能测试)、总结与展望。
几个高效率产出技巧。需求分析里的用例图、系统设计里的 E-R 图和流程图,用 DrawIO 或 ProcessOn 画,先画粗线框再填充内容,半小时能产出全套图。数据库设计章节,直接拿数据库的建表语句反向生成表结构说明,把字段名、类型、含义、约束整理成表格。系统实现章节,每个模块放 2-3 张截图加核心代码片段,关键代码用三五行讲清楚业务逻辑即可,不要整篇贴长代码。
答辩时老师最爱问的问题也就是几个:数据库表之间的关系是什么?预约挂号怎么处理并发?为什么用 JWT 不用 Session?权限控制怎么做的?你项目做过的同学把本文第 3 章的内容讲明白,这几个问题基本都覆盖到了。
5.4 部署文档配套经验
标题里明确写了“部署文档”,说明这是个容易被忽视但导师和评委很看重的附件。一份好的部署文档不需要写得词藻华丽,但必须能让人照着操作就把系统跑起来。建议包含六部分:环境要求(JDK 版本、Node 版本、MySQL 版本、服务器配置)、本地开发环境配置步骤、数据库初始化步骤(sql 导入命令)、后端打包和运行命令、前端打包和 Nginx 配置、常见问题排查。
写部署文档时有个反直觉的注意点:越详细越好,甚至要把“用命令行进入 sql 文件所在目录再执行导入”这种基础操作都写进去,因为读文档的人可能是零基础的操作者。另外文档里截图建议统一裁剪成一致的尺寸,排版清晰,这属于给评委看的“印象分”。
6. 常见问题与避坑指南
6.1 高频问题速查表
这一节直接把我在开发部署过程中遇到过的、以及周围同学问得最多的问题整理成速查表,方便你直接定位排查:
| 现象 | 原因 | 解决方案 |
|---|---|---|
| MySQL 连接报 Public Key Retrieval is not allowed | MySQL 8 默认认证协议问题 | JDBC URL 加 allowPublicKeyRetrieval=true |
| 数据库连接报 SSL 相关 warning | JDBC URL 未关闭 SSL | URL 加 useSSL=false,或 SSLMode=REQUIRED |
| 查询出的中文全是乱码 | 字符集不匹配 | 数据库、表、连接 URL 统一 utf8mb4 |
| 系统时间比实际时间少 8 小时 | JDBC 时区未设置 | URL 加 serverTimezone=Asia/Shanghai |
| 前端请求后端接口报跨域 | 前后端分离默认跨域 | 前端 proxy 代理,或后端 CORS 配置 |
| npm install 很慢或失败 | 网络源问题 | 配置国内 npm 镜像(npmmirror)临时源 |
| 前后端口被占用 | 本地有其他进程占用 | netstat 查到 PID 后结束进程 |
| vue-router 找不到模块或报错 | 路由版本与 Vue 版本不匹配 | Vue2 用 vue-router@3,Vue3 用 vue-router@4 |
| ECharts 图表数据不更新 | 数据变化未触发 setOption | 在 watch 或 nextTick 后重新设置图表配置 |
| SpringBoot 3 启动报 javax 不存在 | javax 改名为 jakarta | 换回 SpringBoot 2.7.x,或全局替换 import |
| 打包时资源文件丢失 | Maven 默认不包含 src/main/resources 外的资源 | 在 pom 里配置 resources 路径 |
| 部署后页面刷新 404 | 前端 history 模式未配置 | Nginx location 加 try_files |
| 数据库连接池超时断开 | 长时间空闲连接失效 | 配置连接池 validation-timeout / keepalive |
| 逻辑删除后插入数据报唯一键冲突 | 唯一索引包含逻辑删除字段 | 联合唯一索引增加 deleted 字段 |
6.2 关于 jar 反编译与二次开发的安全姿势
经常有人问“怎么把一个 SpringBoot 的 jar 反编译成项目”,这个诉求在毕设场景里其实很常见——拿到一份别人打包好的 jar,想恢复出源码结构来学习和二次开发。这里说的前提必须是合法场景,比如这份 jar 是你自己打包的、或者是授权允许学习研究的示例项目。用 IDEA 自带的反编译能力就能看 class 文件的内容:把 jar 作为 Library 添加到项目,双击任意 .class 文件,IDEA 会直接展示反编译后的 Java 代码。也可以用 JD-GUI、Luyten 这类桌面工具,反编译结果对学习框架调用逻辑很有帮助,基本能还原出 Controller、Service、Entity 的大致结构。
但必须提醒几个关键点:反编译只能还原字节码层级的信息,注释、常量命名、泛型详情和 Maven 依赖关系会丢失,不要指望百分百还原成原始工程。更重要的是版权和诚信问题,拿反编译手段去还原别人的商业系统或者未授权代码,是绝对不行的。毕设阶段建议把反编译当成“阅读学习工具”,而不是“复制粘贴快捷方式”。真正想复现一套系统,更应该关注数据库脚本、文档和接口行为,从业务层面去理解设计思路,反编译代码只是辅助验证。
6.3 我踩过的几个印象深刻的坑
分享几个让我记忆深刻的实际问题,希望能帮读者避开。
第一个坑是逻辑删除和唯一索引的冲突。我给 patient 表的 phone 字段加了唯一索引,又用了 MyBatis-Plus 的逻辑删除机制。结果测试时发现:删掉一个患者后再用同样的手机号建档,插入直接报唯一键冲突。原因很简单,逻辑删除只是把 deleted 字段置为 1,记录本身还在表里,唯一索引照样生效。解决方法是去掉单一 phone 索引,或者建联合唯一索引 (phone, deleted),这样已删除记录不会阻塞新增数据。
第二个坑是 @Transactional 事务失效。我在 Service 内部写了一个方法调用另一个同类方法,被调方法上加了 @Transactional,结果压测时发现数据竟然没回滚。原因是 Spring 事务默认基于代理机制,同类内部调用绕过了代理对象,事务注解根本不生效。解决方式是把事务方法拆到另一个 Service 类,或者注入自身的代理对象,再或者直接用 TransactionTemplate 手动控制事务边界。
第三个坑是部署到服务器后的时区问题。本地开发时一切正常,服务器上查询出来的创建时间总是少 8 小时。排查了半天,发现是服务器系统时区是 UTC,而我 JDBC URL 里虽然写了 serverTimezone=Asia/Shanghai,但 MySQL 服务端的 time_zone 变量没改。最终在 my.cnf 里加了 default-time-zone = '+08:00' 再重启 MySQL 才解决。所以部署时记得同时检查连接 URL、MySQL 时区变量、服务器系统时区三个位置,多一层检查就少一次返工。
第四个坑和数据库工具相关。很多同学喜欢在网上下载“破解版 Navicat”,其实这类工具来路不明,安全风险很高,做毕设阶段强烈建议用 DBeaver 社区版或者 DataGrip,前者免费开源,后者学生授权也方便。数据库工具只是辅助,重要的是把 SQL 逻辑和表关系理清楚,工具纯粹看个人顺手程度。
最后再分享一个小技巧
做完这个项目以后,我最大的体会是:一个管理系统能不能打,不在于功能有多华丽,而在于基础业务闭环是不是真正跑通了。登录、挂号、接诊、收费、统计,这条线上任何一个环节出现数据不一致,整个系统的可信度都会崩塌。所以做的时候不用着急堆页面,先确保核心流程的数据流转是自洽的,再考虑界面好不好看。
还有一个可以后续扩展的方向:当前系统的预约挂号是按天和时段走的,可以进一步引入“号源池”概念,比如每个时段放多个号、支持批量导入排班、支持医生请假时批量停诊。这些改动不会伤筋动骨,数据库层面只需要增加一个排班状态字段和批量操作接口,对系统整体架构没有冲击。如果你有余力,往这个方向加两个功能点,答辩的时候讲出来也是很好的亮点。
这套 SpringBoot + Vue + MySQL 组合其实特别适合毕设场景,理解透了这套系统的业务逻辑和代码结构,以后换个题目,比如宠物诊所、药店管理、驾校预约,本质上都是同一套骨架,换皮换需求就好。希望这篇经验文能帮你把这个项目吃透,顺利过关。