SpringBoot+Vue+MySQL:生鲜超市销售系统开发全流程详解
2026/9/17 2:45:51 网站建设 项目流程

简介:这是一份基于Java生态的生鲜超市销售系统毕业设计源码包,使用Vue+SpringBoot+MySQL搭建,面向计算机类专业学生、毕业设计开发者以及需要快速搭建权限管理后台的Java工程师。系统涵盖商品档案、进货、销售、供应商、活动管理、消息通知等核心业务模块,并集成用户、部门、角色、菜单、日志、数据字典、文件管理、图表展示等基础功能,基于角色访问控制,权限可精确到按钮级别,适合需要自定义权限模型的场景。包体共396个文件,其中Java源文件175个、Vue组件122个,另有JS、PNG、LESS、SQL等类型,涵盖后端业务逻辑、前端页面、样式与数据库脚本,压缩包仅1.38MB,结构紧凑便于快速部署学习。内容预览显示包含树形表格、添加编辑页面、Controller/Service/Entity等典型分层代码模板,能帮助理解SpringBoot+Vue项目从接口到页面的完整实现路径。已有548人学习下载,可作为毕业设计参考或企业内部销售系统二次开发的基础工程。

1. 生鲜超市销售系统选 Vue+SpringBoot+MySQL:先看这个组合要解决的业务问题

一个订单创建完库存没扣,或者库存扣了订单明细没有落库,生鲜超市销售系统在验收现场往往撑不过三个追问。这类系统核心业务不大,但闭环清楚:管理员维护商品与库存,前台按分类浏览、加购结算,后台查订单、盯临期商品。用 Vue 处理页面交互,SpringBoot 提供 HTTP 服务,MySQL 负责商品、库存、订单持久化,三段职责互相独立,又正好把实体映射、接口设计、事务控制这些 Java 方向的主线知识串在一起。这套技术栈能控制项目复杂度,也能在有限时间里交付一个演示功能完整、答辩有话讲的系统。下面按我通常做的顺序展开:先搭后端骨架,再设计表和事务,然后接 Vue 页面,最后讲联调验证。

2. 搭建 SpringBoot 后端骨架:Maven 依赖、数据源配置和商品接口

后端部分我习惯先分层再写代码。项目虽然不大,但不分层到答辩时会很难讲清“事务加在哪一层”这种问题。

2.1 后端工程要拆成哪几个包,为什么毕业设计也值得分层

常见做法是把 controller、service、mapper、entity 四层分开,另外放一个 common 包放统一返回结构。目录结构如下:

fresh-market/ ├── pom.xml └── src/main/java/com/example/freshmarket/ ├── FreshMarketApplication.java ├── controller/ │ ├── CategoryController.java │ ├── ProductController.java │ └── OrderController.java ├── service/ │ ├── ProductService.java │ └── OrderService.java ├── mapper/ │ ├── ProductMapper.java │ └── OrderMapper.java ├── entity/ │ ├── Product.java │ └── SalesOrder.java └── common/ └── Result.java

分层不是给代码数量凑数。controller 只负责接收参数和返回结果,service 里放事务和业务校验,mapper 只管 SQL 读写。这样当“下单后库存不减少”这类问题出现时,排查路径很清楚:先看 controller 是否把参数传全,再看 service 是否加了 @Transactional,最后看 mapper 的 update 语句影响行数。答辩中“事务加在哪一层”是高频问题,能明确答出“service 层”会比含糊解释强很多。

实体类放在 entity 包内,字段直接对应一张业务表。MyBatis 的 mapper 接口用 @Mapper 注解标记,或者在启动类上使用 @MapperScan,两种方式任选其一。我用 @MapperScan 方式,因为 mapper 接口数量增多时,不需要每个接口都重复注解。

2.2 pom.xml 和 application.yml 的关键配置项

