基于Vue3+SpringBoot的前后端分离电商系统设计与实现:以雪地装备销售平台为例
简介一套基于Vue.js、SpringBoot与MySQL构建的雪地装备销售系统成品源码面向计算机专业毕业设计或课程设计场景可直接作为高评分项目提交。系统覆盖商品展示、搜索、购买、订单处理、支付接口及用户注册登录等完整电商流程采用前后端分离架构前端以Vue组件化界面为主后端基于SpringBoot处理业务逻辑MySQL负责数据持久化。资源包共639个文件约25.9MB主要包含java后端源码、vue前端页面、svg图标、xml配置、sql数据库脚本以及docx论文、pptx答辩演示、txt说明、bat启动脚本等既有可直接导入IDE运行的工程结构也有讲解视频和二次开发教程便于快速上手与扩展。目前已有32人学习下载适合需要完整项目参考、论文配套或希望学习前后端分离开发流程的开发者使用。1. VueSpringBoot雪地装备销售系统一份能直接进答辩的完整底子做电商类毕业设计最怕的就是选通用商城题千篇一律不说数据库设计和业务逻辑都讲不出深度。雪地装备这个垂类反而讨巧——商品SKU少但规格明确雪板长度、固定器尺寸、雪靴码数都有具体属性库存、订单、统计能讲出细节又不至于被复杂类目体系拖累。这套基于VueSpringBootMySQL的雪地装备销售系统前台覆盖注册登录、商品浏览、搜索分页、购物车、订单结算后台覆盖商品管理、订单管理、数据统计接口按RESTful风格设计拿到手导库就能跑通。适合两类人一是需要一份能答辩的完整系统的在校同学二是想拆解前后端分离电商项目、理顺技术与业务分层的新手。下面我按架构、后端、前端、避坑、验证这条线逐一拆开讲。2. 技术选型与前后端分离架构Vue 3与SpringBoot如何分工2.1 前后端分离的请求链路从页面点击到数据库返回这套系统用的是标准前后端分离结构前端跑在Vite默认端口5173后端SpringBoot跑在8080两者通过HTTP接口通信不走模板渲染。前端工程基于Vue 3组合式APIUI框架是Element Plus状态管理用Pinia后端是SpringBoot 2.x加MyBatis Plus数据库MySQL 8.0。之所以这么选是因为Vue 3加Element Plus的标签页、表格、分页组件都现成适合快速搭建商品管理和订单管理后台MyBatis Plus则把单表CRUD的重复代码省掉让代码量维持在答辩能讲清楚的范围。完整链路是用户点开商品列表页Vue页面生命周期钩子触发loadList()函数axios请求带上认证token访问后端GET /api/product/list接口SpringBoot的Controller接收参数后转给ServiceService调用Mapper查询MySQL数据逐层返回到前端渲染。这个链路里每一层职责都不重叠前端只管UI交互和数据展示后端只管业务规则数据库只管存储。答辩被问到“前后端是怎么联通的”时建议按三条线回答先说链路顺序再说其中一个具体接口的参数和返回结构最后落到一次完整购买场景。比如以商品详情为例/api/product/detail/{id}返回商品名称、价格、库存、图片、描述前端详情页拿到后渲染点“加入购物车”时再请求购物车接口。能串起一条完整业务场景的回答比背项目介绍强得多。2.2 数据库表设计六张核心表与字段边界这套系统的数据库是典型的电商五表结构加一张分类表分别支撑用户、商品、购物车、订单四块业务。表与表之间的关系很清晰用户与购物车是一对多购物车与商品是多对一订单与订单明细是一对多商品与分类是多对一。表名用途关键字段说明user前台用户与后台管理员id, username, password, nickname, phone, role, avatarrole区分user与admincategory商品分类id, name雪板、固定器、雪靴、护具等product雪地装备商品id, name, category_id, price, stock, image, description, status通过status控制上下架cart购物车id, user_id, product_id, num, checkedchecked控制是否参与结算orders订单主表id, order_no, user_id, total_price, status, address, phone, create_time状态用0到4的数字表示order_item订单明细id, order_id, product_id, product_name, price, num, image保存下单时的商品快照字段设计上有几个细节值得抄作业。价格字段一定要用decimal(10,2)不要用float或double否则金额出现精度丢失结算金额和数据库对不上时非常难排查。库存字段用int类型但这只是基础后面第3章会讲到并发场景下的库存控制。订单号order_no要加唯一索引生成规则一般用时间戳加随机数或UUID防止并发下单撞号。订单明细表单独拆出来的原因很关键商品的价格和名称随时可能修改但用户下单那一刻的快照必须固定。如果不建order_item而是直接通过订单表关联商品表去查历史订单商品改价后历史订单金额就会跟着变这是电商系统的大忌也是评审容易追问的点。订单主表里的status字段用0到4五个数字表示状态流转后面第3章会展开讲状态机设计。2.3 目录结构与本地启动顺序拿到资源后第一件事不是看界面截图而是先打开前后端工程目录确认目录层次和启动入口。前端工程和后端工程通常是两个独立目录结构大致如下前端工程/ ├─ src/ │ ├─ api/ # axios请求封装按模块拆分 │ ├─ views/ # 页面商品列表、详情、购物车、结算、后台管理 │ ├─ components/ # 复用组件分页、轮播图、商品卡片 │ ├─ router/ # 路由表与路由守卫 │ ├─ stores/ # Pinia状态管理 │ ├─ App.vue │ └─ main.js └─ vite.config.js # 开发代理与构建配置 后端工程/ ├─ src/main/java/ │ └─ com/xxx/shop/ │ ├─ controller/ # 接收HTTP请求 │ ├─ service/ # 业务逻辑处理 │ ├─ mapper/ # 数据访问层 │ ├─ entity/ # 数据库实体类 │ ├─ config/ # 跨域、拦截器、静态资源映射 │ └─ utils/ # JWT工具、订单号生成等 ├─ src/main/resources/ │ ├─ application.yml # 数据源、端口、MyBatis配置 │ └─ mapper/ # 复杂SQL的XML文件 └─ pom.xml本地启动顺序有讲究顺序反了会看到一堆假报错。正确顺序是先建MySQL数据库并导入SQL脚本再启动后端最后启动前端。代码块里是完整的三步# 1. 先初始化数据库字符集务必用utf8mb4 mysql -u root -p -e CREATE DATABASE IF NOT EXISTS snow_shop DEFAULT CHARACTER SET utf8mb4; mysql -u root -p snow_shop snow_shop.sql # 2. 启动后端默认端口8080 cd snow-shop-backend mvn spring-boot:run # 3. 启动前端开发服务器端口5173 cd snow-shop-frontend npm install npm run dev第一步里的字符集很多人会忽略直接用命令行创建默认库结果商品名里的生僻字或特殊符号变成乱码后面怎么改都改不干净。其实从建库开始显式指定utf8mb4能省掉后续一大半编码问题。第二步的mvn spring-boot:run首次执行会下载依赖耗时较长是正常的不要中途强行中断。第三步的npm install如果报peer dependency版本冲突优先看package.json里Vite和Vue的版本搭配不要急着换Node版本很多环境坑都是Node版本和Vite不匹配导致编译失败。提示初次跑通后建议把三个终端窗口的启动报错都截图保留答辩时可以讲“我遇到过依赖冲突通过锁定版本解决”这类实操细节比背诵概念更有说服力。3. SpringBoot后端从登录鉴权到库存扣减三个核心业务一次讲透3.1 登录鉴权JWT生成与拦截器校验前端和后端分离部署意味着后端不能依赖HttpSession保存登录状态因为浏览器直接访问的是前端地址后端接口是无状态的。这套系统采用的常见方案是JWT用户登录成功后后端签发一个token前端把token存进localStorage后续每个请求在请求头里带上Authorization: Bearer token后端通过拦截器校验token并解析出用户ID和角色。token生成工具类代码如下// JwtUtil.java —— token生成与解析 public class JwtUtil { private static final String SECRET snow-shop-secret-key; /** * 生成token过期时间2小时 * payload里放userId和role后端拦截器直接取用 */ public static String createToken(Integer userId, String role) { long now System.currentTimeMillis(); return Jwts.builder() .claim(userId, userId) .claim(role, role) .setIssuedAt(new Date(now)) .setExpiration(new Date(now 2 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }逻辑说明token的payload里只放userId和role不放密码、手机号这类敏感信息token体量小传输效率高。过期时间2小时对毕业设计演示足够如果希望演示更流畅改成7天也完全可以。parseToken在token过期或签名非法时会抛异常由拦截器统一捕获不要把异常堆栈直接抛给前端。拦截器校验是这套系统权限控制的枢纽// AuthInterceptor.java —— 请求拦截与身份解析 public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 预检请求直接放行避免跨域时的OPTIONS请求被拦截 if (OPTIONS.equals(request.getMethod())) { return true; } String auth request.getHeader(Authorization); if (auth null || !auth.startsWith(Bearer )) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } try { Claims claims JwtUtil.parseToken(auth.substring(7)); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } } }参数说明代码里先用请求方法判断OPTIONS这是跨域场景的必踩点——浏览器发复杂请求前会先发一次预检如果预检被拦截后面真正的GET或POST根本到不了后端。解析token成功后把userId和role放进request属性后续Service层直接通过参数拿到当前用户不用为了拿用户信息再查一次数据库。登录接口本身还有一个常被忽略的细节user表里的password字段存的是BCrypt加密后的密文不是明文。登录时用BCryptPasswordEncoder.matches()校验明文与密文是否一致。很多毕设为了省事直接用MD5存储答辩老师一问“哈希冲突和加盐怎么处理”就容易卡壳。3.2 商品库存扣减事务加条件更新防止超卖电商系统里最容易被追问超卖问题。假设商品库存只有3件4个用户同时下单如果每个请求都先查库存再减库存查询到库存充足然后同时执行扣减最后库存会变成负数。这套系统里用的方案是条件更新加事务控制// OrderServiceImpl.java —— 下单主流程 Override Transactional(rollbackFor Exception.class) public Integer createOrder(OrderCreateDTO dto, Integer userId) { // 1. 创建订单主表初始状态为待付款 Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setStatus(OrderStatus.PENDING_PAYMENT); // 2. 循环商品明细扣库存、生成订单明细 for (OrderItemDTO item : dto.getItems()) { // 关键SQL语句 // UPDATE product SET stock stock - #{num} // WHERE id #{productId} AND stock #{num} int rows productMapper.deductStock(item.getProductId(), item.getNum()); if (rows 0) { throw new BusinessException(商品库存不足下单失败); } OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(item.getProductId()); orderItem.setNum(item.getNum()); orderItem.setPrice(productMapper.selectPrice(item.getProductId())); orderItemMapper.insert(orderItem); } // 3. 计算总金额并发写入订单表 BigDecimal total calcTotal(dto.getItems()); order.setTotalPrice(total); orderMapper.insert(order); return order.getId(); }逻辑说明这里的核心不是“先查询再判断”而是让UPDATE语句自带条件stock #{num}。MySQL执行update时会对匹配行加锁在并发场景下两个事务同时执行这条语句时会排队后执行的事务检查到库存不足受影响行数为0代码抛出业务异常事务回滚。这样既避免了超卖又不用手写悲观锁代码量最少也最容易在答辩时讲清楚。事务注解Transactional必须加在public方法上且方法内部所有数据库操作都在同一个事务里。这里有一个非常隐蔽的坑Spring事务基于代理如果同一个类里A方法调用B方法B上的Transactional不生效也就是所谓的自调用失效。所以下单、取消订单这类强事务方法我一般会放到独立的OrderService里避免被内部调用绕过代理。参数说明deductStock这个Mapper方法通过update语句操作要注意返回值的含义——MyBatis的update返回的是受影响行数而不是修改后的库存值。用受影响行数判断库存是否充足是最可靠的方案如果返回0还需要区分是库存不足还是商品不存在所以商品ID的合法性要在进入循环前先校验。3.3 订单状态机与统计接口状态流转收敛到一个方法里订单模块最容易把自己绕晕的地方就是状态。一套系统里存在多个页面都会改动订单状态用户取消、模拟支付、管理员发货、用户收货如果每个接口都自己判断后续维护时会出现一种经典脏数据待收货状态的订单被取消了。订单状态设计成数字枚举流转规则统一收敛在一个方法里// OrderStatus.java —— 订单状态与流转校验 public class OrderStatus { public static final int PENDING_PAYMENT 0; // 待付款 public static final int PENDING_SHIPMENT 1; // 待发货 public static final int PENDING_RECEIPT 2; // 待收货 public static final int COMPLETED 3; // 已完成 public static final int CANCELLED 4; // 已取消 /** * 判断状态能否从 from 流转到 to */ public static boolean canChange(int from, int to) { switch (from) { case PENDING_PAYMENT: return to PENDING_SHIPMENT || to CANCELLED; case PENDING_SHIPMENT: return to PENDING_RECEIPT; case PENDING_RECEIPT: return to COMPLETED; default: return false; } } }参数说明规则很简单——待付款可以模拟支付进入待发货也可以取消待发货只能发货进入待收货待收货只能确认完成。所有操作订单状态的Service方法执行update前先调一次canChange不满足就抛异常。这样状态流转逻辑只有这一个入口新增状态时也只需改这一个类。后台首页的统计接口是这套系统展示效果的重点一般包含今日订单数和今日销售额、近七天订单趋势、热门商品榜。统计类SQL常见写法如下-- 今日订单数与销售额排除已取消订单 SELECT COUNT(*) AS orderCount, COALESCE(SUM(total_price), 0) AS salesAmount FROM orders WHERE create_time CURDATE() AND status ! 4; -- 近七天热门商品排行 SELECT p.name, SUM(oi.num) AS saleCount FROM order_item oi LEFT JOIN product p ON oi.product_id p.id WHERE oi.create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY oi.product_id ORDER BY saleCount DESC LIMIT 5;这里有两个容易翻车的细节。第一SUM()在订单表为空时返回NULL前端拿到NULL后显示会异常所以套一层COALESCE(..., 0)兜底。第二统计“今日”时数据库的时区必须和业务时区一致否则晚上11点下单会算到第二天这也是第5章会单独讲的时区坑。首页图表如果不满足于简单折线图还可以扩展一个“近7日订单趋势”接口按时间分组统计每日订单数前端用ECharts展示。模板值这块改动起来非常方便后端加一个按日分组的SQL即可。4. Vue前端商品浏览、购物车与结算的交互实现4.1 路由守卫与登录态持久化前端交互层面最先要解决的是登录态。用户登录成功后后端返回token前端把它同时写进Pinia状态和localStorage。Pinia负责当前会话里的状态同步localStorage负责页面刷新后恢复登录态。两者缺一不可只写localStorage不做状态同步会导致组件响应式失效只写Pinia不做持久化会导致F5刷新后登录态丢失。路由守卫的写法如下// router/index.js —— 路由守卫控制页面访问 import { createRouter, createWebHistory } from vue-router import { useUserStore } from ../stores/user const router createRouter({ history: createWebHistory(), routes: [ { path: /login, component: () import(../views/Login.vue) }, { path: /product/list, component: () import(../views/ProductList.vue) }, { path: /product/detail/:id, component: () import(../views/ProductDetail.vue) }, { path: /cart, component: () import(../views/Cart.vue), meta: { requiresAuth: true } }, { path: /order/confirm, component: () import(../views/OrderConfirm.vue), meta: { requiresAuth: true } }, { path: /admin, component: () import(../views/admin/AdminLayout.vue), meta: { requiresAuth: true, role: admin } } ] }) router.beforeEach((to, from, next) { const userStore useUserStore() const token userStore.token || localStorage.getItem(token) // 需要登录但没token跳登录页并记录来源路径 if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) return } // 需要管理员角色但当前用户不是admin跳403页面 if (to.meta.role userStore.role ! to.meta.role) { next({ path: /403 }) return } next() })逻辑说明meta字段里的requiresAuth控制需要登录的页面role控制管理员专属页面。两种判断分开写是因为后台管理页不仅要求登录还要求角色为admin。这里最容易出现的坑是刷新页面后Pinia状态丢失userStore里的role变成空值导致登录用户访问后台被误判为无权限。解决办法是在Pinia的store定义里初始化state时就从localStorage读取而不是等登录后统一写入state: () ({ token: localStorage.getItem(token) || , role: localStorage.getItem(role) || })4.2 商品列表、搜索与分页组件商品列表页是前台流量的入口承担浏览、搜索、分页三个功能。列表数据由后端分页返回前端只负责传参和渲染。前端组合式API的写法如下// views/ProductList.vue —— 商品列表逻辑 script setup import { ref, onMounted } from vue import { getProductList } from ../../api/product const list ref([]) // 商品列表 const total ref(0) // 总数 const keyword ref() // 搜索关键词 const currentPage ref(1) // 当前页 const pageSize ref(8) // 每页条数 async function loadList() { const params { keyword: keyword.value, page: currentPage.value, pageSize: pageSize.value } const { data } await getProductList(params) list.value data.records total.value data.total } onMounted(loadList) /script参数说明后端接口返回的是符合MyBatis Plus分页规范的结构——records是当前页数据total是记录总数。前端拿到这两个字段后把total传给分页组件的total属性页面切换时把新页码赋给currentPage再调用一次loadList。搜索这里有个体验优化点不要每次输入一个字符就发一次请求否则后端压力大还容易出竞态。常见做法是加一个300毫秒防抖用户停止输入后再触发搜索。用Vue 3的watch加setTimeout实现即可watch(keyword, () { clearTimeout(timer) timer setTimeout(() { currentPage.value 1 loadList() }, 300) })列表页的商品卡片建议使用Element Plus的el-card加el-image组合商品图片用el-image的fitcover保持裁剪比例统一价格显示用红色加粗右下角一个“加入购物车”按钮交互清晰即可。4.3 购物车与结算联动前端算总价后端算信任购物车模块前端要做的事很明确展示勾选的商品、修改数量、显示合计金额、点击结算跳转。这里用Pinia管理购物车状态因为购物车数据在商品详情页和购物车页都可能被修改放组件内部会重复维护。// stores/cart.js —— 购物车状态管理 import { defineStore } from pinia export const useCartStore defineStore(cart, { state: () ({ items: [ // { productId, name, price, image, num, checked } ] }), getters: { // 只统计勾选商品的总价 checkedTotal: (state) state.items .filter(i i.checked) .reduce((sum, i) sum i.price * i.num, 0), checkedCount: (state) state.items .filter(i i.checked) .reduce((sum, i) sum i.num, 0) }, actions: { toggleCheck(productId) { const item this.items.find(i i.productId productId) if (item) item.checked !item.checked }, addItem(product) { const existing this.items.find(i i.productId product.id) if (existing) { existing.num 1 } else { this.items.push({ productId: product.id, name: product.name, price: product.price, image: product.image, num: 1, checked: true }) } } } })逻辑说明checkedTotal只统计勾选的商品这是购物车的核心逻辑——展示给用户看的合计金额必须和结算时产生的订单金额一致否则用户以为只要付50元下单后端算出80元体验非常差。addItem里同一个商品重复加入时累加数量而不是新增一条重复记录这也是购物车的标准行为。结算跳转时前端通过路由跳转到确认订单页把checkedCount、checkedTotal和勾选项列表通过临时状态或URL参数带过去。这里必须强调前端算出的总价仅用于展示提交订单后后端会重新遍历订单明细计算总价。所以第3章的下单接口代码里totalPrice由后端calcTotal重新计算而不是直接信任前端传的值。前后端各算一遍前端管体验后端管正确性这是电商设计的底线原则。支付环节这套系统用的是模拟支付点击“提交订单”后弹窗一个月选择“微信支付”或“支付宝支付”点击确认后把订单状态从待付款改成待发货并在订单列表里展示支付方式。这个改动量不大但对完整度提升非常明显。5. 避坑排查跨域、时间格式、上传路径四五个高频翻车点5.1 本地开发跨域CORS配置和预检请求现象前端页面打开正常但点击登录或加载商品列表时浏览器控制台报CORS error或Network Error后端日志却完全看不到请求进来。原因前端跑在5173端口后端跑在8080端口浏览器同源策略认为这是跨域请求在预检阶段就被拦截了。加上axios的Content-Type为application/json时属于复杂请求浏览器会先发OPTIONS预检后端没有处理OPTIONS请求就会失败。解决后端加一个全局CORS配置类把跨域放行规则写清楚// CorsConfig.java —— 全局跨域配置 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); } }参数说明这里有个细节allowedOrigins(*)和allowCredentials(true)同时配置时在某些版本会冲突报错换成allowedOriginPatterns(*)就可以兼容。开发阶段把maxAge设为3600浏览器会缓存预检结果避免每次请求都发两次页面响应会明显变快。如果后端配了CORS还是不行再用前端Vite代理兜底在vite.config.js里把/api前缀的请求代理到http://localhost:8080server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }5.2 时间字段相差8小时或显示成乱码现象订单列表里的创建时间比实际时间晚了8个小时比如晚上9点下单显示是下午1点或者数据库里明明存的2025-01-06 14:30:00前端接口返回的却是2025-01-06T14:30:00格式的字符串。原因两个问题叠加。第一Jackson序列化时间时默认按UTC时区输出导致本地时间被当成UTC处理后差了8小时第二没配置统一的日期格式Jackson输出ISO格式字符串前端直接展示出来就是带T的乱码。解决在application.yml里全局配置日期格式和时区同时给数据库连接串指定时区spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 datasource: url: jdbc:mysql://localhost:3306/snow_shop?serverTimezoneAsia/ShanghaicharacterEncodingutf8注意这里的time-zone: GMT8解决的是Jackson序列化时区问题serverTimezoneAsia/Shanghai解决的是JDBC驱动读取数据库时间时的时区问题两处都要配。如果只配其中一处时间还是会错乱8个小时。这个坑属于典型的两端不一致现象隐蔽排查起来费时配置到位以后基本不会再犯。5.3 商品图片上传成功前端打开却404现象后台新增商品时图片上传提示成功数据库里也存了图片路径但前端商品列表页展示图片时一直转圈或报404。原因图片被保存到了服务器磁盘的物理目录但SpringBoot默认只映射classpath:/static/目录下的静态资源上传目录不在Web应用的资源映射范围内浏览器访问不了。解决在后端配置类里增加一个资源映射把物理目录映射成Web访问路径// WebConfig.java —— 静态资源映射 Configuration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // /upload/** 映射到配置的磁盘目录 registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir); } }参数说明把上传目录放到application.yml里用file.upload-dir配置而不是在代码里写死盘符路径。写死D:/xxx/upload/的问题在于换电脑后路径不存在前端图片全部404。上传文件的保存路径建议动态拼接日期目录比如2025/01/06/uuid.jpg避免同一目录文件过多影响查找速度。图片上传后返回给前端的URL是/upload/2025/01/06/uuid.jpg这种相对路径前端在访问时自动拼接当前域名端口。5.4 取消订单不回补库存现象用户下单后库存减少取消订单后去后台看商品库存发现没有加回来多取消几单库存就变成负数。原因下单和取消订单是两条独立业务链路。下单时通过事务扣减库存但取消订单的逻辑里只更新了订单状态没有把商品数量加回库存或者回补库存的代码写在事务方法外异常时被吞掉了。解决取消订单必须走独立的Service方法加Transactional先校验订单当前状态确实为待付款再更新订单状态为已取消最后恢复库存Transactional(rollbackFor Exception.class) public void cancelOrder(Integer orderId, Integer userId) { Orders order orderMapper.selectById(orderId); // 只能取消待付款订单 if (order null || order.getStatus() ! OrderStatus.PENDING_PAYMENT) { throw new BusinessException(当前订单状态不可取消); } // 恢复库存UPDATE product SET stock stock #{num} WHERE id #{productId} for (OrderItem item : orderItemMapper.selectByOrderId(orderId)) { productMapper.restoreStock(item.getProductId(), item.getNum()); } orderMapper.updateStatus(orderId, OrderStatus.CANCELLED); }参数说明这里的顺序很重要先校验状态、再恢复库存、最后更新状态。如果先更新状态再恢复库存库存恢复SQL抛异常时状态已经改了回滚后状态恢复原样而库存也回到原来结果看起来一切正常但订单没有被取消先恢复库存再改状态库存更新失败则订单状态不变语义更清晰。5.5 数据库连接池超时空闲一段时间后请求报错现象系统刚启动时一切正常跑了一两个小时后页面操作突然报错后端日志出现Connection is not available, request timed out之类的信息。原因数据库连接长时间空闲后MySQL服务端会把连接回收断开但连接池并不知道这些连接已经失效依然把坏连接分配给新请求导致请求超时。解决给数据源连接池配置保活与空闲检测参数以Druid为例spring: datasource: druid: initial-size: 5 min-idle: 5 max-active: 20 test-while-idle: true time-between-eviction-runs-millis: 60000 validation-query: SELECT 1参数说明test-while-idle开启空闲连接检测time-between-eviction-runs-millis设为60秒每隔一分钟检查一次空闲连接validation-query用最简单的查询来验证连接是否可用。如果是HikariCP对应参数名是maximum-pool-size和max-lifetime不要把两套连接池的配置写混。这个坑在演示现场出现最尴尬因为不是必现问题测试时往往看不到答辩演示时偏偏就赶上了。提前把连接池保活参数配好能避免一场大型翻车。6. 把演示流程走成答辩主线验证清单与一处可落地的二次开发6.1 一套能直接上手的完整功能验证用例很多同学拿到资源后直接开始到处点点完发现功能都在但被评审老师问“你做过哪些测试”时说不出所以然。建议按下表把这套系统的主流程完整走一遍每一步确认预期结果场景操作步骤预期结果注册登录新用户注册后登录跳转首页导航栏显示用户名商品检索搜索“雪板”并翻页列表刷新分页总数正确购物车结算勾选商品提交订单生成订单号库存减少取消订单待付款订单取消订单变已取消库存加回后台发货管理员发货前台订单状态变待收货权限控制普通用户访问后台跳转403页面统计接口后台首页打开今日订单数与销售额正常显示这七条用例覆盖了登录鉴权、商品、购物车、订单状态机、库存回补、角色权限、统计查询七条核心链路也是答辩时最容易被演示到的功能点。6.2 二次开发给订单增加一个会员折扣字段拿到手的系统如果能做一处小改动答辩含金量会明显提升。最简单的是给订单加会员折扣在orders表加一个discount_amount字段下单时根据用户表中member_level字段计算折扣金额结算页展示原价、折扣、实付三个数字。改动范围集中在三处数据库加字段、后端calcTotal方法增加折扣逻辑、前端结算页增加一行折扣展示。这属于典型的低成本高展示价值功能。答辩现场老师通常不看代码走读而看你演示流程的熟练度和对业务逻辑的把控。这一组用例走完基本能证明你对库存、订单、权限三条核心链路是真的理解而不是只改了页面文案。从那以后我每次接触新的成品项目都会强制先跑一遍前端、后端、数据库三端联调的自测用例再动业务代码尤其是取消订单后的库存回滚一定放到最后单独验一遍。集成验证完再改后面出的问题才分得清是资源原本的问题还是自己改出来的问题。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

