简介:围绕 SpringBoot 同城上门喂遛宠物系统撰写的毕业设计论文参考文档,面向计算机相关专业毕业设计阶段的本科生,适合选择 Java Web 方向、需要梳理写作框架与业务建模思路的同学。文档以同城上门喂食、遛宠的预约服务为业务场景,串联选题动因、需求分析、系统设计到结论的完整脉络,可作为开题与正文撰写的参照。功能设计部分覆盖管理员端的爱宠天地、宠物信息、宠物收藏与留言、预约审核、字典与宠物资讯、用户及管理员权限管理,以及用户端的注册登录、宠物信息录入、服务预约、在线支付与评价反馈;技术实现章节说明 SpringBoot、MyBatis、Vue 的搭配方式,并给出用户、宠物、服务、预约、支付、评价等数据表设计,另附安全策略与测试优化内容。压缩包内含 1 个 doc 文件,约 1.34MB,已有 293 人学习。
1. 同城上门喂遛宠物系统:一套订单-审核型的 SpringBoot 后台
很多人第一眼看到"上门喂遛宠物",会以为要做一套美团式的即时配送,得有地图调度、骑手抢单、实时轨迹。真正拆开这套 Java 毕业设计才会发现,它的技术核心根本不是地理计算,而是一张宠物预约表在用户和管理员两个角色之间来回流转:用户提交预约,管理员审核派单,服务结束后回写状态。技术栈是 SpringBoot + MyBatis + MySQL 5.7 + Vue,JDK1.8、Maven3.6、Tomcat 8.0/9.0,标准的 B/S 三层结构。它适合三类人:做同类毕设需要一份可复现骨架的同学、想找一个业务体量适中的 SpringBoot 练手项目的人、以及需要现成的宠物服务业务模型做二次开发的人。底下真正值得抄的是表结构和状态流转,页面反而是最不值钱的部分。
2. 技术选型与 SpringBoot 工程骨架搭建
2.1 依赖版本组合与 pom.xml
毕业设计最常见的事故不是代码写错,而是版本对不上:JDK 装了 17,Spring Boot 却用 2.x 的某些老依赖;Maven 拉下来的 MyBatis 和 Spring Boot Starter 版本打架,启动直接报NoClassDefFoundError。这套系统的版本基线很朴素,但胜在稳定,先在 pom 里把版本钉死,再谈写代码。
| 组件 | 版本基线 | 选它的实际理由 |
|---|---|---|
| JDK | 1.8 | 学校机房与主流教程默认环境,语法够用 |
| Maven | 3.6 | 依赖树解析稳定,国内镜像兼容性好 |
| Spring Boot | 2.x 系列 | 自动装配成熟,JDK1.8 兼容性最好 |
| MyBatis | Starter 方式引入 | XML 写 SQL 直观,答辩时讲得清 |
| MySQL | 5.7 | 行式存储,事务与索引满足预约场景 |
| Tomcat | 8.0 / 9.0 | 内嵌容器可省,外置部署更贴近毕设要求 |
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <!-- 按团队基线替换,JDK1.8 对应 2.x 系列 --> <version>2.7.x</version> </parent> <dependencies> <!-- Web 层:内嵌 Tomcat + SpringMVC --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 持久层:MyBatis 与 SpringBoot 的整合 Starter --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.x</version> </dependency> <!-- MySQL 驱动,5.7 服务端用 8.x 驱动需注意时区参数 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- 参数校验,预约时间、手机号这类字段靠它兜底 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>spring-boot-starter-parent负责统一管理子依赖版本,所以 web、validation 这些可以不写 version,只有 MyBatis Starter 因为不在父 POM 管理范围内需要显式声明。mysql-connector-java的 scope 设为 runtime,编译期不需要它,打包时才进 lib 目录。如果你的 MySQL 是 5.7 而驱动用了 8.x,连接串必须带serverTimezone=Asia/Shanghai,否则启动就会抛时区异常,这是新手第一个坑。
2.2 application.yml 里的数据源与 MyBatis 配置
配置文件是 SpringBoot 自动装配原理最直观的入口:DataSourceAutoConfiguration会读spring.datasource前缀,MybatisAutoConfiguration会读mybatis前缀,把SqlSessionFactory和 Mapper 扫描都替你做完。所以真正要改的东西不多,关键是把驼峰映射和 Mapper 位置写对。
server: port: 8080 servlet: context-path: /pet spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/pet_service?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 123456 # 上传宠物照片、爱宠视频的本地目录 servlet: multipart: max-file-size: 20MB max-request-size: 50MB mybatis: # XML 映射文件位置,多个目录用逗号分隔 mapper-locations: classpath:mapper/*.xml # 实体包路径,开启后 XML 里写 resultType 可用短类名 type-aliases-package: com.pet.entity configuration: # 数据库 chongwu_name 自动映射到实体 chongwuName map-underscore-to-camel-case: true # 开发期打印 SQL,上线前务必改为 NoLoggingImpl log-impl: org.apache.ibatis.logging.stdout.StdOutImpl logging: level: com.pet.mapper: debug几个参数值得单独盯住。map-underscore-to-camel-case: true决定了chongwu_uuid_number能不能自动映射到chongwuUuidNumber,不开这个开关就得在每个resultMap里手写列映射,工作量翻倍。mapper-locations的路径要和 resources 下的目录完全一致,写成classpath*:mapper/**/*.xml才能扫到子目录。log-impl在开发期非常有用,能直接看到拼出来的 SQL 和参数,排错效率提升明显,但生产环境打印全量 SQL 会拖慢响应并泄露数据,上线前记得换成org.apache.ibatis.logging.nologging.NoLoggingImpl。
2.3 包结构与统一返回体
分层不要贪多,这套系统的体量用 controller / service / service.impl / mapper / entity / vo 六层足够。真正要提前设计的是统一返回体,否则前端每个接口都要写不同的解析逻辑,Vue 那边的 axios 拦截器会变得极其难维护。
@Data public class Result<T> implements Serializable { // 200 成功,500 业务或系统异常 private Integer code; private String msg; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMsg("操作成功"); r.setData(data); return r; } public static <T> Result<T> fail(String msg) { Result<T> r = new Result<>(); r.setCode(500); r.setMsg(msg); return r; } }code用 200/500 两态是毕设项目的惯用简化,真实项目里会细分到 401 未登录、403 无权限、422 参数校验失败。data用泛型 T 是为了同一个类能承载分页对象、单个实体或布尔值。msg面向用户展示,所以不要在 fail 里塞堆栈信息,堆栈交给全局异常处理器打日志。加了 Lombok 的@Data后 getter/setter 自动生成,答辩时如果被问到可以直接说清注解在编译期生成字节码的原理。
2.4 与 Vue 前端的对接约定
前后端分离最容易在跨域和字段命名上翻车。后端返回的实体字段是驼峰chongwuName,前端模板里如果直接写chongwu_name就会渲染空白。常见做法是在application.yml里统一配置路径前缀/pet,前端 axios 的baseURL指向http://localhost:8080/pet;跨域则加一个WebMvcConfigurer配置类,允许来源、方法、请求头三件套。
3. 从 E-R 图到建表 SQL:宠物预约业务的数据落地
3.1 实体识别与关系梳理
这套系统里能抽出七个核心实体:爱宠天地、宠物、宠物收藏、宠物留言、宠物预约、宠物资讯、用户。关系上并不复杂:一个用户可以有多只宠物,一个用户可以对多只宠物产生收藏和留言,一条宠物预约记录把用户和宠物绑在一起并带上时间与服务类型。E-R 图里矩形代表实体、椭圆代表属性、菱形代表联系,画图工具用 Visio 或者 draw.io 都行,关键是把主键和外键的方向标清楚,否则建表时会反复改。
3.2 核心表建表语句
论文里的字段类型写的是 String、Integer、Date,这是 Java 视角的抽象,落到 MySQL 需要做一次映射:String 对应 varchar,Integer 对应 int,Date 对应 datetime。下面是宠物表与宠物预约表的实际建表脚本。
CREATE TABLE `chongwu` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `chongwu_name` varchar(64) DEFAULT NULL COMMENT '宠物名称', `chongwu_uuid_number` varchar(64) DEFAULT NULL COMMENT '宠物编号', `chongwu_photo` varchar(255) DEFAULT NULL COMMENT '宠物照片路径', `chongwu_address` varchar(255) DEFAULT NULL COMMENT '宠物地点', `zan_number` int(11) DEFAULT 0 COMMENT '赞数', `cai_number` int(11) DEFAULT 0 COMMENT '踩数', `chongwu_types` int(11) DEFAULT NULL COMMENT '宠物类型,关联字典表', `chongwu_content` text COMMENT '宠物介绍', `chongwu_delete` int(1) DEFAULT 1 COMMENT '逻辑删除,1 正常 2 已删', `insert_time` datetime DEFAULT NULL COMMENT '录入时间', `create_time` datetime DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_uuid` (`chongwu_uuid_number`), KEY `idx_types` (`chongwu_types`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物表'; CREATE TABLE `chongwu_yuyue` ( `id` int(11) NOT NULL AUTO_INCREMENT, `chongwu_yuyue_uuid_number` varchar(64) DEFAULT NULL COMMENT '预约单号', `chongwu_id` int(11) DEFAULT NULL COMMENT '关联宠物', `yonghu_id` int(11) DEFAULT NULL COMMENT '下单用户', `chongwu_yuyue_types` int(11) DEFAULT NULL COMMENT '服务类型:1 喂食 2 遛宠', `yonghu_address` varchar(255) DEFAULT NULL COMMENT '上门地址', `yuyue_time` datetime DEFAULT NULL COMMENT '预约上门时间', `chongwu_yuyue_status_types` int(11) DEFAULT 1 COMMENT '状态:1 待审核 2 已通过 3 已完成 4 已拒绝', `chongwu_yuyue_yesno_types` int(11) DEFAULT 1 COMMENT '审核结果', `insert_time` datetime DEFAULT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_yuyue_uuid` (`chongwu_yuyue_uuid_number`), KEY `idx_yonghu_status` (`yonghu_id`, `chongwu_yuyue_status_types`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物预约表';chongwu_uuid_number上建了唯一索引,是因为业务上用它对外暴露单号,不暴露自增 id,避免别人顺着 id 猜数据量。idx_yonghu_status这个联合索引是给"我的预约列表按状态筛选"这个高频查询准备的,索引列顺序必须把区分度高的yonghu_id放左边。表字符集用 utf8mb4 而不是 utf8,因为宠物昵称里出现 emoji 的概率不低,utf8 三字节存不下会直接插入失败。
3.3 逻辑删除、录入时间与创建时间的统一约定
chongwu_delete这类字段几乎每张表都有,值 1 表示正常、2 表示删除。它带来的好处是数据可追溯,误删能恢复,坏处是每条查询都得带上where chongwu_delete = 1,漏一处就会把删除数据查出来。我的做法是在 MyBatis 里用<sql id="Base_Where">片段统一拼接这段条件,各查询<include>进来,避免手写漏掉。insert_time和create_time看似冗余,实际用途不同:前者是业务发生时间(比如预约提交那一刻),后者是记录落库时间,做数据对账时两者对不上就说明有补录或脚本刷数。
3.4 索引与外键的取舍
MyBatis 项目里普遍不建物理外键,改由 Service 层保证一致性。原因很实际:外键会让批量删除、数据导入变得很麻烦,毕设数据初始化脚本常常直接truncate再插,有外键就删不动。取而代之的是在关联字段上建普通索引,比如chongwu_id、yonghu_id,保证关联查询不回表全扫。索引不是越多越好,每多一个索引就多一份写入开销,宠物表这种读多写少的可以多建,预约表因为状态更新频繁,索引控制在三个以内比较稳。
4. 预约闭环实现:从用户下单到管理员审核
4.1 预约状态机设计
预约表最容易写乱的地方是状态字段。有人用 0/1 表示是否审核,再额外加一个字段表示是否完成,两个字段组合出四种状态,逻辑分支瞬间爆炸。正确做法是用单个状态字段承载完整流转。
| 状态值 | 含义 | 允许的下一状态 | 触发方 |
|---|---|---|---|
| 1 | 待审核 | 2 已通过 / 4 已拒绝 | 管理员 |
| 2 | 已通过 | 3 已完成 | 管理员 |
| 3 | 已完成 | 无(终态) | — |
| 4 | 已拒绝 | 无(终态) | — |
状态流转必须在 Service 层做校验,不能只靠前端按钮隐藏。前端可以绕过,接口不能。
4.2 创建预约的 Service 与事务边界
@Service public class ChongwuYuyueServiceImpl implements ChongwuYuyueService { @Resource private ChongwuYuyueMapper yuyueMapper; @Resource private ChongwuMapper chongwuMapper; @Override @Transactional(rollbackFor = Exception.class) public Result<String> createYuyue(ChongwuYuyue yuyue) { // 1. 校验宠物是否存在且未被逻辑删除 Chongwu chongwu = chongwuMapper.selectById(yuyue.getChongwuId()); if (chongwu == null || chongwu.getChongwuDelete() == 2) { return Result.fail("宠物不存在或已下架"); } // 2. 生成全局唯一预约单号,避免暴露自增 id yuyue.setChongwuYuyueUuidNumber( UUID.randomUUID().toString().replace("-", "")); // 3. 初始状态固定为待审核,不接受前端传值 yuyue.setChongwuYuyueStatusTypes(1); yuyue.setInsertTime(new Date()); yuyue.setCreateTime(new Date()); yuyueMapper.insert(yuyue); return Result.ok("预约提交成功,等待管理员审核"); } @Override @Transactional(rollbackFor = Exception.class) public Result<String> audit(Integer id, Integer status) { ChongwuYuyue db = yuyueMapper.selectById(id); if (db == null) { return Result.fail("预约单不存在"); } // 只有待审核状态才允许被审核,防止重复点击造成状态回退 if (db.getChongwuYuyueStatusTypes() != 1) { return Result.fail("该预约单已被处理"); } if (status != 2 && status != 4) { return Result.fail("非法的审核结果"); } ChongwuYuyue update = new ChongwuYuyue(); update.setId(id); update.setChongwuYuyueStatusTypes(status); update.setChongwuYuyueYesnoTypes(status == 2 ? 2 : 3); yuyueMapper.updateById(update); return Result.ok("审核完成"); } }@Transactional(rollbackFor = Exception.class)比默认的@Transactional更安全,因为默认只在运行时异常回滚,遇到受检异常不回滚,容易留下半截数据。创建方法里三处赋值是刻意的防御:单号服务端生成、状态服务端写死、时间服务端填充,任何一项让前端传都会变成漏洞入口。审核方法里先查再判再改,中间那次状态检查就是乐观锁的简化版,能挡住管理员的重复点击。
4.3 重复预约的幂等处理
用户手抖连点两次提交,会生成两条一模一样的预约单。前端按钮置灰只能减少概率,不能根治。常见做法是给yonghu_id + chongwu_id + yuyue_time建一个唯一索引,让数据库来兜底。
ALTER TABLE `chongwu_yuyue` ADD UNIQUE KEY `uk_user_pet_time` (`yonghu_id`, `chongwu_id`, `yuyue_time`);加了唯一索引后,重复插入会抛DuplicateKeyException,在全局异常处理器里捕获并翻译成"请勿重复提交"即可。注意索引列不能选太长的时间精度,如果yuyue_time精确到秒,用户隔一秒再点一次还是能插进去,所以业务上一般会把时间对齐到分钟再做比较。
4.4 参数校验与全局异常
@Validated配合实体上的@NotBlank、@Future能把大部分脏数据挡在 Controller 之前。预约时间用@Future限制必须是将来时间,避免用户填个历史时间去刷单。全局异常处理用@RestControllerAdvice统一拦截三类:参数校验失败MethodArgumentNotValidException、唯一键冲突DuplicateKeyException、兜底的Exception。三类返回的 msg 都要是人话,别把java.sql.SQLIntegrityConstraintViolationException原样丢给用户。
5. 收藏、留言与资讯模块的复用写法
5.1 收藏表的多态复用
宠物收藏表的字段是chongwu_id + yonghu_id + chongwu_collection_types,那个 types 字段是给后续扩展留的口子:现在只收藏宠物,将来要收藏资讯、收藏爱宠天地,就不用再建新表。
| collection_types | 含义 | 关联字段指向 |
|---|---|---|
| 1 | 收藏宠物 | chongwu_id |
| 2 | 收藏资讯 | news_id(扩展) |
| 3 | 收藏爱宠天地 | aichong_id(扩展) |
取消收藏不要物理删除,改chongwu_delete反而更省事。判断"是否已收藏"的查询走(yonghu_id, chongwu_id, chongwu_collection_types)联合索引,三个字段一起查,命中率最高。
5.2 留言与回复的字段设计
宠物留言表里的reply_text + update_time是成对出现的,只有reply_text非空时update_time才有意义。前端展示时先判空再渲染回复区块,否则空字符串会撑出一个空的回复框。回复动作用一个独立的update接口实现,只允许chongwu_liuyan_id对应的记录被更新,防止越权改别人的留言。留言内容字段建议用 text 而不是 varchar(255),用户吐槽起来很容易超长。
UPDATE chongwu_liuyan SET reply_text = #{replyText}, update_time = NOW() WHERE id = #{id} AND chongwu_delete = 1这条语句里chongwu_delete = 1不能省,否则可能回复到一条已被逻辑删除的留言上,产生"回复了看不见的评论"这种诡异现象。
5.3 分页查询与 N+1 问题
列表接口一律分页,用LIMIT加 count 两次查询,或者引入 PageHelper。最容易踩的坑是 N+1:先查出 10 条留言,再循环查每条留言对应的宠物名称,10 条留言就是 11 次数据库往返。正确做法是在 XML 里用<collection>做嵌套结果映射,或者干脆一次 join 把宠物名带出来,返回一个 VO。
<select id="selectLiuyanPage" resultType="com.pet.vo.LiuyanVO"> SELECT l.id, l.chongwu_liuyan_text AS content, l.insert_time AS insertTime, c.chongwu_name AS chongwuName, u.yonghu_name AS yonghuName FROM chongwu_liuyan l LEFT JOIN chongwu c ON c.id = l.chongwu_id LEFT JOIN yonghu u ON u.id = l.yonghu_id WHERE l.chongwu_delete = 1 ORDER BY l.insert_time DESC LIMIT #{offset}, #{pageSize} </select>用 LEFT JOIN 而不是 INNER JOIN,是为了让被删掉的宠物或用户不至于把整条留言查丢。offset由(pageNum - 1) * pageSize算出,pageSize 建议在 Controller 层做上限校验,防止前端传pageSize=100000把内存打满。
6. 上线前验证:功能测试、慢 SQL 与高频报错排查
6.1 关键功能的测试用例设计
登录功能是必测项,要覆盖账号不存在、密码错误、账号被禁用、正常登录四条分支。预约流程按状态机逐条走:待审核提交后能否被管理员看到、审核通过后用户端状态是否同步、已完成的单子能否被再次审核。测试数据要刻意造边界值,比如预约时间填当前时间的前一分钟、上门地址填 300 个字符、宠物照片传 21MB。这些用例写下来不到一页纸,但能挡住答辩现场大部分尴尬。
6.2 慢 SQL 定位与索引复核
接口变慢先别改代码,开slow_query_log把超过 1 秒的语句捞出来。
# my.cnf 中开启慢日志 slow_query_log = 1 long_query_time = 1 slow_query_log_file = /var/log/mysql/slow.log捞到嫌疑 SQL 后统一在前面加EXPLAIN,重点看三列:type如果是 ALL 说明全表扫描,rows是预估扫描行数,Extra里出现Using filesort或Using temporary说明排序和分组没走索引。预约列表按insert_time倒序且带用户过滤时,idx_yonghu_status帮不上忙,需要再补一个(yonghu_id, insert_time)的联合索引。索引加完记得重新ANALYZE TABLE,让优化器更新统计信息。
6.3 高频报错速查
| 报错关键字 | 根因 | 处理方式 |
|---|---|---|
Access denied for user | 账号密码或授权库不对 | 核对 yml 配置,确认该账号有库权限 |
Unknown column 'xxx' | 实体字段与表列不匹配 | 检查驼峰映射开关和 XML 列名 |
Public Key Retrieval is not allowed | MySQL 8 驱动连接 5.7 | 连接串加allowPublicKeyRetrieval=true |
Invalid bound statement | Mapper 方法找不到 XML | 核对 namespace 与 id 是否一致 |
| 上传大文件 500 | multipart 限制太小 | 调大 max-file-size 与 max-request-size |
| 时间差 8 小时 | 服务端时区与 JDBC 时区不一致 | 统一设置serverTimezone=Asia/Shanghai |
Invalid bound statement是初学阶段出现频率最高的一个,八成是 XML 里的namespace写成了实体类的全限定名而不是 Mapper 接口的,或者<select>的 id 和方法名差了一个字母。排查时可以直接在 IDEA 里用全局搜索定位 XML 文件,比逐个打开快得多。
本文还有配套的精品资源,点击获取