框架版本直接影响兼容性。我一般选用 SpringBoot 2.7 系列,它支持 JDK8,也支持后续常见的 JDK17 迁移,是当前毕业设计环境里最稳妥的选择。如果电脑上装的是新版 JDK,或者想体验 SpringBoot 3,需要同步切换到 JDK17+,映射类和配置上会有少量差异,别只改版本号就指望项目跑起来。

pom.xml 中核心依赖如下:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis 与 SpringBoot 的整合包,不是 MyBatis 官方单独维护的包 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

依赖选择上,mysql-connector-j 是 MySQL 8.x 官方驱动坐标;如果项目里还在用老坐标 mysql-connector-java,两者功能等价,换名而已。mybatis-spring-boot-starter 2.3.1 对 SpringBoot 2.7 适配良好,控制台不会出现版本冲突日志。

数据源配置写在 application.yml:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/fresh_market?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 hikari: minimum-idle: 2 maximum-pool-size: 10 connection-timeout: 30000 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.freshmarket.entity configuration: map-underscore-to-camel-case: true logging: level: com.example.freshmarket.mapper: DEBUG

几个关键配置项的取值参考:

配置项推荐值说明
characterEncodingutf8中文写入不乱码,配合数据库 utf8mb4 使用
serverTimezoneAsia/ShanghaiMySQL8 驱动校验时区,不改会启动报错
allowPublicKeyRetrievaltrue解决 caching_sha2_password 首次连接问题
maximum-pool-size10演示场景足够,调大浪费数据库连接资源
logging.level.mapperDEBUG控制台打印 SQL,联调排错必需

HikariCP 是 SpringBoot 默认连接池,连接数不需要跟着并发数膨胀。mybatis 的 type-aliases-package 配置让 XML 里可以写 resultType="Product",不需要写全限定名;map-underscore-to-camel-case 把 category_id 自动映射为 categoryId,少写大量 resultMap。

2.3 从 Product 实体到商品列表接口的完整链路

先从实体类开始。生鲜商品相比普通电商商品,需要额外关注批次和保质期,字段设计如下:

