☰
Spring Boot购物商城系统实战:从课程设计到可运行项目
2026/10/23 3:25:57 网站建设 项目流程

简介:这份资源是面向计算机及相关专业本科生、研究生的MySQL网上购物系统课程设计文档,基于Spring Boot框架与Java技术,采用B/S结构和MySQL数据库,完整呈现从需求分析、系统设计到实现与测试的全过程。系统划分用户、商家、管理员三大模块,涵盖注册登录、商品浏览购买、商品信息管理及用户商家管理等核心功能,并给出E-R图与数据库表结构设计,测试部分从功能、性能、安全三方面展开,适合作为课程设计参考或电商系统入门案例。资源包共1个docx文件,约3.8MB,内容为结构完整的课程设计论文,含中英文摘要、目录、技术选型、系统分析、详细设计与测试总结等章节,便于直接借鉴写作框架与实现思路。目前已有76人学习,对需要完成同类课设或理解Spring Boot项目落地流程的读者具有较高参考价值。

1. 从一份课程设计到能跑起来的购物商城:这个项目到底能干什么

如果你手里正拿着一份“基于 Spring Boot 的网上购物商城系统”课程设计文档,翻到目录看到“需求分析、E-R 图、顺序图、测试方案”这些字眼,大概率会有点懵——文档写得挺全,但代码从哪跑、数据库怎么建、三个角色怎么切,文档里往往一笔带过。这份资源解决的正是这个断层:它把一套完整的网上购物商城系统从需求到实现串了起来,用 Java + Spring Boot + MySQL 落地,B/S 结构,浏览器打开就能用。

系统里分了三个角色:用户能注册登录、浏览商品、加购物车、下单支付、管理收货地址和收藏;商家能上架商品、维护库存和订单;管理员管用户、管商家、管分类、管订单。说白了,这就是一个缩小版的电商后台,麻雀虽小,但购物车、订单状态流转、多角色权限这些电商核心链路一个不少。适合正在做课程设计的学生,也适合想拿一个完整 Spring Boot 项目练手二次开发的人。

2. 技术选型拆解:为什么是 Spring Boot + MySQL + B/S

2.1 后端为什么选 Spring Boot 而不是裸 Spring

课程设计里用 Spring Boot,最直接的好处是省配置。裸 Spring 要写一堆 XML 或者 JavaConfig 来配 DispatcherServlet、数据源、事务管理器,而 Spring Boot 的自动配置把这些都包了。项目里常见的做法是:pom.xml引入spring-boot-starter-web和spring-boot-starter-data-jpa,然后写一个application.yml配数据库连接,剩下的交给 starter。

# application.yml 核心配置 spring: datasource: url: jdbc:mysql://localhost:3306/shopping_mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update # 开发阶段自动建表,上线前改成 none show-sql: true # 控制台打印 SQL,方便调试

这里ddl-auto: update是开发期的后悔药——实体类改了字段,重启后表结构跟着变,不用手动 ALTER。但上线前一定要改成none或validate,否则哪天实体改错一个字段,生产表就被改了。serverTimezone不配的话,MySQL 8 驱动会报时区错误,这是新手最容易翻车的地方。

2.2 数据层用 JPA 还是 MyBatis

文档里提到了 Spring Data JPA,说它“是一套规范,在没有实现类的接口时候是没有办法使用的”。这句话翻译过来就是:你定义一个UserRepository extends JpaRepository<User, Long>,不用写实现,Spring 自动给你生成增删改查。对于课程设计这种 CRUD 密集、复杂查询不多的场景,JPA 比 MyBatis 省事得多。

// 商品数据访问接口,继承 JpaRepository 即拥有基础 CRUD public interface ProductRepository extends JpaRepository<Product, Long> { // 按分类查商品,方法名即查询语义 List<Product> findByCategoryId(Long categoryId); // 按商品名模糊搜索 List<Product> findByProductNameContaining(String keyword); }

findByCategoryId和findByProductNameContaining这两个方法名不是随便起的,Spring Data JPA 会解析方法名生成对应 SQL。Containing对应LIKE %keyword%,findBy后面跟实体字段名。如果你写findByCategory_id也能跑,但驼峰命名更规范。复杂查询比如“查某用户最近三个月的订单并按金额排序”,方法名会变得很长,这时候建议用@Query注解写 JPQL,别硬拼方法名。

