☰
SpringBoot+Vue+MyBatis+MySQL商城管理系统开发实录
2026/10/12 5:50:33 网站建设 项目流程

企业级商城管理系统开发实录:SpringBoot+Vue+MyBatis+MySQL完整方案拆解

最近在做一个商城管理系统,前后端分离架构,技术栈选了SpringBoot+Vue+MyBatis+MySQL。很多人一听到“商城管理系统”,第一反应就是网上那些满大街的简易demo,界面简陋、逻辑混乱、连个事务都不开。但真正到了企业级或者毕业设计要冲高分的时候,你会发现商城管理系统并不是“增删改查的堆砌”,而是从前端交互、接口设计、数据库建模到权限控制、订单流转都有完整闭环的一整套系统工程。

这篇文章就把我这次从零搭建一套含前台商城和后台管理系统的完整过程拆开来讲。内容涵盖技术选型逻辑、数据库设计、Mapper层写法、权限与订单状态机、前端Vue工程搭建、前后端联调以及部署上线的关键步骤。不管你是正在做Java课程设计的学生,还是准备用SpringBoot+Vue起步接触企业级项目开发的初级工程师,这篇文章都能帮你少走很多弯路。

1. 项目整体设计思路与架构选型

1.1 为什么选SpringBoot+Vue+MyBatis+MySQL这套组合

整套技术栈看起来“传统”,甚至有人会觉得不够新潮。但我要说的是,对于商城系统这类以业务逻辑和数据处理为核心的项目,稳定性、可维护性和社区生态的成熟度远比技术新颖更重要。

SpringBoot负责后端服务,它的价值在于大幅度简化了Spring的配置成本。SpringBoot的自动配置机制可以把项目从“写一堆XML配置文件”的泥潭里解放出来,Maven坐标一拉,启动类一写,内嵌Tomcat直接启动,开发效率肉眼可见地提升。另一个很实际的点是,SpringBoot天然集成了Spring MVC,RESTful接口的开发几乎就是写几个注解的事,而且事务管理、参数校验、全局异常处理这些企业级必备能力都有成熟的解决方案。

Vue作为前端框架,最大的优势是组件化和响应式数据绑定。商城后台的页面结构很固定,无非是导航栏、表格、表单、弹窗、统计图这一套,用Vue组件化之后,公共的表格组件、分页组件、表单校验逻辑可以抽象复用,联调阶段能省下不少时间。Vue生态里的Vue Router和Axios也是前后端分离项目的标配,路由守卫配合登录token做页面访问控制,实现起来非常顺。

MyBatis的定位是“轻量级的持久层框架”。有人说MyBatis不如MyBatis-Plus方便,但手写Mapper映射有一个不可替代的优势:SQL可控性。商城系统的报表查询、多表关联、条件筛选往往带着复杂的动态SQL,MyBatis的XML映射文件可以非常精细地控制SQL的生成逻辑,尤其是<if>、<foreach>这类动态标签,写起来比LambdaQueryWrapper更直观,也更好排查SQL性能问题。

MySQL则是目前中小型项目最稳妥的数据库选择。商城的数据模型以关系型为主,商品、分类、订单、用户之间的关联性很强,MySQL的事务支持、索引机制以及丰富的运维工具,足够支撑一个日活几千上万的商城系统。

总结一下这套组合的实际收益:开发效率高、排错成本低、资料多到不可能找不到解决方案,而且SpringBoot+Vue的招聘市场需求量摆在那里,学会这套栈对你今后的职业发展也有直接帮助。

1.2 功能模块划分:前台+后台双端闭环

做商城管理系统,最忌讳的就是只做后台的马马虎虎一点,或者只做前台的下单流程,却把后台的订单管理和数据统计全砍掉。真正完整的商城系统一定是“前台+后台”双端闭环。

前台面向C端用户,功能包括:用户注册登录、首页轮播图、商品分类导航、商品列表搜索、商品详情展示、购物车、订单结算、支付模拟、个人中心订单查询等。

