
做电商类系统这几年前后端分离已经是绕不开的标配了。我自己经手过好几套销售系统的搭建从最早的JSPServlet到后来的模板引擎渲染再到现在的SpringBootVue3彻底前后端分离说句实话这套组合确实是目前做Web电子产品销售系统最稳、最省心的方案之一。这套基于Java SpringBootVue3MyBatis的电子产品销售系统核心概括下来就是后端用SpringBoot搭接口服务MyBatis负责数据库交互MySQL做底层数据存储前端用Vue3Element Plus构建管理后台和商城页面通过RESTful API完成数据通信。它解决的典型问题包括商品管理、用户登录鉴权、购物车、订单流转、后台数据维护这些电商系统里绕不开的环节。如果你正准备做毕业设计、刚接触全栈项目想找一套完整源码参考或者公司需要快速搭一套内部销售管理后台这套系统的架构思路和关键实现都非常值得拆开来看一遍。1. 整体架构设计与技术选型为什么是这套组合1.1 前后端分离到底解决了什么问题先说一个很多新手容易忽略的点。前后端分离不是简单地把代码拆成两个工程而是把“数据渲染”和“数据交互”彻底解耦。传统单体项目用Thymeleaf或者JSP页面上的数据是服务端拼好的HTML直接扔给浏览器看着是省事但项目一大了问题就出来了前端要改个样式得找后端同事后端改个接口返回结构前端页面直接崩部署还得整个应用一起重启。这套系统把后端拆成纯接口服务前端Vue3项目独立开发、独立部署两边只通过JSON格式的API通信。带来的实际好处有三个第一前端可以并行开发后端把接口定义好就能开干不用互相等第二接口复用性高管理后台和手机端如果以后要出APP同一套后端接口能直接撑起来第三部署灵活前端静态资源扔Nginx就行后端独立维护。1.2 技术栈选型背后的考量选SpringBoot不用多说Java生态里做微服务和接口开发最成熟的那一批自动配置把繁琐的XML配置干掉了一大半内置Tomcat让打包成jar一键启动对中小型电商系统来说开发效率极高。MyBatis和JPA的选择我倾向于MyBatis。电商系统的SQL逻辑复杂尤其是多表联查、动态条件筛选、统计报表这类场景MyBatis的XML里写SQL直观可控优化起来也方便。JPA虽然上手快但遇到复杂查询时生成的SQL很难精准控制性能调优时会有种拳头打在棉花上的无力感。前端Vue3配Element Plus是现在的主流组合。Vue3的Composition API让组件逻辑聚合度更高配合Vite构建冷启动速度和热更新体验比Vue2Webpack时代好太多。Element Plus的表格、表单、弹窗组件覆盖后台管理80%以上的界面需求不用从零手搓UI组件。数据库选MySQL是市场占有率决定的。稳定、社区活跃、性能足够搭配Navicat或命令行工具管理数据都非常顺手。这套系统的表结构设计、索引优化、事务隔离后面我会详细拆解。2. 后端核心模块实现SpringBootMyBatis的实战要点2.1 实体层与表结构的映射思路MyBatis里最基础也最容易被轻视的是实体类和数据表的映射关系。这套系统的表结构大致包括用户表、商品表、商品参数表、购物车表、订单表、订单明细表、分类表这几类。每个表对应一个实体类字段类型必须严格对齐尤其是时间类型和金额类型。一个容易踩的坑是Java的时间类型和MySQL日期类型的映射。我的习惯是数据库里DATETIME对应Java的LocalDateTimeDATE对应LocalDateTIMESTAMP对应LocalDateTime并且在MyBatis的配置里开启jdbcType的显式声明。不然你会发现查询出来的时间总是比数据库里的少了8个小时或者格式不对这个问题排查起来非常隐蔽。金额字段必须用DECIMAL(10,2)而不是FLOAT或DOUBLEJava实体里对应BigDecimal。浮点数做金额运算会出现精度丢失订单金额算错一毛钱客户投诉起来就麻烦了。这个属于基本功但我在代码评审里见过太多次了。2.2 用户登录与JWT鉴权的完整闭环用户模块是这套系统的基础。接Spring Security显得过重我用的是JWT拦截器的方式做鉴权。用户在登录接口输入账号密码后端校验通过后生成一个Token返回给前端前端存在localStorage里后续每个请求都在Header里带上Authorization: Bearer Token。后端写一个HandlerInterceptor拦截器在preHandle里校验Token的有效性。要注意的细节是Token的续期策略——我通常会把Token的有效期设为2小时同时让前端在Token过期前30分钟静默调用一个刷新接口这样用户操作到一半不会被突然踢下线。电商系统里用户正在确认订单突然跳回登录页这体验是很糟糕的。密码存储用BCrypt加密别用MD5。MD5反查彩虹表成本太低BCrypt加了盐且慢哈希暴力破解的成本高到没人愿意试。用户表里存的是BCrypt的哈希值登录时用BCrypt.matches()校验。2.3 商品模块的SPU/SKU设计电子产品销售系统里“手机”“笔记本电脑”这类商品有个特点同一种型号会有多个规格变体比如手机有不同颜色、不同存储容量价格和库存各自独立。商品表需要拆成SPUStandard Product Unit标准产品单元和SKUStock Keeping Unit库存量单位两层设计。SPU表存商品的基础信息名称、品牌、分类、封面图、描述SKU表存具体可下单的单元颜色、容量、价格、库存、SKU编码。前端的商品详情页展示的是SPU信息用户选择规格后确定的是SKU下单时锁定的是SKU的库存。这个设计直接决定了后续购物车和订单模块的复杂度。如果商品表只做一张大宽表一个商品多个规格就得存多条重复记录商品列表查重、库存扣减逻辑都会变得极其拧巴。很多新手做电商系统在这里栽跟头我建议一开始就按SPU/SKU拆后面能省掉一次大规模重构。2.4 购物车和订单状态机的流转购物车表设计相对简单用户ID、SKU ID、数量、加入时间联合唯一索引保证同一用户同一SKU只有一条记录。前端传SKU ID和数量后端校验库存后返回最新的购物车列表。订单模块是整个系统里逻辑最重的部分。我把它设计成一个状态机待付款、已付款、已发货、已完成、已取消、售后中。每个状态之间的转换必须有明确条件比如只有待付款状态才能取消只有已付款才能发货。代码层面我用一个状态转换校验方法每次更新订单状态前先校验当前状态是否允许跳转到目标状态非法操作直接拒绝。这样能避免并发情况下订单状态被覆盖。库存扣减要放在订单创建的事务里使用UPDATE语句的原子性来做条件扣减UPDATE sku SET stock stock - #{num} WHERE id #{skuId} AND stock #{num}。受影响行数为0说明库存不足直接抛出异常回滚事务。这种写法比先SELECT再UPDATE安全得多从根源上避免超卖。2.5 MyBatis分页插件PageHelper的使用与坑列表页的分页是必做的功能。MyBatis生态里最主流的方案是PageHelper一个轻量级的分页插件原理是在MyBatis执行SQL前用拦截器改写SQL自动拼接LIMIT语句。基础用法非常简单PageHelper.startPage(pageNum, pageSize); ListProduct products productMapper.selectByCondition(condition); PageInfoProduct pageInfo new PageInfo(products);startPage方法只对紧接着执行的第一条查询生效所以它和select方法之间千万别插入其他查询操作否则分页会作用到错误的SQL上——这是我见过最频繁的误用场景。还有一个坑是PageHelper.startPage之后如果查询语句是left join带子查询的复杂语句偶尔会出现count查询拼接错误。我的处理方式是对于这类复杂查询不走PageHelper手写两个方法一个countByCondition查总数一个selectByCondition手动加LIMIT #{offset}, #{pageSize}。虽然代码多一点但SQL完全可控不会出现莫名其妙的统计错误。2.6 MyBatis缓存机制与配置文件的关键设置MyBatis默认开启一级缓存作用域是同一个SqlSession二级缓存默认关闭需要显式配置。在电商系统里商品信息属于读多写少的冷数据开启二级缓存能显著降低数据库压力。配置方式是在Mapper XML里加一行cache evictionLRU flushInterval60000 size512 readOnlytrue/但要注意两个问题。第一flushInterval要结合业务设置电子产品商品的更新频率不高60秒刷新一次足够如果是库存这种高频变化数据千万别开缓存不然展示的库存数会是过期的。第二二级缓存是跨SqlSession共享的一旦涉及多表联查缓存命中的可能是脏数据。我的建议是只在单表操作的场景开启联查询一律禁用。配置打印SQL语句也是个好习惯。在application.yml里加mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样控制台会输出每条SQL和参数排查问题的时候一眼就能看到实际执行的语句省去盲猜的时间。3. 前端Vue3开发实战从搭建到联调的关键环节3.1 Vite环境搭建与SCSS集成Vite是Vue3官方推荐的构建工具基于ESModule的dev server启动速度快到让人回不去Webpack。搭一个新项目npm create vitelatest frontend -- --template vue cd frontend npm install npm run dev项目里经常会用到SCSS安装也很简单npm install -D sassVite对SCSS的支持是开箱即用的安装node-sass或现在的dart-sass后.vue文件里的style langscss直接能跑。如果你遇到过node-sass装不上的问题大概率是Node版本和sass版本不匹配换成dart-sass就消停了这是我在新机器上实测下来的经验。3.2 组件化设计与Composition API组织逻辑Vue3里最核心的变化是Composition API也就是setup语法。它不是让你抛弃Options API而是在逻辑组织上多了一个更灵活的选择。这套系统里我是这样划分组件的布局组件侧边栏、顶部导航、面包屑业务组件商品表格、筛选表单、订单状态标签通用组件图片上传、富文本编辑器、弹窗确认商品管理页就是典型的组合式封装页面组件只负责拼装子组件和持有数据状态表格组件负责渲染和事件抛出筛选表单组件负责收集查询条件。用defineProps和defineEmits把子组件的数据出入口定义清楚父子组件之间的数据流像一条单向管道调试起来很舒服。3.3 Pinia状态管理与路由守卫的使用Vue3配套的状态管理库现在主流是Pinia相比Vuex它去掉了mutations环节API更简洁TypeScript支持也更好。这套系统的用户状态管理export const useUserStore defineStore(user, () { const token ref(localStorage.getItem(token) || ) const userInfo ref({}) const isLoggedIn computed(() !!token.value) function setLogin(data) { token.value data.token userInfo.value data.userInfo localStorage.setItem(token, data.token) } function logout() { token.value userInfo.value {} localStorage.removeItem(token) router.push(/login) } return { token, userInfo, isLoggedIn, setLogin, logout } })路由守卫是鉴权的第一道防线在router.beforeEach里检查目标路由是否需要登录态没有Token直接弹回登录页。后端接口还有拦截器做第二道防线前端守卫解决的是体验问题后端拦截解决的是安全问题两者不能互相替代。3.4 Axios封装与跨域问题的处理策略前后端分离必然遇到跨域。Axios封装的第一步是统一配置const service axios.create({ baseURL: /api, timeout: 15000 }) service.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers[Authorization] Bearer ${userStore.token} } return config }) service.interceptors.response.use( response response.data, error { if (error.response?.status 401) { // 跳转登录页并清除过期Token } return Promise.reject(error) } )关于跨域我的方案是前端开发环境通过Vite的proxy配置把/api代理到后端地址生产环境用Nginx反向代理前端静态资源和后端接口到同一个域名。这样浏览器看到的请求都是同源的从根本上绕开跨域限制比在SpringBoot里配CrossOrigin或者CORS全局过滤器要干净也更安全。4. 数据库设计与系统安全细节容易被忽视但决定成败的地方4.1 核心表的字段设计与索引建议用户表核心字段id, username, password, phone, email, avatar, status, create_time。username加唯一索引登录场景的查询条件基本就是它索引是必加的。商品SPU表核心字段id, name, brand, category_id, cover_image, description, status。分类ID加普通索引品牌加普通索引列表页经常按分类筛选这个索引特别有用。SKU表核心字段id, spu_id, spec_names, spec_values, price, stock, sku_code。spu_id加索引因为一个SPU下的SKU会被频繁查。sku_code加唯一索引订单明细关联的时候用得上。订单表核心字段id, order_no, user_id, total_amount, status, address_info, create_time。order_no唯一索引user_idcreate_time联合索引。订单列表页通常是按用户查订单这个联合索引能覆盖绝大多数查询场景。4.2 MySQL事务管理的正确姿势订单流程里最典型的事务场景是创建订单 扣减库存 生成订单明细这三个操作必须同生共死。用Spring的Transactional注解就能实现Transactional(rollbackFor Exception.class) public Order createOrder(CreateOrderRequest request) { // 1. 校验库存并扣减 // 2. 创建订单主表记录 // 3. 创建订单明细 return order; }rollbackFor Exception.class这个参数很多人会漏掉。Spring的Transactional默认只在RuntimeException时回滚像IOException这种受检异常不会被触发。你手动throw new Exception()也不会回滚所以显式声明rollbackFor是必备操作。另一个经验是事务尽量短。在一个事务里做耗时操作会长时间占用数据库连接并发一大连接池就告急。正确的做法是事务只包裹必须原子的数据库操作像发送短信通知、调用第三方接口这些耗时动作放到事务外面执行或者用消息队列异步处理。4.3 安全性处理密码加密与上传文件过滤前面提到的BCrypt加密是必须的这个再强调一遍不过分。另外还有两个安全细节容易被忽略。第一个是SQL注入防御。虽然MyBatis的#{}预编译能防住大部分注入但如果在XML里用${}拼接动态表名或排序字段就给了注入可乘之机。我的原则是能用#{}的地方坚决不用${}确实需要动态拼接的地方先用白名单校验一遍输入值。第二个是上传文件的安全过滤。系统里涉及商品图片上传、PDF说明书上传的场景需要写一个全局过滤器对所有上传请求统一校验文件后缀名白名单.jpg,.png,.pdf等、文件内容头校验读取文件头字节判断真实格式、文件大小限制图片不超过2MBPDF不超过20MB。这样做不只是防XSS攻击更是防恶意上传可执行文件打穿服务器。4.4 MySQL配置优化与连接池参数开发环境和生产环境的MySQL配置需求差别很大。开发时本地装个MySQL把sql_mode里的STRICT_TRANS_TABLES留着严格模式能在数据写入时立刻暴露类型错误省得线上炸了才查。生产环境建议做这几项配置调整innodb_buffer_pool_size设置为物理内存的50%左右InnoDB的缓冲池是性能核心max_connections默认151个通常不够根据业务并发调到500左右slow_query_log开启将超过1秒的查询记录下来配合分析工具定位慢SQL连接池我用DruidSpringBoot集成简单功能强大连接复用、连接泄漏检测、慢SQL监控全都有。Druid的监控页面可以看到每个接口的SQL执行时间排查性能问题效率极高。5. 常见问题排查实录与避坑经验5.1 时间字段映射异常症状数据库里存的2025-01-01 12:00:00查询出来变成了2025-01-01 04:00:00。原因有两类。第一类是数据库连接URL没有配置serverTimezoneAsia/ShanghaiMySQL驱动会取服务器的默认时区导致偏差。第二类是Java实体用了Date类型但JSON序列化时没有指定格式Jackson默认的时区处理和本地不一致。解决方案是在application.yml里明确spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai datasource: url: jdbc:mysql://localhost:3306/electronics?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai一个项目里时间格式统一用LocalDateTime并在全局配置序列化规则能少掉90%的时间问题。5.2 商品分页数据重复或丢失症状切换页码时出现同一商品出现两页的情况或者某条数据凭空消失。这类问题大多数出在排序不稳定。MySQL在LIMIT分页时如果没有明确的ORDER BY字段或排序字段有重复值查询优化器可能改变返回顺序导致分页结果混乱。解决办法简单但容易被忽略在查询SQL里加上ORDER BY id DESC或者使用稳定唯一的字段排序。如果排序字段是价格就追加第二个排序条件price DESC, id ASC保证排序稳定。5.3 前后端联调时的请求参数丢失症状前端明明传了参数后端接口却收到null或空数组。排查顺序从外到内浏览器Network面板确认请求URL正确查看请求Payload是JSON格式还是form-data格式后端Controller的方法签名是否用了RequestBody接收JSON、RequestParam接收表单参数常见错误是后端用RequestBody接收实体对象前端Axios传参数时却设置了application/x-www-form-urlencoded格式导致后端解析不了。解决方案是前端统一定义Axios请求格式POST请求传对象时用JSON序列化并确保Header的Content-Type是application/json。5.4 线上问题排查把SpringBoot jar反编译看代码线上环境偶尔会出现本地复现不了的问题。比如环境差异导致的配置文件不对或者打包的版本不是最新的。这时候最快的定位方式是直接把jar包里的class反编译出来看。工具推荐jd-gui操作流程用jd-gui打开jar包里的对应class文件反编译后直接看到Java源码逻辑。配合javap -c可以看字节码指令。有一次线上订单状态卡在待付款我反编译了OrderServiceImpl.class对照了一下发现打包的代码里有个状态判断的if条件写反了本地代码明明是对的——是同事没有重新打包导致的。排查效率提升极大。5.5 页面展示崩溃的常见原因Node版本不匹配是Vue3项目最常见的问题。Vite要求Node版本大于等于18很多老机器上还跑着Node 14npm run dev一启动就报错。解决方案是使用nvm管理多个Node版本项目根目录放一个.nvmrc文件团队协作时大家切换统一版本。组件使用报错“组件未注册”也是高频问题。Element Plus在Vue3里使用有两种方式全量引入和按需引入。全量引入省事但打包体积大按需引入要用unplugin-vue-components插件自动导入。如果你发现表格组件的某功能不生效先检查是不是没有引入对应的子组件比如el-table-column单独使用时可能不会被自动识别。6. 项目扩展方向这套系统还能怎么升级运行一段时间后这套系统会面临几个明显的扩展需求。商品图片和附件是典型的存储瓶颈。我建议提前集成MinIO做对象存储把图片上传、PDF文件从应用服务器里剥离出来。MinIO部署简单兼容S3协议接入SpringBoot只需要几行配置。好处是静态资源独立管理、支持动态扩容也方便以后接入CDN加速。支付功能是电商闭环的最后一环。这套系统目前可能只做到了订单生成还没对接真实支付。如果要上线运营支付宝和微信支付的接入是迟早的事。我的建议是写一个PaymentService接口先以Mock实现跑通流程后面替换成真实支付SDK时不用改业务代码。数据分析方面销售报表、热门商品排行、用户复购率这些统计需求可能很快会出现。这类需求用MyBatis写聚合SQL就能搞定但数据量大之后建议引入定时任务做预聚合把统计结果存到单独的报表表避免每次看报表都全表扫描。复盘这套系统的开发过程我最深的体会是技术选型和架构设计的重要性远超单纯堆代码。前后端分离、SPU/SKU建模、状态机设计、事务边界划分——这些决策在项目初期就把地基打好了后续所有功能都是在稳固地基上盖楼。如果你正在准备开发类似系统我建议先花时间把这几个核心模块的逻辑想透再动手写代码比边写边改顺畅得多。最后再分享一个小技巧把所有Mapper接口的SQL都打开日志打印开发阶段不要关你会在debug时感谢这个决定。