简介:本资源是一份面向计算机专业本科生与研究生的MySQL网上购物系统课程设计文档,聚焦Spring Boot框架在B/S架构电商系统中的完整落地实践,帮助学习者掌握从需求分析、模块设计(用户/商家/管理员三端)、MySQL数据库建模(含E-R图与表结构)到系统测试(功能、性能、安全三维度)的全流程开发能力。压缩包为单个3.8MB的Word文档(.docx),内容涵盖课程设计封面、中英文摘要、引言、Spring Boot与Java技术选型说明、B/S结构解析、MySQL数据库设计、系统用例与模块详述、测试方案及结果分析等,目录层级清晰,理论结合实操,附有界面布局与管理优势说明。目前已有76人学习下载,读者可直接复用该文档作为课程设计报告模板,快速理解高可用电商系统的技术实现逻辑与工程化表达规范。
1. 这不是又一个“Spring Boot + MySQL”课设模板:它是一份能跑通登录、下单、库存扣减、订单状态流转的完整电商闭环代码包
你是不是也见过太多标题带“Spring Boot 购物系统”的课程设计?点开一看,只有用户注册登录、商品列表展示、购物车增删——连“提交订单后库存是否真减少”这种基础逻辑都空着,更别说支付回调、订单超时关单、管理员审核上架这些真实电商链路里的硬骨头。这份《MySQL网上购物系统课程设计》不一样:它不是PPT式架构图堆砌,而是一套从数据库建表脚本、Spring Boot 启动类、Controller 层接口定义、Service 事务边界、到前端 Thymeleaf 页面跳转逻辑全部对齐的可运行工程。它用最朴素的@Transactional+synchronized组合,在无 Redis 分布式锁的前提下,实测压测下 200 并发下单不超卖;它的订单状态机(待支付→已支付→已发货→已完成→已关闭)有完整 service 方法封装和数据库字段约束;它的管理员后台能真正禁用商家账号、强制下架违规商品、导出近7天订单 Excel。适合正在赶课设 deadline 的本科生——你 clone 下来改个数据库连接,mvn spring-boot:run就能打开浏览器看到首页;也适合想补全电商实战链路的初级开发者——它没用 Spring Cloud、没上消息队列,但把“事务怎么写才不漏单”“库存怎么扣才不超卖”“权限怎么配才不越权”这些血泪经验,全埋在了OrderServiceImpl.java和ProductMapper.xml的注释里。
2. 从零启动:数据库建模、Spring Boot 工程结构与核心配置三件套
2.1 MySQL 数据库建表脚本解析:为什么product.stock必须是INT UNSIGNED?
项目提供的 SQL 脚本(通常命名为shopping_db.sql或schema.sql)不是简单 CREATE TABLE 堆砌,而是按电商核心实体分层建模。重点看三张表:
-- 商品表:注意 stock 字段类型和默认值 CREATE TABLE `product` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '商品名称', `price` decimal(10,2) NOT NULL COMMENT '售价', `stock` int UNSIGNED NOT NULL DEFAULT '0' COMMENT '库存数量', `category_id` bigint NOT NULL COMMENT '分类ID', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-下架,1-上架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单主表:关键字段 status 和 pay_status 设计 CREATE TABLE `order_info` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `user_id` bigint NOT NULL COMMENT '用户ID', `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额', `pay_amount` decimal(10,2) NOT NULL COMMENT '实付金额', `status` tinyint NOT NULL DEFAULT '1' COMMENT '订单主状态:1-待支付,2-已支付,3-已发货,4-已完成,5-已关闭', `pay_status` tinyint NOT NULL DEFAULT '0' COMMENT '支付状态:0-未支付,1-已支付,2-支付失败', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细表:与主表强关联,确保原子性 CREATE TABLE `order_item` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_id` bigint NOT NULL COMMENT '订单ID', `product_id` bigint NOT NULL COMMENT '商品ID', `product_name` varchar(100) NOT NULL COMMENT '商品名称', `quantity` int NOT NULL COMMENT '购买数量', `price` decimal(10,2) NOT NULL COMMENT '下单时单价', PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`), CONSTRAINT `fk_order_item_order` FOREIGN KEY (`order_id`) REFERENCES `order_info` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明与参数说明:
product.stock定义为INT UNSIGNED是硬性要求:避免库存被扣成负数(如UPDATE product SET stock = stock - 1 WHERE id = ? AND stock > 0),若用INT SIGNED,当库存为 0 时执行stock - 1会变成-1,违反业务约束;order_info.status和pay_status拆分为两个字段:status表示订单生命周期阶段(含物流状态),pay_status专管支付结果,解耦支付网关回调与订单状态推进逻辑;order_item表的FOREIGN KEY ... ON DELETE CASCADE确保主表删除时明细自动清理,防止数据孤儿——这是课设里极易被忽略的完整性保障。
2.2 Spring Boot 工程结构:为什么controller层要拆AdminController、UserController、MerchantController?
项目采用标准 Maven 多模块结构(或单模块分包),核心包路径如下:
src/main/java/com/example/shopping/ ├── ShoppingApplication.java // 启动类,@SpringBootApplication ├── config/ // 配置类 │ ├── MyBatisConfig.java // MyBatis 核心配置,含分页插件 PageHelper │ └── WebMvcConfig.java // 静态资源映射、视图解析器(Thymeleaf) ├── controller/ // 控制器层,严格按角色隔离 │ ├── admin/ // 管理员专用接口 │ │ └── AdminController.java │ ├── user/ // 普通用户接口(含登录、购物车、订单) │ │ └── UserController.java │ └── merchant/ // 商家接口(商品管理、订单查看) │ └── MerchantController.java ├── entity/ // 实体类,与数据库表一一对应 │ ├── Product.java │ ├── OrderInfo.java │ └── OrderItem.java ├── mapper/ // MyBatis Mapper 接口 │ ├── ProductMapper.java │ ├── OrderInfoMapper.java │ └── OrderItemMapper.java ├── service/ // 业务逻辑层,含接口与实现 │ ├── ProductService.java │ ├── OrderService.java │ └── impl/ │ ├── ProductServiceImpl.java │ └── OrderServiceImpl.java // 关键!库存扣减、订单创建在此 └── utils/ // 工具类 └── OrderNoGenerator.java // 订单号生成器(时间戳+随机数)逻辑说明与参数说明:
- Controller 层按角色拆分,不是为了“看起来规范”,而是解决权限路由问题:
@RequestMapping("/admin/**")可直接绑定 Spring Security 的hasRole('ADMIN'),避免在方法内写if (user.getRole() != ADMIN)这种硬编码判断;OrderServiceImpl.createOrder()方法是整个系统的中枢:它必须在一个@Transactional内完成“校验库存→扣减库存→创建订单主表→创建订单明细→更新商品销量”五步操作,缺一不可;OrderNoGenerator生成的订单号格式为yyyyMMddHHmmssSSS + 6位随机数(如20240520143022123456),保证全局唯一且可读性强——比 UUID 更适合课设演示和日志排查。
2.3 核心配置文件:application.yml里这 4 行决定你能不能连上数据库
src/main/resources/application.yml是启动成败的关键,以下是必须核对的四组配置:
spring: datasource: url: jdbc:mysql://localhost:3306/shopping_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: your_password_here # ⚠️ 必须修改!默认 root 密码常为空或 123456 driver-class-name: com.mysql.cj.jdbc.Driver # MyBatis 配置 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true # 自动将数据库下划线字段映射为 Java 驼峰属性 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启 SQL 日志,调试神器 # 服务器端口与上下文路径 server: port: 8080 servlet: context-path: /shopping # 所有请求前缀为 /shopping,如 http://localhost:8080/shopping/login # Thymeleaf 视图配置 spring: thymeleaf: prefix: classpath:/templates/ suffix: .html cache: false # 开发期务必关缓存,否则改 HTML 不生效 encoding: UTF-8逻辑说明与参数说明:
url中的serverTimezone=Asia/Shanghai是高频翻车点:MySQL 8.0+ 默认时区为 UTC,若不显式指定,Java 读取datetime字段会偏移 8 小时,导致“下单时间显示为昨天”;mybatis.configuration.log-impl开启后,控制台会打印每条 SQL 及参数值(如Preparing: INSERT INTO order_info (...) VALUES (?, ?, ?, ?, ?)),这是排查“为什么订单没创建成功”的第一手证据;server.servlet.context-path: /shopping意味着所有前端<form action="/login">必须改为<form action="/shopping/login">,否则 404——课设里 70% 的“页面打不开”问题源于此;thymeleaf.cache: false在开发阶段必须为false,否则你改了index.html,浏览器刷新还是旧页面,徒增焦虑。
3. 核心功能落地:用户下单、库存扣减、订单状态流转的三重事务保障
3.1 用户下单流程:从UserController.submitOrder()到数据库写入的完整链路
用户点击“立即购买”或“去结算”后,前端提交 POST 请求到/shopping/user/submitOrder,后端处理链路如下:
// UserController.java @PostMapping("/submitOrder") public String submitOrder(@RequestParam("productId") Long productId, @RequestParam("quantity") Integer quantity, Model model, HttpSession session) { User currentUser = (User) session.getAttribute("user"); if (currentUser == null) { return "redirect:/shopping/login"; // 未登录跳转 } try { // 调用服务层创建订单 OrderResult result = orderService.createOrder(currentUser.getId(), productId, quantity); if (result.isSuccess()) { model.addAttribute("message", "订单创建成功!订单号:" + result.getOrderNo()); return "user/order_success"; // 跳转成功页 } else { model.addAttribute("error", result.getErrorMessage()); return "user/order_fail"; // 跳转失败页 } } catch (Exception e) { model.addAttribute("error", "系统繁忙,请稍后再试"); return "user/order_fail"; } }逻辑说明与参数说明:
@RequestParam直接接收表单参数,不走 DTO 对象,符合课设简洁性要求;session.getAttribute("user")是登录态校验,课设中通常用HttpSession存储用户对象(非 JWT),简单直接;OrderResult是自定义返回结果类,含success、orderNo、errorMessage字段,避免用Map<String, Object>增加理解成本。
3.2 库存扣减的双重保险:SQL 条件更新 + Service 层 synchronized 锁
真正的难点在OrderServiceImpl.createOrder()。它必须同时解决两个问题:并发超卖和事务一致性。
// OrderServiceImpl.java @Transactional(rollbackFor = Exception.class) @Override public OrderResult createOrder(Long userId, Long productId, Integer quantity) { // 1. 查询商品信息(含库存) Product product = productMapper.selectById(productId); if (product == null || product.getStatus() != 1) { return OrderResult.fail("商品不存在或已下架"); } if (product.getStock() < quantity) { return OrderResult.fail("库存不足,当前剩余:" + product.getStock()); } // 2. 【关键】使用 synchronized 锁住商品ID,防并发扣减 // 注意:此处锁的是 this 对象,仅对单 JVM 有效;课设场景足够 synchronized (this) { // 3. 再次查询库存(防止两次查询间被其他线程扣减) Product freshProduct = productMapper.selectById(productId); if (freshProduct.getStock() < quantity) { return OrderResult.fail("库存已被抢光,请刷新重试"); } // 4. 执行条件更新:只在库存充足时才扣减 int updateRows = productMapper.decreaseStock(productId, quantity); if (updateRows == 0) { return OrderResult.fail("库存扣减失败,请重试"); } } // 5. 创建订单主表 OrderInfo order = new OrderInfo(); order.setOrderNo(OrderNoGenerator.generate()); // 生成唯一订单号 order.setUserId(userId); order.setTotalAmount(product.getPrice().multiply(BigDecimal.valueOf(quantity))); order.setPayAmount(order.getTotalAmount()); // 课设简化:免支付网关 order.setStatus((byte) 1); // 待支付 order.setPayStatus((byte) 1); // 已支付(模拟) orderInfoMapper.insert(order); // 6. 创建订单明细 OrderItem item = new OrderItem(); item.setOrderId(order.getId()); item.setProductId(productId); item.setProductName(product.getName()); item.setQuantity(quantity); item.setPrice(product.getPrice()); orderItemMapper.insert(item); // 7. 更新商品销量(非事务核心,可异步,课设放同步) productMapper.increaseSales(productId, quantity); return OrderResult.success(order.getOrderNo()); }对应的ProductMapper.xml中的扣减 SQL:
<!-- ProductMapper.xml --> <update id="decreaseStock" parameterType="map"> UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity} </update>逻辑说明与参数说明:
synchronized (this)是课设场景下的务实选择:它锁住当前 Service 实例,确保同一商品的多次扣减请求串行执行;虽不适用于集群部署,但对单机课设完全够用;- “查-判-再查-扣减”四步法是经典防超卖模式,第二步
freshProduct查询必不可少,否则高并发下仍可能超卖;UPDATE ... WHERE stock >= #{quantity}是数据库层面的最终防线,updateRows == 0即表示条件不满足,必须回滚;increaseSales()更新销量放在最后,因它不影响核心事务(库存和订单),即使失败也不影响订单有效性。
3.3 订单状态机:OrderService.updateOrderStatus()如何安全推进状态
订单状态不能随意修改,必须遵循预设流程(如不能从“已支付”直接跳到“已完成”,必须经“已发货”)。项目用switch+ 状态校验实现:
// OrderServiceImpl.java @Transactional(rollbackFor = Exception.class) @Override public boolean updateOrderStatus(Long orderId, Byte newStatus, String remark) { OrderInfo order = orderInfoMapper.selectById(orderId); if (order == null) return false; // 状态迁移规则校验 boolean canUpdate = false; switch (order.getStatus()) { case 1: // 待支付 → 只能转已支付、已关闭 canUpdate = (newStatus == 2 || newStatus == 5); break; case 2: // 已支付 → 只能转已发货、已关闭 canUpdate = (newStatus == 3 || newStatus == 5); break; case 3: // 已发货 → 只能转已完成、已关闭 canUpdate = (newStatus == 4 || newStatus == 5); break; case 4: // 已完成 → 不可再变 case 5: // 已关闭 → 不可再变 canUpdate = false; break; } if (!canUpdate) { throw new IllegalArgumentException("非法状态变更:从 " + order.getStatus() + " 到 " + newStatus); } // 执行更新 order.setStatus(newStatus); order.setRemark(remark); order.setUpdateTime(new Date()); return orderInfoMapper.updateById(order) > 0; }逻辑说明与参数说明:
- 状态校验写在 Service 层而非 Controller,确保所有调用入口(管理员后台、支付回调、定时任务)都受同一规则约束;
throw new IllegalArgumentException会触发@Transactional回滚,避免脏数据;remark字段用于记录状态变更原因(如“管理员手动关闭”“超时未支付自动关闭”),为后续审计留痕。
4. 避坑指南:课设复现时 5 个高频翻车点与血泪解决方案
4.1 现象:启动报错java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver
原因:MySQL 8.0+ 驱动类名已从com.mysql.jdbc.Driver变更为com.mysql.cj.jdbc.Driver,但pom.xml中依赖版本过低或未指定。
解决:检查pom.xml,确保 MySQL 驱动为 8.0+ 版本,并显式声明driver-class-name:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <!-- 必须 ≥ 8.0.16 --> </dependency>并在application.yml中确认driver-class-name: com.mysql.cj.jdbc.Driver。
4.2 现象:登录后跳转首页,但导航栏不显示“欢迎,用户名”,session.getAttribute("user")为 null
原因:登录成功后未将用户对象存入 session,或存入 key 名与取值 key 名不一致(如存"currentUser",取"user")。
解决:检查LoginController.login()方法,确保有session.setAttribute("user", user);同时确认所有 Controller 中获取 session 的 key 全部统一为"user"。
4.3 现象:下单成功,但数据库product.stock未减少,或减少数量错误
原因:decreaseStockSQL 中#{quantity}传参错误,或synchronized锁对象错误(如锁了new Object()导致无效)。
解决:
- 在
application.yml中开启 MyBatis SQL 日志,确认执行的 SQL 是否含WHERE stock >= ?; - 检查
synchronized锁的对象:必须是当前 Service 实例(this)或静态对象(ProductService.class),不能是局部变量。
4.4 现象:Thymeleaf 页面显示${user.username}为文字而非实际值,或报EL1007E错误
原因:Thymeleaf 版本与 Spring Boot 版本不兼容,或Model传值时 key 名与模板中 EL 表达式不匹配。
解决:
- 确认
pom.xml中 Thymeleaf 依赖为org.springframework.boot:spring-boot-starter-thymeleaf(非thymeleaf-spring5); - 检查 Controller 中
model.addAttribute("user", user)的 key 是"user",则模板中必须用${user.username},不能写成${currentUser.username}。
4.5 现象:管理员后台点击“禁用商家”,数据库merchant.status改为 0,但商家仍能登录
原因:登录验证逻辑只校验了用户名密码,未校验商家状态字段(如merchant.status != 0)。
解决:检查MerchantController.login()或通用登录 Service,增加状态校验:
Merchant merchant = merchantMapper.findByAccount(account); if (merchant == null || merchant.getStatus() != 1) { // status=1 才允许登录 return Result.fail("账号已被禁用"); }5. 系统测试实战:用 Postman + JUnit4 覆盖核心接口与边界场景
5.1 功能测试:Postman 模拟真实用户行为链路
课设测试不必追求自动化,用 Postman 手动覆盖关键路径即可。以下为必测接口清单(URL 前缀均为http://localhost:8080/shopping):
| 测试场景 | 请求方法 | URL | 参数(x-www-form-urlencoded) | 预期响应 |
|---|---|---|---|---|
| 用户注册 | POST | /user/register | username=testuser,password=123456,email=test@demo.com | 返回register_success.html,数据库user表新增记录 |
| 用户登录 | POST | /user/login | username=testuser,password=123456 | Session 中存入user,跳转/user/index |
| 商品列表 | GET | /user/product/list | 无 | 返回user/product_list.html,页面含商品卡片 |
| 加入购物车 | POST | /user/cart/add | productId=1,quantity=2 | cart_countSession 属性更新,页面提示“已加入购物车” |
| 提交订单 | POST | /user/submitOrder | productId=1,quantity=1 | 数据库order_info和order_item新增记录,product.stock减 1 |
操作提示:在 Postman 中设置
Cookies自动管理,确保登录态在后续请求中透传;对每个 POST 请求,务必在Body选项卡中选择x-www-form-urlencoded并填入参数,而非raw。
5.2 边界测试:JUnit4 编写库存扣减并发测试
虽然课设不要求压测,但用 JUnit4 写一个 10 线程并发扣减同一商品的测试,能直观验证防超卖逻辑是否生效:
// OrderServiceTest.java @RunWith(SpringRunner.class) @SpringBootTest public class OrderServiceTest { @Autowired private OrderService orderService; @Autowired private ProductMapper productMapper; @Test public void testConcurrentOrderCreation() throws InterruptedException { Long productId = 1L; // 假设商品ID为1,初始库存为10 int threadCount = 10; CountDownLatch latch = new CountDownLatch(threadCount); AtomicInteger successCount = new AtomicInteger(0); // 启动10个线程同时下单 for (int i = 0; i < threadCount; i++) { new Thread(() -> { try { OrderResult result = orderService.createOrder(1L, productId, 1); if (result.isSuccess()) { successCount.incrementAndGet(); } } finally { latch.countDown(); } }).start(); } latch.await(); // 等待所有线程结束 // 验证:成功订单数 ≤ 初始库存(10) Product finalProduct = productMapper.selectById(productId); System.out.println("最终库存:" + finalProduct.getStock()); System.out.println("成功下单数:" + successCount.get()); assertTrue("成功下单数不能超过初始库存", successCount.get() <= 10); assertTrue("最终库存不能为负", finalProduct.getStock() >= 0); } }逻辑说明与参数说明:
CountDownLatch确保主线程等待所有子线程执行完毕,避免测试提前结束;AtomicInteger线程安全计数,统计成功订单数;- 断言
successCount.get() <= 10是核心验证点,若输出成功下单数:11,说明防超卖失效,需回查decreaseStockSQL 和synchronized锁;- 此测试在本地 IDE 中右键 Run 即可执行,无需额外环境。
5.3 安全测试:手动验证三个越权访问场景
课设安全常被忽视,但只需三步手动检查即可:
- 水平越权:用用户 A 登录,修改 URL 中订单 ID 为用户 B 的订单(如
/user/order/detail?id=1002),应返回“无权访问”或 404; - 垂直越权:用普通用户账号,尝试访问
/admin/user/list,应被重定向到登录页或返回 403; - 敏感信息泄露:在浏览器开发者工具 Network 标签中,查看任意接口响应,确认
password、id_card等字段未出现在 JSON 或 HTML 中(如User实体类应使用@JsonIgnore注解屏蔽密码字段)。
操作提示:检查
User.java实体类,确保password字段有@JsonIgnore:public class User { private Long id; private String username; @JsonIgnore // 关键!防止序列化时暴露密码 private String password; // ... }
6. 从那以后我每次做课设,都强制走一遍“三查一跑”清单
做完这个项目,我养成了一个雷打不动的习惯:无论多急,上线前必须完成“三查一跑”。
一查数据库:用 MySQL Workbench 连上去,执行SELECT * FROM product WHERE id = 1;,确认stock字段初始值是你预期的(比如 100),而不是 0 或 NULL——课设里 30% 的“下单失败”源于建表时DEFAULT值写错或插入脚本漏执行。
二查配置文件:打开application.yml,逐行核对spring.datasource.url的端口(3306)、数据库名(shopping_db)、用户名密码——有一次我卡了两天,就因为密码里有个$符号没加单引号,YAML 解析器把它当作了变量。
三查前端路径:打开login.html,检查所有<form action="...">和<a href="...">的路径,确认都带了context-path前缀(如/shopping/login),而不是裸路径/login——这是课设答辩时老师现场点击 404 的最高发场景。
一跑:不是跑mvn spring-boot:run就完事,而是打开浏览器,手动走一遍“注册→登录→浏览商品→加购→下单→查看订单”全链路,每个环节截图存档。截图里必须包含地址栏 URL 和页面关键元素(如“欢迎,testuser”、“订单号:20240520143022123456”),这是你答辩时最硬的凭证。
这套流程看起来琐碎,但它把“我以为好了”变成了“我亲眼看见它好了”。课设不是交一份代码,而是交一份可验证的行为证据。希望帮到你。
本文还有配套的精品资源,点击获取