后台面向管理员和运营人员,功能包括:后台登录与权限校验、仪表盘数据统计(商品数、订单数、销售额、用户数)、商品管理(上架下架、库存修改、分类管理)、订单管理(订单列表、发货、状态修改)、用户管理(用户列表、账号状态),以及一些辅助功能如轮播图配置、公告管理。

这套结构拆分之后,前后端接口的边界就非常明确了,基本就是围绕商品、用户、订单、购物车这几个核心领域在做资源管理。

2. 数据库设计与MyBatis持久层实战

2.1 核心表结构与关系建模

商城的数据库设计,我用了8张核心表做支撑,分别是用户表、商品分类表、商品表、购物车表、订单表、订单明细表、轮播图表和后台管理员表。有些系统会再加一张地址表,但考虑到演示场景,我先把地址合并到用户表里,字段设计足够清晰就好。

商品分类和商品是一对多关系,分类表的id作为商品表的categoryId外键。商品表和商品图片我建议单独拆一张商品图片表,可以做多图轮播展示,也可以在一张表里用逗号分隔JSON数组的方式存图片路径。如果你追求简单,逗号分隔是够用的,但如果想让后台支持自定义图文详情,拆表是更合理的方案。

这里给出核心建表SQL的参考片段:

CREATE TABLE `tb_product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(200) NOT NULL COMMENT '商品名称', `subtitle` varchar(500) DEFAULT NULL COMMENT '副标题', `main_image` varchar(500) DEFAULT NULL COMMENT '主图', `detail` text COMMENT '商品详情', `price` decimal(10,2) NOT NULL COMMENT '价格', `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存', `status` tinyint(4) DEFAULT '1' COMMENT '状态:1上架 2下架', `category_id` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

订单表和订单明细表的设计是整个系统中的关键点,主订单表负责订单总金额、订单状态、收货信息、用户id等汇总信息,订单明细表则记录每一个商品的单价、数量、小计。为什么一定要拆明细表?因为订单生成后再查商品表价格是不现实的,历史价格必须保存在明细表里,否则后续商品涨降价,用户订单数据就会对不上。

