1. 选题拆解与项目定位
先说个实话:每年毕业季,后台私信问得最多的就是两类问题——第一类是“老师,我Java学得一般,毕设选什么题能过?”第二类是“网上那些毕设源码靠谱吗?拿回来改一改真的能答辩吗?”
这两类问题的答案,其实都可以落在一个选题上:基于SpringBoot+Vue的游戏装备交易商城系统。
这个选题听起来很常规,但“常规”恰恰是它最大的价值。毕业设计的评分标准从来不是“你做出了什么惊天动地的东西”,而是“你有没有完整走完一个软件项目的生命周期”——需求分析、数据库设计、后端接口、前端页面、联调测试、部署演示。游戏装备交易商城看上去平平无奇,但它天然覆盖了一个电商系统最核心的业务闭环:商品展示、购物车、订单交易、支付结算、用户管理。这正好把Java后端开发最常见的技术点全部串起来了。
如果你是计算机科学与技术、软件工程、信息管理这类专业的学生,毕业设计需要的是一个“可控的复杂度”——太简单了(比如单表的增删改查)评委一眼看穿,太复杂了(比如分布式高并发秒杀)你自己hold不住。装备交易商城处在一个非常舒服的中间位置:业务逻辑足够讲出花来,技术栈又是主流公司里最通用的组合。
适合谁?三类人:
- 打算走Java开发方向、想借毕设把SpringBoot和Vue彻底搞明白的;
- 时间紧张(剩下两三个月)、需要一份能快速跑起来并讲清楚的项目;
- 想走管理类或产品类方向,但学校要求必须做系统开发,需要一个业务故事完整、演示效果好的选题。
这个项目能帮你解决的问题非常直接:用一套完整、可演示、讲得清楚的技术栈,把毕业设计这件“苦差事”变成一份可以写进简历的项目经历。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心技术栈选型:为什么偏偏是SpringBoot + Vue
我发现很多同学选技术栈的时候,习惯是“别人用什么我就用什么”,答辩被老师问一句“为什么选这个技术”,直接卡壳。这一章把每个核心组件背后的“为什么”拆开讲清楚,答辩的时候你就能答出点东西来。
2.1 SpringBoot:把“配置地狱”变成“自动装配”
如果你大二学的是SSH(Struts + Spring + Hibernate)或者SSM(Spring + SpringMVC + MyBatis),你一定记得那种被XML配置文件支配的恐惧——一个applicationContext.xml写上几百行,少配一个bean启动就报错,排查半天发现是classpath路径写错了一个字母。
SpringBoot的核心思想是“约定大于配置”。它内置了Tomcat,你不需要手动部署WAR包;它通过spring-boot-starter-*系列起步依赖,把常用的功能模块打包好,你只要在pom.xml里声明,它就能自动完成配置。
比如你要用MyBatis-Plus操作数据库:
xml复制<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
</dependency>
加上这一个依赖,MyBatis-Plus的自动配置类就会生效,你连SqlSessionFactory都不用自己创建。这就是SpringBoot能成为Java后端绝对主流的核心原因。
那么对于毕设这个场景,SpringBoot还有一个隐藏优势: 它一定能跑起来。JDK版本选8或11,SpringBoot版本选2.7.x,几乎不会碰到环境层面的“天坑”。这对只有两三个月时间的毕设党来说,本身就是决定成败的事。
2.2 Vue:渐进式框架拿捏前端页面
Vue和SpringBoot前后端分离,是目前企业里最主流的开发模式之一。前端跑在localhost:8080做页面渲染和用户交互,后端跑在localhost:9090只提供JSON数据接口,两者通过HTTP通信,互不干扰。
Vue的好处有三个。
第一,渐进式。你不用一上来就学Vuex(状态管理)、Vue Router(路由)全家桶,可以先用最基础的{{ }}插值表达式和v-for把列表渲染出来,然后再逐步引入组件化、路由、状态管理。这个学习曲线对非前端专业的同学非常友好。
第二,组件化。商城系统的页面很多——首页、商品列表、商品详情、购物车、订单确认、个人中心、后台管理……如果用传统的jQuery写法,这些页面的公共部分(导航栏、商品卡片)会复制粘贴无数遍,改一个样式要全局搜索替换。用Vue组件,一个GoodsCard.vue定义好之后,到处引用就行。
第三,开发体验好。Vue Devtools浏览器插件可以直接查看组件树和数据流,排查问题比看console.log高效得多。配合Element-UI组件库,一套还算体面的商城UI,一个前端小白两三周就能搭出来。
2.3 前后端分离:毕设答辩的加分点
很多学生的毕设还是传统的“服务端渲染”——Thymeleaf或JSP,前端页面写在后端项目里。这种方案不是不行,但现在企业里已经很少这么干了,答辩时你很难把这点讲成亮点。
前后端分离架构下,你可以很自然地在答辩时说:
前端工程通过Nginx部署,后端工程以JAR包形式部署,两者通过RESTful API通信。前端不关心数据从哪里来,后端不关心数据怎么展示,职责清晰、易于扩展。
这句话本身,就是答辩时有条理的体现。
2.4 补充组件:Redis、JWT、MinIO
一个“能讲的”商城项目,光有SpringBoot和Vue还不够,我会建议你加上这三个组件:
Redis:用来做缓存和验证码存储。比如把首页轮播图、游戏装备热销榜缓存起来,减少数据库压力;登录验证码存入Redis并设置有效期。数据一致性在毕设里要求不高,用Redis解决“读多写少”的性能问题,是答辩时能说清楚的优化点。
JWT(JSON Web Token):用来做登录状态管理。传统Session方案在前后端分离下会有跨域和集群共享问题,JWT把用户信息加密后放在Token里,前端每次请求时放在请求头中,后端解析即可。代码量不大,但含金量高。
MinIO:用来存储商品图片。很多同学喜欢把图片Base64编码后直接存入数据库,或者上传到本地磁盘文件夹——这两种方案都有明显缺点:前者会让数据库体积膨胀得可怕,后者一旦打包部署就找不到图片了。MinIO提供S3协议的对象存储,本地也能安装,是接近生产环境的方案。
这三个组件把商城系统从“课程设计”提升到了“能写进简历”的层次。不是说多复杂,而是它们代表你确实了解生产环境常用的技术。
3. 数据库设计:电商系统最考验基本功的地方
数据库设计是整个系统里最不能偷懒的部分。答辩时老师大概率会问“为什么这么建表”“这个字段为什么不放那表里”。设计得好,答辩从容;设计得烂,就是一场灾难。
我们先想想这个系统的角色。装备交易商城,其实模式类似闲鱼和网易藏宝阁的结合体:用户既可以买装备,也可以卖装备(发布商品),后台管理员做审核和管理。这个业务模型决定了至少需要以下数据表。
3.1 核心表结构与字段设计
用户表(sys_user)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,雪花算法生成 |
| username | varchar(50) | 登录名,唯一 |
| password | varchar(255) | BCrypt加密后的密码 |
| nickname | varchar(50) | 昵称 |
| avatar | varchar(255) | 头像URL |
| balance | decimal(10,2) | 账户余额,用于交易模拟 |
| role | tinyint | 角色:0管理员 1普通用户 |
| status | tinyint | 状态:0禁用 1正常 |
| create_time | datetime | 创建时间 |
角色这里只分两种:管理员和普通用户。我见过不少毕设搞RBAC权限模型,搞出五六个表(用户表、角色表、权限表、用户角色关联表、角色权限关联表),但这在商城项目里其实是过度设计。如果你不是专门做权限管理这个方向,两种角色用role字段区分就够了,省下来的精力可以投入到订单流程这种更核心的地方。
游戏装备商品表(goods)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| name | varchar(100) | 装备名称 |
| category_id | bigint | 分类ID |
| price | decimal(10,2) | 售价 |
| original_price | decimal(10,2) | 原价,用于展示折扣 |
| description | text | 装备描述 |
| cover_image | varchar(255) | 封面图URL |
| images | text | 详情图,JSON数组格式存储 |
| seller_id | bigint | 卖家用户ID |
| sales_count | int | 销量 |
| status | tinyint | 状态:0待审核 1在售 2已下架 3已售出 |
| create_time | datetime | 上架时间 |
这里有几个细节要注意:images字段我建议用JSON字符串存储多个图片URL,而不是拆成一张图片表。毕设规模下,拆表徒增复杂度,JSON字段足够用。status字段的流转逻辑(待审核→在售→已售出)是业务规则的核心,在Service层要写好状态校验。
订单表(orders)
订单表是电商系统的核心表,字段设计直接反映你是否理解交易流程。
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 订单号,全局唯一 |
| goods_id | bigint | 商品ID |
| buyer_id | bigint | 买家ID |
| seller_id | bigint | 卖家ID |
| price | decimal(10,2) | 成交价 |
| status | tinyint | 状态:0待支付 1已支付 2已完成 3已取消 |
| pay_time | datetime | 支付时间 |
| create_time | datetime | 创建时间 |
这里的关键设计思考是:为什么要把买卖双方ID都存进订单表? 因为这笔交易一旦发生,买家和卖家都需要在自己的订单列表中看到这条记录。如果只存买家ID,卖家查订单就要反查商品表,多一次关联查询不说,逻辑也绕。
购物车表(cart)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 用户ID |
| goods_id | bigint | 商品ID |
| quantity | int | 数量(虚拟商品通常为1) |
| create_time | datetime | 加入时间 |
注意:购物车表应该有用户和商品的联合唯一约束,避免同一个用户重复添加同一件商品。这个约束在数据库层面加了,代码里就没那么多if判断了。
除了这四张核心表,还需要分类表(category)、收藏表(favorites)、留言/评论表(comment)。
3.2 为什么订单号和ID要用不同字段
初学者容易犯一个错误:直接用自增ID当订单号。但订单号有两个硬性要求:全局唯一和不可预测。自增ID是可预测的,别人看到订单号为10086,就能推断出系统大约有多少订单,这是安全隐患。
我的做法是用Redis生成趋势递增的订单号,核心代码如下:
java复制// 订单号格式:20250347120001 + 用户ID后四位
String dateStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
String userIdSuffix = String.format("%04d", userId % 10000);
// 完整订单号
String orderNo = dateStr + userIdSuffix + RandomUtil.randomNumbers(4);
时间戳保证了全局唯一性(同秒内再加随机数兜底),用户ID后四位让订单号具备业务可追溯性。答辩时你要是能说出这段设计思路,老师对你的印象分直接不一样。
3.3 数据库设计答辩问答指南
我把自己当年被问过的问题整理了一下,提前准备这几个问题的答案:
- “为什么余额用decimal而不是double?”——因为double存在精度丢失问题,金额计算必须用高精度类型。用
BigDecimal操作,避免0.1 + 0.2 != 0.3这种尴尬场景。 - “商品表和管理员表为什么都在sys_user里?”——因为卖家和买家本质上都是用户,只是角色不同,一张表加角色字段就够了。如果未来业务复杂,可以再拆成用户和商家两种账号体系。
- “怎么保证库存和订单的一致性?”——装备交易是虚拟商品,不存在超卖问题(没有库存约束,一件装备只能被买走一次),但需要通过事务保证:创建订单、扣减商品状态、更新余额,这三步要么全部成功,要么全部回滚。
提前准备好这些问题的思路,数据表这一关你就稳了。
4. 后端接口设计与核心实现
数据库设计好后,后端开发就是围绕“CRUD + 业务规则”展开的。很多同学拿到代码后不知道该从哪个文件看起,我建议的顺序是:config包看配置和拦截器 → entity包看实体类 → mapper包看数据访问 → service包看业务逻辑 → controller包看对外接口。这正好是一个请求从进入到响应的完整链路。
4.1 使用MyBatis-Plus:写SQL还是代码生成器?
MyBatis-Plus是目前国内中小公司使用率极高的持久层框架,它API丰富、上手快、CRUD代码量极低。在你的项目中引入它后:
java复制@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements GoodsService {
// 很多基础CRUD方法不需要自己写
// 继承的ServiceImpl自带 save/update/remove/getById/list 等方法
}
对于简单的单表CRUD,你甚至不需要写SQL。配合代码生成器(MyBatis-Plus Generator),实体类、Mapper接口、Service、Controller,全部一键生成,你只需要改业务逻辑部分。这对时间紧张的毕设党来说,是效率神器。
但注意,不是所有查询都适合用通用Mapper。比如:
xml复制<select id="selectGoodsPage" resultType="com.example.entity.Goods">
SELECT g.*, c.name AS categoryName
FROM goods g
LEFT JOIN category c ON g.category_id = c.id
<where>
<if test="keyword != null and keyword != ''">
AND g.name LIKE CONCAT('%', #{keyword}, '%')
</if>
<if test="categoryId != null">
AND g.category_id = #{categoryId}
</if>
AND g.status = '1'
</where>
ORDER BY g.create_time DESC
</select>
带条件拼接和关联查询的场景,手写SQL反而更加清晰可控。你说“MyBatis-Plus提高开发效率”的时候,也要能说清楚“自定义SQL用于解决复杂查询”,这才显得你理解工具的应用边界。
4.2 JWT登录鉴权:从设计到代码
登录鉴权模块是整个系统里代码量不大但设计含量最高的地方。我描述一下完整流程:
第一步,用户登录,后端校验用户名密码:
java复制@Service
public class LoginService {
@Resource
private SysUserMapper sysUserMapper;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Value("${jwt.secret}")
private String secret;
public LoginVO login(String username, String password, String code, String uuid) {
// 校验Redis中的验证码
String cacheCode = stringRedisTemplate.opsForValue().get("captcha:" + uuid);
if (cacheCode == null || !cacheCode.equalsIgnoreCase(code)) {
throw new BusinessException("验证码错误");
}
// 查询用户
SysUser user = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUsername, username));
if (user == null || !BCrypt.checkpw(password, user.getPassword())) {
throw new BusinessException("用户名或密码错误");
}
// 生成JWT
String token = JWT.create()
.withSubject(username)
.withClaim("userId", user.getId())
.withClaim("role", user.getRole())
.withExpiresAt(new Date(System.currentTimeMillis() + 7 * 24 * 3600 * 1000L))
.sign(Algorithm.HMAC256(secret));
return new LoginVO(token, user);
}
}
第二步,写一个拦截器统一拦截需要鉴权的接口:
java复制public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("Authorization");
if (token == null || token.isEmpty()) {
throw new BusinessException(401, "未登录,请先登录");
}
// 解析token,将用户信息放入ThreadLocal
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret)).build();
try {
DecodedJWT jwt = verifier.verify(token.replace("Bearer ", ""));
UserContext.set(jwt.getClaim("userId").asLong(), jwt.getClaim("role").asInt());
} catch (JWTVerificationException e) {
throw new BusinessException(401, "登录状态已过期,请重新登录");
}
return true;
}
}
这里有两个常见的坑,提前给你排掉:
- Token过期时间设7天比较合适。设太短(1小时)会让开发调试时频繁被拦截,影响体验;太长(永久)又会带来安全隐患。
- 拦截器排除登录、注册、商品列表等公开接口,否则会出现“用户还没登录,连首页都打开不了”的问题。
4.3 订单流程:事务处理是核心难点
订单流程是后端代码里业务规则最密集的地方,我建议把核心逻辑写在Service层,用@Transactional注解保证事务:
java复制@Transactional(rollbackFor = Exception.class)
public Long createOrder(Long goodsId, Long buyerId) {
// 1. 查询商品
Goods goods = goodsMapper.selectById(goodsId);
if (goods == null || goods.getStatus() != 1) {
throw new BusinessException("商品不存在或已下架");
}
// 2. 不能买自己发布的装备
if (goods.getSellerId().equals(buyerId)) {
throw new BusinessException("不能购买自己发布的装备");
}
// 3. 创建订单
Order order = new Order();
order.setOrderNo(generateOrderNo(buyerId));
order.setGoodsId(goodsId);
order.setBuyerId(buyerId);
order.setSellerId(goods.getSellerId());
order.setPrice(goods.getPrice());
order.setStatus(0); // 待支付
orderMapper.insert(order);
// 4. 扣减买家余额
int update = sysUserMapper.deductBalance(buyerId, goods.getPrice());
if (update == 0) {
throw new BusinessException("余额不足");
}
// 5. 增加卖家余额
sysUserMapper.addBalance(goods.getSellerId(), goods.getPrice());
// 6. 更新商品状态为已售出
goods.setStatus(3);
goodsMapper.updateById(goods);
return order.getId();
}
事务原理补充:@Transactional的默认回滚条件是RuntimeException,因此务必设置rollbackFor = Exception.class,否则遇到受检异常事务不会回滚。另外要注意,在同一个类中通过this调用被事务标注的方法,事务是不生效的——Spring事务是AOP代理实现的,只有跨Bean调用才经过代理。
这里还有个余额扣减的细节:我用的是UPDATE sys_user SET balance = balance - #{price} WHERE id = #{userId} AND balance >= #{price}这种带条件更新的SQL,这是原子操作,天然避免并发问题。如果在代码里先查询余额再判断再更新,高并发下会有超扣风险,这正是面试里常考的“数据一致性”问题。
5. 前端开发:Vue页面与核心交互
后端接口完成后,前端开发就是把这些接口“拼装”成用户能看能用的页面。
5.1 项目初始化:Vite + Vue 3 + Element-Plus
现在毕设写Vue,我建议直接用Vue 3 + Vite + Element-Plus组合。相比Vue 2的Webpack方案,Vite启动速度快、配置更简洁。
初始化项目:
bash复制npm create vite@latest game-mall-front -- --template vue
cd game-mall-front
npm install element-plus axios vue-router pinia
项目的目录结构建议:
text复制src/
├── api/ # 接口封装
│ ├── goods.js
│ ├── order.js
│ └── user.js
├── components/ # 公共组件
│ ├── NavBar.vue
│ └── GoodsCard.vue
├── router/ # 路由配置
│ └── index.js
├── views/ # 页面视图
│ ├── Home.vue
│ ├── GoodsList.vue
│ ├── GoodsDetail.vue
│ ├── Cart.vue
│ ├── Login.vue
│ ├── Register.vue
│ ├── UserCenter.vue
│ └── admin/ # 后台管理页面
│ ├── Dashboard.vue
│ ├── GoodsManage.vue
│ └── OrderManage.vue
└── utils/ # 工具函数
└── request.js # axios封装
5.2 axios封装:处理Token和统一错误
前端和后端交互的过程中,最影响开发效率的就是token管理和错误处理。在request.js中统一处理:
javascript复制import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
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
})
// 响应拦截器:统一处理后端返回
request.interceptors.response.use(
response => {
const res = response.data
if (res.code === 200) {
return res.data
}
ElMessage.error(res.message)
return Promise.reject(new Error(res.message))
},
error => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
router.push('/login')
}
ElMessage.error(error.message || '网络错误')
return Promise.reject(error)
}
)
export default request
统一封装后,每个页面的接口调用变得非常简洁:
javascript复制// api/goods.js
import request from '@/utils/request'
export function getGoodsPage(params) {
return request.get('/goods/page', { params })
}
export function getGoodsDetail(id) {
return request.get(`/goods/detail/${id}`)
}
5.3 商品列表页:条件查询联动
商品列表页是展示前端基本功的好地方。搜索框、分类筛选、价格排序这三个条件需要联动:
vue复制<template>
<div class="goods-list">
<!-- 搜索栏 -->
<el-input v-model="query.keyword" placeholder="搜索装备名称" clearable @clear="loadData" @keyup.enter="loadData" />
<!-- 分类筛选 -->
<el-select v-model="query.categoryId" placeholder="全部分类" clearable @change="loadData">
<el-option v-for="item in categoryList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<!-- 价格排序 -->
<el-radio-group v-model="query.sort" @change="loadData">
<el-radio-button label="">默认</el-radio-button>
<el-radio-button label="price_asc">价格从低到高</el-radio-button>
<el-radio-button label="price_desc">价格从高到低</el-radio-button>
</el-radio-group>
<!-- 商品卡片栅格 -->
<el-row :gutter="20">
<el-col v-for="item in goodsList" :key="item.id" :span="6">
<goods-card :goods="item" @click="goDetail(item.id)" />
</el-col>
</el-row>
<!-- 分页 -->
<el-pagination
v-model:current-page="query.pageNum"
v-model:page-size="query.pageSize"
:total="total"
layout="total, prev, pager, next"
@current-change="loadData"
/>
</div>
</template>
这段代码对应的后端查询条件就是我在4.1节里写的那个手写SQL——前端传什么参数,后端映射成什么查询条件,前后端联调时把这个对应关系理清楚,调试效率就高了。这是我在实际开发里踩过最多坑的地方:前端传的参数字段名和后端实体属性名对不上,查半天才发现是categoryId写成了category_id。
5.4 商品发布:富文本与多图上传
卖家发布装备是另一个核心页面。这里我用的是Element-Plus的el-upload组件配合MinIO实现图片上传:
vue复制<el-upload
action="/api/upload/image"
:headers="{ Authorization: 'Bearer ' + token }"
:on-success="handleSuccess"
list-type="picture-card"
>
<el-icon><Plus /></el-icon>
</el-upload>
后端接收上传后,将文件写入MinIO并返回访问URL:
java复制@PostMapping("/upload/image")
public Result<String> uploadImage(MultipartFile file) {
// 校验文件类型和大小
String originalFilename = file.getOriginalFilename();
String ext = originalFilename.substring(originalFilename.lastIndexOf("."));
if (!Arrays.asList(".jpg", ".jpeg", ".png", ".webp").contains(ext.toLowerCase())) {
throw new BusinessException("仅支持jpg/png/webp格式图片");
}
// 生成唯一文件名
String objectName = "goods/" + UUID.randomUUID() + ext;
// 上传到MinIO
minioClient.putObject(PutObjectArgs.builder()
.bucket("mall-bucket")
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build());
// 返回可访问的URL
String url = minioEndpoint + "/mall-bucket/" + objectName;
return Result.success(url);
}
MinIO的默认端口是9000,控制台是9001,如果你在本机测试,上传后的图片访问地址是http://localhost:9000/mall-bucket/goods/xxx.png。这个地址需要在后端配置跨域允许,否则前端页面上图片会加载不出来——这是部署阶段最容易踩的坑,我放到第八章详细讲。
6. 后台管理模块:让项目多一个维度
很多同学做毕设只关注前台用户的操作流程,却忽略了一个问题:系统里总得有个人来管理商品、审核订单、处理用户,否则“商城”就只是个“展示页面”。
后台管理模块是区分“课程设计”和“完整项目”的重要标志。它本身代码量不大,但能让你的系统功能闭环。
后台管理核心功能:
- 商品审核:用户可以发布商品,但需要管理员在后台审核通过后才会在前台展示。这个“审核流”是电商平台的普遍做法,也正好解释了我设计
goods.status字段(0待审核、1在售)的用途。 - 订单管理:管理员查看所有订单列表,可以按状态筛选(待支付、已支付、已完成),也可以处理异常订单。
- 用户管理:查看用户列表、禁用/启用账号。
- 数据统计:用ECharts展示每日订单量折线图和商品分类占比饼图。
后台管理的前端开发思路是复用前台的基础布局,只换内容区域。路由配置上做一个简单嵌套路由:
javascript复制// router/index.js 部分代码
{
path: '/admin',
component: AdminLayout,
children: [
{ path: 'goods', component: GoodsManage },
{ path: 'orders', component: OrderManage },
{ path: 'users', component: UserManage },
{ path: 'stats', component: Dashboard }
]
}
ECharts图表虽然简单,但放在后台首页,视觉效果直接拉满,答辩演示时“数据可视化”这个点自然就讲到了。
7. 测试用例与演示脚本:答辩不翻车的关键
做开发的时候总觉得“能跑就行”,但到答辩现场,最怕的就是演示过程中翻车——不是这个页面打不开,就是那个功能点了没反应。提前准备好一套完整的演示脚本,能帮你避免90%的翻车现场。
7.1 演示路径设计
我建议你的演示走这样一条完整业务闭环:
- 首页展示:打开系统首页,说明这是什么项目、核心功能有哪些,配合导航栏说明系统分前台和后台两个部分。
- 浏览与搜索:进入商品列表页,演示关键词搜索(比如“传说武器”)和分类筛选,讲解前后端交互流程。
- 用户登录与商品详情:登录账号,进入某一件装备的详情页,演示收藏、加入购物车。
- 购物车与下单:购物车页面勾选商品,点击结算,演示订单创建、余额扣减的完整过程。
- 卖家视角:切换另一个账号(卖家账号),演示登录、发布装备、填写装备信息、上传图片的流程,说明发布后需等待管理员审核。
- 后台管理:用管理员账号登录后台,演示商品审核通过、查看订单列表、数据统计图表。
- 收尾:展示项目代码结构、接口文档或数据库设计说明,回答老师提问。
这条路径覆盖了项目所有核心模块,全程大概6-8分钟,节奏刚刚好。
7.2 必备的测试用例
测试用例不要写一堆“点点点”的流水账,重点覆盖关键流程和异常场景:
| 编号 | 测试场景 | 预期结果 |
|---|---|---|
| TC01 | 正确用户名密码登录 | 登录成功,返回Token,跳转首页 |
| TC02 | 错误密码登录 | 提示“用户名或密码错误” |
| TC03 | 未登录访问购物车接口 | 返回401,前端跳转登录页 |
| TC04 | 搜索存在的装备关键词 | 返回包含关键词的商品列表 |
| TC05 | 搜索不存在的关键词 | 返回空列表,前端显示“暂无数据” |
| TC06 | 购买自己的商品 | 提示“不能购买自己发布的装备” |
| TC07 | 余额不足时下单 | 提示“余额不足”,订单不创建 |
| TC08 | 下架状态的商品购买 | 提示“商品不存在或已下架” |
| TC09 | 管理员审核通过商品 | 商品状态变为在售,前台可见 |
| TC10 | 上传非图片格式文件 | 提示“仅支持jpg/png/webp格式” |
每个用例测完后,把结果记下来。答辩时如果老师问“你这系统有没有测试依据”,你把这些用例记录亮出来,说服力完全不一样。
7.3 JUnit单元测试:再给答辩加一点分
很多毕设项目的测试环节是缺失的,但写几个核心Service的单元测试其实不难:
java复制@SpringBootTest
class OrderServiceTest {
@Resource
private OrderService orderService;
@Test
void testCreateOrderWithInsufficientBalance() {
// 假设用户余额为0
Long goodsId = 1001L;
Long buyerId = 999L;
assertThrows(BusinessException.class, () -> {
orderService.createOrder(goodsId, buyerId);
});
}
}
这几个测试类放在项目里,代码质量评估这一项的优势是非常直观的。
8. 常见问题排查与避坑指南
这部分是实操沉淀下来的经验,源码拿到手后如果跑不起来或运行出错,先对照这里排查。
8.1 必踩坑位:环境与配置
CORS跨域问题
前后端分离项目,前端在http://localhost:5173(Vite默认端口),后端在http://localhost:9090,前端发起请求必然触发跨域。解决方案是后端配置跨域过滤器:
java复制@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 允许所有来源,开发阶段够用
config.addAllowedOriginPattern("*");
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
这里有个细节:配置了allowCredentials(true)之后,addAllowedOrigin("*")会失效,必须用addAllowedOriginPattern("*")。这个坑网上很多帖子写过,但新手经常忽略。
MySQL时区报错
连接MySQL时如果遇到The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,在连接串上加参数:
properties复制jdbc:mysql://localhost:3306/mall?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false
SpringBoot版本太高导致的兼容问题
毕设建议锁定版本身份:SpringBoot 2.7.x + MyBatis-Plus 3.5.x + JDK 8。如果你用了SpringBoot 3.x,注意它要求JDK 17以上,且MyBatis-Plus有些语法需要换成mybatis-plus-spring-boot3-starter,改动成本比较高。没有特殊需求,别追新版本。
8.2 必踩坑位:前后端联调
Vite代理配置
开发环境下,建议在Vite中配置代理,避免前端代码里写死IP和端口:
javascript复制// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:9090',
changeOrigin: true
}
}
}
})
这样前端请求/api/goods/page会自动转发到后端9090端口。部署时用Nginx配置相同逻辑,前端代码完全不用改。
时间字段格式不一致
Java后端默认返回的LocalDateTime格式是2025-03-04T12:30:00,前端直接用会显示一个T在中间,很不美观。在配置文件中加:
properties复制spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
这个配置看似小事,但在答辩演示时,时间显示是否规范,是最直观的“工程质量”信号。
8.3 排查问题的方法论
在毕设阶段,很多同学遇到Bug的第一反应是:去百度/问AI/问学长。我建议你换一种思路——先把日志看完再问别人,这个习惯对以后工作极其重要。
后端日志定位:
bash复制# 在生产环境查看日志
tail -f /logs/spring.log
如果接口报错500,优先看控制台完整堆栈信息,找到第一个Caused by:,那才是问题的根源。
前端接口调试:
打开浏览器F12,Network面板看请求和响应。点击一个请求,检查三个东西——请求URL、请求方式、响应内容。80%的接口问题都能在这找到答案。
我统计过自己写毕设那段时间的问题类型:配置类问题占40%(端口冲突、缺少依赖、版本不对)、数据库问题占25%(SQL语法、字段名写错、编码问题)、前后端参数不匹配占20%、业务逻辑漏洞占15%。所以拿到项目第一件事,是先解决配置问题,把系统跑起来,再谈改业务逻辑。盲目改代码而不先跑通,越改越乱。
8.4 论文写作:代码是骨架,论文是血肉
最后顺带说一句论文。很多同学代码跑通了,但论文不会写。一个比较实用的方法是:先写完代码再倒逼论文。因为做毕设的过程中你已经经历了“需求分析→系统设计→详细设计→编码实现→测试调试”的完整过程,论文就是把这个过程用学术语言复述一遍。
论文大纲建议:
- 第一章 绪论(研究背景与意义、国内外研究现状、论文结构安排)
- 第二章 相关技术介绍(SpringBoot、Vue、MyBatis-Plus、Redis、MinIO)
- 第三章 系统需求分析(可行性分析、功能需求分析、用例图)
- 第四章 系统设计(总体架构设计、功能模块设计、数据库设计)
- 第五章 系统实现(各部分功能界面截图 + 核心代码 + 实现说明)
- 第六章 系统测试(测试环境、测试用例、测试结果)
- 第七章 总结与展望
数据库设计这一章,你把第三章的E-R图和建表语句贴进去,再把本篇文章里讲到的每个设计决策写清楚,这部分工作量就完成一大半了。
9. 这个项目还能怎么扩展:写在最后
做完这个系统,如果你还有余力,我建议你尝试往这几个方向扩展,每个方向都能让项目多一个亮点:
引入WebSocket做实时消息通知。比如买家下单后卖家立刻收到消息提醒,管理员收到待审核提醒。WebSocket是很多企业项目的常客,写在简历上是明确的技术加分项。
增加支付模拟流程。不需要真的对接支付宝或微信支付——那种需要企业资质,学生没有。可以做个充值页面,模拟调用第三方支付平台的流程,增加一个payment记录表,把“余额支付”升级成“充值再支付”,整个资金流的逻辑就完整了。
前后端分离的权限控制细化。虽然我前面说不要过度设计权限,但如果你的毕设题目带“管理系统”三个字,那就需要把用户-角色-权限三表模型补上,并配合前端路由守卫实现按钮级权限控制。这是管理类系统的另一座山头。
部署到云服务器。花几十块钱买个轻量服务器,把前端和后端都部署上去,数据库也搬到云端,让老师访问一个公网可访问的地址来检查你的毕设。这一项就是碾压级的印象分——老师演示过太多次“请稍等,我本地启动一下”。
我在实际带毕设的这几年里,最大的体会是:绝大多数同学缺的不是写代码的能力,而是把项目“做完”的能力。这里的“做完”,指的是跑通全流程、测试所有的关键路径、写好配套文档、练熟演示流程。每个环节都补一点,你的毕设答辩就不会差到哪里去。
希望这篇拆解对你真正有用,方向选好了,剩下的就是沉下心,把这个系统从0到1走一遍。你会发现在这个过程中学到的东西,比大学前三年课堂上的总和还要多。
