☰
Spring Boot宠物医院管理系统:从需求设计到答辩加分的实战指南
2026/10/8 2:39:06 网站建设 项目流程

1. 为什么宠物医院管理系统是毕业设计的"高性价比答案"

每年到了毕业设计开题季,后台私信里问得最多的就是"学长,Java方向选什么题目好"。我的回答通常很简单:去你们学校周边的宠物医院坐一小时,比在网上刷一百个题目都管用。

说实话,Spring Boot宠物医院管理系统这个选题,在计算机毕业设计里算是"老牌经典"了,经典不代表没价值,恰恰是因为它踩中了毕业设计最需要的几个点。

1.1 从宠物医疗行业现状看选题背景

先聊点实际的。养宠人群这几年增长很快,宠物医院的数量也越来越多,但很多中小型宠物诊所的管理方式还停留在手写病历、Excel记流水账、微信群里喊排班的阶段。宠物看病和人生病不一样,宠物不会说话,病情描述全靠主人转述,病史记录、疫苗档案、过敏信息一旦丢失,后续诊疗的准确性会受到很大影响。

这种行业现状意味着什么?意味着宠物医院管理系统这个选题有真实的业务土壤,不是凭空捏造的假需求。你在毕业论文里可以很自然地写"为了解决宠物诊疗信息碎片化、预约排班效率低、药品库存管理混乱等问题,设计并实现了该系统",这句话有行业背景支撑,答辩老师一听就知道你是认真调研过的,而不是从网上抄了个题目。

1.2 毕设技术覆盖面评估

再从技术角度算一笔账。这个选题覆盖的技术点,恰好是Java后端开发岗位面试里最常被问的几块:

技术点在系统里的落点面试可聊深度
Spring Boot整体框架、自动配置、starter机制中
MyBatis/MyBatis-Plus数据持久层、动态SQL、多表关联较高
权限认证JWT登录、拦截器、角色权限控制较高
RESTful API接口设计规范、状态码语义中
前端框架Vue/Element UI 或 Thymeleaf中
数据库设计多表关系、索引、事务较高

一个系统能把"预约挂号—接诊开方—划价收费—药品出入库"这条完整链路跑通,技术上横跨了业务建模、数据持久化、前后端交互、权限控制这些核心环节。这比单纯做个"图书管理系统"或者"学生信息管理系统"的含金量高出一截,因为它涉及多角色协同和真实业务状态流转,而CRUD只是基本功。

另外一个容易忽略的点:这个选题的素材密度足够写出一篇像样的毕业论文。很多同学毕设做完了,论文憋不出来,就是因为系统功能太寡淡,业务逻辑太简单,没东西可写。宠物医院管理系统不同,它的业务天然复杂——宠物和主人的一对多关系、预约与排班的冲突校验、处方和库存的联动扣减、多角色权限隔离,每一个小节都能展开写两三千字。选题选得好,论文的事先解决一半。

所以我的结论很直接:如果你确定用Java技术栈做毕设,宠物医院管理系统是一个风险低、上限高、好答辩的题目。接下来我把这套系统从功能拆解到落地实现的完整思路捋一遍,你哪怕不照着敲代码,也能把整个系统的设计逻辑吃透。

2. 系统功能边界:做全但不做杂的三层模块划分

很多同学做毕设容易犯一个毛病——功能越加越多,最后做出一个四不像。宠物医院管理系统如果无脑堆功能,可以做成:商城、寄养、美容、论坛、在线问诊……但那是产品经理的事,对毕设来说,核心业务链路越清晰,系统完成度才越高。

我给这套系统定了一个原则:一切功能围绕"一只宠物从进门到离开医院"这条主线展开。基于这个原则,系统拆成三个端、两大主流程。

2.1 面向客户的C端服务:预约、宠物档案、记录查询

C端服务解决的是宠物主人的需求。一个宠物主人走进一家陌生医院,最关心的三件事是:怎么挂号不排队、医生能不能快速了解我家宠物的病史、看完病后去哪里查病历和费用。

