资讯详情

基于SpringBoot+Vue的网上商城系统开发实战与踩坑记录

📅 2026/10/7 2:36:19 | 华诺云谱 👁 阅读
基于SpringBoot+Vue的网上商城系统开发实战与踩坑记录
最近刚把爱琴海购物公园的网上商城系统从头到尾做了一轮完整的开发从需求梳理、技术选型到前后端编码、联调部署前前后后踩了不少坑也整理出了一些实打实的经验和踩坑记录。这是一个典型的基于SpringBootVue的前后端分离商城项目覆盖用户端购物全流程和管理端的商品、订单、用户管理。这篇文章我会把这套系统的设计思路、数据库建模、核心代码实现、联调部署中的实际操作和问题排查全部捋一遍希望能给正在做类似项目尤其是拿商城系统当毕业设计或者练手项目的朋友提供一份能直接抄作业的参考。1. 项目本质与整体设计思路1.1 爱琴海购物公园为什么需要一套网上商城爱琴海购物公园本身是线下实体商业综合体覆盖零售、餐饮、亲子、影院等多种业态。线下购物公园天然存在几个痛点营业时间受限于商场开门关门辐射范围局限在周边几公里顾客在到店之前无法浏览场内商品和了解实时活动。网上商城的价值在于把线下的商品和服务搬到线上让用户随时随地浏览商品、查看活动、完成下单同时为商场沉淀会员数据做后续的精准运营。从这个角度来说这套商城的定位不是要替代线下而是做线上线下的互补。系统设计的关键点在于商品信息要能实时同步订单流转要清晰用户端体验要流畅管理端操作要高效。理解了这一层商业逻辑才能明白为什么要做用户模块、商品模块、购物车、订单、支付这些功能而不是拍脑袋堆功能。1.2 技术选型的核心考量为什么是SpringBootVue现在的企业级项目里SpringBootVue基本是事实上的标准组合选它不只是因为人气高而是背后有一堆非常实际的考量。先说后端SpringBoot。Java生态里SpringBoot把过去Spring MVC时代繁琐的XML配置几乎全部干掉依赖引入、自动配置、内嵌Tomcat一个mvn spring-boot:run就能把服务跑起来这对快速搭建后端服务来说太友好了。商城系统涉及用户、商品、订单、支付多个业务模块SpringBoot的starter机制能快速集成MyBatis、Redis、JWT这些工具社区里踩坑记录也足够多。再说前端Vue。Vue最大的优势是渐进式架构和组件化开发。商城页面多是列表、详情、购物车这类交互密集的页面Vue的数据驱动视图特性让开发者不需要手动操作DOM状态变了页面自动更新。配合Vue Router做路由管理、Pinia或者Vuex做全局状态管理整个前端工程的结构会非常清晰。对比过其他方案如果纯用JSPServlet写开发效率太低前后端耦合严重代码维护性差用PHP系框架虽然上手快但复杂业务逻辑的表达能力和工程化程度不如Java系若依这类开源脚手架可以直接拿来改但定制成本有时候比从零写还高因为你要去理解别人封装好的代码逻辑。SpringBootVue的组合在灵活性、开发效率、学习价值之间取了比较平衡的点。1.3 系统整体功能模块划分这套商城系统从角色上分为用户端和管理端从业务流程上覆盖了从商品浏览到订单完成的完整链路。用户端核心模块首页轮播图、分类入口、热门商品推荐、活动专区商品模块商品列表、分类筛选、关键词搜索、商品详情、库存展示购物车加购、修改数量、删除、选中结算订单提交订单、选择收货地址、订单列表、订单详情、取消订单支付模块对接模拟支付流程生成订单后模拟支付回调更新状态个人中心个人信息维护、收货地址管理管理端核心模块商品管理商品上下架、库存修改、价格调整、商品图片上传、分类维护订单管理订单列表、订单状态筛选、发货操作用户管理用户列表、账号禁用/启用在这个项目里用户角色通过JWT中的角色字段区分前端根据角色动态渲染管理端路由和菜单这是前后端分离项目里很常见的权限控制方案。2. 数据库设计与核心表结构2.1 表结构设计原则与整体规划商城系统的数据库设计是整个项目的基石表建得不合理后面写代码会处处难受。我在设计时遵循几个原则一是核心业务表独立避免字段冗余二是金额字段统一用高精度类型不用浮点三是状态字段用int并且注释清晰便于扩展四是逻辑删除代替物理删除保留数据可追溯性。整个系统规划了核心业务表、用户相关表、商品相关表、订单相关表四类。其中核心业务表包括用户表、商品分类表、商品表、购物车表、订单表、订单明细表一共六张主表。管理端的操作日志和轮播图表根据需求可以再加但核心链路先保证这六张表设计到位。2.2 各表核心字段与设计说明用户表userCREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, phone varchar(20) DEFAULT NULL COMMENT 手机号, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, role tinyint(4) DEFAULT 0 COMMENT 角色 0-用户 1-管理员, status tinyint(4) DEFAULT 1 COMMENT 状态 0-禁用 1-启用, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted tinyint(4) DEFAULT 0 COMMENT 逻辑删除标记, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;密码字段长度给100是因为BCrypt加密后密文长度固定60位左右留点余量。role字段用tinyint而不是字符串是为了后续扩展更多角色类型时不需要改表结构。这里特别注意username要加唯一索引不然注册接口并发时会插入重复数据。商品分类表categoryCREATE TABLE category ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 分类名称, parent_id bigint(20) DEFAULT 0 COMMENT 父分类ID0表示顶级, sort_order int(11) DEFAULT 0 COMMENT 排序值越小越靠前, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;parent_id字段支持两级甚至多级分类爱琴海这种线下商场会涉及服饰、餐饮、亲子、影院、超市等多个品类二级分类的灵活性很有必要。商品表productCREATE TABLE product ( id bigint(20) NOT NULL AUTO_INCREMENT, category_id bigint(20) NOT NULL COMMENT 所属分类ID, name varchar(100) NOT NULL COMMENT 商品名称, subtitle varchar(200) DEFAULT NULL COMMENT 副标题, main_image varchar(500) DEFAULT NULL COMMENT 主图地址, detail text COMMENT 商品详情支持富文本HTML, price decimal(10,2) NOT NULL COMMENT 商品价格, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存, status tinyint(4) DEFAULT 1 COMMENT 状态 1-上架 0-下架, sales int(11) DEFAULT 0 COMMENT 销量, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;price用decimal(10,2)这里不展开说为什么不用double整个项目里涉及金额计算的字段全部统一用decimal避免浮点精度丢失导致对账出问题。detail字段用text类型存储富文本管理端可以直接用富文本编辑器提交HTML内容前端v-html渲染。购物车表cartCREATE TABLE cart ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL COMMENT 用户ID, product_id bigint(20) NOT NULL COMMENT 商品ID, quantity int(11) NOT NULL DEFAULT 1 COMMENT 数量, checked tinyint(4) DEFAULT 1 COMMENT 是否选中 1-选中 0-未选中, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_product (user_id,product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;购物车表设计上做了一步很关键的处理user_id和product_id加了联合唯一索引。这个设计避免同一用户反复添加同一个商品时产生多条记录后端在加购接口里先查询是否存在记录存在就累加数量不存在就插入新记录。订单表order和订单明细表order_itemCREATE TABLE order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, user_id bigint(20) NOT NULL COMMENT 下单用户ID, total_price decimal(10,2) NOT NULL COMMENT 订单总金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态 0-待支付 1-待发货 2-待收货 3-已完成 4-已取消, receiver_name varchar(50) NOT NULL COMMENT 收货人姓名, receiver_phone varchar(20) NOT NULL COMMENT 收货人电话, receiver_address varchar(200) NOT NULL COMMENT 收货地址, pay_time datetime DEFAULT NULL COMMENT 支付时间, delivery_time datetime DEFAULT NULL COMMENT 发货时间, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id bigint(20) NOT NULL AUTO_INCREMENT, order_id bigint(20) NOT NULL COMMENT 订单ID, product_id bigint(20) NOT NULL COMMENT 商品ID, product_name varchar(100) NOT NULL COMMENT 商品名称快照, product_image varchar(500) DEFAULT NULL COMMENT 商品图片快照, current_price decimal(10,2) NOT NULL COMMENT 下单时价格快照, quantity int(11) NOT NULL COMMENT 购买数量, total_price decimal(10,2) NOT NULL COMMENT 小计金额, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单表和订单明细表为什么要拆开这是电商系统里的核心设计。一个订单对应多个商品把订单的公共信息收货人、总金额、状态放主表把每个商品的详细信息放明细表避免一条订单记录里存多个商品导致的数据冗余和查询困难。订单明细表里把商品名称、图片、价格都做了一次快照存储这是非常关键的设计。因为商品的价格和名称后续可能修改但对某笔订单来说用户下单时看到的价格和商品信息应该是不可变的所有订单历史必须和当时的商品信息保持一致。如果下单时不做快照等用户查询历史订单时商品已经改价或者下架订单展示就乱套了。2.3 表关系梳理与理解通过外键逻辑关系梳理用户和购物车一对多一个用户可有多条购物车记录用户和订单一对多一个用户可有多个订单订单和订单明细一对多一个订单包含多个明细分类和商品一对多一个分类下有多个商品这套关系不算复杂但已经覆盖电商主链路。用Navicat或者MySQL Workbench生成E-R图后整个项目的数据流会非常清晰。实际开发时可以不用物理外键靠逻辑关联维护关系因为物理外键在数据量大、并发高时会带来额外的索引开销和锁竞争但目前这个体量的项目加不加物理外键影响不大。3. SpringBoot后端核心模块实现3.1 后端项目结构与分层架构后端项目我习惯按功能模块建包而不是按技术层建包。按技术层分包会出现controller包下面有几十个文件的情况而按模块分包则是每个业务模块把Controller、Service、Mapper放在一起阅读代码时上下文是聚合的。com.aegis.mall ├── config # 配置类包含跨域配置、拦截器配置 ├── controller # 接口层 │ ├── UserController │ ├── ProductController │ ├── CartController │ ├── OrderController │ └── AdminController ├── service # 业务逻辑层 │ ├── impl ├── mapper # MyBatis数据访问层 ├── entity # 实体类 ├── dto # 请求/响应对象 ├── common # 通用类统一返回结果、异常处理、常量 ├── util # 工具类JWT工具等Dao层使用了MyBatis-Plus这是MyBatis的增强工具内置了常用的增删改查方法像selectById、selectPage这些不用写SQL针对复杂查询再写自定义SQL。对于商城这种CRUD居多的系统开发效率提升很明显。分层架构的核心是Service层要负责业务逻辑的判断Controller只做参数接收和结果返回。比如下单这个操作减库存、生成订单号、创建订单明细这些耦合的操作都在Service的同一个事务方法里完成而不是拆到Controller里分散处理。统一返回结果封装是前后端分离项目里必须做的一件事。我定义了一个Result类包含code、message、data三个字段接口执行成功返回code200业务异常返回自定义业务错误码系统异常返回500。前端axios拦截器统一处理code遇到特定码跳转登录遇到业务错误码弹出提示这样的好处是前后端约定清晰不会出现接口一会儿返回对象一会儿返回string的情况。3.2 用户登录与JWT鉴权登录是商城系统的入口。密码存储使用BCrypt加密不使用MD5因为MD5加盐也要自己实现而且彩虹表攻击风险大。Spring Security的BCryptPasswordEncoder是专门做密码哈希的同一密码每次加密结果不同因为内部自动生成了随机盐校验时用matches方法即可。public class JwtUtil { private static final String SECRET your-secret-key; private static final long EXPIRE 7 * 24 * 60 * 60 * 1000L; public static String generateToken(Long userId, String username, Integer role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(username, username) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } }登录成功后在Controller返回token前端存储到localStorage后续所有请求在axios拦截器中携带Authorization请求头。后端用拦截器统一处理token校验public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.hasText(token)) { try { Claims claims JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, Long.valueOf(claims.getSubject())); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { // token无效或过期 } } // 返回401状态码前端跳转登录页 response.setStatus(401); return false; } }写JWT拦截器时踩过一个坑从请求头拿token时前端习惯上是带Bearer 前缀后端解析时必须先把前缀去掉再解析否则会抛JWT解析异常。这个细节在和前端联调时经常会导致后端日志一堆报错排查半天才发现是前缀没处理。管理端的接口权限校验再加一层角色判断拦截器里检查claims中的role字段管理员接口只允许role1访问。3.3 商品模块与购物车模块实现商品列表接口的分页和搜索是商城系统的常客。使用MyBatis-Plus的分页插件只需要在配置类里注册PaginationInnerInterceptor然后调用selectPage方法即可。搜索商品用LambdaQueryWrapper拼接like条件public PageProduct getProductPage(int pageNum, int pageSize, String keyword, Long categoryId) { PageProduct page new Page(pageNum, pageSize); LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.eq(Product::getStatus, 1); // 只查上架商品 if (StringUtils.hasText(keyword)) { wrapper.like(Product::getName, keyword).or().like(Product::getSubtitle, keyword); } if (categoryId ! null) { wrapper.eq(Product::getCategoryId, categoryId); } wrapper.orderByDesc(Product::getSales); return productMapper.selectPage(page, wrapper); }有一点要注意keyword判断必须用StringUtils.hasText不能直接用! null因为前端不传搜索词的时候keyword是空字符串直接用isNotEmpty之类的判断会在分页查询时组装出一个错误的search条件。购物车接口相对简单加购逻辑是核心Transactional public void addToCart(Long userId, Long productId, Integer quantity) { // 1. 校验商品是否存在且上架 Product product productMapper.selectById(productId); if (product null || product.getStatus() ! 1) { throw new BusinessException(商品不存在或已下架); } // 2. 查询购物车是否已有该商品记录 LambdaQueryWrapperCart wrapper new LambdaQueryWrapper(); wrapper.eq(Cart::getUserId, userId).eq(Cart::getProductId, productId); Cart cart cartMapper.selectOne(wrapper); if (cart null) { // 3. 没有则新建购物车记录 cart new Cart(); cart.setUserId(userId); cart.setProductId(productId); cart.setQuantity(quantity); cartMapper.insert(cart); } else { // 4. 已有则累加数量 cart.setQuantity(cart.getQuantity() quantity); cartMapper.updateById(cart); } }这里的Transactional注解保证了整个加购操作的原子性查询、判断、插入或更新任何一个环节报错都会回滚不会出现购物车里插入一半数据的情况。但是要注意一点购物车加购时只做了商品存在性校验没有校验库存库存校验留在提交订单时做这样更合理。3.4 订单模块与支付流程下单流程是整个系统最核心、最容易出bug的部分。基本流程是前端提交购物车中勾选商品的id列表和收货地址信息后端在同一个事务里完成以下步骤查询购物车中选中的商品记录组装商品id和数量列表校验每个商品的库存是否足够不足则抛出异常中断下单生成唯一订单号计算订单总金额扣减商品库存写入订单主表写入订单明细表清空购物车中对应的商品记录生成订单号是这里面的一个技术细节。直接用数据库自增id当订单号暴露给用户不合理因为会泄漏订单量格式不美观。我这里采用时间戳加随机数的方式public static String generateOrderNo() { SimpleDateFormat sdf new SimpleDateFormat(yyyyMMddHHmmss); String timestamp sdf.format(new Date()); int random (int) ((Math.random() * 9 1) * 1000); return timestamp String.valueOf(random); }在并发量不大的场景下这种方式够用但严格来说如果同一秒内并发下单超过一万笔随机数可能碰撞。更稳妥的方案是使用Redis的INCR命令按天自增生成类似202501011200000001的订单号。库存扣减的SQL需要注意并发超卖问题。直接用update product set stock stock - #{quantity} where id #{id} and stock #{quantity}通过数据库的行锁和条件判断保证库存不会减成负数。这也是实际开发中常用的乐观锁思路的一种变形不用专门加version字段因为库存这个业务本身就适合用条件更新来实现。支付模块在毕设或练习场景下通常做模拟支付前端点击支付后请求后端pay接口后端把订单状态从待支付改为待发货记录支付时间模拟支付回调。如果要对接真实支付微信支付和支付宝都要求企业资质个人开发者在沙箱环境测试即可支付宝的沙箱环境可以申请接口对接逻辑和真实环境几乎一致。4. Vue前端页面与联调4.1 Vue项目搭建与目录结构前端我选择Vite作为构建工具。Vite基于ES Module开发环境冷启动秒开热更新速度快相比Vue CLI的Webpack构建环境体验好非常多。如果还在用Vue CLI更新Vite后你会明显感觉到开发效率提升。src ├── api # 接口请求封装 │ ├── product.js │ ├── cart.js │ ├── order.js │ └── user.js ├── assets # 静态资源 ├── components # 通用组件 │ ├── Header.vue │ ├── ProductCard.vue │ └── Pagination.vue ├── router # 路由配置 │ └── index.js ├── store # Pinia状态管理 │ ├── user.js │ └── cart.js ├── views # 页面组件 │ ├── home │ ├── product │ ├── cart │ ├── order │ ├── user │ └── admin ├── utils # 工具函数 │ └── request.js (axios封装) ├── App.vue └── main.js状态管理这里用了Pinia。Vue 3官方推荐Pinia替代Vuex它的API更简洁去除mutations概念直接在store里定义state、getters、actions而且天然支持TypeScript。购物车角标数量、用户登录状态这些全局共享数据放store里管理非常合适。4.2 核心页面与路由实现前端路由需要配置两个类型用户端页面和管理端页面。管理端页面必须做权限控制只有角色为管理员的用户才能访问。Vue Router的导航守卫是很方便的权限控制手段。router.beforeEach((to, from, next) { const userStore useUserStore(); if (to.path.startsWith(/admin) userStore.role ! 1) { next(/login); return; } if (to.meta.requiresAuth !userStore.token) { next(/login); return; } next(); });早期的前端权限控制方案是后端返回当前用户的菜单列表前端动态添加路由逻辑稍显复杂。对于这个体量的项目直接在前端判断角色和路由路径就足够可靠。商品详情页的图片展示用el-carousel轮播组件商品描述区域直接把后端返回的HTML片段用v-html渲染。这里有一个安全知识点v-html渲染不受信任的HTML会有XSS风险但商城场景下商品详情的HTML来自管理端富文本编辑器内容是管理员录入的所以风险可控。购物车页面需要处理一个典型的交互选中商品、修改数量、实时计算总价。用watch监听购物车列表的变化重新计算选中商品的小计和总金额。这里要注意修改数量后要节流提交后端更新避免每次点击加减都发请求导致后端压力大。4.3 前后端联调与跨域处理开发环境下前端运行在5173端口后端运行在8080端口跨域问题是绕不开的。跨域解决方式非常多后端配置是最省事的方案。SpringBoot里通过实现WebMvcConfigurer的addCorsMappings方法来配置全局跨域Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意allowCredentials(true)和allowedOriginPatterns不能和allowedOrigins(*)同时使用因为允许携带凭证的跨域请求不能使用通配符来源。如果不加allowCredentials前端请求就带不上cookie但这套系统用JWT做token认证不依赖cookie所以会把token放在Authorization头里来传。axios请求封装是联调时沟通成本最低的方案。统一设置baseURL请求拦截器里加token响应拦截器里统一处理错误码和401跳转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 ! 200) { ElMessage.error(res.message); if (res.code 401) { router.push(/login); } return Promise.reject(new Error(res.message)); } return res; }, error { ElMessage.error(网络异常请稍后重试); return Promise.reject(error); } );联调时最烦的是后端接口写好后前端拿到数据后发现字段对不上。比如后端返回驼峰命名前端写了个下划线命名去取页面一片空白。规范的做法是前端所有接口请求都封装在src/api目录下每个接口清楚标注请求方式和参数类型后端每个字段严格遵循统一的命名规范。5. 常见问题与避坑指南5.1 LocalDateTime序列化问题SpringBoot默认使用Jackson序列化LocalDateTime如果前端不指定格式默认格式是2025-01-01T10:30:00这个格式前端展示时通常不符合要求。最简单的解决方案是在实体类的LocalDateTime字段上加JsonFormat注解也可以全局配置JacksonBean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder - builder.simpleDateFormat(yyyy-MM-dd HH:mm:ss); }这个坑是在做订单列表接口时遇到的前端拿到时间字段格式不对排了半天没发现是后端序列化问题最后加上全局时间格式化才解决。5.2 金额计算精度问题这个前面提过但值得单独强调。商城项目中金额计算贯穿购物车、下单、订单查询多个环节如果用了double类型0.10.20.30000000000000004这种精度问题一定会出现。整个项目所有金额相关字段用BigDecimal并且购物车结算时把每个商品的单价乘以数量累加全部在BigDecimal的运算方法里完成不经过浮点转换。一个小细节BigDecimal构造时推荐使用BigDecimal.valueOf(double)或者new BigDecimal(String)不要直接new BigDecimal(double)否则会得到二进制浮点数的精确值依旧有精度问题。5.3 库存超卖与并发控制商城系统的经典问题。下单时不校验库存直接用update减库存并发场景下会出现库存变负数的情况。解决方案是在扣库存SQL上加上stock #{quantity}的条件判断让数据库层面的行锁来保证安全。实际项目中更复杂的场景需要引入Redis分布式锁或者乐观锁但这个商城系统这样处理已经够用。写这段代码时要保证扣减库存和创建订单在同一个事务里并且事务的隔离级别不会造成脏读。5.4 前端部署方案前后端分离项目部署有两种常见方式。一种是前端打包成dist目录后用Nginx托管API请求通过Nginx反向代理到后端服务生产环境用Nginx配置location /api/的代理规则。另一种是直接把前端构建产物拷贝到SpringBoot的src/main/resources/static目录然后一起打成jar包运行相当于SpringBoot既提供API又托管静态页面。第二种方式适合毕设演示和个人项目因为只需要部署一个jar包不用单独配Nginx。但要注意如果前端使用了vue-router的history模式访问非根路径刷新页面时后端需要配置转发否则会404。SpringBoot中适配的方法是把所有非API路径转发到index.htmlController public class PageForwardController { RequestMapping(value {/, /{path:[^\\.]*}}) public String forward() { return forward:/index.html; } }这个配置的意思是除了包含点号的文件路径外所有路径都转发到index.html让Vue Router接管路由。如果不做这个处理在商品详情页刷新一下就白屏是非常影响体验的问题。5.5 图片上传与访问路径管理端商品图片上传开发时我把图片保存到本地磁盘目录然后通过一个静态资源映射接口对外暴露访问。SpringBoot配置类里加一行registry.addResourceHandler(/upload/**).addResourceLocations(file: uploadPath);部署到服务器后要注意上传路径的绝对路径配置不要用相对路径因为jar包运行时的工作目录不固定相对路径容易出错。更合理的方式是把图片存到OSS等对象存储但本地磁盘方案在课程设计和毕设场景足够用了。5.6 前端路由刷新404问题这个坑是部署后最容易踩到的。开发模式下Vue Router的history模式不会有问题因为Vite的开发服务器自己会处理历史回退。但是部署到Nginx或SpringBoot静态托管后刷新非首页路径就会404原因在于服务器不知道前端路由的存在只会按路径去找文件。Nginx场景的解决方案是配置try_files指令location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; }SpringBoot场景的解决方案就是上面提到的页面转发Controller。这两个方案二选一根据你的部署方式来决定。5.7 购物车刷新后丢失如果购物车数量只保存在前端store里刷新页面state就被清空了导致购物车数据丢失。解决办法有两个方向一是登录用户每次进入页面时调用后端接口拉取购物车列表把store初始化二是把购物车数据也持久化到localStorage。最合理的做法是用前者以后端数据为准前端store只做展示层的缓存。当时做购物车角标的实时更新时也遇到了一个问题用户在不同页面之间切换购物车数量需要保持一致。最终通过Pinia全局管理购物车数量每次购物车接口返回新数据时更新数量页面组件统一从store读取。最后说几点个人心得做这类前后端分离的商城系统最容易陷入的误区是先把所有页面写完再回头写后端接口最后对接口时发现前端需要的字段后端根本没返回改造工作量巨大。正确的方式是先把接口文档定好字段名、返回结构、错误码全部明确前后端可以并行开发。哪怕没有正式的Swagger文档至少要在api目录的注释里把接口和参数写清楚。另一个值得注意的经验是商城系统虽然功能看着多但核心永远是订单流程。购物车、商品、用户模块都是为订单服务的设计时把订单相关的事务边界、状态流转想清楚代码写起来会顺畅很多。订单状态机的设计建议在动手编码前先用文字把每个状态的触发条件和流转路径描述清楚。还有一点如果这是毕设项目答辩时老师大概率会问数据库设计为什么这么建、库存扣减怎么防超卖、token过期了怎么办这类问题。把这些问题背后的原理弄明白比堆功能有意义得多。这套系统的扩展空间其实也很大比如接入真实支付、增加秒杀模块、引入消息队列做订单异步处理、用Redis做热点数据缓存这些都是后续可以继续深入的方向。如果做完了基础版还想提升项目亮点优先从缓存和性能优化角度入手性价比最高。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