ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Spring Boot与MyBatis的Java宠物管理系统设计与实现

基于Spring Boot与MyBatis的Java宠物管理系统设计与实现

简介:基于Spring Boot与Vue实现的一套宠物管理系统完整项目,面向Java毕业设计、课程作业以及前后端开发初学者,适合需要快速掌握业务系统整体构建流程、完成相似管理功能开发的读者参考。压缩包共471个文件,整体约10.81MB,包含154个Java后端源码和111个Vue前端组件,另有JavaScript、CSS、XML等资源与配置,以及SQL数据库脚本、YML环境配置和DOC说明文档,能方便地初始化数据库并启动项目。已有132人学习使用。系统按常见管理业务划分模块,包括用户注册登录与角色权限、宠物档案添加编辑与照片上传、健康档案与疫苗接种提醒、服务预约与评价、社区论坛与私信互动、日志审计与数据备份恢复等。通过学习该资源,可直观理解Spring Boot接口与Vue页面的数据交互,体会权限控制和业务分层的实现方式,也能为课程设计报告撰写、系统功能演示提供完整的可运行示例。

1. 一个宠物管理系统,卡在前台 Excel 里的管理系统

很多宠物医院、宠物店的前台,到现在还在用 Excel 记宠物档案:姓名、品种、疫苗日期、上次就诊记录全堆在几张表里,查一次要翻半天,换个电脑还得靠微信传文件。java宠物管理系统 设计与实现这个方向,解决的就是这种「信息在表格里,但不在业务里」的尴尬——用一套 Java Web 系统把宠物档案、预约、病历、疫苗接种提醒串起来,让前台能查、医生能写、老板能看统计。这篇笔记按我实际做过的方案讲清楚:技术选型为什么是 Spring Boot + MyBatis、数据库怎么设计才能扛住后续扩展、核心功能的关键代码怎么写、以及那些不跑一遍根本发现不了的坑。适合正在做课程设计或毕业设计的同学,也适合想拿管理系统练手 Java 后端的小团队。

2. 技术选型:Spring Boot + MyBatis + JWT,哪里比老牌 SSM 更适合这个规模

2.1 为什么是 Spring Boot 而不是 SSH 或 SSM

早期做管理系统,教科书里常见 Struts + Spring + Hibernate,后来流行 SSM(Spring + Spring MVC + MyBatis)。这两个方案不是不能做宠物管理系统,但你会发现大量时间花在「配置」而不是「业务」上。XML 里写一堆 bean 定义、数据源、事务管理器,还没开始写宠物档案,先折腾了两天环境——尤其是新手跟着教程走,卡在 xml 命名空间报错的情况太多了。换成 Spring Boot 之后,内嵌 Tomcat、自动配置、约定优于配置,一个 main 方法起来就是一个可运行的 Web 服务,这对「设计与实现」这种需要快速看到效果的项目来说,价值非常大。

我在选型时一般遵循两条原则:第一,团队里最熟的人用什么就用什么,不要为了简历好看引入没人能维护的技术;第二,项目规模决定复杂度,宠物管理系统的核心是宠物档案 CRUD、预约流程、病历记录、接种提醒,这些用 Spring Boot + MyBatis 已经覆盖得非常好,杀鸡用不上微服务那一套。如果你在写毕业设计,答辩老师问「为什么不用 SSH」,回答「SSH 的配置成本高,Spring Boot 的自动配置和生态更适合快速落地业务,且社区活跃、遇到问题容易查」就是很稳的答案。

2.2 工程分层与包结构设计

做「设计与实现」类的项目,最怕的是所有代码堆在 controller 里,一按 Ctrl+Shift+F 满屏的重复代码。这个项目我建议按常见的四层结构拆:

com.petms ├── controller // 接收 HTTP 请求,做参数校验和结果封装 ├── service // 业务逻辑层,事务边界在这里 ├── mapper // MyBatis 数据访问层,对应数据库表 ├── entity // 数据库实体类 ├── dto // 与前端交互的数据对象,避免直接暴露实体 ├── config // 全局配置:JWT 拦截器、跨域、WebMvc 配置 ├── common // 统一返回结果、异常处理、常量 ├── utils // 工具类:JWT 工具、日期工具 └── PetApplication.java

