简介这是一套完整的宠物管理系统项目包涵盖可运行的源码、高保真原型与配套数据库脚本主要面向需要完成课程设计、毕业设计或快速搭建业务演示的开发者也可作为Java/Web方向初学者的练习参考。压缩包为RAR格式共4个文件两个ZIP压缩包分别封装源码与原型设计PDF文档提供系统说明或操作指引SQL脚本包含建表语句与初始示例数据整体仅1.54MB轻量易部署。数据库脚本导入后可与源码联动运行两个ZIP包分开存放便于按需提取设计稿或修改代码原型文件则可直观展示页面布局与交互逻辑方便需求评审和方案汇报。目前已有2539人学习下载直接复用可省去从零搭建的时间适合用于项目启动、二次开发或教学展示。对课程答辩或项目实训而言这套包能快速提供可演示的完整流程。1. 拿到“宠物管理系统”三件套先跑通源码再画原型很多接触“宠物管理系统”的人——不管是做课程设计、毕业设计还是接了一个宠物店的管理后台私活——看到“源码原型数据库”三件套第一反应都是先翻源码。源码是黑匣子、原型是需求清单、数据库是地基。我一般会先把源码里的启动入口跑起来把登录、新增宠物、寄养下单这几条主流程点一遍知道它到底能做什么然后对照原型页面一个个字段去核对数据库表。这样你拿到的不只是一堆文件而是一套能说清楚“这个系统为什么长这样”的完整方案。这套东西适合想在短时间内部署交付、又怕被“改需求”反复拖垮的人。2. 从原型锁定功能边界宠物档案、寄养订单、库存三张核心页面先画出来2.1 原型不是画图是一份可评审的需求清单做过宠物管理系统的人都有这个体验需求方说“做个简单的管理后台”结果做到一半突然问“能不能加个疫苗到期提醒”“能不能在寄养订单里看到宠物主人”。这不是需求方难缠是需求一开始就没锁死。原型在这里的作用不是画几个好看的框而是把每个页面、每个按钮、每种状态变成能被逐条确认的清单。我一般会先列一张页面清单再决定要不要动笔。清单里只写三列页面归属哪个模块、这个页面做什么、操作后落到哪些数据。给宠物店看的时候鼠标一页页点过去对方只要回答“有”“没有”“不对”三个字需求边界就清楚了。常见的宠物管理系统至少包含五块宠物档案、客户宠物主人管理、寄养订单、商品库存、统计看板。如果标题里还有疫苗和洗澡预约就把预约也加进寄养模块不要单独建页面。页面模块核心页面主要操作涉及数据宠物档案宠物列表 / 新增宠物搜索、筛选、查看详情、编辑宠物基本档案、主人关联寄养管理寄养订单列表 / 新建订单下单、续期、结算、取消寄养订单、费用记录商品管理商品列表 / 上下架新增商品、改价、停售商品表、库存表库存管理库存流水 / 预警入库、出库、盘点调整库存流水、预警阀值系统管理员工账号 / 角色增删账号、分配权限系统用户、角色表这张表本身比原型文件更值钱。因为评审时大家盯的是“这个系统要管哪些事”而不是“这个蓝色好不好看”。原型工具用 Axure RP、墨刀或者即时设计都可以选你上手最快的重点是把它当工具用不要沉迷在交互动画里。2.2 用低保真线框锁字段宠物档案页的字段取舍表很多原型翻车都翻在字段数量上先画了个特别全的表单开发时发现一半字段用不上或者反过来漏了“体重”“是否绝育”这种日常必填项上线后再加又要改数据库表、又要改列表页。我建议在低保真阶段就把宠物档案页的字段定死像素和配色后面再说。宠物档案页我一般保留这些字段取舍逻辑也写在表里。给宠物店演示的时候他们往往盯着“毛色”争论是“橘白”还是“橘猫白”这时候要意识到字段是给查询和统计用的不是给百科写词条。能做成下拉的不要手填能存字典的不要直接塞字符。字段类型参考是否必填取舍理由宠物名varchar(50)必填列表和详情页都会展示搜索高频宠物品种字典表外键必填品种要支持后续扩展不能写死文本出生日期date选填能算出年龄比直接存“年龄”字段靠谱体重(kg)decimal(5,2)选填洗澡、寄养、用药都要参考浮点类型会失真毛色varchar(30)选填视觉识别用文本够用不做枚举是否绝育tinyint(1)必填寄养和医疗场景需要它做条件判断疫苗状态tinyint(1)必填寄养接待时第一问题就是疫苗是否齐全这个表一出来数据库基本已经写完一半。原型评审时不要去抠菜单动画是左滑还是右滑只确认字段和状态。字段确认后把表发给开发看开发如果还在问“宠物主人在哪呀”说明原型里少了客户管理入口立刻补上这时候补一张页面只需要半小时等到建完表再补就要动数据库了。2.3 把异常流程画进原型寄养超期、疫苗过期提醒原型不止画正常路径。多数宠物管理系统翻车都在异常流程上寄养订单到期没人处理、宠物主人忘了接、疫苗过期还进了寄养间。这些流程如果不在原型里出现开发阶段就会默认“不做”等客户验收时才发现少了一块。我习惯在原型里给每个状态加一行备注。比如寄养订单至少要有四个状态待入住、寄养中、待结算、已完成另外再加一个“超期”作为特殊状态。原型页面上只需要有一个状态标签但标签背后的判断逻辑必须在原型说明里写清订单超过预计离店日期时系统把它标记成“超期”并把宠物主人的联系方式醒目展示。疫苗状态同理档案里疫苗过期时列表页置灰寄养下单时直接拦截。评审时多问三个问题谁录入这些数据这个按钮误点了会发生什么订单取消后库存怎么回来这三个问题如果能在原型阶段答上来后面源码和数据库的改动就会少很多。3. 把原型落成 MySQL 物理模型5 张核心表字段边界3.1 从原型到 ER 模型三范式在宠物管理系统里怎么让步数据库设计没有标准答案我对于这种中小型管理系统有一条经验范式不要硬套但核心关系必须清晰。宠物管理系统的业务主线是“宠物主人把宠物送来寄养顺便买点猫粮狗粮”围绕这条主线最少只需要 5 张表客户表 customer、宠物档案表 pet、寄养订单表 boarding_order、商品表 product、订单明细表 order_item。如果你把管理后台的账号也做进来再加一张 sys_user第六张但不要一上来就设计十几张表。三范式里有一条“消除冗余”我在这类系统里会刻意违反它。寄养订单表里我会冗余 customer_name 和 pet_name而不是只存外键因为订单是一次历史记录宠物以后改名字了订单里显示的名字不应该跟着变。这就是“快照”字段做报表时少关联两张表查询速度也快。库存不从订单倒推单独开“库存流水”的思路也是反范式但换来了可追溯性。小系统不怕多存几个字的冗余怕的是多表关联撑不起并发。选型上MySQL 就是最稳的选择InnoDB 引擎、utf8mb4 字符集。不要用 MyISAM它不支持事务寄养订单的扣库存操作没有事务兜底就是埋雷。字符集这一点在后面的乱码坑里会再提一次建库语句里直接写明免得后面返工。3.2 核心建表 SQL宠物档案、寄养订单、商品库存的完整 DDL下面这套建表脚本是简化过但可以直接运行的最小集包含前面原型章节里锁定的主要字段。我在关键字段上都写了注释建立表的时候把注释一起建进去后面写文档时能少查很多次。-- 客户表宠物主人 CREATE TABLE customer ( id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 主键, name varchar(50) NOT NULL COMMENT 主人姓名, phone varchar(20) NOT NULL COMMENT 手机号, address varchar(200) DEFAULT NULL COMMENT 联系地址, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT客户宠物主人表; -- 宠物档案表 CREATE TABLE pet ( id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 主键, customer_id bigint unsigned NOT NULL COMMENT 主人ID, name varchar(50) NOT NULL COMMENT 宠物名, species tinyint NOT NULL COMMENT 1猫 2狗 3其他, breed varchar(50) DEFAULT NULL COMMENT 品种文本原型阶段保留自由录入, birth_date date DEFAULT NULL COMMENT 出生日期, weight_kg decimal(5,2) DEFAULT NULL COMMENT 体重(kg)最多支持999.99, hair_color varchar(30) DEFAULT NULL COMMENT 毛色, is_neutered tinyint(1) NOT NULL DEFAULT 0 COMMENT 是否绝育0否 1是, vaccine_status tinyint(1) NOT NULL DEFAULT 1 COMMENT 疫苗是否在有效期0过期 1有效, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 建档时间, PRIMARY KEY (id), KEY idx_customer_id (customer_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT宠物档案表; -- 寄养订单表 CREATE TABLE boarding_order ( id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 主键, order_no varchar(32) NOT NULL COMMENT 业务单号手动生成必须唯一, customer_id bigint unsigned NOT NULL COMMENT 客户ID, pet_id bigint unsigned NOT NULL COMMENT 宠物ID, customer_name varchar(50) NOT NULL COMMENT 客户姓名快照, pet_name varchar(50) NOT NULL COMMENT 宠物名快照, status tinyint NOT NULL DEFAULT 0 COMMENT 0待入住 1寄养中 2待结算 3已完成 4超期, checkin_date datetime NOT NULL COMMENT 预计入住时间, checkout_date datetime NOT NULL COMMENT 预计离店时间, actual_checkout_date datetime DEFAULT NULL COMMENT 实际离店时间, total_amount decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 应收总额, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_customer_id (customer_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT寄养订单表; -- 商品表 CREATE TABLE product ( id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 主键, name varchar(100) NOT NULL COMMENT 商品名称, price decimal(10,2) NOT NULL COMMENT 销售单价, stock_qty int NOT NULL DEFAULT 0 COMMENT 当前库存, status tinyint NOT NULL DEFAULT 1 COMMENT 1上架 0下架, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT商品表; -- 商品订单明细表 CREATE TABLE order_item ( id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 主键, order_id bigint unsigned NOT NULL COMMENT 寄养订单ID这里复用为销售订单ID, product_id bigint unsigned NOT NULL COMMENT 商品ID, product_name varchar(100) NOT NULL COMMENT 商品名快照, price decimal(10,2) NOT NULL COMMENT 成交单价快照, qty int NOT NULL COMMENT 购买数量, subtotal decimal(10,2) NOT NULL COMMENT 小计金额, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT订单商品明细表;这段 DDL 里有几个点值得细说。第一是 order_no 专门建了唯一索引这是防止重复下单的最后一道物理防线业务代码写错了它也能拦住。第二是 boarding_order 里的 customer_name 和 pet_name 两个冗余字段前面已经解释过是快照查询列表页时可以少关联两张表。第三是 decimal 类型的精度把体重和金额都按实际业务上限来设定宠物体重最多 999.99kg金额 99999999.99 对宠物店完全够用不要随手写个 decimal(255,0) 出来。外键我在这套表里没有建物理外键只建了普通索引比如 idx_customer_id。中小型系统为了性能和解耦这个做法很常见删除数据时用业务代码控制。如果你做的是毕业设计想体现完整性把 FOREIGN KEY 加回去也可以但生产环境我建议保留逻辑外键避免删除客户时被订单表挡住。3.3 典型查询源码增删改查与列表分页的 SQL 和 Mapper表建好只是第一步项目里真正跑起来的是查询和写入。最常见的两个场景是“宠物档案列表分页”和“寄养下单扣库存”。列表分页我在后端用的是 MyBatis 的 XML前端传 pageNo 和 pageSizeSQL 里用 limit 加偏移量。注意一点不要用 PageHelper 一把梭它的总结数查询在大数据量下会明显变慢中小系统没事但理解底层 SQL 会让你排查问题时更有底。!-- 宠物档案分页查询 -- select idselectPetPage resultTypecom.example.pet.entity.PetVO SELECT p.id, p.name, p.species, p.breed, p.weight_kg, p.is_neutered, p.vaccine_status, c.name AS customer_name, c.phone AS customer_phone FROM pet p LEFT JOIN customer c ON c.id p.customer_id where if testkeyword ! null and keyword ! AND (p.name LIKE CONCAT(%, #{keyword}, %) OR c.name LIKE CONCAT(%, #{keyword}, %) OR c.phone LIKE CONCAT(%, #{keyword}, %)) /if if testspecies ! null AND p.species #{species} /if if testvaccineStatus ! null AND p.vaccine_status #{vaccineStatus} /if /where ORDER BY p.id DESC LIMIT #{offset}, #{pageSize} /select这段 XML 里的关键点是动态条件全部拼在 where 里面查询走的是主键排序和覆盖索引如果加了 vaccine_status 筛选要确保索引里有这个字段否则会变成全表扫描。LIKE 查询用在了宠物名和主人手机号上手机号字段建了索引但在 LIKE %keyword% 这种写法里索引会失效数据量小的时候无所谓数据过万后就要考虑分词或者前后缀匹配优化。下单扣库存是另一个必考场景直接用事务和行锁来处理。刚才建的 product 表里 stock_qty 就在 product 行上扣库存和创建订单必须放进同一个事务否则会出现“订单建了库存没扣”或者反过来。我一般会在 Service 层写成这样Transactional(rollbackFor Exception.class) public Long createBoardingOrder(BoardingOrderDTO dto) { // 1. 生成业务单号带日期和随机数保证唯一 String orderNo BO System.currentTimeMillis() RandomUtil.randomNumbers(4); // 2. 从商品表锁行读取当前库存 Product product productMapper.selectByIdForUpdate(dto.getProductId()); if (product.getStockQty() dto.getQty()) { throw new BusinessException(库存不足当前剩余 product.getStockQty()); } // 3. 扣减库存 productMapper.decreaseStock(dto.getProductId(), dto.getQty()); // 4. 创建寄养订单和商品明细 BoardingOrder order new BoardingOrder(); // 这里省略 order 字段赋值 boardingOrderMapper.insert(order); return order.getId(); }selectByIdForUpdate 是在事务里对商品行加了悲观锁两个用户同时下单同一件商品时第二个请求会阻塞到前一个事务提交。对宠物管理系统这种低并发场景悲观锁简单直接不会出现库存超卖。参数上要注意的是事务在同一个类里自己调自己时容易失效我遇到过一次排查了半天发现是没有走 Spring 代理这个坑下面的避坑章里细说。4. 落地避坑5 个让你返工的数据库与源码问题4.1 体重和金额字段用错类型宠物档案数据直接“失真”现象数据库里宠物体重显示 5.199999809265137商品价格 9.9 变成了 9.899999999999999。前端页面小数点后面跟着一长串数字看起来像系统坏了。原因建表时把体重、金额这类精确数值定成了 float 或 double。这两个类型在 MySQL 里是浮点数底层是近似存储算出来的结果天然带误差。做订单和计价哪怕差一分钱都会被客户发现。解决已经建好的表只能改字段类型。先去业务代码里确认有没有依赖这个字段的计算逻辑再执行 ALTER TABLEALTER TABLE pet MODIFY COLUMN weight_kg DECIMAL(5,2) NOT NULL DEFAULT NULL COMMENT 体重(kg); ALTER TABLE product MODIFY COLUMN price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 销售单价;改完字段后历史数据里的小数尾巴会被四舍五入此时要立刻抽查几条记录并且把代码里所有写入该字段的地方都改成 BigDecimal 而不是 Double。这里有个细节Java 的 BigDecimal 构造时要用字符串写成 new BigDecimal(19.9)直接 new BigDecimal(19.9) 等于又把浮点误差带回来了这是同一条坑的两个入口。4.2 订单重复提交导致重复扣库存少了唯一索引和事务现象用户快速点了两次“提交订单”页面第一次没跳转又点了一次。后台出现两条相同内容的寄养订单商品库存被扣了两遍。客户当场发火开发一脸无辜地说“前端背锅”。原因前端按钮没有做防重复点击是一层但后端也有责任——下单接口没有做幂等控制。单靠前端 disabled 按钮拦不住网络重发和客户端工具重放后端必须有一道物理防线。解决两道防线都补上。第一道是数据库层面boarding_order 表的 order_no 字段加唯一索引之前 DDL 里已经写进去了。第二道是代码层面下单前先按 order_no 查一次存在就直接返回原订单不再扣库存同时把创建订单和扣库存包进同一个事务。如果还担心极端并发就把查重也放进事务里。我这里吃过亏单独一个 insert 没有异常处理唯一索引冲突抛出来的 DuplicateKeyException 没接住导致接口直接 500前端拿到的是“系统错误”用户完全看不懂。后来我在事务边界上统一 catch 一下转成“订单已提交请勿重复操作”的提示这事才算闭环。4.3 列表分页翻到第 100 页变慢深分页让索引失效现象寄养订单列表前 10 页飞快翻到第 80 页明显卡顿第 100 页请求耗时超过 5 秒。查看慢查询日志发现都是同一个分页 SQL。原因LIMIT 10000, 20 这样的写法MySQL 会先扫描 10000 行然后把前 10000 行丢掉只返回最后 20 行。数据量到几万条时这个偏移量带来的扫描成本会线性上升。索引建了也没用因为 InnoDB 的二级索引和主键排序不一致回表成本高。解决不要用深偏移量改成延迟关联。常见做法是先查主键再用主键去关联明细行-- 优化前 SELECT * FROM order_item ORDER BY id DESC LIMIT 10000, 20; -- 优化后 SELECT oi.* FROM order_item oi INNER JOIN ( SELECT id FROM order_item ORDER BY id DESC LIMIT 10000, 20 ) tmp ON tmp.id oi.id;子查询里只查主键MySQL 扫描索引比扫描全行快得多拿到主键列表后再回表取完整数据。另一种常用做法是用 last_id 游标代替 offset前端传“上一次看到的最后一条记录的 id”后端直接 WHERE id #{lastId} ORDER BY id DESC LIMIT 20。游标分页翻到第 200 页也不会变慢只是不能随便跳页。宠物管理系统的订单列表按时间倒序看游标完全够用。这个优化做完慢查询直接就消失了。4.4 中文乱码和连接池耗尽两个最容易“玄学”的后台故障中文乱码的现象很典型宠物名字“橘白”入库后变成“”或者读出偶数个问号。我排查过很多次根源通常不在代码而在数据库连接串和建库字符集。建库时用了 utf8MySQL 的 utf8 是 utf8mb3最多只能存 3 字节像一些生僻字、emoji 字符直接变问号。正确做法是建库建表统一用 utf8mb4连接串里显式带上 characterEncodingCREATE DATABASE pet_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;jdbc:mysql://localhost:3306/pet_system?useUnicodetruecharacterEncodingutf8mb4useSSLfalse连接池耗尽是另一种“玄学”故障系统跑几天后突然报“Too many connections”重启就好了过几天又出现。这种情况多数是连接池参数没调默认的连接池连接数不够用同时慢 SQL 把连接一个个占住不释放。排查时先看 MySQL 的 max_connections 配置再看连接池空闲超时。调整 HikariCP 参数时我一般这样设置spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000maximum-pool-size 不是越大越好20 对小项目足够太大反而消耗数据库资源。idle-timeout 和 max-lifetime 保证连接不会长期占用。配合前面深分页的优化慢 SQL 少了连接池自然就稳了。遇到这类问题不要急着去改代码逻辑先看监控是连接创建多了还是连接释放不了对症下药。5. 三件套对齐用字段清单反向校验源码和数据库项目交付前最后一步我习惯做一次“三件套对账”把原型里每个页面的字段和数据库里每个表的字段以及源码中增删改查涉及的所有字段列成一张核对表。这个过程手工做太容易漏我写过一个简单的 Python 校验脚本连接数据库后就地扫描 information_schema把表和字段导出来再跟手工维护的字段清单做差集。import pymysql conn pymysql.connect( hostlocalhost, userroot, passwordyour_password, databasepet_system, charsetutf8mb4 ) expected_fields { pet: [id, customer_id, name, species, breed, birth_date, weight_kg, is_neutered, vaccine_status], boarding_order: [id, order_no, customer_id, pet_id, status, checkin_date, checkout_date], } with conn.cursor() as cursor: for table, expected in expected_fields.items(): cursor.execute( SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA pet_system AND TABLE_NAME %s, (table,) ) actual [row[0] for row in cursor.fetchall()] missing set(expected) - set(actual) extra set(actual) - set(expected) print(f[{table}] missing{missing or 无} extra{extra or 无}) conn.close()这个脚本不复杂但它把校验从人肉对表变成了一行命令。跑完脚本后我会把所有表字段导到一个 Excel 里在原型的每个页面下方标注“字段来源pet 表第 3、5、7 列”这样客户做验收测试时指着页面上的任何一个输入框我都知道数据存在哪张表哪个字段。这个操作看着土但对交付结算和后面维护都特别有用。我自己的教训是做宠物管理系统这类中小型项目顺序永远是“原型锁字段 → 数据库锁结构 → 源码填逻辑”不要倒过来。有一回我接手的项目源码先写好数据库也建好了最后补的原型结果原型的“线上预约”功能和源码里的“到店登记”流程对不上只能返工改一边。后来凡是带“源码原型数据库”字样的项目我都把对账这一步当作验收必经流程客户提的任何修改也能快速判断出——这个改动要动表结构还是只动页面。希望这套流程和方法能帮到你少踩一个坑就少加一次班。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?