☰
SpringBoot+Vue+MyBatis+MySQL:电商后台管理系统实战全解析
2026/10/14 3:48:47 网站建设 项目流程

1. 项目概述与技术选型思路

记得去年年中那会儿,我接手了一个典型的"帮忙看看"需求:某公司的内部电商管理系统要推倒重做,老系统还是JSP那一套,每次加个字段都要改半天,订单模块和新上线的商品审核流程完全跟不上运营节奏。当时给我的要求很明确——换一套前后端分离的架构,数据库用MySQL,ORM用MyBatis,前端用Vue,后端用SpringBoot,也就是现在标题里这套组合。

这个项目表面上看就是一个常见的电商后台管理系统,但真正做起来之后,里面涉及的东西远比"增删改查"要复杂得多。商品管理、订单流转、会员等级、库存扣减、权限控制、数据统计,每一块单独拎出来都能写不少文章。这篇文章我就以这个实战项目为线索,把SpringBoot + Vue + MyBatis + MySQL这套全栈组合从选型到落地的完整过程拆开讲清楚,包括我踩过的坑、后来优化掉的代码、以及那些文档里不会写清楚但实际开发中特别重要的细节。

1.1 系统要解决的业务痛点

先说业务背景。这类电商管理系统,本质上就是给运营和客服人员用的"操作后台",一般包含商品上下架、分类管理、库存调整、订单处理、会员查询、数据报表几个大模块。表面上看起来功能差别不大,但每个电商公司的业务侧重点不一样,有的注重订单拆单逻辑,有的注重多平台商品同步,有的注重分销体系。我们这次要做的系统,核心需求集中在三个方面:一是商品信息的多维度管理(包括规格、图片、上下架状态);二是订单从支付成功到发货完成的闭环处理;三是基于角色的后台权限控制,不能让客服误改商品价格。

老系统最让人头疼的地方就是页面和后端逻辑耦合严重,改一个按钮要重新部署整个应用,而且数据库表设计也比较混乱,订单表和商品表之间靠一堆冗余字段硬关联。这些问题直接推动了新方案的选择:前后端分离,接口化,数据层重新设计。

1.2 技术栈选型背后的取舍

技术选型这块,SpringBoot + Vue + MyBatis + MySQL这套组合现在看起来是"标准答案",但在真正落地的时候,每个选择背后都要想清楚为什么。

SpringBoot的优势不用多说,它把Spring生态里那些复杂的配置自动搞定,内嵌Tomcat让部署变得极其简单,打一个Jar包扔到服务器上就能跑。这在中小型项目中特别实用,团队不需要专门配一个运维来管理应用服务器。

Vue这块,我当时对比过React和Vue。团队成员对Vue的熟练度更高是一个原因,另一个原因是Vue的生态对后台管理系统太友好了——Element UI这类组件库几乎是专门为后台界面设计的,表格、表单、弹窗、分页这些高频组件开箱即用,开发效率能提升一大截。

MyBatis的决定就更有意思了。很多人一听到MyBatis就觉得"手写SQL麻烦",但电商系统的SQL复杂度其实很高,特别是订单统计、报表查询这类需要灵活拼接条件的场景。MyBatis的xml映射文件让我能精确控制每一条SQL语句,直接优化JOIN和索引,这种可控性比全自动的ORM强得多。

MySQL作为存储层是顺理成章的选择,成本低、社区成熟、稳定可靠。这套组合看起来"传统",但恰恰是这种成熟稳定,让项目能在一个可控的时间周期内稳定交付上线。

2. 后端核心实现:SpringBoot + MyBatis 的落地细节

后端是整个系统的中枢,从数据库表设计到接口开发,每一个环节都要考虑清楚。这一章我按照实际的开发顺序来拆解:先建表、再写映射、最后实现业务接口。

2.1 数据库设计与MyBatis映射

数据库设计是第一步,也是决定后续开发顺不顺的关键。电商管理系统涉及到的基础表有用户表、商品表、分类表、订单表、订单明细表、购物车表。这些表之间的关系,说白了就是用户一对多订单,订单一对多订单明细,商品与分类多对一。这里有一个我反复强调的设计习惯——订单金额和商品相关的关键字段,必须在订单表里冗余一份快照。

比如商品价格可能会经常调整,如果订单明细表只存了商品ID而没有存当时的价格快照,等三个月后再查历史订单,显示的金额就会跟用户实际支付金额对不上。所以我在订单明细表里冗余了商品名称、商品主图、下单时的单价这三个字段。这是电商系统中特别重要的一个设计思路,用空间换准确性。