这个结构对应的是「controller 只做转发和校验,service 处理业务,mapper 只管 SQL」的原则。县体到宠物管理系统,宠物档案的新增可以拆成:controller 接收 PetDTO 并校验必填项、service 层判断宠物主人是否存在并写入宠物表、mapper 执行 insert 语句。这样做的好处是后续加「同一主人最多登记 5 只宠物」这种规则时,只改 service 层,不碰 controller 和 mapper。

有个容易被忽略的细节点:entity 直接用 MyBatis-Plus 的注解映射数据库表,而 controller 层接收的是 DTO,两者不要混用。有个容易被忽略的细节点是:entity 里的数据类型不要用基础类型 int、long,要用包装类型 Integer、Long,否则查询结果为空时会因为自动拆箱抛空指针异常。比如宠物表有个 owner_id 字段,数据库为空时 MyBatis 会映射成 null,实体类是 int 的话直接 NPE。

2.3 认证选型:JWT 还是 Session

宠物管理系统的用户分两类:前台接待和医生。权限其实不复杂,但一定要有登录态校验,否则任何人都能调接口删档案。Session 方案简单,但前后端分离部署时跨域要处理 Cookie,而且集群部署时要引入 Redis 共享 Session——对于这个规模有点重。我用 JWT(JSON Web Token)的常见做法:用户登录成功后生成一个带过期时间的 token 返回给前端,前端每次请求在 Header 里带 token,后端用一个拦截器解码 token 并校验用户身份。

JWT 在宠物管理系统这种内部系统里的好处是「无状态」:后端不需要存 Session,重启服务不丢登录态,扩展多实例时也不用配置 Session 同步。坑在于 token 一旦签发,在过期之前是控制不了的,所以过期时间不要设太长,管理后台的场景 8 小时比较合适。另外,校验 token 的拦截器要排除登录接口和静态资源,不然用户第一次访问就被拦在门外。拦截器里拿到用户信息后放到 ThreadLocal 或直接塞进请求参数,service 层再拿去读当前的用户 id,用来做操作日志的登记人字段。

3. 数据库设计:五张表撑起宠物档案与就诊闭环

3.1 核心表拆解:owner、pet、record、appointment、vaccination

宠物管理系统的数据库设计是整个项目的根基。表设计错了,后面写代码全是补丁。我按业务闭环拆成五张核心表:宠物主人表(owner)、宠物表(pet)、就诊记录表(medical_record)、预约表(appointment)、疫苗接种表(vaccination)。为什么不把宠物和主人的信息塞在一张表里?因为一个主人可以带多只宠物来就诊,一对多在这种场景里是明确的业务语义,拆成两张表后续统计「每位主人名下的宠物数」就是一条 group by 的事情。

owner 表除了姓名、电话、地址外,至少要有 created_time 和 updated_time 两个时间字段。pet 表的关键字段是 name、breed、gender、birthday、weight、owner_id,其中 owner_id 是外键语义。medical_record 表记录每次就诊的病情描述、诊断结果、用药信息、就诊医生、就诊时间。appointment 表处理预约排期:预约日期、时间段、预约项目、状态(待确认/已完成/已取消)。vaccination 表记录疫苗名称、接种日期、下次接种日期——这个表是后续做接种提醒的数据来源。

3.2 建表 SQL 与字段含义说明

直接给一段可执行的 DDL,注意我在字段注释里写了业务含义,这个习惯在答辩和交接时很加分。

CREATE TABLE `owner` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `name` VARCHAR(50) NOT NULL COMMENT '主人姓名', `phone` VARCHAR(20) NOT NULL COMMENT '联系电话,预约提醒用', `address` VARCHAR(255) DEFAULT NULL COMMENT '家庭地址', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记,0正常 1删除', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物主人表'; CREATE TABLE `pet` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `owner_id` BIGINT NOT NULL COMMENT '所属主人id,owner表主键', `name` VARCHAR(50) NOT NULL COMMENT '宠物昵称', `breed` VARCHAR(50) DEFAULT NULL COMMENT '品种,如英短、金毛', `gender` TINYINT DEFAULT NULL COMMENT '性别:0公 1母', `birthday` DATE DEFAULT NULL COMMENT '出生日期,用来计算年龄', `weight` DECIMAL(5,2) DEFAULT NULL COMMENT '体重kg', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', PRIMARY KEY (`id`), KEY `idx_owner_id` (`owner_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物信息表';

