最近抽空把一个萌宠宠物医院系统从零完整做了一遍,从需求梳理、数据库设计、后端接口到前端上线的过程中,我发现宠物医院的管理和普通门诊完全不是一回事。宠物没有医保、没有统一身份证,还可能频繁更换主人,一只猫狗名下可以挂好几个会员档案,再加上疫苗、驱虫、住院这些特殊环节,光是把数据关系理清楚就够折腾一轮。如果你正好在做类似的管理系统,或者准备用Spring Boot做一个能写进简历的项目,这篇文章会从宠物医院系统的业务模型、表结构设计、核心接口逻辑到部署排错,把整个设计和实现过程掰开揉碎讲清楚。我会直接讲我实际采用的技术方案和踩过的坑,不说空话。
1. 项目整体设计与业务模块拆解
1.1 宠物医院的业务场景和核心痛点
宠物医院的项目看着不大,但涉及的流程比一般诊所更细碎。普通患者到医院就是挂号、就诊、取药,而宠物就诊往往伴随疫苗接种、定期驱虫、绝育手术、住院护理、寄养服务等多个场景。我这次设计的系统面向一家中型宠物医院,日常服务对象是狗、猫为主,也顺带接诊兔子、仓鼠等小宠。不同动物的生理指标差异很大,病例记录里必须留存物种、品种、年龄、体重和过敏史信息,这些字段在人类医院里几乎不需要单独考虑。
另一个核心痛点是“宠物档案归属”。一只宠物可能被主人带来复诊,也可能被救助站暂时寄养,甚至还遇到过一只狗被两个家庭同时在线上挂号。所以在会员和宠物之间必须用一对多的关系建模,同时保留宠物自身的基础档案和疫苗接种时间线,不能直接把宠物信息塞在会员表里。
医院管理方最关心的则是经营统计:每天接诊量、挂号费收入、药品消耗、热门科室、医生工作量。这些数据如果靠手工表格统计,不仅慢而且容易漏。系统在设计之初就需要把挂号、诊疗、收费、药品库存这几条链路上的数据完整记录下来,为后续的业务看板和大屏展示留好扩展点。
1.2 核心功能模块梳理
我把整个系统拆成了七个主要功能域:
- 会员与宠物档案:保存主人联系方式、宠物基本资料、体重记录、免疫记录。
- 挂号与排队:支持现场挂号和预约挂号,按科室分配医生,门诊大厅显示屏能实时刷新叫号状态。
- 门诊诊疗:医生录入主诉、检查结果、诊断结论、医嘱,关联历史病历。
- 药品与库存管理:药品入库、出库、库存预警,处方开药时自动扣减库存并生成收费记录。
- 住院与护理:针对需要留院观察的动物,记录住院床位、护理项目、每日状态。
- 收费与结算:支持挂号费、诊疗费、药品费、住院费混合结算,并生成明细账单。
- 系统管理:用户角色权限、部门科室、数据字典、操作日志。
这些模块之间通过几个关键业务单据串联:挂号的目的是生成一次就诊记录,就诊记录里可以开出处方单、检查单或住院单,最终所有费用汇总到收费结算单。设计数据表时我会以“诊疗记录”作为核心枢纽,而不是简单把功能堆在一起。
1.3 角色权限与业务流程
系统用户分为管理员、医生、护士、收费员和前台五种角色。管理员负责科室配置、药品维护、数据统计;医生是系统的核心使用者,需要看到今天待诊列表、写病历、开药;护士负责住院护理记录和疫苗注射记录;收费员处理结算单据;前台管理现场挂号。
权限控制我使用了基于角色的访问控制方案:角色表、菜单表、用户角色关系和角色菜单关系四张表。登录后根据用户角色加载对应的菜单和操作按钮。这样做的好处是后续如果新增一个“美容师”角色,只需要在菜单表和角色关系上批量配置,不需要改代码。
以最常见的就诊流程为例:前台在系统中给宠物建档,然后挂号选择一个科室和医生,产生当日挂号记录。系统将该记录放入对应科室的叫号池,医生在登录后刷新“我的接诊列表”,点击“开始接诊”后接诊列表状态变为就诊中。医生提交诊疗记录后,系统自动关联处方药品项,同时扣减库存。收费员根据诊疗单和处方单生成结算单,整个流程闭环。后面所有模块都围绕这条主流程来设计,避免出现数据断层的尴尬。
2. 技术选型与工程初始化
2.1 后端技术栈:Spring Boot + MyBatis Plus + MySQL
后端技术栈我选得比较保守,因为这类业务系统最重要的是稳定和可维护性。主体使用Spring Boot 2.7.x,搭配MyBatis Plus 3.5.x框架操作MySQL 8.0。Spring Boot提供自动配置和生态支持,MyBatis Plus把单表增删改查、分页查询和逻辑删除做了封装,能在保证SQL可控的前提下大幅减少重复代码。
为了照顾老项目和部分毕业设计的兼容性,我把JDK设置为1.8。如果读者电脑里只有JDK 17,也可以在pom里将java.version改为17,Spring Boot 2.7和3.x都能运行,但3.x需要修改部分依赖名。我这里使用Spring Boot 2.7更稳妥,网上资料也最多。
项目的依赖管理用Maven,核心依赖如下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql-connector-j</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency>同时引入了Lombok简化实体代码、Hutool工具库处理字符串和日期、Knife4j用于生成API接口文档。这些工具不改变业务逻辑,但是能让开发效率提升一个档次。
2.2 前端方案:Vue + Element UI 还是模板引擎?
我做了两个方案对比。第一个方案是服务端渲染,直接用Thymeleaf或FreeMarker模板,优点是项目结构简单、不需要前后端分离,适合单人开发和小型系统。第二个方案是前后端分离,前端使用Vue 3 + Element Plus + Axios,后端只提供JSON接口,前端单独部署在Nginx上。
考虑到系统需要支持中后期的大屏展示,以及可能扩展小程序端,我最终采用前后端分离方案。因为只要后端接口设计得足够规范,将来小程序、移动端可以直接复用同一套API,不需要在模板文件里大改。前端开发环境用Vite,生产环境打包后用Nginx部署。前端项目目录下包含views、router、store、api、components等结构,以api目录统一管理所有请求函数,这样后端接口地址变化时只需要改一个文件。
开发时前端请求后端接口通过代理解决跨域问题,生产环境由Nginx将/api前缀转发到后端服务。这个方案在现场演示和本地联调中都很顺手。
2.3 项目目录结构与开发规范
后端按经典的分层结构组织,避免把所有代码塞进controller里:
com.pethospital ├── controller // 接口入口层 ├── service // 业务逻辑层 │ └── impl ├── mapper // 数据库访问层 ├── entity // 数据库实体 ├── dto // 数据传输对象 ├── vo // 视图返回对象 ├── config // 配置文件 ├── interceptor // 拦截器 ├── utils // 工具类 └── common // 通用返回结果、异常处理我要求自己遵循一条规则:controller只做参数接收和结果封装,业务逻辑全部下沉到service层,事务控制也放在service层。mapper只负责单表操作,多表联查写自定义SQL时必须在XML文件中清晰命名,方便以后维护。
前后端交互的返回结果统一封装为Result对象,结构是code、message、data。前端axios响应拦截器里统一判断code,不需要每个页面都写错误处理。异常处理使用全局@RestControllerAdvice,业务抛出业务异常后,统一返回带错误码的JSON,而不是直接把堆栈信息暴露出去。这些看似是基础设施,但对后期维护影响非常大。
3. 数据库设计与核心表结构
3.1 实体关系分析
画出整体E-R关系是开工前最重要的一步。我用数据库建模工具画完图后,反复检查了三遍才动手建表。核心关系可以概括为一句话:会员可以拥有多只宠物;宠物经过多张挂号单产生多次诊疗记录;一次诊疗可以包含多个处方明细和检查项目;药品库存变化由处方明细触发;最终所有项目汇总到一张结算单。
为了清晰表达实体关系,这里不用图表工具,直接列举关键的一对多关系:
- 会员表
member与宠物表pet:member.id对应pet.member_id,一对多。 - 挂号表
registration与宠物表pet:一次挂号对应一只宠物,pet_id外键。 - 诊疗记录表
medical_record与挂号表registration:一对一关联,系统靠挂号单号追溯接诊医生和就诊时间。 - 诊疗记录与处方明细
prescription_item:一对多,一条诊疗记录可能开出多盒药。 - 药品库存表
drug_stock与处方明细:由处方明细回写库存,保持数据一致性。 - 结算单
settlement关联挂号、诊疗、住院等多种单据,通过费用类型区分。
有了这层关系图,实现业务闭环就不容易乱。
3.2 核心表设计
数据库我统一使用utf8mb4字符集,因为有时需要记录宠物主人的表情符号或者特殊字符。下面列举几张最重要的表结构,字段不全部照搬,只保留我在实际项目中验证过的设计。
会员表member:
id bigint 主键自增 member_no varchar 会员编号,按时间戳生成 name varchar 主人姓名 phone varchar 手机号,登录账号 password varchar 加密后的密码 gender tinyint 性别 address varchar 联系地址 status tinyint 状态 0禁用 1正常 create_time datetime update_time datetime宠物表pet:
id bigint member_id bigint 关联会员 pet_name varchar pet_type varchar 猫、狗、小宠 breed varchar 品种 gender tinyint birthday date weight_kg decimal 最新体重 vaccine_status tinyint 免疫状态 photo varchar 照片路径 create_time datetime这里有个小技巧:pet_type和breed不设死代码,而是在数据字典里维护。因为动物种类可能会变,如果一开始写死在代码里,后续加一种稀有品种就得改Java枚举。
挂号表registration:
id bigint reg_no varchar 挂号单号,格式如 R202501010001 pet_id bigint member_id bigint doctor_id bigint 接诊医生用户ID department_id bigint 科室ID reg_time datetime 挂号时间 visit_date date 就诊日期 period tinyint 时段:1上午 2下午 3夜间 status tinyint 状态:1待就诊 2就诊中 3已完成 4已取消 5过号 amount decimal 挂号费 create_time datetime挂号单必须区分“预约”和“现场挂”两种来源,我在实际中用一个source_type字段保存。因为预约渠道需要后续做回访和通知,现场挂号和在线挂号的统计维度也不同。
药品表drug与库存表drug_stock:
drug表字段: id, drug_name, specification, unit, dosage_form, manufacturer, price, prescription_required drug_stock字段: id, drug_id, stock_id, current_quantity, warn_quantity, total_quantity, update_time药品设计使用一个单独库存表,而不是直接在主表里放库存数量,是为了支持多库房或不同批次入库。如果医院只有一个小药房,直接在drug表里加字段也可以,但多一个表会让未来的批次追溯更方便。我建议一开始就分开,省得后面拆表痛苦。
3.3 状态字段与逻辑删除设计
这类管理系统里,状态字段命名必须统一。我统一用status存放业务状态,用deleted存储逻辑删除标记。MyBatis Plus的@TableLogic注解可以直接实现逻辑删除:查询时自动追加deleted=0条件,删除时自动变成update语句。这个能力非常适合再也不用物理删表数据。
状态字段的取值必须写清楚注释,或者维护成字典。我踩过一个坑:挂号状态在不同模块中有的用0-1-2,有的用1-2-3,前端判断完还要转一次,后来我统一把状态值从1开始,且每个模块的注释都写进SQL的表结构说明里,才彻底解决混乱。数据库注释、代码常量和前端枚举必须保持一致,这一步不做,后期一定改出问题。
住院表hospitalization的字段设计也需要预先规划床位状态:
id, case_id, pet_id, doctor_id, bed_no, admission_time, discharge_time, status(1在院 2已出院 3取消), care_level, nursing_notes住院和门诊最大的差异是多了一个“每日护理记录”。护理记录不是一次性写完的,护士每天早上要更新体温、喂食、精神状态,所以建了一张nursing_log子表,字段包括温度、心率、食欲、排泄情况、护理备注,这样后续可以打印出清晰的住院日程表。
4. 核心后端接口实现与业务细节
4.1 登录鉴权与JWT拦截器
登录接口使用手机号加密码,密码通过BCrypt加密存储。用户登录成功后,后端返回一个JWT令牌,有效期为2小时。生成令牌时把用户id、角色id写入claims,后续接口通过拦截器解析令牌,从Redis或数据库里取出用户信息。
我使用了JWT Filter加Interceptor的组合方案。自定义一个JwtInterceptor,在配置类里注册到排除登录接口之外的所有路径。拦截器先判断请求头里是否有Authorization字段,然后调用解析工具,解析失败直接返回401。
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); } if (StringUtils.isBlank(token)) { throw new BusinessException("未登录或登录已过期"); } Claims claims = JwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); request.setAttribute("roleId", claims.get("roleId")); return true; } }对于管理员账号,我额外在拦截器里做了一次角色权限检查,防止普通用户直接访问“用户管理”这类接口。这个权限检查建议用注解实现,比如在方法上标注@RequirePermission("system:user:list"),再用AOP切面统一验证,比在方法体里写if判断优雅得多。我只在两个核心模块用了注解,其他模块靠前端菜单控制,整体风险可控,但如果是生产项目,后端必须全部校验,不能指望前端藏按钮。
4.2 挂号与排队叫号流程
挂号接口是整个流程的入口,直接决定后续数据质量。我的挂号保存逻辑是事务性的:插入挂号记录时同时锁定宠物状态,避免同一宠物被重复挂号。具体实现是先在pet表中根据id和status查询宠物是否存在,然后使用悲观锁SELECT ... FOR UPDATE锁住这条宠物记录,再判断当天是否已有未完成的挂号单,最后插入挂号单。
这个并发控制在医院场景里非常必要。之前测试时我用JMeter模拟10个用户同时给同一只宠物挂号,不加锁的情况下生成了7条重复挂号记录,加了锁后只剩1条。代码大致如下:
SELECT * FROM pet WHERE id = #{petId} AND deleted = 0 FOR UPDATE排队叫号逻辑没有采用消息队列,因为医院规模不大,直接用一个内存列表加轮询查询反而更简单。护士或前台点击“叫下一个”时,系统把该科室当前状态下最早一条待就诊挂号单更新为“就诊中”,同时通过WebSocket向前端大屏推送叫号数据。前端大屏在宠物医院大厅显示当前科室和当前被叫号的患者信息,这也是项目展示时的亮点,后期做数据大屏时可以直接复用这套推送消息。
挂号费统计归属到挂号单上,但就诊中的修改需要校验:如果挂号单状态已经是“已完成”,不允许再修改医生。有次前台误操作改掉了一个已完成单的医生,导致司机的业绩统计全乱了。所以我在更新挂号信息的接口里强制判断状态,非待就诊或就诊中的挂号单一律拒绝修改。
4.3 诊疗记录与药品库存扣减
诊疗记录表保存了接诊过程中最重要的业务数据,包括主诉、现病史、检查结果、诊断结论、治疗建议。我设计为每次就诊只生成一条record主记录,处方项目则放在子表。医生在前端点击“保存并开药”时,后端一次性把主记录和子表明细全部提交,使用@Transactional保证原子性。
处方开药时的库存扣减必须做锁并发。我先查询药品库存并锁住行,然后判断当前库存是否大于等于处方数量,不够则直接抛异常并回滚整个处方。扣减库存成功后再记录一条出入库流水,方便以后对账。
@Transactional(rollbackFor = Exception.class) public Result createPrescription(PrescriptionDTO dto) { DrugStock stock = drugStockMapper.selectForUpdate(dto.getDrugId()); if (stock.getCurrentQuantity() < dto.getQuantity()) { throw new BusinessException("药品库存不足,当前库存:" + stock.getCurrentQuantity()); } drugStockMapper.deductStock(dto.getDrugId(), dto.getQuantity()); drugStockLogMapper.insert(DrugStockLog.build(dto.getDrugId(), -dto.getQuantity(), "处方扣减")); // 保存处方明细 }这里有一个容易忽略的坑:处方明细中保存的“单价”必须是从drug表里取出的快照价格,不能在接口中直接信任前端传来的单价。否则前端篡改价格就可以低价结算。我的处理方式是前端只传drugId和数量,后端根据database价格计算总价,确保收费金额准确。
如果开的是检查项目,比如血常规、B超,我使用另外一张examination_item表,并由检验科在系统里登记结果,医生写诊断时可以直接引用检验结果。这样整个诊疗链路就能形成闭环。
4.4 报表统计与数据看板
统计模块我用@RequestMapping("/api/report")单独实现。医院管理员首页看板上要展示今日接诊量、挂号收入、药品消耗金额、热门科室排行。这些指标如果每次实时聚合所有单据,数据量大时响应很慢。我的做法是每天早上由定时任务计算前一天的统计数据存入report_daily_summary表,看板接口只查当天的动态几个汇总列,其余读前一日的汇总即可。当天实时数据量并不大,直接sum几条几个表也没有明显压力。
典型的统计SQL包括:
- 今日接诊量:按visit_date和status计算挂号单数量,排除取消状态。
- 药品消耗排行:按prescription_item聚合drug_id,统计quantity和total_amount。
- 医生工作量:按medical_record聚合doctor_id,统计count和总费用。
我开发时先写临时SQL验证结果,再整合到Mapper的XML中。这样能把预期结果和代码结果对照,避免报表数字对不上业务。后续扩展大屏可视化时,前端调用这些统计接口,返回ECharts能够直接消费的数据格式,不需要额外转换层。
5. 前端页面与交互流程
5.1 架构设计:Vue3 + Element Plus + Axios
前端页面我选择Vue3组合式API方式开发,相比Vue2的选项式API,代码复用性更强。使用Vite作为开发服务器,启动速度飞快。UI组件库用Element Plus,它对后台管理系统的表格、表单、弹窗支持很全面。
项目里统一封装了request.js:
import axios from 'axios' import { ElMessage } from 'element-plus' const service = axios.create({ baseURL: '/api', timeout: 15000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = 'Bearer ' + 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.data }, error => { ElMessage.error(error.response?.data?.message || '网络异常') return Promise.reject(error) } )这样每个页面里调用接口时只需要写业务代码,不用每个接口都处理token和错误信息。跨域开发时,配置vite.config.js的proxy,把/api请求代理到后端8080端口。
5.2 关键交互:宠物档案、门诊开单
宠物档案页面是一个典型的列表加弹窗表单结构。列表支持按会员手机号、宠物姓名、宠物类型搜索。新增宠物表单需要动态级联选择:先选宠物类型(猫/狗/小宠),再选对应品种。品种数据通过后端字典接口加载,这样可以随时在后端维护新的品种,前端不用每次发版。
门诊开单页面是医生的核心工作台。页面左侧是今日待诊列表,点击一个患者后右侧重型抽屉展示宠物资料和历史病历。历史病历里如果是之前的医生写的,只读展示,如果是自己写的,可以继续追加。病历文本我使用了富文本组件,但存储时只存纯文本或简单HTML,避免复杂HTML带来的XSS风险。
处方明细区域使用可编辑表格,每次添加药品时调用药品查询接口,按药品名称模糊搜索,选择后自动带入规格和价格。开单完成后点击提交,后端一次性保存整个处方。这个交互比传统的分页表单更高效,医生的实际使用感受也是这个顺手。
5.3 角色菜单与路由权限控制
前端路由权限通过动态路由实现。登录后前端拿到当前用户角色对应的菜单列表,使用router.addRoute动态添加路由。没有权限的路由不会渲染到侧边栏,而且直接访问URL时也会被全局前置守卫拦截。
实现方式分两步。第一步:路由配置里只保留login、404、dashboard等公共页面。第二步:用户登录后调用getUserMenus接口获取菜单树,前端遍历菜单树并匹配组件路径,然后动态注册。这里工程上有一个组件映射的坑:后台返回的菜单component字段是字符串,比如/views/pet/PetList,而Vue3中不能直接通过字符串加载组件,需要预先建立一个映射对象并配合() => import():
const componentMap = { 'pet/list': () => import('@/views/pet/PetList.vue'), 'registration/create': () => import('@/views/registration/Create.vue') }我一开始偷懒用() => import(menu.component),结果发现打包后加载不到文件,因为Vite无法解析动态变量路径。后来老老实实维护映射表,问题才彻底解决。
菜单权限和按钮权限我使用了两个字段控制:菜单权限控制路由可见性,按钮权限控制表格操作列的“新增/编辑/删除/统计”等按钮是否渲染。操作按钮不是单纯的disabled处理,而是不渲染,这样不相关按钮根本不会出现在DOM里,安全性更高一层。
6. 系统部署与常见问题排查
6.1 本地运行环境和打包部署
本地开发需要JDK 8+、MySQL 8.0、Node 16+。启动后端时先创建数据库,并执行init.sql初始化表结构,然后修改application.yml中的数据库账号密码。后端启动后访问Swagger接口文档地址,可以快速自测所有接口。
前端启动步骤:
npm install npm run dev生产环境我习惯使用Linux服务器,后端打成jar包,直接nohup java -jar pet-hospital.jar &运行,前端打包后把dist目录部署到Nginx中。Nginx配置里最关键的部分是将/api开头的请求转发到后端服务,同时解决前端路由history模式的刷新404问题:
location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }使用history模式时,还需要在Nginx中配置try_files $uri $uri/ /index.html;,不然前端路由刷新时会报404。这几乎是新手部署Vue项目最常踩的坑。
如果没有正式服务器,用docker-compose也能一键部署MySQL、后端和前端。我在项目里提供了三个docker容器配置,但不建议在开发阶段直接用容器,因为调试日志和热更新都不方便。
6.2 常见问题:端口冲突、数据库时区、跨域
我开发过程中遇到频率最高的三个问题如下:
数据库时区报错。MySQL 8.0默认时区是非UTC,如果连接字符串不加serverTimezone=Asia/Shanghai,操作时间字段会报The server time zone value ...错误。正确配置是:
url: jdbc:mysql://localhost:3306/pet_hospital?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true后端启动端口冲突。明明没启动任何Java进程,8080端口却被占用,通常是本机安装了其他中间件。排查时先执行netstat -ano | findstr 8080查看PID,再在任务管理器或kill -9处理,或者临时在application.yml里改为8081端口绕过冲突。
前端跨域请求失败。如果前端直连后端且没有使用代理,浏览器就会出现CORS错误。开发环境必须配置vite代理,生产环境必须配置Nginx反向代理。如果发现浏览器里请求显示200但response为空,也需要检查代理返回头是否被后端CORS配置干扰,此时优先统一使用Nginx代理这一种模式,不要同时开启后端CORS和前端代理,否则可能出现重复响应头问题。
6.3 性能优化与安全建议
对于这个体量的系统,性能优化不需要上缓存中间件。我实际使用的优化策略是:
- 列表查询全部走分页,禁止前端一次加载全表数据。
- 高频查询的字典表使用Spring Cache的
@Cacheable缓存,避免每次查询数据库。 - 药品搜索接口在drug_name字段上建立普通索引,配合MyBatis Plus的like查询,速度和稳定性都够。
安全方面,JWT令牌过期时间设为2小时,同时支持在Redis中存储token黑名单,用户修改密码后旧token强制失效。这个功能我不是通过拦截器实现,而是用了一个自定义注解标记在需要强制校验的接口上。这样做的好处是,管理员重置密码后能真正掐断已经登录的会话。
文件上传功能(宠物照片)也需要防范恶意文件。我限制只允许jpg、png、gif格式,并且上传后重命名成随机字符串,不暴露原始文件名。压缩图片时使用Java原生ImageIO,不依赖额外命令,兼容性和部署都更方便。
7. 实际开发中的经验与易错点复盘
项目做到最后一阶段,我总结了几个容易被忽略但影响体验的细节,单独记录下来供大家参考。
第一,关于时间字段的前后端类型。Java后端返回LocalDateTime时,默认序列化格式是2025-01-01T12:00:00,前端Element Plus的日期组件可能无法直接解析。我在application.yml中设置了全局序列化格式:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8这样所有接口返回时间都是统一字符串,前端展示不用额外转换。
第二,数据库表的主键必须统一回填策略。MyBatis Plus默认使用ASSIGN_ID雪花算法生成Long型主键,但我在初始化脚本里把主键设置成自增,导致插入时主键冲突。最终我在全局配置中固定使用IdType.AUTO,与数据库自增特性保持一致。如果没有统一配置,不同的表出现两种主键生成方式,分页和导出时会隐现各种诡异问题。
第三,删除操作要慎重。我规定核心业务表全部使用逻辑删除,比如会员、宠物、挂号单、诊疗记录都不能物理删除。只有系统操作日志表可以根据时间批量清理。因为宠物医院的病历属于长期保存资料,将来随时可能会被复查或保险理赔调取,一旦物理删除就无法恢复。
第四,手动插入测试数据时务必让外键关系真实。为了快速看效果,我一开始直接通过SQL脚本给宠物和会员插入了伪造数据,结果宠物关联了一个不存在的会员,导致会员详情页出现乱码。后来我发现写一个测试数据生成脚本比手工录入靠谱得多,脚本会根据真实的外键ID来生成关联数据,再跑到前端页面上看效果,体验差距一下子出来了。
如果你想把这个项目从后台管理扩展到移动端,我的建议是先复用现有的接口,再补三个专属移动端接口:宠物主人查看宠物档案、在线预约挂号、查询历史就诊记录。小程序端不需要独立的统计模块,但需要处理微信授权登录与自己系统账号的绑定。大屏可视化方面,目前的报表接口已经按日期和科室聚合,前端用ECharts可直接绘制柱状图和饼图,唯一需要增加的是动画刷新逻辑。
我自己在跑通全流程后最大的体会是:这类管理系统最大的技术难度不在某个框架使用上,而在业务流程和数据一致性上。把业务关系理清、事务边界划清、状态流转管住,后端代码自然就清晰了。宠物医院系统表面上只是普通的信息管理系统,但因为涉及活体健康数据,设计时更需要有敬畏心。希望这篇拆解能给你一个完整的参考,少走我走过的弯路。如果你正在基于Spring Boot开发类似系统,遇到具体问题也可以按模块去排查,多数疑难杂症都会落在表关系、事务边界和状态管理这三件事上。