2.3 B/S 结构带来的部署便利

B/S 结构意味着用户端只需要浏览器,不用装客户端。项目里前端页面通常放在src/main/resources/static或templates下,Spring Boot 内嵌 Tomcat,打成 jar 包后java -jar就能跑。这对课程设计答辩特别友好——老师电脑上只要有 Java 环境和 MySQL,拷过去就能演示,不用配 Tomcat。

三层结构在这个项目里的映射是:浏览器(表示层)→ Controller/Service(业务逻辑层)→ Repository/MySQL(数据层)。Controller 接收请求参数,Service 做业务判断(比如库存够不够、订单状态能不能取消),Repository 只管存取。常见的新手错误是把业务逻辑全写在 Controller 里,导致后面加一个“下单扣库存”要改好几个地方。

3. 数据库设计与核心表结构:E-R 图落到建表语句

3.1 从 E-R 图到实际表:五个核心实体

文档里的 E-R 图给了商品分类、商家、订单、管理员、商品五个实体。落到 MySQL 里,我一般会建这几张表:user(用户)、merchant(商家)、admin(管理员)、product(商品)、category(分类)、orders(订单)、order_item(订单明细)、cart(购物车)、address(收货地址)。

订单和商品是多对多关系,所以拆出order_item中间表。购物车也是用户和商品的多对多,单独一张cart表。这里有个设计决策:订单表里要不要冗余商品名称和图片?文档的订单实体图里画了“商品名称、商品图片”,说明设计者选择了冗余。这样做的好处是订单详情页不用联表查商品,坏处是商品改名后历史订单显示的是旧名。电商场景下通常接受这个冗余,因为订单是历史快照。

-- 商品表核心字段 CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_name VARCHAR(200) NOT NULL COMMENT '商品名称', category_id BIGINT NOT NULL COMMENT '分类ID', merchant_id BIGINT NOT NULL COMMENT '商家ID', price DECIMAL(10,2) NOT NULL COMMENT '售价', stock INT DEFAULT 0 COMMENT '库存', image_url VARCHAR(500) COMMENT '主图地址', detail TEXT COMMENT '商品详情', status TINYINT DEFAULT 1 COMMENT '1上架 0下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category_id), INDEX idx_merchant (merchant_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

price用DECIMAL(10,2)而不是FLOAT,因为浮点数算钱会出现0.1 + 0.2 = 0.30000000000000004这种玄学问题。utf8mb4而不是utf8,因为要支持 emoji 和生僻字。idx_category和idx_merchant两个索引是必须的,商品列表按分类筛选、商家后台按商家查商品,没索引的话数据量一上来就慢。

3.2 订单状态流转:一个容易被忽略的设计点

订单表里有个status字段,文档里没展开说状态怎么流转,但这是电商系统的核心。常见做法是:0 待付款、1 已付款、2 已发货、3 已完成、4 已取消。用户下单后是 0,支付回调后变 1,商家发货变 2,用户确认收货变 3。取消订单只能从 0 或 1 取消,已发货的不能直接取消。

// 订单状态枚举,避免魔法数字 public enum OrderStatus { UNPAID(0, "待付款"), PAID(1, "已付款"), SHIPPED(2, "已发货"), COMPLETED(3, "已完成"), CANCELLED(4, "已取消"); private final int code; private final String desc; // 构造方法和 getter 省略 }

用枚举而不是直接写 0、1、2,是因为半年后你回头看代码,看到status == 2得翻文档才知道是已发货,而OrderStatus.SHIPPED一眼就懂。状态流转的校验放在 Service 层,比如取消订单时先查当前状态,只有 UNPAID 和 PAID 允许取消,否则抛业务异常。

3.3 购物车表的设计取舍

购物车有两种存法:存数据库或者存 Session。存 Session 的优点是快,缺点是换浏览器就没了;存数据库的优点是持久化,缺点是每次操作都要读写库。课程设计项目我一般建议存数据库,因为要演示“加入购物车后关闭浏览器再打开还在”这个功能。

