做了一个学期的毕设辅导,我手里过的学生项目没有一百也有八十,发现一个很有意思的现象:SpringBoot + Vue 的题目年年都是热门的王炸组合,而宠物商城又是这批选题里最有“眼缘”的一类。原因很简单,这个组合的技术栈主流、业务链路完整,而且视觉效果好——宠物图片本身就有天然的吸引力,页面一撑起来就很好看,答辩演示的时候也容易出效果。如果你正打算拿“SpringBoot + Vue 宠物商城网站管理平台”来做毕设、课设,或者只是单纯想学一下前后端分离项目的完整开发流程,这篇博文应该能帮你省下大量瞎折腾的时间。
我会从项目的整体设计思路说起,再拆到数据库表结构、后端接口、前端页面,最后把联调和部署阶段最容易被绊倒的几个坑也一并交代清楚。内容尽量按“为什么这么做”来讲,而不只是告诉你“这么做”,毕竟答辩的时候老师最爱问的就是这几个为什么。
1. 项目整体定位与模块划分
1.1 为什么宠物商城适合做毕设/课设
先说个很多人没有意识到的点:毕设选题最忌讳的不是“太简单”,而是“业务闭环不完整”。如果你只做一个图书列表增删改查,技术再好也很难撑起一篇论文的叙事;但如果去做电商那种超大平台,一个月的时间光是在订单状态机里转圈就能把你转晕。宠物商城正好卡在中间——它有商品展示、购物车、订单、支付(可选)、后台管理这些电商核心链路,但没有复杂的秒杀、高并发部署、分布式事务这类硬骨头。
这个项目对学生的技术覆盖也非常全:前台消费者能看到商品展示、分类浏览、搜索、加入购物车、下单;后台管理员要维护商品上架下架、库存修改、订单处理、会员管理。你把这些走一遍,等于把前端交互、后端API、数据库设计、状态管理全都串起来了。而且宠物商品的品类天然丰富,猫、狗、水族、爬宠、用品、口粮,随便一拆就是五六个分类,页面层级和筛选逻辑有了真实的复杂度,而不是人为编造的需求。
1.2 双端功能模块拆分
整个项目按照“前台用户端 + 后台管理端”两条线来拆,用户端服务于普通消费者,管理端服务于管理员。
用户端的核心模块我建议做成这几块:
- 注册登录:邮箱/用户名 + 密码,密码用MD5加盐或者BCrypt加密,别明文存。
- 宠物商品浏览:按分类展示商品卡片、搜索关键字、商品详情页看图片与库存。
- 购物车管理:加入、修改数量、删除、批量结算。
- 订单管理:确认订单、选择收货地址、生成订单、查看订单状态(待付款、待发货、待收货、已完成)。
- 个人中心:个人信息修改、我的订单列表。
管理端的核心模块:
- 管理员登录:与用户表共用一张表,用role字段区分,或者单独建一张admin表。
- 商品管理:添加宠物商品、编辑信息、上传图片、上下架、库存调整。
- 订单管理:按状态筛选订单、发货操作、查看订单详情。
- 分类管理:维护宠物品类,分类树不用做得太深,两级就够。
- 轮播图管理:首页轮播图的后台配置。
- 数据概览:简单统计商品数量、订单数量、用户数量,给首页仪表盘用。
这个功能量对课设来说是标准配置,对毕设来说也完全够用。如果你想让论文更有深度,可以挑其中一个模块往细做,比如把订单模块拆出“购物车合并下单 + 库存锁定 + 订单超时取消”的完整事务流程,这就很有东西可写了。
1.3 技术栈选择的底层逻辑
SpringBoot + Vue 这个组合之所以成为主流,有一个常被忽视的原因:它对不同层次的学习者都很友好。
SpringBoot 帮你把 Spring 那套复杂的 XML 配置简化掉了,你只需要遵循“约定优于配置”,写一个启动类,配合注解就能把 Controller、Service、Mapper 串起来。Java 学生学了两年 JavaSE 和数据库基础之后,上手 SpringBoot 是最平滑的路径,而且国内企业招聘对 Java 后端的岗位需求依然很大,学这个不亏。
Vue 则是目前国内最容易找到资料、上手门槛最低的前端框架。Vue 的响应式数据绑定、组件化开发、Vue Router 路由管理、Vuex(或 Pinia)状态管理,这些概念的学习曲线比 React 要缓和很多。学生只要花一周左右把视频过一遍,就可以开始写项目了。
MySQL 不用多说,免费、通用、资料多。毕设阶段没有分布式数据需求,单机数据库完全够用。唯一要注意的就是安装版本和连接驱动的兼容性问题,这个我在后面章节会详细说。
很多人纠结要不要用 Redis、要不要用 ElasticSearch、要不要用微服务。我建议不要,除非你的论文有专门的章节去论证这些技术的引入。否则只会增加系统复杂度和答辩时被追问的风险。把单体架构做扎实,把业务逻辑写清楚,比堆技术名词要可靠得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与表结构要点
2.1 核心业务表梳理
数据库设计是整篇论文的地基,答辩时如果老师问“为什么这样设计表结构”,答不上来就会很尴尬。宠物商城的核心表,我建议控制在六到八张左右,不要贪多。
- user表:用户表,存用户ID、用户名、密码、昵称、手机号、头像、角色(0普通用户、1管理员)、创建时间。
- pet_category表:宠物分类表,存分类ID、分类名称、分类图片、排序号。
- pet_info表:宠物商品表,存商品ID、分类ID、商品名称、商品描述、图片URL、价格、库存、销量、上架状态、创建时间。
- cart表:购物车表,存购物车ID、用户ID、商品ID、商品数量、加入时间。
- orders表:订单表,存订单ID、订单编号、用户ID、商品总价、收货人姓名、电话、地址、订单状态、下单时间。
- order_item表:订单明细表,存明细ID、订单ID、商品ID、商品名称快照、单价、数量。这里一个订单对应多个商品明细,所以要拆出来。
另外还可以加一张 comment 表做宠物商品的评价功能,加一张 address 表做收货地址管理。这两张表不是必备的,但如果你论文想要多一个数据维度,它们都很容易做。
2.2 关键字段设计与常见反模式
先讲几个我在带学生时反复强调的字段设计要点。
价格字段一定要用 decimal(10,2),不要用 float 和 double。这不是洁癖问题,是钱不能出精度误差。float 在累加和比较的时候会产生不可预料的偏差点,这在电商项目里面是不能接受的。学生做毕设时间紧,你可能觉得用什么都无所谓,但是一旦订单总金额对不上,排查起来会非常痛苦。
图片字段永远不要存 base64 编码。我见过有学生把图片转成 base64 字符串直接塞进 MySQL 的 text 字段里,几兆的图片能撑爆数据库连接和传输带宽,页面加载慢到怀疑人生。正确做法是把图片文件传到本地磁盘或者云存储,数据库只存文件的相对路径,页面直接用 URL 去访问。
订单状态用 tinyint 存数字状态码(0待付款、1待发货、2待收货、3已完成、4已取消),在前后端分别用枚举或常量映射成中文显示。不要直接在数据库里存“待付款”这种汉字字符串,后续状态统计和筛选查询会非常别扭。
角色字段建议直接放到 user 表里面,用 is_admin 或 role 标记。很多学生习惯单开一张 admin 表,把普通用户和管理员完全拆开。这个设计在小项目中会让代码多一层判断,但也不是不行。我的个人建议是:共用一个用户表,用 role 区分,这样以后想扩展“店长”“运营”等角色也方便。
2.3 几段核心建表SQL分析
这里我摘两段经常被学生模仿的建表语句来讲,重点不在这段 SQL 本身,而在于它里面积累了几次改表之后的修改痕迹。
sql复制CREATE TABLE `pet_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`category_id` int(11) DEFAULT NULL COMMENT '分类ID',
`name` varchar(100) NOT NULL COMMENT '商品名称',
`subtitle` varchar(200) DEFAULT NULL COMMENT '商品副标题/卖点',
`main_image` varchar(255) DEFAULT NULL COMMENT '主图路径',
`sub_images` text COMMENT '轮播图路径,逗号分隔',
`detail` text COMMENT '商品详情',
`price` decimal(10,2) NOT NULL COMMENT '价格',
`stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存',
`sales` int(11) NOT NULL DEFAULT '0' COMMENT '销量',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1上架 0下架',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_category_id` (`category_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='宠物商品表';
说一下设计细节:主图 main_image 单独存,轮播图 sub_images 用逗号分隔的路径串存,这算是一个在小项目里很务实的方案,避免了单独建一张轮播图表。detail 用 text 类型,存富文本编辑的 HTML 内容,前后台直接展示。
sql复制CREATE TABLE `orders` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_no` varchar(32) NOT NULL COMMENT '业务订单号',
`user_id` int(11) NOT NULL COMMENT '下单用户',
`total_amount` decimal(10,2) NOT NULL COMMENT '订单总价',
`receiver_name` varchar(50) NOT NULL COMMENT '收货人',
`receiver_phone` varchar(20) NOT NULL COMMENT '手机号',
`receiver_address` varchar(255) NOT NULL COMMENT '收货地址',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0待付款 1待发货 2待收货 3已完成 4已取消',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
order_no 用业务订单号而不用自增主键,这是为了让前端和历史订单有一个可读性更好的流水号。具体生成规则可以用时间戳+随机数,也可以按日期序列拼接。自增主键 id 保留为内部关联使用,对外暴露 order_no。这算是一个小的设计意识,很多学生做毕设时会忽略。
另一个实践要点:pet_info 和 order_item 之间,我建议在外键上不要加物理外键约束。因为订单明细中的商品名称、单价要做历史快照——商品下架、改名、改价之后,历史订单依然要保持可追溯。如果强行关联去更新,反而会破坏订单的客观性。这就是典型的“字段快照”思路,答辩时如果老师问到,你可以理直气壮地说这是为了订单数据的稳定性。
3. SpringBoot后端核心实现解析
3.1 统一返回体与异常处理
后端接口不建议裸返回一个 Map 或者直接返回实体对象,这在前后端分离项目中会让前端非常烦躁。最稳妥的方式是定义一个泛型返回体,把所有接口统一包一层。
java复制public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
return result;
}
public static <T> Result<T> error(String message) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMessage(message);
return result;
}
}
然后在 Controller 的返回类型里统一写 Result<你想要的类型>,前端就可以只认 code,200 做正常渲染,500 弹错误提示,不用每个接口配一种返回格式。
除了 Controller 返回格式统一,还要处理异常。SpringBoot 提供了 @RestControllerAdvice 全局异常拦截机制,你可以在里面拦截自定义业务异常和通用Exception,统一返回 Result.error。这样就算代码里忘了 try-catch,用户也只会看到“服务器开小差了”这种友好提示,而不是一坨500报错的丑陋页面。
我在实际开发中还会在异常处理器里打印完整的堆栈日志。排错这事,日志就是后端的第一证物。
3.2 JWT认证与拦截器实现
管理端接口不能裸奔,用户登录后才能下单、查看个人信息,这就需要一个认证方案。传统的 Session 方案在前后端分离部署时有点麻烦——它依赖浏览器 Cookie,部署跨域时会遇到 CORS 凭证问题。现在最主流的方式是 JWT。
JWT 的流程不复杂:用户登录成功后后端验证用户名密码,签发一个带用户ID、用户名、过期时间的 Token 返回给前端;前端保存 Token,在后续请求的请求头里带上 Authorization: Bearer
核心拦截器代码大致是这样:
java复制public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 放行预检请求
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
return handleError(response, "未登录或登录已过期");
}
try {
Claims claims = JwtUtil.parseToken(token.replace("Bearer ", ""));
request.setAttribute("userId", claims.get("userId"));
request.setAttribute("userRole", claims.get("role"));
return true;
} catch (Exception e) {
return handleError(response, "Token无效或已过期");
}
}
}
拦截器注册时注意放行规则:/api/user/login、/api/user/register、/api/pet/list、/api/pet/detail 这些公开接口要放行;/api/admin/、/api/order/、/api/cart/** 这些要拦截。管理端还要校验角色是不是管理员,可以在拦截器里结合 role 判断。
有一个经常被忽略的小坑:你把拦截器加上之后,如果发现跨域请求报错,大部分情况下不是 CORS 配置错了,而是拦截器拦住了预检请求 OPTIONS。你需要在拦截器最前面放行 OPTIONS 方法,否则前端请求直接失败在预检阶段。
3.3 图片上传与虚拟路径映射
商品图片上传是管理端的必做功能。说一个最简单的实现路径:后端接收 MultipartFile,保存到本机的 upload 目录,用 UUID 重命名防止文件名冲突,然后返回相对路径给前端存数据库。
java复制@PostMapping("/api/admin/upload")
public Result<String> upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return Result.error("上传文件不能为空");
}
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = UUID.randomUUID().toString().replace("-", "") + suffix;
File dest = new File(uploadDir + fileName);
try {
file.transferTo(dest);
} catch (IOException e) {
return Result.error("文件保存失败");
}
return Result.success(fileName);
}
但这块有个关键问题:SpringBoot 的默认静态资源目录是 classpath:/static/,你在本地磁盘存的文件,不在这个目录里,直接访问会404。解决办法是添加一个 WebMvcConfigurer,把本机磁盘的目录映射成虚拟路径:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${upload.dir}")
private String uploadDir;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/upload/**")
.addResourceHandler(uploadDir + "/");
}
}
这样配置之后,文件保存在 D:\pet-shop-upload\202403151234.png,前端就可以直接访问 http://localhost:8080/upload/202403151234.png。我在配置里特意用了 @Value 注入了 upload.dir,这样换环境部署时只需要改配置文件,不用改代码,细节虽小但确实实用。
3.4 订单状态流转与库存扣减
订单模块是宠物商城的业务重心,也是最值得在论文里写逻辑的地方。用户下单的完整流程是:前端带着购物车里的商品列表和收货地址调下单接口,后端先查询商品最新价格和库存,再校验每个商品是否还有货,库存足够的就扣减库存,然后生成主订单和订单明细,最后减掉购物车里对应的商品。
先查库存再扣减库存这个问题上,我建议你用一条带条件的更新语句来扣库存,避免高并发超卖:
sql复制UPDATE pet_info SET stock = stock - #{quantity}
WHERE id = #{petId} AND stock >= #{quantity}
这条 SQL 的返回值是受影响行数,如果返回0,说明库存不足,本次扣减失败,事务回滚。这个方法在单体架构下足够安全,而且答辩时你还能讲出“乐观锁控制库存一致性”的亮点。对应生单接口的伪代码是这样的:
java复制@Transactional
public OrderResult createOrder(OrderCreateParam param) {
// 1. 遍历购物车项,锁定商品,验库存、算总价
// 2. 扣库存 update ... where stock >= quantity
// 3. 生成订单主记录
// 4. 生成订单明细(存储商品名称快照、价格快照)
// 5. 清空已下单的购物车项
}
@Transactional 这个注解要加在实现方法上,保证上述这些操作要么全部成功,要么全部回滚。一旦库存扣减之后生成订单明细失败,订单就不该存在,库存也应该恢复原状。不做事务控制的订单模块是最容易被答辩老师挑刺的。
订单取消同样要写清楚状态判断:只有待付款状态的订单可以取消;取消时如果支付已完成则要走退款(毕设可以简化成模拟状态);取消后要恢复库存。把订单每个状态的可操作动作列成一张表,代码按表来写,逻辑清晰也不会漏分支。
4. Vue前端工程化与页面实现
4.1 工程初始化与路由规划
前端我建议直接使用 Vue CLI 来创建项目,虽然 Vite 现在更新,但对毕设来说 Vue CLI 稳定性和资料友好度都更高。执行 vue create pet-shop-front 之后,选择 Vue 2 或者 Vue 3 都可以。如果你用的是 Element UI 那类组件库,Vue 2 配 Element UI 最稳妥;Vue 3 则对应 Element Plus。
src 目录建议按模块来组织,而不是全部堆到一起:
- src/api:按业务域拆分的接口请求文件(pet.js、order.js、cart.js、user.js)。
- src/router:路由配置文件。
- src/store:Vuex 或 Pinia 状态管理。
- src/views:页面组件,注意按“前台”和“管理后台”拆成两个大目录。
- src/components:公共组件,比如商品卡片、轮播图、分页条、面包屑。
路由规划上有几个页面是必须的:首页、宠物列表、宠物详情、购物车、订单确认、订单列表、个人中心、员工后台(商品管理、订单管理、分类管理、客户管理)。需要注意的是,用户端和管理端建议用不同的布局容器,或者用嵌套路由来区分,统一加侧边栏的那种布局模式,前台用户页面也可以用,页面结构会清晰很多。
管理端页面要增加路由守卫,未登录或非管理员跳回登录页:
javascript复制router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token');
if (to.meta.requiresAuth && !token) {
next('/login');
} else {
next();
}
});
这里用 to.meta.requiresAuth 来标识哪些页面需要登录,是最简洁的写法。管理端页面 meta 里再加一个 requiresAdmin,守卫里读取本地存的角色信息做判断。
4.2 Axios封装与请求拦截
前端所有接口请求建议封装在一个 axios 实例里,统一处理 baseURL、超时时间、Token 携带和错误提示。
javascript复制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 = `Bearer ${token}`;
}
return config;
});
// 响应拦截器:统一处理 code
request.interceptors.response.use(
response => {
const res = response.data;
if (res.code === 200) {
return res;
}
// 未登录或Token过期
if (res.code === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(new Error(res.message));
},
error => {
return Promise.reject(error);
}
);
export default request;
这个封装有一个好处,就是在页面里调用接口时,不用每个地方都做 token 判断和错误弹出,只需要 focus 在业务数据上。比如说获取商品列表,页面里只写 request.get('/pet/list', { params }),然后 .then 里拿数据渲染,处理逻辑全部收敛在 api 文件里。
baseURL 到底写什么取决于你怎么部署。开发环境下我建议 Vue 项目的 vue.config.js 里配一个代理,把 /api 转发到后端 localhost:8080,这样既解决了开发环境的跨域,生产环境打包后也能直接用相对路径访问后端,是两全其美的方案。
4.3 购物车与商品列表的交互细节
商品卡片页和购物车虽说是两个模块,但底层逻辑是联动的。宠物列表页点击“加入购物车”,前端不该只是简单地展示一个成功提示,因为后端还要校验这个商品是否在售、是否还有库存。
购物车的实现有“存本地”和“存后端”两种路线。存本地就是用户未登录也能加购物车,用 localStorage 存一组商品ID和数量,登录后再合并;存后端就是每次加购直接调用接口,购物车数据持久化到数据库。如果你的项目时间只有一个月,我建议直接走后端存储方案,好处是换手机、换浏览器数据都在,而且整体代码少很多。前端只需要在登录后调 getCartList 拉取购物车数据,每次修改数量、删除、清空操作都实时同步后端。
商品列表页还有一个很多人不重视的细节:图片懒加载。宠物商城的商品图通常较多,如果一口气把首屏所有的图片全都加载出来,流量和渲染压力都很大。用 Element UI 的 el-image 自带懒加载属性,或者给 img 标签加 v-lazy,成本极低,但优化体验非常明显。
购物车页面操作后端口的顺序也有讲究。批量结算时,前端拿到的购物车条目需要带着 petId 和 quantity 发给后端,后端在事务里校验最新价格,前端不要直接展示购物车里保存的价格作为最终金额,因为商品可能在你浏览的过程中调价了。这个“以服务端价格为准”的约定,需要在接口返回的 order_amount 为准展示,同时写得清楚一点,避免前端自行求和的订单总金额跟后端不一致。
4.4 Element UI与页面组件化
管理后台不要自己从零写 CSS,直接用 Element UI(Vue 2)或 Element Plus(Vue 3)的现成组件,能省一半以上的工作量。表格用 el-table,配 el-pagination 分页;表单用 el-form,配 el-dialog 做新增和编辑;订单状态变成 el-tag 加上 tag type 的映射,状态一目了然。这一套组合拳下来,后台页面的开发速度会非常快。
前台页面则建议留一点手写 CSS 的空间,因为宠物商店最重要的就是氛围感。卡片式布局、圆角、阴影、奶油色背景,这些纯 CSS 的能力就能给页面加分。我在指导学生的过程中一直强调,宠物商城视觉效果做得好,答辩时演示环节就有天然的“亲和力”,老师会更容易认可你的项目完成度。
组件化思想也要贯彻:商品卡片是一个组件,传入 pet 对象就能渲染;轮播图是一个组件,传入图片数组就能显示;分页条是一个组件,传入 total 和 currentPage,emit 一个 pageChange 事件。这样在列表页、搜索结果页、后台商品管理页能反复复用一个商品卡片组件,代码的维护成本大幅下降。答辩老师问“代码复用怎么体现”,组件化就是你最现成的答案。
5. 前后端联调与常见问题排查
5.1 跨域问题与解决方案
前后端分离开发最常遇到的就是跨域报错。你在 Vue 的 localhost:8081 页面里向后端 localhost:8080 发请求,浏览器默认会拦截非同源的响应,后端不处理的话,前端控制台就会刷出一片红。
后端解决跨域的方式是在 SpringBoot 里配置全局 CORS 规则。贴一段最常用的配置:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
}
allowedOriginPatterns 用星号通配是开发阶段的省心写法,但如果你用了 cookie 跨域,注意 allowCredentials(true) 的情况下不能直接用 allowedOrigins("*"),要用 allowedOriginPatterns。
这条配置配好之后,前端还是报跨域,我碰到的第一个原因就是拦截器提前拦截了 OPTIONS 预检请求,这个前面已经提过;第二个常见原因是前端 axios 过程中在此请求头上带了“origin”导致预检不通过,这种状况就需要在拦截器里放行 OPTIONS。记住一个排查顺序:先确认后端 CORS 配置加载了,再确认拦截器放行 OPTIONS,最后确认浏览器 Network 面板里实际发的请求类型。
5.2 图片404与路径访问异常
图片404这个问题在开发阶段出现概率极高,尤其是在你按前面第3.3节做完虚拟路径映射之后,仍然可能出现图片加载不出来。
我的排错经验分三步。第一步,数据库里的 image 字段存的到底是什么?如果存的是“upload/xxx.jpg”这种相对路径,那么前端渲染的时候要拼上后端域名,比如 http://localhost:8080/ + pet.mainImage。如果数据库里直接存了完整 URL,那就是后端上传接口返回时没拼域名,设备之间迁移时会有问题。
第二步,确认映射路径是不是匹配。你在 WebConfig 里注册了 /upload/** 映射到物理磁盘路径,那么前端访问的 URL 必须是 http://localhost:8080/upload/xxx.jpg,而不是 http://localhost:8080/pet_image/xxx.jpg。这种路径写错的问题,往往一眼看不出来。
第三步,确认文件是不是真的存在于目标目录。很多学生上传成功后以为文件写进了项目里头,实际上按前面的代码 hardcode 了一个 uploadDir,这个目录在项目根目录或者某个临时目录。如果你在 IDE 里重启项目,运行时用到的目录不发生改变,那还比较稳定;如果你改了上传代码的目录路径,原有的图片就会突然404。所以上线之前统一在配置文件里搭好 upload.dir,这样切换环境后就改这一个配置。
重要提示:上传目录尽量不要放在项目的 target/classes 下面,因为 Maven recompile 或者 clean package 后文件会被清除。单独的磁盘绝对路径或者项目外部的相对路径更可靠。
5.3 常见问题速查表
下面这张表是我在带学生时汇总的真实高频问题,基本覆盖了从搭建到部署最容易踩的境界。
| 问题表现 | 根本原因 | 解决方案 |
|---|---|---|
| 前端请求后端频繁超时 | 后端8080端口没启动或启动失败 | 看后端控制台报错日志,确认 SpringBoot 正常启动 |
| 访问 8080 端口出现404 | 后端路径写错了,或安全框架拦截 | 检查 @RequestMapping 路径,检查拦截器放行规则 |
| MySQL 连不上,报 Access denied | 数据库用户名密码没对上 | 检查 application.yml 里的 url、username、password |
| MySQL 报错时区异常 | 连接串没加 serverTimezone | 在 url 后附加 serverTimezone=Asia/Shanghai |
| 前端 npm run serve 报错 | 依赖安装不完整或 Node版本太新 | clean 掉 node_modules,重新 npm install |
| JWT 登录后前端拿不到用户信息 | 后端只返回了 token,没返回用户基本信息 | 登录接口里返回 token + 用户JSON |
| 加入购物车后数量对不上 | 后端并发减库存没加锁 | 使用 update ... where stock >= quantity 这条原子语句 |
| 页面首次加载过慢 | 图片没有懒加载、接口没做分页 | 商品列表加 v-lazy,后端接口用 PageHelper 分页 |
| 打包部署后页面白屏 | Vue 路由 mode 是 history | 改用 hash 路由,或后端配置 history fallback |
6. 打包部署与答辩准备
6.1 前后端分离部署方式
毕设项目的部署方式总的说来有三种,你可以按自己的熟悉程度选。
第一种是前后端完全分离。后端 Maven 打包成 jar,在服务器上执行 java -jar pet-shop-backend.jar 命令运行;前端 npm run build 生成 dist 目录,交给 Nginx 做静态文件托管,再给 Nginx 配一个反向代理,把 /api 前缀的请求转发给后端端口。这种方式最接近企业生产环境的形态,但需要你懂一点 Nginx 的基本配置。
第二种是把前端 dist 放进 SpringBoot 的 resources/static 目录,重新打包成单 jar,直接运行一个端口同时提供页面和接口。这种方式配置简单,不用单独安装Nginx,演示的时候一个命令就能跑起整个系统,但也会丧失前后端分离部署的灵活性。
第三种是使用 Docker Compose 把 MySQL、后端、前端三个容器编排起来。这种方式现在越来越流行,也是简历和论文里的加分项。不过如果你的宿主机环境没有 Docker,现场演示时反而多了一层不确定性。
毕设答辩我只建议选你最能稳定复现的一种,别贪多。毕竟现场演示的时候,你能把系统“一键启动”跑起来已经很加分了,没必要让环境问题成为翻车点。
6.2 把Vue打包进SpringBoot的单端口运行方式
如果你的毕业设计演示环境不方便装 Nginx,我最推荐的方式就是第二种:把 Vue 打包后的产物整合进 SpringBoot 单 jar 运行。
具体操作是三步。第一,在前端项目里执行 npm run build,生成 dist 目录;第二,把 dist 目录里的文件全部复制到后端的 src/main/resources/static 下;第三,Maven 重新打包。启动 jar 之后,浏览器直接访问 http://localhost:8080/ 就是你的首页,登录、跳转、请求大家都在同一个端口上,跨域问题彻底消失。
但有三个细节你必须注意。第一,路由模式要改成 hash,Vue Router 里写成 createWebHashHistory(Vue 3)或 mode: 'hash'(Vue 2),否则刷新非根页面时会出现 404(history 模式需要后端把未匹配路径都转发到 index.html)。第二,axios 的 baseURL 要改成 /api 这种相对路径,不能硬编码 http://localhost:8080。第三,SpringBoot 里的前端路由有多个路径时,需要把非接口请求转发到 index.html,这种需求可以通过一个简单的 Controller 或者 WebMvcConfigurer 来处理。
把这一套搭配在一起,你会发现这个单端口部署的方式很适合毕设,因为不管在教室还是老师的电脑上演示,只要装好 JDK 和 MySQL,一个 jar 包就能把整个系统跑起来。
6.3 答辩讲解思路与扩展方向
答辩的时候,老师最常问的问题其实很集中在几个方向:系统的业务流程是什么、数据库设计的依据是什么、遇到的最大难点是怎样解决的、这个系统还能做什么改进。
给你的建议是准备一份“项目难点解答词”。哪怕你没有遇到真实的难点,也可以把技术方案包装成你遇到的挑战。比如:库存控制你用了条件更新语句,这是为了应对并发超卖;图片上传你做了虚拟路径映射,这是为了解决 SpringBoot 静态资源访问问题;登录认证你用了 JWT,这是为了兼容前端分离部署的跨域凭证问题。每一个方案背后,你都能讲清楚“为什么不用别的方案”,这就能体现出真实的思考过程。
扩展方向这块,如果你的论文需要更精细的深度,可以考虑加这几个点:
- Redis 缓存首页轮播图和热门商品列表,减少数据库压力。
- 对接阿里云 OSS 存商品图片,替换本地磁盘存储。
- 引入 RabbitMQ 实现订单超时取消,抢购场景下削峰填谷。
- 增加微信小程序端,复用后端接口,呈现跨端能力。
这些不是必须做的,但如果你学有余力,挑其中一个模块做出一个 demo,论文的评委评定空间就会大很多。
我个人的建议是,宠物商城这个选题之所以经久不衰,就是因为它“够真实”。从浏览商品到下单发货,完整走了一遍电商系统的核心链路,而且宠物图片带来的视觉愉悦感,能在很大程度上掩盖你对代码细节的生疏。如果你现在还在犹豫选题或者正在开发过程中卡住了,不妨先把系统跑起来,再逐步完善那些加分模块。很多事情,动手做了,就会发现并没有想象中那么难。
