Spring Boot 3网上购物系统课程设计实战
简介本资源是一套基于SSMSpringSpring MVCMyBatis框架开发的Java Web课程设计项目——网上购物系统面向高校计算机专业学生及Java初学者旨在通过完整电商场景实践系统掌握Web开发全流程核心技能。压缩包共800个文件涵盖49个Java后端逻辑类、25个JSP动态页面、29个HTML前端入口、251个JS交互脚本、124个JPG与111个PNG商品/界面图、52个CSS样式文件以及XML配置、MD说明文档等总大小10.98MB结构清晰模块划分明确含用户、商品、订单、支付等完整业务域。已有1808人学习下载资源附带可直接运行的SSM整合源码、数据库建表SQL、基础安全配置含登录验证示意及典型RESTful接口实现学习者可快速部署调试、理解MVC分层协作机制并通过代码注释与目录组织反向梳理企业级Java Web项目开发范式。1. Web Java 框架课程设计-网上购物系统不是写个登录页就叫“系统”它得能扛住学生小组答辩老师现场点单数据库崩三次还不丢订单这门课设标题里藏着三个硬门槛Web不是纯控制台、Java 框架拒绝手写 ServletJDBC 硬刚、网上购物系统不是 CRUD 练习册得有用户、商品、购物车、下单、支付模拟、订单状态流转。我带过六届课程设计最常翻车的不是代码写不出来而是——学生用 Spring Boot 写完首页老师一问“用户加购后关浏览器再打开购物车还在吗”当场哑火或者“订单生成后怎么防止重复提交库存扣减怎么不超卖”答案是“我……没做”。这不是功能堆砌问题是工程闭环意识缺失从 HTTP 请求进来到数据库落盘中间每层Controller → Service → Mapper → DB谁该管什么、怎么兜底、怎么验证必须显式设计。本篇不讲 MVC 是啥只带你用主流技术栈Spring Boot 3.x MyBatis-Plus Thymeleaf H2/MySQL搭出一个可演示、可调试、可解释每行代码责任的最小可行系统。适合大三下刚学完 Java Web 基础、正被“框架太多不知从哪下手”卡住的同学也适合想快速验证教学案例落地性的某高校实验课导师。2. 用 Spring Boot 3.2 MyBatis-Plus 快速初始化项目骨架跳过 17 个 starter 选型纠结直接跑通首页和用户登录2.1 用官方脚手架生成基础结构但必须砍掉 3 类默认依赖别用 IDE 内置的 Spring Initializr 点点点——它默认塞进spring-boot-starter-webflux你根本用不上响应式、spring-boot-starter-validation表单校验先放后面、lombok新手容易被注解黑盒搞懵。老老实实去 start.spring.io 注意选Spring Boot 3.2.xJava 17只勾选Spring Web核心 MVCSpring Data JDBC轻量级数据访问比 JPA 更透明Thymeleaf服务端模板比 Vue/React 学习成本低答辩时老师能直接看 HTMLH2 Database开发阶段内存库免装 MySQL重启即清空防脏数据干扰生成 ZIP 后解压删掉pom.xml里所有spring-boot-starter-validation和spring-boot-starter-webflux相关dependency块。这是血泪经验Validation 的NotBlank注解在 Thymeleaf 表单回显时会触发未预期的绑定错误新手调试三天找不到原因。2.2 替换 MyBatis-Plus 并配置多环境数据源MyBatis-Plus 比原生 MyBatis 少写 80% XML且自带分页、逻辑删除等课程设刚需功能。在pom.xml中添加dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version4.1.0/version /dependency提示版本必须匹配 Spring Boot 3.x用 3.x 版本的 MP 会报Invalid bound statement (not found)因为 Mapper 接口扫描机制变了。然后在src/main/resources/application.yml里配数据源spring: datasource: url: jdbc:h2:mem:shopdb;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE driver-class-name: org.h2.Driver username: sa password: h2: console: enabled: true # 开启 H2 控制台地址 http://localhost:8080/h2-console mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印 SQL调试神器 global-config: db-config: id-type: assign_id # 主键用雪花算法避免自增 ID 在演示时暴露数量2.3 写第一个 Controller让首页和登录页真正“活”起来创建com.example.shop.controller.HomeController.javaController public class HomeController { GetMapping(/) public String index(Model model) { model.addAttribute(welcomeMsg, 欢迎来到网上购物系统); return index; // 对应 src/main/resources/templates/index.html } GetMapping(/login) public String loginPage() { return login; } PostMapping(/login) public String doLogin( RequestParam String username, RequestParam String password, HttpSession session, RedirectAttributes redirectAttributes) { // 模拟登录验证真实项目应查 DB if (admin.equals(username) 123456.equals(password)) { session.setAttribute(user, username); return redirect:/dashboard; } else { redirectAttributes.addFlashAttribute(error, 用户名或密码错误); return redirect:/login; } } }关键点说明Controller而非RestController因为要返回 Thymeleaf 模板HTML不是 JSONRedirectAttributes用于跨重定向传参如登录失败提示addFlashAttribute数据只在下次请求有效避免刷新页面重复显示错误session.setAttribute(user, ...)是课程设最简用户态管理比 JWT 简单十倍答辩时老师问“怎么保持登录”你指这行代码就行return redirect:/dashboard中的redirect:前缀是 Spring Boot 内置重定向语法不是字符串拼接。启动应用访问http://localhost:8080/应看到空白首页http://localhost:8080/login显示登录表单。此时你已越过“项目跑不起来”第一道坎——接下来所有功能都基于这个骨架延展。3. 用户与商品模块用 MyBatis-Plus 自动生成 CRUD但必须手动补全 3 处业务逻辑断点3.1 用代码生成器一键产出 User 和 Product 的 Entity/Mapper/ServiceMyBatis-Plus 自带代码生成器比手敲快 10 倍且零出错。新建CodeGenerator.java放在 test 目录下public class CodeGenerator { public static void main(String[] args) { AutoGenerator generator new AutoGenerator(); GlobalConfig gc new GlobalConfig(); gc.setOutputDir(System.getProperty(user.dir) /src/main/java); // 输出到 main/java gc.setAuthor(student); gc.setOpen(false); // 生成后不自动打开文件夹 gc.setSwagger2(true); // 启用 Swagger 注释答辩时可展示接口文档 generator.setGlobalConfig(gc); DataSourceConfig dsc new DataSourceConfig(); dsc.setUrl(jdbc:h2:mem:shopdb); dsc.setDriverName(org.h2.Driver); dsc.setUsername(sa); dsc.setPassword(); generator.setDataSource(dsc); PackageConfig pc new PackageConfig(); pc.setModuleName(shop); pc.setParent(com.example); generator.setPackageInfo(pc); StrategyConfig strategy new StrategyConfig(); strategy.setInclude(user, product, order, order_item); // 指定生成哪些表 strategy.setNaming(NamingStrategy.underline_to_camel); // 下划线转驼峰 strategy.setColumnNaming(NamingStrategy.underline_to_camel); generator.setStrategy(strategy); generator.execute(); } }运行main()方法自动生成User.java、Product.java、UserMapper.java、ProductMapper.java等。但注意生成器只管结构不管业务。下面三处必须手动补3.2 手动修正 User 实体密码不能明文存储加盐哈希是硬性要求生成的User.java中password字段是String类型但课程设必须体现安全意识。修改为TableField(fill FieldFill.INSERT) // 插入时自动填充 private String password; // 新增 salt 字段用于密码加盐 TableField(fill FieldFill.INSERT) private String salt;并在UserMapper.java的insert方法调用前注入密码加密逻辑在 Service 层Service public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService { Override public boolean save(User entity) { // 生成随机 salt String salt UUID.randomUUID().toString().replace(-, ).substring(0, 8); // 使用 BCrypt 加密比 MD5/SHA256 更安全且自带盐 String encryptedPwd new BCryptPasswordEncoder().encode(entity.getPassword() salt); entity.setPassword(encryptedPwd); entity.setSalt(salt); return super.save(entity); } }注意BCryptPasswordEncoder需添加依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency即使不用 Spring Security 全套它的密码编码器也是业界标准。3.3 商品列表分页用 MyBatis-Plus 分页插件但必须关掉 Total 查询课程设演示时商品数往往 100但老师可能问“如果商品上万select count(*)不会拖慢首页”——这就是考你是否理解分页优化。在application.yml中配置mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl pagination: overflow: true # 允许查询超出最大页数 as-count: false # 关键禁用自动 count 查询改用 limit offset然后在 Controller 中GetMapping(/products) public String productList( RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, Model model) { PageProduct productPage new Page(page, size); // 不调用 page.getTotal()避免触发 count 查询 PageProduct result productService.page(productPage); model.addAttribute(products, result.getRecords()); model.addAttribute(currentPage, page); model.addAttribute(totalPages, (int) Math.ceil((double) result.getTotal() / size)); return product/list; }这样首页加载只执行SELECT * FROM product LIMIT 10 OFFSET 0无 count 压力。答辩时老师追问你就答“生产环境用游标分页或 ES课程设用as-count: false是平衡性能与功能的合理取舍”。3.4 商品详情页用Select注解写关联查询拒绝 N1 问题商品详情需显示分类名category_name但生成器只建了product表没建category表。手动在ProductMapper.java中加Select(SELECT p.*, c.name as category_name FROM product p LEFT JOIN category c ON p.category_id c.id WHERE p.id #{id}) Results({ Result(property categoryName, column category_name), Result(property id, column id), Result(property name, column name) }) Product selectWithCategory(Param(id) Long id);关键避坑不用One嵌套查询否则查 1 个商品会触发 1 次SELECT * FROM category查 10 个商品就是 10 次——这就是经典的 N1 问题。用单 SQL JOIN 是课程设最稳方案。4. 购物车与订单状态机驱动的流程设计绕开“库存超卖”和“重复下单”两大天坑4.1 购物车用 Session 存储但必须支持跨设备同步课程设简化版很多同学用HttpSession存购物车但老师一问“手机扫码登录后购物车还在吗”答不上来。课程设不实现分布式 Session但要做个可扩展伏笔Component public class CartService { // 优先从 Session 取Session 无则查 DB为后续迁移到 Redis 预留接口 public ListCartItem getCart(HttpSession session) { Object cartObj session.getAttribute(cart); if (cartObj ! null) { return (ListCartItem) cartObj; } // 此处可加User user getCurrentUser(session); return cartMapper.selectByUserId(user.getId()); return new ArrayList(); } public void addToCart(HttpSession session, Long productId, Integer quantity) { ListCartItem cart getCart(session); // ... 业务逻辑 session.setAttribute(cart, cart); // 当前仅存 Session } }这样写答辩时老师问“怎么支持多端”你指// 此处可加那行注释“未来把查 DB 的逻辑放开购物车就自动持久化了”。4.2 下单接口用数据库行锁 事务隔离死守“库存不超卖”底线这是课程设最高危环节。错误写法// ❌ 危险先查库存再扣减中间可能被并发请求穿透 Integer stock productMapper.selectStockById(productId); if (stock quantity) { productMapper.updateStock(productId, stock - quantity); // 可能负数 }正确做法MyBatis-Plus 乐观锁 数据库行锁在Product.java加版本字段TableField(fill FieldFill.INSERT) private Integer version; // 乐观锁版本号在application.yml开启乐观锁mybatis-plus: global-config: db-config: version-field: version optimistic-locker: true下单 Service 方法Transactional(rollbackFor Exception.class) public Order createOrder(Long userId, ListCartItem items) { Order order new Order(); order.setUserId(userId); order.setStatus(CREATED); order.setCreateTime(LocalDateTime.now()); orderService.save(order); // 先保存订单获取 order.id for (CartItem item : items) { // 关键用 WHERE version ? AND stock ? 一次更新失败则重试 int updated productMapper.decreaseStock( item.getProductId(), item.getQuantity(), item.getVersion() ); if (updated 0) { throw new RuntimeException(库存不足请刷新重试); } } return order; }对应ProductMapper.xmlupdate iddecreaseStock UPDATE product SET stock stock - #{quantity}, version version 1 WHERE id #{id} AND version #{version} AND stock #{quantity} /update这里用的是数据库层面的原子操作UPDATE ... WHERE stock ?保证扣减不超限AND version ?保证乐观锁生效。比 Java 层 synchronized 或 Redis 锁更可靠且课程设无需额外组件。4.3 订单状态流转用枚举定义状态禁止字符串硬编码订单状态CREATED → PAID → SHIPPED → COMPLETED必须可追溯、可枚举。建OrderStatus.javapublic enum OrderStatus { CREATED(待支付, 10), PAID(已支付, 20), SHIPPED(已发货, 30), COMPLETED(已完成, 40), CANCELLED(已取消, 0); private final String desc; private final int sort; OrderStatus(String desc, int sort) { this.desc desc; this.sort sort; } // getter... }在Order.java中TableField(value status) private OrderStatus status; // 类型是枚举不是 String这样在 Thymeleaf 模板中可直接用span th:text${order.status.desc}待支付/span答辩时老师问“怎么防止非法状态跳转”你答“枚举类型强制约束数据库字段用 tinyint 存 sort 值Service 层用 switch-case 校验流转规则比如不能从 CREATED 直接到 COMPLETED”。5. 避坑指南课程设计答辩前必测的 5 个致命场景90% 的小组栽在第 3 条5.1 现象点击“立即购买”跳转到空白页控制台无报错Network 面板显示 404原因Thymeleaf 模板路径写错。return product/detail期望找src/main/resources/templates/product/detail.html但你建成了src/main/resources/templates/productDetail.html少斜杠或detail.htm后缀错。解决检查application.yml中 Thymeleaf 配置spring: thymeleaf: prefix: classpath:/templates/ suffix: .html # 必须是 .html不是 .htm 或 .jsp cache: false # 开发时关缓存改 HTML 不用重启然后严格按controller 返回值 templates/目录/文件名对齐路径。5.2 现象登录成功后访问/dashboard报 403 Forbidden原因Spring Boot 3.x 默认启用 CSRF 保护而你的登录表单没带_csrftoken。解决在login.html表单内加form th:action{/login} methodpost input typehidden th:name${_csrf.parameterName} th:value${_csrf.token} / !-- 其他字段 -- /form提示_csrf对象由 Spring Security 自动注入即使你没配 Security只要引入了 starter 就生效。这是 Spring Boot 3.x 的默认行为不处理就会 403。5.3 现象同一商品两个用户同时下单库存扣成负数超卖原因没用数据库行锁或乐观锁而是先SELECT stock再UPDATE stock stock - 1中间存在时间窗口。解决必须用 4.2 节的UPDATE ... WHERE stock ?原子语句。额外验证在 H2 控制台执行SELECT * FROM product手动把 stock 改为 1然后用 JMeter 模拟 100 并发下单观察是否出现负库存。这是答辩前唯一可信的压测方式。5.4 现象Thymeleaf 页面显示${user.username}而不是真实用户名原因Model 传值时 key 名不匹配。Controller 写model.addAttribute(user, user)但 HTML 里写th:text${currentUser.username}key 写错了。解决统一约定 Model key 命名用户对象一律用user商品列表用products订单用order。在HomeController的index()方法里加一行model.addAttribute(user, session.getAttribute(user)); // 即使未登录也传 null避免模板报错然后模板用th:if${user ! null}判断。5.5 现象H2 控制台能连但执行INSERT INTO user ...报Table user not found原因H2 默认关闭大小写敏感而user是 H2 内置函数名建表时需加双引号。解决在application.yml的 JDBC URL 末尾加;DATABASE_TO_UPPERfalsespring: datasource: url: jdbc:h2:mem:shopdb;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE;DATABASE_TO_UPPERfalse然后建表 SQL 改为CREATE TABLE user (id BIGINT PRIMARY KEY, username VARCHAR(50));这是 H2 的经典陷阱。用 MySQL 就没这问题但课程设用 H2 图省事就得认这个坑。6. 答辩加分技巧用 3 个可演示的“小而美”设计让老师记住你的名字6.1 订单号生成不用 UUID用“年月日6 位自增”格式体现业务思维老师喜欢问“订单号怎么生成为什么不用 UUID”——这是考你是否理解业务需求。UUID 难记、不可读、无法排序。课程设用简单方案Service public class OrderNoGenerator { private final AtomicInteger counter new AtomicInteger(0); public String generate() { String datePart LocalDate.now().format(DateTimeFormatter.ofPattern(yyyyMMdd)); int seq counter.incrementAndGet() % 1000000; // 6 位循环 return datePart String.format(%06d, seq); } }在OrderService中调用order.setOrderNo(orderNoGenerator.generate());演示时打开数据库指着20240520000001说“这是今天第一单按日期分片运维查日志时一眼定位”。老师会点头——这比背Snowflake算法实在。6.2 支付模拟页用 CSS 动画做“支付中”状态替代弹窗提示很多同学写个alert(支付成功)就交差。高阶做法是创建pay.html含一个按钮和状态条点击后 JS 发送支付请求接口返回{status: PROCESSING}前端用 CSSkeyframes做进度条流动效果3 秒后返回{status: SUCCESS}进度条变绿色并显示“支付成功”。div classprogress-container div classprogress-bar th:stylewidth: ${progress} %/div /divkeyframes flow { 0% { background-position: 0 0; } 100% { background-position: 100% 0; } } .progress-bar { animation: flow 3s linear infinite; background: linear-gradient(90deg, #4CAF50, #8BC34A, #4CAF50); }答辩时点一下进度条“活”起来老师会觉得你懂用户体验不止会写后端。6.3 日志埋点在关键方法加log.info()用 ELK 思维做简易追踪课程设不需要真上 ELK但要有意识。在OrderService.createOrder()开头加log.info(【ORDER】开始创建订单用户ID{}商品数{}, userId, items.size());在ProductMapper.decreaseStock()的 XML 中加update iddecreaseStock statementTypePREPARED bind namelogMsg value【STOCK】扣减商品 id 库存 quantity / UPDATE product SET ... !-- 无实际日志但命名体现意图 -- /update演示时打开控制台滚动日志指着【ORDER】行说“所有核心链路都有标记未来接入 SkyWalking 只需改日志框架”。这叫架构前瞻性不是炫技。最后说句实在话我当年做这个课设花 3 天搭骨架2 天调通购物车剩下 5 天全在改 Bug 和写答辩稿。最后悔的是没早用 H2 控制台——直到答辩前夜才发现user表名冲突重装 H2 驱动折腾两小时。所以把 H2 控制台地址http://localhost:8080/h2-console贴在桌面每次改完 SQL 先去那里跑一遍。希望帮到你。本文还有配套的精品资源点击获取