养宠人给宠物记体重、打疫苗、驱虫,往往靠备忘录和微信记录,数据散落得到处都是。把这一整套流程系统化、线上化,就催生了“宠物成长监管系统”这类项目。作为 SpringBoot 毕设题目,它的定位很典型:不是一个纯 CRUD 空壳,而是要覆盖宠物档案、成长数据采集、健康预警、到期提醒等多个环节的全栈应用。
这篇文章我打算从需求拆解讲起,一路聊到表结构设计、核心业务逻辑、文件存储、远程部署、论文写作和答辩准备。无论你是已经选了类似题目、正在赶进度的学生,还是想用 SpringBoot 做一个完整项目练手的人,都可以直接对着参考。项目涉及的东西不复杂,但坑不少,我把能踩的坑提前给你列出来。
1. 项目需求盘点与整体方案设计
1.1 先搞清楚“宠物成长监管”到底监管什么
很多同学拿到这类题目就急着开写,结果做了半个月发现只是个平平无奇的增删改查。问题出在需求没想透。“宠物成长监管”这个说法,核心落在“监管”两个字上,落到业务里就是三件事:记录、分析和提醒。
第一件事是记录。宠物基本信息要有,包括品种、性别、出生日期、绝育状态、疫苗记录、驱虫记录。成长数据也要有,比如定期记录的体重、体长、体温、每日进食量、运动量。这些数据是后续所有分析的基础。
第二件事是分析。系统要根据已记录的成长数据生成趋势曲线,比如体重随月龄的变化曲线。更进一步,要能对照品种标准值判断宠物发育是否异常。金毛和柯基的体重标准完全不同,所以品种标准表必须独立维护,不能写死在代码里。
第三件事是提醒。疫苗到期、驱虫时间、体检计划、喂食时间,这些都需要提前通知。提醒是这类系统最容易被用户感知的价值点,也是答辩时能讲清楚业务闭环的地方。
所以在需求阶段就要把角色划分清楚:普通用户管理自己名下的宠物,管理员维护品种标准和健康知识库。整个系统的价值不在于功能多,在于“数据录入后有反馈、有预警”,这一点想明白了,后面做设计才不会跑偏。
1.2 技术选型背后的取舍逻辑
技术栈方面,SpringBoot 项目几乎有默认答案:SpringBoot 2.7.x + MyBatis-Plus + MySQL 8.0 + Redis + MinIO,前端可选 Vue 2/3。不过每个选择背后都有原因,我逐个说。
首先,SpringBoot 版本千万别选 3.x。网上问“springboot版本太高导致启动失败”的人特别多,大多是在跟着 2.x 教程敲代码,自己却建了个 3.x 工程,结果 javax 和 jakarta 命名空间冲突、配置项变更、自动装配机制变化,一堆莫名其妙的问题。毕设要的是稳定复现,稳妥的方案是用 SpringBoot 2.7.x 配 JDK 8 或 JDK 11,不要为了“新版”给自己挖坑。
其次,持久层用 MyBatis-Plus 而不是 JPA。JPA 自动建表确实方便,但复杂统计查询写起来很别扭,生成的 SQL 也不好控制。MyBatis-Plus 的 BaseMapper 把单表 CRUD 封装好了,自定义 SQL 放在 XML 里,逻辑清晰,学生更容易理解和解释,答辩时被问到 SQL 细节也能讲明白。
文件存储单独选了 MinIO。宠物成长监管一定要传照片和体检报告 PDF,很多初学者会把文件存在项目的 upload 目录下,这在本地跑没问题,但远程部署时路径混乱、重启丢文件、容器里写不进宿主机目录,都很折腾。MinIO 是开源对象存储,Docker 一条命令就能启动,接口兼容 S3,用起来和阿里云 OSS 几乎一样,写在简历里也是加分项。
Redis 则用来缓存热点数据,比如品种标准、宠物档案的常用查询。同时,疫苗到期提醒可以用 Redis 的过期键或者简单的定时扫描+消息队列来实现,让项目有“中间件”的参与感。技术深度不是靠堆框架,而是靠把合适的工具用在合适的环节。
1.3 系统模块划分与功能清单
结合以上分析,我把系统划分成几个模块,每个模块的职责和主要功能如下表:
| 模块 | 核心功能 | 权限 |
|---|---|---|
| 用户模块 | 注册登录、个人信息维护、密码修改 | 用户本人 |
| 宠物档案模块 | 添加宠物、编辑档案、查看详情 | 用户本人,管理员可查看全部 |
| 成长记录模块 | 体重/体长/体温/进食记录、成长曲线展示 | 用户本人 |
| 健康监测模块 | 品种标准维护、异常指标预警 | 管理员维护标准,用户查看结果 |
| 提醒模块 | 疫苗/驱虫/体检到期提醒、消息列表 | 用户本人 |
| 系统管理模块 | 用户管理、数据统计 | 管理员 |
这样划分有个明显的好处:模块之间职责清晰,写论文的时候可以按模块一章一节地写,数据库表也能一一对应。更重要的是,每个模块都有业务出口,不是为做而做——比如健康监测模块对应的就是“提交体重后系统判断是否超标”,这一条业务线能从表走到代码再到界面,完整串起来。
2. SpringBoot 核心机制与配置文件实操
2.1 自动装配原理与 starter 的作用
SpringBoot 最核心的机制是自动装配。你也许听过“约定大于配置”这句话,但真正理解自动装配,需要知道它到底做了什么。简单来说,当你引入 spring-boot-starter-web 这个依赖后,SpringBoot 会在启动时自动帮你创建 Spring MVC 所需的一系列 Bean,比如 DispatcherServlet、RequestMappingHandlerMapping、内置 Tomcat 等,而你不需要写 XML 配置。
自动装配的入口是主类上的 @SpringBootApplication 注解,它其实是个组合注解:@SpringBootConfiguration 表示这是一个配置类,@ComponentScan 扫描当前包及子包的组件,最关键的是 @EnableAutoConfiguration。这个注解会加载 META-INF 目录下的 spring.factories 文件(2.7 版本)或者 AutoConfiguration.imports 文件(3.x 版本),里面列出了所有候选的自动配置类。
但注意,候选不等于全部生效。SpringBoot 会通过 @ConditionalOnClass、@ConditionalOnMissingBean 等条件注解来判断:当你的 classpath 里有某个类、且你没有手动定义相关 Bean 时,自动配置才生效。这就是为什么引入 Redis 依赖后,Spring Boot 自动帮你创建 RedisTemplate,而你引入 MySQL 驱动后,它又会去配置 DataSource。理解这条逻辑线,以后遇到“为什么配置没生效”的问题就不慌了。
2.2 一份能直接跑的 application.yml 配置
配置文件是项目跑起来的第一道关卡。我用一份实际可用的 application.yml 来展示,注释都写清楚,你可以直接抄。
server: port: 8080 # 服务端口,远程部署时记得改或通过环境变量覆盖 servlet: context-path: /pet # 统一访问前缀,结合Nginx反向代理更灵活 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/pet_growth?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 hikari: maximum-pool-size: 10 minimum-idle: 5 redis: host: localhost port: 6379 database: 0 timeout: 3000ms servlet: multipart: max-file-size: 10MB # 体检报告PDF、宠物照片不超过10MB max-request-size: 20MB mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL,排错神器 map-underscore-to-camel-case: true minio: endpoint: http://localhost:9000 access-key: minioadmin secret-key: minioadmin bucket-name: pet-images logging: level: com.example.pet.mapper: debug这里有几个细节:数据源 URL 必须加 serverTimezone,否则高版本 MySQL 驱动会报时区错误;Redis 的 timeout 不要写 0,否则连接池可能一直阻塞;MyBatis-Plus 的日志配置在开发环境一定要开,看到 SQL 才能定位问题,部署时可以关掉。
2.3 基于 IDEA 工程搭建与两种启动方式
有了配置文件,工程的搭建方式我强烈建议用 IDEA 的 Spring Initializr:New Project 里选 Spring Boot 2.7.18,依赖选 Web、MySQL Driver、Redis、Lombok,然后手动加 MyBatis-Plus 和 MinIO 的依赖。不要用网页版初始化器生成 3.x 版本,注意看 Spring Boot 版本号。
pom.xml 里核心依赖长这样:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.5.7</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>启动方式也有两种,本地开发直接在 IDEA 里点绿色三角,远程运行则要打 jar 包:
mvn clean package -DskipTests java -jar target/pet-system-0.0.1-SNAPSHOT.jar --server.port=8081这里命令里的--server.port=8081不是随便加的。服务器上很可能 8080 已经被别的进程占用,用命令行参数覆盖配置文件,比改代码再重新打包高效得多。如果你在 IDEA 2026 这类新版本里改了启动端口不生效,检查一下右上角运行配置里是否勾选了“Enable launching multiple instances”或环境变量覆盖项,这类问题八成是 IDE 运行配置和 application.yml 的优先级没弄清。
3. 数据库设计与核心业务实现
3.1 六张核心表的设计与 DDL 示例
数据库设计是这类系统的重头戏。我见过很多同学把所有信息塞进一张大表,字段几十个,又乱又难维护。合理的做法是按业务拆表。我这里给出一版精简但完整的设计,实际项目可以直接扩展。
第一个是用户表 user_info,字段包括 id、username、password(BCrypt 加密)、nickname、phone、create_time。第二个是宠物档案表 pet_info,核心字段有 id、user_id、pet_name、species(犬/猫)、breed、gender、birth_date、sterilization_status(是否绝育)、avatar_url、create_time。
第三个是成长记录表 growth_record,这是系统的核心数据表,字段包括 id、pet_id、record_date、weight(kg)、body_length(cm)、temperature、feed_amount、exercise_minutes、remark、create_time。第四个是品种标准表 breed_standard,用于存储不同品种的参考数据,字段有 id、breed_name、min_weight、max_weight、min_height、max_height、standard_source。
第五个是疫苗记录表 vaccine_record,字段包括 id、pet_id、vaccine_name、vaccine_date、next_due_date、hospital、remark。第六个是提醒任务表 remind_task,字段包括 id、user_id、pet_id、task_type(VACCINE/DEDWORM/CHECKUP)、task_title、trigger_date、status(PENDING/DONE)、create_time。
核心表的 DDL 我贴一两张,感受一下字段类型和约束:
CREATE TABLE pet_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', user_id BIGINT NOT NULL COMMENT '所属用户ID', pet_name VARCHAR(50) NOT NULL COMMENT '宠物昵称', species VARCHAR(20) NOT NULL COMMENT '物种:犬/猫', breed VARCHAR(50) NOT NULL COMMENT '品种', gender TINYINT NOT NULL DEFAULT 1 COMMENT '1公 2母', birth_date DATE NOT NULL COMMENT '出生日期', sterilization_status TINYINT NOT NULL DEFAULT 0 COMMENT '0未绝育 1已绝育', avatar_url VARCHAR(255) DEFAULT NULL COMMENT '头像地址', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_id (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物档案表'; CREATE TABLE growth_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, pet_id BIGINT NOT NULL COMMENT '宠物ID', record_date DATE NOT NULL COMMENT '记录日期', weight DECIMAL(5,2) NOT NULL COMMENT '体重kg', body_length DECIMAL(5,1) DEFAULT NULL COMMENT '体长cm', temperature DECIMAL(3,1) DEFAULT NULL COMMENT '体温℃', feed_amount DECIMAL(5,2) DEFAULT NULL COMMENT '日喂食量g', exercise_minutes INT DEFAULT NULL COMMENT '运动时长分钟', remark VARCHAR(255) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_pet_date (pet_id, record_date), CONSTRAINT uk_pet_date UNIQUE (pet_id, record_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成长记录表';注意 growth_record 表加了一个唯一约束 uk_pet_date,同一天对同一只宠物只能记录一条成长数据,从数据库层面杜绝重复,这个细节在实际开发里很实用。在代码里要做好异常捕获,避免重复提交时暴露生硬的数据库错误。
3.2 成长指标计算的实现思路
数据表建好后,第二个容易卡住的点是“成长分析怎么算”。我建议做三个常用指标,计算逻辑不复杂,但能很直观地体现系统智能性。
第一个指标是月龄计算。这是很多功能的基准,计算方式为:
public static int getMonthAge(LocalDate birthDate, LocalDate currentDate) { if (birthDate == null || currentDate == null) { return 0; } Period period = Period.between(birthDate, currentDate); return period.getYears() * 12 + period.getMonths(); }这里有个细节:直接用 Period 计算,如果当前日期是“月末最后一天”,可能出现月数不准确的情况,比如 1 月 31 日到 2 月 28 日会被算成 0 个月。更稳妥的做法是基于天数换算:currentDate.toEpochDay() - birthDate.toEpochDay(),再除以 30.44 取整。这样能避免跨月边界的问题。
第二个指标是体重增长率,用于判断宠物近期发育速度。核心公式是“(本次体重 - 上次体重)/ 间隔天数 再乘以 100”:
public static BigDecimal calcDailyGain(BigDecimal currentWeight, BigDecimal previousWeight, long days) { if (days <= 0) { return BigDecimal.ZERO; } return currentWeight.subtract(previousWeight) .divide(BigDecimal.valueOf(days), 3, RoundingMode.HALF_UP) .multiply(BigDecimal.valueOf(100)); }日增重率更适合幼年期宠物,成年宠物更关注体重波动区间。所以第三个指标是品种区间比对:取到当前宠物的品种标准,判断体重是否落在 minWeight 和 maxWeight 之间,超出就返回预警级别(偏低/偏高/正常)。这个判断逻辑单独抽成一个工具方法,Service 层调用即可,不要写散。
3.3 基于 MyBatis-Plus 的持久层实现
持久层用 MyBatis-Plus 后,单表 CRUD 基本不需要写 SQL。定义一个实体类对应表结构,比如 GrowthRecord 类加 @TableName("growth_record") 注解,Mapper 接口继承 BaseMapper 就行:
@Mapper public interface GrowthRecordMapper extends BaseMapper<GrowthRecord> { // 自定义查询在XML中实现 List<GrowthRecord> selectLatestList(@Param("petId") Long petId, @Param("limit") Integer limit); List<Map<String, Object>> selectMonthlyAvgWeight(@Param("petId") Long petId, @Param("startDate") String startDate); }复杂查询写在 XML 里。比如“最近N次体重记录”和“按月统计平均体重”,这类是成长曲线图的数据来源。XML 大致这样:
<select id="selectLatestList" resultType="com.example.pet.entity.GrowthRecord"> SELECT * FROM growth_record WHERE pet_id = #{petId} ORDER BY record_date DESC LIMIT #{limit} </select> <select id="selectMonthlyAvgWeight" resultType="java.util.Map"> SELECT DATE_FORMAT(record_date, '%Y-%m') AS month, ROUND(AVG(weight), 2) AS avgWeight FROM growth_record WHERE pet_id = #{petId} AND record_date >= #{startDate} GROUP BY DATE_FORMAT(record_date, '%Y-%m') ORDER BY month </select>这里重点说一下为什么月平均曲线用 SQL 聚合而不是查明细再在 Java 里算。数据量小的时候两种方式都没问题,但明细数据一旦上千条,前端要画出一年 12 个月的曲线,传 365 条数据和传 12 条数据的网络开销完全不一样。用数据库做预聚合,既快又简单。这就是为什么要在 XML 里写 SQL,而不是所有东西都靠 BaseMapper 自带的方法。
3.4 Redis 缓存与到期提醒的实现
Redis 在这个项目里可以承担两个职责:缓存热点数据和辅助提醒任务。
缓存方面,品种标准表 breed_standard 是典型的读多写少,每次判断体重超标都查数据库太浪费。用 Spring Cache 加注解是最简单的方案:
@Cacheable(cacheNames = "breedStandard", key = "#breed") public BreedStandard getByBreed(String breed) { return breedStandardMapper.selectOne(new LambdaQueryWrapper<BreedStandard>() .eq(BreedStandard::getBreedName, breed)); }一点注意:@Cacheable 的缓存 key 默认包含参数,如果 breed 参数为空会缓存空数据,下次命中就查不到更新。所以在方法入口做好参数校验,或者用unless = "#result == null"阻止空值缓存。
提醒任务方面,最简单的实现是写一个定时任务,每天扫描 remind_task 表中未来 7 天内到期的任务,扫描到就发通知:
@Scheduled(cron = "0 0 8 * * ?") public void scanDueRemind() { LocalDate today = LocalDate.now(); LocalDate dueDate = today.plusDays(7); List<RemindTask> taskList = remindTaskMapper.selectList(new LambdaQueryWrapper<RemindTask>() .between(RemindTask::getTriggerDate, today, dueDate) .eq(RemindTask::getStatus, "PENDING")); for (RemindTask task : taskList) { // 发站内信、短信或邮件,这里以站内信为例 messageService.sendToUser(task.getUserId(), "宠物提醒", task.getTaskTitle() + "即将到期"); } }我这用的是 Spring 自带的 @Scheduled,单机部署完全够用。如果追求技术亮点,可以改成 ActiveMQ 或 RabbitMQ 做延迟消息,但这会引入消息中间件的运维成本。对毕设而言 @Scheduled 容易解释、容易演示,性价比最高。提醒的触发状态后续要更新为 DONE,同时记录提醒发送时间,这些都要在事务里处理,避免重复发送。
4. 文件存储、打包与远程部署
4.1 宠物照片和体检报告的 MinIO 接入
文件上传是宠物成长监管系统的刚需,宠物头像、成长照片、体检报告 PDF 都需要落地存储。我建议统一走 MinIO,而不是存本地目录。MinIO 的接入代码有几个关键步骤。
第一步,创建配置类,初始化 MinioClient:
@Configuration public class MinioConfig { @Value("${minio.endpoint}") private String endpoint; @Value("${minio.access-key}") private String accessKey; @Value("${minio.secret-key}") private String secretKey; @Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } }第二步,封装上传方法。这里要注意两个细节:bucket 不存在时自动创建,以及文件名用 UUID 重命名避免中文乱码和重名覆盖:
public String uploadFile(MultipartFile file, String folder) throws Exception { // 1. 确保bucket存在 boolean exists = minioClient.bucketExists(BucketExistsArgs.builder() .bucket(bucketName).build()); if (!exists) { minioClient.makeBucket(MakeBucketArgs.builder() .bucket(bucketName).build()); } // 2. 生成存储路径:folder/yyyyMMdd/uuid.png String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String objectName = folder + "/" + LocalDate.now() + "/" + UUID.randomUUID() + ext; // 3. 上传 minioClient.putObject(PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return endpoint + "/" + bucketName + "/" + objectName; }第 3 步的stream方法传的是file.getSize()作为对象长度,如果文件流不准确会导致上传失败。另外,从安全角度讲,要对文件类型做白名单校验,不能只根据扩展名判断,图片可以校验 content-type 前缀是不是 image/,PDF 校验是不是 application/pdf。做这些不是吹毛求疵,而是这类系统容易被上传恶意文件,答辩时提到这个点,老师会觉得你考虑问题全面。
4.2 Docker Compose 一键部署全流程
远程运行是这个项目交付时的一个重要环节。很多同学在本地跑得好好的,传到服务器就起不来,主要是因为环境不一致。用 Docker Compose 可以把 MySQL、Redis、MinIO 和应用本身一起编排,大大减少环境问题。
先需要给 SpringBoot 项目写一个 Dockerfile,最简单的版本是多阶段构建:
FROM maven:3.8.6-eclipse-temurin-8 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests FROM eclipse-temurin:8-jre WORKDIR /app COPY --from=builder /app/target/pet-system-0.0.1-SNAPSHOT.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]多阶段构建的好处是最终镜像里只有运行环境,没有 Maven 和源码,镜像体积能小一半以上。然后把所有依赖服务写进 docker-compose.yml:
version: '3.8' services: mysql: image: mysql:8.0 container_name: pet-mysql environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: pet_growth ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql redis: image: redis:7-alpine container_name: pet-redis ports: - "6379:6379" minio: image: minio/minio:latest container_name: pet-minio command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin ports: - "9000:9000" - "9001:9001" volumes: - minio-data:/data app: build: . container_name: pet-app depends_on: - mysql - redis - minio environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/pet_growth?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: root123456 SPRING_REDIS_HOST: redis MINIO_ENDPOINT: http://minio:9000 ports: - "8080:8080" volumes: - upload-data:/app/upload volumes: mysql-data: minio-data: upload-data:这里有一个关键点:容器内应用访问 MySQL,不能用 localhost,而要用服务名 mysql,因为每个容器有独立网络。如果你在远程部署时发现应用报“Communications link failure”,八成是数据库地址写成了 localhost。另一个容易忽略的是 init.sql 挂载:MySQL 容器首次启动时,会按文件名顺序执行 /docker-entrypoint-initdb.d 目录下的 SQL 脚本,实现自动建库建表和初始数据导入。这是一键部署的灵魂。
4.3 远程运行的调试清单
远程部署不是打包上传就完事,我整理了一份排查清单。第一,检查防火墙端口是否放行,尤其是云服务器安全组,8080、9000、9001、3306 这些端口要在控制台放行。第二,确认数据库初始化成功,进入容器查看表是否创建:docker exec -it pet-mysql mysql -uroot -p然后show tables;。第三,查看应用日志定位问题:docker logs -f pet-app --tail=200,日志里出现什么错误就解决什么。第四,确认 MinIO 的 endpoint 用外网地址,很多部署失败是因为代码里配了 http://localhost:9000,外部浏览器访问不到。
还有一个远程运行常见的坑是文件上传大小限制。Nginx 默认限制 1MB 请求体,如果你前端通过 Nginx 反向代理到 SpringBoot,上传稍大的照片就会被 413 拦截。需要在 Nginx 配置里加client_max_body_size 20m;,后端 spring.servlet.multipart 也同步设置,两层都要放开。
5. 常见问题排查与论文写作要点
5.1 启动失败与接口异常排查速查表
我整理了近年来做这类项目最常见的几个问题,每条都对应具体现象和解决方案,你遇到问题可以直接对照:
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 启动报 Port already in use | 端口被占用 | 换端口或用netstat -ano找到 PID 杀掉进程 |
| 启动报 Failed to configure DataSource | 数据源配置错误或 MySQL 未启动 | 检查 application.yml 的用户名密码、库名,确认 MySQL 服务正常 |
| Unknown database 'pet_growth' | 数据库没建 | 先用 CREATE DATABASE 建库,再导入 SQL 脚本 |
| 接口返回 404 | 访问路径不对或 context-path 导致前缀变化 | 检查 server.servlet.context-path 配置,访问时加上 /pet 前缀 |
| 上传文件报 Connection refused | MinIO 未启动或 endpoint 配置错误 | 确认 MinIO 容器运行,地址用外网 IP 或服务名 |
| No qualifying bean of type 'StringRedisTemplate' | Redis 依赖缺失或版本冲突 | 检查 pom.xml 是否引入 spring-boot-starter-data-redis,并清理本地仓库 |
| mapper 接口报 Invalid bound statement | XML 没被扫描 | 检查 mapper-locations 路径和 XML namespace 是否对应 |
| 页面中文乱码 | 连接串未指定编码 | URL 加 characterEncoding=utf8,并确认数据库字符集为 utf8mb4 |
第 1 条的实际解决方式我多说一句:开发环境 IDEA 里改端口,直接在运行配置的 VM options 里加-Dserver.port=8081,比改 yml 再重启更灵活,适合多实例联调。第 6 条很多人会碰到,往往是 Maven 本地仓库里缓存了过期的依赖,解决方案是执行mvn clean install -U强制更新快照。
5.2 接口认证与安全配置的落地方式
很多 SpringBoot 毕设项目用拦截器做简单的登录校验,而不是一上来就上 Spring Security + JWT。我推荐的做法是:登录接口放行,其余接口通过拦截器校验请求头中的 Token,Redis 存储登录态。
核心代码可以这样写:
@Component public class LoginInterceptor implements HandlerInterceptor { @Autowired private StringRedisTemplate redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token != null && Boolean.TRUE.equals(redisTemplate.hasKey("login:token:" + token))) { // 可以在这里把用户ID放入request Attribute,供后续使用 String userId = redisTemplate.opsForValue().get("login:token:" + token); request.setAttribute("userId", userId); return true; } response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"message\":\"未登录或登录已过期\"}"); return false; } }注册拦截器时要注意排除登录、注册、静态资源等路径:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/api/user/login", "/api/user/register", "/error"); } }这种方案的好处是简单直观,好解释,Token 存在 Redis 里可以控制过期时间,登出时直接删除 Key 即可。如果你觉得不够硬核,再提一句“接口签名认证”思路:客户端请求时携带 AppId + 时间戳 + 签名(参数按字典序拼接后做 HMAC-SHA256),服务器端校验签名和时间戳过期时间。这能在答辩时展示你对接口防重放的理解,实现也不复杂。
5.3 论文写作思路与 LW 文档整理技巧
项目标题里的“LW文档”,我理解为“论文文档”,这是毕设交付的重要组成部分。很多同学代码写完了,论文憋不出来。实际上论文结构和项目代码是强对应的,写论文就是把你做的东西说清楚,别编造。
推荐的结构是:第一章绪论,写背景和国内外现状,不要长篇大论,重点说清楚为什么要做宠物成长监管。第二章相关技术介绍,你可以说 SpringBoot、MyBatis-Plus、Redis、MinIO、Vue,介绍技术时不要写成手册,而要说明“为什么选它”。第三章需求分析,把 1.1 节里的角色、功能需求、非功能需求整理成用例图、表。第四章系统设计,贴总体架构图和功能模块图,画数据库 ER 图并解释表结构关系。第五章系统实现,按模块贴核心代码,每一段代码配一段解释,重点贴亮点代码,比如成长指标计算、Redis 缓存、定时提醒,不要贴几十行的一键生成代码。第六章系统测试,写测试用例表格,包括用例编号、输入、预期结果、实际结果,再贴几张运行截图。最后是总结和致谢。
写作时有几个实际操作心得:一是图务必自己画,架构图用 ProcessOn 或 draw.io,不要直接截取系统界面截图代替架构图;二是代码不要全部塞进去,每个小节 5 到 15 行核心代码足够,粘贴大段代码会被导师认为是凑字数;三是字数不够时多写测试用例表和数据库设计说明,这些内容有实际价值,不显空洞。
5.4 答辩前的自测与亮点提炼
答辩是这个项目的“最后一公里”。建议提前从三个角度准备。
第一,功能演示要顺。准备两三个核心流程,从登录到创建宠物、添加成长记录、查看曲线、触发预警、收到提醒,完整走一遍。演示之前把测试数据准备好,别现场输入,网络卡顿或输入慢都会影响观感。
第二,技术问答要熟。以下几个问题要练到能脱口而出:MyBatis-Plus 和 MyBatis 的区别、自动配置原理、Redis 缓存和数据库一致性怎么处理、登录 Token 过期机制如何实现、Docker 容器之间如何通信、数据库为什么这样设计。这些内容在前面都讲过,重点是不要答得支离破碎。
第三,有一个体现思考深度的“亮点故事”。比如你可以这样讲:最初我把体重预警逻辑写死在 Service 里,后来发现不同品种标准经常变,就把标准抽成了 breed_standard 表,再用 Redis 缓存,这样既方便管理员维护,又提升了接口性能。这样的小故事比堆砌术语更有说服力,老师会觉得你真正理解了项目。
我在实际调试这类项目时最深的体会是:代码写不出来不是问题,问题是业务流程没有想透。宠物成长监管系统从表面看是个课程设计题,里子其实是一套完整的数据采集、分析、提醒链路。你先把这个链路画清楚,再动手写代码,后面的路会顺畅得多。如果你正在做类似的 SpringBoot 项目,记住一句话:技术是手段,业务是灵魂。能把一次体重异常波动讲成一条从数据库到用户提醒的完整故事,这个项目就真正立住了。