2026/10/29 11:14:16

Spring Boot+MySQL网上购物系统:权限边界与订单快照设计

Spring Boot+MySQL网上购物系统:权限边界与订单快照设计 简介面向计算机及相关专业本科生、研究生与初学电子商务系统开发的读者这份课程设计文档围绕基于Spring Boot的网上购物商城系统给出从需求分析、总体设计、数据库建模到实现与测试的完整案例可用于课程设计参考、项目复现与答辩准备。压缩包共1个docx文件约3.8MB内容以系统设计说明书为主包含E-R图、数据库表结构、用户/商家/管理员三类模块说明以及功能、性能与安全测试记录。已有76人学习下载适合希望理解B/S结构、Java与MySQL在真实购物场景中如何落地的人群。读者可据此梳理模块划分与表结构设计对照测试方案检查系统薄弱点并参考文末的维护与扩展思路为后续加入智能推荐、图像识别等方向预留空间。1. 加购物车时价格对了下单后价格却变了答辩前一晚A同学演示下单流程商品在后台改过一次价格结果历史订单列表里的金额跟着一起变了——订单里存的不是下单那一刻的金额而是每次查询时从商品表里现算的。这类问题在基于 Spring Boot 的 MySQL 网上购物系统课程设计里极其常见功能清单每一项都能点通一碰数据一致性和角色权限就露馅。这套方案要解决的不是页面能不能跑起来而是三件事管理员、商家、用户三类角色的权限边界怎么切干净订单这类强一致数据该在哪一层定稿以及系统测试和验收怎么写才经得起追问。适合正在做网上购物商城系统毕设或课设的计算机专业学生也适合要交一份电商后台方案的从业者换掉字段名和模块名就能当骨架用。2. 需求拆解与角色边界三套功能清单怎么切才不打架需求分析这一章最容易被写成系统有商品管理、订单管理、用户管理这样的名词罗列。真正决定后面代码长什么样的是三件事每个角色能看到什么、每个字段谁能改、以及哪些操作必须登录。把这三件事在文档里写死后面的表结构、接口、拦截器基本都是顺着推下来的。2.1 管理员、商家、用户的功能清单与权限矩阵先把三个角色的用例盘清楚再谈界面。管理员侧是全局视角个人中心、用户管理、商家管理、商品分类管理、商品信息管理、我的收藏管理、系统管理、订单管理。注意商家管理和商品信息管理这两个管理员拥有的是审核和处置权不是替商家上架商品——这个区分不写清楚后面很容易做成管理员和商家共用一套商品编辑页权限就糊了。商家侧的清单明显更窄注册登录、个人中心、商品信息管理、我的收藏管理、订单管理。商家的商品管理只作用于自己名下的记录所以商品表里必须有一个所属商家账号字段所有商家侧的查询都要带上这个条件。这是整个系统里最容易被忽略的一条漏了就是越权别的商家能改你的价。用户侧是前台视角首页、商品信息浏览、注册登录、购物车、个人中心、我的地址、我的收藏管理、订单管理另外加一个客服入口。用户对商品只有读权限对订单只有创建和查看自己订单的权限。把这些压成一张矩阵表放进设计文档比写五页文字管用功能点管理员商家用户用户信息增删改查无仅本人查看与修改商家信息审核、禁用仅本人资料无商品分类增删改查只读上架时选择只读商品信息审核、下架、删除本人商品增删改查浏览、搜索订单全量查看、状态干预仅本人商品的订单仅本人订单收藏全量管理本人收藏本人收藏地址无无本人地址增删改查系统公告增删改查只读只读这张表的价值在于它直接对应到后面拦截器的路径规则/admin/**、/merchant/**、/user/**三组前缀各自校验登录态加角色。写文档的时候顺手把路径规划出来实现阶段几乎不用再想。2.2 非功能需求怎么从形容词变成可验收的数字原文提到的存储性、易学性、数据要求、稳定性、可靠性如果只写系统要稳定可靠答辩时老师一句你怎么证明就答不上来。我的习惯是每一条都配一个可测的指标哪怕指标定得保守也比形容词强。存储性对应到数据量和响应时间单表按一万条商品、五万条订单估算商品列表分页查询单页响应控制在 200 毫秒以内靠的是在分类字段、商家账号字段和创建时间上建索引。易学性对应到操作路径从登录后首页到达任意一个功能点击次数不超过三次界面上每个表单都带占位提示。数据要求对应到完整性约束删除商品分类时不能连带删除商品要么加外键限制要么改成逻辑删除。稳定性对应到连续运行模拟连续两小时下单、查询交替操作接口错误率低于百分之一内存不持续上涨。可靠性对应到三条硬规则——密码不能明文存、所有 SQL 走参数化、未登录请求一律拦在业务层之前。指标写进文档有个副作用是好的它逼着你在编码前就想清楚索引建在哪、密码怎么存。等写完再补通常就是补一段文字交差。2.3 可行性分析四段怎么写才不像凑字数技术可行性别写我是计算机专业学生所以技术上没问题。换成三段式已经掌握什么Java 语法、MySQL 基本增删改查、HTML/CSS 页面布局需要补什么Spring Boot 的自动配置机制、JPA 的实体映射、会话管理补的路径是什么官方文档、开源示例、按模块拆解练手。这么写既诚实又能体现规划能力比空喊有信心强得多。经济可行性列清单最省事开发工具用社区版数据库用开源版前端模板用开源 UI 库服务器用本机环境全部零成本。把每一项的名字列出来一行一个比一段成本很低有说服力。操作可行性聚焦界面所有功能入口在菜单里可见表单提交失败有明确提示删除操作二次确认列表页有搜索和分页。这三条做到就可以写无需培训即可上手。时间可行性必须有排期。按六周算第一周需求与选型第二周数据库设计与建表第三到四周核心模块编码第五周联调与页面完善第六周测试与文档。排期表本身就是可行性论证比任何形容词都硬。3. 架构与数据库设计B/S 三层结构落到六张核心表架构和数据库这两块如果分开写很容易出现架构图画得很漂亮表结构对不上的情况。我一般把它们放在同一章连着推先定分层再定每层负责什么数据最后落到表。这样读者顺着读下来能看清一个字段为什么长这样。3.1 为什么这套系统选 Spring Boot 加 MySQL课程设计里常见的另一条路是传统 SSM手写一堆 XML 配置。选 Spring Boot 的核心理由只有一个把时间花在业务上而不是配置上。内嵌 Tomcat 省掉单独部署容器的步骤起步依赖把 Web、数据访问、模板引擎一次性拉齐自动配置把数据源、事务管理器、JSON 序列化都按约定装好。对一个六周周期的课设来说这能省出至少三天。MySQL 这边选它的原因也很实在开源、体积小、安装配置简单、可视化客户端多学生机器上跑得动。它支持事务订单扣库存这种操作必须靠事务兜底。原文提到的多线程、多用户、开放源码这些特性落到项目里其实就是两句话——并发下单时靠行锁和事务保证不超卖数据文件可以直接拷走做备份。有一点要说清楚Spring Boot 不是不用懂 Spring。自动配置背后的条件装配、Bean 生命周期、事务传播行为答辩时问到的概率很高。我的建议是至少把Transactional的传播级别和失效场景弄明白这个后面避坑那一章会专门讲。3.2 B/S 三层结构与工程包结构的一一对应B/S 结构的工作方式是浏览器发请求、服务器给响应客户端不装专用软件。落到代码上就是三层表现层负责接收请求和渲染页面业务层负责规则和事务持久层负责和数据库对话。三层之间单向依赖表现层不直接碰数据库这是硬约束。工程包结构按这个分层切com.example.shop ├── controller 表现层接收参数、校验、返回视图或 JSON │ ├── admin 管理员端接口路径 /admin/** │ ├── merchant 商家端接口路径 /merchant/** │ └── user 用户端接口路径 /user/** ├── service 业务层事务边界都在这一层 │ └── impl ├── repository 持久层继承 JpaRepository 的接口 ├── entity 实体类与数据库表一一对应 ├── config 配置类拦截器注册、跨域、静态资源 ├── interceptor 登录与角色拦截 └── common 统一返回体、异常定义、工具类这个结构的好处是可预期要找某个功能的实现先看它属于哪个角色再进对应 controller业务逻辑一定在 service 里SQL 一定在 repository 里。评审或者答辩被问到这个功能在哪实现的三十秒内能翻出来。数据流是这样的浏览器请求打进 controllercontroller 做参数格式校验后调 serviceservice 里开事务、调一到多个 repository组装结果返回controller 再决定渲染模板还是返回 JSON。异常统一往上抛由全局异常处理器转成友好提示页面不会崩出一堆堆栈。3.3 从 E-R 图到表五类实体怎么落成六张核心表原文档的 E-R 图覆盖了商品分类、商家、订单、管理员、商品五类实体。落到物理表的时候有五类实体是必须的另外三张辅助表要根据功能清单补出来——购物车、收货地址、收藏。这三张在 E-R 图里没画但功能列表里有对应入口不建表就只能把数据塞进会话刷新一次就没了。实体识别有个简单判断标准如果它有独立的生命周期、会被单独查询、并且有多个属性就该独立成表。购物车满足这三条——用户会反复查看、按用户查、有数量和加入时间所以独立建表。收藏同理。地址如果不独立建表用户下单时就没法从历史地址里选只能每次手填体验直接掉一档。订单这块有个设计取舍值得单独说。原文档的订单实体把商品名称、商品图片、商品价格都放进了订单表也就是一笔订单对应一件商品。这在课设里是合理的简化好处是查询简单、不用连表。缺点是用户一次买三件商品就得生成三笔订单支付和退款都会变复杂。如果时间允许我建议拆成订单主表和订单明细表主表存订单号、用户、地址、总金额、状态、下单时间明细表存商品、单价、数量、小计。多花半天时间换来的是整个订单模块的扩展性后面要加优惠券、拆单发货都不用改结构。不管拆不拆有一条必须守住订单里的价格是快照来源于下单那一刻的商品价格之后商品调价、改名、下架都不影响历史订单。这就是第 1 章开头那个问题的根因。3.4 关键配置与三张核心表的建表语句配置这块数据源和 JPA 是最容易出问题的地方。先看application.ymlserver: port: 8080 servlet: session: timeout: 30m # 会话半小时过期演示时够用别设太长 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/shop_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: shop password: shop123 driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 # 课设环境 10 个连接足够别开太大 connection-timeout: 30000 jpa: hibernate: ddl-auto: validate # 关键只校验不自动改表防止误改字段丢数据 show-sql: true properties: hibernate: format_sql: true open-in-view: false # 关掉避免懒加载在视图层报错ddl-auto这个参数是重点。很多人图省事用update结果实体类改个字段名启动时表结构跟着变历史数据直接丢。改成validate之后实体和表对不上会启动失败反而能第一时间发现问题。建表统一走 SQL 脚本团队协作和答辩演示都更可控。再看三张关键表。商品表承载了分类、商家、审核状态三类信息CREATE TABLE goods ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, merchant_id BIGINT NOT NULL COMMENT 所属商家商家侧查询必带此条件, category_id BIGINT NOT NULL COMMENT 商品分类, name VARCHAR(120) NOT NULL COMMENT 商品名称, cover_img VARCHAR(255) DEFAULT NULL COMMENT 主图地址, detail TEXT COMMENT 详情富文本, price DECIMAL(10,2) NOT NULL COMMENT 当前售价订单快照的来源, stock INT NOT NULL DEFAULT 0 COMMENT 库存, sales INT NOT NULL DEFAULT 0 COMMENT 月售销量, audit_status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审 1通过 2驳回, audit_remark VARCHAR(255) DEFAULT NULL COMMENT 审核回复, clicks INT NOT NULL DEFAULT 0 COMMENT 点击次数, is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除标记, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id), KEY idx_merchant (merchant_id), KEY idx_audit (audit_status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品信息表;金额字段用DECIMAL(10,2)而不是DOUBLE这一点后面避坑章会展开。库存字段独立存在扣减时配合条件更新使用。is_deleted逻辑删除标记是为了让商品下架后历史订单仍能查到商品名这个问题同样在后面细说。订单表的关键在于快照字段CREATE TABLE orders ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单号业务唯一, user_id BIGINT NOT NULL COMMENT 下单用户, merchant_id BIGINT NOT NULL COMMENT 订单归属商家商家端按此过滤, goods_id BIGINT NOT NULL COMMENT 商品主键, goods_name VARCHAR(120) NOT NULL COMMENT 商品名快照, goods_img VARCHAR(255) DEFAULT NULL COMMENT 商品图快照, price DECIMAL(10,2) NOT NULL COMMENT 成交单价快照, quantity INT NOT NULL COMMENT 购买数量, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总额, address VARCHAR(255) NOT NULL COMMENT 收货地址快照, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待付款 1已付款 2已发货 3已完成 4已取消, pay_type TINYINT DEFAULT NULL COMMENT 支付方式, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id), KEY idx_merchant_status (merchant_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;goods_name、goods_img、price、address这四个快照字段是这个表设计的核心。商品改名、调价、下架订单里的信息岿然不动。地址同理用户改了自己的默认收货地址历史订单上的地址不能跟着变。购物车表相对简单但要加唯一约束防止同一商品重复加购CREATE TABLE cart ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, goods_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 1, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_goods (user_id, goods_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT购物车表;uk_user_goods这个唯一索引让同一用户同一商品只保留一条记录变成数据库层面的约束业务代码里只需要判断存在就加数量不存在就插入不用先查再插少一次往返也少了并发下的重复插入风险。4. 核心模块实现注册登录、购物车、订单三段主链路功能列表有十几项真正决定系统能不能立住的是三段主链路注册登录管住入口购物车管住过程订单管住结果。这三段写扎实其余的商品浏览、收藏、地址管理都是同类增删改查照葫芦画瓢即可。4.1 注册登录唯一性校验与密码存储注册流程原文写得很清楚判断用户名是否已存在存在则提示重填不存在则校验其他信息通过后写入数据库。这个逻辑不能只在页面上做必须在服务端做而且要依赖数据库唯一索引兜底——页面校验能绕过唯一索引绕不过。密码存储这条线我的态度很明确课程设计也不要存明文。用 Spring Security 提供的BCryptPasswordEncoder就行引入依赖后注册时加密、登录时校验代码量比手写 MD5 还少。Service public class UserService { Resource private UserRepository userRepository; // 无状态编码器直接作为单例使用不需要每次 new private static final BCryptPasswordEncoder ENCODER new BCryptPasswordEncoder(); public void register(String username, String rawPassword, String phone) { // 1. 唯一性校验数据库唯一索引是最后一道闸这里先给出友好提示 if (userRepository.existsByUsername(username)) { throw new BizException(用户名已存在请更换); } User user new User(); user.setUsername(username); // 2. 加盐哈希同一个密码每次加密结果都不同 user.setPassword(ENCODER.encode(rawPassword)); user.setPhone(phone); user.setCreateTime(LocalDateTime.now()); userRepository.save(user); } public User login(String username, String rawPassword) { User user userRepository.findByUsername(username) .orElseThrow(() - new BizException(用户名或密码错误)); // 3. 统一提示用户名或密码错误不暴露账号是否存在 if (!ENCODER.matches(rawPassword, user.getPassword())) { throw new BizException(用户名或密码错误); } return user; } }三个点值得说。第一existsByUsername需要 repository 里定义对应方法Spring Data JPA 靠方法名自动解析不用写 SQL。第二BCryptPasswordEncoder每次加密都会带随机盐所以同一个密码两次加密结果不同校验时用matches不能用字符串比较。第三登录失败提示统一成用户名或密码错误别分成账号不存在和密码错误两种后者等于告诉别人哪些账号是有效的。登录成功后把用户信息和角色写进 session键名固定成LOGIN_USER和ROLE后面拦截器统一读这两个键。商家和管理员的登录走同一套逻辑只是查的表不同、写入的角色值不同。4.2 购物车到订单库存扣减、金额计算与状态机这一段是整个系统最容易出问题的地方。下单要做四件事校验库存、扣减库存、生成订单快照、清空购物车。四件事必须在一个事务里任何一步失败全部回滚。核心难点是并发扣库存。先查库存再更新两个请求可能同时读到库存 1都判断够最后卖出 2 件。正确做法是把判断和扣减合成一条带条件的更新语句// GoodsRepository 中定义靠影响行数判断是否扣减成功 Modifying Query(update Goods g set g.stock g.stock - :num where g.id :id and g.stock :num) int reduceStock(Param(id) Long id, Param(num) Integer num);这条语句在数据库层面把库存足够和扣减绑成原子操作返回 0 就说明库存不够。再配合事务整个流程就成了Service public class OrderService { Resource private GoodsRepository goodsRepository; Resource private OrdersRepository ordersRepository; Resource private CartRepository cartRepository; Transactional(rollbackFor Exception.class) public String createOrder(Long userId, ListLong cartIds, String address) { ListCart carts cartRepository.findAllById(cartIds); if (carts.isEmpty()) { throw new BizException(请先选择要购买的商品); } BigDecimal totalAmount BigDecimal.ZERO; for (Cart cart : carts) { Goods goods goodsRepository.findById(cart.getGoodsId()) .orElseThrow(() - new BizException(商品已下架)); // 1. 条件更新扣库存返回 0 说明被别人抢先买走 int rows goodsRepository.reduceStock(goods.getId(), cart.getQuantity()); if (rows 0) { throw new BizException(商品[ goods.getName() ]库存不足); } // 2. 逐字段写快照绝不引用 goods 对象本身 Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setMerchantId(goods.getMerchantId()); order.setGoodsId(goods.getId()); order.setGoodsName(goods.getName()); order.setGoodsImg(goods.getCoverImg()); order.setPrice(goods.getPrice()); order.setQuantity(cart.getQuantity()); order.setTotalAmount(goods.getPrice().multiply(BigDecimal.valueOf(cart.getQuantity()))); order.setAddress(address); order.setStatus(OrderStatus.WAIT_PAY.getCode()); order.setCreateTime(LocalDateTime.now()); ordersRepository.save(order); totalAmount totalAmount.add(order.getTotalAmount()); } // 3. 清空已下单的购物车记录 cartRepository.deleteAllById(cartIds); return totalAmount.toPlainString(); } }参数说明几个关键点。Transactional(rollbackFor Exception.class)里的rollbackFor不能省Spring 默认只对运行时异常回滚抛受检异常时不回滚库存扣了订单没生成就是这么来的。BigDecimal做乘法时必须用multiply不能用*而且BigDecimal.valueOf(int)是安全的直接new BigDecimal(double)会带出一串精度尾巴。订单状态不要用字符串硬编码用枚举public enum OrderStatus { WAIT_PAY(0, 待付款), PAID(1, 已付款), SHIPPED(2, 已发货), FINISHED(3, 已完成), CANCELED(4, 已取消); private final int code; private final String desc; // 构造与 getter 省略 }状态流转也要有约束待付款只能转已付款或已取消已付款只能转已发货跳着转一律拒绝。取消订单时要把库存加回去这一条同样得写进事务里。没有这层约束测试时就可能出现已完成的订单又被取消库存加了两遍。4.3 商品检索与分类联动分页查询的写法商品列表是访问量最大的页面写法上要注意三点只查上架且审核通过的商品、按分类过滤、分页。GetMapping(/list) public PageGoods list(RequestParam(defaultValue 0) int page, RequestParam(defaultValue 12) int size, RequestParam(required false) Long categoryId, RequestParam(required false) String keyword) { Pageable pageable PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, createTime)); // 审核通过 未删除是固定条件不能交给前端传 return goodsService.pageOnline(categoryId, keyword, pageable); }page从 0 开始这一点和前端常见的从 1 开始不一样接口文档里必须写清楚否则第一页数据永远看不到。size加个上限比如超过 100 就按 100 处理防止有人传size999999把内存打爆。排序字段用创建时间倒序新品靠前符合购物场景。分类和关键词都是可选条件用required false在 service 里拼动态查询或者用 JPA 的 Specification。分类这块有个联动细节删除分类时不能直接删要先检查该分类下有没有商品有就拒绝并提示。直接把分类删了、商品的外键悬空商品列表按分类筛选时会整片消失。稳妥做法是分类也加逻辑删除或者干脆不允许删除有商品的分类。4.4 登录拦截与角色鉴权一个拦截器管三类路径前面权限矩阵里规划的三组路径前缀就是在这里落地的。写一个HandlerInterceptor在preHandle里做两件事查登录态、查角色。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(false); Object loginUser (session null) ? null : session.getAttribute(LOGIN_USER); // 1. 未登录统一拦截返回 JSON 让前端跳登录页 if (loginUser null) { writeJson(response, 401, 请先登录); return false; } // 2. 按路径前缀校验角色三组路径互不越界 String uri request.getRequestURI(); String role String.valueOf(session.getAttribute(ROLE)); if (uri.startsWith(/admin/) !ADMIN.equals(role)) { writeJson(response, 403, 无权限访问); return false; } if (uri.startsWith(/merchant/) !MERCHANT.equals(role)) { writeJson(response, 403, 无权限访问); return false; } return true; } private void writeJson(HttpServletResponse resp, int code, String msg) throws IOException { resp.setContentType(application/json;charsetUTF-8); resp.getWriter().write({\code\: code ,\msg\:\ msg \}); } }getSession(false)的意思是有会话就取没有就返回 null不主动创建。用getSession()会在每次未登录请求时都建一个空会话白白占内存。注册拦截器时通过addPathPatterns圈定范围把登录页、注册页、静态资源、商品浏览接口排除掉否则用户连首页都看不到。角色这块只做了粗粒度拦截。更严的做法是在 service 层再校验一次数据归属——商家查询订单时SQL 条件里必须带merchant_id 当前登录商家不能只靠路径拦截。路径拦截挡的是商家访问管理员接口挡不住商家 A 查商家 B 的订单。5. 避坑与常见问题排查五个最容易翻车的地方下面这五条基本都是我在类似项目里真实踩过或者看别人踩过的。现象、原因、解决三段式写照着对一遍能省掉答辩现场大半的尴尬。5.1 金额字段用了 double对账永远差几分现象购物车里三件商品单价 19.9页面显示合计 59.7订单表里存的是 59.699999999999996导出报表或者做统计时金额对不上。原因double是二进制浮点数很多十进制小数在二进制里是无限循环的存进去就已经不精确再参与运算误差会累积。Java 的float和double都不适合做金额计算。解决数据库统一用DECIMAL(10,2)Java 侧统一用BigDecimal。BigDecimal做除法时必须指定精度和舍入模式比如divide(new BigDecimal(3), 2, RoundingMode.HALF_UP)不指定会抛ArithmeticException。另外比较金额大小用compareTo不要用equals——new BigDecimal(1.0)和new BigDecimal(1.00)的equals返回 false因为精度不同。5.2 商品用了物理删除历史订单里的商品名变成空白现象商家下架并删除某个商品后用户翻历史订单商品名、图片全空了订单列表出现一片空白行。原因订单表通过商品 id 关联商品表查询时做连表或者实时查商品信息商品被物理删除后查不到。更糟的是如果加了外键约束删除商品直接报错商家那边又删不掉。解决商品表加is_deleted字段做逻辑删除删除操作改成update goods set is_deleted 1所有查询默认带is_deleted 0。同时订单表里的商品名、图片、价格必须存快照就是 3.4 节那几个字段。这两条一起做历史订单才真正不受影响。购物车里的商品被删除时查询要过滤掉或者在购物车列表里标成已失效。5.3 订单状态用字符串硬编码改一个状态要动五个文件现象一开始状态写的是待付款、已付款后来要加退款中前端页面、后端判断、数据库里的存量数据全得改改漏一处就出现状态显示空白。原因状态值散落在 controller 的 if 判断、service 的流转校验、页面模板的条件渲染、数据库的存量数据里没有唯一来源。解决定义OrderStatus枚举code 存数据库、desc 给页面显示所有判断都引用枚举常量。状态流转集中到一个方法里用MapInteger, SetInteger或者 switch 定义合法流转非法流转统一抛异常。这个改动成本很低但能让后面加状态变得非常轻松。5.4 前端传什么后端就存什么改价一测一个准现象下单接口接收前端传来的price和totalAmount直接入库用浏览器开发者工具改一下请求参数1 块钱买到原价 999 的商品。原因把前端当成可信来源。前端的任何参数都可以被篡改尤其是价格、库存、用户 id、订单状态这类敏感字段。解决价格一律由后端从商品表读取前端只传商品 id 和数量。用户 id 从 session 里取不接收前端传的userId。订单状态由后端按流程设置创建订单时固定为待付款不接受前端指定。商家操作的接口merchant_id从 session 取SQL 条件里带上防止改别人的商品。这几条是底线一条都不能省。5.5 测试只跑正向流程答辩被一个边界值问住现象演示时一切顺利老师随手输入一个空格、一个负数、一个超长字符串系统直接 500 报错页面。原因测试用例只覆盖了正常下单成功这类主流程边界值和异常输入没测。很多课设的测试章节写的全是点击按钮功能正常这种描述老师一眼就能看出没真跑过。解决每个功能至少准备四类用例——正常值、边界值、异常值、权限越界。比如数量输入正常 1 件、边界 0 件和库存上限、异常 -1 件和 99999 件、越界是未登录直接调接口。后端每个入参都加校验注解比如Min(1)、NotBlank、Size(max 120)配合Valid生效校验失败统一返回提示而不是 500。这一条在第 6 章还会展开成完整的测试方案。6. 验收与答辩系统测试怎么写演示怎么讲测试章节是课设文档里最容易糊弄也最容易被追问的部分。我的写法是先把测试方法说明白再上用例表最后给结论。方法这块功能测试用黑盒等价类划分加边界值分析不关心内部怎么实现只看输入输出是否符合预期兼容性测试至少覆盖两种浏览器性能测试做一个简单的并发压测用工具模拟 50 个用户同时访问商品列表和下单接口记录响应时间和错误率。不需要做到生产级但要能说出数据。用例表按模块分组每条写清楚前置条件、操作步骤、预期结果、实际结果。给个样例编号模块前置条件操作步骤预期结果TC-01注册用户名已存在输入已注册用户名提交提示用户名已存在TC-02注册无密码输入 5 位提示密码长度不足TC-03登录已注册密码输错提示用户名或密码错误TC-04购物车已登录同一商品加购两次数量变为 2不新增记录TC-05下单库存为 1两个会话同时下单一个成功一个提示库存不足TC-06下单已登录篡改请求中的价格参数订单金额仍为商品表价格TC-07权限商家登录直接访问 /admin/user/list返回 403不返回数据TC-08订单已下单商家修改商品价格历史订单金额不变TC-05 和 TC-06 这两条是加分项很多同学习惯性跳过并发和越权测试你能拿出来讲说明想过真实场景。答辩演示有个顺序讲究。别一上来就翻代码先用三分钟讲清楚这个系统解决什么问题、分几个角色、每个角色能做什么配那张权限矩阵表。然后按一条完整业务线走商家注册登录、发布商品、管理员审核通过、用户注册、浏览搜索、加购物车、下单、商家看到订单、发货、用户确认收货。这条线走完系统的核心价值就展示完了。演示过程里主动埋两个点。第一个是改价下单后切到商家端把商品价格改掉再回到用户端刷新订单详情金额纹丝不动这时候说一句订单里存的是快照。第二个是越权用商家账号在地址栏直接敲管理员路径页面返回无权限提示说一句路径前缀加拦截器角色不匹配直接拦掉。这两个动作各十秒钟但能挡掉答辩里最难缠的两类提问。被问到系统还有什么不足时别答功能还不够完善这种空话。挑一个具体的技术债说比如订单目前是一单一商品扩展性有限下一步会拆成订单主表和明细表支持一次结算多件商品或者库存扣减目前靠数据库行锁并发量再上一个量级就要引入缓存和消息队列削峰。承认边界比吹嘘完备更能体现你真的想过。最后说个习惯。从那以后我每次做完这类系统交之前都强制走一遍换一台干净机器从零部署的流程装数据库、导 SQL、改配置、启动、跑主流程。本地能跑不代表交上去能跑路径、端口、字符集、时区这四个地方几乎每次都会翻车一个。这一步花不了半小时但能避免答辩当天在讲台上和一堆报错干瞪眼。希望帮到你。本文还有配套的精品资源点击获取