MyBatis映射这一层,我没有用MyBatis-Plus这类增强工具,而是直接用原生MyBatis。每个实体类对应一个Mapper接口和一个xml文件。一开始定义了一个BaseResultMap,把表字段和实体属性的映射关系统一管理起来,后续在所有查询中复用这个ResultMap,避免每次写SQL都重复映射列名。

商品列表这种带多条件筛选的场景,是MyBatis最擅长处理的。通过<where>标签和<if>标签动态拼接条件,避免了在Java代码里拼SQL字符串那种又丑又容易出SQL注入问题的做法:

<select id="selectProductList" resultMap="ProductResultMap"> SELECT * FROM product <where> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND (name LIKE CONCAT('%', #{keyword}, '%') OR subtitle LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select>

提示:MyBatis的#{}预编译能有效防止SQL注入,千万不要图省事用${}拼接参数。特别是排序字段这种动态列名,如果必须用${},一定要做白名单校验,只允许传入预定义的几个字段名。

2.2 登录鉴权与接口安全管理

管理后台的接口不能谁都能调,登录鉴权是必须的。我使用的是JWT方案,流程很简单:用户提交账号密码,后端校验通过后签发一个有效期为24小时的Token,前端每次请求都把Token放在请求头里,后端通过拦截器统一校验。

这里有一个很多初学者容易忽略的问题:SpringBoot拦截器只拦Controller层的请求,但静态资源和放行接口要配置到位。我当时就踩过一个坑——登录接口本身也被拦截器拦了,导致前端无论怎么调都返回"未登录"。解决方案是在WebMvcConfigurer配置里,明确排除登录、注册这类接口的路径匹配。

密码存储这块,不管项目多小,都不要明文存密码。我用的是BCrypt加密,Spring Security的crypto包里就有现成的类,每次校验时用BCryptPasswordEncoder.matches()比对即可。BCrypt的特点是每次加密结果都不一样,即使两个用户密码相同,存进数据库的密文也不同,能有效防范彩虹表攻击。

JWT的密钥管理也是一个细节。开发环境可以写在配置文件里,但生产环境必须通过环境变量注入,避免密钥写死导致的安全隐患:

@RestController @RequestMapping("/api/auth") public class AuthController { @PostMapping("/login") public Result login(@RequestBody LoginRequest request) { User user = userService.getByUsername(request.getUsername()); if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) { return Result.error("用户名或密码错误"); } String token = JwtUtil.createToken(user.getId(), user.getUsername()); return Result.success(new LoginResponse(token, user)); } }

关于JWT的失效问题也要提前想清楚,用户改密码之后,旧Token理论上还是有效的,直到过期。要处理这个情况,一个简单的方案是在Redis里维护一个Token黑名单,改密时把旧Token加入黑名单。我当时为了控制复杂度,选择了缩短Token有效期(8小时)的方式,同时前端在收到401响应后自动跳转到登录页,让用户重新登录,算是用体验换安全的折中方案。

2.3 商品与订单的核心业务逻辑

商品管理和订单处理是电商系统的两大命脉,这两块业务逻辑如果没写明白,后面上线准出事。

商品管理的核心是上下架和库存调整。我设计了一个统一的商品Service层,把商品主表和商品SKU表(不同规格对应不同库存和价格)放在一个事务里操作。比如运营在后台把某个商品从"上架"改为"下架",同时要判断该商品是否存在未处理的订单,如果有未发货订单,通常不允许直接下架,或者需要弹出一个二次确认。这个"状态机"思维在电商系统里特别重要,每个状态能跳转到哪些状态、跳转条件是什么,要在设计阶段就列出清单来。

订单处理这块,我按操作视角拆分成两条链路。用户视角是:购物车中提交订单→生成待支付订单→模拟支付回调→订单变为待发货→后台发货→用户确认收货→订单完成。后台视角是:订单列表筛选→查看订单详情→发货操作→处理售后。我做了个订单状态字段,用数字表示:0待支付、1待发货、2已发货、3已完成、4已取消,每次状态变更都在订单操作记录表里写一条日志。这招特别重要,后面如果出现纠纷,翻记录表就能定位到是谁在什么时间做了什么操作。

事务控制是订单处理的重中之重。库存扣减和订单创建必须在同一个事务里执行,否则用户在抢购的时候,很容易出现订单创建了但库存没扣成功,超卖就是这么来的。我当时的实现是:在Service方法上标注@Transactional,执行顺序是先扣库存(UPDATE product_sku SET stock = stock - #{count} WHERE id = #{skuId} AND stock >= #{count}),再创建订单。注意这个SQL里带了stock >= #{count}的校验条件,如果影响行数为0,说明库存不够,直接抛出异常回滚事务,用数据库行锁天然解决了并发超卖问题,没有用复杂的分布式锁方案。

注意:不要为了省事把@Transactional加在Controller方法上,事务是Service层的职责。Controller只负责参数接收和响应封装,事务边界要由Service来控制,这样才方便在事务内进行多个DAO操作,也方便单元测试。

3. 前端Vue实现:后台管理界面的搭建思路

前端这块,Vue的后台管理界面开发效率很关键。我先说项目结构和整体的页面规划,然后重点讲请求封装和几个核心页面的实现细节。

3.1 Vue项目结构与页面规划

一个成熟的后台管理系统,前端项目结构应该是模块化的,不能所有路由都堆在app.vue里。我的做法是:用Vue Router的模块化配置,按功能域划分路由文件;视图组件统一放在views目录下,按业务模块分文件夹;公共组件放components目录;API请求全部放在api目录下,每个模块一个js文件。

整个系统的页面规划,我梳理成了五个一级菜单:

  • 仪表盘:展示今日订单数、销售额、热门商品Top10等统计卡片
  • 商品管理:商品列表、添加/编辑商品、分类管理、品牌管理
  • 订单管理:订单列表、订单详情、发货操作入口
  • 会员管理:会员列表、会员详情、等级设置
  • 系统设置:管理员账号管理、角色权限分配、操作日志

这五个模块基本覆盖了一个电商管理后台的全部日常操作场景。Element UI的el-container搭建整体布局,左侧是el-menu菜单,右侧是router-view主内容区,顶部放用户信息和面包屑导航。

3.2 Axios请求封装与状态管理

Axios请求封装这个环节,很多刚开始做前端的朋友经常会忽略,觉得每个页面直接import axios然后调就行。实际上,如果不在请求层做统一处理,后面排查问题和改鉴权逻辑的时候会被折磨到怀疑人生。

我在api目录下的request.js里做了一层封装,核心做了三件事:

第一,统一baseURL。开发环境用/api作为前缀配合Vite的代理转发到后端,生产环境改为后端真实地址。这样前端代码里不需要到处写IP和端口。

第二,请求拦截器统一附加Token。从localStorage里取出登录时存的Token,放到请求头Authorization里,所有接口一次性搞定:

service.interceptors.request.use(config => { const token = localStorage.getItem('admin_token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config })

第三,响应拦截器统一处理错误码。后端约定返回格式是{code, message, data},code为200表示成功。拦截器里判断如果code不是200,弹出错误提示;如果code是401,说明Token过期,清除本地Token并跳转登录页。这样一来,每个业务页面调用接口时只需要处理成功逻辑,错误处理完全收敛到一层。

状态管理我用了Vuex,但只存了用户信息和菜单权限这类全局共享的数据。商品列表、订单详情这类业务数据没有往Vuex里放,原因是在组件内用ref定义,配合接口刷新就够用了。放太多东西进Vuex反而容易造成数据不一致的坑。

3.3 商品列表、订单管理页面的交互细节

商品列表页是后台最常用的页面,交互设计直接关系到运营的工作效率。我用Element UI的el-table展示数据,el-pagination做分页,顶部的筛选栏包括商品名称搜索、分类下拉选择、状态选择(在售/下架/草稿),还有批量上下架操作按钮。

这里有一个细节值得说说:筛选条件变化时,页码要重置回第一页。因为用户可能在第三页筛选出了新条件,如果还停留在第三页,看到的数据会因为页码偏移而缺失。我用watch监听筛选条件,变化时把currentPage设为1,再重新请求列表。这个交互逻辑说小不小,很多后台系统都有这个Bug,一旦数据量大起来,用户会以为系统出问题了。

订单列表页同样用el-table,但多了展开行展示订单明细的功能。我在el-table-column里用type="expand"插槽展示该订单下的商品明细列表,方便客服快速查看用户买了哪些东西。订单操作列根据状态动态渲染按钮:待发货显示"发货"按钮,已发货显示"查看物流"按钮,已取消显示灰色禁用状态。这样的动态渲染是后台系统必须具备的交互能力。

另外一个常见的坑是前端对后端传来的金额处理。数据库里存的是Decimal类型,通过JSON序列化后字符串传给前端,前端展示时不做任何转换直接显示。曾经有同事图省事在字段上用了Double类型,结果出现金额精度丢失,99.99变成了99.9899999。我在后端统一用BigDecimal接收和返回金额,配合@JsonFormat(shape = JsonFormat.Shape.STRING)序列化策略,前端拿到的就是精确的字符串,展示和计算都不会出错。

4. 环境搭建、前后端联调与部署上线的实操记录

从本地开发到发布上线,中间有很多细节环节。这一章讲讲我完整的环境搭建过程和联调踩坑经验。

4.1 开发环境配置清单

开发环境我列个清单,都是实际开发中必须准备的:

  • JDK 1.8,不要用11,有些老版本依赖和云服务器上已有的环境之间容易出兼容问题
  • MySQL 5.7,生产环境也是5.7,保持版本一致,避免开发和生产之间出现语法差异
  • Maven 3.6+,用来管理后端依赖
  • Node.js 16版本,对应Vite 4和Vue 3的构建要求
  • Redis,开发环境的验证码缓存和Token黑名单会用到

后端配置文件application.yml里面,有几个地方需要特别注意。数据库连接配置中,MySQL的时区参数serverTimezone=Asia/Shanghai必须加上,否则默认的UTC时区会导致时间字段差8个小时。连接池用HikariCP,SpringBoot自带的,只需要配置几个参数:

spring: datasource: url: jdbc:mysql://localhost:3306/shop_admin?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你自己的密码 hikari: maximum-pool-size: 20 minimum-idle: 5 redis: host: localhost port: 6379

前端项目用Vite创建,npm create vite@latest选择Vue模板,然后安装vue-router、vuex、axios、element-ui。注意Element UI要用Element Plus版本,因为组件风格和API设计都面向Vue 3做了重构。

4.2 前后端联调与跨域问题处理

联调阶段最经典的问题是跨域。前后端分离的项目,前端开发时跑在8080端口,后端跑在8081端口,前端直接发请求肯定触发CORS。跨域有几种解决思路,开发环境最推荐用Vite的proxy代理,因为这样前端代码里的请求路径可以保持/api前缀,不需要区分环境:

// vite.config.js export default { server: { port: 8080, proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } } }

rewrite把/api前缀去掉,是为了配合后端的@RequestMapping路径。如果后端接口也是/api开头,那就不用rewrite。这个要根据自己团队约定的规范来,关键是前后端要一致。

生产环境的跨域问题发生在浏览器直接访问http://服务器IP:8081/api/xxx的时候。解决方式我推荐让前端构建产物交给后端一起服务。把Vue打包后的dist目录复制到SpringBoot的src/main/resources/static目录下,重新打包Jar。这样整个应用只有一个服务端口,不走跨域通道。对于内部管理系统,这个方案最省心,无需在Nginx里配置转发。

还有一个容易踩的坑,就是前后端参数格式不一致。比如后端用@RequestBody接收JSON对象,前端用Content-Type: application/json发送,这没问题。但如果后端接口接收的是@RequestParam,前端就必须用params方式传递。我当时在开发商品列表接口时用了GET /api/products?pageNum=1&pageSize=10&keyword=xxx,前端Axios调用时参数对象放在config的params属性里,Vue模板中需要确保字段名和值类型完全对应。

4.3 服务器部署与配置优化

部署阶段,我的目标是尽量简化运维。生产环境用的是阿里云ECS,配置不高,2核4G,MySQL和Redis直接装在云服务器上,没有用云上单独的数据库实例,成本控制优先。

SpringBoot应用打成Jar包后,用nohup java -jar shop-admin.jar > logs/console.log 2>&1 &方式启动。这是最粗糙但最实用的部署方式,没有引入systemd脚本,适合这种规模的项目。配合阿里巴巴开源的Arthas工具做线上问题排查,定位CPU使用率高、慢接口等问题非常方便。

JVM参数这块,根据2G内存的分配情况,我设置的是-Xms512m -Xmx1024m -XX:+UseG1GC。G1垃圾回收器在JDK 8以上都是推荐的,可以让垃圾回收停顿时间更可控。

部署完成后,数据库还需要做定期备份。写了一个简单的Shell脚本,用mysqldump在每天凌晨2点将整个shop_admin库导出,然后压缩成gz格式,保留最近7天的备份文件:

#!/bin/bash BACKUP_DIR=/data/backup/mysql DATETIME=$(date +%Y%m%d%H%M) mysqldump -uroot -p密码 shop_admin > $BACKUP_DIR/shop_admin_$DATETIME.sql gzip $BACKUP_DIR/shop_admin_$DATETIME.sql find $BACKUP_DIR -name "*.sql.gz" -mtime +7 -delete

这个脚本用crontab定时执行,省心又可靠。

5. 踩坑记录与问题排查实录

最后分享几个我开发这个系统过程中实际遇到的问题,每一个都是花了时间才解决的,写出来供大家参考。

5.1 典型问题速查表

问题现象根本原因解决办法
登录接口返回404拦截器把登录请求也拦截了在WebMvcConfigurer的excludePathPatterns里排除登录路径
商品列表查询条件失效前端传参参数名与后端不一致使用浏览器开发者工具的Network面板检查实际请求参数,统一命名规范
订单金额出现"99.9899999"金额字段用了Double统一改为BigDecimal,JSON序列化强制字符串格式
Ajax请求一直报跨域后端未配置CORS开发环境用Vite proxy;生产环境用前后端同端口部署
数据表字段"create_time"查询显示nullMyBatis映射时字段下划线和驼峰未对应配置map-underscore-to-camel-case=true,然后在BaseResultMap里明确定义每一列
批量上下架时部分成功部分失败单个update方法自动提交事务批量操作外层统一加@Transactional,保证原子性
图片上传后无法回显上传的路径是本地磁盘路径,前端无法访问配置静态资源映射:addResourceHandlers将磁盘目录映射为/images/**虚拟路径
管理后台Token过期不跳登录页请求错误码未统一判断在Axios响应拦截器统一判断401,跳转登录页并清除本地登录信息

5.2 个人调试心得与建议

调试过程中我最常用的就是浏览器开发者工具的Network面板和Postman。联调接口时,先在Postman里单独验证后端接口是否通,有问题直接看后端日志定位,然后再去前端页面排查。这个顺序很重要,能快速判断Bug出在哪一层,避免前后端互相甩锅。

还有个习惯,就是从项目开始就给全系统的接口约定统一的响应结构。前后端各自封装好自己的层,后面无论加多少功能,都不需要为协议发愁。

对于MyBatis的SQL问题排查,我建议在application.yml里开启SQL日志打印,生产环境则通过logback配置将mapper包下的日志输出为debug级别,这样能看到MyBatis实际执行了哪些SQL以及传了什么参数。上线排查问题的时候,这个日志帮了我大忙。比如有次商品列表查询特别慢,打开SQL日志后发现MySQL没走索引,因为条件里用了LIKE '%keyword%'这种前置百分号写法。后来把搜索条件改成了全文索引方案,查询速度提高了非常多。

MyBatis的<foreach>标签批量操作SQL也有个坑,如果List为空,生成的SQL会变成IN ()导致语法报错。所以使用前要判空:

<if test="idList != null and idList.size() > 0"> AND id IN <foreach collection="idList" item="id" open="(" separator="," close=")"> #{id} </foreach> </if>

最后,开发这种管理系统,一定要自己有从零完整走一遍的能力。很多人习惯用在线代码生成工具或自动建表工具,确实快,但出了问题往往不知道怎么排查。拿这个项目来说,我建议买一台便宜的云服务器(新用户优惠期很划算),手动装一次JDK、MySQL、Maven、Node、Redis,把完整流程跑通一遍。这个过程看起来很费时间,但能帮你把整条链路的每一个螺丝都拧明白,后面换任何环境都不会慌。

这套SpringBoot + Vue + MyBatis + MySQL的组合,到现在依然是我做管理类系统最顺手的选择。其实系统本身不算复杂,复杂的是把每个环节的细节都想清楚,比如事务边界划在哪里、状态流转怎么设计、异常情况下数据怎么保持一致。这些经验,只有亲自把一套系统从零写到上线,真正对着问题调试过,才能沉淀下来。上面提到的这些配置和代码片段,都是我按实际项目里的方案整理的,你可以直接拿去做参照,然后结合自己系统的业务逻辑去填充扩展。

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

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

立即咨询