字段类型上有几个要留意的点。owner 表的 phone 用 VARCHAR(20) 而不是 VARCHAR(11),因为座机、区号、国际号码都可能出现,别把长度卡死。pet 表的 gender 用 TINYINT 而不是 CHAR(1),避免「公母」的中文编码问题,展示时在前端做映射。weight 用 DECIMAL(5,2) 而不是 FLOAT,浮点数做金额和体重计算会出现 0.1+0.2 不等于 0.3 的问题,DECIMAL 是精确数值类型。create_time 和 update_time 用 DATETIME 并且设置默认值,这样 insert 的时候不需要手动 set 时间,MyBatis-Plus 的插入逻辑也不用关心这个字段。

3.3 外键到底要不要建

很多教材强调外键约束,但在实际项目中我基本不建物理外键。原因有三个:第一,数据库在做删除和更新时会多一次约束检查,影响写入性能;第二,业务上做逻辑删除(deleted 字段标记)之后,物理外键与逻辑删除天然冲突——你删不掉「已删除」的数据,外键约束就成了摆设;第三,MyBatis-Plus 的 mapper 层操作单表时,有物理外键反而要多维护一句。所以 pet 表里的 owner_id 只是逻辑上的外键,靠 service 层去保证「插入宠物前先确认 owner 存在」。

这个设计在答辩时容易成为提问点。回答思路是:物理外键保证的是数据库层面的引用完整性,但在逻辑删除、分库分表、性能优化这些真实约束下,引用完整性交给应用层代码更可控。service 层新增宠物时校验 owner_id 是否存在,删除主人时先查有无名下宠物、有则提示先转移或删除宠物——这条规则写在 service 是显式的,写在外键约束里是隐式的,出问题排查起来前者快很多。

3.4 MyBatis-Plus 的实体映射与自动填充

用 MyBatis-Plus 时,实体类和表的映射有两种方式:驼峰自动映射和注解指定。强烈建议实体字段名与数据库字段名完全对应或者开启驼峰转换,否则每次查询都要写 resultMap。我用的是注解方式,因为数据库字段是下划线风格,实体类字段是驼峰风格,配置了 map-underscore-to-camel-case=true 之后,owner_id 自动映射到 ownerId,不需要额外代码。

自动填充是 MyBatis-Plus 一个非常实用的能力。实体类里 createTime 和 updateTime 这两个字段,每次 insert 和 update 都要手动 set 当前时间,代码重复度高。用 @TableField(fill = FieldFill.INSERT) 和 fill = FieldFill.INSERT_UPDATE 注解标记字段,再实现 MetaObjectHandler 接口,在 insertFill 和 updateFill 方法里统一填充时间。配合前面 DDL 里 DEFAULT CURRENT_TIMESTAMP,双保险,任何一条写入路径都不会漏时间字段。这也是 MyBatis-Plus 根据实体类生成建表 SQL 的底层基础——实体字段注解完整,数据库表才建得干净。

4. 核心功能落地:宠物档案、预约排期与接种提醒的三段代码

4.1 宠物档案新增与分页查询:三层代码的完整链路

先讲宠物档案的新增,这是理解 Controller → Service → Mapper 三层调用关系最直观的入口。我把校验逻辑放在 controller 层做基础校验(必填字段、格式),把业务校验放在 service 层(主人是否存在、名下限重),这样职责清晰。下面是新增宠物的核心代码:

