如果你正在准备毕业设计或课程设计,手里又需要一个前后端分离的完整项目,那SpringBoot+Vue的销售管理类系统,十有八九会出现在你搜索列表的前几页。今天要说的是一个更细分的场景:手机销售网站管理平台,Java+MySQL做后端存储,Vue承担前端交互,源码完整,稍微改改就能变成自己的东西。这个项目能做什么?简单说就是一套“商城前台+管理后台”的双端系统:访客能在前台浏览手机、加购物车、下单,管理员能在后台维护商品、处理订单、看销售数据。对于毕设、课设、或者不想从零开荒的入门学习者,它的体量刚好——不至于像电商巨头那样复杂到看不懂,又比普通的CRUD示例多了业务逻辑和权限控制。接下来我会从技术选型、数据库设计、核心代码实现、部署排查这几个维度,把整个项目拆开揉碎讲清楚,尤其会提一些你在学校课设或者自学时最容易踩的坑。
1. 整体架构与设计思路
1.1 技术栈选型:为什么是SpringBoot+Vue
现在做Web项目,前后端分离已经是默认姿势了。选SpringBoot做后端,最大的理由就是“省事”。它内置了Tomcat,不再需要单独配置外部容器,一个带main方法的jar包就能跑起来;配合Spring全家桶的自动装配,写接口的效率比传统SSH那套高出一大截。而Vue在前端的地位不用多说,组件化的开发方式特别适合这种页面多、交互频繁的管理系统,尤其是后台的表格、表单、图表这类重复性UI,拆成组件后复用起来非常舒服。
MySQL作为数据库也没有悬念,它是Java技术栈里最稳妥的选择。手机销售平台的数据结构清晰:用户、商品、订单、购物车,都是典型的关系型数据表,用MySQL的ACID特性来保证订单流程的一致性再合适不过。这不像一些高并发项目需要刻意引入缓存中间件,毕设和课设的访问量用单机MySQL完全够用。
我见过很多同学一上来就堆技术:Redis、RabbitMQ、Elasticsearch全塞进去,结果压测根本没并发,反而把答辩时间都耗在环境配置上。这个项目最大的设计智慧就两个字:克制。它把精力放在了核心业务逻辑上,让你能真正讲清楚每一个接口的用途和每一张表的关系。
1.2 系统角色与功能拆分
手机销售平台从使用场景上天然分成两个角色:前台浏览的普通用户和后台管理的管理员。如果登录用户再细一点,还可以加一个“普通用户”和“管理员”的权限字段,但本质上还是一套用户体系。
前台用户侧的核心功能有:注册、登录、浏览手机商品列表、按品牌或价格筛选、搜索、查看手机详情、加入购物车、提交订单、支付模拟、查看个人订单。这里面有几个地方是答辩时候的高频问点:购物车和订单的关系、库存扣减的时机、订单状态的变化。
后台管理侧的功能包括:商品分类管理、手机型号管理、库存管理、订单管理、用户管理、销售数据统计。商品和分类是典型的一对多关系,订单和订单详情是一对多关系,用户和订单是一对多关系。把这些关系捋清楚,数据库设计就完成了一大半。
还有一点值得注意:商品表里通常会设计一个“上架/下架”状态字段。为什么要有这个字段?因为前台用户不应该看到管理员还没准备好的商品,而这时候商品记录已经在数据库里了。这种小细节能在答辩时体现出你对真实业务的理解,而不是单纯做增删改查。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与关键表解析
2.1 用户、商品、订单三张核心表
看一个项目质量高不高,第一步先看它的数据库脚本。手机销售平台最少需要这六张表:用户表、手机品牌表或分类表、手机商品表、购物车表、订单表、订单明细表。如果做宽一点,还可以加一张轮播图表或者公告表,但核心还是前六张。
用户表(user)字段通常包括:id、用户名、密码、昵称、性别、手机号、邮箱、头像、角色标识、创建时间。这里有个大家常犯的错误:密码明文存储。真实项目不可能这么干,答辩老师也基本都会问。所以源码里至少要用MD5加盐或者BCrypt加密来保存密码。我自己写毕设的时候用的是BCrypt,它自带随机盐,同一个密码每次哈希出来的结果都不同,安全性比MD5高几个档次。
商品表(product)的字段是重头戏:id、分类id、手机名称(比如某品牌某型号)、主图、轮播图列表、原价、现价、库存、销量、是否热销、是否新品、详情描述、上架状态、创建时间。价格字段请一律使用Decimal而不是Float/Double,这是必考知识点。浮点数在数据库里存金额会出现精度丢失,比如0.1+0.2变成0.30000000000000004,这在电商平台里属于致命错误。用Decimal(10,2)表示最多可存8位整数加两位小数,足够用了。
订单表(order)需要重点设计:订单号、用户id、订单总金额、收货人姓名、联系电话、收货地址、订单状态、支付时间、创建时间。订单号建议用时间戳加随机数生成,不能只用自增id,因为订单号可能在退款、对账等场景中对外暴露,自增id太容易被猜测。订单状态我又单独立了一个字段,用整数标识:0待支付、1已支付、2已发货、3已收货、4已完成、5已取消。用状态字典来表达流转,比单纯写死字符串要清晰得多。
订单明细表(order_item)用来记录下单时商品快照信息。为什么叫快照?因为商品的价格、名称、图片都有可能被管理员修改,如果订单明细只存一个商品id,等订单完成后商品被下架,用户查看历史订单就会显示“商品不存在”。所以订单明细里必须冗余一份快照信息:商品id、商品名称、商品主图、下单时价格、购买数量、小计金额。
2.2 购物车与库存的关联处理
购物车表(cart)看起来简单:id、用户id、商品id、数量、加入时间。但它在业务上有一个容易被忽略的点:同一用户把同一个商品多次加入购物车时,不要生成多条记录,而应该把数量累加。这需要在前端判断,也可以在后端做唯一约束(用户id+商品id唯一)之后用插入或更新的方式处理。
库存处理的正确姿势也值得一提。手机商品的库存字段就放在商品表里,下单时的库存扣减有两种方案:一种是提交订单后在事务里直接UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?,这种写法可以防止超卖,因为数据库会锁住这一行;另一种是先查库存再减库存,这就存在并发问题,两个请求同时查到库存为1,结果都下单成功,就超卖了。毕设项目里用第一种写法就够,但如果能在代码里明确注释一句“这里使用乐观锁/条件更新防止超卖”,评委老师会高看你一眼。
购物车转订单的流程也要处理好:当用户勾选多个购物车项去结算时,前端要传一个购物车项id数组,或者直接把商品快照传到后端。更合理的做法是后端根据用户id从购物车表里查询勾选的数据,因为前端传来的数据是可以伪造的,比如篡改价格。真正的后端接口应该只接收用户id和一批选中的商品id,然后自己去查询价格和库存。
3. 后端核心模块的实现细节
3.1 统一返回体与全局异常处理
看任何一个SpringBoot项目的源码,第一件事找它的公共返回类。如果没有统一返回体,接口一会儿返回Map一会儿返回String,前端对接时一定会崩溃。这个项目里常见的做法是定义一个Result类,包含code、message、data三个字段,比如Result.success(data)和Result.error("msg")。code用200表示成功,500表示业务失败,401表示未登录,403表示无权限。这样前端可以统一拦截处理。
光有返回体还不够,必须有全局异常处理。我之前带过的一个学弟,他写的Controller里每一个方法都自己try-catch,然后返回Result.error,写起来又累又容易漏。正确做法是写一个@RestControllerAdvice全局异常处理器,配合@ExceptionHandler分别处理业务异常、参数校验异常和兜底异常。这样Controller只关注正常流程,业务逻辑里可以用throw new BizException("库存不足")直接中断,被全局处理器捕获后转换成统一JSON格式返回。
3.2 JWT权限控制与拦截器配置
用户登录这块,这个项目不是用传统的Session,而是用JWT,这也是现在前后端分离项目的标配。JWT说白了就是一个经过签名的字符串,后端在用户登录成功后生成,把用户id和角色信息放进去,返回给前端保存;前端每次请求在请求头里带上Authorization: Bearer <token>,后端拦截器解析token就能知道当前用户是谁。
实现上需要三步:引入jjwt依赖,写一个JWT工具类负责生成和解析token,再写一个拦截器在进入Controller前检查token。拦截器里还要分清哪些接口需要登录、哪些接口需要管理员权限。比如前台查询商品列表不需要登录,但提交订单必须登录;后台管理接口必须校验角色是管理员。这里可以使用SpringMVC的HandlerInterceptor配合WebMvcConfigurer注册,也可以在@RequestMapping上手动加自定义注解。用拦截器的方式更通用,而且不需要改动具体的Controller方法。
有一个很容易踩的坑:token过期时间的设置。太短了用户玩着玩着就被踢下线,太长了又不安全。一般单机练手项目设置2小时或者7天都可以,但记得在后端解析异常时返回401而不是500。前端拿到401后跳转到登录页并清除本地token,这个流程是面试官比较爱问的链路。
3.3 订单状态机的设计与并发保护
订单模块是整个系统业务最重的部分,也最能体现一个人的代码水平。它不是一个简单的insert,而是一个状态流转:待支付 -> 已支付 -> 已发货 -> 已收货 -> 已完成,以及任意非终态 -> 已取消。
在设计接口时,比如/order/pay、/order/ship、/order/confirm,不要只在接口里写一句“修改订单状态”。你应该在改动之前判断当前状态是否允许目标状态。比如一个已取消的订单不能被支付;一个已完成的订单不能发货。这个判断逻辑抽成一个状态机方法最好,也可以用简单的switch实现。
下单接口的事务处理是关键。我当时写的时候是这样实现的:在OrderService.createOrder方法上加@Transactional,方法内做三件事:检查商品是否存在及上架状态、检查并扣减库存、创建订单主记录和订单明细、清空购物车对应项。任何一步异常都会整体回滚,保证数据一致性。
这里还要注意控制器的参数校验。接收订单提交的参数不能直接用一个巨大的实体类,最好用DTO对象。DTO里只包含商品id集合、收货信息,然后通过@Valid和@NotNull校验。这一点能体现你的分层设计能力,因为如果把HttpServletRequest直接传进Service层,那代码基本就废了。
4. 前端页面与交互实现
4.1 商城首页与分类筛选
Vue前端部分通常分为两个独立的子应用:商城前台和管理后台。整个项目如果是用Vue2写的,一般基于Vue CLI或Vite搭建,路由使用Vue Router,状态管理用Vuex或Pinia。商城首页的布局大概是顶部导航栏带搜索框,主体是轮播图、热销精品、分类商品列表。
商品列表页的核心是筛选条件组合:按分类id、按品牌、按价格区间、按关键字。这些条件传到后端时建议用Query参数拼接,后端Controller接收后动态拼SQL。这就要用到MyBatis或者MyBatis-Plus的动态SQL了。如果你是课设,直接用MyBatis-Plus的LambdaQueryWrapper最幸福,不用写一堆if判断去拼XML。比如:
java复制LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.isNotBlank(categoryId), Product::getCategoryId, categoryId)
.eq(brandId != null, Product::getBrandId, brandId)
.between(minPrice != null && maxPrice != null, Product::getPrice, minPrice, maxPrice)
.like(StringUtils.isNotBlank(keyword), Product::getName, keyword)
.eq(Product::getStatus, 1);
分页用MyBatis-Plus的Page<Product>,返回给前端时带上总数total和当前页记录列表records。前端用ElementUI的el-pagination组件就能轻松对齐。
商品详情页有一个小细节:SKU属性。手机通常有颜色、内存版本之分,比如某款手机有8GB+128GB和12GB+256GB两个版本,价格还不一样。如果项目时间紧,可以把这些规格以JSON字符串的形式存进商品表的一个字段里,前端解析后展示。如果做简化,也可以不区分SKU,一个手机型号就一款价格。毕设的数据库设计里用冗余字段或者子表都可以,但答辩时一定要能自圆其说。
4.2 后台管理面板与图表可视化
后台管理界面使用Vue加ElementUI是最省力的,布局左边菜单(el-menu),右侧内容区域(router-view)。菜单项包括:仪表盘、商品分类、商品管理、订单管理、用户管理。
仪表盘这个页面是展示能力的加分项,一般放一些统计卡片和图表,比如今日销售额、今日订单数、商品总数、用户总数,以及近7天销售趋势曲线。图表建议使用ECharts,在Vue里通过echarts的init方法挂载到div上。数据来源可以单独写一个统计接口,后端用SQL的DATE_FORMAT(create_time,'%Y-%m-%d')做分组查询,返回每天的销售汇总。
我曾经看到有同学的统计接口直接查全表然后在Java里循环求值,数据量小的时候还能忍,但效率很差。正确的做法是让SQL替你聚合,哪怕有索引问题,也比全表加载到内存强。这个习惯从课设开始养成,对你以后进公司写报表SQL很有帮助。
商品管理模块主要就是表格操作:分页查询、新增、编辑、上下架、删除。这里删除建议用逻辑删除,也就是给商品表加一个deleted字段,删除只是把字段置为1。因为商品一旦被订单引用,物理删除会导致订单明细里的商品信息变成孤岛。这种设计理念可以主动在项目说明文档里提一句。
4.3 axios封装与路由守卫
前端项目中axios的封装是必不可少的一环。不要在每个页面里都写一遍axios.get,而是建一个request.js工具,统一配置baseURL、超时时间、请求拦截器(从localStorage取token加进请求头),以及响应拦截器(处理code不为200的情况、后端的401未登录跳转)。
后台管理系统的路由守卫也很关键。由于管理后台必须登录且角色为管理员才能进入,所以在Vue Router的beforeEach守卫里检查token和用户角色:
javascript复制router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.meta.requiresAuth && !token) {
next('/login')
} else if (to.meta.requiresAdmin && localStorage.getItem('role') !== 'admin') {
next('/403')
} else {
next()
}
})
这里的role通常是在登录成功后存到localStorage里的。注意不能只在前端拦截,后端的拦截器也必须校验角色,否则懂技术的人可以直接调用后端接口绕过前端限制。前端拦截只是体验优化,后端拦截才是安全底线。
前台商城和后台管理如果放在同一个Vue工程里,可以用不同路径前缀区分,比如/admin/*下边的都是管理页面。但更干净的做法是拆成两个独立的前端项目,各自打包,用Nginx或后端静态资源分别发布。不过对毕设来说一个工程拆模块也行,关键是路由和权限做清楚。
5. 部署运行与学习建议
5.1 本机运行步骤与常见环境问题
这个项目拿下来以后,第一件事不是急着看代码,而是先让它跑起来。通常源码包会附带SQL脚本和README说明。运行步骤基本是:把SQL脚本导入MySQL;修改后端application.yml里的数据库用户名密码;启动后端SpringBoot项目,默认端口8080;进入前端目录执行npm install然后npm run serve,默认端口8081。浏览器访问前端地址,能打开页面就说明环境没问题。
我遇到过最多的环境坑是这三个。
第一个,MySQL版本导致的时区问题。在application.yml连接字符串后面加上serverTimezone=Asia/Shanghai,否则会报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized这种乱码错误。如果没有加useSSL=false的话,高版本MySQL也可能警告,顺手一起加上就行。
第二个,Maven依赖下载失败。很多同学用的SpringBoot版本是2.x,它要求JDK8或以上。如果本地JDK版本太高(比如17),某些旧版本依赖会起不来。最简单的解决办法是统一改用SpringBoot 2.7.18搭配JDK11,或者干脆用3.x的版本搭JDK17,不过3.x的代码兼容性需要测试。毕设环境建议别折腾,跟源码的JDK版本保持一致。
第三个,前端npm install速度慢或者报错。国内环境建议设置淘宝镜像源:npm config set registry https://registry.npmmirror.com。如果node-sass安装失败,大概率是node版本和node-sass版本不匹配,可以用sass替代node-sass,或者在package.json里锁定好版本后重新install。
5.2 提问率最高的几个坑
关于MyBatis-Plus的selectById返回null的问题。很多同学用@TableId注解时没有把数据库主键的type指定为ASSIGN_ID或AUTO,导致主键值解析不了。如果是自增主键,要把注解写成@TableId(type = IdType.AUTO)。否则插入数据后查询返回的id永远是0。
关于跨域问题。前后端分离部署必然遇到跨域请求。解决方案有几种:后端配置CORS、使用Nginx反向代理、或者在前端Vue的vue.config.js里配devServer代理。我推荐在开发环境用前端代理的方式,因为这样后端就不用写一堆跨域代码,而且也更贴近生产环境的部署方式。生产环境用Nginx,把/api请求转发到后端服务即可。
还有登录注册里的密码加密问题。如果你在数据库里直接看到明文密码,记得改成BCrypt加密再存。而且后端校验密码时不要拿加密后的字符串去equal,要用passwordEncoder.matches(rawPassword, encodedPassword)去比对,因为BCrypt每次加密的结果都不一样,直接equal永远是false。
订单模块中最容易被问倒的一个问题:一个用户同时下两单,商品库存只剩1件,会不会超卖?如果你没有做数据库层面的扣减保护,那答案就会很尴尬。正确代码大概是:
java复制boolean success = productMapper.reduceStock(productId, count);
if (!success) {
throw new BizException("库存不足");
}
对应的SQL就是一个带条件的更新:
sql复制UPDATE product SET stock = stock - #{count}
WHERE id = #{productId} AND stock >= #{count}
如果受影响行数为0,证明库存不够,直接抛出业务异常,事务回滚。这段代码建议你手写一遍,并理解它的原子性来源:数据库对单行更新会自动加锁。这是整个项目里最有含金量的三行代码。
最后说说个人体会。我见过很多同学拿到一个开源项目就开始改页面颜色、换轮播图,折腾了半天却连登录接口都没看懂。正确的学习方式应该是先画一张系统的功能图和数据库ER图,闭着眼睛能说出“用户点下单之后,后端在哪些表里做了什么操作”,再去动代码。答辩时老师不关心你的页面多好看,更在意的是你能不能讲清楚引用的表结构、缓存与事务、接口权限这些设计决策。
这个手机销售平台项目,难度曲线其实很平缓:你有两三个周末就能跑通全部功能,但如果你想在答辩时拿高分,一定要自己重写一遍订单模块和权限模块。手写之后,那些看起来平平无奇的CRUD项目,就成了真正属于你自己的东西。