所以我明确划分了三个模块:

  • 宠物档案管理:一位主人可以绑定多只宠物,每只宠物维护品种、年龄、性别、体重、绝育状态、过敏史、疫苗记录。这里有个细节:过敏史必须单独建表或者字段标注,因为这个信息在接诊时直接关系用药安全,要能在医生工作台首页高亮显示。
  • 在线预约挂号:选择科室(内科、外科、皮肤科、影像科、体检科)、选择医生、选择时间段。预约要校验三个条件:医生在该时间段是否有排班、该时间段是否已被约满、宠物是否已建档。
  • 历史记录查询:主人可以查看宠物历次就诊的处方明细和费用记录,方便追溯和报销。

C端功能我建议用 Vue 或类似前端框架做成一个独立页面,如果前端基础弱,用 Thymeleaf 模板引擎也可以,但接口层面一律按 RESTful 风格设计,这样无论前端怎么换,后端都不用动。

2.2 面向医护的诊疗工作台:接诊、病历、处方、药品

这是系统的心脏部分,也是答辩时最能展示技术深度的地方。

医生登录后进入工作台,看到的应该是一串待接诊的预约列表,点开任意一条,左侧是宠物档案和过敏史高亮提醒,右侧是病历填写区。病历表单至少要包含:主诉、现病史、初步诊断、检查项目、治疗方案。填写完成后,医生可以生成处方——从药品库中选择药品、填写用量用法、系统自动按库存余量校验并计算出费用。

这个地方我特别想强调一个细节:处方和库存的联动逻辑。开处方时不能只做记录,后端必须在生成处方明细的同时扣减库存,并且开启事务控制。如果同学在答辩时能主动讲出"我在生成处方的方法上加了@Transactional,一旦库存扣减失败,整个处方会回滚,不会出现处方开了但药房没货的脏数据",这个技术敏感度很容易给答辩老师留下印象。

2.3 面向管理者的运营后台:排班、收费、库存、统计

管理端的功能定位是"让人省心",核心模块包括:

  • 医生排班管理:管理员按周设置医生出诊时间,排班数据直接喂给预约模块做校验。
  • 收费与结算:挂号费、检查费、药品费、住院费等收费项汇总。这里建议引入"收费单"概念,而不是让处方直接关联支付,因为一次就诊可能包含多项费用。
  • 药品库存管理:药品入库、出库、低库存预警。库存低于阈值时,系统在后台醒目提示补货。
  • 数据统计分析:用ECharts展示每日接诊量、科室热度、药品消耗排行、营收趋势。这部分是答辩加分项,后面单独展开说。

三个端的功能加在一起,覆盖了宠物医院最核心的日常运营场景,既不会因为功能太少显得单薄,也不会因为功能太杂导致做不完。记住一个原则:模块可以多,但每个模块的业务逻辑必须闭环——比如预约模块必须跟排班联动,处方模块必须跟库存联动,这是"闭环"的意思,也是系统设计水平的体现。

3. 数据库设计:宠物医疗业务的关键表和字段建模

数据库设计是系统设计的底盘。很多同学在建表环节偷懒,想着反正是MyBatis-Plus自动生成CRUD,表结构随便建建就行。我劝你别这么干——答辩老师最常翻的就是ER图和数据库设计文档PDF,表关系对不对、字段理由清不清楚,一眼就能看出来。

3.1 九张核心表的关系梳理

基于上面三个端口的业务分析,这套系统的核心表我建议至少设计以下九张:

表名用途关键外键/字段
user系统用户(管理员/医生/前台/客户)user_type区分角色
pet宠物档案owner_id关联user
doctor医生信息扩展user_id关联user
schedule医生排班doctor_id, work_date, period
appointment预约挂号pet_id, doctor_id, schedule_id, status
medical_record诊疗记录/病历pet_id, doctor_id
prescription处方主表record_id, total_amount
prescription_item处方明细prescription_id, medicine_id, quantity
medicine药品库存stock, sale_price, warning_line

如果还想加住院管理或疫苗管理,各加一张表即可。九张表对于毕设来说不多不少,既能体现多表关联查询的能力(比如"查询某位主人的全部宠物及每次就诊记录"至少跨四张表),又不会把自己陷入过度设计。

3.2 容易建模错误的几个业务字段