@Data public class Product { private Integer id; private String name; private Integer categoryId; private BigDecimal price; private String unit; // 单位:份/斤/盒 private Integer stock; // 当前库存数量 private String batchNo; // 进货批次号 private LocalDate produceDate; private LocalDate expireDate; private Integer status; // 1 上架,0 下架 private String imageUrl; }

unit 字段是我建议保留的。生鲜超市里同一件商品可能按份卖,也可能按斤称重,单位不同展示文案完全不同;如果演示要扩展称重商品,可以把 stock 改成 DECIMAL(10,3),用 kg 做基本单位,这里先按整数量来做。price 用 BigDecimal 而不是 double,后续计算订单总额时不会出现浮点误差。

Mapper 接口只写一个方法:

@Mapper public interface ProductMapper { List<Product> selectOnSale(@Param("categoryId") Integer categoryId); }

对应 XML:

<select id="selectOnSale" resultType="Product"> SELECT id, name, category_id, price, unit, stock, batch_no, produce_date, expire_date, status, image_url FROM product WHERE status = 1 AND (#{categoryId} IS NULL OR category_id = #{categoryId}) ORDER BY id DESC </select>

这里用了“#{categoryId} IS NULL OR category_id = #{categoryId}”的写法,前端不传分类时返回全部上架商品,传了分类就按分类过滤,避免在 Java 代码里写两套查询逻辑。需要注意,product 表此时还没有建立,MyBatis 启动后第一次访问这张表会直接报错,所以跑通接口前必须先把下一章的建表脚本执行完,顺序不要反。

Service 层直接调用 mapper,Controller 负责接收查询参数:

@RestController @RequestMapping("/api/product") public class ProductController { private final ProductService productService; public ProductController(ProductService productService) { this.productService = productService; } @GetMapping("/list") public Result<List<Product>> list(@RequestParam(required = false) Integer categoryId) { List<Product> products = productService.listOnSale(categoryId); return Result.success(products); } }

统一返回结构 Result 里包含 code、message、data 三个字段,code 为 200 表示成功。前端 Axios 拦截器只需判断 code 即可进入成功分支,错误提示也能统一处理,不用每个接口自己写 try catch。后端的商品接口到这里就能跑通,这相当于整条开发链路的基准线,此后订单、库存功能都参照这个模式扩展。

3. MySQL 表设计:生鲜商品字段、库存扣减和订单事务

后端骨架通了,接下来要把“生鲜”两个字落在表结构上。很多人把生鲜超市系统做成普通商城,丢失了业务特征,答辩时容易被问倒。

3.1 生鲜商品表要单独承载哪些字段

普通电商商品关注的是标题、图片、SKU、销量,而生鲜商品有另一个时间维度:保质期。西红柿放三天可能就变成损耗,肉类批次不同进货价也不同。所以商品表里 batch_no、produce_date、expire_date 这三个字段不是装饰,它们支撑两个实际功能:临期商品提醒和批次管理。

另一个特征是损耗与拆零。蔬菜水果在售过程中会出现自然损耗,这类系统如果只记录“入多少卖多少”,月底盘点对不上账。毕业设计阶段不需要把损耗模块做成完整进销存,但至少可以在库存变动表里预留一个变动类型字段,区分入库、销售扣减、盘亏调整,这样复盘数据时有据可查。下面 DDL 中 stock_change 表就承担这个角色。

3.2 商品、订单、库存变动三张核心表的 DDL

以 MySQL 8.0 为基准,建议字符集使用 utf8mb4,因为它能完整支持中文和特殊符号,避免 emoji 或生僻字写入报错。

CREATE DATABASE IF NOT EXISTS fresh_market DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE fresh_market; CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, sort INT NOT NULL DEFAULT 0 ) ENGINE=InnoDB; CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category_id INT NOT NULL, price DECIMAL(10,2) NOT NULL, unit VARCHAR(10) NOT NULL DEFAULT '份', stock INT NOT NULL DEFAULT 0, batch_no VARCHAR(40), produce_date DATE, expire_date DATE, status TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', image_url VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_category (category_id), KEY idx_expire (expire_date) ) ENGINE=InnoDB; CREATE TABLE sales_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 0 COMMENT '0已创建 1已支付 2已取消', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, product_name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, KEY idx_order (order_id) ) ENGINE=InnoDB; CREATE TABLE stock_change ( id INT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL, change_type TINYINT NOT NULL COMMENT '1入库 2销售扣减 3盘亏', quantity INT NOT NULL, remark VARCHAR(200), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_product (product_id) ) ENGINE=InnoDB;

DDL 里值得说明的点有三个。price 用 DECIMAL 而不是 FLOAT,因为浮点数在金额计算上存在精度误差,订单里对不上账会很麻烦。sales_order 表没有 user_id,是刻意简化的行为:如果系统只面向门店收银场景,订单属于门店而不是某个注册用户,就不需要会员体系,可以让系统更聚焦在商品和库存上;如果指导老师要求登录注册,再补充 user 表和 user_id 字段。stock_change 是流水表,只做累加,不修改之前的记录,盘点时以流水 trace 库存变化,这比直接改商品表的 stock 字段更可信。

商品表里 idx_category 和 idx_expire 两个二级索引分别服务分类筛选和临期查询。数量级别到百万行时,这两个索引的作用会体现出来;在演示数据量下,它们能帮助说明“索引是建立在查询模式之上的”,答辩时可以顺势讲。created_at 和 updated_at 用数据库默认值维护,Java 代码里不需要手动 set 时间,避免多台服务器时间不一致的问题。

3.3 扣库存用条件 UPDATE,别用“先查后改”

下单扣库存是销售系统最容易出错的一段。常见误用是先查询剩余库存,在 Java 代码里判断数量够不够,再执行 UPDATE。单用户操作没问题,一旦前端两个请求同时到达,两个请求都读到 stock=5,都判断“够扣”,先后执行扣减,最后一次写入就把结果覆盖了,这就是超卖。

两种实现方式对比:

实现方式并发安全性推荐度原因
select 后判断再 update不安全不推荐两端读可能读到相同库存,覆盖更新
UPDATE 带 stock >= 数量安全推荐行锁加条件判断一次原子完成
SELECT FOR UPDATE 悲观锁安全此场景不推荐增加锁等待处理,超卖问题不需要上升到悲观锁

正确做法是把校验放进 SQL 条件里:

<update id="deductStock"> UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity} </update>

