资讯动态

基于Java的宠物管理系统:从建表到接口的落地路径

发布时间:2026/10/26 17:21:28 来源:尧图企业网站定制
简介这是一套面向Java Web初学者与课程设计需求的宠物管理系统完整项目源码基于SSMSpring、SpringMVC、MyBatis框架搭建配合Layui前端与MySQL数据库在MyEclipse环境下开发可用于宠物店或动物保护机构的信息化管理也适合作为毕业设计或实训参考。压缩包共1442个文件约136.6MB涵盖js、css、jsp等前端资源java、class、jar等后端与依赖文件以及png、gif、jpg等界面素材另含sql脚本、xml配置与说明文档结构完整。目前已有2172人学习下载。项目覆盖宠物信息、用户资料、领养记录等模块包含需求分析、数据库设计、编码实现与测试调试全流程读者可借此理解SSM分层架构、MyBatis持久化操作与Layui组件化界面开发快速掌握Java Web项目从搭建到部署的实践思路。1. 基于Java的宠物管理系统从建表到接口一套能跑通的落地路径宠物医院前台还在用Excel登记疫苗时间宠物店老板靠微信群接龙预约洗澡这是很多中小型宠物服务门店的真实状态。基于Java的宠物管理系统要解决的就是这件事把宠物档案、主人信息、预约记录、疫苗提醒、消费流水这些散落的数据收进一个后台用Spring Boot MyBatis-Plus搭出一套能查、能改、能提醒的服务端。它适合两类人一是正在找Java毕业设计或练手项目的开发者二是想给自家门店做一套轻量管理工具的小团队。整套系统的技术栈不复杂核心难点不在框架本身而在表结构怎么设计、宠物和主人的关联怎么处理、疫苗到期提醒怎么定时触发。下面按建表、搭工程、写接口、排坑的顺序把这条路走一遍。2. 表结构设计宠物管理系统最容易翻车的地方2.1 为什么宠物和主人不能塞进一张表新手最常见的做法是建一张pet表里面直接放owner_name、owner_phone、owner_address。这个设计在单宠物家庭还能跑一旦遇到一个主人养三只猫、两只狗数据就会重复三到五倍。更麻烦的是主人换了手机号你得更新所有宠物记录漏一条就是脏数据。正确的做法是拆成owner和pet两张表用owner_id做外键关联。这是面向对象编程里「一个主人对应多个宠物」的一对多关系在数据库层面的直接映射。宠物管理系统里还有几个关系要提前想清楚一只宠物可以有多条疫苗记录一对多、一条预约对应一只宠物和一个服务项目多对一、消费流水挂在预约下面一对多。把这些关系画出来再建表比先写代码后改表省事得多。2.2 建表SQL与字段说明下面这套SQL覆盖了核心的五张表字段类型按MySQL 8.0来定。注意pet表里的birthday用DATE而不是DATETIME因为宠物的出生日期精确到天就够了用DATETIME反而会在计算年龄时引入时区问题。-- 主人表 CREATE TABLE owner ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, name VARCHAR(50) NOT NULL COMMENT 主人姓名, phone VARCHAR(20) NOT NULL COMMENT 手机号, address VARCHAR(200) DEFAULT NULL COMMENT 地址, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宠物主人表; -- 宠物表 CREATE TABLE pet ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, owner_id BIGINT NOT NULL COMMENT 主人ID, name VARCHAR(50) NOT NULL COMMENT 宠物名, species VARCHAR(20) NOT NULL COMMENT 物种猫/狗/其他, breed VARCHAR(50) DEFAULT NULL COMMENT 品种, gender TINYINT DEFAULT 0 COMMENT 性别0未知 1公 2母, birthday DATE DEFAULT NULL COMMENT 出生日期, weight DECIMAL(5,2) DEFAULT NULL COMMENT 体重(kg), create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_owner_id (owner_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宠物表; -- 疫苗记录表 CREATE TABLE vaccine_record ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, pet_id BIGINT NOT NULL COMMENT 宠物ID, vaccine_name VARCHAR(100) NOT NULL COMMENT 疫苗名称, vaccine_date DATE NOT NULL COMMENT 接种日期, next_date DATE DEFAULT NULL COMMENT 下次接种日期, remark VARCHAR(255) DEFAULT NULL COMMENT 备注, PRIMARY KEY (id), KEY idx_pet_id (pet_id), KEY idx_next_date (next_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT疫苗记录表; -- 服务项目表 CREATE TABLE service_item ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, name VARCHAR(100) NOT NULL COMMENT 项目名称, price DECIMAL(10,2) NOT NULL COMMENT 价格, duration INT DEFAULT 30 COMMENT 预计耗时(分钟), PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT服务项目表; -- 预约表 CREATE TABLE appointment ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, pet_id BIGINT NOT NULL COMMENT 宠物ID, service_id BIGINT NOT NULL COMMENT 服务项目ID, appoint_time DATETIME NOT NULL COMMENT 预约时间, status TINYINT DEFAULT 0 COMMENT 状态0待确认 1已确认 2已完成 3已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_pet_id (pet_id), KEY idx_appoint_time (appoint_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约表;几个参数要重点说。owner表的phone加了唯一索引因为手机号是主人身份的自然标识重复注册会导致后续查询混乱。pet表的owner_id和vaccine_record表的pet_id都建了普通索引这两个字段是关联查询的入口没索引的话数据量上到几千条就会明显变慢。vaccine_record表的next_date单独建索引是为了后面做疫苗到期提醒时能快速筛出「未来7天内需要接种」的记录。注意外键约束在互联网项目里通常不建物理外键靠应用层保证一致性。上面SQL只建了索引没建FOREIGN KEY这是常见做法避免级联操作锁表。2.3 用MyBatis-Plus根据实体类反向生成建表SQL热搜里有人问「mybatisplus根据java实体类生成创建表的sql语句」这个需求在宠物管理系统里很实用——先写实体类再生成表比手写SQL快。MyBatis-Plus本身不直接提供这个功能但可以借助它的TableInfoHelper拿到实体类的元信息再拼SQL。下面是一个简化版的工具类思路// 核心逻辑读取实体类注解拼出CREATE TABLE语句 public class TableSqlGenerator { public static String generate(Class? entityClass) { TableInfo tableInfo TableInfoHelper.getTableInfo(entityClass); StringBuilder sql new StringBuilder(CREATE TABLE ) .append(tableInfo.getTableName()).append( (\n); // 遍历字段取TableField注解的value作为列名 for (TableFieldInfo field : tableInfo.getFieldList()) { sql.append( ).append(field.getColumn()).append( ) .append(field.getSqlType()).append( COMMENT ) .append(field.getComment()).append(,\n); } sql.append( PRIMARY KEY (id)\n) ENGINEInnoDB DEFAULT CHARSETutf8mb4;); return sql.toString(); } }这段代码的关键在于TableInfoHelper.getTableInfo()它要求实体类已经被MyBatis-Plus扫描过所以调用时机要放在Spring容器启动之后。field.getSqlType()返回的是Java类型映射后的数据库类型但精度和长度需要自己根据TableField里的配置补全。实际用的时候我一般会把这个生成结果打印到日志里人工核对一遍再执行直接跑容易因为类型映射不精确导致字段长度不对。3. Spring Boot工程搭建与核心接口实现3.1 依赖选型与工程结构宠物管理系统的后端用Spring Boot 3.x MyBatis-Plus 3.5.x是当前比较稳的组合。Spring Boot 3要求JDK 17起步如果本地环境还是JDK 8要么升JDK要么把Spring Boot降到2.7.x。热搜里「java环境变量使用多个jdk」和「java启动失败怎么解决」这两个问题多半就是JDK版本和Spring Boot版本对不上导致的。pom.xml里核心依赖就三个spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-j。如果要做疫苗提醒的定时任务再加一个spring-boot-starter-quartz或者直接用Spring自带的Scheduled。工程结构按标准的分层来controller、service、mapper、entity、dto别把所有类塞一个包后面改起来会疯。dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependencyapplication.yml里配好数据源和MyBatis-Plus的日志spring: datasource: url: jdbc:mysql://localhost:3306/pet_manager?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: truemap-underscore-to-camel-case必须开否则数据库的owner_id映射不到Java的ownerId查出来全是null。serverTimezone要显式指定不写的话MySQL 8会报时区错误。3.2 宠物档案的增删改查接口实体类用MyBatis-Plus的注解标注主键策略用ASSIGN_ID雪花算法还是AUTO数据库自增看场景。宠物管理系统数据量不大用AUTO更直观。Data TableName(pet) public class Pet { TableId(type IdType.AUTO) private Long id; private Long ownerId; private String name; private String species; private String breed; private Integer gender; private LocalDate birthday; private BigDecimal weight; private LocalDateTime createTime; }Mapper接口继承BaseMapperPet就自动有了单表CRUD。Service层写业务逻辑比如新增宠物时要先校验ownerId是否存在Service public class PetService { Autowired private PetMapper petMapper; Autowired private OwnerMapper ownerMapper; public boolean addPet(Pet pet) { // 校验主人是否存在避免孤儿宠物记录 if (ownerMapper.selectById(pet.getOwnerId()) null) { throw new BizException(主人不存在请先录入主人信息); } return petMapper.insert(pet) 0; } // 分页查询某主人的所有宠物 public PagePet listByOwner(Long ownerId, int page, int size) { return petMapper.selectPage(new Page(page, size), new LambdaQueryWrapperPet().eq(Pet::getOwnerId, ownerId)); } }LambdaQueryWrapper的好处是字段名用方法引用写编译期就能发现拼写错误比字符串写owner_id安全。分页查询记得配MyBatis-Plus的分页插件不配的话selectPage不会真正分页会把全表查出来再内存分页数据一多就OOM。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }3.3 疫苗到期提醒的定时任务疫苗提醒是这个系统里最有业务价值的功能。思路是每天凌晨跑一次任务查出next_date在「今天到未来7天」之间的记录然后给对应主人发通知短信、站内信或日志。Component public class VaccineReminderTask { Autowired private VaccineRecordMapper vaccineMapper; // 每天凌晨2点执行 Scheduled(cron 0 0 2 * * ?) public void remind() { LocalDate today LocalDate.now(); LocalDate deadline today.plusDays(7); ListVaccineRecord list vaccineMapper.selectList( new LambdaQueryWrapperVaccineRecord() .between(VaccineRecord::getNextDate, today, deadline) .isNotNull(VaccineRecord::getNextDate)); for (VaccineRecord record : list) { // 这里替换成实际的短信或推送逻辑 log.info(宠物{}的{}疫苗将于{}到期请提醒主人, record.getPetId(), record.getVaccineName(), record.getNextDate()); } } }cron表达式0 0 2 * * ?表示每天2点整执行。between查询用到了next_date上的索引几千条数据毫秒级返回。要注意的是如果系统部署了多个实例定时任务会重复执行这时候要么用分布式锁要么把任务单独部署。单机部署的话在启动类上加EnableScheduling就能跑起来。4. 接口联调与数据一致性排查4.1 预约状态流转的并发问题预约表里的status字段有四个状态从「待确认」到「已确认」再到「已完成」中间还可能被取消。如果两个管理员同时操作同一条预约一个点确认一个点取消最后状态就乱了。常见做法是在更新时加状态条件// 只有当前状态是待确认时才能改成已确认 boolean updated appointmentMapper.update(null, new LambdaUpdateWrapperAppointment() .eq(Appointment::getId, id) .eq(Appointment::getStatus, 0) .set(Appointment::getStatus, 1)) 0; if (!updated) { throw new BizException(预约状态已变更请刷新后重试); }这段代码利用MySQL的行锁update语句带status0条件如果状态已经被别人改了影响行数为0直接抛异常。这比先查再改的两步操作可靠避免了并发窗口。4.2 宠物删除时的关联数据处理删除一只宠物时它的疫苗记录和预约记录怎么办三种策略级联删除、软删除、禁止删除。宠物管理系统的业务场景下我一般用软删除——在pet表加deleted字段删除时只改标记查询时过滤掉。这样历史预约和消费记录还能关联到宠物信息不会出现「预约记录里宠物是null」的情况。// 软删除MyBatis-Plus的TableLogic注解自动处理 TableLogic private Integer deleted;加了TableLogic之后deleteById会自动变成UPDATE pet SET deleted1 WHERE id?所有查询自动带上deleted0条件。这个注解省事但要注意它和自定义SQL的配合手写的XML查询不会自动加过滤条件得自己写。5. 避坑与常见问题排查5.1 启动报OutOfMemoryError但堆内存明明够现象IDEA里启动项目报java.lang.OutOfMemoryError: GC overhead limit exceeded但看任务管理器内存占用不高。原因通常是IDEA编译时的堆大小不够不是运行时。热搜里「idea 编译时进程堆大小调整为8000还是报错」说的就是这个。解决在IDEA的Settings Build Compiler Shared build process heap size里调到2048以上同时检查pom.xml里有没有引入冲突的依赖导致编译期加载过多类。5.2 手机号唯一索引导致插入失败但没提示现象新增主人时接口返回500日志里是Duplicate entry异常。原因owner表的phone字段有唯一索引重复插入触发约束。解决在Service层先selectOne查一下手机号是否存在存在就返回友好提示同时全局异常处理器里捕获DuplicateKeyException统一转成业务异常返回给前端。5.3 疫苗提醒任务查不到数据现象定时任务日志显示执行了但一条记录都没查到。原因排查三步先确认next_date字段有没有值很多录入时忘了填再确认between的边界between a and b是闭区间包含两端最后确认时区LocalDate.now()取的是服务器时区如果数据库存的是UTC时间差8小时就会漏掉当天的记录。解决统一用Asia/Shanghai时区next_date用DATE类型存避免时间部分干扰。5.4 分页查询返回总数不对现象selectPage返回的total是0或者明显偏小。原因分页插件没配或者配了但DbType写错。另一个常见原因是查询条件里用了or但没加括号导致WHERE条件优先级出错。解决确认MybatisPlusInterceptor里加了PaginationInnerInterceptorDbType和实际数据库一致复杂条件用LambdaQueryWrapper的and(w - w.eq(...).or().eq(...))显式分组。5.5 打包成tar包部署后连不上数据库现象本地跑得好好的打成tar包扔到服务器上启动就报连接超时。原因application.yml里的url写的是localhost服务器上MySQL不在本机。解决把数据库地址改成实际IP或者用环境变量注入。热搜里「怎么把java项目打成tar包」的做法是mvn clean package后把jar和启动脚本一起打包启动脚本里用-Dspring.datasource.urlxxx覆盖配置比改配置文件再打包灵活。6. 用MyBatis-Plus的代码生成器把开发速度提上来前面手写实体类和Mapper是为了讲清楚原理实际做宠物管理系统的时候五张表对应的实体、Mapper、Service、Controller如果全手写光样板代码就得写半天。MyBatis-Plus自带的代码生成器能根据表结构反向生成这四层代码省下来的时间可以花在业务逻辑上。配置代码生成器主要改三个地方数据源、包名、要生成的表名。下面是一个可复用的配置片段public class CodeGenerator { public static void main(String[] args) { FastAutoGenerator.create( jdbc:mysql://localhost:3306/pet_manager?serverTimezoneAsia/Shanghai, root, your_password) .globalConfig(builder - builder .author(your_name) .outputDir(System.getProperty(user.dir) /src/main/java) .disableOpenDir()) .packageConfig(builder - builder .parent(com.example.pet) .entity(entity) .mapper(mapper) .service(service) .controller(controller)) .strategyConfig(builder - builder .addInclude(owner, pet, vaccine_record, service_item, appointment) .entityBuilder() .enableLombok() .logicDeleteColumnName(deleted) .controllerBuilder() .enableRestStyle()) .execute(); } }addInclude里列出要生成的表不写就生成全库的表容易把无关表也生成出来。enableLombok()让实体类用Data代替getter/setter。logicDeleteColumnName(deleted)自动给实体类加TableLogic注解前提是表里真有这个字段没有的话生成出来会报错。enableRestStyle()让Controller用RestController而不是Controller省得每个方法再加ResponseBody。生成完之后有几处要手动调。一是实体类里的LocalDate和LocalDateTime类型生成器默认可能给成Date要按字段实际含义改。二是Controller里的接口路径生成的是/pet这种实际项目里一般会加版本号或者模块前缀比如/api/v1/pet。三是Service层的业务校验生成器只给空壳方法主人是否存在、预约时间是否冲突这些逻辑得自己补。用生成器还有一个好处是字段注释会带到实体类的JavaDoc里前端对接的时候直接看Swagger文档就知道每个字段什么意思。如果项目里集成了knife4j或者springdoc在Controller上加个Tag注解接口文档自动就出来了比手写Markdown接口文档省事也不会出现文档和代码不一致的情况。我自己的习惯是先用生成器跑一遍把生成的代码提交到Git然后再在生成的代码上改业务逻辑。这样万一改乱了还能git diff看看原始生成的是什么样。另外生成器配置类不要留在主代码目录里放到test目录或者单独的tools模块避免打包的时候把生成器依赖也打进去。这个方案值不值得做取决于你的表数量——三张表以下手写更快五张表以上生成器能省至少半天。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