资讯动态

SpringBoot+Vue全栈实战:从数据库设计到部署的服装商城系统拆解

发布时间:2026/10/13 23:23:17 来源:尧图企业网站定制
1. 为什么要做这样一个服装商城系统从校园练手到可上线的完整闭环先聊点实际的。最近后台经常有人问我有没有那种既能交毕设/课程作业又真能跑起来当个小项目亮出来的完整系统。我一开始以为大家只是想要个代码包后来发现不是——真正难住新手的是前后端怎么串起来数据库表怎么设计才不返工部署之后为什么一堆跨域问题。于是我花了大概三周时间基于SpringBoot Vue这套国内最主流的技术栈从零实现了一个网上服装商城管理系统数据库用MySQL持久层用MyBatis整套源码干净可跑。这篇文章就是完整的拆解从表结构设计讲到前端鉴权最后到部署上线适合正在做毕业设计、或者想通过一个全栈项目把SSM/Vue技能点串起来的开发者。先说清楚这套系统到底能干什么。它不是一个简单的商品展示页而是包含了两套完整权限体系的商城后台前台面向消费者提供注册登录、商品分类浏览、关键词搜索、购物车、订单结算、地址管理后台面向运营人员提供商品管理、库存管理、订单发货/取消处理、用户管理、销售数据统计。技术选型上后端是SpringBoot 2.x MyBatis MySQL前端是Vue 2 Element UI Axios身份认证用的JWT部署产物是前后端分离的war包 静态文件。整套项目跑起来之后你会看到一个很典型的、在真实小团队中也会采用的商城架构。这篇文章我不想只丢给你一套能用就行的代码而是把每个模块为什么这么设计讲清楚。毕竟网上相似的项目太多了如果你只是抄一遍面试官随便问一个订单状态怎么流转的你就卡壳那就白做了。我会从数据库设计开始讲因为这一层错了后面写多少行代码都是在给错误奠基。2. 数据库表设计先想清楚状态机再动手建表2.1 十张核心表的关系梳理商城系统的数据模型说复杂也复杂说简单也简单核心不外乎围绕人、货、单三个维度。我最终落地的表结构一共十张用户表、商品分类表、商品表、商品图片表、购物车表、收货地址表、订单表、订单明细表、库存表其实并入了商品表字段、管理员表。没有引入冗余过度的设计但每张表都踩在了够用且不返工的边界上。先看商品分类。很多新手会设计一张无限级分类表parent_id自关联但对服装商城来说意义不大——它不像京东那样有十几层类目。我这里只做一级分类比如上衣裤装裙装外套配饰字段就id、name、sort_order三个。别小看sort_order列表页分类展示的排序全靠它别说我按id排序就行等运营想调整顺序时你就知道这个字段多重要。用户表和收货地址表是一对多关系。用户表除了id、username、password这些常规字段我额外加了nickname、avatar、gender、birthday这些字段在服装场景太有用了——后面你要做猜你喜欢或者会员等级没有基础画像数据根本推不出来。地址表我特意设计了is_default字段下单时如果没有选中地址就直接取默认地址这个细节容易被漏掉但用户体验差别很大。订单相关的是核心中的核心。订单主表的status字段是整个系统的灵魂我用的是int类型的状态码-- 订单状态枚举说明 0: 待付款 1: 待发货已付款 2: 待收货已发货 3: 已完成 4: 已取消 5: 已退款这里有个经验之谈一定不要用字符串存状态比如pendingpaid这种因为Java后端查一次改一次很麻烦而且前端状态展示还得做映射。用int配合枚举类写起来干净查询也快。订单明细表里快照了几个关键字段商品名称、商品主图、单价、数量、小计。为什么叫快照因为商品以后可能改价、改名、甚至删除但历史订单必须保留当时的下单信息。这个设计很多新手不理解等他们上线后商品改了个名、历史订单全变空白的时候就会回来谢我这张表。商品表和库存我合并了因为服装类目单品SKU尺码、颜色如果展开会变成一张巨大的SKU表。但考虑到这毕竟是教学性质项目我做的是单规格商品所以直接在商品表里放了stock字段。如果你的毕设想加分可以在商品表下方挂一个sku表把红色M码“蓝色L码”这种组合拆开但对应的下单逻辑、库存扣减逻辑都会复杂一个量级量力而行。2.2 外键到底要不要我的建议是物理外键别加这是所有MySQL初学者最容易纠结的问题。我的做法是逻辑外键不建物理约束。也就是说订单表的user_id跟用户表的id保持概念上的关联但不在DDL里写FOREIGN KEY。原因有三点物理外键在高并发写入场景下会有额外的完整性检查开销虽然商城项目测试阶段感觉不到但这是个坏习惯。MyBatis分页插件、批量插入、大批量更新时物理外键容易引发各种锁问题。删除用户、删除商品这类操作如果你有物理外键就必须先删子表数据否则直接报错而用逻辑外键你可以在代码里决定是禁止删除级联删除还是假删除置一个deleted标志位。我推荐最后一种每张业务表加deleted字段0正常1删除。商城系统删除商品不是真删而是下架。同理用户也不是真删是禁用。这套软删除思路在很多生产项目里都是标配。2.3 订单状态机的流转规则上面写了状态码这里补充一下流转边界因为这是我见过踩坑最多的地方。我实现的状态机如下待付款可以取消可以支付待发货商家可以发货用户不能取消部分真实商城在发货前允许用户申请退款这里简化了待收货用户确认收货后进入已完成已完成可以申请售后简化版里没有售后模块但状态位预留了已取消/已退款终止态不能再改变这个流转我放在了Service层统一校验而不是放任每个Controller自己判断。比如支付方法里第一步就是查状态public void pay(Integer orderId) { Order order orderMapper.selectById(orderId); if (order.getStatus() ! 0) { throw new BizException(订单状态异常无法支付); } // ... 更新状态为待发货 }为什么要这么做因为商城系统最怕脏数据状态流转写在业务层就像交规——所有订单操作必须经过这条路谁也不能逆行。有些代码会把状态判断写在Controller里看起来也行但一旦有多个入口调用同一个支付逻辑就会出现有的入口校验了、有的没校验的隐患。3. 后端接口设计从Controller到Mapper的一整条调用链3.1 包结构怎么分才不迷路后端源码的包结构直接决定了你后期维护的心情。我的划分方式是按模块而不是按技术层堆叠。先给目录com.shop ├── controller # 控制层只做参数接收和结果封装 ├── service # 业务层接口impl ├── mapper # MyBatis的Mapper接口 ├── entity # 数据库实体类 ├── dto # 前端入参对象跟entity解耦 ├── vo # 出参对象按前端需求裁剪 ├── config # 配置类跨域、拦截器、JWT等 ├── common # 公共结果类、异常处理、工具类 └── ShopApplication.java看到这个结构你可能会问entity、dto、vo分这么细不累吗说实话小项目确实可以简化但如果你毕设的论文里需要画分层架构图这三层分开是标准的加分项。更实际的原因是——数据库字段绝对不能直接暴露给前端。比如用户表里有password如果你直接把entity返回给前端哪怕是空字符串也会成为安全隐患。用vo做隔离前端需要什么就给什么。3.2 统一返回结果和全局异常这两个一定要有所有接口的返回结构必须统一否则前端写axios拦截器会想骂人。我的Result类长这样public class ResultT { private Integer code; // 200成功500失败401未登录 private String message; private T data; }前端拿到的数据永远是这么一个壳子axios里一次拦截搞定错误处理。全局异常我用RestControllerAdvice业务异常(BizException)返回500 具体提示参数校验异常返回400 字段错误信息兜底异常返回500 系统繁忙。这里要特别提醒一点异常信息不要直接透传给前端。就拿数据库唯一键冲突来说底层报的是Duplicate entry xxx for key uk_xxx这种东西扔给用户看既暴露了表结构又不像人话。全局异常里统一转成操作失败请勿重复提交安全又体面。3.3 几个典型接口的关键实现3.3.1 商品列表的分页和条件查询商品列表是访问量最大的接口肯定不能一次性查全量。我用了PageHelper分页插件配合条件构造器public PageInfoGoodsVO queryGoodsPage(GoodsQueryDTO dto) { PageHelper.startPage(dto.getPageNum(), dto.getPageSize()); ListGoodsVO goodsList goodsMapper.selectGoodsByCondition(dto); return new PageInfo(goodsList); }查询条件包括分类id、关键词模糊匹配商品名、价格区间、上下架状态。SQL是动态拼接的注意如果按价格排序排序字段要走索引否则数据量一大就好几百毫秒的延迟。我在Goods表加了一个普通的idx_category_id索引服装类目并发量不算高这个级别够用了。3.3.2 下单这个接口到底做了什么这是整个系统最重量级的业务方法。步骤拆开如下从token里解析当前用户idJWT工具类。校验购物车选中的条目非空。遍历选中的购物车条目校验商品状态为上架、库存充足。预扣库存更新商品表stock stock - 1并补上一条stock 0的where条件。计算订单总金额商品单价乘以数量求和。插入订单主表生成订单编号插入订单明细表。清空已下单的购物车条目。这一步全部在Transactional事务里执行。第4步里那个WHERE stock 0是关键。这样即使两个请求同时抢最后一双鞋数据库层面也会拦下库存为负的情况。很多新手在这里喜欢先查再改先select stock出来Java里判断大于0再update这在并发下一定会超卖必须在一条update语句里完成条件扣减。订单号生成我没有用数据库自增id而是yyyyMMddHHmmss 用户id后四位 随机四位这样既看得出下单时间又不会太长。3.3.3 登录和JWT鉴权怎么落地登录接口逻辑不难难的是后面每个接口的鉴权。我在SpringBoot里写了一个JwtInterceptor拦截器配置类里排除掉登录、注册、商品列表、商品详情这几个公开接口其余全部要过token校验。用户登录成功后后端签发token前台每次请求在header里带Authorization: Bearer xxx。拦截器里解析token把用户id塞进Request对象业务层通过UserContext.getUserId()拿当前用户。管理员体系是独立的用一张admin表登录接口是/admin/login签发的是另一个secret的token。这样前台后台完全隔离互不干扰。有人说用Spring Security做权限更标准但对商城这类中小项目一个JWT拦截器完全够了还能让你在答辩时把自定义拦截鉴权这个过程讲得清清楚楚。4. 前端Vue实现从零搭建到页面联调的那些坑4.1 脚手架和目录组织Vue这边我用的Vue CLI创建的项目依赖只装了vue-router、vuex、axios、element-ui、sass。目录上按views拆页面components放公共组件api目录里统一管理所有请求。为什么要单独建api目录因为前后端分离项目最大的痛点是接口地址分散——如果每个页面里直接写axios.get(/api/goods)等后端改一次路径你全局搜索会改到怀疑人生。统一放在api/goods.js里页面只调用fetchGoodsList(params)以后就算接口路径变了你只改一处。路由设计我用了动态路由 静态路由结合。静态路由是登录页和注册页动态路由根据登录用户的role去渲染不同的菜单和页面这里用到了Vue Router的addRoutes方法后台管理页面的router-view是独立布局跟用户前台分开。4.2 通用请求封装拦截器里统一处理token和错误码axios封装是前端工程质量的分水岭。我先放代码// request.js 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 401) { router.push(/login) return Promise.reject(new Error(未登录)) } if (res.code ! 200) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { Message.error(网络异常请稍后重试) return Promise.reject(error) } )这段代码解决了90%的接口报错问题后端只要按约定返回Result结构前端所有页面的错误弹窗全部统一不用每个页面都写try-catch然后Message.error。token过期也在这里集中处理自动跳登录页。记住一个原则通用逻辑能放拦截器就绝不放业务页。4.3 商品展示页和购物车的状态联动商品卡片我用了Element UI的Card组件点击进入详情页详情页里有尺码单规格下其实就是数量选择器和加入购物车按钮。加入购物车时调后端接口传goodsId count后端如果库存不足会抛BizException前端拦截器统一弹库存不足提示。这里我特意做了个细节商品列表页上每个卡片旁有一个库存角标如果stock 0就置灰缺货标记不能再点。这个交互看起来小但对服装商城来说很能提升购物体验。购物车页面的操作有勾选商品夹带全选/反选、修改数量、删除、合计计算。合计我放在前端算因为乘法求和这点开销没必要请求后端。但下单金额以后端重新计算为准这个原则至要前端显示只是视觉预览。4.4 后台管理页面和权限菜单后台首页是一个Dashboard放商品总量、今日订单数、总销售额三个数字卡片数据来自一个统计接口。商品管理页用表格展示所有商品支持搜索、编辑、上架/下架切换打开编辑会弹一个Dialog表单字段跟商品新增一致。订单管理页用Tab来区分不同订单状态每个状态下有对应的操作按钮发货、取消、退款点击按钮后二次确认再调接口。菜单权限我是这样处理的前端登录管理员后根据adminInfo里的role字段拼出可访问路由表。虽然这个项目只有超级管理员一种角色但代码把可扩展多角色的架子搭好了以后加店长、运营只要在菜单表加字段就行。打包之后后台访问地址是/admin/dist/index.html前台是/web/dist/index.html顺便配了nginx把两个静态目录分别指向不同域名路径看起来就像两个独立站点。5. 上线部署实战从本机跑通到打包到服务器5.1 本地开发环境最容易踩的坑如果你clone下来源码第一件事就想跑起来大概率会碰到下面几个问题。第一个是数据库版本和连接配置我用的是MySQL 8.0驱动配置里必须带时区和SSL参数不然会报Public Key Retrieval is not allowedspring: datasource: url: jdbc:mysql://localhost:3306/shop?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456第二个是端口冲突。SpringBoot默认8080前端Vue开发服务器默认8080两个肯定打架。我前端vue.config.js里改了devServer端口为3000然后配了代理devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端写请求时全部写/api/goods这种相对路径开发环境代理到后端生产环境nginx代理到后端同一份代码两边跑不会出现生产环境因为多了个域名前缀导致404的问题。第三个坑是MyBatis的mapper.xml扫描路径。记得在启动类或配置类上加MapperScan(com.shop.mapper)否则Spring容器里找不到Mapper Bean启动直接报Invalid bound statement (not found)。这个问题看着低级但几乎每周都有人来问。5.2 生产打包jar nginx静态分离我后端采用的是jar包部署方式虽然没有做微服务拆分但对单机商城足够。打包命令mvn clean package -DskipTests生成的jar在target目录下放到服务器上执行nohup java -jar shop-backend.jar --spring.profiles.activeprod shop.log 21 配置文件里prod环境用的是另一套数据库账号密码不要用root直接连生产库这是个底线问题。前端打包npm run build产物是dist目录把它放到nginx的html目录下nginx配置里把/api前缀的请求反代到http://127.0.0.1:8080其他请求指向静态文件。这一步做完前后端就真正分离开了。我建议新手第一次上线时全程开着--spring.profiles.activedev跑一遍流程确认每一步都通了再切prod否则日志信息不够排查起来很痛苦。5.3 部署中常见的三类故障第一类是跨域问题。如果你前后端都是同域部署通过nginx代理转发就不存在跨域但如果你是本地开发前端3000端口直连后端8080后端必须开启CORS配置。我在配置类里写了registry.addMapping(/**) .allowedOriginPatterns(http://localhost:*) .allowedMethods(*) .allowCredentials(true);注意allowedOriginPatterns的作用域用了通配符如果你用allowedOrigins(*)的同时开allowCredentials(true)SpringBoot会直接拒绝。这个坑很隐蔽报错信息还特别绕我当时浪费了半小时。第二类是mysql时区问题导致订单时间差了8小时。有些服务器自带时区是UTC你存入的订单时间跟北京时间对不上。解决办法就是在jdbc url里把serverTimezone写死为Asia/Shanghai别依赖系统环境。第三类是文件上传路径问题。商城商品肯定要传图片我这里是存本地磁盘配置里用了一个upload.dir属性指向/data/shop/imagesnginx把/images/**映射到这个目录。如果你不做这一步上传的图片是能落盘但浏览器访问不到会显示裂图。我曾经见过有人把图片传到jar内部目录的每次重新发布图片就全没了千万引以为戒。6. 答辩和面试时的高频追问这六个点一定要能接住如果这个项目是你的毕设那答辩时老师大概率会从下面几个角度拷打你。每个点我都把该怎么答梳理清楚。1. 为什么选择SpringBoot而不是SSH或者SSM回答思路SpringBoot是Spring生态的快速开发脚手架解决了大量XML配置内置Tomcat适合快速构建独立服务SSM是Spring SpringMVC MyBatis的组合需要手动装配太多。但你要能说清楚SpringBoot的核心机制是自动配置和starter这样才显得不是背概念。2. 前端Vue的响应式原理是什么简单答Vue2用Object.defineProperty对data里的每个属性做getter/setter劫持在getter里收集依赖Watcher在setter里触发依赖更新。数组的索引变更无法被劫持所以Vue2提供了$set方法。如果你在项目里用过this.$set(this.form, stock, 10)就说明你是真的会。3. JWT和Session有什么区别为什么选JWT答Session存储在服务端需要维护会话状态水平扩展时要考虑session共享JWT是无状态令牌包含用户信息服务端只验证签名更适合前后端分离。注意补充JWT的缺点无法主动让token失效所以一般配合短期有效期使用。4. 购物车为什么存数据库而不是localStorage答数据库购物车能实现多端同步网页和App登录同一个账号购物车一致而且下单时可以直接查库校验库存localStorage只在单浏览器生效换设备就丢而且浏览器存储不可控。反问自己一句如果用户换个设备就不能下单了这是不是灾难。5. 库存扣减如何防超卖这是必考题。答核心是数据库条件更新UPDATE goods SET stock stock - 1 WHERE id ? AND stock 0利用数据库行锁保证原子性。可以在update后检查受影响行数等于0说明库存不足抛异常回滚事务。如果能补充幂等性考虑就更完美——比如用户重复点击下单按钮要防止生成两个订单。6. 订单超时未支付如何处理这道题很开放。简版方案定时任务每隔一段时间扫描待付款订单超过30分钟就自动取消。加分方案Redis存储订单编号过期时间用监听key过期事件来执行取消逻辑但要注意过期事件延迟问题。我的项目里用了简版定时任务方案因为阉割版比不做强能自圆其说最重要。还有一个每次都会有人问的这套系统有什么不足这个不用怕坦诚说没有做支付对接用的是模拟支付状态、没有做商品多规格SKU、没有做优惠券和秒杀。重点是补一句这些模块在架构上已经预留了表和接口位置下一步计划扩展老师很吃这一套。7. MyBatis踩坑合集全是真实debug出来的教训7.1 返回Map时字段为null不显示的问题商品详情里有个需求是返回品牌信息我某段时间发现有的商品品牌是null但前端的对象里连这个key都没有导致取值时报undefined。原因在于MyBatis默认配置里mapUnderscoreToCamelCase虽然开启了驼峰转换但并没有把null字段填进map里。解决方案是在application.yml里配mybatis: configuration: map-underscore-to-camel-case: true call-setters-on-nulls: true第二行这个call-setters-on-nulls平时没人提一旦遇到某个字段偶尔为null但前端必须显示占位符的场景没配它就会出bug。7.2 动态SQL里if标签的坑0值被当作空字符串这是MyBatis最著名的坑之一。查询商品时如果前端传了priceMax0那动态SQL里的判断条件很可能写成if testpriceMax ! null and priceMax ! AND price lt; #{priceMax} /if问题来了priceMax是Integer类型的0和0比较在OGNL里会返回true吗实际上OGNL会把priceMax ! 判断为true因为Integer和String不相等所以这里没问题。真正容易翻车的是金额区间查询里有个priceMin 0的判断——如果你用了priceMin ! 作为唯一条件而priceMin恰好是0OGNL的字符串比较会有怪异行为。我的建议是所有数值类型的动态条件只用! null判断不要跟空字符串比较。7.3 分页查询总数不对PageHelper分页有一个非常隐蔽的坑如果你在查询前有别的查询并且没有用PageHelper.clearPage()清掉那么分页插件可能作用到错误的那条SQL上。正确姿势是PageHelper.startPage(pageNum, pageSize); ListGoods list goodsMapper.selectList(condition); PageHelper.clearPage();或者你把所有分页查询统一封装到一个BaseService里避免每次手写这三行的遗漏。对我这种记性一般的人来说封装是克服重复劳动最靠谱的方案。7.4 批量插入别一条条insert订单明细肯定是多条一起插入。新手容易写个for循环不断调insert慢不说还不好维护。MyBatis里用foreach标签insert idbatchInsertOrderItems INSERT INTO order_item(order_id, goods_id, goods_name, goods_image, price, count, total_price) VALUES foreach collectionlist itemitem separator, (#{item.orderId}, #{item.goodsId}, #{item.goodsName}, #{item.goodsImage}, #{item.price}, #{item.count}, #{item.totalPrice}) /foreach /insert数据量在几百条以内这种一条SQL的批量插入性能完全够。如果超过几万条你可能需要考虑分批插入但商城订单明细一般不会那么夸张。7.5 注解SQL和XML该如何取舍这个项目里我大部分用的是XML文件原因就一条动态SQL用XML可读性更高。商品查询有六七个可变条件全写在注解的Select里那行字符串拼接根本没法维护。但单表简单查询比如按id查商品用了注解。我的原则是查询条件超过两个就用XML否则注解。8. 项目验证与迭代方向跑起来之后还能怎么升级最后聊聊项目跑通后怎么让它变得更有含金量。如果你是纯粹为了学习或者交差当前功能已经够用了但如果想让它成为简历里的亮点我建议按下面的优先级去迭代第一优先级是接入真实的支付模拟流程。不需要真的去申请支付接口可以用沙箱环境或者做一个模拟收银台用户点击支付弹窗展示订单金额点击确认后订单状态变成已支付。这一步加完后整个交易闭环就完整了在答辩时可以讲清楚支付回调订单金额校验重复回调幂等这几个概念。第二优先级是给商品加多规格SKU。服装天然是一件商品多个SKU的典型场景比如黑色M码黑色L码白色M码等等。你需要新增一张sku表订单明细指向skuId而不是goodsId库存和价格都挪到sku表。这是从课程项目到能说服面试官的重要跨越。第三优先级是增加Redis。不需要多哪怕只用Redis做首页轮播图的缓存或者验证码存储都能在架构上多一个亮点。你可以说验证码存Redis并设置2分钟过期防止暴力注册这条比单纯我用Redis做了缓存具体得多。第四优先级是观察者模式的应用。下单后给管理员发站内通知、给用户发邮件这种场景天然适合观察者模式。把事件发布和监听代码写出来在论文里设计模式应用这一章就能站住脚。目前项目中我用了简单的同步调用如果你想升级把通知逻辑抽出来放到监听器里代码改动很小。我在实际跑这个项目的时候最深的体会是代码能跑通不代表项目完成能讲清楚每一个设计决策才是真的掌握了。你从这篇文章里拿走源码只是一个开始真正值钱的是为什么这么设计的思考过程。如果后面有同学在部署时遇到什么奇怪的报错欢迎带着日志来交流我会按错误码和工作原理帮你排查毕竟这套系统我已经从头到尾踩过一遍坑知道最容易出问题的地方都在哪里。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