这段 SQL 中 stock >= #{quantity} 是原子判断,数据库行锁保证同一时刻只有一个事务能更新这一行。当库存不足时,update 影响行数为 0,程序通过受影响行数判断失败并抛出异常。配合 Service 层事务,商品表更新失败后,前面已插入的订单头和订单明细会一并回滚。

完整下单逻辑:

@Transactional(rollbackFor = Exception.class) public String createOrder(OrderAddRequest req) { if (req.getItems() == null || req.getItems().isEmpty()) { throw new RuntimeException("订单不能为空"); } SalesOrder order = new SalesOrder(); order.setOrderNo("SO" + System.currentTimeMillis() + String.format("%04d", ThreadLocalRandom.current().nextInt(10000))); orderMapper.insert(order); BigDecimal total = BigDecimal.ZERO; for (OrderItemRequest item : req.getItems()) { Product product = productMapper.selectById(item.getProductId()); if (product == null || product.getStatus() != 1) { throw new RuntimeException("商品不存在或已下架:" + item.getProductId()); } int rows = productMapper.deductStock(item.getProductId(), item.getQuantity()); if (rows == 0) { throw new RuntimeException("库存不足:" + product.getName()); } total = total.add(product.getPrice() .multiply(BigDecimal.valueOf(item.getQuantity()))); orderItemMapper.insert(order.getId(), item.getProductId(), product.getName(), product.getPrice(), item.getQuantity()); } order.setTotalAmount(total); orderMapper.updateAmount(order.getId(), total); return order.getOrderNo(); }

方法上的 @Transactional(rollbackFor = Exception.class) 必须写 rollbackFor,默认情况下 RuntimeException 才会回滚,这里的业务异常都设计成 RuntimeException,所以加这个参数保险且含义清晰。流程里先插入订单头拿到自增 id,然后逐条扣库存、插明细,最后回填总金额。中间任何一条扣减失败,事务回滚后订单头和已插明细都不会留在数据库里。

事务的隔离级别这里没有单独声明,使用 MySQL InnoDB 默认的 REPEATABLE READ。行锁加条件 UPDATE 已经解决了超卖问题,不需要上升到悲观锁 SELECT FOR UPDATE,更不需要引入 Redis 分布式锁。毕业设计去演示分布式锁反而容易被追问多实例部署的假设条件,不如把单库事务讲透。

3.4 临期商品预警查询

生鲜系统用一个查询就能体现出业务差异:查到期前 3 天的上架商品。

SELECT id, name, expire_date, DATEDIFF(expire_date, CURDATE()) AS days_left FROM product WHERE status = 1 AND expire_date IS NOT NULL AND DATEDIFF(expire_date, CURDATE()) <= 3 ORDER BY expire_date;

DATEDIFF 返回两个日期相差天数,days_left 为 0 表示当天到期,负数表示已过保。expire_date 有非空判断,防止历史数据里大量 NULL 把结果带偏。页面上给 days_left 加红色标签,就是一个能写进功能清单的亮点,PPT 上截图展示比空讲“库存管理”有说服力。

这段 SQL 也可以做成后端接口/api/product/expiring,Service 里直接返回标记好 daysLeft 的列表,前端在管理后台单独做一个临期商品 Tab。注意 DATEDIFF(expire_date, CURDATE()) 在 expire_date 上无法走索引优化,数据量大时可以改成expire_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 3 DAY),演示阶段用 DATEDIFF 更直观,答辩时能接住这个优化问题,反而加分。

4. Vue 前端:页面路由、Axios 封装、购物车和下单

