提到“基于SpringBoot+Vue的医院病历管理系统”这个标题,我的第一反应是:又是一个典型的Java全栈项目。不过说实话,这类系统在真实业务里非常有代表性——它涵盖了用户权限、多角色操作、复杂表单、模糊检索、分页展示、数据统计这些管理系统的通用场景。无论是拿来练手、做课程设计,还是想以后在此基础上改造成其他业务系统,这套技术组合都能把基本功磨得很扎实。
这篇文章不打算只贴源码、说“你下载下来跑起来就行”,而是把整个系统从需求梳理到前后端实现、从数据库设计到线上排错,按我实际做这类项目的思路完整拆一遍。跟着这条线走完,你收获的不只是一份代码,而是一个清清楚楚的“为什么要这么写”的完整逻辑。
项目地址、依赖包这一类内容网上随手能搜到,我重点讲源码底层的设计模式和关键模块的落地方式,同时把我在调试过程中踩过的坑一并交代清楚。
1. 整体设计与需求拆解:先别急着写代码
1.1 这个系统到底要管理什么
病历管理系统绝对不是简简单单的“增删改查”。往细了说,它要解决的核心问题是:让医生能快速查看患者历史就诊记录,让患者能集中管理自己的健康档案,让管理员能掌控全系统的数据和账号流动。
从核心业务逻辑看,至少要覆盖这些场景:
- 患者初次建档:录入姓名、性别、年龄、身份证号、联系方式、既往病史等基础资料,生成一个全局唯一的患者编号。
- 医生日常写病历:每次就诊记录主诉、现病史、检查结果、初步诊断、治疗方案、医嘱处方,并关联到具体患者。
- 病历检索:按患者姓名、病历号、诊断关键词快速查历史记录,支持按时间倒序排列。
- 权限隔离:医生只能写自己经手的病历,管理员管理账号和基础数据,患者本人只能查看自己的病历信息。
- 基础数据维护:科室、医生的信息维护,后续还可以扩展药品库。
我当时自测的时候,为了让项目更像真实业务,还额外加了两个功能:按科室统计病历数量和简单的图表展示。这两个功能不算复杂,但很能体现SpringBoot+MyBatis做分组统计的能力,建议你也保留。
1.2 角色权限怎么设计
权限设计是这个系统里最容易被新手做糊的部分。常见方案有两种:
第一种是粗粒度的角色判断——在一个接口里用 if (user.getRole() == 1) 来判断当前登录人是不是管理员。这种方案写起来快,但后续要扩展“护士可以录入体温单但不能修改诊断”这类需求时,就变得非常痛苦。
第二种是细粒度的RBAC(基于角色的访问控制)——用户表关联角色表,角色表关联权限表,在拦截器或者注解层做接口级校验。
实际项目中我推荐用第二种,但不要一上来就把五张表铺开。合理的做法是:user表里带一个role_id字段,role表里存角色标识,再通过一个自定义注解 + Spring AOP拦截器去校验访问权限。这样既避免了太复杂的表关系,又把扩展性留住了。
我自己做的时候,角色分了三类:
| 角色 | 权限范围 | 前端可见菜单 |
|---|---|---|
| 管理员 | 用户管理、科室管理、全部病历查看与删除 | 系统管理、病案中心、统计报表 |
| 医生 | 本人病历新增与编辑、患者查询、查看所在科室病历 | 患者台账、病历填写、我的记录 |
| 患者 | 查看本人基本信息与病历记录 | 个人中心、我的病历 |
这里的操作按钮也是按角色区分的。比如删除病历的按钮只有管理员看得到,医生端连入口都不展示,防止误操作引发数据问题。
1.3 功能模块的划分方法
按我的习惯,一个中型管理系统在动手前会先用一张表把功能模块、数据表和核心接口的对应关系列出来。这张表就是后面所有代码的“地图”。
| 功能模块 | 涉及数据表 | 典型接口 |
|---|---|---|
| 登录与身份认证 | user | POST /api/auth/login |
| 患者信息管理 | patient | POST /api/patient、GET /api/patient/list |
| 病历管理 | medical_record | POST /api/record、GET /api/record/page |
| 诊断与处方 | diagnosis、prescription | POST /api/prescription |
| 数据统计 | medical_record | GET /api/record/stats |
| 用户与角色管理 | user、role | GET /api/user/list、PUT /api/user |
这个映射关系一旦建立,数据库建模的时候心里就特别有数,不会写着写着发现缺了一张表。
2. 技术选型解析:为什么是这套组合
2.1 后端为什么选SpringBoot+MyBatis
先说SpringBoot。这类管理系统用SpringBoot一定是首选,没有悬念。它内嵌了Tomcat,不需要额外部署外部容器;自动配置机制省掉了大量Spring配置文件;配合Spring生态的starter,引入什么功能就加什么依赖,开发体验非常顺滑。
真正需要你斟酌的是持久层的选择。这里不只有MyBatis一个选项,常见的还有MyBatis-Plus、Spring Data JPA。说下我的结论:
- 如果用Spring Data JPA,日常CRUD确实非常爽,省事到不可思议。但问题是复杂报表统计、动态SQL拼接这一类需求,写起来要么靠JPA的Specification,要么就得写原生SQL,维护成本反而上去了。
- 如果用MyBatis-Plus,代码量最小,内置了分页插件、逻辑删除、代码生成器。但它的Service层封装得比较重,一旦遇到需要精细控制SQL的场景,反而会感觉被框架束缚。
- 原生MyBatis最“不傻瓜”,但SQL完全掌握在自己手里。尤其是病历检索这种多重条件动态拼接的查询,用MyBatis的动态SQL标签写出来非常直观。
我这个项目最后选了原生MyBatis。原因很朴素:病历管理系统的查询逻辑复杂度远高于普通表单,而原生MyBatis在SQL控制力上是最强的。再说了,以我的经验,面试或者答辩的时候,考官对MyBatis动态SQL的追问概率非常高,把这个基本功练扎实非常划算。
2.2 前端为什么选Vue这套生态
前端用Vue几乎是这类后台管理项目的标准答案。Vue的响应式数据绑定让表单交互写起来很自然,组件化开发让页面复用变得简单,生态里的Element UI(或Element Plus)组件库更是专门为解决后台管理界面而生的。
具体组件选型上,我建议:
- UI组件库用Element UI,表格、表单、弹窗、日期选择器都是现成的,能省出一大堆样式代码。
- 路由用Vue Router,注意配置路由懒加载,否则首屏加载会明显变慢。
- 状态管理用Vuex或Pinia,用来存登录用户的token和基本信息,全局访问起来非常方便。
- HTTP库用Axios,封装好拦截器之后,每次请求自动附带token,收到401状态码自动跳转登录页。
这套组合的实际体验就是:开发阶段组件开箱即用,构建阶段体积可控,团队协作时分工清晰。前后端分离开发调试也很省心,后端只需要把接口定义清楚,前端就可以并行推进。
2.3 为什么不做微服务架构
有些同学看到“医院病历管理系统”这个名字,下意识就想去搞Spring Cloud、分布式事务那一套。我特别想提醒一句:架构选型要匹配业务复杂度。
病历管理系统本质上是一个企业内部的信息管理系统,并发量在百级以内,数据量在百万条级别,单机MySQL完全能扛得住。这时候引入微服务,带来的是注册中心、配置中心、网关、分布式事务这些额外负担,每个环节都会成为新的故障点。
我个人的判断标准很简单:当前技术团队几个人?项目周期多久?业务增长预期有多大?小于等于两个人维护、交付周期一到两个月、业务量没有爆发式增长预期,单体应用就是最优解。真到了需要拆分的时候,基于这个项目清晰的模块边界,拆起来也不难。
2.4 版本与环境建议
为了避免“在我电脑上明明可以跑”的尴尬,环境版本我建议锁定一套稳定的组合:
- JDK 8(生产环境普遍还在用,兼容性最好)
- SpringBoot 2.7.x(注意别用3.x配JDK8,会直接启动失败)
- MySQL 8.0(注意时区配置)
- Node.js 16.x + Vue 2.7 + Element UI 2.x(如果要上Vue3就选Element Plus)
- Maven 3.6以上
提示:SpringBoot 3.x最低要求JDK17,如果直接拿较新的Boot版本配合JDK8运行,启动阶段就会报“无法读取”的包错误。我的建议是,如果对SpringBoot版本不熟悉,就老老实实选择2.7.x这条线。
3. 数据库设计与核心表结构
3.1 实体关系先理清楚
数据库设计是一套系统中最容易“返工”的部分。我在设计表之前习惯先画实体关系图,哪怕只是在纸上画几个方框和连线,也可以避免后续大量修改。
这个系统的核心实体关系并不复杂:
- 用户(user)和角色(role)多对一:一个角色下挂多个账号。
- 患者(patient)和病历(medical_record)一对多:一个患者可以有多条就诊记录。
- 病历(medical_record)和诊断(diagnosis)一对一。如果一次就诊有多个诊断,就可能是一对多。
- 病历(medical_record)和处方(prescription)一对多:一次就诊会开多组药品。
从这段关系能看出,病历表是整个系统的核心枢纽。所有查询的终点几乎都会落在病历表上,所以这张表的索引设计和字段类型就是设计的重头戏。
3.2 用户表与角色表:权限的根基
用户表我建议这样设计,注意看加了哪些关键字段,以及为什么加:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| username | varchar(64) | 登录名,唯一索引 |
| password | varchar(128) | BCrypt加密后的密文 |
| real_name | varchar(64) | 真实姓名 |
| role_id | bigint | 关联角色表 |
| status | tinyint | 账号状态,1启用 0禁用 |
| create_time | datetime | 创建时间 |
密码这个字段是我特别想提醒的。无论项目有多大,不要用明文存密码,也不要用MD5(太多已知冲突)。必须用BCrypt这类加盐哈希算法。Spring Security里自带BCryptPasswordEncoder,哪怕项目没有引入Spring Security,也可以单独引入spring-security-crypto这个依赖来使用它。
角色表就更简单了:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| role_code | varchar(32) | ADMIN、DOCTOR、PATIENT |
| role_name | varchar(32) | 管理员、医生、患者 |
用role_code做程序判断,用role_name做界面展示,这是规范的做法,不要反过来。
3.3 病历主表:字段不仅要够用,还要有前瞻性
病历表是整个系统的灵魂,字段我会这样考虑:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| record_no | varchar(32) | 病历号,唯一索引 |
| patient_id | bigint | 关联患者表 |
| doctor_id | bigint | 关联用户表,记录医生 |
| visit_date | datetime | 就诊时间 |
| chief_complaint | varchar(500) | 主诉 |
| present_illness | text | 现病史 |
| past_history | text | 既往史 |
| examination | text | 检查情况 |
| diagnosis_result | varchar(500) | 初步诊断 |
| treatment_plan | varchar(1000) | 治疗方案 |
| status | tinyint | 数据状态,1正常 0删除 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
关于record_no这个病历号,不同医院有自己的编码规则,但作为项目实现,我建议用一个简单的规则:当前年月日时分秒 + 患者ID + 随机三位数字。这样生成的编号在展示层级看起来比较专业,同时也能作为唯一索引保证不重复。
主诉、现病史这类长文本字段,直接用text类型就好,不要为了省空间用varchar(255)。真实病历文本动不动就几百上千字,字段长度不够在保存时会被截断,这属于上线后才会被发现、但一旦发现就很尴尬的问题。
另外,我建议每个表都保留status作为逻辑删除标记,而不是真的执行DELETE语句。病历数据涉及长期留存,物理删除了就很难追责。查询列表时统一在SQL里加上 AND status = 1,这套约定在写代码前就要定下来。
3.4 字段设计时容易犯的几个错
第一,金额字段不要用float或double。处方里的药品单价、总价,用decimal(10,2)最稳。浮点数在计算机底层是二进制近似存储,加减乘除会“凭空多出0.001”这种问题,一旦涉及金额就是事故。
第二,时间字段建议用datetime而不是timestamp。虽然timestamp占用空间更少,但它有2038年的存储上限问题,而且受时区配置影响大。datetime的显示和写入行为更可控,对开发者更友好。
第三,身份证号用varchar(18)或varchar(20),不用char(18)。原因是某些地区的新版本证件号可能格式略有差异,而且首位的0如果存在,数值类型会直接丢掉。
第四,所有核心表的主键都用bigint自增。系统规模没到一定的量级,不要人为引入雪花ID。自增主键在MySQL的InnoDB存储引擎下插入性能最优,索引结构最紧凑。
4. 后端核心功能实现:从Controller到SQL
4.1 项目分层结构不能省
拿到源码后先别急着点运行,把项目的包结构看清楚,这比代码本身更重要。我采用的分层是标准的controller、service、mapper三层结构:
- controller:只做参数接收、结果返回,不做任何业务逻辑。
- service:负责业务流程编排、事务控制、数据校验。
- mapper:纯数据访问层,只放数据库操作和对应SQL。
- entity:数据库表的映射实体。
- dto:前端传输数据的对象,防止实体类直接暴露给前端。
- common:统一返回结果、异常处理、工具类。
这个结构看上去是“老八股”,但它最大的价值是职责单一。我和同事合作的时候,大家一看包名就知道新功能在哪写,不用互相问。
4.2 统一返回结果和全局异常处理
前后端分离的项目,必须约定一个统一的返回格式。我在项目里用的是最常见的结构:
{ "code": 200, "message": "success", "data": {} }对应后端写一个Result类,提供静态方法Result.success(data)和Result.error(code, message)。写起来很简单,但如果没有统一规范,前端接手的时候就会被折腾疯——有的接口返回{status: 1},有的返回{success: true},每接一个接口就得单独判断,这是最典型的返工诱因。
全局异常处理我用@RestControllerAdvice实现。DTO校验、业务异常、SQL异常,统一拦截后转成Result错误的JSON格式返回。新手容易忽略的一点是:异常必须输出日志,用log.error记录堆栈信息。否则线上出问题的时候,你能看到用户反馈一个“系统错误”,但后端控制台一片空白,查错就像大海捞针。
4.3 JWT登录与拦截器实现思路
登录模块的逻辑其实不复杂。前端把用户名和密码发给后端,后端校验通过后生成一个JWT令牌返回给前端。JWT令牌里会包含用户的id、用户名、角色编码,过期时间我建议设置成2小时,运维时可以按需调整。
前端拿到这个令牌后,每次请求在HTTP Header的Authorization字段里带上它。后端写一个拦截器,对所有需要登录的接口做token校验,这段逻辑用一句话总结就是:能拦截接口,绝不手写校验。
拦截器里面要做的事包括:从Header取token、解析token、把解析出的用户信息放到ThreadLocal里,方便后续代码直接获取当前登录用户。
有一个坑值得单独说:JWT解析失败时不要直接抛异常中断请求,而是返回401状态码。前端Axios拦截器收到401后统一跳转登录页。这个联动逻辑要是没做好,用户token一过期就会出现页面卡死、请求无限报错的现象。
4.4 病历查询接口:分页与模糊检索的实现
病历查询是后端里最有技术含量的部分,因为它不是单纯的一条select。实际场景通常是:用户输入一个患者姓名,或者选一个诊断关键词,再或者选择一个时间范围,然后按条件分页展示。
我提供一段核心的Service层思路:
public PageResult<MedicalRecordVO> queryRecordPage(RecordQueryDTO dto) { // 1. 补充当前登录医生ID,用于数据隔离 // 2. 调用Mapper的selectPage方法 // 3. 分页参数由PageHelper或手写limit控制 }如果用原生MyBatis,分页我建议用PageHelper插件,一行代码完成分页,不需要手写limit计算。但PageHelper使用时有一个很典型的坑:它必须在查询语句前一行调用PageHelper.startPage(pageNum, pageSize),并且紧跟的查询必须就是我们要分页的那一条SQL,中间不能穿插其他查询语句。在Mapper里有多个查询时,一旦startPage和真正需要分页的查询之间隔了别的SQL,数据就会错乱。这个问题的排查过程非常折磨人,建议一上来就记住这个规则。
模糊检索则在XML里面用动态SQL实现:
<select id="selectPage" resultType="com.example.entity.MedicalRecord"> select * from medical_record <where> <if test="patientName != null and patientName != ''"> patient_id in (select id from patient where name like concat('%', #{patientName}, '%')) </if> <if test="diagnosis != null and diagnosis != ''"> and diagnosis_result like concat('%', #{diagnosis}, '%') </if> <if test="startDate != null"> and visit_date >= #{startDate} </if> <if test="endDate != null"> and visit_date <= #{endDate} </if> </where> order by visit_date desc </select>注意XML里出现了 >= 和 <=。直接写大于号小于号在XML文件里会被解析器当成标签开始符号,导致文件直接加载失败。这个转义规则我每次写带日期范围查询的时候都会默念一遍。
为什么患者姓名条件要在子查询里做,而不是用join联查?这个看个人习惯。用join一次把patient表和medical_record表关联出来的确更高效,但会把返回字段变得复杂,还需要在resultType里处理嵌套对象。如果你刚开始做,我建议用子查询先把业务跑通,等确认性能瓶颈了再做join优化也不迟。
4.5 事务处理:写病历必须保证数据一致
一次就诊过程如果同时写病历表、诊断表、处方表,三步操作必须绑成一个事务。要么全部成功,要么全部回滚。这个需求在Service层方法上加一个@Transactional注解就搞定了。
有几个细节需要注意:
- @Transactional注解应该加在ServiceImpl类的方法上,而不是Controller层。
- 开发时配置一下事务失效的常见场景:方法被同类内部调用时,注解不生效。
- 默认情况下,只有RuntimeException和Error会触发回滚。如果抛出的是检查异常,比如IOException,事务不会回滚,需要手动指定rollbackFor = Exception.class。
为了省心,我统一在类上或者方法上写@Transactional(rollbackFor = Exception.class),养成习惯,不给自己留隐患。
5. 前端核心功能实现:从Vue页面到接口对接
5.1 路由规划与页面结构
前端的结构直接影响后端的接口设计。我建议前端按模块拆views目录,大致规划如下:
- views/login:登录页。
- views/layout:主布局,包含左侧菜单和顶部导航。
- views/patient:患者管理页面。
- views/record:病历填写与病历列表。
- views/user:用户管理。
- views/stats:统计报表。
Vue Router里有个好习惯:给每个路由配好meta信息,例如是否需要登录、需要什么角色。在全局前置守卫里统一判断:
router.beforeEach((to, from, next) => { const token = store.state.token if (to.meta.requiresAuth && !token) { next('/login') } else { next() } })这段代码的意义是把登录校验集中在一个位置处理,任何页面刷新也不会漏掉登录状态的判断。
5.2 Axios封装:该拦截的都拦截掉
Axios如果不做二次封装,代码里会到处出现重复的header设置、错误提示弹窗,维护起来非常痛苦。我的做法是创建一个request.js作为统一的请求入口:
const service = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动附带token service.interceptors.request.use(config => { const token = store.state.token if (token) { config.headers['Authorization'] = token } return config }) // 响应拦截器:统一处理错误码 service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response.status === 401) { router.push('/login') } ElMessage.error('服务器异常,请稍后重试') return Promise.reject(error) } )这段代码里的首页加载大坑:如果加上了响应拦截器,返回值就已经被处理过一次了,调用接口的地方再取res.data,拿到的是undefined。前后端联调阶段,这种问题非常频繁。统一规范就是:拦截器统一返回res(后端Result对象),业务代码里自行取res.data。
5.3 登录状态与本地存储
用户登录成功后,前端需要把token和用户基本信息存起来。我的方案是:
- token直接存localStorage,刷新页面不丢失。
- 用户基本信息存localStorage,同时放到Vuex里。
- 页面每次刷新,在路由守卫里从localStorage读回Vuex。
为什么不把用户信息也直接存在本地不轮询呢?从体验上没问题,但万一管理员在后台把某用户账号禁用了,本地存的角色信息可能还是会让人继续访问一段时间。所以更稳妥的做法是:登录后只存token,用户信息在每次刷新或者进入系统时调用一次GET /api/auth/info接口获取最新数据。
5.4 病历表单的设计与校验
病历填写页是这个系统前端最复杂的页面。字段多、类型杂、校验规则繁,而且体验要求很高。
我用Element UI的el-form来做表单。核心是定义好rules校验规则:
- 姓名:必填,长度限制30个字符。
- 就诊时间:必填,默认当前时间。
- 诊断结果:必填,长度限制500字。
- 主诉:必填,至少5个字符。
校验时的细节:所有文本域建议统一加show-word-limit属性,给用户显示还可输入多少字。如果超长,后端接口也会因DTO校验注解@Size而拒绝保存,这个前后端双重校验是必需的。
日期选择器的格式要跟后端对齐。Element UI的日期选择器默认返回Date对象,如果我直接序列化传给后端,后端收到的是“2025-04-03T16:00:00.000Z”这种带时区的格式,存进MySQL里时间会差8个小时。我的做法是在日期选择器上设置value-format="yyyy-MM-dd HH:mm:ss",保证传参是字符串而不是时间对象,这一步在我第一次做项目时就踩过雷。
5.5 列表展示与分页联动
病历列表页需要一个清晰的表格展示,加上分页器。我的核心绑定逻辑是:
- 搜索条件放在data里。
- 分页参数pageNum和pageSize也放在data里。
- 点击搜索按钮时重置pageNum为1。
- 调用后端接口时同时传入搜索条件和分页参数。
- 表格数据接口返回total后,分页器用total渲染总条数。
这个逻辑听起来很基础,但新手最容易漏掉的一环是“点击搜索要把页码重置为1”。如果不重置,用户在第5页搜索一个关键词,搜索出来的结果可能只有2页,前端页码却还停留在第5页,页面就成了空白。
6. 常见问题与排查技巧实录
6.1 跨域问题
前后端分离开发时,前端跑在8080端口,后端跑在8081端口,浏览器默认会拦截这种跨域请求。一旦出现CORS错误,后端接口返回的数据前端根本拿不到。
有两种解法:一种是后端配置CorsFilter,允许指定来源地址;另一种是前端在Vue.config.js里配置devServer的proxy代理。我推荐在开发阶段用代理方案,因为代理模式下前端请求的URL和后端实际URL由Node服务器转发,浏览器感知不到跨域,跟生产环境的表现更接近。
如果后端采用CorsFilter方式,注意allowedOrigins不要写成*,否则在某些需要携带Cookie的场景下会被浏览器拒绝。更严谨的做法是指定具体的来源地址。
6.2 数据库时区错误
MySQL 8.0最常见的启动错误是类似“The server time zone value 'Öйú±ê׼ʱ¼ä'”。这个错误看起来很吓人,实际上就是连接串里的时区参数没指定。
解决办法是在SpringBoot的application.yml里把jdbc连接URL加上serverTimezone=Asia/Shanghai,比如:
jdbc:mysql://localhost:3306/hospital?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8如果不想在连接串里写,也可以在MySQL服务端把全局时区设置成+8:00。不过我觉得连接串里写是最直接最不容易出错的方式,代码跟着项目走,换台机器也不会丢配置。
6.3 中文乱码问题
前后端分离项目里出现中文乱码,要从三条链路排查:
第一道:数据库连接串是否有characterEncoding=utf8,指的是后端写到MySQL或从MySQL读出的编码。 第二道:MySQL表本身的字符集是否utf8mb4。注意utf8mb4和utf8的区别,utf8在一些场景下连emoji都存不了。 第三道:前端页面meta标签charset=utf-8,以及请求头Content-Type是否带charset。
排查顺序从前往后,先检查数据库连接串,再检查表结构,最后检查前端页面。90%以上的乱码都是连接串缺少编码配置导致。
6.4 MyBatis的动态SQL运行报错
如果我写了很多字段都放到if判断里,然后发现控制台报“BadSqlGrammarException”,先检查是不是XML里的标签闭合问题。这个问题的典型特征有:多了一个 标签、标签嵌套不匹配。
如果报“Parameter 'xxx' not found”,通常是Mapper接口的参数没加@Param注解。参数超过一个时,MyBatis无法直接通过参数名找到对应值,必须显式声明。一个直观的规律是:只有一个参数且参数类型是JavaBean时不需要@Param,其余情况建议都加上。
6.5 前端保存数据后列表不刷新
这是一个体验问题,也算是一个经典坑。在新增病历成功后,列表页不自动刷新,必须手动点一下浏览器的刷新按钮才能看到新数据。
原因是新增和列表是两套接口,保存成功后前端只是把弹窗关了,并没有重新拉取列表数据。正确做法是在保存接口的then回调里调用一次列表查询方法,或者用this.$refs.table.loadData()触发表格重新加载。顺便说一句,如果用了Vuex保存了一些全局数据,也要注意数据更新的响应式问题,深度改变对象时要用Vue.set或者重新赋值。
6.6 打包部署时的那些事
后端打包直接用Maven的package命令,打成jar包后执行java -jar运行。但实际部署时我有两个建议:
一是application.yml里把数据库密码等敏感信息放到外部配置文件中,不要打包进jar。通过--spring.config.additional-location参数指定外部配置文件路径,这样换环境不需要重新打包。
二是前端打包默认走npm run build,生成dist目录。部署方式有两种:一种是把dist目录放到后端项目的static目录下,让SpringBoot同时托管前端页面和接口;另一种是单独用Nginx托管前端,并把/api路径通过反向代理转发给后端。
我实际推荐Nginx方式,因为前后端分开之后,后续任何一边改动都不用重新部署另一边。Nginx配置转发大概是:
location /api/ { proxy_pass http://127.0.0.1:8081/; }注意proxy_pass这里是否带尾斜杠,会直接影响到实际转发路径是保留还是裁剪/api前缀。这块建议单独用curl测试验证出结果,别凭感觉填。
7. 最后再分享一点做这类系统的体会
把整个系统完整做下来,最大的收获反而不是新技术,而是一套“怎么把模糊需求变成可落地系统”的思路。我一开始也犯过“拿到标题就开写”的毛病,结果做到一半发现缺了患者角色登录、缺了数据统计,回头反复改表结构,浪费了非常多时间。
如果你正在做或者准备做同类系统,我个人建议分三步走:先把需求文档写清楚,再花一小时把数据表设计出来,最后才动手写代码。这个过程看起来磨蹭,实际上恰恰是把项目做快最有效的路径。
这个系统的扩展空间也非常大,后续可以加药品库存管理、检查检验报告上传、患者移动端查看报告,甚至通过定时任务做数据备份。骨架搭得稳,后续加功能就是做加法的事,不会因为早期设计偷懒而把自己困住。