恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
SpringBoot+Vue+MyBatis+MySQL商城系统实战解析
首页
资讯中心
/
SpringBoot+Vue+MyBatis+MySQL商城系统实战解析
SpringBoot+Vue+MyBatis+MySQL商城系统实战解析
发布时间:2026/10/10 17:41:09
我最近刚把一套企业级商城系统的源码从头到尾梳理完技术栈正好就是标题里那套SpringBoot Vue MyBatis MySQL。项目代号叫“米家商城”配套的运营后台我们内部叫“ABO”全称是 Admin-Back-office-Operations就是给运营、客服、仓储人员用的管理操作平台。如果你正准备做类似的全栈电商项目或者只是想把前后端分离的这套组合吃透那么这篇东西应该能帮你少踩几天的坑。我尽量不写教科书式的废话直接讲项目里真实遇到的设计决策、代码思路和上线前必须留意的细节。先说结论这个组合放在今天依然是非常稳妥的工业化选择。SpringBoot负责快速封装业务接口Vue负责前端交互和工程化MyBatis把SQL操作权完全还给开发人员MySQL则稳扎稳打地扛住核心业务数据。别看网上天天吹微服务、吹云原生对于绝大多数年交易额在千万级别的商城项目来说这套架构的性价比其实是最高的。原因很简单团队招聘成本低、排错链路短、部署运维也轻。下面我按整个项目的落地顺序把关键设计一步步拆开讲。1. 一个真实电商项目的技术选型与设计取舍1.1 项目为什么锁定这四件套立项之初我们也讨论过用不用Spring Cloud、要不要上MongoDB之类的选项。最后拍板用SpringBootVueMyBatisMySQL不是因为保守而是基于三个硬指标团队熟悉度、交付周期、运维成本。项目团队一共六个人后端三个、前端两个、测试一个。后端里真正精通Spring Cloud整套组件的其实只有一个如果强行上微服务光搭基础设施就要拉长两周。而SpringBoot可以在一天之内把骨架工程跑起来加上它内置的Tomcat和自动配置能让团队集中精力写业务而不是调框架。前端用Vue也是一样的逻辑大家熟组件生态全招聘市场上也容易补人。MyBatis和MySQL的搭配则更多是从SQL可控性考虑的。商城类业务有很多复杂的多表关联查询比如“根据规格参数筛选SKU”、“查询订单时带上商品快照和物流信息”这些用JPA/Hibernate写起来非常别扭最后还得补原生SQL。MyBatis允许我们直接维护SQL同时用动态SQL处理不确定的查询条件刚好命中电商场景。MySQL则让数据库容量和熟练度都处在舒适区先用主从缓存顶住压力真到了海量数据再考虑分库也不迟。1.2 商城核心链路与ABO管理系统的定位整个米家商城分两个端用户端商城和ABO后台管理系统。用户端的核心链路很常规浏览商品 - 加购物车 - 下单 - 支付 - 发货 - 确认收货。ABO这边的核心链路则围绕运营动作展开商品上下架、价格调整、库存同步、订单改价、退款审核、物流单号回填等。我特别想强调ABO系统的定位因为它经常被当作普通CRUD后台来做结果上线后运营抱怨一堆。ABO的核心不是“能管理数据”而是“能高效、安全地完成业务操作”。比如客服在ABO里给用户退款系统需要同时完成订单状态变更、支付渠道退款请求、库存回滚、操作日志记录四个动作。如果只做了数据库字段更新那就会出现订单显示已退款但用户没收到钱的问题。所以ABO在设计上必须和商城共用同一套Service层不能简单复制一份Mapper去读写订单表。2. 米家商城业务域拆分从商品到订单的状态流转设计2.1 商品与库存模型SKU/SPU的落地商城第一个要建模的是商品域。我们采用了SPUStandard Product Unit标准产品单元和SKUStock Keeping Unit库存量单位两层模型。SPU是商品的概念层比如“米家台灯1S”SKU是具体可下单的规格层比如“白色插电版”库存和价格都挂在SKU上。这个模型本身不新鲜但落地时有几个坑要提前处理。第一是规格属性的JSON存储问题我见过有人把规格拆成十几个字段结果每次加规格都要改表。我们采用的是规格名称为键、规格值为值的JSON字符串存储MySQL的json类型直接支持索引和查询前端渲染时直接解析兼容性很好。第二是商品详情的多端联动详情页由富文本、图片轮播、视频地址、参数表格组成我建议单独建一张商品详情表和SPU一对一关联避免在SPU表里塞大字段。库存数据我分了两个维度SKU可用库存和SKU锁定库存。可用库存是真正能卖的锁定库存是下单但未支付时的暂扣。这样设计方便处理“超卖”问题后面会讲到具体的扣减逻辑。2.2 订单与支付流程的状态机设计订单状态是最容易写崩的部分。我们定义了如下状态集待支付、待发货、待收货、已完成、已关闭、售后中。所有状态变更都必须经过一个独立的订单状态机类不允许在Mapper层随便改状态字段。下面这张表格展示了核心状态流转路径当前状态触发动作下一状态涉及系统待支付用户付款待发货支付回调、订单服务待支付超时未付已关闭定时任务、库存服务待发货商家发货待收货物流服务、订单服务待收货用户确认收货已完成订单服务待收货超时自动确认已完成定时任务已完成用户申请售后售后中售后工单、支付渠道状态机的好处是让流转路径清晰可控每个动作后面都可以加钩子函数。比如从“待支付”到“待发货”钩子函数里要先解锁库存并扣减可用库存再生成履约单推给仓储系统。如果先改状态再扣库存一旦扣失败订单就卡在状态错乱里了。我们在代码里把状态变更和库存操作放在同一个事务里后面事务部分会细讲。2.3 多级分销与会员价本次实现中的业务扩展点米家商城这个项目里还接了一个分销需求用户A推荐用户B下单A能拿到一定比例的佣金。这个逻辑并不复杂但要注意两点。第一推荐关系表里要记录“上下线关系生效时间”避免历史订单的佣金计算混乱。第二佣金结算做成异步任务用户确认收货后再触发避免在主订单事务里做太多计算。会员价则是在SKU价格之外再加一档“会员售价”。我们把价格放在单独的price表中包含SKU_ID、价格类型普通/会员/活动、价格值、生效时间。这样比直接在SKU上加会员价字段灵活得多双十一时可以增加“活动价”类型而不改表结构。3. MySQL数据库设计核心表结构、索引与分库分表思路3.1 商品、订单、用户核心表结构详解数据库是整套商城的心脏我给出几个核心表的关键字段设计方便你直接参考。商品表spuid主键spu_codeSPU编码唯一索引title标题cover_url主图detail_id关联详情表status上下架状态created_time / updated_time时间戳SKU表skuidspu_id关联SPUsku_codeSKU编码唯一索引specs规格JSON比如 {颜色:白色,版本:插电版}可用库存、锁定库存用int非负数price_id关联价格表status订单表ordersidorder_sn订单号唯一索引用全局ID生成器user_id下单用户spu_id、sku_id下单时的核心信息sku_snapshot下单时的SKU快照JSON防止后续修改影响历史quantity数量order_status状态字段total_amount / pay_amount金额用decimal(10,2)receiver_info收货人JSONcreated_time用户表t_useridphone手机号唯一索引nicknameinvite_code邀请码parent_id上级推荐人ABO后台表后台员工、角色、权限、操作日志sys_user后台账号密码用BCrypt存储sys_role角色sys_menu菜单/按钮权限sys_operation_log操作日志3.2 索引策略从慢查询日志反推索引设计索引不是建得越多越好而是要从实际查询路径出发。上线第一周我们开了MySQL慢查询日志阈值设置为1秒然后收集了一天里最慢的20条SQL。结果发现两类查询最危险一是按用户ID查询订单列表时如果只建了user_id普通索引ORDER BY created_time会让文件排序非常慢二是后台运营按订单号模糊查询时用了LIKE %xxx%直接导致全表扫描。对第一个问题建议建联合索引user_id, created_time可以同时满足WHERE和ORDER BY。对第二个问题我们的方案是让运营尽可能使用完整订单号并给order_sn建唯一索引如果实在要模糊搜就单独建一张订单搜索辅助表用Elasticsearch或简单的关键字表来兜底避免在核心订单表上跑模糊查询。另外一个容易被忽视的点是索引列上的函数操作比如WHERE DATE_FORMAT(created_time, %Y-%m-%d) 2025-01-01这种写法让索引直接失效。正确做法是查询范围created_time 2025-01-01 00:00:00 AND created_time 2025-01-02 00:00:00。3.3 数据量增长后的问题分库分表与读写分离预留虽然单库单表能撑一阵子但我们要提前预留扩展点。我的建议是订单表按照user_id哈希分成256张子表分表键用user_id这样可以保证同一个用户的订单都在同一个表里方便分页查询。商品表数据量相对小可以先做读写分离不急着分表。读写分离的话我们在SpringBoot里配置了动态数据源利用DataSource注解把只读方法路由到从库。注意要点事务必须是只读的才能走从库否则主从延迟会带来脏读。我们实际遇到过一次问题用户支付成功后跳转订单详情结果读从库延迟页面显示“待支付”把用户吓一跳。后来做了“支付成功后强制主库读”的标记才解决。4. SpringBoot MyBatis 落地细节Mapper、事务与缓存4.1 XML与注解的选择为什么推荐XML管理复杂SQL我接手项目后把团队里的规则统一了简单CRUD用MyBatis注解复杂联动查询一律使用XML Mapper。原因是XML可以显式地管理SQL并且支持动态SQL的标签比如 、 、 。当查询条件超过三个时注解方式会变得极其难以阅读而XML配合缩进和注释代码评审时可以非常清晰地看到SQL全貌。举一个我们经常用的动态更新案例ABO后台编辑商品时只更新非空字段update idupdateSkuSelective parameterTypemap UPDATE sku set if testprice ! nullprice_id #{price},/if if teststatus ! nullstatus #{status},/if if testspecs ! nullspecs #{specs},/if updated_time NOW() /set WHERE id #{id} /update这是把“部分字段更新”的控制权全部拉到SQL层避免了写一堆Java if判断来拼接Update对象。4.2 事务边界下单场景的分布式事务隐患下单接口是最典型的需要精确定义事务边界的地方。简化版代码如下Transactional(rollbackFor Exception.class) public OrderResult createOrder(CreateOrderDTO dto) { // 1. 锁定库存 int updated skuStockMapper.lockStock(dto.getSkuId(), dto.getQuantity()); if (updated 0) { throw new BizException(库存不足); } // 2. 创建订单 Order order buildOrder(dto); orderMapper.insert(order); // 3. 清空购物车 cartMapper.deleteByUserAndSku(dto.getUserId(), dto.getSkuId()); return success(order); }这里每个步骤都依赖同一个数据库连接事务所以从理论上讲中间出现异常都能回滚库存和订单不会不一致。但要注意两个隐藏风险第一lockStock方法里的SQL是UPDATE sku SET locked_stock locked_stock #{quantity}, available_stock available_stock - #{quantity} WHERE id #{skuId} AND available_stock #{quantity}。这行SQL本身在MySQL的InnoDB引擎下会锁住那一行SKU记录。如果同一时间很多用户抢购同一SKU后续请求都会阻塞在这把行锁上造成吞吐量下降。我们的应对方式是在高并发场景下改成Redis预扣库存异步扣减数据库库存或者使用乐观锁加版本号重试。第二事务里尽量不要做远程调用比如下单成功后再调用第三方支付接口这会拖长数据库事务让连接池迅速耗尽。我们在项目里把支付动作放在订单创建成功之后不在同一个事务里订单状态先置为“待支付”然后去调支付网关。即使支付网关超时也能通过定时任务去对账保证最终一致。4.3 MyBatis缓存机制与一次缓存失效问题的追查MyBatis的二级缓存我们一开始没打算用但为了提升热点SKU的查询性能给商品模块开了二级缓存。结果上线后出现一个诡异的Bug后台运营改了某个商品的标题前台页面过了一个小时还是旧标题。排查链路是这样走的先怀疑是浏览器缓存清掉后仍然复现然后看Vue端是否有状态缓存也没有最后查MyBatis的二级缓存发现默认的PerpetualCache是进程内本地缓存后台管理系统和商城服务是同一个SpringBoot进程理论上应该能更新。问题出在我们的缓存命名空间设置商品查询Mapper的namespace是com.xxx.mapper.SpuMapper但后台更新商品时更新的是SpuDetailMapper两个Mapper各自的缓存没有联动导致SpuMapper的二级缓存里还是一小时前的旧数据。最后方案很简单要么关闭商品查询的二级缓存要么在更新时手动调用清空相关缓存。我们最终选择了用Spring Cache注解 Redis缓存来做统一缓存管理因为MyBatis的二级缓存跟Transactional结合时如果有多数据源还容易出现脏数据不如缓存统一走Redis可控。5. Vue前端工程化动态路由、状态管理与移动端适配5.1 基于Vue Router的动态路由与权限控制用户端商城的前端相对简单ABO管理系统倒是花了不少心思。ABO要求不同角色的登录用户看到不同的菜单甚至同一菜单下按钮的可见性也由权限控制。我们采用动态路由方案用户登录后后端返回当前角色能访问的路由配置前端用router.addRoute动态挂载。具体流程是用户输入账号密码登录成功后拿到token请求 /sys/user/permissions 接口返回菜单树和按钮权限标识前端把菜单树递归转换成Vue Router的RouteRecordRaw数组通过router.addRoute注册侧边栏渲染时也基于同一份菜单树保证路由和菜单一致。这里一定要提防一个问题动态路由只在全局前置守卫里判断了一次如果用户刷新页面Vue Router的缓存会被清空必须重新拉取权限。我们当时的做法是在main.js初始化时检查本地是否已有token并有用户信息如果没有完整路由就重新走一遍动态添加逻辑。否则就会出现“登录后能访问一刷新就白屏”的经典坑。5.2 状态管理Vuex与Pinia的取舍商城这个项目启动时我们还用的是Vue2所以状态管理用的Vuex。如果你是Vue3新项目我建议直接用Pinia更简洁且天然支持组合式API。不过这里的核心不是选哪个而是明确哪些状态需要放到全局Store哪些不需要。我把购物车、用户信息、当前订单的支付倒计时放到了全局Store。商品列表页的筛选条件、详情页的当前SKU规格选择等尽量不要放全局否则切换页面时状态残留会导致各种莫名其妙的问题。有一个典型例子用户从商品详情页选好规格加入购物车成功后跳转购物车页面购物车页面读取到了上一个页面残留的规格状态导致展示异常。后来我们增加了状态重置逻辑进入页面时主动清空非必要缓存。5.3 组件库选型与移动端适配实战用户端商城的移动端我们选择了uni-app一套代码同时覆盖H5和小程序。ABO管理系统则使用桌面端组件库Element UI配合Vue2的生态非常成熟。两个端分开维护避免在同一个工程里混用组件库导致包体积失控。移动端适配方面我建议规范使用viewport单位配合rem方案。我们的基准是设计稿750px宽根字号设置37.5px组件库内部自带响应式则直接用百分比。有一个实际经验图片列表使用懒加载但懒加载组件在低端Android机上容易出现闪烁后来我们把图片区域的高度提前用占位比例锁定宽度以容器为准解决了滚动时的高度跳动问题。这类细节对商城首屏体验影响很大值得多花时间打磨。6. ABO管理系统权限模型、操作审计与低代码思路6.1 ABO系统的整体功能结构ABO覆盖的功能包括商品管理、订单管理、用户管理、营销管理、财务结算、系统设置、操作日志。每个模块在导航上都有对应的菜单子页面则承载各类明细操作。我用一张表格列出主要模块和核心交互模块核心交互商品管理商品上下架、SKU编辑、审核、批量改价订单管理订单查询、发货、改价、关闭、备注用户管理用户列表、等级设置、邀请关系查看营销管理优惠券创建、活动配置、分销佣金设置财务结算订单对账、退款审核、数据报表系统设置管理员维护、角色权限、数据字典ABO的页面请求频率不高但对数据一致性要求极高所以前端在提交操作时要有确认提示后端则需要对每个写操作做幂等校验。6.2 基于RBAC的权限模型扩展数据权限与操作日志权限模型我们采用经典RBAC用户-角色-权限。权限细化到按钮级别比如“商品管理-编辑按钮”就是一个权限码。后端使用Spring Security的PreAuthorize注解做接口控制PreAuthorize(hasAuthority(product:edit)) PostMapping(/product/edit) public Result editProduct(RequestBody ProductEditDTO dto) { return productService.edit(dto); }但RBAC只解决了“能不能操作”的问题没解决“能操作哪些数据”的问题。比如运营A只能管理自己负责的商品分类客服B只能查看自己创建的售后工单。我们扩展了“数据权限”配置角色上增加数据范围字段取值包括全部、本部门、本人。后端在查询时自动拼接SQL条件例如select idselectProductPage resultType... SELECT * FROM spu where if testdataScope SELF AND created_by #{userId} /if if testdataScope DEPT AND created_by IN (SELECT user_id FROM sys_user WHERE dept_id #{deptId}) /if /where /select操作日志是审计必需项。我们在ABO统一封装了一个OperationLog注解在需要记录的方法上标注操作类型通过AOP自动记录操作人、操作时间、请求参数、响应结果和IP地址。这个日志不能只写成功记录异常操作也要记录否则运营误操作后没有证据扯皮都扯不清。6.3 用设计稿驱动前端页面后台系统的标准化开发流程做ABO这类后台系统最浪费时间的不是写代码而是界面规范不统一。今天这个程序员给表格加了个“导出”按钮明天那个程序员把弹窗按钮放在左侧运营用起来非常别扭。我们的做法是固定一套页面模板列表页统一左上方搜索区、右侧表格、底部翻页编辑页统一弹出式Dialog或抽屉详情页统一只读描述列表。前端写了一批通用组件——SearchForm、DataTable、ModalForm、DetailDescriptions。新页面通过配置这些组件的字段数组即可完成80%的开发量相当于一个轻量的低代码方案。这样也带来了一个好处新增菜单时后端只需提供对应的CRUD接口前端复制一个现有页面改改配置半小时就能上线一个新功能。运营和产品都非常喜欢这种效率。7. 上线前必做的压测、安全加固与问题排查7.1 压测过程中发现的热点行锁问题我们的下单接口在压测到200并发时数据库出现大量锁等待。原因前面提过库存扣减SQL锁定同一行SKU记录。当时用JMeter压测TPS卡在80左右thread dump里大量线程处于等待锁状态。解决思路有三个第一把“可用库存”和“锁定库存”字段拆到单独一张库存流水表每次操作插入一条流水然后汇总计算剩余库存这样避免在同一行上反复update第二引入Redis预扣库存用Lua脚本保证原子性数据库库存通过异步任务最终扣减第三热门SKU切割库存行比如一个SKU拆成10个子库存记录随机选一个扣减减少行锁竞争。最终实战我选了方案二因为实现相对简单对业务代码改动小。Redis key设计为stock:{skuId}值就是可用库存。下单时执行Lua脚本if redis.call(get, KEYS[1]) - ARGV[1] 0 then return redis.call(decrby, KEYS[1], ARGV[1]) else return -1 end数据库中则把库存扣减放到异步任务里。压测结果表明TPS从80提升到700以上效果明显。7.2 安全加固参数校验、SQL注入与XSS的防护实践电商系统比较容易成为攻击目标所以安全加固这块我单独提一下。第一所有对外接口要经过参数校验框架使用javax.validation的NotNull、Size、Pattern等注解。最容易被忽视的是金额和数量字段必须限制范围比如订单数量不能超过99单价不能为负数。我们曾遇到一个测试小伙伴用负数的数量下单结果金额变成负数还好是测试环境不然就是事故。第二SQL注入。由于MyBatis的#{}会转义参数所以大部分注入漏洞被天然堵住。但有一次开发图方便把动态排序字段直接拼接了Order By后面的部分这地方不能使用#{}如果不对字段做白名单校验就会产生注入风险。我的做法是维护一个排序字段白名单orderBy只能是allowedMap中的key否则强制使用默认排序。第三XSS攻击。后台系统的输入框比较多运营可能会直接粘贴一些包含脚本的内容。我们使用全局过滤器对请求体中的字符串进行HTML标签转义存储时保持转义后的内容展示时再通过富文本组件的安全配置过滤。需要注意如果富文本需要保留图片和超链接不能简单全文转义而是要按需配置白名单标签。7.3 一次内存溢出排查从OOM到dump分析的思路上线后大概第三周ABO系统某天下午突然频繁Full GC最后直接OutOfMemoryError。当时我从表象判断是导出功能引起的因为运营在使用“导出全部订单”时系统会在内存里把全部订单列表一次性生成Excel数据量达到几十万条时HashMap和List对象把堆撑爆了。这也是一个很典型的教训后台列表的“导出”必须带去异步化。压缩回方案后我们把导出改成异步任务先生成CSV临时文件再通过消息推送提示运营下载。对内存里的查询结果则必须做分页循环去读取而不是一次性List装入内存while (true) { ListOrder pageList orderMapper.selectPage(page, 1000); if (CollectionUtils.isEmpty(pageList)) break; appendToCsv(pageList, writer); page; }此外我们给JVM加了-Xmx4g并配置了HeapDumpOnOutOfMemoryError参数。出问题时直接拿到dump文件用MAT分析出是org.apache.poi.xssf.usermodel.XSSFWorkbook占用了超过80%的空间这才定位到根因。排查内存问题就是这么枯燥先看参数再看dump最后对应到代码热点。踩完这一圈坑整个项目的稳定性才算真正立住了。最后再分享一点个人体会企业级商城项目最考验人的不是某个单一技术而是把所有技术串起来后的数据一致性和边界梳理能力。SpringBoot、Vue、MyBatis、MySQL这套东西你大概率都认识但真正把它们组合成一套能稳定运营的系统需要大量“抠细节”的时刻。比如订单状态机的每一步校验、事务的边界、SQL的索引、前端路由的权限控制每一个点都可能成为上线后的定时炸弹。希望这篇梳理能给你一些提前预判的参考少走几段弯路。