Betterfox 完整指南:5 分钟用 user.js 完成 Firefox 提速、隐私加固与界面净化

Betterfox 完整指南:5 分钟用 user.js 完成 Firefox 提速、隐私加固与界面净化

Betterfox 完整指南:5 分钟用 user.js 完成 Firefox 提速、隐私加固与界面净化 【免费下载链接】Betterfox Firefox user.js for optimal privacy and security. Your favorite browser, but better. 项目地址: https://gitcode.com/GitHub_Trending/be/Betterfox…

2026/10/14 2:01:18 阅读更多 →
准备 AI 方向校招:没大厂项目,面试官到底看什么

准备 AI 方向校招:没大厂项目,面试官到底看什么

授权与合规声明 本文为原创技术分享,所有示例代码均运行于作者自建的隔离环境,仅供学习交流。文中涉及的第三方模型与服务以各自官方文档为准,请在服务条款范围内使用。转载请保留出处。一、校招和社招筛人逻辑不同 很多同学准备 AI 岗校招时…

2026/10/14 2:01:18 阅读更多 →
Vue2+Node.js实现高并发团购系统:从开团到成团的全链路设计

Vue2+Node.js实现高并发团购系统:从开团到成团的全链路设计

简介:本资源是一套完整的仿拼多多团购商城全栈开发实战项目,面向前端与全栈初学者及Vue2.x、Node.js进阶学习者,旨在通过真实电商场景打通前后端开发闭环,解决技术碎片化、缺乏工程实践的问题。压缩包含2016个文件,主体…