后端接口成型后,前端主要做三件事:把页面和路由组织好、把请求统一封装、把购物车和下单流程跑通。以下使用 Vue3 + Vite + Pinia 的常见组合;如果你拿到的项目模板是 Vue2 + Vuex,组件的写法会变,但状态管理的设计思路不变。

4.1 前端目录结构和路由参数设计

常规目录结构如下:

fresh-market-web/ ├── vite.config.js ├── package.json ├── index.html └── src/ ├── main.js ├── App.vue ├── router/index.js ├── api/ │ ├── request.js │ ├── product.js │ └── order.js ├── store/cart.js └── views/ ├── Home.vue ├── CategoryProducts.vue └── OrderConfirm.vue

路由主要定义三条路径:

import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', name: 'home', component: () => import('../views/Home.vue') }, { path: '/category/:id', name: 'category', component: () => import('../views/CategoryProducts.vue'), props: true }, { path: '/order/confirm', name: 'orderConfirm', component: () => import('../views/OrderConfirm.vue') } ] export default createRouter({ history: createWebHistory(), routes })

vite 项目里用 createWebHistory 是 HTML5 History 模式,URL 干净,没有 # 号;如果部署到静态服务器,需要配置 fallback,否则刷新二级页面会 404,开发环境 Vite 自带处理,不用纠结。component 使用动态 import 做路由级懒加载,首屏只加载首页代码,商品页和订单页按需请求,演示机器配置低时页面打开会快一些。

路由传参有几种常见方式,场景不同选择不同:

传参方式适用场景获取方式
path query订单成功页回显参数route.query.orderNo
路由 params分类页传 idprops.id 或 route.params.id
Pinia 状态大批量对象传递store 直接读取

路径里加入props: true后,CategoryProducts 组件直接用 defineProps 接收 id,而不是在组件内部读 route.params.id。props 方式类型更明确,页面跳转传参也更直观。CategoryProducts 里监听 id 变化,重新请求商品列表:

watch( () => props.id, (newId) => { loadProducts(newId) }, { immediate: true } )

immediate: true 让组件首次创建时就执行一次加载,不需要额外在 onMounted 里重复写逻辑,这是 Vue3 组合式 API 里比较省事的写法。

4.2 Axios 请求封装与后端返回结构约定

Axios 封装统一处理 baseURL、超时、响应拦截和错误提示。后端接口的公共前缀 /api 写在这里,页面里不会出现具体 URL。

