接手这个“企业级社区医院管理系统”项目时我第一反应是终于不是那种花架子演示项目了。市面上打着SpringBootVue旗号的开源系统很多但真正能从挂号、处方、药品库存一路跑到统计报表还能在社区医院那种“设备一般、网络一般、人手也一般”的真实环境里稳定跑起来的并不多。这篇就把我实际搭建和改造这套系统过程中的设计选择、踩坑经历、代码落点一次说清楚给正在做同类项目的朋友做个参考无论你是拿来学习还是准备二次开发都会有用。1. 项目定位社区医院的业务流程和这套技术栈为什么匹配1.1 社区医院的信息化痛点社区医院的业务体量比三甲医院小得多但业务流程一点都不少。挂号、门诊、收费、药房发药、药库管理、检验检查、健康档案、慢病随访这些环节该有的都有只是患者量和数据规模相对可控。也正因为规模不大很多社区医院的信息化系统反而做得粗糙不少地方还在用单机版软件或者依赖区级平台下发数据医生开完处方后药房还要手动录一遍药品信息。这种场景下做管理系统核心目标不是“支撑超大规模并发”而是把流程串起来患者在窗口挂号后诊间医生能立刻看到候诊信息开方时自动带出药品库存收费完成后药房能同步收到待发药列表药库能根据消耗量做库存预警。这个闭环一旦打通社区医院的人手压力会明显下降。我之前见过一个真实的低效案例某社区卫生服务中心日门诊量在500人次左右药房只有两个人每天下午要花两个小时手工核对处方和库存账目月底盘点误差率达到百分之三。手工模式下想精确掌握药品效期和滞销品种基本靠运气。社区医院管理系统要解决的就是这类实际运营痛点而不是做出一个看着很炫但落地无门的“数字大屏”。1.2 技术栈选型SpringBoot Vue MyBatis MySQL的组合逻辑这套组合在社区医院这个场景里不是追新潮而是考虑到了项目建设、交付、后期维护全周期的现实问题。SpringBoot的好处不必多说内嵌Tomcat、自动配置、生态成熟。社区医院信息科普遍没有专门的后端运维人员不会有人去折腾WebLogic或者JBoss一个java -jar就能把服务拉起来出了问题恢复也快。所以SpringBoot作为后端基础框架几乎没有争议。Vue作为前端方案选型时我看重的是它的渐进式和生态成熟度。社区医院管理系统涉及大量表单页面、数据录入界面Vue的数据双向绑定能让这类交互开发效率高很多。而且Element UI这类组件库对表单场景的支持非常完整表格分页、弹窗校验、日期范围选择开箱即用。相比ReactVue在国内医疗信息化圈的认知度更高后续接手的开发者更容易看懂。MyBatis在这个项目里是有讲究的。医疗系统的查询逻辑复杂值班日志、库存流水、处方明细、科室收入统计动不动就是多表连锁查询联表条件还经常根据统计口径变化。MyBatis把SQL写在XML里改动时不需要重新编译Java代码对于上线后频繁调整统计SQL的场景太合适了。相比JPA那种偏自动化的ORMMyBatis虽然要多写一些XML但胜在可控性强SQL性能出了问题一眼就能定位。MySQL的选择就更现实了。社区医院的数据量级撑死也就是日均几百到几千笔业务流水年数据量在百万级上下。MySQL 8.0在事务、索引、查询优化器上已经足够扎实关键是完全开源免授权费配合主从备份就能满足中小型医院的数据安全需求。没必要为了“显得专业”引入Oracle或者SQL Server那种授权和维护成本对社区医院来说就是灾难。1.3 模块拆分与部署形态我把整个系统按业务域拆成了以下模块模块核心功能主要角色系统管理用户、角色、菜单、字典、日志系统管理员挂号管理挂号、退号、候诊队列、号源维护导诊台、挂号收费员门诊医生站接诊、电子病历、处方开立、检查检验申请门诊医生收费管理划价收费、退费、费用查询、日结收费员药房管理发药、退药、处方核对、库存查询药师药库管理入库、出库、盘点、采购计划、效期预警药库管理员统计报表门诊量、收入、科室工作量、处方统计院长、管理员部署形态上我建议直接采用“后端单体服务 前端静态页面 MySQL数据库”的经典组合而不是一上来就上微服务。原因很直接社区医院的IT人员配置有限微服务体系拆出一堆SpringCloud组件出了问题没人能排查。单体服务把代码模块化做好将来真要拆分SpringBoot项目按module边界拆也容易前期过度设计等于给自己挖坑。2. 数据库建模核心业务表和权限模型的设计思路2.1 基础数据表从组织架构到药品字典医疗系统的数据建模最忌讳上来就写业务流水表。基础数据不敲定后面所有关联查询都是空中楼阁。社区医院虽然机构不大但组织架构、人员信息、科室设置这些主数据一样都不能少。组织机构和人员这块我做的是医院表、科室表、用户表三张主表。医院表在最顶层科室表通过parent_id支持树形结构这样既能表达“内科—呼吸内科”这种分级也能支撑后续按院区扩展。用户表关联科室同时保存身份证号、联系电话、执业资格证号这些基础字段。有一个容易被忽略的点医疗系统里的用户通常不是单纯系统账号而是“人”所以用户表里我会加一个work_no工号字段和登录账号区分开未来对接医保接口时直接用工号与医保系统映射。药品字典是另一个关键的基础表。很多人做药品管理只存药品名称和规格实际跑起来才发现远远不够。我在药品字典里至少维护了字段组具体字段说明标识信息药品编码、通用名、商品名、剂型编码是院内唯一标识规格信息规格、单位、生产厂家用于处方划价和库存计数管理属性处方类型、是否抗菌药、医保类别社区医院对抗菌药处方有严格比例限制库存属性当前库存量、库存上限、库存下限库存下限用于自动预警药品编码的生成有讲究最好使用“分类码流水号”的结构。比如01开头代表西药02代表中成药03代表中药饮片这样后续做报表按编码前缀分组就能直接统计不同药品类别的消耗情况。我在实际项目中就看到过有人把药品编码做成纯自增数字结果做统计报表时不得不额外关联分类表白白增加一次联表查询。2.2 核心流程表挂号、处方、收费、发药的链路设计基础表建好后业务流水表的设计才是真正决定系统质量的部分。社区医院最核心的链路就是挂号 → 就诊 → 开方 → 收费 → 发药。这一步我拆成了多张表每张表承担明确的业务职责。挂号流水表需要记录的就诊日期和就诊序号。为什么把序号单独拿出来因为临床上每个医生每天接诊的患者都是按序排队患者挂号单上打印的“上午第15号”就是从这个序号来的。这张表同时要冗余患者姓名和科室名称虽然违反了第三范式但在发药、统计场景里能减少大量联表查询。社区医院系统数据量不大冗余带来的存储成本几乎可以忽略但查询性能提升是实实在在的。处方设计这里有个常见的错误把一条处方记录设计成一条完整数据包含所有药品字段。这种拍脑袋设计在发药环节会直接卡死。正确思路是处方主表和处方明细表分开主表存患者、医生、科室、处方类型西药/中成药/中药饮片、处方状态明细表存每条药品的药品编码、数量、用量、用法、单价、金额。这样做的核心意义在于一张处方可能包含多种药品发药环节往往因为缺货只发其中一部分必须能针对明细做部分发药和部分退药。收费表和处方之间通过business_id关联而不是直接外键关联到处方表。因为挂号费、检查费也可能成为收费记录如果把收费表就写成“处方收费表”后面扩展就僵住了。我这里的做法是设计一张patient_fee记录包含费用类型字段挂号/诊查/药品/检查/检验再通过biz_id关联具体业务单号。设计层面这叫“超级表关联模式”扩展性非常强。发药表相对简单核心就是记录处方明细里哪些药品已发、发了几份、由谁发、什么时间发。值得一提的是发药操作必须记录操作人。社区医院药房一旦发生药物发放错误责任追溯是刚需这个字段将来就是事故追踪的依据。2.3 权限模型RBAC和菜单权限的落地细节权限设计我直接采用了经典的RBAC模型用户表、角色表、菜单表、用户角色关系表、角色菜单关系表一共五张表。社区医院的角色相对固定主要就是系统管理员、挂号收费员、药剂师、医生、护士、院长。但每个角色能访问什么数据、操作什么按钮必须能精确配置。让我特别说明一下“按钮级权限”。很多系统的权限只做到菜单级结果就是某个收费员能进入“系统管理”页面看到所有用户列表只是不能操作而已这在医疗系统里是不可接受的。菜单只要可见页面加载时就会请求该页面的接口数据等于把敏感数据暴露给了未授权角色。所以我在角色菜单关系表里不只有menu_id还有一个action字段取值包括view/add/edit/delete/export。前端根据登录用户的权限列表中是否包含对应action决定按钮是否渲染后端在接口上必须同步校验。2.4 MySQL设计细节字符集、索引和事务隔离级别字符集这一项直接决定系统会不会在中文数据上翻车。我全部采用utf8mb4而不是utf8mb3。千万别用utf8那实际上是utf8mb3只能存基本的多语言字符存不了emoji和一些生僻汉字。患者名字里出现生僻字的情况在医疗场景一点都不少见某些药品说明里的特殊符号也会要求4字节字符集。索引设计上我基本上遵循了以下原则所有外键关联字段必须建索引比如patient_id、doctor_id、dept_id高频查询的时间范围字段必须建索引比如挂号流水表的visit_date联合索引优先考虑“等值字段在前范围字段在后”的规则不轻易给status这类低区分度字段单独建索引事务隔离级别我用的是MySQL默认的REPEATABLE READ。这里有个关键的坑在REPEATABLE READ下多个并发事务同时扣减药品库存时直接用UPDATE inventory SET stock stock - 1 WHERE drug_id ?这种语句在InnoDB行锁机制下是安全的因为行锁会让后到的更新事务等待前一个事务提交。但如果是先SELECT出来判断总量再UPDATE就会产生超卖风险。所以我在扣库存时坚决采用“一次UPDATE语句完成判断和更新”的写法UPDATE drug_inventory SET stock stock - #{num} WHERE drug_id #{drugId} AND stock #{num}如果影响行数为0说明库存不足直接抛出提示。这个句式的巧妙之处在于数据库层面的行锁和条件判断一起保证了不会超卖不需要引入分布式锁这种重武器。3. 后端落地SpringBoot MyBatis 的工程实践3.1 后端工程结构和MyBatis配置要点分层结构我按社区医院系统的技术栈整理为经典的四层结构。用GroupId ArtifactId来说明更直观com.chinacommunity.hospital ├── controller // 接口层接收参数返回统一Result对象 ├── service // 业务层事务边界在这里 ├── mapper // MyBatis数据访问层接口类 ├── entity // 数据库表映射实体类 ├── dto // 接口入参/出参对象 ├── config // MyBatis、缓存、WebMvc等配置 ├── security // 登录认证、权限校验相关 └── common // 统一返回、全局异常、工具类这里有一个实践经验entity和dto必须分开不要直接拿数据库实体返回给前端。原因很简单数据库表结构不能直接暴露给前端尤其用户表里的密码字段如果序列化时忘了加JsonIgnore密码就直接发给浏览器了。另外前端需要的字段往往需要多表聚合比如挂号记录里要带患者姓名和科室名这在entity里根本没有必须用dto承接查询结果。MyBatis的配置里有几个参数我每次都要强调。先说map-underscore-to-camel-case这个参数设为true后数据库表字段create_time会自动映射到实体的createTime属性省去大量手写resultMap的动作。如果数据库字段命名规范是下划线风格这个开关必须开。另外log-impl要配置为StdOutImpl而不是Slf4jImpl开发阶段能直接在控制台看到完整SQL语句和参数排查问题会方便很多。再说typeAliasesPackage很多人图省事不配。不配的话在XML里写resultType时只能写全限定类名com.chinacommunity.hospital.entity.DrugInfo又长又丑。配置了这个包扫描后直接写DrugInfo就可以了。3.2 自定义Mapper XML与业务层事务MyBatis的使用界面上我一直坚持“简单操作用注解复杂查询用XML”的原则。社区医院系统中像根据主键查询实体、单条插入这类操作用Select、Insert注解完全没有问题代码量少且直观。但涉及多表关联、动态条件时必须进XML因为注解里写动态SQL实在太难受。拿门诊工作量统计这个需求举例要统计某天每个医生的挂号量、处方量、处方总金额筛选条件可能有科室、时间段、医生姓名、是否只看初诊患者。这种查询在XML里用动态SQL组织起来非常灵活select idcountDoctorWorkload resultTypemap SELECT d.real_name AS doctorName, COUNT(DISTINCT r.id) AS registerCount, COUNT(DISTINCT p.id) AS prescriptionCount, IFNULL(SUM(CASE WHEN p.status 2 THEN p.total_amount END), 0) AS totalAmount FROM doctor_info d LEFT JOIN register_record r ON r.doctor_id d.id AND r.visit_date BETWEEN #{startDate} AND #{endDate} LEFT JOIN prescription p ON p.doctor_id d.id AND p.create_time BETWEEN #{startDate} AND #{endDate} where if testdeptId ! null AND d.dept_id #{deptId} /if if testdoctorName ! null and doctorName ! AND d.real_name LIKE CONCAT(%, #{doctorName}, %) /if /where GROUP BY d.id, d.real_name ORDER BY totalAmount DESC /select业务层的事务划分我的经验写在下面挂号操作开一个事务包括写挂号流水、更新号源余量、生成候诊队列记录开处方操作开一个事务包括写处方主表、处方明细表、更新患者就诊状态收费操作开一个事务包括写收费记录、更新处方状态、调药品库存扣减发药操作单独开事务写发药记录更新处方明细状态为什么收费和发药要分开事务因为社区医院的实际流程里收费后可能因为药品缺货发药环节要拖到下午才有药师执行。如果收费和发药是一个事务收费后马上要扣库存库存不足时整个收费用不了但药房实际上可以等下一批药品入库后再发货完全没必要让收费阻塞。这种业务判断只有在现场待过才会懂。3.3 登录认证与接口权限登录认证这块社区医院系统不需要做成超级复杂的单点登录体系JWT Token方案完全够用。我在项目中用的是jjwt库登录成功后生成Token包含userId、userName、roleIds过期时间设置为8小时刚好覆盖一个白班的工作时长。下班后Token过期第二天重新登录简单实用。Token校验逻辑放在SpringBoot的拦截器里。写一个AuthInterceptor实现HandlerInterceptor接口在preHandle方法里从请求头Authorization取出Token解析成功后把用户信息放入ThreadLocal再通过HandlerMethod获取接口上标注的RequirePermission注解比对用户权限集合做校验。这套代码不复杂但社区医院项目里医生、护士、药房、收费员、管理员五种角色每个接口到底谁能调、谁不能调必须落在代码里光靠前端隐藏按钮是不够的。有个细节登录接口本身不能走这个拦截器否则用户没登录就无法登录了。我在拦截器里加了一个不用鉴权的路径白名单数组存放/login、/captcha这些公开接口。另一个细节是登录失败不能只返回401状态码还要返回统一结构的错误信息否则前端难以统一处理错误提示。3.4 分页查询与SQL优化社区医院系统的数据查询有个特点单表数据量不算大但查询条件花哨、经常需要联表。最典型的就是“流水查询”页面患者姓名、时间范围、操作员、科室四个条件组合一查会导致SQL里多个LEFT JOIN。分页我用的是PageHelper插件这个小工具原理上是拦截Executor的query方法在执行前改写SQL为带LIMIT的语句然后自动发一条COUNT查询统计总数。使用上有几个硬性注意点PageHelper.startPage()后面必须紧跟第一条查询语句否则分页条件就串到别的查询上了第二条查询语句返回的必须是Entity或DTO不能是Map因为Map类型无法靠谱统计总数。SQL优化方面分享一个高频场景查询今日待发药处方时原来的写法是SELECT p.*, u.real_name AS patient_name, d.drug_name FROM prescription p LEFT JOIN user_info u ON p.patient_id u.id LEFT JOIN prescription_detail pd ON pd.prescription_id p.id LEFT JOIN drug_info d ON pd.drug_id d.id WHERE p.status 1 AND p.visit_date 2025-01-15这个查询的问题在于一张处方有5种药就会返回5行要获取“处方患者”本身就是重复数据。优化后先只查处方主表和患者SELECT p.*, u.real_name AS patient_name FROM prescription p LEFT JOIN user_info u ON p.patient_id u.id WHERE p.status 1 AND p.visit_date ?然后在Java业务代码里循环查询处方明细或者用一条IN查询把多个处方ID的明细一次查出来SELECT * FROM prescription_detail WHERE prescription_id IN (...)这样从“N行重复数据”变成“1行主记录N行明细”前后端传输的数据量大幅下降前端渲染表格也不会出现重复行。看似简单的调整实际体验会有明显差异。4. 前端落地Vue后台管理系统的搭建与业务页面4.1 Vue项目初始化和动态路由设计前端部分我使用的是Vue 2 Element UI的组合不是Vue 3。选择Vue 2的原因很直白社区医院系统大量使用的现成后台管理模板、教程、二次开发示例都是基于Vue 2的Element UI的组件稳定性和文档成熟度也更高对于这种一个项目要做很多年的管理系统稳定压倒一切。如果你打算用Vue 3那配Element Plus也可以但团队培训成本、踩坑成本都会明显上升。创建项目方式我推荐用vue-cli 5而不是现在流行的Vite。这里请听我说完Vite虽然启动快但在老项目里可能遇到Node版本兼容问题还有它对CommonJS生态的一些处理不太友好。vue-cli 5虽然启动慢个几秒但构建后的兼容性和稳定性都很可靠适合医疗这种偏保守的行业场景。路由这一块我做了投标路由。侧边菜单是根据当前登录用户的权限从后端接口动态获取完全没有写死在路由表里。实现方式是路由表只保留login、home、404、403这几个固定路由登录成功后请求菜单接口拿到用户有权限的菜单树再通过Vue Router的addRoutes方法动态挂载。这样不同角色登录后看到的菜单完全不同收费员看不到药房菜单医生看不到系统管理菜单既安全又清爽。4.2 Axios请求封装与Token处理整个项目所有接口请求都走封装好的Axios实例。下面这段核心代码我放在utils/request.js里每一位新接触这个项目的人都要先读这段代码import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: /api, timeout: 15000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }, error { return Promise.reject(error) }) service.interceptors.response.use(response { const res response.data if (res.code 200) { return res } if (res.code 401) { localStorage.removeItem(token) router.push(/login) return Promise.reject(new Error(登录状态已过期)) } Message.error(res.message || 请求出错) return Promise.reject(new Error(res.message)) }, error { Message.error(error.message || 网络异常) return Promise.reject(error) }) export default service这段封装里有三个点值得细说。第一点API请求超时设置成了15秒社区医院偶尔网络不稳定特别是老院区的网络改造期间超时设置过短会导致医生开处方连续报错患者排队不耐烦这个体验损失非常大。第二点后端统一返回格式约定为code/message/datacode为200表示成功为401表示Token失效前端只做统一拦截业务细节由各页面自行判断。第三点baseURL用的是/api而不是具体IP部署时通过Nginx反向代理转到后端服务端口这样前端代码里永远不写死后端地址环境切换时只改Nginx配置就好。4.3 医院业务页面的组件化设计选Element UI组件库后业务页面的开发速度会起飞但组件该封装还是要封装。以患者信息组件为例我在很多页面里都需要选择患者挂号页面要填患者开方页面要选患者收费页面也要带出患者信息。所以我把“患者搜索选择器”封装成一个独立组件包含姓名、身份证号、手机号三个查询条件结果呈现为表格选中后通过事件传出患者对象。这个组件在挂号、收费、开方、检验申请四个模块里复用改动一次全局生效。处方药品选择器的封装也很有必要。这个组件内部绑定药品字典数据输入药品名自动匹配带出规格、单位、零售价加入处方明细后自动计算小计金额。最关键的是药品库存显示当库存小于处方数量时组件会立即标红并阻止提交这个交互挽救过不止一次发药时的短缺尴尬。另外一个容易被忽略的是M3U8视频播放和PDF报告预览需求。社区医院现在也做健康宣教视频点播医生上传的健教视频通常是M3U8格式浏览器默认不支持播放我用video.js接入hls.js解决代码不超过20行。检验报告导出成PDF前端直接用pdf.js预览完全不需要后端生成图片再返回。这些看似边缘的功能实际落地需求非常旺盛。4.4 部署上线Vue打包放进SpringBoot社区医院的服务器配置通常不高有的甚至不给独立IP这时“前端打包进后端”的部署方式非常实用。把Vue项目build生成的dist目录直接复制到SpringBoot的src/main/resources/static目录下启动SpringBoot后浏览器直接访问后端端口就能看到前端页面。需要注意一个关键配置前后端分离部署时前端请求的/api路径要在后端上做重写。SpringBoot里加上下面这个配置类就能解决Configuration public class WebConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/).setViewName(forward:/index.html); } }同时Vue项目里的baseURL要写成相对路径/api而不是绝对路径http://localhost:8080/api。这样换服务器、换端口都不用重新改代码。如果是生产环境有独立前端服务器则用Nginx把前端静态页面和后端接口分开代理。这时候注意跨域问题最稳妥的做法是在Nginx层直接配置反向代理避免前端代码里开跨域server { listen 80; server_name hospital.example.com; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }这段配置同时解决了Vue Router的history模式刷新404问题。很多新手部署后直接地址栏刷新页面就白屏就是因为Nginx没配置try_files回退到index.html。5. 常见问题排查与踩坑记录5.1 MyBatis缓存与脏读问题MyBatis的一级缓存是SqlSession级别的默认开启。在一个SqlSession生命周期内执行相同的查询第二次会走缓存不查数据库。在Spring环境下SqlSession默认生命周期和事务绑定。当业务较长时一次事务里的二级查询可能读到一级缓存里的旧数据。我遇到过的真实案例是收费后修改处方状态为“已收费”然后立即查询处方列表结果列表里显示的还是“待收费”。原因就是同一个事务里第一次查询把旧状态缓存了第二次查询命中了缓存。这个问题的解决方式是在更新操作后手动调用sqlSession.clearCache()或者把查询数据的SqlSession和更新数据的SqlSession拆开更好的方案是让涉及状态变更的查询直接加Options(flushCache Options.FlushCachePolicy.TRUE)。MyBatis的二级缓存我直接禁用了。社区医院系统数据时效性要求高处方、库存这些数据改了必须立刻生效二级缓存带来的性能提升根本不值一提但缓存过期策略没设计好就会引发严重的数据不一致。5.2 MySQL连接与时区问题现阶段新建项目基本直接用MySQL 8.0这里最大的坑是时区。MySQL 8.0默认时区是UTC而国内服务器时间大多是东八区用JDBC连接时如果URL里不指定serverTimezone查询出来的时间会比实际时间少8小时。我在JDBC驱动连接串里明确配置jdbc:mysql://127.0.0.1:3306/hospital_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue其中allowPublicKeyRetrievaltrue也是MySQL 8.0连接时的必配项不配会出现Public Key Retrieval is not allowed的报错。再补充一个实操感受MySQL 8.0的驱动类名是com.mysql.cj.jdbc.Driver老项目里的com.mysql.jdbc.Driver在新驱动里已经废弃不换的话连接会报ClassNotFoundException。用SpringBoot 2.7及以上版本时这些参数都可以直接配置在application.yml的spring.datasource.url里。5.3 SpringBoot版本过高带来的兼容性问题SpringBoot版本选择上我建议锁定2.7.x系列不要盲目上SpringBoot 3.x。原因有两个第一SpringBoot 3.x强制要求JDK 17而社区医院机房普遍还在用JDK 8Java版本升级牵扯到老系统兼容问题第二SpringBoot 3.x使用了Jakarta命名空间原有的javax.servlet包全部要改成jakarta.servlet大量第三方组件和示例代码不兼容。实际项目中我就遇到过某次开发机装的是SpringBoot 3.0.2项目一启动就报错“Failed to instantiate [javax.servlet.Filter]”排查了两天才意识到是SpringBoot 3的Jakarta迁移问题。后来统一降到2.7.6同样代码直接起飞。我的建议是团队里如果没人深度研究过SpringBoot 3.x的变更就老老实实使用2.7.x社区医院系统要的是稳定不是尝鲜。5.4 数据库并发与锁问题社区医院虽然并发量不大但高峰期会出现“多个窗口同时挂号、药房同时发药”的场景。这里最容易出的问题有两个第一个是扣库存超发。我之前用先查询再更新的方式写扣库存逻辑某天下午两个收费窗口同时给患者开同一种药结果系统显示库存还剩1盒但两个处方都收费成功了。排查后确认是并发下先查后扣产生的竞态条件改成前面说过的一次性UPDATE条件判断后问题再也没出现过。第二个是挂号号源冲突。同一医生的号源上午只有30个两个窗口同时挂号时可能把第30号发给两个患者。解决方式是在号源表上加唯一约束比如(doctor_id, visit_date, visit_seq)组合唯一数据库层兜底保证不会重复放号。切记这种关键业务约束不能只依赖应用层逻辑必须把约束落到数据库里。5.5 高频问题排查速查表症状可能原因处理方式中文乱码MySQL连接串没指定utf8mb4URL加characterEncodingutf8mb4时间少8小时MySQL 8.0时区默认UTCURL加serverTimezoneAsia/Shanghai登录后跳转404动态路由未处理刷新全局路由守卫里重新addRoutes页面刷新白屏Nginx未配置try_files增加try_files $uri $uri/ /index.html接口跨域报错前后端分离未配代理Nginx配置/api反向代理查询数据重复多表JOIN时主表字段重复SELECT主表字段明细单独查扣库存超卖先查后更的竞态条件UPDATE带stock #{num}条件事务部分成功方法内异常被吞掉事务方法内不要try-catch吞异常本地能跑部署失败配置文件环境不一致用Nacos或profile切换环境配置Token过期不跳登录响应拦截器未处理401拦截器判断code统一跳转这套系统前后打磨了大约两个月从数据库设计到前后端联调再到现场部署期间的改动超过200次。代码写到最后我才真正意识到所谓“企业级”并不是用了多高深的技术栈而是从挂号窗口那一句“请问您挂哪个科”开始到药房药师扫一眼处方就能找到对应的药品位置每一步都不能断。系统真正的存在意义是让患者少排队、医生少重复录入、药房少熬夜对账而不是给领导看一个多漂亮的界面。如果你正在做类似的医院管理系统先把业务流程走一遍再写代码比什么都管用。