2026/10/14 2:01:18 阅读更多 →

最新新闻

单片机毕设选题推荐:基于STM32的婴儿摇篮哭泣尿床监测与环境联动控制系统设计 基于物联网的婴儿床温湿度自动调节与哄睡装置设计(032707)

单片机毕设选题推荐:基于STM32的婴儿摇篮哭泣尿床监测与环境联动控制系统设计 基于物联网的婴儿床温湿度自动调节与哄睡装置设计(032707)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

2026/10/14 2:44:28 阅读更多 →
Lua数值类型全解析:整数浮点与整除取余避坑指南

Lua数值类型全解析:整数浮点与整除取余避坑指南

很多朋友第一次接触 Lua程序设计,是在某个配置文件或者游戏模组里。打开一个脚本,想改个数值看看效果,结果被注释符号、表达式、类型转换这些基础概念卡住了。我最初接触Lua也是因为类似场景——某项目的规则引擎用Lua做嵌入式脚本&#xff0…

2026/10/14 2:44:28 阅读更多 →
Workerman 5.x 完整实战指南:基于事件驱动的 PHP 异步网络框架

Workerman 5.x 完整实战指南:基于事件驱动的 PHP 异步网络框架

后端 【免费下载链接】workerman An asynchronous event driven PHP socket framework. Supports HTTP, Websocket, SSL and other custom protocols. 项目地址: https://gitcode.com/gh_mirrors/wo/workerman 点击查看 免费下载 Workerman 是一个使用纯 PHP 编写…

2026/10/14 2:44:28 阅读更多 →
uniPush离线推送实战:厂商通道配置与到达率优化指南

uniPush离线推送实战:厂商通道配置与到达率优化指南

1. 离线推送到底在解决什么问题做移动端应用的人迟早会撞上这个需求:用户把App划掉了、手机锁屏了、后台进程被系统回收了,这时候业务侧发出一条通知,用户还能不能在通知栏看到?答案就是离线推送要解决的核心命题。uniPush这套方案…

2026/10/14 2:44:28 阅读更多 →
单片机计算机毕设之基于物联网的婴儿床环境自动调节与哄睡联动装置设计 基于单片机的智能婴儿摇篮温湿度调节与远程监控系统设计(032707)

单片机计算机毕设之基于物联网的婴儿床环境自动调节与哄睡联动装置设计 基于单片机的智能婴儿摇篮温湿度调节与远程监控系统设计(032707)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

2026/10/14 2:44:28 阅读更多 →
Java异步编程进阶:CompletableFuture任务编排与线程池实战

Java异步编程进阶:CompletableFuture任务编排与线程池实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 2:43:28 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →