网上服装商城管理系统,算是我把SpringBoot+Vue这一套组合从“会写接口”到“能完整交付一个项目”的关键练手作品。这个项目后端用SpringBoot + Java + MySQL + MyBatis,前端用Vue全家桶,实现了用户注册登录、商品浏览搜索、购物车、下单流程、订单管理、后台商品管理等功能。对刚准备做毕设或者想熟悉前后端分离开发流程的同学来说,这是一套可以直接参考甚至跑起来的完整源码,不是零散Demo,而是完整业务闭环。
先说清楚它解决的问题:很多初学者写商城项目,容易出现前端是一套,后端是另一套,最后硬凑在一起对不上接口;或者只做好了商品展示,订单模块直接放弃。这个项目在设计之初就把“前后端分离、接口先行、状态明确”作为核心,把商品、用户、购物车、订单这条主链路全部打通。下面我从技术选型、数据库设计、后端实现、前端实现到部署排错,完整聊聊这个系统是怎么做出来的,以及实测中踩过哪些坑。
如果你正处于以下阶段:需要交一个课程设计或毕业设计,想了解真实项目里SpringBoot如何整合MyBatis,或者想知道Vue项目打包后怎么放进SpringBoot统一部署,这篇内容都能给你一个比较完整的参考路径。完整源码的骨架并不复杂,但把它讲透,能少走不少弯路。
1. 项目概述与系统目标
1.1 为什么选择SpringBoot+Vue组合
先聊选型。市面上的商城项目模板很多,有用JSP的,也有前后端完全分离的,但SpringBoot+Vue这套组合在校园项目和企业初期项目中尤其常见。SpringBoot解决了Spring繁琐的XML配置问题,用自动配置和starter依赖让一个Web服务几分钟就能跑起来;Vue则用组件化和响应式数据绑定把页面交互做得很轻快。两者结合,正好是“后端负责数据和业务规则,前端负责展示和交互”的清晰分工。
我选择这套组合还有一个现实原因:资料多、招人问得多。无论是Java面试还是前端面试,SpringBoot和Vue都属于高频考点,做完这个项目,之后去面Java开发岗聊项目经历,至少能说清楚“用户从注册到下单,这条链路里每一步数据是怎么流转的”。而且部署也简单,后端可以打成Jar包,前端可以打成静态文件放进SpringBoot,一个进程就能跑整个系统,对服务器资源不宽裕的场景非常友好。
1.2 系统核心角色与功能边界
整个商城系统分成两个角色:普通用户和后台管理员。用户端主要功能包括注册登录、商品分类浏览、关键词搜索、商品详情、加入购物车、下单、查看订单列表和订单状态。后台管理端则包括商品分类管理、商品上架下架、库存调整、订单发货、用户列表查看等。
功能边界一定要在设计初期划清楚,否则写着写着就会膨胀。比如用户端要不要做支付?我在这版源码里用“模拟支付”处理,下单后默认待发货,避免审单、对账这些复杂逻辑。后台要不要做权限细分?管理员直接用一个角色,访问后台接口时统一拦截。这样既保证了核心流程完整,又不至于让代码量失控。你要做毕设,也建议先划好这个边界,把“核心链路跑通”放在第一位,再考虑优惠券、秒杀这类扩展功能。
2. 技术栈选型与整体架构
2.1 后端:SpringBoot + MyBatis的组合逻辑
后端基础框架是SpringBoot 2.x + MyBatis + MySQL,额外用到了Lombok减少实体类样板代码、Hutool或Apache Commons处理字符串和集合、JWT做登录令牌。SpringBoot负责接收HTTP请求、参数校验、依赖注入和全局异常处理;MyBatis负责把Java方法映射成SQL语句,灵活度比JPA更高,适合我这种喜欢直接控制SQL的人。
有些同学喜欢用MyBatis-Plus,确实能省不少CRUD代码,但这版源码我保留的是纯MyBatis,原因是网上商城有大量动态查询和关联查询,纯XML能更清楚地看到每一条SQL。比如商品列表有分类ID、关键字、价格区间、上下架状态等多个筛选条件,MyBatis的动态SQL可以按需拼接where语句,执行计划更可控。真实业务里,SQL出问题往往是性能瓶颈最大的地方,所以把SQL握在自己手里更踏实。
2.2 前端:Vue + Element UI + Axios
前端用的Vue 2 + Vue Router + Vuex + Element UI。Vue 2在国内项目里存量很大,教程好找,生态稳定,选它不容易掉进新版本兼容的坑。Element UI提供了一套现成的后台管理组件,像表格、表单、对话框、分页器这些,直接拿来用,页面效果也算专业。
Axios用于发送HTTP请求,统一封装了请求拦截器和响应拦截器。请求拦截器里从localStorage取token,加到请求头;响应拦截器里判断HTTP状态码和业务状态码,如果登录过期就跳转到登录页。这套写法几乎每个前后端分离项目都用,值得记下来。另外,Vue Router路由表按模块拆分,商品页、购物车、订单中心、后台管理每个模块独立懒加载,初次加载体积更小。
2.3 后端包结构与接口设计
后端采用经典分层:controller负责接收请求参数和返回结果,service负责业务逻辑,mapper负责数据库操作,pojo里放实体类,dto放接收参数的传输对象,vo放返回给前端的视图对象。如果项目里遇到多个模块都要用的通用返回结构,就放在common包。
接口设计统一走RESTful风格,GET /api/product/list表示分页查询商品,POST /api/cart/add表示加入购物车,PUT /api/order/status表示更新订单状态。后端统一返回Result对象,结构是code、message、data三个字段。这样前端只用判断code是否为200,逻辑统一,避免了“有的接口返回JSON,有的接口直接返回字符串”这种混乱情况。前端配合request.js里的拦截器,基本不用在每个页面重复处理错误提示。
3. 数据库设计与核心表结构
3.1 数据建模的核心思路
数据库设计是这个项目最值得花时间的地方。我一般先画业务链路:用户注册后浏览商品,商品属于分类,用户把商品加入购物车,购物车中的条目最终生成订单,订单里包含多个订单项。所以最基本的表是:user、category、product、cart_item、order、order_item。另外还需要一张address表存收货地址,用来在下单时生成收货信息。
设计表的时候要特别注意关联关系。一个订单对应多个订单项,所以订单表和订单项表是“主表-子表”结构,订单项里存下单时的商品快照,包括商品名称、图片、单价、数量。为什么存快照?因为商品价格和名称后续可能会改,如果只关联商品ID,历史订单显示的数据可能和用户当时购买的不一致。所有需要保留历史状态的业务,都要做这种快照设计。
3.2 核心表结构与字段解析
用户表的主要字段有id、username、password、nickname、phone、avatar、role、create_time。密码不能存明文,我用了BCrypt加密,登录时比对加密后的哈希值。
商品分类表很简单:id、name、sort、status。商品表字段稍微多:id、category_id、title、cover、images、detail、price、stock、sales、status、create_time。images可以用JSON字符串存多张商品轮播图,查询时前端再解析;price用decimal(10,2),避免浮点数精度问题;stock用int,每次下单都做库存校验。购物车表字段是id、user_id、product_id、quantity、checked,用user_id + product_id建唯一索引,防止同一个商品重复添加。
3.3 订单状态流转和订单项设计
订单表字段要多一些:id、order_no、user_id、address_id、total_amount、status、pay_time、delivery_time、finish_time、create_time。这里order_no建议用“时间戳+随机数”生成,比如20250114120345加六位随机数,避免并发下重复。订单状态我用0待付款、1待发货、2待收货、3已完成、4已取消这几个值,前端根据状态值显示对应的操作按钮。
订单项表字段包括id、order_id、product_id、product_title、product_image、product_price、quantity。下单时在事务里做三件事:插入订单主表、批量插入订单项表、清空购物车对应条目。这里必须用数据库事务,任何一步失败都要整体回滚,否则会出现“订单创建了但购物车没清空”或者“库存扣了但订单没生成”的数据不一致问题。
4. 后端核心功能实现
4.1 用户注册登录与JWT拦截器
用户模块最核心的就是密码加密和身份校验。注册接口接收用户名和密码,先校验用户名是否已存在,存在则直接返回“用户名已被注册”,否则用BCrypt加密后写入数据库。登录接口根据用户名查出用户,用BCryptPasswordEncoder.matches()比对密码,比对成功就生成JWT令牌返回给前端。
JWT令牌里我只放userId和role,签名密钥配置在application.yml里。后端写一个LoginInterceptor拦截器,拦截所有以/api/**开头的请求,放行/api/user/login、/api/user/register、/api/product/**等公开接口,其余接口校验请求头里的token。有一个坑:前端请求时可能把token写成Authorization,后端也要在拦截器里从request.getHeader("Authorization")取,否则会一直提示未登录,排查半天还找不到原因。
4.2 商品分页查询与多条件动态SQL
商品列表页要支持分页、分类筛选、关键字搜索、价格区间,这是MyBatis动态SQL最典型的应用场景。我写一个ProductMapper.xml,核心SQL大概是:
<select id="selectPage" resultType="com.example.shop.pojo.Product"> select * from product <where> <if test="categoryId != null"> and category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> and title like concat('%', #{keyword}, '%') </if> <if test="minPrice != null"> and price >= #{minPrice} </if> <if test="maxPrice != null"> and price <= #{maxPrice} </if> and status = 1 </where> order by id desc limit #{offset}, #{pageSize} </select>这里status = 1是强制条件,保证前端只能看到上架商品。分页没有用PageHelper插件,而是手动传入offset和pageSize,虽然多写两行代码,但能看到很清晰的SQL逻辑。<where>标签会自动去掉多余的and,比直接用where 1=1更规范,也更容易写对。注意价格比较的>=和<=是XML中大于等于、小于等于的转义写法,漏掉转义启动时会报XML解析错误。
4.3 购物车与订单提交的事务控制
购物车模块相对简单,加入购物车时先查购物车表里有没有该用户和该商品的记录,有就累加数量,没有就新增。购物车里展示数据时要关联商品表,查出最新的商品标题、单价、图片和库存,前端让用户勾选需要结算的商品。
订单提交是整个后端逻辑最重的部分。我在OrderService里写了一个带@Transactional的方法,先校验下单的商品是否还有库存,然后算总价,生成订单号,插入订单和订单项,最后扣减库存并清空购物车。库存扣减的SQL要写成update product set stock = stock - #{quantity} where id = #{id} and stock >= #{quantity},这一句用数据库行锁防止超卖。如果你直接把库存查出来在Java里减,并发下一定会超卖,这个坑我测试时用JMeter压过,几十个并发就出了问题。
4.4 MyBatis的Mapper接口、缓存与踩坑
写Mapper接口时,方法参数多个时要加@Param注解,比如List<Product> selectPage(@Param("categoryId") Integer categoryId, @Param("keyword") String keyword);,不加的话MyBatis会提示Parameter 'xxx' not found。实体属性和表字段不一致时,要么开启驼峰映射map-underscore-to-camel-case: true,要么用resultMap显式映射,二选一即可。我建议两种都掌握,因为面试时候很可能会被问到。
关于MyBatis缓存:项目里我没开二级缓存。商城系统的商品库存和价格是实时数据,缓存稍不注意就会给用户展示过期信息。虽然一级缓存默认开启,但不同会话不共享,意义不大。如果以后要做高并发,更合适的办法是引入Redis缓存商品列表热点数据,MyBatis缓存就不要在订单这类写多场景里用了。这一块如果面试官问,你可以很自信地聊出来。
5. 前端页面与交互实现
5.1 Vue项目搭建与路由设计
前端我直接用Vue CLI创建项目,命令是vue create shop-web,选择Router和Vuex,后续再手动安装Element UI和Axios。路由设计上,用户访问首页、商品列表、商品详情、购物车、订单列表,后台管理单独放在/admin路径下,用嵌套路由把商品管理、分类管理、订单管理串起来。
路由守卫是前端必须做的校验。我在router.beforeEach里判断目标路由的meta.requiresAuth,如果为真就检查localStorage里的token,没有token就跳转登录页。后台管理路由再加一层meta.role === 'admin'的判断,普通用户无权限访问。注意用户登录后刷新页面,Vuex里的用户信息会丢失,所以持久化用户信息要用localStorage,刷新时再从本地读取并重新初始化Vuex。
5.2 商品列表与购物车交互
商品列表页用el-card配合图片懒加载展示商品,点击卡片跳转到详情页。搜索框输入关键字后,触发列表接口重新请求,分页组件绑定currentPage和pageSize,每次翻页都调用fetchList()方法。这里有个小细节:切换页码时要把滚动条滚回顶部,否则用户翻页后还停留在页面底部,体验很差。
加入购物车时,页面调POST /api/cart/add,成功后在顶部显示“已加入购物车”的轻提示。购物车页面的每个条目都可以修改数量、勾选/取消勾选、删除。结算按钮读取当前勾选的条目,把商品ID和数量提交给订单创建接口。前端要知道一件事:购物车里的数量不能超过库存,下单接口也会二次校验,所以前端要提前禁用超出部分,减少无效请求。
5.3 Axios统一封装与接口对接
前端所有请求统一走src/utils/request.js,这个文件里用Axios创建实例,设置baseURL为/api,超时时间10秒。请求拦截器里从localStorage取token并放到Header;响应拦截器里先判断HTTP状态码,200后再看后端返回的code字段。code不是200时,通过Element UI的Message.error弹出错误信息,同时把Promise reject掉,避免页面里到处写try catch。
接口对接过程中最烦的是日期格式。Java后端返回的LocalDateTime默认序列化成2025-01-14T12:30:00,这种格式前端展示起来不好看,我直接在实体类的日期字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8"),统一成中国时区的常用格式。还有一个协同问题:后端接口字段命名用驼峰,前端用下划线,两边经常对不上。我让后端统一返回驼峰JSON,前端全部按驼峰取字段,约定好了以后,联调成本立刻降下来。
6. 完整源码运行与部署指南
6.1 本地环境准备
运行这套源码前,先把环境准备好。JDK要求1.8以上,我推荐用JDK 8或者JDK 11,太新的版本可能会出现某些依赖兼容问题。MySQL我用5.7,也可以用8.0,但要注意8.0的驱动类名是com.mysql.cj.jdbc.Driver,连接串里要加serverTimezone=Asia/Shanghai,否则会报时区错误。
前端环境需要Node.js,我用的是14版本。如果你装的是Node 18以上,也可以跑,但Vue CLI 5对高版本Node的兼容性问题偶尔会出现,用了新版本Node可以先执行npm install看有没有报错,再决定是否降级。IDE方面后端用IDEA,前端用VS Code,两者都装好。数据库工具推荐Navicat或者DataGrip,导入SQL文件方便。
6.2 数据库初始化与配置修改
源码包里会有一个shop.sql文件,里面包含建库、建表和初始化数据。用Navicat新建一个数据库,字符集选utf8mb4,排序规则选utf8mb4_general_ci,然后直接运行SQL脚本。脚本里会默认创建管理员账号(admin)和测试用户,方便你登录测试。
后端配置在src/main/resources/application.yml里,需要修改的主要是数据源:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的数据库密码 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true另外JWT的secret和token过期时间也在这个文件里,测试阶段可以直接用默认值。改完密码后启动,如果控制台出现Tomcat started on port(s): 8080,说明后端已经跑起来了。
6.3 后端启动与接口验证
后端启动有两种方式。第一种是IDEA里直接运行主类Application.java,适合开发调试。第二种是打包后运行java -jar shop-admin.jar,适合部署。打包命令是mvn clean package -DskipTests,打包前确保测试用例不会影响构建,如果某个测试类纯属多余,可以加@SpringBootTest的排除配置或者直接删掉测试目录。
启动后可以用Postman或者Apifox验证接口。先调POST /api/user/login,拿到token后放到请求头,再调GET /api/product/list,看是否能返回商品分页数据。验证接口通过后再启动前端,避免前端一打开就全是网络错误。
6.4 前端启动与打包放进SpringBoot
前端开发环境启动很简单:
npm install npm run serve默认端口是8080,但后端的SpringBoot也占8080,所以我一般把@vue/cli服务端口改成8081,在vue.config.js里配置devServer.port = 8081,并设置proxy把/api代理到http://localhost:8080,这样开发时就不存在跨域问题。如果现在你用的是Vite创建的项目,对应配置写在vite.config.js里,逻辑类似。
正式部署时,执行npm run build,生成dist目录,里面是编译后的index.html和静态JS/CSS文件。后端为了让前端能一起跑,把dist里的所有文件复制到SpringBoot项目的src/main/resources/static目录下,再重新打Jar包。这样用户访问http://localhost:8080时,SpringBoot会直接返回前端页面。Vue打包放进SpringBoot的关键就是这步静态资源目录映射,很多同学卡在这里,原因多半是复制了dist文件夹本身,而不是复制里面的内容。
7. 常见问题与避坑实录
7.1 运行时典型问题速查表
我在跑项目过程中整理过一张问题速查表,可以直接对照排查。
| 现象 | 常见原因 | 解决办法 |
|---|---|---|
| 前端访问接口404 | 请求baseURL配置错误,或代理没生效 | 检查vue.config.js中proxy配置,重启前端服务 |
| 登录后接口提示未登录 | 拦截器没取到token,或token过期 | 查看请求Header里有没有Authorization字段 |
| 下单失败但购物车空了 | 事务没控制好,或异常被吞掉 | service方法加@Transactional(rollbackFor=Exception.class) |
| 中文乱码 | 数据库字符集或连接串编码问题 | 数据库设为utf8mb4,连接串加characterEncoding=utf8 |
| 商品列表查不到数据 | 接口库中有status=0的下架商品 | 列表SQL强制status=1,后台管理员可上下架 |
| 端口被占用 | 之前启动的实例没关掉 | 杀掉占用端口进程,或改SpringBoot端口 |
| 前端打包后刷新404 | Vue Router使用了history模式 | 改用hash模式,或后端配置forward到index.html |
| MySQL 8.0连接失败 | 驱动或时区配置问题 | 使用com.mysql.cj.jdbc.Driver,加serverTimezone |
这些基本都是项目里实测会遇到的高频问题。特别是前端打包后刷新404,因为Vue Router如果用了history模式,前端路由路径在服务端并不存在,刷新时后端找不到资源就只能返回404。简单方案是改用hash模式,URL里会多个#,不美观但省事。如果坚持history模式,就在SpringBoot里增加一个WebMvcConfigurer,把非静态资源的路径都转发到index.html。
7.2 我在实际开发中总结的几条经验
第一,数据库表设计一定要预留create_time和update_time两个字段,就算做的时候没用到,后续排查问题也能看到数据是什么时候变化的。最好再统一一个字段命名风格,不要有的表叫user_id,有的表叫userId,顺手写个命名规范文档很有用。
第二,后端返回结果要统一,尽量别让前端去猜。项目里所有接口都返回Result结构,即使异常也要返回一个标准JSON。如果出现空指针,全局异常处理器统一兜底,返回code=500和“服务器开小差了”这样的提示,避免前端拿到一长串堆栈信息,既难看又暴露内部实现。
第三,写购物车和订单时,要站在“数据最终一致”的角度思考。比如用户下单前先加购物车,购物车里的价格只是展示用,真正下单价要以订单接口校验后的商品价格为准。前端传过来的价格后端一律不信任,金额计算全部在后端重新计算,这是商城项目的基本安全底线。
第四,前后端接口要做好字段约束。比如数量字段必须传正数,价格为负数要拦截,String类型长度要做校验。我早期懒省事没做参数校验,测试时发现用户能通过接口直接改订单状态,后面补上权限校验和参数校验才安心。以后做任何系统,接口的第一道关口一定是参数校验,不能等着前端帮你挡。
最后说点实际体会
这个项目做完之后,我最大的感受是:写商城系统难的不是某个单独技术点,而是把各种细节串起来。SpringBoot和Vue本身都有很多“开箱即用”的东西,但真正到联调、部署、排查问题的时候,才会发现自己对HTTP状态码、数据库事务、JWT生效机制、前端路由模式这些基础知识的理解还不够扎实。建议你拿到源码后不要只跑起来看效果,而是跟着链路改一改代码,比如把商品表加个字段、给订单增加一个取消理由,改一次比看十遍效果都好。
最后再分享一个小技巧:本地开发时,前端代理跨域,后端接口写日志,两者配合调试效率会高很多。后端加一个简单的拦截器把请求路径、耗时、参数都打印出来,前端请求出错时,看后端日志比猜前端代码快得多。这个习惯我一直保留到现在,遇到线上问题也能快速定位。希望这份源码和踩坑记录能帮你少走点弯路。