CREATE TABLE `tb_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号', `user_id` bigint(20) NOT NULL, `total_price` decimal(10,2) NOT NULL, `status` tinyint(4) NOT NULL DEFAULT '0', `receiver_name` varchar(50) NOT NULL, `receiver_phone` varchar(20) DEFAULT NULL, `receiver_address` varchar(500) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2.2 Mapper层怎么封装最顺手

MyBatis的Mapper文件,我分三个层次来写。

第一层是基础的单表CRUD,直接用XML里写简单的select * from tb_product where id = #{id},或者用注解方式简化。这一层没什么技术含量,但要注意所有SQL语句都必须用到#{}预编译占位符,禁止直接字符串拼接${},否则就是SQL注入的活靶子。有些场景比如动态排序字段确实要用到${},那就必须做白名单校验。

第二层是带条件的动态查询,比如商品列表的后台筛选,可能要组合商品名、分类、价格区间、上下架状态等多个条件。

<select id="selectProductPage" resultType="map" parameterType="map"> SELECT * FROM tb_product WHERE 1=1 <if test="title != null and title != ''"> AND title LIKE CONCAT('%', #{title}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="status != null"> AND status = #{status} </if> ORDER BY id DESC LIMIT #{offset}, #{pageSize} </select>

WHERE 1=1的写法在很多老项目里能看到,虽然不推荐,但MyBatis动态SQL没有更简洁的替代方案。你当然可以用<where>标签处理,干净、正规,还能自动去掉多余的AND,实际开发中我建议用<where>。

第三层是多表关联查询和统计报表。比如后台仪表盘的销售统计:

<select id="selectSalesTrend" resultType="map"> SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS date, SUM(total_price) AS amount, COUNT(*) AS orderCount FROM tb_order WHERE status IN (2, 3) AND create_time >= #{startTime} GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') ORDER BY date </select>

2.3 事务与缓存:订单场景下到底怎么用

商城系统中,最容易出问题的就是“下单”这个动作。扣库存、生成订单、锁定优惠这些操作必须保证原子性。

我在下单接口上直接加了@Transactional注解,它的作用是让方法内所有数据库操作共享同一个事务连接,任何一个步骤抛异常,整体回滚。有人说Spring事务是在代理层实现的,自己调用自己的内部方法会失效,这个坑确实真实存在。我在开发中就遇到过:一个内部公共方法把下单逻辑拆成了多个私有方法,解决了。

@Override @Transactional(rollbackFor = Exception.class) public OrderVO createOrder(Long userId, Long productId, Integer count) { // 1. 查询商品,校验状态和库存 // 2. 扣减库存(带条件更新,防止超卖) // 3. 生成订单记录和订单明细 // 4. 清空购物车条目 }

库存字段扣减不要用先查后改的方式,而是直接在UPDATE语句里带条件:

UPDATE tb_product SET stock = stock - #{count} WHERE id = #{productId} AND stock >= #{count}

如果影响行数为0,说明库存不足,直接抛业务异常,这样可以避免并发超卖。

缓存方面,商城的热点数据主要是商品详情和分类导航。这类数据读多写少,适合用Redis做缓存,但如果你不想引入额外中间件,MyBatis自带的二级缓存也可以用来做简单缓存。不过我用下来的感受是,二级缓存在分布式环境下需要非常小心,比如不同节点间的缓存同步问题。简单的演示项目直接用一级缓存就行,真正的性能瓶颈通常在主键查询和SQL带宽上,先把索引和SQL优化做好,再考虑缓存不迟。

3. SpringBoot后端核心实现

3.1 分层架构与统一响应封装

整个后端我采用了经典的五层结构:Controller层、Service层、Mapper层、实体层、DTO/VO层。严格限制Controller层只能做参数接收和结果封装,业务逻辑全部下沉到Service层。

每一层都有它的职责,就像餐厅的分工。Controller层是迎宾员,只负责接单、传菜,不负责厨房里的事;Service层是厨师,做菜的顺序和火候全靠它;Mapper层是采购员,去数据库那里拿食材;实体对象是菜品模型,负责把数据准确地带到需要的地方。

为什么要这么分?原因很实际:如果业务逻辑写在Controller里,一旦后期接口复用或逻辑调整,改起来就是噩梦。我第一次做商城项目时就吃过这个亏,登录逻辑散落在多个Controller里,后来要统一加验证码校验,改了一下午。

为了给前端一个规范的数据交互格式,我封装了统一的Result<T>响应类,包含code、message和data三个字段。code为0时表示成功,非0时表示各种业务错误码。

public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(0); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }

配合全局异常处理器@RestControllerAdvice,可以捕获统一异常类型,返回友好提示,避免把一堆堆栈信息直接甩到前端。

3.2 登录鉴权与角色权限控制

商城有两种角色:普通用户和管理员。普通用户从前台登录,管理员从后台登录。两边的登录态我用了同一个JWT方案,但通过token里的角色标识区分权限范围。

JWT的实现逻辑不复杂:用户登录成功后,服务端生成一个包含userId和role的token,返回给前端;前端后续每次请求都在请求头里带上Authorization: Bearer <token>;服务端通过拦截器解析token,确认用户身份。

SpringBoot的后端实现我用了一个AuthInterceptor拦截器,配合WebMvcConfigurer注册拦截路径:

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); // 解析token逻辑,无效则抛出401异常 return true; } }

拦截器里要做的事包括:从header中取出token、校验签名和有效期、从Redis或JWT中取出用户信息放入ThreadLocal、判断该接口需要的角色是否匹配。后台管理接口统一以/admin/**开头,所以拦截器对/admin/**路径会额外校验管理员角色。

这里补充一个我在开发中踩过的坑:前后端分离项目里,拦截器放行路径配置错了,前端请求一直报CORS跨域错误。排查到最后才发现是所有请求都被拦截了,没走到CORS过滤器逻辑,最后还是在前端代理和后端跨域配置两边做了协同处理。

3.3 订单状态的流转引擎设计

订单状态的演进,是整个系统里最需要动脑筋的地方,因为它不是简单的一个字段,而是有严格的状态流转规则。

我的订单状态定义如下:0待支付、1已支付待发货、2已发货、3已完成、4已取消。用户操作和后台操作分别对应不同的状态迁移路径:用户下单后status=0;用户支付或后台标记支付后status=1;后台发货后status=2;用户确认收货或后台标记完成后status=3;用户在待支付状态取消则status=4。

状态流转实现,最简单可靠的方式是写一个状态机工具类,或者叫“状态流转校验器”,每个状态变更操作都先校验当前状态是否允许跳转到目标状态:

public class OrderStatusFlow { private static final Map<Integer, List<Integer>> FLOW = new HashMap<>(); static { FLOW.put(0, Arrays.asList(1, 4)); // 待支付 -> 已支付/已取消 FLOW.put(1, Arrays.asList(2, 3)); // 已支付 -> 已发货/已完成 FLOW.put(2, Arrays.asList(3)); // 已发货 -> 已完成 } public static boolean canChange(Integer currentStatus, Integer targetStatus) { List<Integer> allowed = FLOW.get(currentStatus); return allowed != null && allowed.contains(targetStatus); } }

这个方法的好处是,任何状态下都清晰知道能跳到哪些状态,不允许的跳转直接返回抛出“订单状态更新异常”,不会出现一个订单从“已取消”跳到“已完成”这种离谱情况。

4. Vue前端与联调细节

4.1 页面结构与组件化拆分

Vue前端工程按模块划分成两套:一套是商城前台(用户端),一套是管理后台(管理员端)。虽然同在一个项目里,但通过路由区分入口。

管理后台我用了经典的布局方案:左侧菜单栏、顶部导航栏、中间主体内容区。菜单项根据角色动态生成,管理员能看到全部菜单,普通用户登录后台只能看到部分基础页面。这个动态菜单逻辑可以用Vue Router的addRoute方法实现,登录成功后根据权限从后端拉菜单配置,再动态挂载路由。

前台页面分了首页、分类页、商品详情页、购物车页、订单确认页、个人中心、登录注册页。商品列表的筛选功能用Vue的computed计算属性处理非常方便:前端选中的分类、价格范围、排序方式作为响应式数据,计算属性实时算出筛选后的商品列表。

Vue中有一个很常用的技巧叫“组件通信的优雅处理”。商品列表和商品详情是不同组件,但购物车里的小红点数字要在多个页面上同步更新。我引入了一个简单的全局事件总线,或者用Vuex/Pinia管理购物车状态。商城这类项目,我建议数据放在Pinia里管理,因为购物车状态是跨页面共享的,刷新页面后还要从localStorage或接口恢复。

4.2 API请求封装与token管理

前端的Axios请求封装,直接决定了联调效率。我封装了一个统一的request.js模块,做了三件重要的事:

第一,请求拦截器统一注入token。登录成功后把token存到localStorage,每次请求从localStorage取出来塞进请求头。这里要注意的是token过期后,服务端返回401,前端需要统一处理:跳转登录页、清除本地登录态。

第二,响应拦截器统一处理错误码。后端返回的Result结构里code非0就是业务错误,直接弹出ElMessage提示。网络异常、超时、服务器500等情况,也统一在响应拦截器里处理,避免每个页面都去try-catch。

第三,请求超时和重试机制。商品搜索、列表这种接口响应慢一点可以接受,但下单和支付接口的稳定性很重要,我把下单接口的超时时间调到了10秒,并且加了简单的重试提示。

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 !== 0) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response && error.response.status === 401) { router.push('/login') localStorage.removeItem('token') } return Promise.reject(error) } )

4.3 前后端联调中那些容易翻车的点

联调阶段,我至少遇到过三类典型问题,每类都值得记录一下。

第一类是跨域问题。开发环境下我用Vite或Webpack DevServer的proxy代理解决,后端设置跨域放行;生产环境通过Nginx统一反向代理,避免跨域。前后端接口路径必须约定好,全部走/api前缀,Nginx一个location /api规则就全部转发到后端服务。

第二类是数据格式不一致。后端返回的日期字段是2025-01-15 10:30:00,前端用的是时间戳或者YYYY-MM-DD,展示的时候经常变成一串乱码或者NaN。后来后端统一把日期格式化成DateTimeFormatter的字符串,前端只需要渲染就行。

第三类是空值和类型转换问题。比如后端返回的BigDecimal价格是199.00,前端数值组件能正常处理,但表格排序时把价格当成字符串排序,出现9排在199后面这种奇怪现象。解决方法是前端把价格字段转成Number类型,再用排序函数处理。

5. 部署上线与问题排查实录

5.1 从本地到服务器:部署的关键步骤

本地开发跑起来容易,真正上线部署才是考验。我这次的部署方案是:前端打包成静态文件,由Nginx托管;后端打包成jar包,由Systemd或Docker管理;MySQL单独部署,并做好定期备份。

前端构建这一步,执行npm run build后会生成dist目录,里面是压缩后的静态资源。把dist目录下的文件上传到服务器的/usr/share/nginx/html目录,再配置Nginx:

server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

try_files $uri $uri/ /index.html;这一行很关键。因为Vue是单页应用,刷新某个路由时比如刷新/admin/product,Nginx需要把请求回退到index.html,否则会出现页面404。

后端jar包打包后,直接用java -jar启动只能作为临时方案。生产环境我推荐用Systemd管理进程,崩溃自动重启,日志统一收集:

$ sudo systemctl start mall-server $ sudo systemctl enable mall-server

5.2 典型问题速查表

我把整个开发过程中遇到的频率最高的几个问题整理成了速查表,给遇到同样坑的人一个参考:

问题现象原因分析排查解决方法
前端请求接口全部404接口前缀不匹配或Nginx转发路径配错检查/api前缀、Nginx location规则、后端context-path配置
登录后刷新页面又跳到登录页前端路由守卫误判,token读取逻辑有问题检查路由守卫里token的判断时机,确认localStorage里是否真的存了token
下单后库存没扣减事务没生效或SQL条件写错检查@Transactional是否加在public方法上,检查UPDATE语句的stock >= count条件
商品图片不显示图片路径是相对路径,域名错误图片统一存绝对路径或配置Nginx静态资源映射
数据库连接超时连接池参数过小适当调整HikariCP的maximum-pool-size和connection-timeout
Vue打包体积过大引入的第三方库太多按需引入Element Plus,关闭不必要的依赖,使用路由懒加载

MySQL连接池的参数,我用的是SpringBoot默认的HikariCP,建议从小做大调整:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mall_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: xxxxxx hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000

记住,serverTimezone=Asia/Shanghai这个参数一定要加,否则本地没问题,服务器上直接时差8小时。

5.3 项目可以怎么继续扩展

一套完整的商城系统只要底子打好了,后续扩展功能其实非常顺:

支付模块,可以在下单后跳转到支付页面,接入支付宝沙箱环境或微信支付测试号,前端用二维码展示,后端通过回调通知确认支付结果。秒杀/限时活动,把商品表的库存字段独立成库存表,引入Redis预扣库存,再配合消息队列做异步扣减,这套流程可以很好地控制并发流量。数据统计,仪表盘页面增加ECharts图表,按天/周/月展示销售额、订单量趋势,这个对运营来说非常实用。

如果还有精力,可以把商品SKU概念引入进来,把“一个商品对应单一价格库存”升级为“一个商品对应多个规格(颜色、尺寸、版本)”,这会涉及规格表、SKU表和购物车明细表的结构调整。

6. 写在最后的几句实话

开发这套商城系统,我最大的感受是:学习SpringBoot和Vue,单独学一天就能上手,但把两者串起来完成一个真实业务场景,中间要踩的坑远比想象中多。从数据库设计到事务控制,从JWT鉴权到状态机设计,每个环节都不是孤立的,而是彼此咬合的整体。

我给还在做类似项目的人一个建议:先跑通最核心的下单链路,再回头逐步补齐管理员后台的各种细节。因为下单这条链路涉及了商品查询、用户登录、库存操作、订单生成、事务回滚等几乎所有核心知识,只要这条链路通了,整个项目的骨架就立住了,后续加功能只是往骨架上添血肉。

另外,源码不要只停留在“能跑”的层面,多去想想“为什么这样写”。比如为什么订单要拆明细表、为什么要用JWT而不用session、为什么扣库存要用条件更新。把这些为什么想明白了,这套项目才真正变成了你的东西。

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

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

立即咨询