十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue农产品销售系统商城:从数据库设计到答辩演示全流程

SpringBoot+Vue农产品销售系统商城:从数据库设计到答辩演示全流程 简介面向高校毕业设计、期末大作业及课程设计场景这是一份基于SpringBoot与Vue前后端分离架构的农产品销售系统商城完整项目包含前端页面、后端接口、数据库SQL脚本及相关部署配置可直接演示或二次开发。代码注释清晰适合Java入门者学习功能覆盖商品管理、购物车、订单处理、支付宝支付、用户鉴权等典型电商模块后端控制层、安全配置与数据库脚本均已整理到位运行环境配置简便。资源包共145个文件约9.68MB以Java源码、Vue组件、PNG界面图片、JavaScript逻辑文件及XML/yml配置为主其中Java与Vue文件占比最高另包含SQL数据库脚本与项目说明文档目录结构明确便于按功能模块查阅。该项目经过严格调试作者在目录中已整理运行说明可快速部署使用并曾用于高分毕业设计具备较高的完成度和实战参考价值。目前已有308人学习下载适合需要获取完整商城项目作为课设、毕设参考的人群。1. 这个标题真正比的是什么演示链路完整而不是真实交易闭环每年到了毕设季节「基于SpringBoot-Vue的农产品销售系统商城」这类命题总会成批出现。它表面是套电商系统实际比的不是支付对账、物流拆单、商户入驻这些生产级能力而是从 MySQL 建表、后端接口、前端页面到答辩演示这条链路能不能一气呵成跑通。「源码数据库」点明了交付形态不是论文加几张截图而是拿到手、装进数据库、启动后端、npm run dev就能点出页面的完整工程。农产品只是业务外壳核心闭环是用户注册登录、浏览商品、加购、下单、后台看订单。适合的人群也清楚正在做课程设计或毕业设计的学生以及需要快速交付一套前后端分离演示系统的在职开发者。2. SpringBoot-Vue 双端框架的选型逻辑以及第一版工程怎么起2.1 为什么这套组合成了默认答案自动装配、组件化与版本红线SpringBoot 的核心价值是「约定优于配置」。对于一个 5 张表、十几个接口的演示系统SpringBoot 的 Starter 机制把 Tomcat、Jackson、数据源这些组件的版本冲突全部挡在外面你只需要关心业务代码。Vue 这边则是组件化开发一个商品卡片是一个组件、一个导航栏是一个组件页面之间通过 Vue Router 切换数据通过 axios 向后端要整个前端工程天然适合按「页面 - 组件 - 接口」三层组织。前后端分离这个形态既是当前教学的默认姿势也是面试时能讲清楚的技术叙事。版本选择有个容易被忽略的坑springboot 版本太高 会连带要求 JDK 版本升级。SpringBoot 3.x 强制 JDK 17而很多实验室和导师本机的 JDK 还是 8直接开 3.x 会导致本地跑不起来。我的建议是走成熟线SpringBoot 2.7.18 JDK 8 Vue 2.6 Element UI。这不是技术落后而是「毕业设计能过」和「生产环境最新」是两套评价标准。Vue 3 Vite Element Plus 当然可以但要确认答辩环境能装 Node 18 以上。前端环境配置也是个重复性痛点。常见做法是npm install -g vue/cli vue create farm-mall cd farm-mall npm install npm run servevue create交互式生成工程时会问预设选「Manually select features」勾上 Router 和 VuexVue 2 场景。npm install阶段最容易翻车报 ERESOLVE 错误一般是依赖树冲突在 Vue 2 工程里把 npm 降到 6 或者用npm install --legacy-peer-deps这属于 vue 安装依赖 的经典处理方式。跑起来之后先用浏览器打开http://localhost:8081确认默认首页能渲染再继续写业务。2.2 后端最小骨架controller-service-mapper 三层不要省拿到源码包之后第一步不是读代码而是先看目录结构。一个高分的 SpringBoot 后端工程包结构必须让人一眼看懂。我一般会按这个方式组织com.example.farm ├── controller # 接收请求、返回统一结果 ├── service # 业务逻辑接口 │ └── impl # 业务实现 ├── mapper # MyBatis-Plus 的 Mapper 接口 ├── entity # 数据库表对应的实体类 ├── config # 拦截器、跨域、MyBatis-Plus 配置 ├── common # Result 统一返回体、异常处理 └── utils # JWT、MD5 等工具类三层分法的理由不是教条。controller 只做参数接收和结果包装service 处理业务规则比如下单时扣库存、校验状态mapper 只碰 SQL。这样做的直接好处是答辩被问「订单流程怎么走」时你能沿着 controller - service - mapper 说清楚调用路径。很多低分源码的问题恰恰是业务逻辑全写在 controller 里一个方法 200 行看起来能跑一问就露馅。后端全局配置里application.yml是必改文件。数据库连接、端口、MyBatis 配置都在这里server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/farm_mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 hikari: maximum-pool-size: 10 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0map-underscore-to-camel-case: true表示数据库的create_time自动映射到实体字段createTime。logic-delete-field开启逻辑删除这样「删除商品」实际是置deleted1列表查询自动过滤答辩时演示「删除后数据还在库里」是个加分操作。2.3 跨域问题不在后端解决vue.config.js 代理是最省事的方案前后端分离最常报的错是跨域。浏览器拦截来自http://localhost:8081到http://localhost:8080的 Ajax 请求后端的对策是加CrossOrigin或者配 CORS 过滤器。我的建议是开发阶段直接用 Vue CLI 的代理转发前端所有/api开头的请求都转发到 8080 端口浏览器视角里是同一个源没有跨域后端也不用额外配const { defineConfig } require(vue/cli-service) module.exports defineConfig({ transpileDependencies: true, devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })配置完成后前端的请求路径写/api/product/list代理会把请求原样转发给http://localhost:8080/api/product/list。后端 controller 的RequestMapping(/api/product)保持不变。这里的port: 8081要和前端页面访问地址一致changeOrigin: true会把请求头里的 Host 改成目标地址避免后端在某些框架下做域名校验时出问题。开发期用代理部署期再用 Nginx 统一转发这条路径足够覆盖答辩场景。3. 数据库设计是「源码数据库」交付质量的试金石农产品商城的 5 张核心表3.1 从业务实体到 ER 关系先定闭环再补字段拿到这类题目我不建议急着写代码。先用纸把业务闭环画出来用户进入系统看到商品列表点进详情加入购物车提交订单管理员在后台看到订单并修改状态。这条链路上的实体就四类用户、商品、购物车、订单。购物车和订单还要拆出明细否则一张订单里多个商品没法存。所以最少 5 张表。用户和购物车是 1:N一个用户可以有多条购物车记录。购物车和商品是 N:1购物车记录通过product_id指向商品。订单和用户是 N:1订单和订单明细是 1:N明细通过order_id关联订单同时冗余一份商品快照商品名称、单价、图片防止商品下架后历史订单数据变空。农产品比较特殊的地方是商品表需要体现「产地」和「单位」。蔬菜水果的计价单位是斤、箱、份和普通电商按件卖不一样。这两个字段加上后商品表的设计就能在答辩时说「针对农产品领域做了字段定制」这是低分和高分在细节层面的差别。3.2 建表 SQL 全文与字段规范金额、状态、时间的取舍建表脚本是交付物里最该被认真对待的部分。评分老师拿到源码包第一个打开的文件通常就是.sql。字段类型不规范在这个环节会被直接扣分。我给出核心表的建表语句可以直接抄CREATE DATABASE IF NOT EXISTS farm_mall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE farm_mall; DROP TABLE IF EXISTS user; CREATE TABLE user ( id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT MD5加密后的密码, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, role TINYINT(4) NOT NULL DEFAULT 0 COMMENT 角色0-普通用户 1-管理员, status TINYINT(4) NOT NULL DEFAULT 1 COMMENT 状态1-正常 0-禁用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, deleted TINYINT(4) NOT NULL DEFAULT 0 COMMENT 逻辑删除0-未删除 1-已删除, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB AUTO_INCREMENT1 COMMENT用户表;DROP TABLE IF EXISTS product; CREATE TABLE product ( id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 商品ID, name VARCHAR(100) NOT NULL COMMENT 商品名称, category VARCHAR(50) DEFAULT NULL COMMENT 分类蔬菜/水果/粮油, origin VARCHAR(100) DEFAULT NULL COMMENT 产地, unit VARCHAR(20) DEFAULT 斤 COMMENT 计价单位斤/箱/份, price DECIMAL(10,2) NOT NULL COMMENT 售价, stock INT(11) NOT NULL DEFAULT 0 COMMENT 库存, image VARCHAR(255) DEFAULT NULL COMMENT 商品图片URL, description TEXT COMMENT 商品描述, status TINYINT(4) NOT NULL DEFAULT 1 COMMENT 上下架状态1-上架 0-下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted TINYINT(4) NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY idx_category (category) ) ENGINEInnoDB COMMENT商品表;字段规范上有三条铁律。第一价格用DECIMAL(10,2)不用FLOAT和DOUBLE浮点数存金额会产生 0.1 0.2 不等于 0.3 的精度问题答辩时一句话讲清楚这个选择就是加分项。第二状态字段用TINYINT存数字不用字符串on/off数字状态可扩展且查询走索引更高效。第三每张表都带create_time和deleted创建时间统一给DEFAULT CURRENT_TIMESTAMP逻辑删除配合 MyBatis-Plus 的全局配置后面写查询不用手动加WHERE deleted 0。剩下的 cart、orders、order_item 建表逻辑同理。订单表要注意总金额用DECIMAL(10,2)订单号用user_id 时间戳生成不用自增 ID 暴露订单量。order_item 要冗余product_name和product_price这样删除商品后历史订单明细仍然完整。3.3 数据库脚本交付的两种方式手动导入和自动初始化「源码数据库」里的数据库通常交付一个.sql文件加一份数据库设计文档。.sql 文件用 Navicat 导出时注意两个选项勾选「包含库」会在脚本里带上CREATE DATABASE这样导入方只需要执行一次不勾选则要求对方先手动建库多一步操作就多一个失败点。我建议导出时勾选「包含库」和「包含数据」让验收方直接运行脚本。字符集统一utf8mb4否则插入 emoji 或生僻字会报 1366 错误。另一种做法是让 SpringBoot 自动建表。在application.yml里配置spring: sql: init: mode: always schema-locations: classpath:sql/schema.sql >public class JwtUtil { private static final String SECRET farm-mall-secret-key; private static final long EXPIRE 1000 * 60 * 60 * 24; // 24小时 public static String createToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }这里用的是 jjwt 0.9.x 的 API注意signWith的密钥长度要求。如果用 jjwt 0.11SignatureAlgorithm.HS256已经过时要改成Keys.hmacShaKeyFor(secret.getBytes())这是常见的 API 版本坑。token 有效期设置 24 小时足以覆盖一次答辩演示超时后前端会收到 401需要重新登录。拦截器负责校验除了登录、注册、商品列表以外的所有接口Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } try { Claims claims JwtUtil.parseToken(token.substring(7)); request.setAttribute(userId, claims.getSubject()); return true; } catch (Exception e) { response.setStatus(401); return false; } } }拦截器写好后在 WebMvcConfigurer 里注册并配置放行路径registry.addInterceptor(jwtInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/user/login, /api/user/register, /api/product/**);/api/product/**放行是因为商品列表和详情页需要未登录也能看这是电商的常规设计购物车、订单相关接口都在拦截范围内。前端登录成功后把 token 存进localStorage后续每次请求由 axios 拦截器统一加上Authorization头这部分在第五章展开。4.2 商品分页与条件查询MyBatis-Plus 分页插件的参数边界商品列表是商城被访问最多的接口必然要做分页。MyBatis-Plus 的分页必须先配置拦截器否则selectPage只会查询全部数据再内存截断Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }service 层的分页查询代码public Result pageProduct(Integer pageNum, Integer pageSize, String keyword, String category) { pageNum (pageNum null || pageNum 1) ? 1 : pageNum; pageSize (pageSize null || pageSize 1 || pageSize 50) ? 10 : pageSize; PageProduct page new Page(pageNum, pageSize); LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), Product::getName, keyword) .eq(StringUtils.hasText(category), Product::getCategory, category) .eq(Product::getStatus, 1) .orderByDesc(Product::getCreateTime); return Result.ok(productMapper.selectPage(page, wrapper)); }几个参数细节值得在答辩时主动讲出来。pageSize上限设为 50防止有人传9999把全表拉出来这是一种基础的接口保护。like查询用StringUtils.hasText判断keyword 为空时不会拼进 SQL。商品列表只查status 1的上架商品下架的status 0只在后台管理接口中可见。这里StringUtils用的是 Spring 提供的不是 hutool减少一个依赖就能少一个装包失败的可能。还有一点如果你在application.yml里配了spring.jpa.hibernate.ddl-auto: update用了 JPA 的情况在上架前必须去掉否则启动时表结构会被自动改动。MyBatis-Plus 场景下不存在这个选项但有同学会在schema.sql里写DROP TABLE IF EXISTS再建表每次启动清空演示数据这也是个隐蔽坑。4.3 购物车加购与下单的事务边界Transactional 放在哪一层购物车的核心逻辑是用户已登录从 request 里拿 userId传商品 ID 和数量校验商品存在、库存足够然后执行 insert。同一个用户对同一商品加购多次应该做数量累加而不是插入两条记录。这个「先查再更新」的操作要用唯一索引兜底表结构里cart表对(user_id, product_id)建唯一键代码里先selectOne判断存在则setNum(num oldNum)update否则插入新记录。下单流程涉及多张表的写入必须开事务。创建一个订单逻辑上要完成四件事生成订单主记录、批量插入订单明细、扣减商品库存、清空对应购物车记录。这四步任何一个失败都不能出现「订单建了但库存没扣」的脏数据。Transactional(rollbackFor Exception.class) public Long createOrder(Long userId, ListCartItemVO items) { // 1. 计算总金额校验库存 BigDecimal total BigDecimal.ZERO; for (CartItemVO item : items) { Product product productMapper.selectById(item.getProductId()); if (product null || product.getStock() item.getNum()) { throw new BusinessException(商品库存不足: item.getProductName()); } total total.add(product.getPrice().multiply(BigDecimal.valueOf(item.getNum()))); } // 2. 创建订单主记录 Orders order new Orders(); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(0); // 0-待支付 1-已支付 2-已发货 3-已完成 orderMapper.insert(order); // 3. 插入明细并扣库存、清购物车 for (CartItemVO item : items) { OrderItem detail new OrderItem(); detail.setOrderId(order.getId()); detail.setProductId(item.getProductId()); detail.setProductName(item.getProductName()); detail.setProductPrice(item.getProductPrice()); detail.setNum(item.getNum()); orderItemMapper.insert(detail); productMapper.decreaseStock(item.getProductId(), item.getNum()); cartMapper.deleteByUserIdAndProductId(userId, item.getProductId()); } return order.getId(); }Transactional(rollbackFor Exception.class)必须写在 public 方法上且这个方法的调用方不能是同类内部调用否则事务注解会失效这是 Spring 代理机制决定的。rollbackFor指定了异常类型运行时异常默认回滚但检查异常默认提交不回滚显式声明rollbackFor Exception.class是最稳的写法。decreaseStock用 UPDATE 语句在数据库层面做加减不要先查出 stock 再 set 回去并发下会超卖一句UPDATE product SET stock stock - #{num} WHERE id #{id} AND stock #{num}就能在 SQL 层面保证安全。5. Vue 前端对接接口路由、Axios 封装、购物车状态管理5.1 前端目录与 Vue Router 配置带参跳转商品详情前端工程的结构直接影响到写代码的速度和维护体验。views 目录下按业务分页面components 目录放公共组件api 目录放接口请求定义store 目录放 Vuex/Pinia 状态。一个常见的推荐结构src ├── api │ ├── product.js │ ├── cart.js │ └── order.js ├── assets ├── components │ ├── NavBar.vue │ └── ProductCard.vue ├── router │ └── index.js ├── store │ └── index.js ├── views │ ├── Home.vue │ ├── ProductDetail.vue │ ├── Cart.vue │ ├── Login.vue │ └── admin │ └── OrderManage.vue └── utils └── request.jsVue Router 的配置里有一个高频考点路由参数怎么传。商品列表页点击卡片跳到详情页URL 是/product/12路由定义要写成/product/:id组件里通过this.$route.params.id拿到参数。有人在列表页用 query 传参?id12也能跑但刷新页面参数会丢失而且不符合 RESTful 风格。还有一个常见错误是把整个商品对象放进 queryURL 会变得极长且敏感信息暴露正确做法只传 id详情页重新拉接口。路由配置示例const routes [ { path: /, component: Home }, { path: /product/:id, name: ProductDetail, component: ProductDetail }, { path: /cart, component: Cart }, { path: /login, component: Login } ] const router new VueRouter({ routes, mode: history }) router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /cart !token) { next(/login) } else { next() } })mode: history让 URL 不带#页面上更干净。但 history 模式部署到服务器时需要后端配置 fallback 到 index.html否则直接访问/product/12会 404。开发模式下 devServer 默认支持不需要额外配置答辩若只在本地演示这个点不影响。路由守卫做的是基础登录校验购物车这种必须登录才能访问的页面在这里统一拦截比在每个页面里写判断更干净。5.2 Axios 封装Token 注入与 401 跳转避免每个页面重复写请求头不封装 axios 的代码长这样每个页面里axios.get(/api/product/list, { headers: { Authorization: Bearer token } })token 获取逻辑重复六七遍一旦要改成从 cookie 读所有页面都得改。封装的核心是拦截器。// utils/request.js import axios from axios import router from ../router const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) service.interceptors.response.use( response { return response.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } ) export default service封装之后接口定义文件里不需要关心 token// api/product.js import request from ../utils/request export function getProductList(params) { return request({ url: /product/list, method: get, params }) } export function addToCart(data) { return request({ url: /cart/add, method: post, data }) }response.interceptors里的response.data直接把后端统一返回体{ code: 200, data: {...}, msg: success }拆掉业务代码拿到的就是 data 部分不用每个页面都写.data.data。401 跳转要注意一个边界登录页本身如果返回 401拦截器里要判断当前路由不是/login否则会陷入「登录页跳登录页」的循环。上面代码在真实场景中需要加一个if (router.currentRoute.path ! /login)判断这是封装拦截器时最常见的隐性 bug。5.3 购物车数量、用户信息的全局状态为什么用 Vuex 而不是 localStorage商品列表页、导航栏、购物车页都要显示购物车商品数量。如果不用状态管理导航栏的角标数量只能在每次跳转时重新拉接口页面刷一下闪一下体验很差。用 VuexVue 2 配套 Vuex 3把购物车数量作为全局 state加购后 commit 一个 mutation 更新数字导航栏组件通过mapState自动响应。// store/index.js const store new Vuex.Store({ state: { cartCount: 0, userInfo: JSON.parse(localStorage.getItem(userInfo) || null) }, mutations: { setCartCount(state, count) { state.cartCount count }, setUserInfo(state, info) { state.userInfo info localStorage.setItem(userInfo, JSON.stringify(info)) }, logout(state) { state.userInfo null state.cartCount 0 localStorage.removeItem(token) localStorage.removeItem(userInfo) } } })为什么userInfo要和 localStorage 双写因为 Vuex 的 state 存内存刷新页面就没了localStorage 持久化到磁盘刷新后还在。双写的含义是初始值从 localStorage 读运行时以 state 为准mutation 里同时更新两边这样刷新和跳转都不会丢登录态。Angular 和 React 生态里也有类似方案但 Vue 的响应式让这个模式写起来最顺。cartCount 每次进入页面时重新拉接口刷新一次不要用 localStorage 存购物车数量因为后台如果改了数据前端的状态就是脏的。6. 答辩与交付的最后一步数据库脚本回灌、演示数据与验收清单6.1 用命令行完成「源码数据库」的 90 秒验收答辩现场最容易出状况的环节就是「现场跑不起来」。为了避免这种情况验收路径要简化到一条命令能检查的程度。在全新环境中数据库导入的标准动作用于保证这条路通畅mysql -u root -p farm_mall farm_mall.sql如果.sql里已经带了CREATE DATABASE先删除同名库再导入更干净mysql -u root -p -e DROP DATABASE IF EXISTS farm_mall; mysql -u root -p farm_mall.sql导入后立刻执行一条查询确认数据完整SELECT COUNT(*) FROM product; SELECT COUNT(*) FROM user WHERE role 1; SELECT COUNT(*) FROM orders;三个数分别对应用户、商品、订单三类核心数据。商品不能是 0 条管理员账号必须存在订单至少有一条能演示「订单状态流转」的记录。这三个数字在答辩前记录好现场展示时能主动报出来比老师问一句答一句印象好得多。6.2 演示数据的准备口径一个品类 8 条商品一条订单走完全状态演示数据不是随便 insert 几百条重点在「每种状态都有样例」。商品维度蔬菜、水果、粮油三个分类各准备 6 到 8 条数据图片可以用淘宝、京东的商品图 URL商品描述留 2 到 3 句带产地和口感的文案订单维度一条待支付、一条已支付、一条已完成这样后台管理页面里三个状态标签都不是空的。库存要给足至少 50 起演示加购 3 件不会触达下限。初始化的 SQL 脚本不要手动敲 insert用 Navicat 的「数据生成」功能批量造数据或者从网上找一份农产品数据模板替换字段。注意保持create_time有近有远列表按时间倒序时排在前面的是最新上架的商品视觉效果更自然。如果没有真实图片可以在本地放几张占位图通过相对路径引用不要依赖外网图床。6.3 答辩现场熟悉一下这三个问题逻辑删除、事务、跨域答辩演示完系统老师大概率会挑几个工程性问题。准备以下三个点就够了第一商品删除是物理还是逻辑删除为什么这么设计「因为订单明细冗余了商品快照物理删商品不影响历史订单但为了保留后台的恢复能力我选了逻辑删除」第二下单过程如果库存不够会怎样「UPDATE ... WHERE stock #{num}保证原子性事务回滚后订单不会残留」第三跨域是怎么解决的「开发期用 Vue 代理部署时交给 Nginx后端没有单独配 CORS 过滤器」。这三个问题对应本章的数据库脚本、后端事务、前端代理三块内容答完基本就能覆盖评分表里的技术深度项。最后一个现场演示的小技巧用浏览器的 Vue Devtools 插件先展示商品列表页里某个商品的 Vue 组件树再切到 Network 面板展示/api/product/list的请求和响应 JSON。这一步说明你对前端数据流和后端接口都心里有数比对着 PPT 念功能清单更有技术说服力。演示结束后记得把数据库脚本重新导出一次确保交到老师手里的.sql和你现场演示的数据完全一致。本文还有配套的精品资源点击获取
返回列表