
1. 项目概述与整体思路1.1 为什么是Spring Boot Vue住院管理系统这类项目在医疗信息化领域里属于典型的信息管理类系统核心价值就是让病房管理、患者信息登记、医嘱执行、费用结算这些原本靠纸质单据和人肉传递的流程变成一套线上可追溯、可统计的闭环流程。搞过医院相关项目的人应该深有体会住院部是最能暴露管理混乱的地方——患者转床了护士没更新、药费记了一笔找不到出处、医生开了长期医嘱但执行护士换了班没看明白这些问题背后本质上都是信息同步的时效问题。所以一套住院管理系统要解决的不是单个环节的自动化而是打通“入院—住院—出院”全流程的信息流转。技术选型上Spring Boot加Vue这个组合在近些年的Java后端项目里几乎是统治级的组合。Spring Boot负责把后端服务跑起来内置Tomcat不需要额外部署WAR包自动配置机制省掉了一大堆XML配置Vue则负责前端页面组件化开发让页面逻辑清晰Element UI这类组件库直接能拼出一个管理后台的界面。我印象最深的是早年做SSH框架项目光配置文件就有五六份环境搭建都能折腾一整天。现在用IDEA新建一个Spring Boot项目选好依赖三分钟就能把这个项目的骨架搭出来。当然选型这件事不是越新越好尤其做医疗这类系统稳定、可维护、招聘市场上容易找到人干活这两点比任何花哨的技术栈都重要。1.2 这套系统的核心目标我要做的是基于Web的住院管理系统主要面向两类角色一类是护士站的护士和住院部的临床医生另一类是信息科或系统管理员。核心目标大致有三个维度第一患者全流程管理。从入院登记开始到病房分配、医嘱执行、费用记账、出院结算所有信息都记录在系统里每一步操作都留痕。第二信息实时共享。医生在办公室开的医嘱护士在护士站立刻能看到护士执行完医嘱系统自动回写状态医生也能知道执行的进度。第三数据统计能力。科室的住院人数、床位使用率、出院人数这些指标系统可以实时统计出来不用月底再翻报表手工汇总。这些听起来像一套标准化SaaS的功能介绍但真正自己从零做过一遍才会知道里面有多少细节要处理。比如住院号的编码规则、床位与患者的一对一关系、医嘱的类型区分长期医嘱和临时医嘱的停嘱逻辑完全不同、费用的单日汇总方式。这些细节我在后面的章节会逐一拆开讲。1.3 适合什么样的人参考这篇文章适合三类人直接拿走参考一是计算机相关专业做毕业设计的学生Spring Boot加Vue是目前Java方向毕设的主流搭配题目里带“住院管理系统”也比较好答辩二是刚入行不久、想练手一个完整前后端分离项目的初级开发通过这个项目能把CRUD之外的软技能——跨域处理、字段校验、分页查询、打包部署——全部过一遍三是医院信息科或小团队需要快速交付一套内部使用的住院管理工具可以直接把我的这套方案拿过去按需改。2. 技术架构与核心模块设计2.1 前后端分离架构这套系统采用前后端分离的架构后端只提供JSON格式的RESTful API不关心页面长什么样前端通过Axios调接口拿数据再渲染成表格和表单。这种架构的好处一句话就能说清楚前后端各自的职责边界清晰后端接口可以独立测试用Postman或Apifox就行前端页面可以独立开发调试两边并行推进互不阻塞。后端基于Spring Boot 2.7.x版本搭建持久层用MyBatis-Plus它相比原生MyBatis最大的优势是内置了通用Mapper单表的增删改查不用手写SQL代码量至少省三分之一。数据库用MySQL 8.0这是当前Java后端项目事实上的标准组合。前端基于Vue 2.6加Element UI组件现成对新手友好。这里要特意说明为什么不用Vue 3加Element Plus并不是Vue 3不好而是考虑到很多学校课程和网上教程还在用Vue 2而且Element UI的成熟度和资料丰富度在Vue 2生态里更高遇到问题更容易搜到解决方案。做项目最怕的不是技术旧而是在坑里爬不出来的时候搜不到答案。前端工程通过Vue CLI创建走标准的src目录结构按views、components、router、api、utils这五个维度组织代码。这样说可能有点抽象打个比方views是每个页面的整块拼图components是块拼图里可复用的碎片router是拼图的翻页目录api是前端跟后端对话的话筒utils是公共工具盒。这样划分之后新增一个页面基本就是“建一个view文件加一条route再加一组api方法”的机械节奏多来两次就能形成肌肉记忆。2.2 五个核心模块住院管理系统的业务模块我按功能边界拆成五个部分每个部分对应一个或几个Controller接口组患者信息管理入院登记、患者列表、住院详情、出院操作。这个模块是系统的数据入口所有后续业务都挂在患者ID下面。病房床位管理病区维护、病房列表、床位分配、转床操作。核心约束是同一时刻一张床只能归属一个在院患者。医嘱管理医嘱录入、医嘱审核、医嘱执行、停嘱操作。这里要区分长期医嘱和临时医嘱因为两种医嘱的时效语义和执行流程不一样。费用管理费用类型维护药品费、检查费、材料费、床位费、费用记账、费用退减、预交金管理、出院结算。这个模块最容易出乱子后面我会详细讲。系统管理用户管理、角色管理、菜单权限。前端路由菜单和后端接口权限要联动控制。每一个模块都不是孤立的。患者入院后自动锁定床位费用模块自动开始按天计算床位费医嘱开立后如果需要消耗药品费用同步记账。这些业务联动逻辑用事务进行控制比如“入院登记”这个接口既要插入患者记录又要更新床位状态为占用还要初始化预交金账户余额任何一个环节失败都要整体回滚避免出现“患者信息写进去了但床位还是空的”这种脏数据。2.3 权限模型设计权限这块我用的是经典的RBAC模型也就是按“用户-角色-权限”三级管理。默认内置三类角色管理员、医生、护士。管理员拥有全部菜单权限负责系统配置和用户管理医生可以管理医嘱和查看患者信息但不能操作费用记账护士负责医嘱执行、费用录入和床位管理。实现上后端用一个拦截器统一校验登录状态再通过自定义注解加Spring AOP做接口级别的权限控制。这里的思路是每个后端接口标注它需要的权限编码拦截器拿到当前登录用户的权限列表比对不上就返回403。前端则根据用户的角色动态生成菜单没有权限的菜单直接不显示。前后端双重控制既能避免页面白屏时暴露接口又能防止直接调接口绕过权限限制。3. 数据库设计与实现细节3.1 核心表结构设计数据库是整个系统的地基表结构设计的好坏直接决定后续开发的难易程度。这套系统我规划了以下核心表患者表patient、床位表bed、病区表ward、医嘱表medical_order、医嘱明细表order_item、费用记录表cost_record、预交金表deposit、用户表sys_user、角色表sys_role、用户角色关联表user_role。以患者表为例核心字段包括CREATE TABLE patient ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, patient_no VARCHAR(32) NOT NULL UNIQUE COMMENT 住院号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT COMMENT 性别:1男2女, age INT COMMENT 年龄, id_card VARCHAR(18) COMMENT 身份证号, phone VARCHAR(20) COMMENT 联系电话, diagnosis VARCHAR(255) COMMENT 入院诊断, admission_date DATETIME COMMENT 入院时间, discharge_date DATETIME COMMENT 出院时间, bed_id BIGINT COMMENT 当前床位ID, status TINYINT COMMENT 状态:1在院2已出院, create_time DATETIME COMMENT 创建时间, update_time DATETIME COMMENT 更新时间 );住院号patient_no的设计上建议用“日期流水号”的规则比如“20250112001”前八位是入院日期后三位是当天入院患者流水号。这样生成的住院号能直观看出入院时间也方便后续按住院号模糊搜索比自增ID更有人性化的表达力。床位表和病区表的设计思路是“区包含房、房包含床”的两级关联。ward表保存病区名称和位置信息bed表通过ward_id关联所属病区保存床号、床位状态。床位状态我用了三个值0空闲、1占用、2停用。转床操作的本质是事务里同时修改两张床的状态和患者记录里的bed_id这一块我在后面的实操章节会再展开。3.2 医嘱表的灵活设计医嘱表的设计是这套系统里比较关键的地方。一条医嘱可能包含多个药品项目比如“5%葡萄糖注射液500ml 维生素C 2.0g 维生素B6 0.2g”这种组合如果用一条记录直接存一个字符串后续统计药费几乎没法做。所以我拆成主表和明细表两张medical_order存医嘱的主信息——患者、医嘱类型、开立医生、开立时间、状态order_item存具体项目——药品ID或收费项目ID、单次剂量、频次、天数、单价、金额。这里有一个关键字段叫作“频次”通常用q.d.每日一次、b.i.d.每日两次、t.i.d.每日三次这类医疗术语表示。费用记账时系统需要把频次转成数量计算每日费用药品费用 单价 × 单次剂量 × 每日频次 × 天数。这个计算逻辑在临时医嘱和长期医嘱上有所不同长期医嘱按天滚动计费直到停嘱为止临时医嘱只计一次费用。所以我在order表里专门加了一个order_type字段1长期2临时费用模块生成账单时会根据这个字段走不同的时间维度。3.3 费用模块的账务约束费用和账务相关的设计我踩过一次比较大的坑。最初设计时费用记录表只有增加类型没有退款或冲正的概念。结果测试时发现护士录错一笔费用只能把金额改成0但审计时根本看不出这笔数据原本是什么、被谁改了、什么时候改的。后来我重新调整了cost_record表的设计加入biz_type字段标记这笔记录是1收费、2退费、3冲正并增加关联的source_id指向被冲正的那条原始记录。这样每一笔钱的来龙去脉都能追踪清楚系统上线后被问“这笔费用哪来的”时能直接查出来龙去脉省掉了大把扯皮的时间。预交金的逻辑也需要单独说一下。患者入院时预交一笔钱存入deposit表费用结算时不是直接扣除deposit余额而是汇总所有cost_record的总费用再用总费用减去预交金得到应补交或应退的金额。这样设计的好处是预交金账户和历史费用流水都能独立审计出院结算时只需算一次减法就能给出明确结论。4. 后端核心功能实现4.1 项目初始化与统一响应体后端项目用IDEA的Spring Initializr创建。创建时需要注意Spring Boot版本的选择这里我特意选了2.7.18也是2.x系列的最终版本。不选3.x的原因很简单3.x基于Jakarta EE命名空间javax改成jakarta部分老教程的代码直接复制会报错而且部分团队还在用的低版本MyBatis-Plus和3.x兼容性不理想。对于毕业设计和内部管理系统稳定省心的2.7.18是性价比最高的选择。创建好后pom.xml里核心依赖包括spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java和lombok。项目启动类不做特殊处理重点是写一个统一响应体Result类所有接口的返回都包一层格式为code、message、data。这是前后端分离项目的基础约定好处是前端Axios拿到的数据结构永远是同一个形状封装统一请求方法时可以一套代码走天下。我见过有的项目每个接口返回结构都不一样前端解析代码要到处写判断维护起来十分痛苦。所以这一点值得从最开始就定好规矩。4.2 MyBatis-Plus的配置与使用MyBatis-Plus在Spring Boot里的配置非常简单spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/hospital?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 mybatis-plus: mapper-locations: classpath:/mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case这个配置很关键它会把数据库字段的user_name自动映射成Java实体里的userName省掉大量ResultMap的书写。log-impl设置为StdOutImpl是为了开发阶段能在控制台直接看到执行的SQL语句排查问题的时候非常有用。实体类用Lombok的Data注解搭配MyBatis-Plus的TableName和TableId注解标注映射关系。这里我特别提醒一点MyBatis-Plus默认的主键策略是ASSIGN_ID会生成雪花算法的纯数字长ID。但部分场景下比如表的主键需要自增且依赖MySQL的auto_increment必须显式在TableId里标注type IdType.AUTO否则插入数据时会看到主键值完全不是预期结果。曾经帮朋友排查过一个问题数据插进去了但主键ID和数据库自增ID完全是两回事最后就是主键策略没配置好。4.3 患者入院登记的完整实现入院登记这个接口是整个系统里事务性最强的一个接口。它的完整流程是校验患者身份证号是否已有在院记录防止重复入院、生成住院号、插入patient记录、更新床位状态为占用、初始化预交金账户。我用Transactional注解包住整个方法任何一个环节抛异常就整体回滚。事务里有一步抛异常导致患者数据写了一半的情况在真实场景中并不少见最开始联调时我就碰上过“患者建档成功但床位没有锁定”的情况查了好久才定位到是事务没有生效——因为自调用导致Spring AOP代理失效。这个方法如果是同类内部方法直接调用Transactional是不会走代理的这是Spring事务机制里最容易踩的暗坑。核心代码示意Transactional(rollbackFor Exception.class) public PatientVO admission(AdmissionRequest request) { // 1. 检查患者是否已在院 Patient existing patientMapper.selectOne( new LambdaQueryWrapperPatient() .eq(Patient::getIdCard, request.getIdCard()) .eq(Patient::getStatus, 1)); if (existing ! null) { throw new BizException(该患者当前已在院请勿重复办理入院); } // 2. 生成住院号 String patientNo generatePatientNo(); // 3. 插入患者 Patient patient new Patient(); BeanUtils.copyProperties(request, patient); patient.setPatientNo(patientNo); patient.setStatus(1); patient.setAdmissionDate(new Date()); patientMapper.insert(patient); // 4. 锁定床位 Bed bed bedMapper.selectById(request.getBedId()); if (bed null || bed.getStatus() ! 0) { throw new BizException(床位不存在或已被占用); } bed.setStatus(1); bed.setPatientId(patient.getId()); bedMapper.updateById(bed); // 5. 初始化预交金 Deposit deposit new Deposit(); deposit.setPatientId(patient.getId()); deposit.setTotalAmount(request.getPreDeposit() null ? BigDecimal.ZERO : request.getPreDeposit()); depositMapper.insert(deposit); return convertToVO(patient); }4.4 分页查询的两种写法患者列表是页面最常用的接口分页查询是必做的。MyBatis-Plus提供两种分页方式一种是使用内置的分页插件IPagePatientVO page new Page(current, size); LambdaQueryWrapperPatient wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(keyword), Patient::getName, keyword) .eq(Patient::getStatus, status) .orderByDesc(Patient::getAdmissionDate); IPagePatient result patientMapper.selectPage(page, wrapper);另一种是自定义SQL搭配分页参数。如果查询需要关联多张表比如患者列表需要显示当前病区名称和床号就要写Mapper XML里的联表查询然后用MyBatis-Plus的Page参数直接传入。用法是Mapper接口的方法签名带上IPage参数MyBatis-Plus会自动拦截拼接LIMIT语句具体来说就是IPagePatientVO selectPatientPage(IPagePatient page, Param(keyword) String keyword, Param(status) Integer status);这里要注意XML里的SQL不要手写LIMIT否则分页参数会冲突。这也是不少新手容易疑惑的地方明明只返回了一页数据总数total却是0就是因为手写了LIMIT导致自动分页逻辑错乱。4.5 自定义异常与全局异常处理后端除了写正常的业务逻辑还有一个必须从一开始就规划好的东西——异常处理。我在项目里定义了一个BizException运行时异常类业务代码里遇到校验失败直接用throw new BizException(该患者当前已在院请勿重复办理入院);同时写了一个RestControllerAdvice全局异常处理器捕获所有异常并做统一处理。具体来说BizException返回code为500的提示信息方法参数校验异常返回400参数错误兜底的Exception返回“系统繁忙请稍后重试”。这样做的好处有三点第一前端不用在每个接口的catch里做错误提示Axios响应拦截器里统一弹提示即可第二后端不会把堆栈信息直接暴露给前端安全性更高第三错误提示的文案能控制在需求方想要的语气而不是满屏的HTTP 500。5. 前端核心功能实现5.1 前端工程搭建前端这块我用Vue CLI创建项目vue create hospital-web创建时选择Vue 2模板安装路由vue-router、状态管理vuex这个项目用不上就直接跳过、HTTP库axios、UI组件库element-ui。npm安装依赖时最容易踩的坑是版本不兼容。比如element-ui和vue2版本是配套的但如果npm默认安装了element-ui的最新版本而项目里vue的版本过低运行起来就会出现各种组件样式错乱。所以创建项目后第一件事就是检查package.json里的依赖版本。如果对自己要用的版本没有把握直接按固定版本安装例如npm install element-ui2.15.13前端项目结构按views、components、router、api、utils五个目录组织。api目录下每个模块一个文件比如patient.js、order.js、cost.js统一封装接口调用页面组件里不直接写axios请求只调用api模块导出的方法。这套分层的好处是接口路径集中管理后端路径一改前端只改一个文件不用全项目搜索替。5.2 登录页面与路由守卫登录页面用Element UI的表单组件加表单校验用户名密码通过axios的post请求提交到后端/api/auth/login成功后把token保存在localStorage里同时把用户信息和角色信息存进Vuex。axios的请求拦截器会从localStorage里取出token统一放到请求头的Authorization字段里响应拦截器则统一处理HTTP 401状态token过期或非法时直接清除本地登录信息并跳转到登录页。路由守卫是Vue Router提供的一个前置钩子函数每次路由跳转前都会执行。我在里面做了两件事判断用户是否已登录——未登录直接redirect到/login已登录则根据角色动态判断该路由是否在可用菜单列表里没有权限就跳转到403页面。这套机制能挡住绝大多数“直接在地址栏输入URL跳过菜单”的尝试但严格来说前端的路由守卫只是体验层面的控制真正的数据安全必须靠后端接口权限这一点做任何管理系统都需要反复强调。5.3 患者列表页面的实现思路患者列表页面采用“表格搜索条件分页”的标准后台布局这在Element UI里有很多现成组件el-table、el-form、el-pagination。页面的核心逻辑在methods里的loadData方法组装查询条件对象调用api模块的getPatientList方法拿到后端返回的数据列表和total总数赋值给data里的表格和分页组件。async loadData() { this.loading true; const params { current: this.pagination.current, size: this.pagination.size, keyword: this.searchForm.keyword, status: this.searchForm.status }; const res await getPatientList(params); if (res.code 200) { this.tableData res.data.records; this.pagination.total res.data.total; } this.loading false; }搜索按钮和重置按钮的逻辑分别是修正current为1后重新调用loadData和清空搜索表单后重新调用loadData。这里有一个细节容易被忽略切换页码时如果搜索条件没有清空但页码不在第一页很容易出现“切到第5页但搜索条件变了结果一页数据带不出来”的现象。所以我在重置搜索时强制把current重置为1页码切换时保持当前搜索条件组装参数即可。5.4 Vue播放m3u8的小技巧做住院管理系统时有客户额外提了一个需求需要在系统里回放病房监控的视频流视频源是m3u8格式的直播流地址。这个需求跟患者管理本身没有直接关系但既然前端是Vue就顺手把这块也实现了一下。播放m3u8最常用的方案是video.js加videojs-contrib-hls插件。要注意的是videojs-contrib-hls这个插件在新版本里已经和video.js的集成方式有变化安装时如果不小心装到最新版本可能出现“Cannot read property handler of undefined”之类的报错。我的建议是安装时固定video.js版本为7.x插件版本为6.x。页面里在mounted生命周期里初始化videojs实例同时在onbeforeunload时销毁实例避免切换路由后视频继续播放占资源。6. 常见部署问题与解决方案6.1 跨域问题前后端分离项目第一个遇到的大概率就是跨域。前端的Vue开发服务器默认跑在8080端口后端Spring Boot跑在8081端口两个端口不同浏览器的同源策略就会拦截请求。解决办法有三种第一种是后端写一个跨域配置类实现WebMvcConfigurer的addCorsMappings方法允许指定来源访问第二种是前端的devServer配置proxy代理把/api前缀的请求转发到后端地址第三种是生产环境直接用Nginx反向代理把前端请求和后端接口放在同一个域名路径下。开发阶段推荐用第二种方式配置链路最短且不用动后端代码生产环境必须用Nginx方案之前项目就是直接用跨域头硬上后来发现部分网络环境的OPTIONS预检请求被拦排查起来非常折腾。6.2 Vue打包后刷新404问题Vue Router默认使用history模式路由路径是干净的URL没有#号。但history模式有个坑打包部署到Nginx后如果用户直接访问或刷新一个子路由地址比如访问http://localhost:8080/patient/listNginx会在磁盘上找这个路径对应的文件找不到就返回404。解决办法是在Nginx配置里加一行location / { try_files $uri $uri/ /index.html; }它会把所有不存在的路径重写到index.html由Vue Router接管并匹配对应的路由页面。这个问题属于打包部署环节的必修课我在README里也特别注明了。还有一种关联问题是“Vue打包后布局异常”最常见的原因是打包后静态资源的路径不对在Vue CLI的vue.config.js里把publicPath设置为相对路径./而不是默认的/这样部署到任意子目录下资源路径都不会出错。6.3 常见异常排查方法后端Spring Boot启动报错是最影响新手心态的问题其实大部分错误都能从控制台的错误栈里找到答案。比如启动时提示“Field patientMapper in xxx required a bean of type that could not be found”十有八九是启动类上忘了加MapperScan注解或者Mapper接口没有标注Mapper。再比如启动时提示“Access denied for user”那是数据库连接配置的用户名密码或IP端口有问题先ping一下数据库IP通不通、再核对一遍配置文件的密码基本能解决一大半问题。遇到启动报错时我给新人的建议是第一不要慌把第一行红色的ERROR信息和紧跟着的Caused by读出来第二去搜索引擎直接粘贴Caused by后面的异常信息基本能找到现成答案第三善用IDEA的断电调试前后端联调时重点检查请求是否到达后端、参数是否有值、返回的数据结构是否符合预期。很多时候前端页面白屏是因为接口返回报错但页面没有做错误状态展示打开浏览器的F12控制台看Network和Console两个面板基本能定位问题方向。我做个简单的排查表格供参考现象可能原因排查方向前端调接口直接CORS报错后端跨域未配置或代理配置错误检查后端CORS配置类、前端proxy配置请求返回404后端接口路径不对或服务未启动检查Controller的RequestMapp映射、后端启动状态请求返回401token未传或已失效检查Axios拦截器是否设置token、本地存储是否过期请求返回500后端代码异常查看后端控制台打印的异常栈定位具体代码行后端能跑但SQL报错表字段名或表名不对检查实体类和数据库表是否对应、字段映射是否开了驼峰转换打包后页面白屏静态资源路径不对检查publicPath配置、Nginx站点根目录是否正确6.4 关于版本选型的一点体会项目做完后回看整个开发过程最想提醒大家的是版本管理这件事。Spring Boot 2.7.18搭配MyBatis-Plus 3.4.x、MySQL 8.0、Vue 2.6加Element UI 2.15.x这套组合经过大量项目验证互相之间的兼容性是没什么大问题的。反观有的同学一上来就装最新版Spring Boot 3.2配MyBatis-Plus还没适配Vue 3配Element UI但网上教程大部分是Vue 2的写法折腾半天光环境就劝退了。做项目之前花五分钟把版本对应关系查清楚比埋头写代码重要得多。7. 部署与上线7.1 后端部署后端打jar包部署到服务器这是目前最常见的开发部署方式。在IDEA右侧Maven面板里执行package命令或者在项目根目录执行mvn clean package -DskipTests打包完成后target目录下会生成一个hospital-server-0.0.1-SNAPSHOT.jar。部署到服务器时用nohup java -jar hospital-server-0.0.1-SNAPSHOT.jar --server.port8081 app.log 21 这里-DskipTests是因为测试类可能会在打包时执行而本地环境的测试数据库连接在服务器上可能不可用跳过测试避免整个打包失败。日志重定向到app.log方便上线后排查问题。7.2 Nginx部署前端前端部署的核心是Nginx的静态文件服务加反向代理。一般流程是本地执行npm run build生成dist目录把dist目录里的全部文件上传到服务器的/www/hospital-web目录下然后在Nginx的server块里配置server { listen 80; server_name your-domain.com; root /www/hospital-web; index index.html; location / { 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里的proxy_pass最后加一个斜杠会把/api/前缀去掉然后转发到后端。比如前端请求/api/auth/login实际转发到后端的地址就是http://127.0.0.1:8081/auth/login。如果后端接口的Controller里没有加/api前缀这种配置方式就能省掉前后端请求路径风格不一致的问题。测试时记得先重启Nginx让配置生效很多人改了配置没reload就直接说“不生效”这是运维环节最常见的低级失误。7.3 一台Nginx部署多个Web项目如果服务器上同时部署了住院管理系统和另一套独立的运维系统并且由同一个Nginx管理就需要用不同路径区分。举个例子住院管理系统放在/hospital路径下另一套系统放在/ops路径下。前端打包时publicPath需要配置为/hospital/确保index.html里引用的JS和CSS资源路径带上前缀Nginx配置则写成server { listen 80; server_name your-domain.com; location /hospital { alias /www/hospital-web; try_files $uri $uri/ /hospital/index.html; } location /ops { alias /www/ops-web; try_files $uri $uri/ /ops/index.html; } location /api/ { proxy_pass http://127.0.0.1:8081/; } }这里用alias而不是root是为了让URL路径和设备实际存放的目录脱钩灵活性更高。多项目共享一个Nginx的场景在公司环境里非常常见搞清楚这套配置后续加新系统就是复制粘贴改个路径的事。8. 项目优化的几个方向8.1 缓存优化住院管理系统里的患者列表和床位状态是高频查询数据每次进入页面都要等接口返回体验上还能接受但并发上来之后数据库压力会变大。后续优化时可以引入Redis做缓存床位状态用Redis的Hash结构存储每次更新床位时同步更新缓存患者列表的查询结果按条件缓存10秒短时间内重复查询直接命中缓存。这一个简单的改造就能把查询接口的响应时间从200ms压到20ms以内。不过要提醒的是缓存与数据库的一致性维护需要额外写代码对于内部住院系统这类数据变更频率不高的场景缓存收益是值得的。8.2 导出功能管理层基本上都会提出“把今天的住院名单导成Excel”的需求。后端可以用EasyExcel这个工具类库快速实现三步搞定定义实体类和Excel列的映射组装数据列表调用EasyExcel.write方法输出到HttpServletResponse的输出流。这个功能写一次能复用到多个列表。值得注意的问题是导出时数据量过大容易内存溢出所以要先分页查询再分批写建议每5000条刷一次。8.3 对接外部系统如果后续需要对接医院的HIS系统医院信息系统或者LIS系统检验信息系统最常用的方式是在后端增加WebService或HTTP接口供外部系统调用。设计时要注意接口调用的幂等性比如外部系统重试多次推送同一个患者的检验结果系统不能重复插入数据。简单做法是在接口里按患者ID加检验单号做唯一约束重复数据直接忽略或返回已存在标识。这块虽然短期内用不上但做架构设计时预留好扩展位后面接问题时就不会手忙脚乱。从第一步创建项目到最后部署上线我实际用下来单人开发完整做完这套住院管理系统大约需要两周时间其中数据库设计和核心业务逻辑各占三分之一时间剩下三分之一花在前后端联调和部署踩坑上。中间肯定会碰到各种边界问题但每解决一个坑对这个系统背后“业务要理顺、数据要闭环、权限要分明”这三点会有更深的体会。如果你正准备动手做类似的系统我建议先花一下午把医院的业务流程图手工画出来把入院的路径、转床的路径、医嘱和费用关联的路径都走通再开始写代码这个时间安排比急急忙忙建工程要值得得多。