@PostMapping("/pet") public Result<Long> createPet(@RequestBody @Valid PetCreateDTO dto) { // 基础校验由 @Valid 完成,这里直接进入业务层 Long petId = petService.createPet(dto); return Result.success(petId); }
@Service @Slf4j public class PetServiceImpl implements PetService { @Autowired private PetMapper petMapper; @Autowired private OwnerMapper ownerMapper; @Override @Transactional(rollbackFor = Exception.class) public Long createPet(PetCreateDTO dto) { // 业务校验:主人必须存在,且未删除 Owner owner = ownerMapper.selectById(dto.getOwnerId()); if (owner == null || owner.getDeleted() == 1) { throw new BizException("宠物主人不存在"); } // 同一主人名下宠物数量上限,防止一个人登记 50 只宠物 Long count = petMapper.selectCount( new LambdaQueryWrapper<Pet>().eq(Pet::getOwnerId, dto.getOwnerId())); if (count >= 5) { throw new BizException("同一主人最多登记5只宠物"); } Pet pet = new Pet(); BeanUtils.copyProperties(dto, pet); // 属性拷贝简写 petMapper.insert(pet); return pet.getId(); } }

这段代码里有几个关键设计。@Transactional(rollbackFor = Exception.class) 声明事务,注意括号里的 rollbackFor 必须写,否则 Spring 默认只对运行时异常回滚,像 BizException 这种自定义异常如果继承 Exception,事务不会生效。业务校验写在 service 而不是 controller,是因为这类规则可能被多个接口复用——比如后台导入宠物数据时同样要走「主人存在」的校验。BeanUtils.copyProperties 把 DTO 属性拷到实体类,避免 controller 直接把 DTO 传进 mapper,防止前端多传字段覆盖数据库值。MyBatis-Plus 的 insert 方法会自动回填自增主键到 pet.getId(),所以返回的 id 不需要手动查。

分页查询是列表页的高频操作。MyBatis-Plus 的分页插件配好后,查询代码很简单:

@Override public PageResult<PetVO> pagePets(PetQueryDTO query) { Page<Pet> page = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<Pet> wrapper = new LambdaQueryWrapper<>(); // 关键字搜索:宠物名或主人姓名模糊匹配 if (StringUtils.hasText(query.getKeyword())) { // 这里关联 owner 表的姓名需要子查询或连表,见下方说明 wrapper.and(w -> w.like(Pet::getName, query.getKeyword())); } if (query.getBreed() != null) { wrapper.eq(Pet::getBreed, query.getBreed()); } // 逻辑删除过滤是 MyBatis-Plus 自动处理的,deleted=0 自动拼接 wrapper.orderByDesc(Pet::getCreateTime); Page<Pet> result = petMapper.selectPage(page, wrapper); // 实体转 VO,补充主人姓名、年龄等展示字段 List<PetVO> voList = result.getRecords().stream() .map(pet -> convertToVO(pet, ownerMapper)) .collect(Collectors.toList()); return new PageResult<>(result.getTotal(), voList); }

分页查询最容易踩的坑是「按主人姓名搜宠物」:pets 表没有 owner_name 字段,不能直接在 wrapper 里 like。常见做法有两种:如果搜索频率不高,先查 owner 表拿到符合条件的 owner_id 列表,再 in 到 pet 查询里;如果搜索是核心功能,直接在 pet 表冗余 owner_name 字段,写入时同步更新。我在这类系统里一般选择冗余字段,宠物表加 owner_name 和 owner_phone,虽然违反了一点范式,但查询走单表、索引友好,比起 join 的性能收益明显。分页插件配置时注意要引入 MyBatis-Plus 的 PaginationInnerInterceptor,否则 selectPage 分页不生效,返回全量数据——这个是新手最容易困惑的「配置了但没生效」问题。

4.2 预约排期的状态机与冲突校验

预约管理比档案管理复杂在「状态」上。一个预约至少有四种状态:待确认、已确认、已完成、已取消。我用一个 int 字段 status 存储,用常量类定义状态值。修改状态时,核心是校验状态流转是否合法——已完成的预约不能直接改成已取消,已取消的不能变成已确认。

public void cancelAppointment(Long appointmentId, Long currentUserId) { Appointment appointment = appointmentMapper.selectById(appointmentId); if (appointment == null) { throw new BizException("预约不存在"); } // 状态机校验:只有待确认和已确认状态可以取消 if (appointment.getStatus() != AppointmentStatus.PENDING && appointment.getStatus() != AppointmentStatus.CONFIRMED) { throw new BizException("当前状态不允许取消"); } Appointment update = new Appointment(); update.setId(appointmentId); update.setStatus(AppointmentStatus.CANCELLED); update.setCancelReason("用户取消"); update.setUpdateBy(currentUserId); appointmentMapper.updateById(update); }

预约冲突校验是另一个要点:同一时间段、同一医生不能同时接两个预约。实现时不要在应用层去循环比对,直接在 SQL 层判断:

// 检查是否已有冲突预约 Long conflict = appointmentMapper.selectCount( new LambdaQueryWrapper<Appointment>() .eq(Appointment::getDate, dto.getDate()) .eq(Appointment::getTimeSlot, dto.getTimeSlot()) .eq(Appointment::getDoctorId, dto.getDoctorId()) .in(Appointment::getStatus, AppointmentStatus.PENDING, AppointmentStatus.CONFIRMED)); if (conflict > 0) { throw new BizException("该时间段已有预约,请选择其他时间"); }

这个方案的关键在于状态列表参数:已取消和已完成的预约不应该阻塞新的预约。另外,数据库层面给 (doctor_id, date, time_slot, status) 加一个联合唯一索引做不到,因为 status 是可变的,所以只能靠应用层校验兜底——这个场景下并发冲突的概率很低,应用层校验已足够。真要搞分布式锁就是过度设计了,管理系统的不需要那么高的并发保障。

4.3 疫苗接种提醒的定时任务实现

接种提醒是整个系统里「增值」的功能:查询下次接种日期在 7 天内的宠物,给主人发送提醒通知。这段逻辑适合用 Spring 的 @Scheduled 来做定时扫描。注意两点:任务要加分布式部署保护(加一个开关,只在指定实例执行),避免多实例重复发提醒。

@Component @Slf4j public class VaccinationReminderTask { @Autowired private VaccinationMapper vaccinationMapper; @Autowired private RemindService remindService; @Scheduled(cron = "0 30 8 * * ?") // 每天早上 8:30 执行 public void sendReminder() { LocalDate today = LocalDate.now(); LocalDate endDate = today.plusDays(7); // 查询下次接种日期落在今天+7天内的记录 List<Vaccination> list = vaccinationMapper.selectList( new LambdaQueryWrapper<Vaccination>() .between(Vaccination::getNextVaccineDate, today, endDate) .eq(Vaccination::getReminded, 0)); for (Vaccination v : list) { // 查询宠物和主人信息,组装提醒内容 try { remindService.sendRemindMessage(v); v.setReminded(1); // 标记已提醒,避免下次重复发送 vaccinationMapper.updateById(v); } catch (Exception e) { log.error("接种提醒发送失败, vaccinationId={}", v.getId(), e); } } } }

定时任务有一个细节:cron 用的是服务器时区,如果部署在云上要注意时区设置,否则 8:30 的提醒可能在北京时间 16:30 才发出去。另一个坑是「明天到期未接种」的记录,不能只查一次就标记 reminded=1,而应该在发送成功后标记,发送失败的保留待下次重试。上述代码里 catch 块只记录日志、不标记,就是这个原因。系统上线后加一个「手动触发提醒」的接口,用来补发漏掉的提醒,这是运维侧的后悔药,很有必要。

5. 避坑排查:宠物管理系统中 5 个常见坑与修复路径

5.1 分页查询的总记录数一直是 0,查了列表却不翻页

现象:列表接口能返回数据,但 total 字段总是 0,前端分页组件显示「共 0 条」。原因:MyBatis-Plus 分页插件没有注册,或者说 PaginationInnerInterceptor 没有放进 MybatisPlusInterceptor 里。很多人只加了 mybatis-plus-boot-starter 依赖,以为分页就是 selectPage 自带的能力。解决:配置一个 @Bean 注入 MybatisPlusInterceptor,添加 PaginationInnerInterceptor,设置 DbType.MYSQL。还有一个更隐蔽的问题:分页插件和自定义拦截器有顺序要求,插件要在最外层。

5.2 联表查询和逻辑删除大战,查出来的数据莫名少了几条

现象:连表查询 medical_record 和 pet 时,发现 join 出来的记录数远少于 pet 表的总数。原因:pet 表开启了逻辑删除,MyBatis-Plus 自动在单表查询里追加 deleted=0,但如果你写的是 xml 里的自定义多表 join,MP 不会自动帮你加条件。解决:自定义 SQL 里手动加 and p.deleted = 0,或者用 MP 提供的 @TableLogic 注解配合它的自定义 SQL 解析能力。这个坑上线后排查了很久,最后是在慢查询日志里看到 join 结果和预期不符才定位到的。

5.3 定时任务在开发环境跑得好好的,部署后不执行

现象:本地启动项目,到点提醒正常发送;用 java -jar 部署到服务器后,完全没反应。原因:第一,服务器时区不是东八区,cron 表达式虽然写的是 0 30 8,但服务器 UTC 时间比北京时间晚 8 小时;第二,打包时没有把 scheduled 注解所在包扫描进去——Spring Boot 默认扫描 main 方法同级及子包,如果你的 Task 类放在别的包路径下,需要配置 @ComponentScan。解决:应用配置里设置 spring.jackson.time-zone=GMT+8,服务器上把时区软链到 Asia/Shanghai,然后确认启动类能扫描到 Task 包。

5.4 登录接口正常,但访问业务接口全部 401

现象:前端登录成功拿到 token,之后每个请求都返回未认证。原因:JWT 拦截器把放行路径写错了。常见错误是拦截器注册时,把「放行登录接口」写在 excludePathPatterns 里但路径写成了 /user/login 而不是 /api/user/login,而 controller 的 RequestMapping 带 /api 前缀。另一个坑是配置类上加 @Configuration 但拦截器没注入,new 出来的拦截器里所有依赖都是 null,解码 token 时直接空指针。解决:打印拦截器注册后的拦截路径清单,人眼核对;拦截器通过构造器注入需要的 service,不要在里面 new。

5.5 环境变量配置了 JDK 却启动失败,报 UnsupportedClassVersionError

现象:下载项目后本地启动,直接抛 UnsupportedClassVersionError,或者提示「找不到或无法加载主类」。原因:JDK 版本不匹配。项目用 JDK 17 编译,本地装的是 JDK 8;还有一种情况是 IDEA 里配置了 JDK 17,但命令行 java -version 还是老版本,因为 Path 环境变量里有多个 JDK,配置的顺序问题导致实际生效的不是想要的版本。解决:命令行里 java -version 确认实际版本;再检查 JAVA_HOME 是否指向正确目录;如果项目是 Maven 构建,在 pom 里显式声明 maven.compiler.source 和 target,并且确认 IDEA 的 Project Structure 和 Maven 的 JRE 设置一致。这个坑特别常见,尤其是新入职拿老项目练手的时候,大部分启动失败都能归因到环境变量配置上。

6. 打包、验证与进阶:从打好 jar 包到慢 SQL 定位

6.1 验证系统该测什么

系统「实现」完了,不能只说「能跑」。我建议至少拉出这样一条验证清单:用户登录后能否按角色(管理员、医生)看到不同的菜单;新增一只宠物后,在列表页和详情页能否立刻看到;创建一个预约、然后改状态,再看「今日预约」列表里的状态颜色是否跟着变;把某只宠物的下次接种日期改成三天后,手动触发一次提醒任务,确认收到提醒后 reminded 字段被标记。其中提醒任务最值得在测试环境手动跑一次接口而不是等 cron 触发,也就是写一个临时 Controller 入口,收到消息后立刻回头看业务表里 reminded 的状态。

6.2 打包部署的常用姿势

项目交付多以 jar 包形式。IDEA 里 Maven 面板执行 package,target 目录下会生成可执行 jar。命令行的习惯是:

java -Xms256m -Xmx512m -jar pet-system.jar \ --spring.profiles.active=prod \ --server.port=8081

注意几个参数的用途:-Xms 和 -Xmx 设置了堆大小,宠物管理系统并发不高,256~512MB 足够;--spring.profiles.active 指定 prod 环境的 application-prod.yml,里面配生产库地址;--server.port 覆盖内置 Tomcat 端口。如果用 Jenkins 或脚本部署,建议增加一个健康检查接口返回 200 后再摘流量,避免启动一半被人访问。生产环境不要把数据库密码明文写在 yml 里,用环境变量的方式注入,比如 spring.datasource.password=${DB_PASSWORD}。

6.3 慢 SQL 定位:从现象到 Explain

系统用了一段时间后,宠物档案列表变慢。不要凭感觉优化,先用 MySQL 的慢查询日志定位具体 SQL。开启慢查询:set global slow_query_log = ON; set global long_query_time = 1,然后重放操作。拿到慢 SQL 后执行 EXPLAIN,看 type 字段是不是 ALL(全表扫描)、key 是不是 null。常见优化手段是加索引:pet 表的 owner_id 已有索引,但如果经常按 breed 查列表,给 breed 加普通索引就够;预约表的查询条件集中在 doctor_id + date 上,建联合索引 (doctor_id, date, time_slot) 比单列索引效果好很多。注意不要过度加索引,写多读少的小表反而因为索引维护拖慢写入速度。

这个项目做下来最深的感受是:管理系统难的不是某个技术点,而是把所有环节接起来的耐心。当初我第一次做这类系统,犯过把时间字段手动 set 导致测试环境时间错乱的错误,也犯过分页插件没注册导致列表「数据全出来」的低级问题。后来养成了一个习惯——每个功能写完,先不看界面效果,直接打开数据库表看数据落得对不对,数据对了再接前端。这个习惯帮我省了大量联调时间。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进