CREATE TABLE cart ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_product (user_id, product_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

uk_user_product这个唯一索引是关键:同一个用户对同一个商品只能有一条购物车记录,再次加入时应该UPDATE quantity = quantity + 1而不是INSERT新行。没有这个唯一约束的话,用户狂点“加入购物车”就会产生一堆重复记录,结算时数量就乱了。

4. 核心功能实现:从登录鉴权到下单扣库存

4.1 登录鉴权:Session 还是 JWT

课程设计项目里,登录鉴权常见两种做法。一种是 Session,登录成功后把用户信息存HttpSession,后续请求从 Session 取。另一种是 JWT,登录成功后发一个 token,前端每次请求带在 Header 里。B/S 结构下如果前端是纯 HTML + 模板引擎,Session 更简单;如果前后端分离,JWT 更合适。

// 基于 Session 的登录拦截器 @Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); Object user = session.getAttribute("loginUser"); if (user == null) { // 未登录,重定向到登录页 response.sendRedirect("/login"); return false; } return true; } }

拦截器注册在WebMvcConfig里,addPathPatterns("/**")拦截所有请求,excludePathPatterns放行登录页、注册页、静态资源。这里有个坑:静态资源(css、js、图片)如果不放行,登录页会样式全丢,因为浏览器请求 css 时也被拦截了。

4.2 商品列表与分页:别一次性查全表

商品列表页如果直接findAll(),数据量大了内存扛不住。Spring Data JPA 提供了Pageable分页。

// 分页查询商品,按创建时间倒序 public Page<Product> getProductPage(int page, int size, Long categoryId) { Pageable pageable = PageRequest.of(page, size, Sort.by("createTime").descending()); if (categoryId != null) { return productRepository.findByCategoryId(categoryId, pageable); } return productRepository.findAll(pageable); }

PageRequest.of(page, size)里 page 从 0 开始,不是 1。前端传页码时要注意转换,否则第一页会跳过。Sort.by("createTime").descending()让新商品排在前面。返回的Page对象里带了总页数、总记录数,前端直接拿来渲染分页条。

4.3 下单流程:库存扣减的并发问题

下单是整个系统最核心也最容易出问题的环节。用户点击“提交订单”后,后端要做几件事:校验库存、扣减库存、生成订单、生成订单明细、清空购物车。这五步必须在一个事务里,否则扣了库存没生成订单,或者生成了订单没清购物车,数据就乱了。

@Service @Transactional // 类级别事务,方法内所有数据库操作要么全成功要么全回滚 public class OrderService { public Order createOrder(Long userId, List<CartItem> items) { // 1. 校验库存 for (CartItem item : items) { Product product = productRepository.findById(item.getProductId()) .orElseThrow(() -> new BusinessException("商品不存在")); if (product.getStock() < item.getQuantity()) { throw new BusinessException("库存不足:" + product.getProductName()); } } // 2. 扣减库存(这里用乐观锁或直接 UPDATE 带条件) for (CartItem item : items) { int affected = productRepository.reduceStock( item.getProductId(), item.getQuantity()); if (affected == 0) { throw new BusinessException("库存扣减失败,请重试"); } } // 3. 生成订单和明细,4. 清空购物车... } }

@Transactional加在 Service 类上,方法内任何一步抛异常,前面的操作全部回滚。库存扣减用UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?,返回影响行数,如果是 0 说明库存不够,抛异常触发回滚。这种写法比“先查再改”安全,因为查和改之间可能有其他请求插进来。

注意:@Transactional默认只对RuntimeException回滚,如果抛的是Exception需要加rollbackFor = Exception.class。另外,同类内部方法调用不会触发事务,因为 Spring AOP 代理的是外部调用。

4.4 商家后台的商品管理

商家登录后能看到自己的商品列表,可以新增、编辑、上下架。这里的关键是数据隔离:商家 A 不能看到商家 B 的商品。做法是在查询时强制带上merchantId条件。

// 商家商品列表,只查当前商家的 public Page<Product> getMerchantProducts(Long merchantId, int page, int size) { Pageable pageable = PageRequest.of(page, size, Sort.by("createTime").descending()); return productRepository.findByMerchantId(merchantId, pageable); }

merchantId从 Session 里取,不能从请求参数取。如果从参数取,商家 A 改一下 URL 里的 merchantId 就能看到商家 B 的数据,这是越权漏洞。同理,编辑商品时要校验product.getMerchantId().equals(currentMerchantId),不匹配就拒绝。

5. 避坑与常见问题排查

5.1 启动报错:时区问题导致连接失败

现象:项目启动时控制台报The server time zone value '?D1ú±ê×?ê±??' is unrecognized。原因:MySQL 8 的驱动需要明确指定时区,不指定就取系统默认,中文系统下会乱码。解决:JDBC URL 里加serverTimezone=Asia/Shanghai,或者用serverTimezone=GMT%2B8。

5.2 页面 404:静态资源被拦截器拦了

现象:登录页能打开,但样式全丢,F12 看 css 请求返回 302 跳转到登录页。原因:拦截器addPathPatterns("/**")把/css/**、/js/**、/images/**也拦了。解决:在excludePathPatterns里放行这些路径,或者把静态资源放到static目录下并配置spring.mvc.static-path-pattern。

5.3 订单金额算错:浮点数精度问题

现象:购物车总价显示99.99999999而不是100.00。原因:用double或float做金额运算。解决:金额字段用BigDecimal,数据库用DECIMAL。BigDecimal做加法用add(),比较用compareTo()而不是equals(),因为equals()会比较精度。

5.4 中文乱码:数据库字符集不统一

现象:商品名称存进去是中文,查出来变成???。原因:数据库、表、连接三处字符集不一致。解决:建库时CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,JDBC URL 加characterEncoding=utf8,application.yml里配spring.http.encoding.charset=utf-8。三处都对齐才行。

5.5 事务不回滚:异常被 catch 了

现象:下单时库存扣了但订单没生成,数据不一致。原因:Service 方法里try-catch把异常吞了,Spring 感知不到异常就不回滚。解决:要么不 catch,让异常抛出去;要么 catch 后重新抛一个RuntimeException;要么手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。

6. 二次开发与改造思路:从课程设计到能用的项目

拿到这个项目后,如果只是跑通答辩,那按文档配好数据库、改一下application.yml里的密码就能启动。但如果你想拿它当二次开发的底子,有几个方向值得动手。

第一个方向是加 Redis 缓存商品详情。现在每次打开商品页都查数据库,热门商品被反复查,数据库压力大。改造思路是在ProductService里注入RedisTemplate,查商品时先查 Redis,没有再去数据库并写回缓存。注意缓存更新策略:商品编辑后要删缓存而不是更新缓存,避免并发下脏数据。

public Product getProductById(Long id) { String key = "product:" + id; Product cached = (Product) redisTemplate.opsForValue().get(key); if (cached != null) return cached; Product product = productRepository.findById(id).orElse(null); if (product != null) { redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES); } return product; }

第二个方向是把 Session 鉴权换成 JWT。现在 Session 存在单机内存里,如果部署多台服务器,用户在第一台登录,请求打到第二台就不认了。JWT 把用户信息编码在 token 里,服务端不用存,天然支持多机。改造时写一个JwtUtil生成和解析 token,拦截器里从 Header 取 token 验证。

第三个方向是加一个简单的推荐位。不用上机器学习,就按“同分类销量前五”查一批商品放在详情页底部。productRepository.findTop5ByCategoryIdOrderBySalesDesc(categoryId)一行搞定。这个改动小、见效快,答辩时演示效果也好。

验证改造是否成功,我一般会走一遍完整链路:注册新用户 → 浏览商品 → 加入购物车 → 下单 → 商家后台看到订单 → 管理员后台看到订单统计。每一步都确认数据落库正确、页面跳转正常。特别是下单后库存有没有减、购物车有没有清、订单状态对不对,这三个点最容易出问题。

从那以后我每次改完订单相关的代码,都强制走一遍“下单-支付-取消”的完整流程,确认事务边界和状态流转没问题才提交。希望这些能帮到你,少踩几个坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询