这几年技术社区里铺天盖地的都是Spring Boot和Vue前后端分离项目,但真正能把一个垂直业务场景做得扎实、经得起问的其实不多。我前段时间完整落地了一个宠物医院管理系统,从需求梳理、数据库建模,到后端接口开发、前端页面联调,再到部署上线,整个链路走下来感受很深。宠物医院这个场景看起来只是"小型医疗管理系统",实际做进去才发现它既有医疗系统的严谨性(病历、诊断、处方),又有电商系统的库存逻辑(药品、耗材),还牵扯预约排班这类强时间约束业务。这篇文章就把我整个设计和实现过程摊开来讲,包括为什么选这套技术栈、表结构怎么设计、哪些接口最容易踩坑、前端哪些交互最费心思,以及我实测下来觉得最有价值的细节,适合正在做毕设或想拿真实业务练手的Java开发者参考。
1. 需求分析与系统架构设计
1.1 宠物医院业务的真实痛点
做系统之前,我在两家宠物诊所蹲过实际业务流程,发现大部分小诊所还在用Excel表格加微信预约的方式管理患者。宠物主人通过电话或微信跟前台约时间,前台手写登记到纸质台账,医生看完诊手写病历,药品出库靠事后补录。这个流程至少有四个致命问题:一是预约冲突频繁,某位医生同一时段被约了多位宠物主人,只能靠前台经验协调;二是病历和检查报告散落各处,复诊时翻找困难,转诊更是几乎无法追溯;三是药品库存账实不符,效期管理全靠人工盯,过期药处理记录不完整;四是经营数据完全靠月底手工汇总,营收、耗材成本、医生工作量都没有实时口径。
所以这个系统在设计之初,我就把核心目标定得很明确:围绕"宠物档案-预约挂号-诊疗开方-药品库存-经营统计"这条主线,把诊所的日常运转数字化。不是简单做一个增删改查的CRUD演示,而是真正贴合诊所的操作节奏——前台能快速建档、快速挂号,医生能高效写病历、开处方,药房能实时看到库存变化,老板能随时看经营报表。这个定位直接影响后续所有的表结构设计和接口划分。
1.2 技术选型与架构设计思路
技术栈选择Spring Boot加Vue这套组合,几乎没什么犹豫。Spring Boot在后端开发里的优势被讲得太多,我这里只强调一点:对于这类业务管理系统,Spring Boot自带的起步依赖和自动配置特性,能把项目从零搭建到跑通第一个接口的时间压缩到10分钟以内,这对中小型团队或独立开发者来说是实打实的效率提升。Vue这边我选的是Vue 2.7加Element UI,虽然Vue 3已经出来很久了,但实际落地时考虑到团队熟悉度和组件库生态,Vue 2.7仍然是个稳妥的选择,它的Composition API支持也足够应对这类管理系统的复杂度。
系统整体采用前后端分离架构,后端只提供RESTful API,前端通过Axios异步调用。数据库用的是MySQL 8.0,缓存和会话状态我直接用了Spring Boot内置的机制加JWT Token组合,没有引入额外的Redis,因为宠物医院单店的并发量级其实很低,引入Redis反而增加部署和运维成本。如果有连锁或多门店需求,后面再升级分布式缓存也不迟,这个决策后面细说。
1.3 功能模块全景拆解
根据业务流程,我把系统切成七个模块。基础数据模块管理宠物主人、宠物档案、医生信息和科室信息,这是整个系统运行的地基;预约挂号模块处理在线预约、现场挂号和号源管理;诊疗管理模块覆盖接诊记录、电子病历、诊断结果和医嘱处方,是整个系统的核心业务闭环;药品管理模块管药品字典、库存流水、效期预警;检查检验模块登记B超、血常规等检查项目结果;收银与统计模块负责收费记录、营收统计和医生工作量统计;最后是系统管理模块,包含用户管理、角色权限和操作日志。
这里有一个很重要的设计心得:模块划分不是越细越好,而是要以"业务闭环能否独立跑通"为标准。我当时把检查检验单独拆出来,是因为检查结果要回传给病历,而且涉及图片上传,权限上医生和技师的操作范围也不同。但收银和统计放一起,是因为收费动作直接产生统计数据,分开反而增加事务处理的复杂度。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 后端核心模块设计与实现
2.1 项目搭建与工程结构
后端工程我直接用了Spring Initializr生成基础骨架,Java版本选的8,Spring Boot版本用的2.7.x系列。这个版本的组合非常成熟,网上的参考资料也最丰富,遇到兼容性问题基本都能找到解决方案。依赖方面引入了Spring Web、MyBatis-Plus、MySQL Driver、Lombok、JWT认证相关的jjwt库,以及Validation参数校验组件。
工程结构上我用了标准的包分层:controller层只做参数接收和响应封装,service层写业务逻辑,mapper层对接数据库。但有一个地方我做了调整——增加了dto和vo两个包。一开始我也嫌麻烦,觉得直接传实体对象省事,但做到后面就发现问题很大。接收前端传参时,直接用实体类会导致多余字段被莫名赋值,形成安全隐患;返回数据时,直接吐实体类会把数据库字段全部暴露给前端,比如用户表的密码哈希值。所以现在所有接口都要求入参用DTO,出参用VO,这已经是我的习惯了,虽然没有中间商赚差价,但也没有中间商埋炸弹。
java复制// 一个典型的Controller写法
@RestController
@RequestMapping("/api/pet")
public class PetController {
@Autowired
private PetService petService;
@PostMapping("/save")
public Result save(@RequestBody @Valid PetSaveDTO dto) {
petService.savePet(dto);
return Result.success();
}
@GetMapping("/list")
public Result list(@RequestParam(required = false) String ownerPhone) {
List<PetVO> list = petService.listPets(ownerPhone);
return Result.success(list);
}
}
2.2 数据库设计要点
数据库设计是整个项目最需要花心思的环节,我前前后后调整了三版。核心表有八张:宠物主人表、宠物档案表、医生表、排班表、预约挂号表、诊疗记录表、药品表、库存流水表。这里挑几个重点讲讲设计取舍。
宠物主人表和宠物档案表我做成了一对多关系,一个主人可能带多只宠物来看病,宠物档案表里存了品种、年龄、性别、绝育状态、疫苗记录等字段。品种字段我没有用字符串直存,而是单独建了品种字典表,这样未来做统计报表时可以按品种维度分析哪些疾病高发。排班表是预约系统的核心支撑,记录每个医生在某个时间段是否坐诊,以及剩余号源数量。这里我没有做成传统的时间窗口表,而是把每天切成固定时段(上午四小时、下午四小时,每半小时一个号源),用一张表同时表达"排班"和"号源"两个概念。
预约挂号表设计时我特意加了状态字段和取消原因字段。状态包含已预约、已到诊、已完成、已取消、爽约五种,通过一个状态机来控制流转。取消原因看似多余,但实际运营时非常有用,能帮助管理者分析哪些时段的爽约率偏高,从而调整号源分配策略。
2.3 核心业务逻辑实现
诊疗记录和电子病历的设计是系统里最需要小心的地方。宠物不像人,它自己不会描述症状,所以病历结构除了主诉、检查、诊断、处方这些通用字段之外,我还设计了一个"主人描述"字段,强调记录宠物主人转述的异常行为。处方这边比较复杂,一张处方对应多条药品明细,每条明细包含药品、剂量、频次、天数、总量、用法说明。在数据库层面我拆成了主表和明细表两张,用主键关联,前端提交时一次性传输整个处方对象,后端在一个事务里完成主表和明细表的写入。
药品库存这块,我引入了库存流水表来记录每一次库存变动。这个表是后面所有库存报表的数据源,每次入库、出库、盘点、报损都记录一条流水,关联操作单号。这么做的好处是账实可追溯,比如某天发现某种药库存对不上,直接把这个药的所有流水拉出来,结合操作人和时间,基本能定位问题出在哪个环节。我当时在库存扣减逻辑上踩过一个坑:药品出库必须在开处方时同时完成扣减,而不是等药房实际发药时再扣,否则会出现处方开了但库存没锁住导致超卖的情况。
java复制// 处方开立时同步扣减库存
@Transactional
public void createPrescription(PrescriptionCreateDTO dto) {
// 1. 保存处方主表
// 2. 循环保存处方明细
for (PrescriptionItemDTO item : dto.getItems()) {
// 3. 扣减库存并写入流水
inventoryService.deductStock(item.getDrugId(), item.getQuantity(),
"PRESCRIPTION", prescriptionId);
}
}
2.4 权限认证与安全控制
权限模型我用了比较经典的RBAC设计,用户-角色-权限三层结构。系统内置四种角色:管理员、前台、医生、药房,每种角色配置不同的菜单权限和接口权限。认证方案选了JWT,登录成功后后端返回Token,前端存在localStorage里,每次请求在拦截器中自动携带到Authorization请求头。后端用拦截器解析Token并校验过期时间,同时把用户信息放到ThreadLocal里,方便Service层获取当前操作人。
这里分享一个我在权限控制上的实践经验:后端接口的权限校验不能只在拦截器里做粗粒度控制,关键业务接口还要做细粒度校验。比如医生只能查看和编辑自己接诊的病历,不能越过权限去看其他医生负责的患者。这个我在Service层实现了数据级权限过滤,查询时自动附加医生ID条件。这种细节看起来不起眼,但在真实业务里非常重要。
3. 前端核心页面与交互实现
3.1 Vue工程搭建与路由设计
前端工程我用Vue CLI创建,Node版本要求16以上,这是我在环境配置时遇到的第一个坑——公司的办公电脑Node版本还是12,Vue CLI 5直接报错,升级Node之后才顺利跑起来。UI组件库选了Element UI,图表这块用了ECharts做统计页面,文件上传用的Element UI的Upload组件搭配后端接口。
路由设计上我采用了动态路由的思路,前端根据登录用户的角色动态生成可访问的路由表。管理员登录能看到系统管理的菜单,医生登录只有接诊和病历相关菜单。实现方案是:登录成功后,后端返回当前用户拥有的菜单权限编码列表,前端根据编码列表从本地路由配置中筛选出可访问的路由,然后用router.addRoutes方法动态注册。这套方案是我做权限管理类系统比较推荐的做法,比把所有路由都写上然后靠导航守卫拦截要灵活得多。
3.2 核心页面实现
预约挂号页是整个系统使用频率最高的页面,也是UI交互上最复杂的。页面左侧是医生排班日历,中间是选定医生的号源时段列表,右侧是预约表单。调排班数据时我做了个小小的性能优化:默认只查当前周的数据,日历切换时重新请求,避免一次拉全年的排班数据造成接口响应慢。预约表单里宠物选择用的级联选择器,先选主人,再选该主人名下的宠物,这个交互需要前端维护一个"主人-宠物"的数据结构,我直接调了一个树形接口,一次返回所有主人及其宠物列表。
医生接诊工作台做成了类似"队列"的布局,当前预约状态为"已到诊"的宠物会进入待接诊列表,医生点击某个宠物就打开接诊页面。接诊页面分成三个区域:左侧显示宠物档案基本信息,包括疫苗记录、过敏史;中间是病历编辑区,包含症状描述、诊断结果;右侧是处方开立区。这个三栏布局是参考了真实医生工作站的操作习惯,尽量减少鼠标切换和页面跳转,提高接诊效率。
病历编辑区用了一大块textarea让医生自由输入,但我在底下加了一个常见症状模板的快捷选择,点一下模板句子就自动填入输入框,这算是我自己加的需求,实际用下来反馈很好,医生觉得省了很多打字时间。
3.3 Axios封装与前后端联调
Axios实例的统一封装是前端工程质量的分水岭。我在项目中封装了request.js工具模块,统一配置baseURL、请求超时时间、请求拦截器和响应拦截器。请求拦截器里做了Token注入和POST请求参数的序列化处理,响应拦截器里统一处理业务状态码和HTTP异常。
javascript复制// axios响应拦截器核心逻辑
service.interceptors.response.use(
response => {
const res = response.data
// 后端统一返回 { code, message, data } 结构
if (res.code !== 200) {
if (res.code === 401) {
// Token失效,跳转登录页
router.push('/login')
}
Message({ message: res.message || '系统异常', type: 'error' })
return Promise.reject(new Error(res.message))
}
return res.data
},
error => {
// 处理HTTP层错误
}
)
这种统一封装最大的好处是业务代码里不用关心错误处理逻辑了,每个接口只负责处理成功响应的数据,错误提示、Token失效跳转都是全局的。联调阶段我开了Vite的代理配置,把/api前缀的请求转发到本机8080端口的后端服务,完美绕过开发环境的跨域问题,这个后面单独讲。
4. 关键功能实现与难点突破
4.1 预约挂号的时间冲突处理
预约挂号最容易出问题的是并发冲突,两个用户同时点了同一个号源,如果后端不做控制,就会产生超卖。我的方案是在号源扣减时使用数据库乐观锁,预约表里增加一个version字段,更新时比较版本号,如果版本号不匹配说明数据已被别人修改,更新失败,提示用户号源已被占用。这个方案实现简单,也不引入额外的分布式锁组件,对单店场景完全够用。
数据库层面,预约挂号表加了一个唯一索引,联合字段是排班ID加号源时间。即使应用层有漏洞,数据库的唯一索引也能兜底,双重保险。这里提醒一下,不要觉得唯一索引可有可无,我实测下来在高并发测试时确实出现过两个请求同时通过应用层校验的情况,最后是数据库索引挡住了问题。
前端这里也配了一个6秒倒计时锁定的交互,用户选定号源后有6秒的确认时间,倒计时结束后如果未提交,号源自动释放,前端恢复可预约状态。这个体验上的细节模仿了真实购票系统的做法,减少无效占号,提升整体预约成功率。
4.2 药品库存预警机制
药品效期管理是宠物医院里特别容易被忽视的环节。刚进系统时我只看库存数量够不够,完全没管效期。后来和一位药房负责人聊天才知道,宠物医院出现药品过期的情况不少,特别是某些不常用的专科药。所以我在药品表里增加了生产日期、有效期至、批次号和预警天数四个字段,并且加了一个定时任务,每天凌晨扫描一次所有库存药品,如果有效期剩余天数小于设置的预警天数,系统自动生成预警记录,同时在前端首页的预警面板中显示。
预警的阈值设置我默认分了三档:30天内到期标红提醒,60天内到期标黄色预警,90天内到期标普通提示。药房人员看到预警后可以操作报损,或者进行退换货处理,整个流程都会写入操作日志。这个功能看起来不起眼,但对药房管理者来说是刚需功能。
java复制// 定时任务示例:每日扫描近效期药品
@Scheduled(cron = "0 0 2 * * ?")
public void checkDrugExpiry() {
List<DrugStock> nearExpiry = drugStockMapper.selectNearExpiry(90);
for (DrugStock stock : nearExpiry) {
expiryAlertService.createAlert(stock);
}
}
4.3 检查报告上传与预览
检查检验模块需要上传B超图片、血液化验单等文件。文件存储这块我没有用云存储,而是用了服务器本地存储加数据库记录的方式。上传接口用MultipartFile接收文件,按日期分目录存储到服务器磁盘,文件名用UUID加原始后缀拼接,数据库里只存相对路径。访问时通过一个独立的文件访问接口做权限控制,只有登录用户才能获取文件流。
文件预览做得比较简单,图片类型直接在页面里用img标签展示,PDF类型用浏览器自带的pdf预览能力。这里遇到一个兼容性问题:Safari浏览器直接打开PDF文件流不能正常预览,后来改成用Blob方式创建临时URL解决。还有一个坑是上传文件大小限制,Element UI的Upload组件默认限制单文件大小,但后端Spring配置文件里也需要同步设置max-file-size和max-request-size,否则前端传大文件时后端直接报413错误。
4.4 经营统计报表的实现
统计报表是老板最爱看的功能,我用ECharts实现了日报、周报、月报三种维度的营收趋势图、诊疗量柱状图、热门药品排行榜。数据来源是收银记录表和处方明细表,统计接口写了个相对通用的查询方法,通过时间参数和统计维度动态拼接SQL。这里用到了MyBatis-Plus的QueryWrapper和Java 8的Stream API做内存聚合,因为单店数据量不大,内存聚合的性能完全没问题,代码反而比写复杂的SQL清晰很多。
热门药品排行榜的统计口径是按处方明细里的药品数量求和排序,而不是按金额排序。因为宠物医院老板更关心哪些药用量大、周转快,而不是哪些药贵。这个口径上的细节也是跟真实经营者聊出来的,做系统的人容易想当然,多沟通才能做出真正对业务有用的功能。
5. 常见问题与排查技巧实录
5.1 跨域问题的经典解法
前后端分离项目第一个拦路虎就是跨域。开发环境我推荐用Vite的proxy代理解决,这是最优方案——浏览器发出的请求还是同源的,不触发跨域拦截。配置非常简单,改一下vite.config.js的server.proxy节点就行。
javascript复制server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
但生产环境部署时,前端静态文件一般由Nginx托管,后端是独立的Java服务,这时就需要在Nginx层配置反向代理,把/api前缀的请求转发到后端服务地址。我在项目里两个方案都用了。有一些项目在后台加CorsFilter全局配置也能解决,但只建议在没有网关层的纯后端开发环境里临时用,生产环境还是Nginx统一调度更干净。
5.2 Token失效与登录态管理
JWT的Token失效问题几乎每个做管理系统的朋友都会遇到。我的方案是Access Token有效期设置为2小时,前端每次接口请求时如果收到401状态码,就清空本地用户信息和Token,强制跳转到登录页。但这个方案的体验比较生硬,用户正在填写接诊信息时突然被踢下线。
后来我改成了双Token方案:Access Token有效期还是2小时,Refresh Token有效期7天。接口返回401时先尝试用Refresh Token调用刷新接口,刷新成功就自动替换本地Token并且重发刚才失败的请求,用户无感知。只有Refresh Token也过期时才强制登录。实现思路不复杂,前端在响应拦截器里多加一段逻辑,后端提供一个refresh接口,刷新时用Refresh Token换新的Access Token,有效期重新计时。
5.3 表单校验与数据回显的坑
我发现很多新手在做的系统页面会有几个"看起来能用"但仔细看全是问题的交互细节,比如编辑功能的数据回显。回显的关键在于数据结构要对齐:从后端拿回来的数据是什么结构,表单绑定的数据就必须一模一样,否则就会发生下拉框选中了但显示的是数字ID、日期选择器赋值却显示Invalid Date这种情况。我在药品编辑页踩过这个坑,药品表单里类型字段和供应商字段都是关联字典表的ID,回显时前端必须先把字典数据转换成label和value对应的映射结构,再赋给表单,否则显示的就是一串数字。
处理方案是封装了一个公共的字典类方法,加载字典列表后根据字典类型和值转换成对应的选项数据。另外,日期范围的选择需要把字符串转换成Date对象或者用时间戳格式统一处理,这个用dayjs库做格式化,避免手工拼接字符串出错。
5.4 后台查询性能优化
管理系统的列表页很容易做成分页查询,但真实使用中还有一个高频场景是数据导出。宠物医院的流水记录、预约记录经常需要导Excel交给财务或管理者留存。用传统的后端导出方式,即接口返回JSON给前端,前端表格库再去生成Excel文件,数据量一大就卡得不行,而且列对齐和格式转换非常容易出Bug。
我后来改成了后端直接产出Excel文件的方案,用阿里开源的EasyExcel组件,后端查询出数据后直接写文件流,前端用blob接收触发浏览器下载。一个几千条的记录导出,后端生成文件不到1秒。这个方案唯一要注意的是导出接口的权限控制,能导出数据就意味着能拿到全量数据,我在这里做了角色限制,只有管理员和财务角色可以调用导出接口。
6. 项目部署与上线经验
部署方案我选的是传统方式:后端打Jar包,放到服务器上用systemd托管;前端构建后生成的dist目录放到Nginx的web目录下。这个方案看着不够"现代",但对一个每天访问量几十上百的小诊所来说,简单可靠才是第一位,引入Docker和K8s反而增加复杂度。
部署过程中有几个容易被坑的点。第一个是后端配置文件里的数据库连接和文件上传路径,我用了application-prod.yml这个独立的配置文件,跟开发环境完全隔离,避免上线时改错参数。第二个是Nginx配置gzip压缩,前端构建后静态文件体积不大,但开启gzip也能显著提升首屏加载速度。第三个是定时任务的时区问题,我一开始忘了配置JVM默认时区,结果所有定时任务都差了8个小时,在启动脚本里加-Duser.timezone=Asia/Shanghai参数解决。
上线后我还做了个简单的监控——写了一个Shell脚本,每5分钟检查一次Java进程是否存在,如果进程挂了就自动重启并记录日志。虽然糙但是管用,至少人不在电脑前出问题时,服务能自动恢复。
这个项目从零开始到上线跑稳定,大概花了一个半月的时间,其中大部分时间都耗在业务理解和细节打磨上。真正写代码的时间其实只占三分之一。做这类管理系统,技术本身不是最大的壁垒,对业务的理解深度才是决定项目质量的关键。如果你也在规划类似的项目,我的建议是至少花一周时间去了解真实的业务流程,把那些"不会出现在需求文档里"的隐形规则挖掘出来,这才是项目能真正落地的分水岭。
