前前后后帮朋友做过好几个管理系统,从图书借阅到校园考勤,但要说最接地气、业务逻辑最贴近真实场景的,还得是前段时间完成的这个宠物医院管理系统。很多初学者一上来就盯着技术栈,什么SpringBoot、Vue,觉得把框架跑通了就算完事,但实际上这类管理系统的难点和乐趣,全在业务模型的设计和前后端联调的细节里。这篇文章我就用这个项目为例,从需求拆解到数据库设计,再到前后端的具体实现和踩坑记录,完整聊聊整条链路是怎么走下来的,希望能给正在做类似毕设或者接私活的朋友一些参考。
1. 项目整体设计与技术选型思路
1.1 为什么还是选SpringBoot+Vue这套组合
这个组合被问过太多次,我也被质疑过没有新意。但说实话,宠物医院管理系统选这套组合,恰恰是因为它足够成熟和稳妥。SpringBoot的生态太全了,权限认证有Spring Security,数据库操作有MyBatis-Plus,做接口文档有Swagger或者Knife4j,包括后续接Excel导出、接消息通知,都能找到非常成熟的开源方案。
Vue这边同样如此,社区资源极其丰富,中文文档完善,招聘需求量也大。对做毕设或者练手项目的人来说,最大的好处不是它有多先进,而是遇到任何问题都能在搜索引擎里快速找到答案,基本不存在卡住几天过不去的情况。我自己的习惯是后端用Spring Boot 2.7.x版本,前端用Vue 2 + Element UI,很多人问我为什么不直接用Vue 3,原因很简单:Vue 2的生态沉淀更久,Element UI更是老牌组件库,资料多、坑少,能在最短时间内稳定完成项目交付。如果你是自己学习,用Vue 3 + Element Plus也完全没问题,思路是相通的。
组件选型上没有谁比谁高贵,关键是“团队认知范围内最稳的方案”,这也是我反复强调的一个观点:技术选型不是在选最好的,而是在选你最熟悉、最能控制风险的。
1.2 业务模块拆解与用例分析
宠物医院和普通的人医院不一样,它的业务链条里多了“宠物档案”这条主线,所有诊疗、住院、寄养、疫苗、驱虫都围绕一只宠物展开。我和朋友当时去一家社区宠物诊所蹲了两天,观察他们的实际工作流,最后把系统拆成了这么几块:
- 宠物主人管理:包括宠物主人的基本信息、联系方式、会员等级、历史消费汇总。
- 宠物档案管理:每一只宠物绑定一个主人,记录品种、年龄、性别、是否绝育、疫苗接种时间、过敏史等信息。
- 挂号预约管理:支持主人线上预约或前台代挂号,按科室和医生排班来分诊。
- 医生工作台:医生查看候诊队列、书写电子病历、开检查单、开处方。
- 药品与库存管理:药品分类、库存出入库、有效期预警、处方关联扣库存。
- 收费与结算:挂号费、检查费、药品费整合生成收费单。
- 住院与寄养管理:住院笼位管理、每日护理记录、寄养到期提醒。
- 系统管理:员工账号、角色权限、操作日志、基础数据字典。
每一块看着都是CRUD,但合在一起之后,表之间的关联关系、状态流转、权限边界就复杂起来了。比如挂号单的状态可能是“待就诊、就诊中、已完成、已取消”,而处方单只有在“已收费”之后才能去药房取药,这些流程都需要在代码层面做严格的校验。
1.3 多角色权限模型该怎么设计
权限模型几乎是管理系统绕不开的话题,宠物医院的用户类型大概有这么几种:管理员、前台、医生、药师、化验员,可能还有老板需要在后台看经营报表。我用的是RBAC(基于角色的访问控制)模型,一张用户表、一张角色表、一张菜单/权限表,再加上用户角色关联表和角色菜单关联表。
有朋友常问我,菜单权限是在后端返回还是前端写死?我的做法是后端返回当前用户的菜单树,前端根据菜单数据动态生成路由。后端用Antd的TreeSelect组件去配菜单和权限的父子关系,然后把数据存到数据库里,每次登录时查询该用户拥有的权限标识,按标识控制按钮级权限。比如药师账号看不到“退款”按钮,医生账号只有“开处方”按钮而没有“作废处方”的权限。
这样设计的最大好处是灵活,给某个医生也赋予药品管理权限时,不需要改任何代码,数据库里加一条角色关联就行。实际项目里如果时间紧迫,也可以做成前端写死路由,然后靠后端接口做鉴权兜底,但这样做后续加权限会很痛苦,我不太推荐。
2. 数据库表结构设计与核心业务实现
2.1 核心表关系梳理与建表规范
数据库设计是整个项目的地基,很多人在这个阶段图快的写个user表、pet表就开搞,后面写业务代码的时候才发现缺字段、缺表,开始频繁改动,非常痛苦。我通常的做法是先把核心实体画出来,列清楚关系,再落库。
宠物医院系统里我建了大概二十多张表,这里列出最核心的几张:
pet_owner:主人表,关键字段有姓名、手机号、微信号、会员等级、累计消费金额。pet_profile:宠物档案表,外键关联主人ID和品种字典表,包含宠物名、性别、生日、体重、绝育状态、疫苗记录、过敏信息、头像。vet_doctor:医生表,包含工号、姓名、职称、科室、排班周期、简介。appointment:挂号预约表,包含预约时间、宠物ID、医生ID、项目类型(普通门诊/专科门诊)、状态。medical_record:电子病历表,记录主诉、现病史、初步诊断、医嘱,关联医生和宠物。prescription:处方表和prescription_item:处方明细表,处方头记录总金额、状态,明细表记录每种药品、数量、剂量。drug_info:药品表,包含商品名、通用名、规格、生产厂家、库存数量、预警阈值、售价。inventory_record:出入库流水表,记录每一次入库/出库/报损/退药的操作。hospitalization:住院记录表,记录入院时间、笼位号、预交金、每日托管费用、出院时间。charge_order:收费单表,记录总金额、优惠、实收金额、支付方式、收款人、支付时间。
建表的时候有三点经验可以参考。第一,金额字段一律用DECIMAL(10,2),不要用FLOAT或者DOUBLE,否则算药品总价很容易出现精度丢失。第二,所有业务表都加上create_time和update_time以及逻辑删除标记deleted,这样后面写统计报表和做数据恢复会方便很多。第三,关联字段务必建索引,尤其appointment表的pet_id、doctor_id和状态字段,前期数据量小看不出什么,数据量一上来这几个字段的索引价值非常明显。
2.2 核心逻辑实现:预约挂号与排班冲突处理
预约模块是整个系统的门面,也是逻辑最需要小心的地方。一个医生在某个时间段可能已经被预约了,或者到了休息时间,系统必须能检测出来并拒绝新的预约。
我先建了schedule排班表,记录医生在一周内的固定出诊时段,每次排班按照时段拆分成一条记录,比如周一到周五每天上午8点到12点是普通门诊时段。生成排班有两种做法,一种是管理员手动维护,另一种是每周自动生成模板排班。我做的是后者,管理员设置好规则后,系统定时任务自动生成未来7天的排班,保存在schedule表里。
前端预约时,选择一个时段,后端先查这个时段是否还有剩余号源。我设计了一个“号源总数=排班的总容量,已约数量=该时段有效预约记录的数量”的校验方式,预约成功则插入一条appointment记录,并在事务里把已约数量加1。这里必须处理好并发,两个主人同时抢最后一个号源时,如果先查再插没有任何锁,就可能超卖。解决方法有两种:
- 用数据库乐观锁,在
schedule表加version字段,更新时校验版本号。 - 利用唯一索引,为
schedule_id和appointment_time建联合唯一索引,由数据库来强制约束。
我第一次实现用的是乐观锁,SparkBoot加@Version注解配合MyBatis-Plus就能实现,逻辑也不复杂。后面改成更土但更有效的方案:直接在schedule表里加一个appointed_num字段,更新时写UPDATE schedule SET appointed_num = appointed_num + 1 WHERE id = ? AND appointed_num < max_num,通过受影响行数判断是否约满。这种方式在并发不高的小型诊所完全够用,而且不需要额外引入分布式锁。
2.3 电子病历与处方药品的联动细节
电子病历的核心是把主诉、检查结果、诊断结论这些文本信息结构化,同时与图片附件(比如血液检查报告单的照片)解耦。我的做法是单独建一张medical_attachment表,用record_id关联病历主表,附件用UUID重命名后存到本地磁盘或OSS,数据库只保存访问路径。
处方这块有个细节值得分享:药品价格、名称这些信息可能会变,处方一旦开出后就不应该随着药品表的变化而变化,所以我在prescription_item里冗余存了drug_name、drug_spec和unit_price这三个字段。这样即使以后药品价格调整了,历史处方上的价格依然准确可查。这是平时写代码不太会注意、但真实业务里非常关键的一点。
医生开处方时,后端会先检查药品库存是否足够,不够则提示“阿莫西林库存不足,当前存量5,处方数量10”,只有全部药品库存充足才可以提交。提交后库存并不会马上扣减,而是等到收费成功后,再去扣库存。这里涉及了状态机的概念:处方单的状态有“待收费、已收费、已取药、已退药”,这样一个简单设计,就能避免病人还没交钱就把药从库存里拿走了。
钱和库存的联动,需要用事务来保证一致性。我在收费接口上加了@Transactional,方法里面先更新收费单状态,再扣减库存,再更新处方状态。任何一步失败都会让整个操作回滚,保证数据不会出现“收了钱但库存没扣”或者“扣了库存但没收到钱”的脏数据。
3. 前端Vue业务实现与交互细节
3.1 路由设计、导航守卫与Axios封装
Vue前端我当时用了干净的Vue CLI快速搭建,用Vue Router管理路由。既然是管理系统,页面结构自然分为公共区、登录页和主框架区。主框架区用嵌套路由,侧边菜单、顶部导航栏是父组件,各个业务页面作为子组件渲染到<router-view>里。
路由守卫主要解决两个问题。第一个是未登录的用户不允许进系统,查看本地token,不存在就强制跳转登录页;第二个是不同角色的用户只能看到自己有权限的菜单,这里我用后端动态返回的菜单列表去过滤静态路由表,再动态注册进去。具体实现是在router.beforeEach里处理:用户从登录页拿到token后,先请求/user/info接口获取用户信息、权限标识、菜单列表,存到Vuex/Pinia或sessionStorage,然后根据菜单列表计算可访问路由。这个过程如果是刷新页面也要处理一次,否则刷新后路由表就丢了,页面会白屏。
Axios请求封装更偏向工程化落地的细节,我写了一个request.js,做四件事:统一设置请求头带上token;统一处理HTTP状态码错误提示;统一拦截后端返回的业务码(比如401表示登录过期、500表示业务异常);文件下载的响应类型单独处理。拦截器写好之后,每个页面里的代码量会明显减少,也不容易漏掉错误处理。
3.2 预约看诊首页与日历排班的实现
预约看诊页面我用Element UI的el-calendar组件展示医生排班。选中某个日期后,下方动态展示当天不同医生的出诊时段和剩余号源数,剩余号源为0的时段置灰不可点。联动的数据流是:选择日期触发getScheduleByDate接口,后端返回当天所有排班时段及剩余号,前端再按医生维度分组成卡片。
这个页面的用户体验重点在于实时反馈。用户选了一个时段点预约,会弹出一个对话框,把自己的宠物带出来绑定(这个用户可能养了两三只宠物,需要选一只),再选择看诊原因,提交后按钮立刻置为“已预约”。我在这个弹窗里做了宠物头像列表,多宠物时可以直接点选切换,亲和力会好很多,这在展示类项目里很加分。
后端接口设计成POST/api/appointment/create,入参包含scheduleId、petId、ownerId、remark。返回结果里有两种错误码,比如2001表示“该时段已被约满”,2002表示“您已经有待就诊的预约,请先就诊或取消”,防止同一个主人重复占号。这个设计是参考线下诊所实际规则的,本质上是想避免两个宠物主人同时占一个号位造成资源浪费。
3.3 药品库存与报表的可视化呈现
药品库存页是前端表格加上筛选条件、分页、导出功能的标准形态,我重点说一下两点:一是库存预警颜色的处理,后端在查询药品列表时,按库存 <= 预警阈值算出stockStatus字段,前端通过cell-class-name给这个单元格加上样式。为了更好看,每个药品状态加了进度条式的展示,进度条由“当前库存/库存上限”算出比例,低于20%会变成红色。
报表页我用了ECharts来做可视化。宠物医院的报表大致有三块:每日接诊量趋势、收费金额统计、药品销售排行。ECharts的使用并不复杂,核心是前端的图表组件和后端接口返回的数据结构要对齐。我习惯定义统一的折线图数据结构:{ dateList: ['2025-01-01', '2025-01-02'], seriesData: [12, 15] }。后端SQL直接按日期分组统计。MySQL的DATE_FORMAT(create_time, '%Y-%m-%d')做分组函数非常顺手,配合GROUP BY date基本不需要前端额外处理。
这里有一个接口性能方面的经验:每天的接诊量统计如果以全表扫描方式去查,周末高峰或者数据量大了会稍微变慢。我的做法是给medical_record的create_time加了索引,并让统计接口按天粒度先去重查询,再把多天的结果拼起来,十天的报表数据基本秒开。
4. 常见问题与排查技巧实录
4.1 跨域问题与拦截器放行配置
前后端分离项目里,跨域是第一个拦路虎。前端跑在localhost:8080,后端跑在localhost:9090,浏览器会直接拦截后端返回的响应。这里分享一个完整的配置:在SpringBoot里写一个WebMvcConfigurer,重写addCorsMappings方法,允许来源、方法和请求头。如果同时用了Spring Security或自定义拦截器,一定要确认拦截器里放行了OPTIONS预检请求,否则前端请求到了后端但拦截器直接返回401,浏览器就懵了。
另外,我见过很多项目把跨域配置写成了allowedOrigin("*"),这在本地开发没问题,生产环境如果涉及Cookie或带Credentials的请求就会出问题。生产环境容易踩的坑是Nginx反向代理之后忘了配合修改前端请求地址,导致接口404。建议前端把baseURL配置成相对路径,由Nginx统一转发到后端服务,这样以后换服务器只需要改Nginx配置,前端代码几乎不用动。
4.2 时间格式与时区导致的日期错乱
有朋友遇到过前端传的生日日期总是晚了一天或者显示成了UTC格式,这个问题十有八九出在Jackson序列化和时区配置。SpringBoot默认会用Jackson把Date序列化成UTC时间,而前端在解析时用的本地时区(东八区),一算就少了8小时。解决办法是在application.yml里配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8同时建议数据库连接串加上serverTimezone=Asia/Shanghai,MySQL的时区也要设置为+8。这个配置看起来简单,但漏一个就会让日期出现各种莫名其妙的问题,越早配上越好。
前端Vue里经常遇到另一个时间问题:后端返回的时间字段是字符串,例如2025-03-18 14:30:00,直接用表格展示没问题,但要跟当前时间比较或者格式化就头疼了。我的做法是前端封装一个dayjs处理工具,统一把后端时间字符串转成dayjs对象,需要比较时直接用isBefore/isAfter,需要展示时统一走dayjs().format()。引入Day.js比Moment.js体积小很多,功能也够用。
4.3 Vue打包后刷新404与图片路径问题
前端开发时一切正常,但npm run build之后部署到服务器,刷新页面就404,或者图片加载不出来。刷新404几乎可以肯定是Nginx没有配置try_files。你的Nginx需要这么写:
location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }这样刷新/appointment时,Nginx会尝试找真实文件,找不到就统一返回index.html,让前端路由接管,问题就解决了。
图片路径问题则是Vue项目打包后静态资源定位不准导致的。开发模式下资源路径是/,生产模式下如果你把项目部署到了子目录,就需要在vue.config.js里配置:
module.exports = { publicPath: './', assetsDir: 'static' }用相对路径虽然可以解决,但会带来一个新的坑:前端路由也要改成hash模式。否则在子目录部署的情况下,history模式刷新还是会404。所以如果你是在子目录部署(比如/pet-hospital/),我建议直接使用hash模式路由,省掉一堆麻烦。
4.4 高并发小诊所场景下的性能优化心得
宠物医院的日均流量其实不高,早高峰预约可能同时有几十个人访问。这种情况下性能瓶颈不在数据库,而在一些容易被忽略的地方,比如图片压缩、静态资源缓存、频繁查询的接口做二级缓存。
我当时做了一组优化,效果非常明显。药品分类这种万年不变的数据,做成Redis缓存,过期时间设24小时,后端启动时自动加载到本地内存。挂号时段查询也做了Redis缓存,过期时间30秒,这样即使高峰期几十个人同时刷新排班,压力也都在缓存上,后端数据库基本没有被高频查询压到。
另外,列表接口的分页查询建议直接用MyBatis-Plus的Page对象,配合排序字段设置顺序。不要用数据库的COUNT(*)做全表统计,在数据量达到几十万条后这个操作会很慢。如果只是想让项目看起来性能更专业,建议给查询量比较大的接口加上Explain语句优化思路,再写几个复合索引。在答辩或者谈项目时,能顺带说出索引和缓存的设计,效果会比单纯说“这个接口查得快”好得多。
4.5 项目文件结构与规范建议
整个项目结构上,我习惯后端分成controller、service、mapper、entity、dto、vo、config、common这几个包,前端按页面拆视图组件,再把公共组件、工具函数、接口定义放到各自的目录。项目目录清晰本身也是一种生产力,接手项目或者写毕设文档时,别人看到明明白白的分层,印象分直接不一样。
同时,编写接口时尽量在外层统一封装返回值,类似Result.success(data)、Result.error(code, msg),前端Interceptor只看code,非200就弹错误信息。这样做的好处是后端代码里出了异常也不会直接抛给前端,前端能拿到稳定的数据结构,联调体验会顺畅很多。
关于代码规范,我的建议是后端起项目前就配置Lombok、统一ExceptionHandler,前端安装ESLint和Prettier,提交代码前自动格式化。这些一开始配置会花一点时间,但也省了后面联调时因为代码风格问题扯皮的额外开销。
5. 从这次实战中总结出的经验与可扩展方向
5.1 复盘:35天从零上线,时间都花在哪里
整个项目前后花了大概35天,一半时间在设计表格和业务逻辑,一半时间在前端页面和联调。设计花得多不是坏事,恰恰因为前期把预约、处方、库存、收费之间的状态流转理清楚了,后面写代码几乎没有推倒重来。唯一一次改动大到头疼的地方,是住院模块嵌套在处方逻辑里,通过临时加了一个hospital_daily_record表解决了护理记录重复的问题。早发现问题就去补,反而比全盘重构舒服很多。
5.2 记录几个项目里不需要的东西
有段时间我为了追求华丽,加了Spring Cloud网关,后来发现宠物医院这类项目根本用不上微服务,单体应用加个Nginx反向代理已经绰绰有余了。还有一段想做消息队列,规划了用RabbitMQ推送预约成功短信,但真实逻辑里短信服务根本不需要那么复杂,@Async异步就是够了。系统设计永远是为业务服务,别为了技术而技术,这句话落在纸面上很虚,但代码写多了自然能体会。
5.3 可以继续发散的方向
这套系统如果能走到真实场景,我接下来会考虑接一个IoT设备,就是宠物体温监测项圈。现在住院护理记录是护士手动录入,如果能靠设备自动采集体温数据并上报到系统,后台实时监测异常,这个能力对宠物医院来说很能提升竞争力。另外一个方向是接微信生态,主人通过小程序预约、查看化验单和缴费,这也是目前医院一体化服务的主流趋势。
再说一个小技巧:后端返回的前端菜单名称、图标标识、排序权重,都应该在数据库的数据字典里管理,而不是前端写死。这样老板某天说“把‘注销账号’移到最底层”的时候,你只需要改一条数据库记录就可以下班,而不是改代码部署。
说实话,做成这个宠物医院管理系统之后你再去看其他管理系统,会发现底层思路高度类似。掌握了用户权限、核心业务流、状态流转、库存与计费联动这些核心能力,往后做修车行系统、美容店系统、健身房系统,思路基本都是一通百通。区别只是在业务流程和表设计上要做场景适配,但本质还是在“管数据、管流程、管权限”这三件事上。