这些细节是实战中踩出来的,建表时一次性考虑到位,后面能省很多事:

第一,预约状态字段。不要只设一个status字段简单地存"已预约/已完成"。预约有完整的状态机:待就诊→已就诊→已完成,中间还可能有"已取消"和"超时未到"。我的建议是状态字段用int类型,约定0待就诊、1已就诊、2已完成、3已取消、4爽约,并且加一个remark字段记录状态变更原因。状态机清晰了,业务逻辑才不会写成一团乱麻。

第二,宠物和主人的关系。一位主人可以带多只宠物来就诊,所以主人和宠物是一对多。同时要思考一个问题:主人信息放user表还是单独建owner表?我的建议是统一放user表,用user_type区分,这样登录体系只做一套,权限靠角色区分。表结构上会增加一点冗余,但对毕设来说,简化登录逻辑的价值远大于那点冗余成本。

第三,金额字段。涉及费用的一律用DECIMAL(10,2),不要用float或double。浮点数精度问题在涉及钱时是致命伤,虽然毕设阶段看不出什么大问题,但答辩老师如果是个有经验的后端工程师,看到你用double存金额,等于主动暴露基础不扎实。

3.3 建表SQL设计参考

这里给出宠物档案和预约两张核心表的参考结构,其余表可以仿照这个思路设计:

