SpringBoot+Vue网上服装商城:从订单状态机到部署避坑全解析
2026/9/14 5:31:17 网站建设 项目流程

简介:这是一套基于Spring Boot、Vue、MyBatis Plus和MySQL构建的网上服装商城系统完整源码,适用于Java方向的毕业设计、课程设计,也适合正在学习前后端分离开发的人员作为工程范例。资源主要解决商城类Web项目从后台管理、商品与用户信息维护到前端展示的整体实现问题,配套文档中包含需求分析、系统设计以及MySQL、Vue、B/S架构、ElementUI等技术介绍。整个压缩包共790个文件,约28.64MB,覆盖117个Java源码、62个Vue组件、157个JavaScript脚本、CSS样式、HTML页面、SVG/PNG图标图片、Maven配置、启动脚本以及docx/pdf说明文档等,结构清晰,便于按目录查找与复用。已有140人学习下载。利用这些源码和素材,可以快速搭建一个可运行的服装商城项目,观察前后端交互与数据库设计细节,也可在此基础上进行功能扩展,或作为论文撰写和答辩演示的实例支撑。

1. 网上服装商城的选型逻辑:为什么是SpringBoot+Vue而不是别的

一套能跑通的网上服装商城系统,放在毕业设计或简历项目里,最怕的不是功能少,而是评审问一句“为什么这么选”就卡壳。这个基于SpringBoot、Vue、MySQL 5.7、MyBatisPlus的商城项目,胜在技术栈足够主流:后端用SpringBoot 2.x简化配置,前端用Vue 2 + ElementUI做后台管理界面,数据层用MyBatisPlus省掉大量XML,整个项目还带上了1-install.bat2-run.bat3-build.bat三个脚本,双击就能装依赖、起服务、打前端包,对本地演示特别友好。它解决的典型问题是:商品上架、用户注册登录、购物车、下单结算、订单列表这些电商闭环里最核心的CRUD和状态流转。适合正在做Java方向毕业设计、或者想快速搭一个前后端分离Web商城练手的人。与其把时间耗在环境搭建上,不如先把这条链路的坑摸清楚。

2. 数据库设计与MyBatisPlus映射:先把订单状态机立住

2.1 用户、商品、订单三张核心表怎么建模

商城系统的表通常不止三张,但用户、商品、订单是骨架。实际工程里还会拆出购物车表、订单明细表、分类表。我一般先把“订单主表 + 订单明细表”这种一对多关系建出来,因为后续结算、库存扣减都依赖这个结构。

CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录名', `password` varchar(100) NOT NULL COMMENT 'BCrypt或MD5加盐', `nickname` varchar(50) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL, `price` decimal(10,2) NOT NULL, `stock` int(11) NOT NULL DEFAULT 0, `cover_url` varchar(255) DEFAULT NULL, `status` tinyint(4) DEFAULT 1 COMMENT '1上架 0下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '业务订单号', `user_id` bigint(20) NOT NULL, `total_amount` decimal(10,2) NOT NULL, `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已发货 3已完成 4已取消', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里的关键不是建表语法,而是order表为什么用order_no而不是直接用自增主键暴露给前端。自增主键会被爬虫遍历,业务订单号还能承载时间信息和分库分表的路由键。status字段注释一定要写清楚,订单状态机是后续所有逻辑判断的源头。如果项目里用了MyBatisPlus,实体类上记得加@TableName("order"),因为order是MySQL关键字,不加注解容易在生成SQL时报语法错误。

2.2 MyBatisPlus的LambdaQueryWrapper与分页配置

MyBatisPlus在这里主要承担单表CRUD和条件构造。不要只写QueryWrapper,有条件时优先用LambdaQueryWrapper,字段名写错了编译期就能发现。下面这段是商品列表分页查询的常见写法,Page对象会返回总条数和当前页数据。

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; public IPage<Product> pageProducts(int pageNum, int pageSize, String keyword, Integer status) { Page<Product> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(keyword), Product::getName, keyword) .eq(status != null, Product::getStatus, status) .orderByDesc(Product::getCreateTime); return productMapper.selectPage(page, wrapper); }

like第一个参数是boolean,条件为false时自动忽略这个条件,这样就不用手动拼SQL或写一堆if。分页插件需要在配置类里注册MybatisPlusInterceptor,并且指定PaginationInnerInterceptor,数据库类型要是DbType.MYSQL,否则分页SQL会生成成SQL Server或Oracle的方言,运行时报错。

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(100L); interceptor.addInnerInterceptor(pagination); return interceptor; } }

setMaxLimit(100L)用来防止一次性查全表,演示环境一般够用。这里还有个容易被忽略的坑:如果自己手写了XML里的selectPage,返回类型必须是IPage,并且Mapper接口方法的第一参数必须是Page,否则SQL会被当成普通查询执行,分页失效。

3. 后端接口与权限控制:从登录鉴权到购物车结算

3.1 JWT登录态设计与拦截器

网上服装商城必须控制谁才能下订单。会话管理最常见方案是JWT,无状态、前端存Token、后端只负责验签。登录接口用usernamepassword查库,密码用BCrypt校验,成功后生成Token,把userId写进claims。

String token = Jwts.builder() .setSubject(user.getId().toString()) .claim("username", user.getUsername()) .setExpiration(new Date(System.currentTimeMillis() + 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();

secretKey不要硬编码在业务代码里,放到application.ymlcustom.jwt.secret配置项,部署时再通过环境变量覆盖。拦截器里解析请求头Authorization,去掉Bearer前缀后校验,如果Token过期或签名不对,直接返回401,并写一个统一的Result.fail(401, "登录已过期")

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } String auth = request.getHeader("Authorization"); if (auth == null || !auth.startsWith("Bearer ")) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}"); return false; } String token = auth.substring(7); try { Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody(); request.setAttribute("userId", Long.parseLong(claims.getSubject())); return true; } catch (Exception e) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"Token无效\"}"); return false; } } }

拦截器注册时要注意路径匹配,/api/user/login/api/product/list这种公开接口必须放行,/api/cart/**/api/order/**要拦截。如果项目里既有后台管理接口又有C端接口,最好拆成/admin/**/api/**两套前缀,拦截规则清晰很多。

3.2 购物车与订单提交的事务处理

购物车本质是user_id + product_id + quantity的映射表。下单时不能只插订单表,要同时扣库存、清购物车、写订单明细,这个过程必须在一个事务里。常见错误是每个Mapper操作单独提交,库存扣成功了订单插入失败,数据对不上。

@Transactional(rollbackFor = Exception.class) public Long createOrder(Long userId, List<CartItemDTO> cartItems) { BigDecimal total = BigDecimal.ZERO; List<OrderItem> orderItems = new ArrayList<>(); for (CartItemDTO item : cartItems) { Product product = productMapper.selectById(item.getProductId()); if (product == null || product.getStatus() != 1) { throw new BizException("商品不可购买"); } product.setStock(product.getStock() - item.getQuantity()); productMapper.updateById(product); OrderItem orderItem = new OrderItem(); orderItem.setProductId(product.getId()); orderItem.setQuantity(item.getQuantity()); orderItem.setPrice(product.getPrice()); orderItems.add(orderItem); total = total.add(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(0); orderMapper.insert(order); // 批量插入明细略 return order.getId(); }

注意@Transactional默认只在RuntimeException时回滚,如果抛的是自定义的BizException且它继承自Exception,必须显式写rollbackFor = Exception.class。库存扣减这里用的乐观思想是“先查再改”,并发量不大没问题;真想严格控库存,得用UPDATE product SET stock = stock - #{n} WHERE id = #{id} AND stock >= #{n},把判断下推到SQL里。事务里别做远程调用或耗时操作,否则连接池容易被占满。

3.3 控制层参数校验与统一返回体

商城接口的入参校验如果靠手写if,代码会非常零散。用Spring Validation的@Validated把规则声明在DTO字段上,可以减少很多模板代码。

public class LoginDTO { @NotBlank(message = "用户名不能为空") private String username; @NotBlank(message = "密码不能为空") @Size(min = 6, max = 20, message = "密码长度6-20位") private String password; // getter/setter }

控制器里加@Validated,校验失败时Spring会抛MethodArgumentNotValidException,在@RestControllerAdvice里捕获并转成统一格式{"code":400,"msg":"密码长度6-20位"}。前端Axios响应拦截器读到code===400就能直接弹消息,不用关心具体是哪个字段失败。统一返回体的泛型设计建议用Result<T>,包含codemsgdata三个字段。成功的code用200,业务失败用400或自定义的5001,认证失败用401,系统异常用500,这样前端可以根据code做统一跳转或提示。

4. Vue+ElementUI前端联调:路由守卫、Axios封装与商品渲染

4.1 Axios拦截器怎么塞Token

前端用Vue 2 + ElementUI时,最常见的做法是把Axios实例单独封装成一个request.js,在request拦截器里从localStorage拿Token,然后加到请求头。

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 }, error => { return Promise.reject(error) }) service.interceptors.response.use(response => { const res = response.data if (res.code === 401) { localStorage.removeItem('token') router.push('/login') return Promise.reject(new Error(res.msg)) } if (res.code !== 200) { ElementUI.Message.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) } return res.data }, error => { ElementUI.Message.error('网络异常') return Promise.reject(error) }) export default service

这里有个容易踩的坑:如果后端用的是ResponseBodyAdvice做全局统一包装,那么部分接口比如/api/file/upload可能返回的不是标准结构,响应拦截器就会误判。我会在request.js里加一个raw参数,上传文件时用service.post(url, formData, { headers: {'Content-Type': 'multipart/form-data'}, raw: true }),然后在响应拦截器里判断response.config.raw是否存在,存在就直接返回整个response。

4.2 商品列表与购物车数量联动

商品列表页通常需要展示封面图、价格、库存、加入购物车按钮。ElementUI的el-cardel-image就能搭出网格布局。数据加载在created里调用productPage接口,分页用el-pagination组件。

fetchProducts() { this.loading = true productApi.page({ pageNum: this.pageNum, pageSize: this.pageSize, keyword: this.keyword, status: 1 }).then(data => { this.products = data.records this.total = data.total }).finally(() => { this.loading = false }) }

加入购物车时要避免直接操作商品列表里的stock字段,因为后端已扣库存,前端本地减数量会导致刷新后不一致。我会维护一个Map<productId, count>,点击加号只更新这个Map,购物车角标由Map的值求和得到。提交订单时把Map转成CartItemDTO数组发给后端。这样商品列表、购物车抽屉、结算页的数据源是同一个Map,不会出现“购物车里2件,列表里还剩1件”的错位。

路由守卫在Vue Router里这样写:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) } else { next() } })

对“我的订单”“购物车”页面加上meta: { requiresAuth: true },登录后查询参数里的redirect回跳,体验比直接跳首页好很多。

4.3 生产环境构建与Nginx部署

前后端分离项目在本地联调时用proxy解决跨域,部署时用Nginx把/api转发到后端服务。vue.config.js里开发环境配置如下:

devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } }

生产环境构建后,dist目录下的静态文件放在Nginx的html/shop目录,反向代理配置:

server { listen 80; server_name shop.example.com; location /api/ { proxy_pass http://127.0.0.1:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { root /usr/share/nginx/html/shop; index index.html; try_files $uri $uri/ /index.html; } }

try_files必须有,否则刷新/order/detail/123页面时Nginx返回404,因为Vue Router用的是history模式。另外注意proxy_pass后面的斜杠,proxy_pass http://127.0.0.1:8081/;会把/api/order/list转成/order/list,如果后端接口本来就没有/api前缀,这样配就正好。

5. 避开毕业设计答辩的坑:初始化脚本、打包脚本与演示数据

5.1 install/run/build三个脚本到底做了什么

项目自带的三个bat脚本,本质是把Maven和前端构建流程固化下来。1-install.bat核心是执行mvn clean install,把SpringBoot的依赖拉下来,顺便把前端npm install也跑一遍。2-run.bat启动SpringBoot的jar包或直接mvn spring-boot:run,我一般建议打成jar再跑,避免IDE环境差异。3-build.bat重点在npm run build生成前端dist目录,再把dist复制到后端src/main/resources/static下,这样可以直接用SpringBoot内置Tomcat访问静态页面,省去单独配Nginx的麻烦。

@echo off echo 开始安装依赖... call mvn clean install -DskipTests call npm install --registry=https://registry.npm.taobao.org echo 安装完成 pause

注意这里npm install用了淘宝镜像,如果你在内网环境,registry地址可能不通,需要改成公司私有仓库地址。还有-DskipTests只是跳过测试执行,不会跳过测试代码编译,如果测试类里有编译错误,mvn clean install照样失败。确认项目里没有杂七杂八的单元测试后,建议用-Dmaven.test.skip=true彻底跳过测试编译。

5.2 常见运行失败排查清单

我见过很多次明明代码没问题,但本地就是跑不起来的案例,大部分集中在MySQL版本、JDK版本、端口占用三件事上。用一个表格把高频问题列出来,答辩前自检一遍:

现象可能原因排查/解决
启动报Access denied for userMySQL密码和application.yml不一致检查spring.datasource.password,注意有没有空格
前端请求接口404后端运行端口不是8081或前端proxy配错看后端控制台启动端口,netstat -ano查占用
数据库表不存在SQL脚本没执行用Navicat/SQLyog执行项目里sql目录下的脚本
商品图片加载不出来图片路径是相对路径,缺少file静态资源映射配置spring.web.resources.static-locations或把图片放static/upload
Invalid bound statementMapper接口和XML没有对应检查mapper-locations配置和@MapperScan

这里还要提一个跟JDK1.8配合的坑:SpringBoot 2.3之后的版本如果用了Java 8,某些新特性比如List.of会直接编译失败,因为List.of要求JDK9以上。这个项目既然写明JDK1.8,就尽量别用高于SpringBoot 2.3的版本,或者在IDEA里把Project Structure、Maven Compiler的编码都设为UTF-8和JDK1.8,避免生成乱码和版本冲突。

5.3 给答辩加分的演示路径

演示时不要上来就点商品列表,要有脚本感。我的习惯是先展示数据库:打开SQLyog,展开order表,清空订单数据,展示product表的库存字段。然后启动项目,注册一个新用户,登录后选一件库存为5的商品,加入购物车,下单。下单成功后立刻切到数据库看order表和order_item表的数据,重点展示order_no生成规则和status=0。接着模拟用户支付失败或取消订单,回数据库看状态变化。这样的路径能让答辩老师看到数据流闭环,而不是只看到前端页面上几个按钮。

还有一个小技巧:在product表里故意把某件商品的stock设为0,前端列表里这件商品应显示“已售罄”且不可点击。这段逻辑虽然简单,但很能体现你对边界情况的考虑。我在前端列表里是这样判断的:

<el-button :disabled="item.stock === 0" @click="addToCart(item)"> {{ item.stock === 0 ? '已售罄' : '加入购物车' }} </el-button>

对应后端在下单时再校验一次stock是否充足,避免前端绕过按钮直接调用接口。最后把构建好的jar包和前端dist产物路径写到答辩PPT里,评委问“这个项目怎么部署”,你能直接说清楚从代码到产物的完整链路,这比背概念实用得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询