做宠物猫认养系统这个毕设,前后大概写了两个月,中间推倒重来了一次。说实话,选这个题目的时候,很多同学的第一反应是“这不就是个增删改查吗”,真正做完之后我才意识到,它把Java Web毕设里该涉及的东西几乎全串起来了——SpringBoot做后端接口、Vue做前端页面、MySQL的SQL脚本、独立的接口文档,再到前后端联调和部署,每一步都是毕业设计答辩时老师会盯着的点。
这个系统要解决的业务问题其实很具体:管理员维护猫咪档案(品种、年龄、健康状况、照片),普通用户浏览猫咪列表、查看详情、提交认养申请,管理员对申请进行审核,审核通过后猫咪状态变成“已认养”,同时还有公告模块负责发布领养须知和平台通知。看起来是几个独立的增删改查页面,但认养申请这条状态链路一旦设计不清,后面全是坑。
接下来的内容我不会只贴一堆代码,而是按我当时做项目的顺序,把每个环节怎么思考、为什么这么选、踩了哪些坑都讲清楚。如果你正在准备Java Web方向的毕设,或者想找一个结构完整、能跑通全流程的项目参考,这篇内容可以帮你把整条链路理顺。
1. 选这个题目之前,我先把业务闭环想清楚了
1.1 表面是增删改查,实际是“状态驱动”的业务流
最开始拿到这个题目,我的第一反应是做一个猫咪信息列表,能上传照片,能删改,看起来就差不多了。后来跟指导老师聊了一次,他说了一句让我印象很深的话:“认养系统里最关键的不是猫咪表格,而是认养申请这条状态链路。”这句话基本决定了后面的设计方向。
认养系统的核心业务流是这样的:管理员或送养人发布猫咪信息 → 猫咪进入“待认养”状态 → 用户浏览猫咪列表并查看详情 → 用户提交认养申请 → 管理员审核申请 → 审核通过的猫咪标记为“已认养”。任何一个环节断了,系统都不完整,答辩的时候也很难讲出逻辑性。
所以我做的第一件事不是建工程,而是把角色和动作列出来。系统里有两类角色:管理员和普通用户。管理员做的事包括登录、猫咪信息的新增/修改/删除/上下架、认养申请的审核、公告发布;普通用户做的事包括注册登录、浏览猫咪、查看详情、提交认养申请、查看自己的申请进度。一个功能清单列完,后端接口有哪些、数据库表有哪些、前端页面有哪些,全部跟着出来了。
1.2 技术栈选择最终落在了“能答辩”和“能跑通”之间
技术选型我纠结过一阵子。参考了不少学长留下的代码和网上的毕设教程,最后定了这套组合:
| 环节 | 我用的方案 | 备选方案 | 为什么这么选 |
|---|---|---|---|
| 后端框架 | SpringBoot 2.7.x | SpringBoot 3.x | 3.x要求JDK 17起步,包名从javax改成jakarta,网上的老教程很多直接不兼容 |
| JDK版本 | JDK 8 | JDK 11/17 | 实验室环境、远程服务器兼容性最好,报错最少 |
| ORM | MyBatis-Plus | 原生MyBatis | 分页插件、LambdaQueryWrapper、自动填充时间戳,能省一大半繁琐代码 |
| 数据库 | MySQL 8.0 | MySQL 5.7 | 8.0是当前主流,注意连接驱动和时区参数 |
| 认证方案 | JWT + 拦截器 | Spring Security + Session | 毕设场景下JWT的代码量小、逻辑直观,答辩时好解释 |
| 前端框架 | Vue3 + Vite + Pinia + Element Plus | Vue2 + Vuex + ElementUI | Vue3是当前趋势,Vite启动快,Element Plus组件生态成熟 |
| 接口文档 | Apifox导出Markdown | Swagger/knife4j | 项目要求提交独立文档,Apifox可以直接生成离线版本 |
这里多说一句SpringBoot版本的事。网上很多教程直接给你贴SpringBoot 3.x的代码,如果你照抄,第一步就会遇到javax.servlet不存在这种报错。我用的SpringBoot 2.7.x搭配JDK 8,在毕设场景下是最稳妥的组合,几乎你搜到的任何资料都能直接用,不会卡在环境问题上。
前端我最终选了Vue3。有些同学担心自己学的是Vue2,其实核心的组件、生命周期、路由思路是通用的,Vue3反而让状态管理更简单(Pinia比Vuex直观很多)。选Element Plus是因为后台管理页面需要大量表格、表单、弹窗组件,现成组件能省下大量样式调试时间。
2. 数据库设计:SQL脚本不是一个表一个表堆出来的
2.1 先列业务实体,再落表结构
我设计表的时候不是直接打开Navicat建表,而是先把业务实体过了一遍:用户、猫咪、认养申请、公告、分类。这五张表基本覆盖了系统的全部核心功能,接下来就是梳理它们之间的关系。
一个用户可以有多个认养申请,每只猫咪在同一时间只能被一个申请最终认养,但可以收到多个申请。这构成了“用户一对多申请,申请多对一猫咪”的关系。公告和分类相对独立,都是单表。
我当时写接口文档和SQL脚本的时候,专门画了一张表关系说明放在文档开头,这份材料在最后的毕设答辩里非常加分,因为老师第一眼就能看出你对整个数据结构是心里有数的。
2.2 核心表DDL与关键字段设计
用户表是所有系统的地基。我设计的时候把登录密码加密存储,角色字段用tinyint,为了以后扩展更多角色:
CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `avatar` varchar(255) DEFAULT NULL COMMENT '头像URL', `role` tinyint NOT NULL DEFAULT 0 COMMENT '角色:0普通用户 1管理员', `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:1正常 0禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';猫咪表是核心业务表,里面很多字段都是给列表页和详情页直接用的。我设计了品种、年龄、性别、毛色、健康状态、疫苗情况、描述、图片地址,以及最重要的status状态字段,它要能表达猫咪在认养流程中的位置:
CREATE TABLE `cat` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '猫咪ID', `name` varchar(50) NOT NULL COMMENT '猫咪昵称', `category_id` bigint DEFAULT NULL COMMENT '分类ID', `breed` varchar(50) DEFAULT NULL COMMENT '品种', `age` int DEFAULT NULL COMMENT '年龄(月)', `gender` tinyint DEFAULT NULL COMMENT '性别:0公 1母', `color` varchar(50) DEFAULT NULL COMMENT '毛色', `health_status` varchar(100) DEFAULT NULL COMMENT '健康状况描述', `vaccinated` tinyint DEFAULT 0 COMMENT '是否已打疫苗:1是 0否', `description` text COMMENT '详细描述', `image` varchar(255) DEFAULT NULL COMMENT '主图URL', `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0待审核 1待认养 2已认养 3已下架', `owner_id` bigint DEFAULT NULL COMMENT '送养人ID', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '发布时间', PRIMARY KEY (`id`), KEY `idx_status` (`status`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='猫咪信息表';认养申请表是连接用户和猫咪的桥梁,也是整个系统最核心的状态流转表:
CREATE TABLE `adoption_application` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '申请ID', `user_id` bigint NOT NULL COMMENT '认养人ID', `cat_id` bigint NOT NULL COMMENT '猫咪ID', `reason` varchar(500) DEFAULT NULL COMMENT '认养理由', `contact_info` varchar(100) DEFAULT NULL COMMENT '联系方式', `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0待审核 1已通过 2已拒绝', `review_remark` varchar(255) DEFAULT NULL COMMENT '审核备注', `reviewer_id` bigint DEFAULT NULL COMMENT '审核人ID', `review_time` datetime DEFAULT NULL COMMENT '审核时间', `apply_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '申请时间', PRIMARY KEY (`id`), KEY `idx_cat` (`cat_id`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='认养申请表';三个设计细节我想重点说一下。
第一个是status字段的状态值设计。猫咪的状态我分了0待审核、1待认养、2已认养、3已下架四种。审核通过一个认养申请时,需要同时把猫咪的status改成已认养,才能保证“一只猫只能被认养一次”这个业务规则成立。这个逻辑如果不在代码里控制,后面会出现一个猫咪被两个人认养的数据脏问题。
第二个是外键的处理。我刻意没有在数据库层加物理外建,而是在Java代码里用id做关联查询。原因很实际:毕设项目经常要插入测试数据或做页面调试,物理外键会带来一大堆约束麻烦;同时MyBatis-Plus本身支持逻辑关联,不需要数据库帮你维护一致性。
第三个是时间字段。我统一用datetime,不用timestamp。timestamp有时区陷阱,尤其在部署到云服务器后发现时间差了8个小时,排查时间比想象中长得多。datetime没有时区问题,省心。
2.3 初始化脚本里的学问
很多同学的SQL脚本只有建表语句,这没问题,但如果能把初始化和测试数据一起给出来,别人拿到项目打开就能跑,体验会完全不一样。我把脚本拆成了两份:01_schema.sql和02_data.sql,前者建库建表,后者插入基础数据。
基础数据至少包含两部分:一个初始管理员账号(用户名admin,密码用BCrypt加密后的字符串),以及若干条猫咪测试数据,覆盖不同的品种、状态。这里有个很实用的技巧:为了演示方便,我故意造了两只不同状态的猫咪——一只“待认养”,一只“已认养”,这样打开前端就能看到状态标签的对比效果。
另外字符集一定要用utf8mb4,不要省事用utf8。原因在于猫咪描述文本里可能包含emoji表情,utf8存不进去,插入记录时直接报错,我第一次就是没注意这个,跑到一半才发现数据库根本写不进带表情的描述。
3. 接口文档与后端实现:先定义返回格式,再做业务代码
3.1 接口文档的组织方式:从Apifox到离线Markdown
项目标题里带“接口文档”,意味着交付物里这是单独一份。我刚开始用Swagger自动生成,但发现Swagger的文档对前端联调并不友好,尤其是请求参数示例不够直观。后来换成Apifox,调试接口的同时就能维护文档,最后直接导出成离线Markdown文件放进交付目录。
接口文档里我固定写清楚五件事:请求地址、请求方式、请求参数说明(名称/类型/是否必填/说明)、请求示例、响应示例。举个例子:
| 接口 | 方法 | 路径 | 说明 |
|---|---|---|---|
| 用户注册 | POST | /api/auth/register | 提交用户名、密码、昵称、手机号 |
| 用户登录 | POST | /api/auth/login | 返回JWT Token和用户信息 |
| 分页查询猫咪 | GET | /api/cats/page | 支持品种、状态筛选,返回分页结构 |
| 猫咪详情 | GET | /api/cats/{id} | 返回猫咪完整信息 |
| 提交认养申请 | POST | /api/adoptions/apply | 用户提交认养理由和联系方式 |
| 我的申请列表 | GET | /api/adoptions/my | 返回当前用户的申请记录 |
| 审核认养申请 | PUT | /api/admin/adoptions/{id}/review | 管理员通过或拒绝申请 |
| 发布公告 | POST | /api/admin/announcements | 管理员新增公告 |
| 文件上传 | POST | /api/file/upload | 返回图片访问URL |
接口文档先于后端代码写完善,有一个实际好处:前端页面需要的字段、后端需要提供的字段,在一开始就能对齐,后面联调阶段几乎不会出现“前端拿不到某个字段”然后临时改接口的情况。
3.2 统一响应结构与JWT认证的实现
后端设计的第一个公共组件是统一响应结构Result。所有接口都返回固定格式:code、msg、data。这个设计看起来简单,但实际开发中非常关键。前端axios拦截器只需要判断一次code,不用每个接口各自处理异常分支。
@Data public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMsg("success"); r.setData(data); return r; } public static <T> Result<T> error(Integer code, String msg) { Result<T> r = new Result<>(); r.setCode(code); r.setMsg(msg); return r; } }认证方案我没有上Spring Security,而是选择了JWT加自定义拦截器。原因很直接:Spring Security的过滤器链对初学者来说是黑盒,出了问题不好排查,答辩时也很难三言两语讲清楚。JWT的思路是所有访问受保护接口的请求都带一个Authorization: Bearer token的请求头,后端拦截器解析token,校验通过后放行,同时把用户id存入当前请求上下文。
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equals(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { Long userId = JwtUtil.parseToken(token.replace("Bearer ", "")); if (userId != null) { UserContext.set(userId); return true; } } response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}"); return false; } }注册登录时密码用BCrypt加密存储,不要存明文。这是答辩时老师大概率会问的“安全措施”问题之一,提前在代码里做了,回答起来就有底气。
3.3 核心接口的实现细节:分页、状态流转与文件上传
猫咪列表的分页查询用MyBatis-Plus的分页插件。这里要手动配置一个MybatisPlusInterceptor,把PaginationInnerInterceptor加进去,否则selectPage不会生效:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }查询接口支持按品种和状态筛选:
@GetMapping("/page") public Result<IPage<Cat>> page(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String breed, @RequestParam(required = false) Integer status) { LambdaQueryWrapper<Cat> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(breed), Cat::getBreed, breed) .eq(status != null, Cat::getStatus, status) .orderByDesc(Cat::getCreateTime); return Result.success(catMapper.selectPage(new Page<>(pageNum, pageSize), wrapper)); }认养申请的状态流转是核心中的核心。提交申请时,需要同时判断猫咪当前状态必须是“待认养”,否则直接提示“该猫咪已被认养或不可申请”。审核通过时,除了更新申请状态为已通过,还要把猫咪状态更新为已认养,并且建议把其他未审核的申请自动置为已拒绝,避免后续用户拿一只已经被认养的猫继续申请。
@Transactional @Override public void review(Long applicationId, Integer status, String remark) { AdoptionApplication app = applicationMapper.selectById(applicationId); if (app == null || app.getStatus() != 0) { throw new BusinessException("申请不存在或已处理"); } app.setStatus(status); app.setReviewRemark(remark); app.setReviewTime(new Date()); applicationMapper.updateById(app); if (status == 1) { Cat cat = catMapper.selectById(app.getCatId()); cat.setStatus(2); catMapper.updateById(cat); // 拒绝该猫咪的其他待审核申请 applicationMapper.rejectOtherPending(app.getCatId(), app.getId()); } }关于文件上传,我建议图片保存到服务器本地指定目录,然后返回一个可通过nginx或SpringBoot静态资源映射访问的URL。注意保存路径要用配置项管理,别写死在代码里的绝对路径,不然换一台电脑路径就不对了。
3.4 LocalDateTime序列化这个坑一定要提前处理
我第一次写完接口,前端拿到的时间字段长这样:"createTime": [2025, 3, 15, 12, 30, 45]。原因是Jackson默认把LocalDateTime序列化成数组了,前端拿到手根本没法直接用。解决办法有两种:要么在字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss"),要么在配置文件里全局处理:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8全局配置只对java.util.Date生效,对LocalDateTime还是不行,所以我最终在统一配置类里注册了JavaTimeModule的序列化器,一次性解决所有LocalDateTime的格式问题。这个坑非常典型,很多毕设项目都是在联调阶段突然卡住的。
4. Vue3前端的搭建:路由、请求封装与页面落地
4.1 初始化前端项目与目录划分
前端我用npm create vite@latest创建了Vue3项目,选择JavaScript而不是TypeScript。毕设场景下JS更省事,不需要处理类型定义,写起来快很多。目录结构按业务模块划分:
src/ ├── api/ // 每个模块的接口调用文件 ├── router/ // 路由配置 ├── stores/ // Pinia状态管理(用户信息) ├── views/ // 页面组件 │ ├── Home.vue // 首页 │ ├── CatList.vue // 猫咪列表 │ ├── CatDetail.vue // 猫咪详情 │ ├── Login.vue │ └── admin/ // 后台管理页面 ├── components/ // 公共组件 └── utils/ // 请求封装、工具函数目录划分这件事比大多数人想象的重要。答辩时老师可能会直接问你“项目结构怎么组织的”,你把目录说清楚,比背一段八股文更有说服力。
4.2 路由配置与登录守卫
路由必须区分游客和管理员。我在路由的meta里标记是否需要登录和需要的角色:
const routes = [ { path: '/', component: Home }, { path: '/cats', component: CatList }, { path: '/cats/:id', component: CatDetail }, { path: '/login', component: Login }, { path: '/admin', component: AdminLayout, meta: { requiresAuth: true, role: 'admin' } } ]再配合全局前置守卫做登录控制:
router.beforeEach((to, from, next) => { const userStore = useUserStore() if (to.meta.requiresAuth && !userStore.isLoggedIn) { next('/login') } else if (to.meta.role && to.meta.role !== userStore.role) { next('/') } else { next() } })这里有个设计细节:管理员的路由地址我用/admin前缀统一管理,和用户端完全分开,入口在首页右上角只有管理员登录后才显示。这样前端结构清晰,后端也方便用拦截器对/api/admin/**做权限校验,前后端的“角色分层”是一致的。
4.3 Axios请求封装与Token处理
axios封装是前端联调的关键。我统一创建了一个axios实例,配置baseURL为/api,然后在请求拦截器里自动带上token,响应拦截器里统一处理code非200的情况:
import axios from 'axios' const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use(res => { const result = res.data if (result.code === 200) { return result } if (result.code === 401) { localStorage.removeItem('token') window.location.href = '/login' return Promise.reject(new Error('未登录')) } ElMessage.error(result.msg || '请求失败') return Promise.reject(new Error(result.msg)) }, err => { ElMessage.error('网络异常') return Promise.reject(err) })统一封装之后,页面里调接口只需要维护src/api/cat.js这样的文件,每个方法对应一个后端接口:
export function getCatPage(params) { return service.get('/cats/page', { params }) } export function getCatDetail(id) { return service.get(`/cats/${id}`) } export function applyAdoption(data) { return service.post('/adoptions/apply', data) }4.4 核心页面的实现要点
猫咪列表页用的是Element Plus的el-card栅格布局,每个卡片展示猫咪主图、昵称、品种、状态标签。状态标签是列表页的视觉重点,我根据后端返回的status数字动态映射成不同的标签颜色:0待审核灰色、1待认养绿色、2已认养橙色、3已下架红色。这个映射在前端做一个纯函数就行,但能明显提升页面质量。
猫咪详情页要展示完整信息,下面的认养申请表单是重点。提交之前前端先判断用户是否登录,未登录就跳转登录页。表单字段包括认养理由和联系方式,提交成功后提示“申请已提交,等待管理员审核”,并按钮置灰防止重复提交。
后台管理页面我用了el-table加el-dialog的组合。猫咪管理表格支持分页、搜索、编辑、删除,编辑弹窗里用el-upload上传图片。认养审核页面是表格展示所有申请,点击审核弹窗选择通过或拒绝,通过时后端会联动更新猫咪状态。
这里有个值得留意的体验细节:管理后台和用户端的登录状态要共用同一个Pinia store和同一个localStorage token。我一开始没注意,后台接口用的独立axios实例,结果管理员登录后后台页面还是提示未登录,排查了半天发现是两个实例没有统一携带token。后来统一用一个请求封装,问题立刻消失。
4.5 dev代理配置:开发环境跨域的正解
开发时前端跑在5173端口,后端跑在8080端口,直接请求会有跨域问题。前端这边我通过Vite的proxy配置解决:
// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })这样前端代码里写的都是相对路径/api/xxx,浏览器请求同源,Vite开发服务器把它转发到后端。再加上后端配置CORS允许来源,两条路都通了。生产环境部署时,前端请求也会经过nginx反向代理到后端接口,整个链路的理解是一致的。
5. 联调、打包与部署:把项目真正跑起来的完整记录
5.1 后端CORS配置与开发环境联调
虽然Vite已经在开发层解决了跨域,但我在后端还是加上了CORS全局配置。原因很简单:万一有人不用Vite的proxy,直接用IP加端口访问后端,跨域问题是必然的。后端配置文件里的方式是最稳妥的:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns不要写allowedOrigins("*")和allowCredentials(true)同时用,后者会被浏览器拦截。这个问题我当时查了不少时间,最后发现是旧写法在新版本浏览器里不兼容。
5.2 生产部署的两种常见姿势
毕设项目的部署方式,我总结了两种主流做法,我都试过,各有适用场景。
第一种是前后端分离部署:SpringBoot打包成jar用java -jar运行,Vue打包成dist目录交给nginx托管,nginx再配一个/api反向代理到后端端口。这种方式最接近真实项目结构,面试聊起来也是加分项。
第二种是把前端打包产物直接放进SpringBoot的src/main/resources/static目录下,整个项目打成一个jar,双击就能跑。这种方式特别适合演示日那种“打开电脑就要跑起来”的场景,不用单独装nginx。但要注意,如果前端路由用了history模式,直接访问刷新会404,需要在后端加一个转发规则。
我在application.yml里配置了静态资源映射,把上传的图片目录也暴露出来:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MB resources: static-locations: classpath:/static/,file:${upload.path}打包的时候注意一件事:前后端要分开构建,别在前端dist生成之前就迫不及待地打成jar。我踩过一次,打出来的jar里static目录是空的,前端页面全404。正确的顺序是:先npm run build生成dist,把dist内容拷贝到后端static目录,再Maven打包。
5.3 我踩过的坑与排查思路汇总
整个项目做完,我把遇到频率最高的几个问题整理成了一张表,这些问题在别人跑我的源码时大概率也会碰到:
| 报错或现象 | 根本原因 | 解决办法 |
|---|---|---|
| 连接MySQL报CommunicationsException | 没有配置时区参数 | JDBC URL加上serverTimezone=Asia/Shanghai |
| LocalDateTime变成数组 | Jackson序列化LocalDateTime默认行为 | 注册JavaTimeModule并指定日期格式 |
| 前端请求全部401 | 拦截器放行了OPTIONS但前端没带token | 检查请求头名称是否一致,统一Bearer前缀 |
| 分页不生效,返回全量数据 | MyBatis-Plus分页插件没注册 | 配置MybatisPlusInterceptor,添加PaginationInnerInterceptor |
| 图片上传成功但访问404 | 静态资源映射没配置上传目录 | spring.resources.static-locations加上file路径 |
| 前端刷新页面404 | Vue history模式需要服务端支持 | nginx配置try_files或后端兜底转发到index.html |
| 同一个猫咪被两个申请同时通过 | 审核接口没做状态判断和并发控制 | 审核前判断cat.status,事务内更新状态,用@Transactional |
排查思路比答案更重要。第一次遇到404,别急着去改代码,先分清楚是“接口404”还是“页面404”。然后拿Postman直接打后端接口,后端通了再怀疑前端;前端报错先看控制台,再看网络请求的响应体。逐层缩小范围,比瞎改要快很多。
5.4 答辩前最容易被问到的几个技术问题
项目做完之后,我专门准备了几道高频答辩问题,我发现老师对毕设项目的提问基本集中在“为什么这么选”和“这里怎么实现”两个方向。
为什么用JWT不用Session?我的回答思路是:JWT无状态,后端不需要存储会话信息,适合前后端分离架构;登录成功后前端持有token,每次请求带上,后端通过签名校验身份。对比Session需要依赖服务端内存和Cookie传输,在API接口场景下JWT更自然。
认养状态的一致性怎么保证?我的回答思路是:提交申请时检查cat.status;审核通过时在同一个事务里更新申请状态和猫咪状态;其他待审核的申请自动拒绝。数据库层面再通过索引保证同一猫咪的申请可查询,避免并发重复通过。
如果用户量大了怎么办?这个问题很常见,不一定真的要实现,但要能说出思路。我的回答是:当前用的MyBatis-Plus分页是物理分页,后续可以把热门猫咪缓存到Redis,把文件存储迁移到对象存储,接口层面加统一限流。能说出这些词,至少说明你想过扩展方向。
最后再说几句大实话
项目全部整理完后,我把整个交付目录按照毕设要求重新整理了一遍:sql脚本、接口文档、后端完整源码、前端完整源码、README说明文件。README里写清楚了环境版本、启动步骤、初始账号密码,我的体会是:一个毕设项目交付质量的判断标准,往往不是代码量多豪华,而是另一个人拿到你的项目之后,能不能在半小时内跑起来。能做到这一点,你的项目就是合格的。
如果你现在正卡在某个环节,建议先从数据库和接口文档抓起,这两个东西定了,前后端就只剩下填代码了。祝顺利。