CREATE TABLE `pet` ( `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '宠物ID', `owner_id` INT NOT NULL COMMENT '主人ID,关联user表', `pet_name` VARCHAR(50) NOT NULL COMMENT '宠物昵称', `species` VARCHAR(20) NOT NULL COMMENT '物种:dog/cat/rabbit等', `breed` VARCHAR(50) DEFAULT '' COMMENT '品种', `gender` TINYINT DEFAULT 0 COMMENT '性别:0未知 1公 2母', `birth_date` DATE DEFAULT NULL COMMENT '出生日期', `weight` DECIMAL(5,2) DEFAULT NULL COMMENT '体重kg', `neuter_status` TINYINT DEFAULT 0 COMMENT '绝育:0否 1是', `allergy_info` VARCHAR(255) DEFAULT '' COMMENT '过敏史,接诊时高亮展示', `avatar` VARCHAR(255) DEFAULT '' COMMENT '宠物照片URL', `deleted` TINYINT DEFAULT 0 COMMENT '逻辑删除标记', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物档案表';
CREATE TABLE `appointment` ( `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '预约ID', `pet_id` INT NOT NULL COMMENT '宠物ID', `owner_id` INT NOT NULL COMMENT '主人ID,冗余存储方便查询', `doctor_id` INT NOT NULL COMMENT '医生ID', `schedule_id` INT NOT NULL COMMENT '关联排班', `appoint_date` DATE NOT NULL COMMENT '预约日期', `time_slot` VARCHAR(20) NOT NULL COMMENT '时间段:09:00-09:30', `status` TINYINT DEFAULT 0 COMMENT '0待就诊 1已就诊 2已完成 3已取消 4爽约', `remark` VARCHAR(255) DEFAULT '' COMMENT '备注或取消原因', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uk_doctor_slot` (`doctor_id`, `appoint_date`, `time_slot`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约挂号表';

这里有个特别值得说道的设计:预约表上的唯一索引uk_doctor_slot。它保证了同一个医生在同一个时间段的预约记录不会重复插入——这是第一道数据库层面的兜底,防止并发情况下两个人同时抢到最后一个号。我在第5章会再详细讲这个机制的重要性。

顺带提醒一下,如果你用MySQL,表结构一定要用utf8mb4字符集,因为宠物系统里可能存在生僻字符或emoji(有的宠主喜欢在宠物昵称里加emoji),不处理的后果就是一次意外的数据写入报错,检查半天才发现是字符集问题。

4. Spring Boot后端落地:从搭建到核心接口实现

数据库结构理顺之后,后端编码就变成一件按部就班的事。但"按部就班"不代表没有讲究,这里我把项目搭建到核心接口实现的完整思路和关键代码骨架走一遍。

4.1 项目结构与版本选型

版本选择是很多人刚开始就容易踩坑的地方。我的建议是:如果对Spring Boot不是非常熟,优先选Spring Boot 2.7.x系列,搭配JDK 8。2.7.x是2.x系列的最后一个稳定分支,网上资料最多、兼容性最好、第三方集成范例最全。Spring Boot 3.x虽然更新,但它强制JDK 17,且部分旧版依赖和新版starter之间存在兼容问题,对毕设来说收益小于风险。别在版本选择上追求"最新",毕业设计的核心是稳定跑通、逻辑完整。

后端项目结构建议采用标准的Maven多包分层结构:

com.example.pethospital ├── controller # 接口层,接收请求参数,返回统一结果 ├── service # 业务逻辑层,核心事务处理 ├── mapper # MyBatis持久层接口 ├── entity # 数据库实体类 ├── dto # 数据传输对象,接收前端参数 ├── vo # 视图对象,返回前端数据 ├── config # 配置类:跨域、拦截器、web配置 ├── common # 通用类:统一返回结果、异常处理器、枚举 └── util # 工具类:JWT工具、时间格式化

这里有个很多同学容易忽略的细节:DTO、VO、Entity三者要分离。Entity对应数据库字段不能随意改动;DTO用来接收前端传来的参数,比如添加宠物时的JSON;VO用来返回给前端展示的数据,比如富化后的宠物信息带主人姓名。三者混用的坏处是:你的接口参数将被迫暴露数据库表结构,前端改一个字段名,连带实体类和多个接口都要改。分层清晰之后,数据库表结构变动时,只需要调整DTO和VO的装配逻辑,接口的对外契约保持稳定。

4.2 基于JWT的登录鉴权和角色控制

宠物医院系统里有三类角色——管理员、医生、客户,每个角色能访问的接口必须隔离。我的实现方案是Spring Boot + JWT + 拦截器。

JWT(JSON Web Token)的机制可以这么理解:用户登录成功后,后端生成一个加密的token字符串返回给前端,前端此后每次请求都在Header里带上这个token,后端解析token判断用户是谁、属于什么角色。不需要在服务端保存session,天然适合前后端分离。

核心代码结构大致如下:

@Component public class JwtUtil { private String secret = "你的自定义密钥字符串"; private long expire = 7 * 24 * 3600 * 1000L; // token有效期7天 public String createToken(Integer userId, String userType) { return Jwts.builder() .claim("userId", userId) .claim("userType", userType) .setExpiration(new Date(System.currentTimeMillis() + expire)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } }
public class AuthInterceptor implements HandlerInterceptor { @Autowired private JwtUtil jwtUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains("/auth/login")) { return true; } String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { throw new BusinessException(401, "未登录或登录已过期"); } Claims claims = jwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); request.setAttribute("userType", claims.get("userType")); return true; } }

再配合一个拦截器注册配置类,把需要拦截的路径和放行的路径都配好。拦截器校验出角色之后,具体接口里的角色判断可以用自定义注解或aop实现。比如医生工作台中接诊接口,方法里可以直接判断userType是否等于"DOCTOR",不是则抛权限不足异常。这一步做完,整个系统的权限骨架就很扎实了,答辩时可以重点展示拦截器里如何放行静态资源和登录接口、如何统一处理token过期。

4.3 预约冲突校验:核心业务逻辑的实现思路

预约模块是系统里最具技术含金量的一个接口,它必须同时处理多层校验逻辑:

  1. 宠物是否存在且属于当前登录用户;
  2. 选择的医生排班是否存在;
  3. 该时间段是否已被占满;
  4. (可选)当前用户在该时间段是否已有一个预约。

第3条的实现,除了数据库唯一索引兜底,业务层还需要预先检查该时间段的当前预约数量是否达到上限。比如某位医生一个时间段最多接待2只宠物,那预约记录里该时间段的非取消状态记录数达到2时,下一个预约请求直接拒绝。

代码逻辑骨架如下:

@Service public class AppointmentService { @Transactional public Appointment createAppointment(AppointmentCreateDTO dto) { // 1. 校验宠物归属 Pet pet = petMapper.selectById(dto.getPetId()); if (pet == null || !pet.getOwnerId().equals(currentUserId())) { throw new BusinessException("宠物不存在或不属于当前用户"); } // 2. 校验排班 Schedule schedule = scheduleMapper.selectByDoctorAndDate(dto.getDoctorId(), dto.getAppointDate()); if (schedule == null || !schedule.isAvailable()) { throw new BusinessException("该医生当日无排班"); } // 3. 校验时间段容量 Integer count = appointmentMapper.countByDoctorAndSlot(dto.getDoctorId(), dto.getAppointDate(), dto.getTimeSlot(), AppointmentStatus.BOOKED); if (count >= schedule.getSlotLimit()) { throw new BusinessException("该时间段已约满"); } // 4. 插入预约记录,依靠唯一索引兜底 Appointment appointment = new Appointment(); // ... 字段装配 appointmentMapper.insert(appointment); return appointment; } }

为什么这个方法要加@Transactional?因为预约创建不是单条insert的事,它实际上是"校验+写入+可能的通知推送"一系列操作的组合。一旦后面的步骤失败,前面的写入必须回滚,否则会出现一个"预约状态显示未支付但占住了号"的脏记录。这个就是事务一致性的意义,也是你在论文里可以写两段的内容。

4.4 诊疗记录和处方生成的联动设计

医生完成接诊后填写病历,生成处方。这里我采用"主从表"结构:处方主表记录本次开方总金额和关联接诊记录,处方明细表记录每种药品的数量、单价、用法。前端页面提交的JSON结构大概是:

{ "recordId": 1, "items": [ { "medicineId": 10, "quantity": 2, "usage": "每日一次,每次一片" }, { "medicineId": 12, "quantity": 1, "usage": "外用,涂抹患处,每日两次" } ] }

后端的处理逻辑里有两件事必须做对:

  • 总金额计算:不能信任前端传过来的金额。服务端必须根据medicineId查出实时售价,乘以数量累加,防止用户篡改价格参数。
  • 库存扣减:遍历明细,逐项检查库存是否充足,任一不足则整体抛异常回滚,全部通过后批量扣减库存。
@Transactional public PrescriptionVO createPrescription(PrescriptionCreateDTO dto) { // 计算金额并检查库存 BigDecimal total = BigDecimal.ZERO; List<PrescriptionItem> items = new ArrayList<>(); for (ItemDTO item : dto.getItems()) { Medicine medicine = medicineMapper.selectById(item.getMedicineId()); if (medicine.getStock() < item.getQuantity()) { throw new BusinessException("药品[" + medicine.getName() + "]库存不足"); } BigDecimal amount = medicine.getSalePrice().multiply(BigDecimal.valueOf(item.getQuantity())); total = total.add(amount); // 装配明细对象 // 扣减库存 medicineMapper.reduceStock(medicine.getId(), item.getQuantity()); } // 插入主表 + 明细表 // 返回含金额和明细的VO }

前后端交互里,前端拿到返回的处方VO后展示总金额和明细给用户确认,确认后走支付流程或者直接生成费用单——如果只做管理端,这一步到"费用单生成"就可以收尾了。

5. 实测踩坑记录:版本冲突、MyBatis映射和联调问题

写代码这件事,最怕的是"看起来什么都对,跑起来哪里都不对"。这一章我把自己实际开发这套系统过程中踩过、并且大概率你也会踩的坑,按照出现顺序完整记录一遍。这些坑单看每个都不大,但加起来消耗掉的调试时间,足够写两章论文了。

5.1 Spring Boot版本与MyBatis-Plus的兼容性陷阱

先说一个很多新手会卡住的场景:你跟着网上教程引入了mybatis-plus-boot-starter,启动类上加@MapperScan也加了,结果一启动就报Invalid value type for attribute 'factoryBeanObjectType': java.lang.String。

这个报错十有八九是MyBatis-Plus版本和Spring Boot版本不匹配。Spring Boot 3.x要求MyBatis-Plus使用3.5.3以上的适配版本,如果你在Spring Boot 3项目里用了3.4.x甚至更早的starter,启动就会挂。解决方案也很简单:用Spring Boot 2.7.x就搭配mybatis-plus-boot-starter 3.5.2;用Spring Boot 3.x就搭配mybatis-plus-spring-boot3-starter 3.5.5+。

我的建议在前面也说过——毕设优先Spring Boot 2.7.x + JDK 8 + MyBatis-Plus 3.5.2,这套组合经过大量项目验证,遇到的坑基本上都能搜到现成答案,不至于卡在一个依赖问题上耗掉两天。

5.2 一对多查询时resultMap的映射细节

查询"某位主人的全部宠物及每只宠物的最近预约记录"这类需求,天然是一对多查询。很多同学刚接触时会在Mapper接口里写一个自定义SQL,然后用List<PetVO>接收,跑到前端死活发现appointmentList是空的。

问题通常出在resultMap的配置上。collection标签里的column属性,必须确保SQL查询结果集中有对应列名的字段。举个例子:

<resultMap id="PetWithAppointmentsMap" type="com.example.vo.PetVO"> <id property="id" column="pet_id"/> <result property="petName" column="pet_name"/> <collection property="appointmentList" ofType="com.example.vo.AppointmentVO" column="id" select="selectAppointmentsByPetId"/> </resultMap>

这种select嵌套写法是先查宠物列表,再逐条发起二次查询获取预约列表。它能跑通,但有个性能隐患:N+1查询——查10只宠物会产生10次额外SQL。数据量大的时候响应会明显变慢,但毕设阶段数据量不大,这么写问题不大。想优化的话,可以改成一条SQL用JOIN查询后用collection直接映射嵌套结果,写法稍微复杂一些,但一次查询全部搞定,这也是我在论文中详细展开的优化方向。

5.3 前后端联调中的日期格式和跨域问题

前端页面写好后联调,最常见的两个问题:日期格式变了、请求被跨域拦截了。

日期问题:后端返回的LocalDateTime默认序列化格式是yyyy-MM-dd'T'HH:mm:ss,中间带了个T,前端拿到这个字符串直接显示会很奇怪。解决办法是在配置类或application.yml中统一配置Jackson的日期格式:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

如果你的实体里有LocalDate字段,还需要加一个依赖:jackson-datatype-jsr310(Spring Boot通常已经内置),并配合@JsonFormat(pattern = "yyyy-MM-dd")注解。

跨域问题:前端跑在8080端口,后端跑在9090端口,前端发请求浏览器会直接拦截响应。解决办法是后端配置一个CorsFilter或实现WebMvcConfigurer的addCorsMappings方法,允许指定来源的跨域访问。开发环境下把allowedOriginPatterns设为*,直接全部放行,省心。

5.4 逻辑删除与唯一索引的恩怨

宠物档案表上我建议加了deleted逻辑删除字段,这是为了避免误删后数据无法恢复。但逻辑删除和唯一索引之间有一个隐藏冲突:如果某张业务表的某个字段设了唯一索引,删除时走逻辑删除(deleted=1),那同一字段再次插入相同值时,唯一索引会认为记录已存在。

举一个真实例子。medicine药品表如果想要药品名称唯一,但用户不小心删掉了一个药,下次重新添加同名药品时,因为那条逻辑删除的旧记录还占着唯一索引的位置,insert直接报Duplicate entry。

解决方案有三条路:

  • 唯一索引改成联合索引,把deleted字段纳入其中,即UNIQUE KEY(name, deleted);
  • 删除时给名称字段追加一个时间戳后缀(阿莫西林_20240601123456),代价是数据可读性变差;
  • 放弃对该字段的唯一约束,在业务层做重名校验。

这三种方案我倾向第一种——联合唯一索引在保留约束兜底的同时,代码改动量最小。每个表涉及逻辑删除和唯一索引时都要过一遍这个检查,这是新手极其容易踩的隐性坑。

5.5 事务不回滚:一个容易被忽视的坑

最后补充一个特别隐蔽的问题。Spring的@Transactional默认只在RuntimeException(运行时异常)时触发回滚,如果你在事务方法里抛的是自定义的Exception的子类(非RuntimeException),事务不会回滚,数据会部分写入。

很多同学自定义了BusinessException,结果继承的是Exception,不是RuntimeException。于是一遍遍调试发现"为什么报错了数据还是写进去了",怎么都找不到原因。

解决办法是在@Transactional注解里显式声明回滚条件:

@Transactional(rollbackFor = Exception.class)

或者更简单——让你的BusinessException继承RuntimeException。这两招二选一,都能避免事务失效的尴尬。这个坑我第一次踩时花了整整一个晚上排查,说出来都是泪。

6. 答辩加分项:从"系统能用"到"系统有思考"

系统跑通了、论文写完了,这时候大多数同学会松一口气,觉得万事大吉。但我想告诉你:同样是两个能运行的系统,答辩评分可以差出两个档次,差距就在"你有没有在系统里体现工程化思考"。

6.1 统计报表模块:让数据自己说话

我强烈建议在系统里加一个数据统计可视化模块,理由很简单:这是成本和收益比最高的加分项。

技术上,后端只需要提供一个统计接口,比如按日统计接诊量:

@GetMapping("/admin/stats/appointments") public Result<List<Map<String, Object>>> getAppointmentStats( @RequestParam String startDate, @RequestParam String endDate) { return Result.success(appointmentMapper.statsByDate(startDate, endDate)); }

Mapper里写一条简单的分组查询:

SELECT appoint_date AS date, COUNT(*) AS total FROM appointment WHERE appoint_date BETWEEN #{startDate} AND #{endDate} AND status IN (1, 2) GROUP BY appoint_date ORDER BY appoint_date

前端用ECharts画一张折线图,接诊量趋势一目了然。再配一张环形图展示各科室接诊占比、柱状图展示药品销售Top10。这三张图一出,整个系统的管理层面向就出来了。答辩时你可以说:"该系统不仅支持日常业务办理,还能为管理者提供运营决策的数据支撑"——这句话是实打实的系统性思考,而不是空喊口号。

6.2 值得展示的"拟解决关键技术问题"

答辩时很少有老师会逐个页面看功能,他们更爱问"你在这个过程中解决了什么有难度的问题"。我建议你在准备答辩PPT时,专门准备三个"关键技术难点及解决方案"的小故事:

  1. 预约并发冲突如何兜底——讲述唯一索引+事务的组合策略,展现数据库设计能力;
  2. 处方与库存的一致性保障——讲述@Transactional整体回滚和库存预检查的逻辑,展现业务闭环思维;
  3. 多角色权限如何隔离——讲述JWT+拦截器+角色注解的权限模型,展现通用设计方案能力。

每个小故事按照"问题背景→技术选型→实现方案→踩过的坑→最终效果"五步来讲,既显深度又有真实感,比平铺直叙"我做了一个管理系统"高到不知哪里去了。

6.3 后续扩展方向:给评委一个"有潜力"的印象

如果答辩时被问"系统还能怎么改进",千万不要说"还没想过"。提前准备两三个靠谱的扩展方向:

  • 消息推送:预约成功、就诊提醒通过短信或小程序订阅消息触达用户,需要引入消息队列或第三方推送服务。
  • 在线问诊:增加图文咨询或远程复诊功能,需要引入IM或WebRTC能力。
  • 药品效期管理:增加药品批次和有效期的追踪,过期预警,这需要建模上更精细,也贴近行业真实痛点。
  • AI辅助诊断:基于历史病历数据做辅助分诊或用药建议,可以聊一些简单的规则引擎或模型思路。

这几个方向都是当前行业真实关注的话题,而且你有已实现的系统作为底座,每一步扩展都有明确的落点,答辩老师听到这里通常会有一种"这小子是认真做过行业调研"的感觉。

最后分享几个实战中的体会

做完这套系统最大的感受是:毕业设计最怕的不是题目难,而是做了一大堆功能却讲不清楚设计逻辑。宠物医院管理系统好就好在业务链路天然完整,从预约到接诊到开药每一步都有前后依赖关系,这种依赖关系本身就是你论文里最好的叙事线索。

如果你时间紧张,我的建议是先从数据库表和核心业务流程入手,把预约和处方两条链路先打通,再补管理端和统计模块。代码宁可写得少一点,也要把事务、权限、异常处理这些工程基础做扎实——因为答辩时真正能为你撑腰的,永远是那些能讲清楚"为什么这么设计"的地方。祝你顺利。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询