做毕设选了“网上宠物管理系统”这个题目,还指定用Spring Boot的话,你多半是看中了它不算复杂、功能点容易凑齐、又不至于太没含金量。这类系统放在计算机毕业设计里确实很合适:电商、内容管理、权限控制、文件上传这些经典模块全都能覆盖,技术栈又是当前企业用的主流,答辩的时候不管老师问业务还是问框架,你都有东西可以讲。
这篇文章我按自己做毕设辅导时最常推荐的方案来拆:整体设计思路、技术选型、数据库核心表、功能模块的落地细节、代码实现里的关键点,再加上那些教材里不会写、但实际开发一定会踩的坑。整个流程走完,你拿到的是一套能写进论文、也能跑起来演示的完整方案。
1. 整体设计与技术选型
1.1 系统定位与角色划分
先说这个系统到底做什么。网上宠物管理系统的本质,是给宠物交易、领养和服务管理提供一个线上化平台。很多人一看到“管理”两个字,就以为只做后台CRUD,结果页面做完一版全是表格,答辩时老师一看就知道没用心设计。实际合理的做法是拆成两端看:
- 前台用户端:面向普通用户(买家和领养人),提供宠物展示、商品浏览、购物车、下单、领养申请、留言、个人中心、我的订单等功能。
- 后台管理端:面向平台管理员或店主,提供宠物档案管理、分类管理、订单处理、领养审核、用户管理、公告管理等。
这种拆分逻辑清晰,也是所有电商类系统的通用骨架。你简历写“完成前后台分离的宠物交易与管理平台”,比单纯写“完成了宠物信息增删改查”高一档。
权限控制方面,我建议用最直观的拦截器或AOP切面校验登录状态和角色类型,就够了。毕设级别不需要上Spring Security全家桶,除非你是为了学安全框架特意选的题,否则配置成本远大于收益。
1.2 技术栈建议:Spring Boot + Vue 前后端分离
既然题目限定Spring Boot,后端不用犹豫。前端有两种路线:第一种是用Thymeleaf直接做服务端渲染,好处是你只需要写一个工程,部署简单;坏处是现在主流开发方式已经是前后端分离,你用模板引擎写会显得技术栈偏旧,答辩时容易被问“为什么不用Vue”。
我的建议是直接采用Spring Boot + Vue的分离式架构。Vue只学基础(数据绑定、路由、axios调用)就够支撑毕业设计,哪怕你从来没接触过前端框架,按下面的结构自学习成本也就一周左右。如果确实时间紧张,可以考虑用Vue Element Admin这类现成后台模板改,把精力集中放在后端业务正确性上。
准确说,答辩时最常被问的Top 3问题之一就是“为什么选这个架构”,标准答法是:
采用前后端分离架构,前端通过Axios调用后端RESTful API,后端只负责业务逻辑与数据持久化,通过JSON交换数据,降低前后端耦合度,便于后续功能扩展与多端接入。
这句话涵盖了架构优势、通信方式、扩展性,技术含量拉满。
技术栈具体清单如下:
| 层级 | 技术选型 | 说明 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.x | 兼容JDK 1.8,稳定且资料最多 |
| ORM | MyBatis-Plus | 单表CRUD零SQL,复杂查询只写少量XML |
| 数据库 | MySQL 5.7/8.0 | 最经典组合,JDK版本选择多 |
| 权限方案 | 拦截器 + Token/Redis缓存登录态 | 简单可控,比Spring Security轻量 |
| 文件存储 | 本地磁盘 + Nginx映射 或 FastDFS | 毕设用本地磁盘就够 |
| 前端框架 | Vue 2.x + Element UI | 中文文档全,适配毕设开发 |
| 接口文档 | Swagger(springfox或springdoc) | 答辩演示接口方便 |
| 依赖管理 | Maven | 比Gradle通用性高 |
这个组合最大优势是遇到报错时百度一下基本都有解决方案,不会被冷门细节卡住。
2. 数据库设计:核心表与关键字段
2.1 整体表结构规划
数据库设计是整个系统的根基,我见过太多人一上来就写代码,结果表结构改来改去,代码返工次数多到最后心态崩掉。表设计的核心原则是:从业务对象出发,一个业务对象对应一张表,对象之间的关系(一对多、多对多)用外键字段或中间表来维护。
网上宠物管理系统需要的核心表我整理成下面这份清单:
- 用户表(用户ID、用户名、密码、昵称、手机号、邮箱、头像、角色类型、状态、创建时间)
- 宠物分类表(分类ID、分类名称、父分类ID、图标、排序、状态)
- 宠物信息表(宠物ID、分类ID、宠物名称、品种、性别、年龄、体重、颜色、疫苗状态、描述、封面图、图片集、状态【待售/已售/下架】、发布者ID、审核状态)
- 领养申请表(申请ID、宠物ID、用户ID、申请人姓名、手机、住址、申请原因、审核状态、申请时间)
- 订单表(订单ID、订单编号、用户ID、订单金额、收货人、联系电话、收货地址、订单状态、创建时间、支付状态)
- 购物车表(购物车ID、用户ID、宠物ID、数量、加入时间)
- 公告表(公告ID、标题、内容、发布时间、管理员ID)
- 留言/咨询表(留言ID、用户ID、宠物ID、内容、回复内容、留言时间)
其中订单表如果要卖宠物周边商品,还要有订单明细表,但纯宠物交易系统里一张宠物快照字段的订单表就足够了,特别注意:订单表不能只存宠物ID,要冗余宠物名称、图片、价格等快照信息。因为用户下单后,后台把宠物状态改成“已售”,如果你再去宠物表拿数据,得到的就是空记录或者价格变了,订单历史就会显示错乱。
2.2 用户表与角色权限设计
用户表的角色类型字段建议这样设计:
role_type tinyint(4) DEFAULT 0 COMMENT '角色:0-普通用户,1-管理员,2-店主/商家'用数字而不是字符串存角色,一是节省空间,二是代码里可以用枚举判断,非常清晰。不用做三张表(用户表、角色表、用户角色关联表),那是标准RBAC的做法,适合多角色多权限的复杂系统。毕设这个规模,一个字段标记角色类型反而更好维护,逻辑不会绕。
密码字段有个细节必须注意:要存加密后的密文而不是明文,使用BCrypt算法加密,这也是Spring Security内置的密码加密器,但你不用整个引入Security,单独引入spring-security-crypto依赖或者用Hutool的BCrypt.hashpw()都行。注册时加密存储,登录时对比密文。
2.3 宠物表的关键状态设计
宠物信息表是整个业务的核心,状态字段建议拆成两个独立字段来管理,而不是合成一个。
// 销售/交易状态 private Integer saleStatus; // 0-可购买,1-已预订,2-已售出 // 信息审核状态 private Integer auditStatus; // 0-待审核,1-审核通过,2-审核驳回很多新手会把状态设计成一个大字段:0-待审核、1-已上架、2-已下架、3-已售出、4-待领养……看似很全,实际上把“内容审核”和“商品交易状态”两个维度混在一起了。这样带来的直接问题是:你无法表达“一只审核通过但已经被买走”的宠物,信息是扭曲的。拆开后所有逻辑都顺了:
- 审核通过且销售状态下架,才是真的下架
- 用户只能看到
auditStatus=1而且saleStatus=0的宠物 - 后台管理员可以分别控制两个流程
图片字段的设计是另一个高频出问题的地方。宠物信息通常需要多图轮播,千万不要在表里设计 image1、image2、image3 这样的固定字段,这是新手最常犯的错。正确做法是主表只存封面图(cover字段),详情图放到单独表或者用逗号分隔的URL存到一个字段里。
cover varchar(255) DEFAULT NULL COMMENT '封面图URL', images text COMMENT '轮播图URL,多张用逗号分隔'用逗号分隔虽然在严格范式上不算最优,但实际开发中非常高效:查询时直接按逗号split为数组传给前端展示,不需要额外查询图片表。
3. 业务功能实现要点
3.1 注册登录模块的几个坑
注册登录是几乎每个毕设都有的模块,但越是基础越容易暴露问题。注册时,前端传过来的参数除了用户名和密码,往往还有确认密码、手机号、邮箱等,后端接收参数必须用DTO对象,加上@Valid校验注解:
public class RegisterDTO { @NotBlank(message = "用户名不能为空") @Size(min = 3, max = 20, message = "用户名长度必须在3-20之间") private String username; @NotBlank(message = "密码不能为空") @Size(min = 6, max = 20, message = "密码长度必须在6-20之间") private String password; }校验用户名是否重复是个典型的并发问题场景。只查一次再插入不好,更好的方式是给数据库用户表的用户名字段加上唯一索引,然后在插入时用try-catch捕获DuplicateKeyException,返回“用户名已存在”。这比先查后插的方式更严谨,在并发情况下也不会出问题。
登录成功后Session还是Token的选择上,我推荐使用Token。具体方案是登录成功用UUID生成一个token,把userId和roleType塞进Redis,key为login:token:{token},同时设置过期时间比如2小时;之后所有需要登录的接口都在请求头里带上token,后端用拦截器统一校验并从Redis取登录态。
拦截器里关于接口放行策略要特别小心。我见过有人把所有接口都拦住了,结果登录接口自己也进不去,反复调试半天才发现拦截路径配置有问题。建议放行规则这样配:
registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns( "/api/user/login", "/api/user/register", "/api/pet/list", "/api/pet/detail/**", "/api/announcement/list", "/error" );看到没有,被放行的是游客也能访问的接口(宠物列表、宠物详情、公告列表、登录注册),其余接口都必须登录。要校验管理员身份时,在拦截器里再判断Redis中存放的roleType是否为1。用两个拦截器分别处理“登录校验”和“管理员校验”,代码层级上更清晰。
3.2 宠物信息发布与图片上传
宠物发布功能涉及必填信息校验、图片上传、数据入库。这里最核心的是图片上传模块。毕设项目本地文件存储就够了,不需要接云OSS服务。
上传接口写法如下:
@PostMapping("/api/pet/publish") public Result publish(@RequestParam("file") MultipartFile[] files, @RequestParam("petForm") String petFormJson) { // 1. 解析JSON Pet pet = JSONUtil.toBean(petFormJson, Pet.class); // 2. 存储图片文件 List<String> imgUrls = new ArrayList<>(); File dir = new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } for (MultipartFile file : files) { String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); // 重命名,避免中文文件名和重复 String newName = UUID.randomUUID().toString().replace("-", "") + ext; file.transferTo(new File(dir, newName)); imgUrls.add("/uploads/" + newName); } // 3. 保存数据库记录 }这里必须强调的是文件重命名。短视频平台、电商系统全都会对上传文件重命名,你用原始文件名直接存,一旦用户上传了两个同名的图片,后面的会把前面的覆盖掉,或者文件名里有中文导致访问时URL编码问题报404。统一用UUID随机命名最稳妥。
另外,上传文件的类型和大小必须做限制。类型限制用后缀校验就行,毕设级别不需要深入检测图片内容的MIME;大小限制可以同时从前端和后端两个方向做,后端用Spring配置文件中的spring.servlet.multipart.max-file-size控制,同时也拦截一下超大文件,避免轻易被人灌爆磁盘。
3.3 宠物列表与条件查询
列表页是用户访问最频繁的页面,也是面试官最可能深挖的地方。你要实现的筛选条件通常是:
- 分类(按宠物类别过滤)
- 关键字(按名称或描述模糊匹配)
- 价格范围(最低价与最高价)
- 排序方式(按发布时间倒序、价格升序、价格降序)
用MyBatis-Plus,这种多条件的列表查询一开始会让人苦恼要不要写一堆if判断,但最清晰的做法是业务层用LambdaQueryWrapper构造:
LambdaQueryWrapper<Pet> wrapper = new LambdaQueryWrapper<>(); // 仅展示审核通过且未售出的宠物 wrapper.eq(Pet::getAuditStatus, 1) .eq(Pet::getSaleStatus, 0); if (StringUtils.hasText(keyword)) { wrapper.and(w -> w.like(Pet::getName, keyword) .or() .like(Pet::getDescription, keyword)); } if (categoryId != null) { wrapper.eq(Pet::getCategoryId, categoryId); } if (maxPrice != null) { wrapper.le(Pet::getPrice, maxPrice); } wrapper.orderByDesc(Pet::getCreateTime);分页功能我单独说一下:前端传current和size,用MyBatis-Plus分页插件返回IPage<Pet>就行。如果只是列表数据直接展示是没有问题的,但如果列表里要同时带出分类名称、发布者的昵称等关联信息,就需要自定义SQL联表查询了。
常见做法是先用Page<PetVO> page = new Page<>(current, size);再在Mapper里写一对一的XML去连表查询。这里有一个更省力的思路:数据量不大时,先分页查宠物主表,再根据返回的分类ID批量查分类名称,代码里封装循环补全信息。但这个方法在数据量大点之后会有N+1查询问题,所以放在Mapper里联表查询其实是更标准的选择。
3.4 购物车与订单流程
先讲购物车逻辑。购物车最简单的一张表存用户ID、宠物ID、数量、加车时间。值得注意的逻辑是:同一用户重复点击“加入购物车”时,应当检测该宠物是否已经存在,存在就更新数量而不是插入新记录。另外宠物这类标品,和普通商品不同,大多数是一次性商品(一只宠物只能被买一次),加进购物车时必须检查它的saleStatus还是不是0,已售出的宠物必须给用户提示不能再加车。
订单生成是整个系统中事务性最强的环节,核心代码如下:
@Transactional(rollbackFor = Exception.class) public Order createOrder(Long userId, Long petId, Address address) { // 1. 查询宠物,并做状态校验 Pet pet = petMapper.selectById(petId); if (pet == null || pet.getSaleStatus() != 0) { throw new BizException("该宠物已售出,无法下单"); } // 2. 生成订单号 String orderNo = "P" + System.currentTimeMillis() + RandomUtil.randomNumbers(4); // 3. 创建订单记录 Order order = new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setPetId(petId); order.setPetName(pet.getName()); order.setPetImage(pet.getCover()); order.setAmount(pet.getPrice()); ... orderMapper.insert(order); // 4. 把宠物状态改为“已预订”,防止别人重复下单 pet.setSaleStatus(1); petMapper.updateById(pet); return order; }@Transactional是必须的,因为订单创建和宠物状态更新要么一起成功、要么一起失败,不能出现订单建了但宠物没锁住,或者宠物状态改了但订单创建失败,导致买家找不到订单的情况。这也是答辩时最容易被问的事务问题,回答“保证数据一致性”就没毛病。
下单后订单状态机通常是:待支付 → 已支付/待发货 → 已发货 → 已完成,外加“已取消”。毕设如果要避免接入真实支付网关的麻烦,可以做一个模拟支付接口:点击“去支付”后端直接把这个订单状态改成“已支付”,同时把宠物saleStatus更新为2(已售出)。这样既展示了电商闭环逻辑,又不必申请支付接口。
3.5 领养流程实现与管理员的审核
如果我这个系统是“宠物销售 + 领养双轨制”,那么领养流程需要单独设计。领养和购买最大的不同是:购买是直接交易,而领养是用户提交申请,管理员审核后才生效。
开发时,领养申请表的关键字段包括宠物ID、申请人信息、申请原因等。管理后台管理员看到申请列表后,可以对每个申请选择“通过”或“驳回”。通过之后,宠物状态同步修改为“已领养”,同时该宠物不能再给其他用户申请。
领养申请还建议做一层防重复限制:同一个用户对同一只宠物只能有一条待审核的申请记录,如果已经有待审核记录就提示“请勿重复申请”。这在代码里无非是一个组合查询判断,逻辑不复杂但边界要想到。
3.6 管理员端的核心面板
管理员端的核心界面怎么设计呢?我建议以数据看板为中心:
- 今日新增用户数
- 宠物发布总数、待审核数、已售出数
- 最近订单列表
- 7天内的订单数量变化趋势(用ECharts画折线图)
其中待审核管理是管理员端最高频操作:列表展示用户新发布的宠物信息,管理员查看详细信息后点击通过或驳回。这块功能虽然简单,但反映了内容审核的业务思维,说明你考虑到了平台内容的安全性和合规性,写到论文里也是亮点。
4. MyBatis-Plus 与通用代码模板
4.1 自动填充时间和逻辑删除
Spring Boot + MyBatis-Plus是现在Java毕设事实标准,它的好处不用多讲。需要补充的是使用过程中的两个隐藏技巧:
第一个是公共字段自动填充。每张核心表的create_time和update_time如果都手动set很烦也容易漏。在实体类时间字段上标注:
@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;然后实现一个MetaObjectHandler:
@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }加完之后,所有表的这两个字段都会自动填充,一劳永逸。但有个前提,你实体类中字段名必须和数据库字段名能对应(驼峰转下划线),默认情况下是可以的。如果你数据库里字段命名不规范,比如某个字段就叫createTime不带下划线,就得单独指定映射关系。
第二个是逻辑删除。宠物删除功能不能真的把数据从库里delete掉,因为用户一旦删除宠物,历史订单里关联的宠物快照就可能查不到(虽然订单冗余了快照信息,但后台还是希望知道这只宠物曾经存在过)。更好的做法:在宠物表加deleted字段,0未删除,1已删除。实体类字段上标注:
@TableLogic private Integer deleted;之后MyBatis-Plus的所有查询都会自动拼接AND deleted = 0,删除操作也会变成UPDATE ... SET deleted = 1。
4.2 统一返回结果与全局异常处理
这部分在答辩时能让代码规范性明显加分。每写一个接口都直接返回“前端要什么就拼什么Map”,这种做法维护起来很累,很快就乱了。我建议统一定义结果类:
public class Result { private Integer code; // 200-成功, 500-失败 private String message; private Object data; public static Result success() {...} public static Result success(Object data) {...} public static Result error(String message) {...} }前端axios统一处理code === 200就取data,否则弹出message。这套返回结构定下来后,所有Controller的代码风格就整齐划一了。
再来是全局异常捕获:写一个@RestControllerAdvice类,把业务异常、参数校验异常、兜底异常分别处理。这样就不会出现用户看到的报错是你代码里某个晦涩难懂的Exception,而是经过包装的中文提示,日志里又能看到真实堆栈,调试时非常舒服。
4.3 自定义SQL的XML映射
用MyBatis-Plus做单表没问题,但一旦涉及多表关联查询,最贴合实际的方式仍然是写XML映射。我建议在resources/mapper目录下建XML文件,典型管理端订单查询的分页SQL就是:
<select id="selectOrderPage" resultType="com.example.pet.vo.OrderVO"> SELECT o.*, u.username, u.phone FROM orders o LEFT JOIN user u ON o.user_id = u.id WHERE 1=1 <if test="orderNo != null and orderNo != ''"> AND o.order_no LIKE CONCAT('%', #{orderNo}, '%') </if> <if test="status != null"> AND o.status = #{status} </if> ORDER BY o.create_time DESC </select>WHERE 1=1是一种很常见的处理方式,虽然有说不优雅的声音,但在动态SQL拼条件下这个方法很稳定可靠,不需要额外考虑AND/OR的位置问题。你用<where>标签也能达到相同效果,两种都可以,选一种熟练的。
5. 前端页面与接口对接
5.1 Vue工程结构与页面规划
前端工程结构建议使用Vue CLI搭建。如果你熟练使用网上现成的脚手架,也可以直接用一些相对正式一点的模板,页面我会按下面清单去规划:
用户端:
- 首页(宠物推荐、最新的公告、分类导航)
- 宠物列表页(搜索筛选分页)
- 宠物详情页(多图轮播、参数、立即购买/加入购物车/申请领养入口)
- 购物车页
- 订单页(含下单页)
- 个人中心(我的信息、我的发布、我的订单、我的领养申请)
- 登录与注册页
管理端:
- 数据看板
- 宠物管理(审核列表、已上架列表)
- 订单管理
- 用户管理
- 领养管理
- 公告管理
- 分类管理
5.2 Axios封装与Token携带
前端调接口时,如果一个页面一个地方写一个axios.get,写着写着就发现重复代码特别多。建议抽一个request.js做统一封装:
import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:统一带上token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }) // 响应拦截器:统一处理业务code request.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res.data } if (res.code === 401) { // 登录过期,跳回登录页 localStorage.removeItem('token') window.location.href = '/login' return Promise.reject(new Error('未登录或登录过期')) } return Promise.reject(new Error(res.message || '请求失败')) }, error => { return Promise.reject(error) } ) export default request响应拦截器做了统一处理之后,业务方调用只需关心成功数据而不用重复写判断,代码量直接减少三分之一。
6. 部署演示、常见报错与避坑清单
6.1 本地运行与演示环境搭建
答辩前一定要保证演示环境能稳定运行,不能等老师在场时才开始启动项目。我把整个流程梳理清楚:
- 本地启动MySQL,导入初始化SQL脚本
- 后端IDEA启动,确认配置文件里的数据库账号密码正确
- 前端npm install后npm run serve启动,因为接口跨域问题,建议在vue.config.js中配置代理(后端就不必额外开放CORS):
module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }注意别把前后端两个端口搞冲突。前端用8081,后端用8080是比较省心的组合。在这里将前端请求/api转发到后端地址,所有带/api开头的接口就都能正常请求,不会报跨域。
有条件的话建议放在一台云服务器上演示。后端打成jar包用nohup java -jar xxxxx.jar > log.out 2>&1 &启动,前端npm run build生成dist目录后用Nginx托管。把Nginx的location /api配置反向代理到后端8100端口,这是高可用而且最标准的做法。没有服务器的话,本地演示也行,但把访问地址也写进论文里,并提前截图一份再说。
6.2 高频报错与解决汇总
我遇到很多同学初学时被类似报错卡住的频率很高,这里做一个记录。
端口被占用
启动Spring Boot一瞬间Application运行失败,日志提示Port 8080 was already in use。解决办法要么是结束占用进程,要么在application.yml中修改端口换个8090、8081都行。Windows下查看端口占用的命令是netstat -aon|findstr 8080,然后taskkill /pid [进程号] /f。
数据库连接失败
这个报错原因是配置文件中spring.datasource.url里的IP或端口不对,或者是账户密码错误,三种情况按顺序排查。要特别注意时区参数,MySQL 8.x连接串需要加serverTimezone=Asia/Shanghai:
spring: datasource: url: jdbc:mysql://localhost:3306/pet_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 你的密码Invalid bound statement (not found)
Mapper里定义了接口但找不到XML或注解SQL。解决方法:先确认XML的namespace和Mapper接口全限定名一致,再确认mapper.xml文件在resources/mapper目录下,并且你在application.yml里加了MyBatis配置:
mybatis-plus: mapper-locations: classpath*:mapper/*.xml type-aliases-package: com.example.pet.entity前端请求404
打开浏览器地址栏输入接口地址测试,如果直接能访问说明后端没问题,问题出在前端代理或请求路径。对比一下前后端的路径拼接方式,比如后端接口是/api/order/list,前端baseURL如果已经包含/api,请求路径中就不要再拼一次/api了。
6.3 论文写作与答辩提点
论文结构一般按“选题背景→技术介绍→需求分析→系统设计→系统实现→系统测试→总结”展开。这一篇博文是分享项目经验,不是指导你具体写论文,但有一件事得提醒你:
系统测试不是跑一下演示就完事,答辩老师大概率会翻论文看有没有测试结论。你至少要准备10条以上的测试用例,覆盖用户注册、发布宠物、添加购物车、下单、订单状态流转、领养审核等核心流程。每条用例写明测试步骤、预期结果与实际结果,这是体现你认真完成毕设的最直观证据。
答辩中最常被问的几个问题你要想清楚答案:
- 为什么选Spring Boot?(生态成熟、自动配置简化开发、内置Tomcat一键部署)
- 登录怎么做的权限校验?(拦截器校验token,Redis存储登录态)
- 订单和购买流程中如何防止超卖?(下单时先查
saleStatus,再在事务里更新为“已预订”,事务保证原子性) - 密码怎么存的?为什么不能明文?(BCrypt加盐哈希,数据库泄露也无法逆推出明文)
- 项目中遇到过什么坑?(别说什么都没遇到,说一个上面这种端口占用或数据库连接报错然后怎么排查的,比说没遇到过更能获得认同)
7. 后续可扩展的优化方向
答辩结束不等于项目结束了,后续如果你想把这套系统写进简历用于找实习或就业,扩展空间其实很大。
第一个值得做的扩展是接入真实的在线支付。支付宝当面付或微信Native支付都有沙箱环境,一步步按文档接入支付回调后,订单状态变成“已支付”就不要再手动模拟了。这个扩展写进简历是个亮点,说明你了解支付流程与回调机制。
第二个是把本地文件存储换成对象存储。可以把前端上传的图片通过SDK传回云端并返回公网URL,这样一来就不依赖服务器磁盘且支持更大并发读写,这在简历中也能体现你对生产级架构的了解。
第三个是引入消息队列。例如用户下单成功之后,通过Spring Boot整合RabbitMQ发一条消息给库存模块,完成减库存与通知。这块在你简历上就是“异步解耦”的实际应用案例。
第四个更实用,给项目写Dockerfile,把后端、MySQL、前端Nginx都做成Docker容器。这一条正好契合最近的Docker部署Spring Boot趋势,熟练之后也可以直接写到简历“具备容器化部署能力”。
我对这类毕业设计重复次数很多后最大的感受是:这类项目的成败不在功能多少,而在逻辑闭环是否完整、代码结构是否清晰、答辩时“为什么这样做”能否自圆其说。宠物销售有发布→审核→浏览→下单→支付→状态变更,领养有申请→审核→结果,全程角色权限分工明确,系统各模块之间有清晰的业务边界。把一个闭环的每一步都做扎实,答辩展示时讲述的条理性会远远超过那些狂加模糊功能的项目。
就按照上面的结构去做,你拿到的不只是能过答辩的代码,还有一段真正能说清楚设计思路的完整开发经验。