ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Vue+MySQL商城系统开发:从环境搭建到答辩避坑指南

SpringBoot+Vue+MySQL商城系统开发:从环境搭建到答辩避坑指南 简介一套面向JavaWeb学习者的网上购物商城完整项目基于SpringBootVueMySQL实现包含前端商城与后台管理适合毕业设计、课程实训及想掌握前后端分离开发的读者。压缩包共774个文件121个Java源文件承载后端业务逻辑46个Vue组件与153个JS文件构成前端交互162个SVG、79个GIF等素材用于界面图标与动效展示另含SQL脚本可快速初始化数据库整体19.59MB目录按前后端与文档拆分便于按需查阅。设计文档与论文详细记录了需求分析、功能模块划分、总体设计及系统测试过程并针对测试结果指出改进方向能帮助读者理解完整开发流程。目前已有84人学习下载资料覆盖从环境搭建到编码实现再到测试总结的完整链路既可作为实战练习参考也能支撑论文撰写与答辩准备。1. 一套能写进简历也能答辩的购物商城系统到底拆成了什么SpringBoot Vue MySQL 这个组合几乎承包了当下 Java 方向课程设计和毕业设计里 70% 的选题。网上购物商城系统听着大其实拆开就是三件事用 SpringBoot 暴露 REST 接口、用 Vue 写页面调接口、用 MySQL 存商品和订单。源码加论文的结构意味着它不是纯粹写着玩的 demo而是要能跑通完整流程、能讲清设计理由、能在答辩现场面对老师追问的项目。这套方案适合两类人一类是正在做课程设计的学生需要一套业务逻辑完整但不过度复杂、代码结构能讲出道理的商城另一类是刚学完 SpringBoot 和 Vue 想练手的初级开发者想看看前后端到底怎么对接、订单状态和库存这种交易核心怎么处理。它解决的核心问题是让你在有限时间内搭出一个有商品浏览、购物车、下单支付模拟、订单管理主流程的完整系统而不是背了一堆框架名词却拼不出一个能点着跑起来的页面。2. 后端骨架先用 SpringBoot 跑通商品列表接口再谈架构做商城这种业务系统别一上来就画微服务架构图。SpringBoot 的价值就在于让 Spring 的配置变薄你只需要关注业务代码本身。我见过太多人把简单项目搞成过度设计最后连启动都要三分钟。先把商品列表这个最小闭环跑通后面加订单、加用户都是往这个骨架里塞东西。2.1 从零创建 SpringBoot 项目的三种姿势和选型理由创建项目常见的路子有三条选哪条取决于你的环境和时间。第一条是在 IDEA 里直接选 Spring Initializr跟着向导走选好 JDK 版本和依赖然后生成这个最省事但国内网络环境有时候拉取极慢我一般直接配阿里云镜像源。第二条是用 Spring Boot CLI 命令创建适合习惯命令行的人但实际开发里很少用。第三条是去 start.spring.io 网站下载压缩包再解压导入效果和第一条一样适合 IDEA 版本太老或者破解版里没集成 Initializr 的情况。依赖方面做商城你会用到 Spring Web、MyBatis或者 MyBatis-Plus、MySQL Driver、Lombok如果要做登录校验再加 Spring Security 或 JWT 相关依赖。我不建议第一次做项目就上 Spring Security配置复杂度会吃掉大量时间用拦截器加 JWT 工具类就够了面试时反而能讲得更透。下面是 pom.xml 里最关键的一段依赖注意版本统一用 Spring Boot 父依赖管理不要自己写 Spring 的版本号parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这段配置里值得注意的有两个点一是 MySQL 驱动在 SpringBoot 2.7.x 里推荐用com.mysql:mysql-connector-j老的mysql-connector-java也能用但会提示过时二是 MyBatis 的 starter 版本没有跟着 SpringBoot 的父依赖走必须自己指定这个坑很多人第一次踩不写版本号的话依赖会拉不下来或者拉到错误版本。2.2 用 MyBatis-Plus 写商品和订单表的第一组 CRUD商城系统最常用的是单表 CRUD、分页查询和简单的多表关联这种场景用 MyBatis-Plus 比原生 MyBatis 舒服得多。它帮你把 BaseMapper 里的通用方法生成好你只需要继承一个接口就能拥有selectById、insert、updateById这些方法省下大量写 XML 的时间。业务复杂的部分再手写 SQL两条腿走路。商品表对应的实体类这样定义字段名直接对应数据库列用驼峰命名也能通过配置开启自动驼峰转换Data TableName(product) public class Product { TableId(type IdType.AUTO) private Long id; private String name; private BigDecimal price; private Integer stock; private String imageUrl; private String description; private Integer status; // 1上架 0下架 TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }TableName注解用来映射数据库表名TableId(type IdType.AUTO)声明主键自增。这里有一个细节createTime和updateTime这类字段我不想在每次插入和更新的时候手动 set所以配了自动填充需要在代码里写一个 MetaObjectHandler 实现类不然这两个字段永远是 null订单列表按时间排序就会全部失效。Mapper 接口只需要继承 BaseMapper这就是 MyBatis-Plus 省事的地方public interface ProductMapper extends BaseMapperProduct { ListProduct selectOnSaleProducts(Param(offset) int offset, Param(limit) int limit); }第一个方法selectOnSaleProducts是自定义的为什么不用 BaseMapper 自带的selectList因为分页查询要带 offset 和 limit如果你自己手写 XML 来控制分页反而更直观。我用 MyBatis-Plus 的分页插件也能做但要多配置一个分页插件拦截器手写 SQL 更适合讲清楚原理答辩时老师问你怎么分页的你能直接说出LIMIT #{offset}, #{limit}的含义而不用背插件配置。2.3 接口返回结构和全局异常处理为什么必须提前定商城系统的接口不能一会儿返回{code:200, data:{}}一会儿又返回{ success: true }前端 axios 拦截器会疯掉。我一般统一用 Result 类包三层code、message、data。code 为 200 表示成功401 表示未登录500 表示业务异常。这个类用泛型写所有接口返回 Result 前端拿到后先看 code 再取数据逻辑就顺了。同时全局异常处理必须写否则 SQL 报错会把 Exception 堆栈直接甩给前端既难看又暴露表结构。用RestControllerAdvice加ExceptionHandler能兜住所有 Controller 层抛出的异常把错误信息包装成统一的 Result 结构返回。这样做的另一个好处是你在 Service 层写业务校验时可以大胆地throw new BizException(库存不足)不用在每个 Controller 里 try-catch代码干净很多。还有一个我已经吃过亏的点后端返回给前端的字段名必须是驼峰还是下划线最好一开始就统一。我习惯保持实体类驼峰JSON 默认也是驼峰Vue 里直接product.productName就能取到。如果你手写 SQL 时用了as product_name别名那前端就变成productName取不到了。这个玄学问题排查起来特别费时间建议规定所有接口返回字段统一走实体类映射别在 SQL 里乱起别名。3. 前端页面Vue2 还是 Vue3路由与购物车状态怎么落地商城前端的工作量通常比后端大因为页面多首页、商品列表、详情、购物车、结算、订单列表、后台管理。选 Vue2 还是 Vue3我的建议是看你的 SpringBoot 版本和参考源码的基线。如果源码是 Vue2 Element UI那就别强行升 Vue3否则一堆组件要重写。如果从零开始直接上 Vue3 Vite Element Plus 也行但要注意 Vue3 的生态和 Vue2 差异不小很多网上的老教程代码跑不通。3.1 Vue 项目初始化与代理配置环境变量怎么设用 Vite 创建 Vue3 项目的命令是npm create vitelatest shop-front -- --template vue创建完装依赖npm install接着安装路由vue-router4、状态管理pinia、HTTP 库axios。这套组合是目前 Vue3 项目的标准配置。如果你拿到的源码是 Vue2那对应的命令是npm install vue-router3别装错了版本Vue2 用的是 Vue Router 3Vue3 用 4不匹配的话路由跳转会直接报错。前端和后端联调最大的问题就是跨域。开发环境最简单的方式是在 Vite 的配置文件里配 proxy让前端请求/api开头的接口都转发到http://localhost:8080。这样浏览器看到的页面和接口是同源的就不存在跨域了// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })这段配置里rewrite的作用是去掉路径开头的/api。比如前端请求/api/product/list实际转发给 SpringBoot 的是/product/list。这意味着后端接口不要加/api前缀两边约定好前端统一加。有些后端会把 Controller 的 RequestMapping 写成/api/product/list那你这里就不能 rewrite否则 404这个要根据你的源码实际路由来调整。3.2 首页、商品详情、购物车页面的组件拆法与路由懒加载页面拆分的思路是按业务模块分目录在views下面建home、product、cart、order、admin这些目录每个目录放一个 index.vue 和它的子组件。比如首页的商品卡片单独抽成ProductCard.vue这样商品列表页和管理后台都能复用它。不要把所有页面都写成巨型组件一个 .vue 文件超过 500 行后面改需求你会怀疑人生。路由配置时用懒加载按需加载页面组件不然首次打开首页要下载所有页面的代码白屏时间会变长const routes [ { path: /, name: Home, component: () import(../views/home/index.vue), meta: { requiresAuth: false } }, { path: /product/:id, name: ProductDetail, component: () import(../views/product/detail.vue), meta: { requiresAuth: false } }, { path: /cart, name: Cart, component: () import(../views/cart/index.vue), meta: { requiresAuth: true } } ]meta.requiresAuth配合全局路由守卫使用未登录用户访问购物车和结算页时跳转到登录页。这个守卫写在router/index.js里判断 localStorage 里有没有 token没有就router.push({ path: /login, query: { redirect: to.fullPath }})。登录成功后带着redirect参数跳回原页面用户体验比直接回首页好很多。购物车的状态管理我用 Pinia 而不是 Vuex因为 Pinia 的 API 更简单且天然支持 Vue3 Composition API。购物车的 state 里维护一个 items 数组每个 item 包含商品对象和数量getter 计算总价和总件数action 里做增加、减少、删除和清空。注意购物车的持久化刷新页面后 state 会重置需要把 items 同步到 localStorage或者在用户加购时就调后端接口存数据库。课程设计一般是后者因为要配合订单流程。3.3 axios 拦截器里的 token 和统一错误处理axios 实例一般单独建一个src/utils/request.js把 baseURL、超时时间、请求拦截器、响应拦截器集中管理。请求拦截器统一在 headers 里带上 token响应拦截器统一处理 code 码比如 401 就清掉本地 token 并跳登录页业务异常直接弹出 message 提示。页面里调用接口时就只需要处理 data不用每次写重复的报错逻辑import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) if (res.code 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(new Error(res.message)) } return res.data }, error { ElMessage.error(error.response?.data?.message || 网络异常请稍后再试) return Promise.reject(error) } ) export default request这里有一个容易搞混的点axios 的响应拦截器里第一个参数是 HTTP 200 时的响应对象response.data才是后端返回的 Result JSON。我在拦截器里直接返回res.data这样页面调用request.get(/product/list)拿到的就直接是 Result 里的 data 字段不需要再.data.data取一次。这个约定一定要和后端商量好如果后端某个接口返回结构不一致前端所有页面都要跟着改。4. MySQL 表设计商品、用户、订单三张表怎么样才不踩坑数据库是商城系统的地基。表设计得好不好直接影响后面 SQL 好不好写、订单流程会不会出 bug。我不建议用太多表课程设计里核心就五张用户表、商品表、购物车表、订单表、订单项表。再加一张分类表可选但一张分类表会把复杂度抬升一级初做者容易在分类联查上卡住。4.1 核心表结构与字段类型选择先看用户表和商品表。用户表存放账号密码和基础信息密码不能存明文至少用 MD5 加盐或者 BCrypt 加密。商品表要注意价格用DECIMAL(10,2)别用FLOAT二进制浮点数算钱会出现 0.1 加 0.2 不等于 0.3 的问题这在订单总价计算里是致命的。库存字段用INT UNSIGNED配合后面修改的 SQL 做扣减校验。CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, phone varchar(20) DEFAULT NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE product ( id bigint(20) NOT NULL AUTO_INCREMENT, category_id bigint(20) DEFAULT NULL COMMENT 分类id可空, name varchar(100) NOT NULL COMMENT 商品名称, price decimal(10,2) NOT NULL COMMENT 售价, original_price decimal(10,2) DEFAULT NULL COMMENT 划线价, stock int(10) unsigned NOT NULL DEFAULT 0 COMMENT 库存, image_url varchar(255) DEFAULT NULL COMMENT 主图地址, description text COMMENT 商品详情, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表;字符集必须用utf8mb4因为 MySQL 的utf8实际上不是完整的 UTF-8它存不了 Emoji 和生僻字。商品描述里如果有特殊符号用utf8会报 “Incorrect string value” 错误这个坑几乎人人都踩。ON UPDATE CURRENT_TIMESTAMP是让更新时间自动改变省去在应用层手动 setupdateTime比 MyBatis-Plus 的字段填充还要省事。4.2 订单状态流转和库存扣减的 SQL 写法订单表的设计核心是状态字段。我用一个TINYINT表示订单状态0 待付款、1 已付款待发货、2 已发货、3 已完成、4 已取消。这个状态流转必须画清楚用户下单后是 0支付成功变 1商家发货变 2确认收货变 3。取消订单可以在 0 和 1 时做但已发货就不能取消了。后端在更新状态时SQL 里要加上对当前状态的判断防止重复提交导致状态错乱。下单时最关键的 SQL 是扣库存。这里不能用SELECT stock FROM product WHERE id 1查到库存再在后端判断那样高并发下会超卖。正确做法是直接在 UPDATE 语句里扣减并带上库存大于购买数量的条件UPDATE product SET stock stock - #{buyNum} WHERE id #{productId} AND stock #{buyNum}这条语句影响的行数如果为 0说明库存不足或者商品被下架了事务就该回滚下单失败。用了这种乐观扣减方式即使两个请求同时带着相同数量下单MySQL 的行锁也会让它们串行执行后一个请求会因为stock buyNum条件不成立而更新 0 行从而避免超卖。这也是答辩时老师最喜欢问的一个点你要能讲出为什么不先查询再更新。订单表字段不必太多关键字段包括订单号、用户 id、总金额、状态、收货人信息、创建时间和支付时间。订单号我一般用时间戳加用户 id 加随机数拼一个唯一字符串不要用自增 id 暴露订单量。订单项表关联订单 id 和商品 id记录下单时的商品快照包括商品名称、单价和购买数量。为什么是快照因为商品表和价格会改但订单里的成交信息不能跟着变。4.3 索引和事务隔离级别怎么配合订单表和订单项表都要建索引。订单表的user_id字段要加普通索引因为每个用户查自己的订单列表是高频查询。订单项表order_id一定要加索引而且最好用UNIQUE KEY uk_order_product(order_id, product_id)避免同一订单里重复加同一商品——但你前端购物车逻辑其实已经避免了这个后端加唯一索引只是兜底。商品表除了主键可以在status和category_id上建复合索引这样“查询某个分类下上架的商品”能走索引。事务隔离级别SpringBoot 默认是 MySQL 的 REPEATABLE READ可重复读。对商城系统来说够用因为库存扣减已经通过行锁和条件 UPDATE 解决了。但要注意如果在 Service 方法上没加Transactional扣库存和生成订单两个操作就不在一个事务里可能出现库存扣了但订单没生成或者订单生成了但库存没扣。Transactional加在 public 方法上且不能在同一类的内部通过this调用否则事务代理不生效这个细节我放到下一章展开。5. 前后端联调避坑本地能跑一打包就翻车的 5 个常见问题做一个商城项目后端单独测接口时一切正常前端npm run dev也能打开页面但把前后端合在一起就各种 404、报错、数据不对。下面这五条是我历年做项目里掉过的坑每条都是现象、原因、解决三步式直接照抄能省半天时间。5.1 CORS 跨域开发环境配一次生产环境又冒出来现象开发时配了 Vite 代理页面能正常访问接口。但把 SpringBoot 打包后和 Vue 的 dist 静态文件放在一起页面打开了却请求不到接口浏览器控制台报 “No Access-Control-Allow-Origin header is present”。原因开发模式下代理解决了跨域但部署时如果直接用 Nginx 或者把 Vue 打包文件放进 SpringBoot 的 static 目录下浏览器请求的地址是同一个域名理论上不应该有跨域。但如果前后端分开部署在不同的端口比如前端 3000、后端 8080Nginx 没配代理转发那跨域问题就会再次冒出来。解决两个思路一是后端加一个全局 CORS 配置类二是在部署层做转发。我习惯两个都做。后端的 CORS 配置类如下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); } }注意allowedOriginPatterns不要用allowedOrigins(*)因为当 allowCredentials 为 true 时Spring 不允许 Origin 为*会报 IllegalArgumentException。使用allowedOriginPatterns(*)配合allowCredentials(true)才是正确姿势。如果线上用 Nginx则在 location 块里加proxy_pass http://localhost:8080;把/api前缀的请求转发到后端这样前端请求走同源后端 CORS 配置甚至可以不启用。5.2 LocalDateTime 序列化前端收到一串数字的玄学现象后端实体类里用 LocalDateTime 表示创建时间接口返回的 JSON 里这个字段要么是一串像1710000000000的数字要么是2024-03-09T10:15:30这种带 T 的格式前端日期组件显示不正常。原因SpringBoot 默认用 Jackson 序列化 LocalDateTime如果不配置格式默认会序列化成 ISO 8601 格式或者时间戳取决于是否引入 jsr310 模块。而且通过TableField(fill FieldFill.INSERT)填充的时间字段特别容易在插入后查出来格式不对。解决统一在application.yml里配置 Jackson 的日期格式和时区spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8如果配置后 LocalDateTime 还是不对那说明实体类字段的JsonFormat注解没有生效检查一下是不是引入了jackson-datatype-jsr310。也可以直接在实体类字段上加注解强制指定JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime;这种做法最保险但它会引入另一个问题如果前端需要同时拿到时间戳做倒计时你又要额外返回一个字段。所以更推荐在全局配置里统一格式字段级别只在特殊场景用。5.3 MyBatis 的 mapper.xml 路径不对启动直接报错现象SpringBoot 启动时报Invalid bound statement (not found)或者提示找不到ProductMapper.selectOnSaleProducts这个方法。原因MyBatis 扫描到 Mapper 接口后去加载对应的 XML 文件如果 XML 的 namespace 写错或者 XML 文件没有放在src/main/resources/mapper目录下又或者 application.yml 里没有配置mapper-locations就会导致接口有方法但找不到 SQL 语句。解决项目里把 XML 统一放在src/main/resources/mapper下文件名和 Mapper 接口名保持一致比如ProductMapper.xml。然后在 application.yml 里这样配置mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case这个参数一定要打开否则数据库的create_time字段无法自动映射到实体类的createTime属性查出的一行数据所有时间字段全是 null。打开了它之后实体类字段与数据库列只要满足驼峰和下划线的对应规则就能自动映射省去写一长串 resultMap。5.4 事务不生效在同一个类里调方法回滚了个寂寞现象下单方法createOrder里调用了deductStock方法如果deductStock执行时库存不足抛出异常createOrder里已经插入的订单数据没有回滚数据库里出现库存没减但订单存在的脏数据。原因Spring 的事务是基于 AOP 代理实现的。当你调用this.deductStock()时调用的是目标对象的方法而不是代理对象的方法事务注解就被绕过了。这被称为内部调用导致事务失效。同样的坑还有方法不是 public、类没有被 Spring 管理、异常被 try-catch 吞掉等。解决最简单的方式是把事务方法拆到不同的 Service 类中比如把扣库存放到StockService.deductStock()下单的 Service 注入 StockService通过 Spring 容器调用事务自然生效。如果非要同类调用可以通过Autowired注入自身代理Spring 循环依赖已经允许或者在类内部用AopContext.currentProxy()但都不如拆分类干净。另外要注意Transactional默认只对 RuntimeException 回滚如果你在 Service 里抛的是 IOException 这类 checked exception事务不会回滚。下单场景可以只抛自定义 RuntimeException或者配置rollbackFor Exception.class我一般直接Transactional(rollbackFor Exception.class)写死省得笔试面试时被问倒。5.5 静态资源 404Vue 打包后 dist 目录放不进 SpringBoot现象执行npm run build生成了 dist 目录把整个 dist 复制到 SpringBoot 的src/main/resources/static下启动项目访问http://localhost:8080/却是 404路由器刷新页面也出不来。原因Vue 是单页应用打包后的 index.html 里引用的 JS/CSS 路径默认是绝对路径/assets/xxx.js如果部署在域名根路径下没问题但如果加了项目前缀比如http://localhost:8080/shop/就会找不到资源。更大的坑是 Vue Router 用 history 模式时后端没有配置 fallback直接访问/product/1这样的路径SpringBoot 找不到对应映射就 404 了。解决如果只是做课程设计建议用 Vue Router 的 hash 模式路径上带#怎么刷新都不会 404最省心。如果一定要用 history 模式那要在 SpringBoot 里做一个转发配置把非接口路径全部转发到 index.htmlController public class ViewController { RequestMapping(value {/{path:[^\\.]*}, /{path:^(?!api$).*}/**/{path:[^\\.]*}}) public String forward() { return forward:/index.html; } }这段配置的正则要小心它会把所有不带点的路径转发到 index.html但必须保证/api开头的接口不被拦截。更好的办法是约定前端打包后用 Nginx 部署在 Nginx 里配置location / { try_files $uri $uri/ /index.html; }这样就不用动后端代码了。但从答辩演示的简便性来说hash 模式永远是最省事的选择。6. 从能跑到能答辩论文怎么写演示数据怎么造项目怎么讲到这一步你有一个能完整跑起来的商城系统但课程设计的交付物不止是代码还有一篇论文和一场演示。论文不是把代码拷贝粘贴然后写一段“本系统采用……”就完事而是要把设计过程讲清楚需求分析拆出了哪些角色和功能数据库为什么这样设计核心接口的时序流程是什么测试用了哪些数据验证了哪些边界。我见过太多人代码写得很棒论文却写成操作手册答辩时被老师一句“这个设计有什么不足”问哑火。演示数据是容易被忽视的一个环节。别用一堆“测试商品1”“测试商品2”这种数据演示观感极其廉价。我用一张图片链接替换本地图片资源商品名称用真实品牌名价格用小数与环境相符的数字库存设成几十到几百不等然后造五六个分类每个分类下十来个商品。订单数据要覆盖不同的状态待付款一单、已发货一单、已完成一单这样演示时点每个菜单都有内容可看。造数据直接写 SQL 脚本别在页面上手动点效率太低。答辩讲项目时建议按“用户故事”来串而不是按代码结构来串。先讲用户进入首页浏览商品点击详情加购去结算登录支付然后商家端看到订单发货用户确认收货。这个流程走一遍系统 80% 的功能都被覆盖了。讲的时候主动抛出两个技术亮点一个是库存的乐观扣减另一个是 JWT 登录态校验这两点最能展示你是真的理解了项目而不是背了别人的代码。如果你想再进一步可以给订单模块加一个定时任务超过 30 分钟未付款自动取消订单用一个Scheduled注解就能实现但论文里要多写一节“订单超时处理的设计与实现”。最后我有一个自己的习惯分享给你项目做完后把启动步骤和测试账号写成 README 放在项目根目录包括怎么初始化数据库、怎么启动后端、怎么启动前端、默认管理员账号是什么。因为课程设计的代码经常要在不同电脑上演示老师可能会现场换机器让你跑一次没有 README 的话你容易在众目睽睽之下现配环境。这个习惯也延续到了我之后的工作里每次交付项目必留一份可复现的运行说明。希望这些经验能帮你少走一些弯路也希望你的商城系统能顺利通过答辩真的把它当成一次做产品的小型演练而不是一次必须应付的作业。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进