基于SpringBoot+Vue的服装销售系统:从数据库到权限控制的实战解析 1. 先说清楚这套系统解决的到底是什么问题小到一家街边的服装门店大到几十个连锁专柜的服装品牌商日常经营最绕不开的就是三件事商品怎么上架、库存怎么管清楚、订单怎么不丢不乱。很多传统门店到今天还在用Excel记款式、颜色、尺码单子一多就乱月底对账更是噩梦。衣依这套系统的定位就是给这类场景一个开箱即用的在线化管理方案用SpringBoot做后端服务Vue做后台管理界面MyBatis管数据持久层MySQL做数据落盘四样东西组合起来覆盖从商品资料建立、库存变动、前台销售下单到订单履约的完整闭环。对于正在学Java全栈的人来说这套系统的参考价值更直接。它不是一个只有登录注册的Demo而是把真实业务里常见的RBAC权限控制、SPU/SKU多规格商品、订单状态机、购物车、会员折扣这些模块全部落地了。你在面试中聊到做过电商项目时真正的底气就来自这些模块里的细节设计而不是泛泛地说我写过增删改查。我后面分享的内容会按这样的顺序展开先讲数据库是怎么从业务逆向推导出来再拆后端接口和权限再到前端页面的组织方式最后给出MyBatis实战中容易踩坑的点以及整套源码在本机跑起来的完整过程。如果你正在用这套源码做毕业设计、做简历项目或者要在公司内部搭一个中小规模的销售管理后台这篇文章可以帮你省下不少自己摸索的时间。提示文中的所有结构设计、接口约定、部署步骤都来自实际经验源码本身可能在某些细节上和你拿到的版本略有出入但整体思路和排错方法完全可以复用。2. 为什么最终选了SpringBootVue这套组合背后是有取舍的第一次看到企业级这三个字很多人容易把它想复杂。其实在这个项目的语境里企业级更多指的是架构上要留出扩展空间、权限模型要完整、数据不能出错而不是说要用上微服务、分布式那一套重型武器。2.1 技术选型的对比逻辑后端层面SpringBoot几乎是当下Java业务系统的事实标准。相比早期SSH框架它最大的价值是自动配置和Starter机制一个web项目只需要引入spring-boot-starter-web内嵌的Tomcat直接启动省去了大量XML配置。和Spring Cloud那套相比单体应用在几百并发以内完全够用部署运维成本却低得多。对于服装销售平台这类内部管理系统我没必要上来就搞服务拆分一个SpringBoot工程把所有模块组织清楚反而更容易维护。前端选择Vue核心原因是它的学习曲线和数据绑定体验比React更平缓。后台管理系统大量场景就是表格表单弹窗Vue生态里的Element UI组件库简直是为这种页面量身定做的。你要是用原生JS去写一个带分页、筛选、批量操作的SKU管理页面光处理DOM就得写到怀疑人生用Vue的响应式数据驱动代码量直接少一半以上。数据层选MyBatis而不是JPA是因为这套系统里有大量联表查询、动态条件SQL和自定义统计。MyBatis对SQL的控制粒度非常细程序员写的每一句SQL都是透明的出了问题能直接拉出SQL分析排查效率高。JPA虽然开发爽但讨厌的地方在于框架自己生成的SQL不可控一旦业务复杂起来性能问题很难定位。至于MyBatis-Plus你在源码里可能没看到它后面我会专门聊一聊这个设计考量。2.2 系统整体分层架构这套项目从代码层面看是标准的三层架构加前端独立工程前端工程Vue Vue Router Vuex Axios Element UI负责页面渲染和用户交互Controller层接收前端请求做参数校验返回统一的结果对象Service层承载业务规则比如下单时的库存校验、支付后的状态流转都在这层完成Mapper层MyBatis接口一个方法对应一条SQL语句MySQL数据库存储商品、库存、订单、用户、角色等业务数据![架构分层示意图文字版]浏览器Vue前端 ↓ axios 请求 / 返回 JSON SpringBoot Controller ↓ 参数校验 / 数据封装 Service 业务层 ↓ 事务管理 / 业务逻辑 Mapper 持久层MyBatis ↓ SQL MySQL 数据库这套分层的好处是每一层只关心自己该做的事。比如以后想把MyBatis换成MyBatis-Plus只要Mapper层的接口签名不变上面两层完全不用动。这就是架构的价值——不是当下省事而是未来改的时候不头大。3. 数据库设计才是这套系统真正的灵魂我接触过不少学生写的项目代码写得很热闹但打开数据库一看表结构全是一坨字段类型乱来、没有外键约束、金额用double存储。这样的系统一旦上真实业务数据早晚要出问题。衣依这套源码的数据库设计我认为是最值得仔细学习的地方。3.1 从业务角色反向推导表清单服装销售平台里有哪些角色在使用系统普通会员在前台小程序或H5页面浏览下单运营人员在后台维护商品和库存客服和库管处理订单发货系统管理员分配账号权限。每个角色关心的数据不同表结构也随之而来业务域核心表说明用户权限sys_user、sys_role、sys_menu、sys_user_role、sys_role_menuRBAC五张基础表商品中心product_spu、product_sku、product_category、product_image商品主档与规格库存仓储stock_detail、stock_record当前库存与流水日志交易订单orders、order_item、cart、payment_record下单到支付全过程会员运营member、coupon、member_coupon会员资料与优惠券绑定统计报表product_sales_stat、daily_summary或通过SQL实时统计销售、库存、会员等多维分析3.2 服装行业特有的SPU/SKU设计服装这个品类和3C数码不一样同样是男士休闲衬衫它可能有黑、白、蓝三种颜色每一种颜色之下又有M、L、XL三个尺码。如果把每个颜色尺码当独立商品管理后台列表会爆炸用户在前台筛选时体验也很差。所以这套系统采用了标准的两级商品模型SPUStandard Product Unit即款对应一件衬衫的基础资料——标题、类目、品牌、主图、详情图、季节属性、风格属性SKUStock Keeping Unit即具体规格项对应黑色L码这个唯一组合包含规格属性值、价格、库存数量、SKU编码SPU和SKU是一对多的关系数据库表设计上就是product_spu的id关联product_sku表的spu_id。前端商品列表页显示的是SPU级信息点击进入详情页后用户选择颜色尺码实际下单转化的是SKU级数据。这套模型是所有电商系统的地基做服装类项目时这块绝对不能简化。3.3 订单和库存交互的关键细节订单表和订单明细表是销售系统的核心涉及三个容易出错的地方金额字段统一用decimal。服装客单价虽然不像大额交易那么高但涉及折扣、优惠券叠加时用double算很容易出现20.999999这种尴尬结果。decimal(10,2)做货币存储是行业共识读出来再配合BigDecimal运算精度不会丢。订单状态字段用varchar存状态码。很多新手喜欢用数字0、1、2表示订单状态过两个月再看代码0代表待付款还是已取消全靠记忆。源码里是用字符串枚举比如WAIT_PAY、PAID、SHIPPED、FINISHED一眼就能看懂也方便扩展。库存扣减必须和下单放在同一个事务里。也就是Service层用Transactional注解包住校验库存→扣减库存→创建订单→创建订单明细这几步任何一步异常前面改动的数据全部回滚。如果你发现源码里没有加事务建议自己补上这是生产环境的底线要求。4. SpringBoot后端接口设计与权限控制是怎么组织的4.1 包结构和核心功能清单源码的后端工程建议先看包结构它能直接告诉你系统的功能边界。一个常见的组织方式是com.yiyi ├── common // 统一返回结果、全局异常、工具类 ├── config // 配置类跨域、拦截器注册、MyBatis驼峰映射等 ├── controller // 接口层 ├── service // 业务逻辑接口 ├── service.impl // 业务实现 ├── mapper // MyBatis映射接口 ├── entity // 数据库实体 ├── dto // 请求/响应参数封装 └── interceptor // JWT拦截器Controller层要做的事很纯粹接收参数、调用Service、封装结果返回。业务逻辑不要写在Controller里这是评审代码时最高频的红线。比如删除商品这个操作看起来就一句DELETE FROM product WHERE id ?但实际业务上你还得检查这个商品有没有未完成的订单、需不需要同时下架关联的SKU、要不要删掉缓存这些流程都放在Service层代码才能演变为人和人都能看懂的结构。DTO层很多人容易略过但在企业级项目里很重要。数据库实体entity直接暴露给前端容易把不需要的字段比如密码哈希值泄露出去而且接口参数变化时还得回头改实体类。用独立的DTO做隔离接口的稳定性和系统安全性都会有明显提升。4.2 JWT 拦截器实现登录态和RBAC权限控制后台管理系统的权限模型通常用RBAC基于角色的访问控制这套标准方案它把用户-角色-权限拆开管理避免给每个用户单独配置权限导致运维灾难。这套系统里有三个关键步骤用户登录成功后后端生成一个JWT令牌返回给前端令牌里面加密了用户ID、用户名和过期时间前端把这个token存到localStorage里每次请求在Header上带Authorization: Bearer token后端写一个拦截器统一处理放行登录接口其余接口都先校验token是否有效再取当前用户信息和角色权限判断有无访问目标接口的权限实际代码里Interceptor大概长这个样子public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 如果是预检请求OPTIONS直接放行 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } // 解析token失败则抛出全局异常 Claims claims JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, claims.get(userId)); return true; } }这里有个细节值得注意跨域配置也得放行OPTIONS预检请求否则前端在浏览器里发跨域请求会先被CORS策略拦掉接口永远调不通。很多初学者前后端联调卡在这一步半天找不到原因就是这个环节处理不完整。4.3 订单流程的Service层设计逻辑订单模块是整套系统业务最重的部分。从用户操作视角来看一条订单会经历这样的流转加入购物车用户端可以批量勾选生成订单提交订单调库存、算总价商品金额-优惠券-会员折扣运费模拟支付真实项目中这里对接微信/支付宝支付网关发货后台库管操作订单进入物流状态确认收货订单完成可进入售后环节退款/换货Service层处理这个流程时设计思路应该是把每个状态流转做成独立方法不要在一个大方法里用if-else堆一坨。比如submitOrder()负责建单扣库存payOrder()负责改状态加支付记录shipOrder()负责登记物流单号。方法之间通过数据库的乐观锁或状态条件判断来防重UPDATE orders SET status PAID WHERE id #{orderId} AND status WAIT_PAY这种写法返回受影响行数为1才算更新成功可以防止两个请求同时把同一笔订单从待支付改成已支付这种并发场景在企业级系统里一旦出现就是事故。5. Vue前端后台管理系统的页面组织与数据交互方式5.1 前端工程结构怎么规划用Vue做后台管理界面最忌讳把代码都堆在一个巨型组件里。比较好的做法是按照页面→模块组件→公共组件三层来组织。这套系统的前端工程规划下来大致是src ├── api // 每个模块的axios请求封装 ├── assets // 静态资源 ├── components // 公共组件文件上传、分页、富文本等 ├── router // 路由表 路由守卫 ├── store // Vuex状态管理token、用户信息、权限 ├── views // 页面级组件 │ ├── login │ ├── dashboard │ ├── product // 商品管理 │ ├── order // 订单管理 │ ├── member // 会员管理 │ ├── report // 统计报表 │ └── system // 系统管理用户、角色、菜单 └── utils // 工具函数日期格式化、token操作等api目录做的事情很重要它把每个后端接口收敛成前端的一个函数页面组件里只负责调用这个函数不需要关心URL怎么拼接、错误怎么统一处理。比如api/product.js里会有import request from /utils/request export function getProductList(params) { return request({ url: /product/list, method: get, params }) } export function createProduct(data) { return request({ url: /product, method: post, data }) }这样当后端接口路径变化时只需要改api下面这一个文件页面组件完全不用动。5.2 Axios封装和路由守卫的统一处理Axios不封装直接用代码里会出现大量重复的token取值、错误弹窗逻辑。这套系统的utils/request.js里应该统一处理好三件事请求拦截器每次请求自动从localStorage读取token并加到header里响应拦截器后端的统一返回结果是包含code、msg、data的对象code为200时把data直接返回给调用方code为401时自动跳转登录页并清空本地用户信息错误处理网络超时、服务器异常统一弹出用户可读的提示路由守卫的作用是防止未登录用户直接通过URL访问后台页面。Vue Router的beforeEach钩子里做一次token判断就够了router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() } else if (token) { next() } else { next(/login) } })如果需要更细粒度地控制某个菜单权限可以结合动态路由登录后根据当前用户的角色权限从后端拉取可访问的菜单列表再用router.addRoutes动态注册。源码如果没做这一步也算是留给你的一个扩展练习。5.3 商品管理页面和订单处理的核心交互商品管理页的难点在于SPU/SKU联动的表单。页面结构上通常是一个主表格展示SPU列表点击编辑打开一个包含多页签的对话框其中规格与库存页签动态渲染SKU表格每行包含规格属性选择、价格输入、库存数量输入。Vue最舒服的地方就是这种动态行能通过数据驱动实现数据模型设计成这样的嵌套结构editForm: { spuName: , categoryId: null, skuList: [ { color: 黑色, size: M, price: 199, stock: 50 }, { color: 黑色, size: L, price: 199, stock: 43 } ] }订单管理页相对简单一些主要是顶部条件筛选订单号、状态、时间范围加订单主表列表每一行可以点击展开查看订单明细。状态列用不同颜色的Tag展示待付款是警告色、已付款是主色、已完成是成功色操作列根据状态渲染不同按钮。这类组件化开发方式本质上就是把数据正确地从后端拉下来、展示好、再正确地把用户操作传回后端Vue的响应式机制让这个流程变得非常顺畅。6. MyBatis在项目里的实际应用缓存、动态SQL和联表查询6.1 为什么这套源码没有引入MyBatis-PlusMyBatis-Plus确实能省掉单表CRUD的一堆XML很多新项目上来就直接用它。但在这个项目里不用它我总结有三个原因第一是学习价值。这是一个用来理解数据持久层的典型项目纯MyBatis会让你亲自动手写SQL映射和动态SQL对底层机制会有更扎实的认知。面试时被问MyBatis的#{}和${}有什么区别“动态SQL有哪些标签”你能答得上来靠的就是亲手写过。第二是可控性。MyBatis-Plus封装了太多省事方法开发者容易在不知不觉中写出全表扫描。纯MyBatis让你对每一条执行的SQL都心里有数这在大型业务系统里是加分项。第三是性能优化空间。遇到复杂查询需要手动优化SQL时纯MyBatis写个resultMap就能精确控制结果映射完全不会被框架层多余的逻辑干扰。6.2 动态SQL处理多条件商品筛选服装商品列表页那个筛选条件多条件组合的场景用MyBatis动态SQL处理再合适不过。按类目、季节、价格区间、风格属性筛选都有可能为空如果为此写很多个if分支的DAO方法代码就崩了。正确姿势是用where标签加if判断select idselectProductList resultTypecom.yiyi.entity.ProductSpu SELECT * FROM product_spu where if testcategoryId ! null AND category_id #{categoryId} /if if testseason ! null and season ! AND season #{season} /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if /where ORDER BY create_time DESC /selectwhere标签的好处是能自动去掉第一个多余的AND让SQL不至于因为某个条件为空而语法报错。另外注意这里我用了gt;和lt;XML中直接写大于号小于号会解析报错这是很不显眼的坑一定要记住。6.3 一级缓存和二级缓存、以及分页的坑MyBatis的缓存机制主要分一级缓存SqlSession级别和二级缓存Mapper级别需要结合项目实际说明它们的坑。一级缓存默认开启同一个SqlSession里查同一条SQL会命中缓存但在Spring整合环境下SqlSession的生命周期经常和事务绑定如果两次查询之间有插入或更新操作缓存会失效所以一级缓存对业务的影响通常不大。二级缓存默认不开启开启之前要考虑缓存的数据是不是变化频繁比如商品库存这种高频变动的数据开二级缓存反而容易让用户看到旧库存。业务系统中我的建议是只对数据变化极小的字典表开启订单、库存这种数据别碰二级缓存。分页的坑源码如果使用PageHelper分页插件最容易遇到的问题是分页失效。常见原因是在执行PageHelper.startPage()之后又执行了其他SQL查询导致PageHelper的线程变量绑定到了错误的SQL上分页就乱了。严格的做法是startPage之后紧跟你要分页的那条Mapper查询中间不做任何多余操作。6.4 订单和明细的一对多映射查询订单列表时一个订单可能要带出多条商品明细。SQL层面要么用子查询要么用关联查询后用resultMap做嵌套结果集映射。推荐resultMap这种写法一次查询就把嵌套数据查全避免了N1次查询的性能灾难resultMap idOrderWithItemsMap typecom.yiyi.entity.Order id propertyorderId columnorder_id/ result propertytotalAmount columntotal_amount/ collection propertyitems ofTypecom.yiyi.entity.OrderItem id propertyitemId columnitem_id/ result propertyskuName columnsku_name/ result propertyquantity columnquantity/ result propertyprice columnprice/ /collection /resultMap这段resultMap的关键是collection标签它告诉MyBatis一个订单对应多个明细MyBatis内部会根据orderId做一个自动分组把同一订单的明细聚合成一个List。这里有话说resultMap里最好显式标注id字段否则当订单明细跨表同名字段出现时数据映射会错乱查出来的结果可能对不上账。7. 从零搭建部署这套系统启动过程中的实际问题与排查思路源码拿到手最核心的事就是让它在自己电脑上跑起来。我从安装到启动给你捋一条完整的路径并针对易错环节做重点提醒。7.1 环境准备清单我建议按下面的版本来踩坑最少依赖项推荐版本说明JDK1.8或11SpringBoot 2.x用这俩版本最稳MySQL5.7或8.08.0需要多注意时区和SSL配置Maven3.6用IDE内置的也行Node.js14~16太高的Node版本可能和旧版依赖冲突IDEIDEA / VSCode后端必须IDEA前端VSCode足够前端开发阶段我用VSCode就够了轻量而且插件全。后端建SpringBoot项目建议用IDEA它对Maven、Spring、MyBatis的支持体验最好断点调试也顺畅。7.2 数据库初始化和后端配置第一步把项目里提供的SQL脚本通常是sql目录下的yiyi.sql导入MySQL用命令行或Navicat执行都行。执行完后检查一下关键表是否有数据尤其看看sys_menu、sys_role、sys_user这三张表因为登录跳转的菜单权限都靠它们撑着。第二步修改后端application.yml里的数据库连接配置这里给出一个MySQL 8.0的典型配置spring: datasource: url: jdbc:mysql://localhost:3306/yiyi?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver重点是serverTimezoneAsia/Shanghai不设的话连MySQL 8.0经常直接报Server returns invalid timezone错误。allowPublicKeyRetrievaltrue是应对MySQL 8.0的caching_sha2_password认证方式连不上的问题加上这一串能省很多事。第三步执行mvn spring-boot:run或在IDEA里直接运行主类。看到Tomcat started on port(s): 8080就说明后端起来了。7.3 前端启动和常见报错处理前端工程打开终端先执行npm install装依赖然后npm run serve启动开发服务器。如果报错90%出在版本上Node版本过高导致依赖安装失败建议切换到Node 14或16。用nvm做版本切换最快比卸载重装省事太多。安装时间过长或者卡住换成淘宝镜像源npm config set registry https://registry.npmmirror.com重装依赖会顺畅很多。页面能打开但接口404或跨域检查Vue脚手架里是否配置了开发代理// vue.config.js module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }前端访问/api/product/list代理会帮你转发到后端的/product/list这样既能解决跨域问题也方便后续部署时用Nginx做统一转发。7.4 启动排查链路从现象倒推问题真碰到项目跑不起来不要急着乱猜按这个链路一步步来确认后端是否启动成功看控制台日志有没有异常堆栈重点找最后一行错误信息。确认数据库是否连通用Navicat或命令行直接测同一个连接串排除账号密码、权限、IP白名单问题。确认端口是否被占用netstat -ano | findstr 8080Windows或lsof -i:8080macOS/Linux如果端口被占用改application.yml里的server.port或者停掉占用程序。确认前端网络请求是否到达后端打开浏览器的F12开发者工具看请求状态。404说明路径不对401说明token过期或没带上500说明后端代码有问题去后端控制台查堆栈即可。数据库报时区错误优先加serverTimezoneAsia/Shanghai别先怀疑SQL脚本有问题。这个坑我见过太多次数据库脚本执行正常、后端配置也改了最后就卡在时区上一加上立马恢复。8. 项目跑通之后值得继续深入改造的几个方向源码跑通只是第一步真正吸收它的营养要看你事后有没有对细节做二次思考。我个人建议重点看两个地方这也是面试官最常追着问的点。第一个是库存扣减的并发控制。现在很多教程项目扣库存就是先select再update高并发下会超卖。要真正的企业级至少要改成直接根据条件更新UPDATE product_sku SET stock stock - #{quantity} WHERE id #{skuId} AND stock #{quantity}受影响行数大于0才算扣减成功否则提示“库存不足”。这就是乐观锁思想的SQL实现如果你能在聊项目时主动讲出这个改造思路它会成为很好的加分项。第二个是订单超时自动取消。系统里待支付订单如果用户一直不付款得有个机制把库存释放回来否则用户不付款却占着库存真正想买的人反而买不到。基础做法是利用Spring的Scheduled定时扫描也可以引入延迟队列但要慎重交付一个能用的方案比引入复杂组件更重要。另外如果这套系统要真正部署上线前端npm run build打出来的静态文件和后端jar包放到同一台服务器上配上Nginx做反向代理和静态文件服务再用HTTPS域名访问整体就具备上线条件了。到那一步再考虑数据库备份、日志收集、监控告警这些运维层面的东西。我个人的看法是一个像衣依这样的项目最大的价值不是代码本身而是它完整地把从需求到数据库再到前后端代码的思考链路打通了。你把这条链路复述清楚能用自己的话讲明白每一个设计决策那这套源码就真正成了你自己的东西。