import axios from 'axios' import { ElMessage } from 'element-plus' import router from '../router' const request = axios.create({ baseURL: 'http://localhost:8080/api', timeout: 10000 }) request.interceptors.response.use( (response) => { const res = response.data if (res.code !== 200) { ElMessage.warning(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, (error) => { if (error.response && error.response.status === 401) { router.push('/login') } ElMessage.error('网络异常,请稍后重试') return Promise.reject(error) } ) export default request

响应拦截器把外层 { code, message, data } 拆开,业务组件里拿到的直接就是 data,不用每页都 res.data.data。对于 401 未登录场景,拦截器统一跳登录页,这样做的前提是后端在需要登录的接口上返回标准 401,前后端约定一致。ElMessage 是 Element Plus 的全局提示组件,如果项目换用 Ant Design Vue 或 Naive UI,替换对应通知组件即可。

商品接口模块:

import request from './request' export function getProductList(categoryId) { return request.get('/product/list', { params: { categoryId } }) }

这里注意 params 传 null 时,Axios 会忽略该参数,正好对应后端接口中 categoryId 可空的设计,两种实现是配套的。

4.3 购物车状态管理和提交订单

购物车数据不需要每个页面都向后端请求,放在前端状态里更流畅。Pinia 写法如下:

import { defineStore } from 'pinia' export const useCartStore = defineStore('cart', { state: () => ({ items: [] }), getters: { totalCount(state) { return state.items.reduce((sum, item) => sum + item.quantity, 0) }, totalAmount(state) { return state.items.reduce( (sum, item) => sum + item.price * item.quantity, 0 ) } }, actions: { addProduct(product) { const found = this.items.find((item) => item.id === product.id) if (found) { found.quantity++ } else { this.items.push({ ...product, quantity: 1 }) } }, clear() { this.items = [] } } })

状态里只保存购物车数组,getters 承担计算逻辑:totalCount 算总件数,totalAmount 算总金额。更新商品数量时直接改 found.quantity,数组是响应式的,页面自动刷新。这里没有把商品价格再封装一层 VO,直接复用商品对象,因为购物车页需要展示名称、单位、单价,这些字段在后端商品列表接口里已经全部返回,前端不需要额外请求。

提交订单时组装后端需要的结构:

import { createOrder } from '../api/order' import { useCartStore } from '../store/cart' const cart = useCartStore() const submitting = ref(false) async function submitOrder() { if (cart.items.length === 0) { return } submitting.value = true try { const orderNo = await createOrder({ items: cart.items.map((item) => ({ productId: item.id, quantity: item.quantity })) }) cart.clear() router.push({ path: '/order/success', query: { orderNo } }) } finally { submitting.value = false } }

提交按钮绑定了 submitting,防止用户连点导致重复下单。后端事务保证了同一时刻同一个商品的扣减是串行的,即使重复请求到达,第二个请求也会因库存不足失败。订单号通过 query 参数传给成功页,成功页可以直接展示“订单 SOxxx 创建成功”,不需要再调用查询接口,减少一次请求往返。

5. 从接口自测到答辩:看请求日志定位前后端问题

5.1 用浏览器开发工具和后端日志定位联调问题

前端页面点加购没反应,最常见的原因是请求根本没发出去,或者返回被拦截器挡掉。打开浏览器开发者工具 Network 面板,先看请求 URL 是否正确指向后端地址。如果状态码是 404,检查后端 @RequestMapping 的 /api 前缀有没有配全;如果状态码是 500,直接看后端控制台堆栈,而不是在前端代码里找原因。

后端侧要养成看 MyBatis SQL 日志的习惯。前面 yml 里配置了 mapper 包 DEBUG,控制台会打印类似内容:

==> Preparing: UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ? ==> Parameters: 3(Integer), 1(Integer) <== Updates: 1

Updates: 1 表示扣减成功,0 表示库存不足。它还能暴露另一个问题:如果打印出来的参数位置和预想不一致,多半是 @Param 没写全,检查 mapper 方法参数即可。前后端联调阶段,大多数问题都能靠对照 Network 面板和控制台日志定位,不需要额外引入链路追踪工具。

5.2 答辩时按业务链路讲技术点

答辩环节不要按“我用了 SpringBoot、Vue、MySQL”逐个罗列技术栈,而是挑一个业务场景顺着链路讲。以“用户下单扣库存”为例:前端购物车提交订单,Axios 把订单明细 POST 到 /api/order/create;SpringBoot controller 接收 JSON,交给 service 方法;service 上的 @Transactional 开启事务,先插订单头,再逐条执行带库存校验的条件 UPDATE,影响行数为 0 就抛异常触发回滚;数据库层面的行锁保证了并发请求不会超卖。讲完这条链路,事务边界、并发控制、前后端接口约定三个点都覆盖了。

数据库部分可以准备一张自己画的 ER 图,标注商品、分类、订单、订单明细的关系,以及 stock_change 流水表的作用。被问到“为什么订单总价不直接存在明细表”时,能回答“总价是订单表的冗余字段,方便订单列表查询;明细用于追溯单品成交价”,就说明你想过数据冗余的代价。

演示临期商品查询时,把某个商品的 expire_date 改成当天日期,刷新页面看到预警标记出现,这个交互比念 PPT 更直观,也证明了表设计时考虑了保质期维度。把加了红色标记的临期商品页面放在演示最后,老师问“你的系统哪里有业务特色”,直接切到管理后台的临期列表,顺势讲 DATEDIFF 的查询逻辑和索引优化思路即可。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询