☰
从零搭建SpringBoot+Vue商城系统:核心设计与部署全攻略
2026/10/20 14:49:04 网站建设 项目流程

自己动手把一套网上商城从零搭到能上线,这事听起来工程量大,实际上踩完一轮坑之后回头看,最值钱的不是那几万行代码,而是整个链路里每个环节你都知道它为什么这么设计。

这个项目用的是Java后端技术栈里最经典的组合:SpringBoot + MyBatis + MySQL,前端走Vue全家桶。整套系统分用户端和后台管理端两大块,覆盖了网上商城最常见的业务闭环:用户注册登录、商品浏览、购物车、下单结算、订单管理、后台的商品与分类维护、轮播图设置、订单处理等。如果你是正在做毕业设计、课程项目,或者想自己搞一套中小规模的电商系统做技术练习,这套路线和踩坑记录会很对胃口。我尽量把关键设计逻辑和实操难点都拆开聊,不只是贴结论。

1. 项目整体设计与核心模块拆解

1.1 需求还是要先理清楚,别急着写代码

很多人拿到“商城系统”这个题目,上来就建表、写接口,做到一半发现需求乱成一团。先花一小时把角色和流程理顺,后面省三天。

这套商城系统从角色上分,就三类人:

  • 普通用户(前台):注册登录、逛首页和分类、搜商品、看详情、加购物车、下单、付款模拟、确认收货、管理收货地址和个人信息。
  • 后台管理员:商品增删改查(含图片上传)、商品分类管理、轮播图维护、订单列表处理(发货、退款等简单状态流转)、用户管理。
  • 系统本身:核心是库存扣减、订单状态机、登录态校验、数据权限隔离。

用户端和管理端虽然是两套界面,但共用同一套后端接口。这种设计在真实项目中很常见,省事、一致性好,权限靠Spring Security或拦截器控制在路径层。

1.2 技术选型:这套方案的取舍思路

后端选择了SpringBoot + MyBatis + MySQL,前端是Vue + Element UI + Axios,认证用JWT,缓存视情况选用Redis(本项目可先用本地内存过渡)。

为什么是这套组合?

  • SpringBoot本人觉得最大的价值是“约定大于配置”,不需要搞一堆XML配置文件,内嵌Tomcat直接跑,项目启动非常清爽。
  • MyBatis相比JPA/Hibernate,SQL写起来更灵活可控。商城系统里订单查询、统计报表这类复杂查询,直接用SQL一把梭,性能也更直观。
  • 前端Vue生态成熟,Element UI组件库做后台管理界面基本是开箱即用,不用从零手搓表格、表单和弹窗。
  • JWT做无状态登录认证,前后端分离场景下比Session更适合,不需要额外维护服务端存储。

注意:如果你的设备性能一般,首次启动SpringBoot项目时Maven会下载大量依赖,建议提前配好国内镜像源,否则会卡到怀疑人生。

1.3 功能模块拆分

整个系统拆成8个模块,分工如下:

模块职责关键接口示例
用户模块注册、登录、个人信息、地址管理/api/user/login、/api/user/address
商品模块商品列表、详情、搜索、分类筛选/api/goods/list、/api/goods/detail/{id}
购物车模块加购、修改数量、删除、勾选结算/api/cart/add、/api/cart/update
订单模块创建订单、订单列表、状态流转、模拟支付/api/order/create、/api/order/pay
分类模块商品分类层级维护/api/category/tree
轮播图模块首页Banner配置/api/banner/list
后台管理模块商品管理、订单管理、用户管理、统计分析/api/admin/goods/save
文件上传模块商品图片等资源上传与静态映射/api/upload/image

如果你准备拿这个项目做毕设,建议把模块再收敛一点,优先保商品、购物车、订单、登录四个主链路,管理端做基础版够了,把时间省下来把核心代码吃透。

2. 数据库设计:表结构定得好,后面少改很多代码

2.1 核心表设计

商城系统的表数量一般不低于9张,核心表如下:

  • user(用户):id、username、password、nickname、avatar、phone、create_time
  • category(分类):id、name、parent_id、sort、icon
  • goods(商品):id、category_id、name、subtitle、main_image、detail、price、stock、status、sales
  • cart(购物车):id、user_id、goods_id、quantity、checked(是否勾选结算)
  • order(订单主表):id、order_no、user_id、total_price、pay_type、status、address_id、create_time、pay_time、deliver_time、finish_time
  • order_item(订单明细):id、order_id、goods_id、goods_name、goods_image、price、quantity
  • address(收货地址):id、user_id、name、phone、province、city、district、detail、is_default
  • banner(轮播图):id、image_url、link_url、sort、status
  • admin_user(管理员):id、username、password、role

2.2 关键设计说明

状态字段用int不用varchar

订单状态我用的是int,用数字表达状态,配合前端做映射。比如order表status字段:

  • 0:待付款
  • 1:待发货(已付款)
  • 2:待收货(已发货)
  • 3:已完成
  • 4:已取消

为什么不用字符串?一是存数字索引查询高效,二是状态机的判断写起来非常干净。当然代价就是阅读性差一些,所以记得在Java里定义一个订单状态常量类。

价格字段要注意精度

商品价格和订单总价,MySQL里用DECIMAL(10,2),Java实体对应BigDecimal,不是double。这块涉及金额精度,建议处理时先思考一下,否则会出现糟糕的浮点误差。

购物车表加check字段

这个字段是不是很常用,但实际体验很好的设计,测试后确定订单流程里会用,后面你可以自己决定。我用checked控制购物车中勾选的项,结算时只处理勾选商品,也能保留“稍后再买”的体验。

逻辑外键

尽量不在数据库层面强制加外键约束,而是靠应用层保证。原因很简单,强制外键在数据量大、分库分表时会成为性能瓶颈,也不利于后续扩展。当然,前提是你的代码逻辑要严谨。

2.3 建库建表的几个坑

  • 字符集要统一用utf8mb4,别用默认的utf8,emoji单独存得下。
  • 表前缀统一加上,比如t_user、t_goods,避免后续业务表重名。
  • 每个表都留create_time、update_time字段,后续做统计和排查问题时非常好用。
  • 数据库连接串加参数:useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,不然时间全部差8小时。

3. 后端开发与核心接口实现

3.1 后端分层结构与代码组织

SpringBoot项目我习惯按功能模块划分包,而不是按技术层堆包。这样基础代码结构如下:

com.example.mall ├── common (通用返回结果、异常处理、常量、工具类) ├── config (WebMvcConfig、拦截器注册、跨域配置) ├── controller (前台控制器) ├── admin (后台管理控制器) ├── service (业务接口) ├── service.impl (业务实现) ├── mapper (MyBatis接口) ├── entity (数据库实体) ├── vo (视图对象,如OrderVO、GoodsVO) ├── dto (接收前端参数的传输对象) ├── interceptor (JWT拦截器) └── MallApplication (启动类)

3.2 登录认证:JWT拦截器方案

登录流程:

  1. 前端提交用户名密码到/api/user/login
  2. 后端校验用户是否存在、密码是否正确(密码用MD5加盐或BCrypt加密存储)
  3. 校验通过,生成JWT,包含userId、username、过期时间,返回给前端
  4. 前端把token存到localStorage,每次请求在Header里带Authorization: Bearer token
  5. 后端拦截器统一解析token,解析失败或过期则返回401

拦截器实现逻辑:

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getMethod().equals("OPTIONS")) { return true; } String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); try { Claims claims = JwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); return true; } catch (Exception e) { // token无效 } } // 返回401,通知前端跳转登录 response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(JSON.toJSONString(Result.error(401, "未登录或登录已过期"))); return false; } }

注册拦截器:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/user/login", "/api/user/register", "/api/goods/**", "/api/banner/**", "/api/upload/**"); } }

提示:如果是毕设项目,可以考虑将权限控制简化为核心拦截,但合理写一遍有助于答辩时解释为什么最近几个接口无法访问。

3.3 商品模块接口实现

商品列表接口需要支持分页、分类筛选、关键字搜索、价格排序。设计时接收参数为pageNum、pageSize、categoryId、keyword、sort。用MyBatis手写动态SQL:

<select id="selectGoodsList" resultType="Goods"> select * from t_goods <where> <if test="categoryId != null and categoryId != 0"> and category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> and (name like concat('%', #{keyword}, '%') or subtitle like concat('%', #{keyword}, '%')) </if> and status = 1 </where> <if test="sort == 'price_asc'"> order by price asc </if> <if test="sort == 'price_desc'"> order by price desc </if> <if test="sort == 'sales_desc'"> order by sales desc </if> limit #{offset}, #{pageSize} </select>

商品详情接口要做几件额外的事:

  • 浏览量+1(简单UPDATE)
  • 查询商品所属分类名称
  • 补充轮播图等装修字段

3.4 购物车模块核心代码

购物车的操作要顺手,核心动作就5个:加购、删商品、改数量、勾选/取消、清空。

这里加购接口有个小细节——判断用户购物车里是否已有同款商品。有则数量累加,没有则新增记录,避免重复行数据:

public Result addToCart(Integer userId, Integer goodsId, Integer quantity) { Cart cart = cartMapper.selectByUserIdAndGoodsId(userId, goodsId); if (cart != null) { cart.setQuantity(cart.getQuantity() + quantity); cartMapper.updateById(cart); } else { Cart newCart = new Cart(); newCart.setUserId(userId); newCart.setGoodsId(goodsId); newCart.setQuantity(quantity); newCart.setChecked(1); cartMapper.insert(newCart); } return Result.success(); }

3.5 订单模块:重点难点

下单是整个系统的核心,步骤看起来简单,真正实现要注意的顺序很多:

  1. 从购物车获取勾选的商品列表
  2. 校验商品状态(是否下架)
  3. 计算总价(要以数据库实时价格为准,不是前端传过来的)
  4. 扣减库存(要判断库存充足)
  5. 生成唯一订单号
  6. 创建订单主记录和订单明细
  7. 删除购物车中已下单的商品

扣库存怎么处理并发?我建议先用update语句带条件判断:

int affectedRows = goodsMapper.reduceStock(goodsId, quantity); if (affectedRows == 0) { throw new BusinessException("库存不足"); }

这里的关键是SQL里带上stock >= quantity条件,利用数据库行锁保证原子性,减少并发超卖问题。不要先查库存再update,在并发下会有明显的超卖漏洞。

订单号生成建议:yyyyMMddHHmmss+ 6位随机数字,或者直接上雪花算法。对于中小项目,时间戳+随机数足够了。

public String generateOrderNo() { String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); int random = (int) ((Math.random() * 9 + 1) * 100000); return timestamp + random; }

3.6 后台管理相关接口

后台商品管理接口覆盖了商品的新增、编辑、上下架、删除。这里比较常见的坑是:删除商品是“假删除”,用status=0标记下架,避免有用户引用这个商品时出问题。

后台订单管理的主要操作是状态流转:发货后,状态从待发货变成待收货;用户确认收货后变成已完成。后台还可以直接取消未付款订单。

权限控制上,管理端接口统一走/api/admin/**,拦截器里单独校验admin身份,避免用户带普通token访问管理接口。

4. 前端页面与关键交互实现

4.1 前端项目结构

Vue + Element UI 的标准结构:

src ├── api (axios请求封装,按模块拆分js) ├── assets (静态资源) ├── components (公共组件,含各模块) ├── router (路由配置,区分前台/后台布局) ├── store (Vuex,存用户信息和token) ├── utils (request封装,工具函数) ├── views │ ├── home (首页、商品列表、详情、购物车、订单相关) │ ├── admin (后台管理页面) │ └── user (登录、注册、个人中心)

4.2 请求封装与拦截器

axios请求封装这块,统一处理三件事:

  • 请求头自动带上token
  • 响应统一解析,401时自动跳登录
  • 统一错误提示,避免每个接口重复写message
import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' 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 }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { Message.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } Message.error(error.response?.data?.message || '网络异常') return Promise.reject(error) } )

注意:在本地开发时需要给Vue项目配置代理转发,把/api开头请求转发到后端的8080端口,解决跨域问题。用Nginx部署时同样配置反向代理。

4.3 商品列表页的筛选与懒加载

商品列表页面主要用Element UI的el-card配合栅格布局实现。筛选区是分类下拉、关键字搜索框、排序切换。一般通过监听筛选条件变化重新请求列表。如果做无限滚动,可以用el-select配合懒加载,或者自定义滚动监听实现分页加载,减少首屏压力。

4.4 购物车页面设计

购物车页面核心是表格+数量步进器+勾选状态绑定。每次勾选、改数量,就把数据更新到Vuex里,同时唤醒后端更新操作。这样即使用户刷新,状态也是一致的。

结算栏要动态计算总金额,并且只统计勾选项。这里我踩过一个坑:如果直接把购物车所有商品总价提交到后端,那用户没勾选的商品也会被下单。所以务必是勾选的ID集合传给后端,后端以此为准。

4.5 订单结算与模拟支付

订单结算页面需要从购物车带上勾选数据跳转过来,展示商品清单、价格明细,让用户选择收货地址。地址管理是独立的模块,默认地址要支持用户“设为默认”。

支付环节,考虑到没有真实支付渠道,用模拟支付即可:订单详情页放【模拟支付】按钮,点击后把订单状态从待付款改成待发货,同时更新支付时间。如果对接真实支付,这里替换为调用支付接口并配置回调即可。

4.6 后台管理前端设计

后台管理端用经典的左侧菜单+右侧内容区布局。Element UI的el-menu做侧边栏,右侧用el-card包内容。路由通过嵌套路由实现:

{ path: '/admin', component: AdminLayout, redirect: '/admin/dashboard', children: [ { path: 'dashboard', component: Dashboard }, { path: 'goods', component: GoodsManage }, { path: 'goods/edit', component: GoodsEdit }, { path: 'category', component: CategoryManage }, { path: 'order', component: OrderManage }, { path: 'user', component: UserManage }, { path: 'banner', component: BannerManage } ] }

商品编辑页用表单+图片上传,Element UI的el-upload组件配好action地址,上传成功后将返回的URL拼到表单数据里。之后做编辑时回显图片,就用el-image加上fit: 'cover'来统一显示。

5. 项目联调中的经典问题与排查技巧

5.1 前端请求跨域报错

最经典的问题。方案有两个:

  1. Vue开发环境配proxy:在vue.config.js中:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }
  1. 后端配置全局CORS(适用于生产环境简单场景)。两个方案选其一,如果两个都配,开发时可能会遇到多次OPTIONS预检请求干扰,容易让人摸不着头脑。

5.2 前端拿到的时间比数据库晚8小时

这个基本就是时区问题。解决方案是数据库连接URL加serverTimezone=Asia/Shanghai,同时Jackson配置统一日期格式:

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.time-zone=GMT+8

5.3 刷新页面后登录状态丢失

因为token存在localStorage,刷新不会丢。但Vuex里的用户信息会丢,需要在mounted钩子里重新通过token请求用户信息接口。或者直接用vuex-persist插件把user信息持久化到sessionStorage,一劳永逸。

5.4 下单时库存被扣成负数

核心原因就是查询和更新不是原子操作。解决方式是:

update t_goods set stock = stock - #{quantity} where id = #{goodsId} and stock >= #{quantity}

如果更新影响行数返回0,说明库存不足,直接抛异常回滚事务。还要记得在Service方法上加上@Transactional,保证扣库存和创建订单是同一个事务。

5.5 图片上传成功后访问不到

检查三点:物理路径是否可写、映射路径是否配置正确、Nginx是否转发图片请求。SpringBoot静态资源映射配置:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + uploadPath + "/"); } }

在Linux服务器部署时,注意上传目录的权限,之前我在CentOS上就是目录权限不足导致上传一直报错,排查了很久。

5.6 MyBatis查询返回字段为null

这个通常是实体类字段与数据库列名映射问题。确认MyBatis的map-underscore-to-camel-case配置是否开启:

mybatis: configuration: map-underscore-to-camel-case: true

如果没开,create_time字段就映射不到实体的createTime。另一类常见问题是查询返回的字段名与实体对不上,用as别名解决即可。

5.7 JWT过期时间设置太短

有些项目为了演示方便把token过期时间设为30分钟,结果演示到一半就得重新登录,现场容易被动。建议开发阶段设置24小时,演示前再调整成2小时,既能满足效果又不会暴露安全底线。

6. 调试心得:把时间花在刀刃上

建议第一次接触这个项目的人,不要一上来就全局看代码。围绕主流程看:

用户注册登录 → 浏览商品 → 加购物车 → 创建订单 → 支付 → 后台发货 → 用户收货

整个闭环跑通了,系统的核心架构、表关联、状态流转就全清楚了。再看管理端的商品、分类维护,基本是常规的CRUD套路,举一反三就够。

还有一点我深有体会:直接看前后端接口配合很吃力。建议同时打开浏览器开发者工具和后端控制台日志。把请求路径、参数、返回码和SQL输出对照着看,效率翻倍。

比如下单请求成功但库存没变化,浏览器Network里看到下单接口返回的orderNo没问题,但后端SQL日志里却没出现update库存的语句,就能判断是事务问题还是代码逻辑漏了一步。这种排查方式比猜快得多。

7. 部署上线:从本地跑起来到服务器

7.1 本地启动步骤

  1. 创建MySQL数据库mall,执行项目中提供的mall.sql脚本
  2. 修改application.yml里的数据库账号密码
  3. 启动后端:mvn spring-boot:run,看到“Started MallApplication”即成功
  4. 前端:npm install→npm run serve
  5. 浏览器访问http://localhost:8081,后端端口8080

7.2 服务器部署要点

前端构建:npm run build,产出dist目录,里面是纯静态文件,放到Nginx或直接丢进服务器目录即可。

Nginx配置示例:

server { listen 80; server_name yourdomain.com; # 前端静态文件 location / { root /usr/share/nginx/html/dist; index index.html; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 上传图片访问 location /upload/ { proxy_pass http://127.0.0.1:8080/upload/; } }

这里有一个很关键的配置:try_files,这行是为了解决前端路由history模式的刷新404问题。如果没用history模式而用默认的hash模式,这行可以不要。

后端部署用java -jar mall.jar运行,注意jar包名改成自己的。生产环境建议用systemd服务管理,日志输出到固定文件,方便排查问题。如果你的套餐配置不够高,内存分配用-Xmx256m起步试试,SpringBoot默认开太大容易崩。

7.3 数据初始化建议

项目自带的SQL里一般会有几类初始数据。建议至少包括:

  • 1个管理员账号:admin / admin123(提醒上线前改掉)
  • 10个左右的测试商品数据,最好覆盖3个分类
  • 首页轮播图数据,否则前端首页banner区空白

数据库里有样例数据非常省事,跑通全流程用一半时间,剩下时间都花在理解代码上。如果你打算自己造数据,注意图片地址要换成统一的占位图服务,否则前端全是裂图。

8. 真实项目演示时的一些技巧

如果是答辩或演示场景,提前准备一条完整演示路径,避免现场手忙脚乱:

  1. 清空浏览器缓存,从登录页开始
  2. 用测试账号登录用户端
  3. 重点展示首页轮播图和商品分类
  4. 搜索某个商品,展示筛选效果
  5. 加购物车 → 去结算 → 选地址 → 模拟支付 → 查看订单状态
  6. 切换到管理员账号登录,展示后台商品管理编辑操作
  7. 到订单管理页面把已支付订单发货

这条流程走下来,功能覆盖完整,评委或观众能直观感受到系统闭环的完成度。同时建议提前把商品数据造好,把演示用的商品名和图片提前换成方便展示的内容,比如“测试商品A”这类一眼能看清名称的数据,避免混乱。

还有一个经验:演示时登录管理员账号用独立的浏览器窗口或隐私模式,避免和用户端账号互相挤下线。JWT虽然是无状态的,但前端localStorage共享时,两个账号来回切换极易出现token覆盖,切回来又得重新登录。

9. 写在最后的个人体会

这套商城系统做下来,我觉得最有价值的不是“会写了增删改查”,而是明白了几个真实项目中的道理:

第一,数据设计是一切的基础。表结构没设计好,后面每个功能都难受;表结构合理了,CRUD自然顺畅,扩展也轻松。

第二,订单和库存的并发问题是电商系统的分水岭。哪怕是模拟项目,把数据库行锁的扣库存方式写对,比随便糊一个逻辑强很多,面试或答辩时也是加分项。

第三,前后端联调是最耗耐心的环节。约定好接口格式、错误码、鉴权方式,能省掉一大半无效沟通。

如果你打算在这个基础上继续玩下去,有几个不错的扩展方向很值得试试:接入Redis做Session加速和首页商品缓存、引入Elasticsearch做商品搜索、对接第三方支付沙箱环境、增加秒杀逻辑(限流+消息队列)。每个方向都能让这个项目再上一个台阶。

还是那句话,先把主流程吃透,再想着加花活。商城系统不神秘,跑通一遍就全明白了。

提示:我用的这套项目最终完整源码在本地仓库里有备份,也上传到了网盘一份,需要的同学自己去取,就不在这里放链接了。注意自己核对一下MySQL版本和JDK版本的兼容性,我用的是JDK8,SpringBoot 2.7.x,MySQL 5.7,这套组合最稳。如果有人用JDK11以上跑SpringBoot 2.7,大概率会有版本兼容问题需要调整依赖,别硬推到项目头上。

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

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

立即咨询