资讯详情

资讯详情

Spring Boot + MySQL购物商城:分层设计、数据库与下单事务

简介面向计算机及相关专业本科生与研究生的课程设计参考资料围绕基于 Spring Boot 框架、MySQL 数据库和 B/S 结构实现的网上购物商城系统展开适合正在完成电商类课程设计、希望理解 Java Web 项目完整落地流程的读者。压缩包内共 1 个 docx 文档约 3.8MB内容按需求分析、总体设计、数据库设计、功能实现与系统测试依次组织涵盖用户、商家、管理员三类角色的注册登录、商品浏览购买、商品信息维护及用户商家管理等模块并给出 E-R 图、数据表结构以及功能、性能、安全三方面的测试方案与结果分析。已有 76 人学习可作为选题参考、论文撰写和答辩准备的对照样本。文档完整呈现从用例分析到测试总结的写作脉络便于读者梳理模块边界、数据库关系与测试指标并据此形成自己的设计文档、排错思路和后续扩展方向。1. 从一次解压开始Spring Boot MySQL 购物商城到底能干什么控制台里Communications link failure那行红字大概是这类课程设计最常见的第一道坎。基于 Spring Boot MySQL 的网上购物商城系统本质是在 B/S 结构下把用户、商家、管理员三类角色的浏览、购物车、下单、发货、审核串起来。商品、订单、购物车、收藏这几张表跑通了后面再加推荐、加评论才有地基。它适合两类人正在赶课程设计、想直接拿一份能跑的系统改名字交作业的同学以及想用一个闭环完整的商城后端把 Spring Boot 自动配置、JPA 映射、拦截器鉴权和事务这几块走一遍的开发者。体量不夸张但登录、商品列表、下单这些链路都在正好够摸清一个电商后端骨架。2. 技术选型与分层B/S 结构下 Spring Boot 该放在哪一层2.1 先把 B/S 三层和 Spring Boot 的职责对上输入文档里明确写了 B/SBrowser/Server结构。落到代码上这三层对应关系是表现层由浏览器负责要么是 Thymeleaf 服务端渲染页要么是静态 HTML AJAX 调接口应用层是 Spring Boot 打包出的可执行 jar内嵌 Tomcat 直接扛请求数据层是 MySQL用JdbcTemplate或者 JPA 去连。相比 C/SB/S 最实际的收益不是「先进」而是一次部署、所有用户立刻更新。课程设计里没有客户端分发的包袱页面改完刷新浏览器就能看到这对反复调 UI 的周期非常友好。我一般把包结构按职责切开而不是按「用户模块 / 商家模块」切。因为同一个商品查询逻辑用户端和管理端都会调用按角色切包后期会出现大量重复代码。常见组织方式如下shopping-mall ├── pom.xml ├── src/main/java/com/example/shoppingmall │ ├── ShoppingMallApplication.java # 启动类SpringBootApplication │ ├── common │ │ ├── Result.java # 统一返回体 │ │ └── BizException.java # 业务异常 │ ├── config │ │ ├── WebConfig.java # 拦截器注册 │ │ └── GlobalExceptionHandler.java │ ├── controller # 按接口前缀分 /user /merchant /admin │ ├── entity # JPA Entity与表一一对应 │ ├── repository # 继承 JpaRepository │ ├── service │ └── service/impl ├── src/main/resources │ ├── application.yml │ ├── templates # Thymeleaf 页面 │ └── static # css/js/img └── sql └── shopping_mall.sql # 建库建表 初始化数据controller下再按/user、/merchant、/admin划子包而不是按业务模块切。拦截器按 URL 前缀做角色隔离时这种切法一眼就能看出哪些路径属于哪个角色。2.2 依赖和配置几个直接决定能不能启动的参数文档里写了「Spring Boot 是 Spring 自动配置的结合」也提到了 Spring Data JPA 作为 ORM 规范。按这个方向pom.xml至少要放这几块!-- Web内嵌 Tomcat Spring MVC -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- JPA实体映射和 Repository -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency !-- MySQL 驱动MySQL 8 用 com.mysql 这个坐标 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- 服务端渲染页面按需保留如果做前后端分离可以删掉 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency对应的application.yml里数据源和 JPA 的参数要成对看server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/shopping_mall?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: # 开发期用 update答辩前建议改成 none避免启动自动改表结构 ddl-auto: update show-sql: true properties: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect参数逐个说useUnicodetruecharacterEncodingutf8决定中文能不能从库里正常出来serverTimezoneAsia/Shanghai不加会在某些 MySQL 8 驱动下直接报The server time zone value is unrecognizedddl-autoupdate让 Hibernate 按实体自动补表字段开发期省事但答辩前一定切回none否则哪天字段名对不上启动时它会悄悄改表你反而看不出问题在哪。提示ddl-autoupdate只做加法不改不改删。实体删掉某个字段表里对应列会保留。答辩前用sql/shopping_mall.sql手动初始化比依赖 Hibernate 自动建表可控得多。2.3 三类角色的入口怎么分文档里管理员、商家、用户分别有各自的登录页和功能集E-R 图上也是三套独立实体。所以这里不搞「一张 user 表加 role 字段」而是三张表、三个登录入口各自查各自的。这样用户和商家字段可以放心差异商家要身份证编号、余额用户要收货地址管理员只关心用户名和角色编号。代价是登录接口得写三份校验但换来的是表结构清晰课程设计文档里也更好对应。3. 数据库设计从 E-R 图到商品、订单、用户这几张核心表3.1 五张核心表的关系先定下来文档 E-R 图给出了五个实体商品分类、商家、订单、管理员、商品。把它们转成表时订单里其实混了「订单头」和「订单明细」两层信息建议拆开orders存一单的总金额、下单人、地址、状态order_item存这单里每件商品的单价、数量、小计。这样查订单列表时只扫orders查详情再关联order_item不用大字段全塞一张表。关系上product.category_id→product_category.id多对一orders.user_id→user.id多对一order_item.order_id→orders.id一对多order_item.product_id→product.id多对一cart.user_id→user.idcart.product_id→product.id建库时先把字符集定死后面所有表继承CREATE DATABASE IF NOT EXISTS shopping_mall DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE shopping_mall;utf8mb4而不是utf8是因为 MySQL 里的utf8只支持三个字节商品名里出现 emoji 或生僻字会直接报错。这个点看起来小等页面提交商品描述突然报Incorrect string value时就知道后悔药没处买了。3.2 建表脚本字段类型和注释一起定用户表和商家表结构接近先看用户表CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT 加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, phone varchar(20) DEFAULT NULL, email varchar(100) DEFAULT NULL, address varchar(255) DEFAULT NULL COMMENT 默认收货地址, balance decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 余额, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;密码用varchar(100)是为了存 BCrypt 之类的哈希串不是明文。金额统一decimal(10,2)不用float或double——后面下单汇总金额时浮点误差会让你在订单详情页看到0.30000000000000004这种数字被答辩老师问一句「你这价格怎么算的」就很难接。商品表按文档 E-R 图里的字段来CREATE TABLE product ( id bigint NOT NULL AUTO_INCREMENT, merchant_id bigint NOT NULL COMMENT 所属商家, category_id bigint NOT NULL COMMENT 分类, name varchar(100) NOT NULL, image varchar(255) DEFAULT NULL, price decimal(10,2) NOT NULL DEFAULT 0.00, detail text COMMENT 商品详情, stock int NOT NULL DEFAULT 0 COMMENT 库存课程设计原文未列下单扣减用, sales int NOT NULL DEFAULT 0 COMMENT 月售销量, click_count int NOT NULL DEFAULT 0 COMMENT 点击次数, like_count int NOT NULL DEFAULT 0 COMMENT 赞数, dislike_count int NOT NULL DEFAULT 0 COMMENT 踩数, audit_status tinyint NOT NULL DEFAULT 0 COMMENT 0待审核 1通过 2驳回, audit_reply varchar(255) DEFAULT NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id), KEY idx_merchant (merchant_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表;注意这里有一处按场景补的字段stock。原文的 E-R 图里列了月售销量、点击、赞踩和审核状态但没列库存。一个能下单的商城不扣库存说不过去所以我在建表时补了stock下单时做扣减这也是后面第 4 章事务要保护的对象。订单表两个结构先看订单头CREATE TABLE orders ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 业务单号对外展示, user_id bigint NOT NULL, address varchar(255) NOT NULL COMMENT 下单时收货地址快照, total_price decimal(10,2) NOT NULL DEFAULT 0.00, status tinyint NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成 4已取消, pay_type tinyint DEFAULT NULL COMMENT 1余额 2其他, 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_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;地址存的是下单那一刻的快照不是外键指向address表。这是踩过坑之后改的用户改了默认地址历史订单的收货地址不能被跟着改否则对账时全是乱的。订单明细表CREATE TABLE order_item ( id bigint NOT NULL AUTO_INCREMENT, order_id bigint NOT NULL, product_id bigint NOT NULL, product_name varchar(100) NOT NULL COMMENT 下单时商品名快照, price decimal(10,2) NOT NULL COMMENT 下单时单价, quantity int NOT NULL, subtotal decimal(10,2) NOT NULL COMMENT 单价 × 数量, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细;3.3 文档里四个价格字段的取舍文档订单 E-R 图上列了「价格、折扣价格、折扣总价格、总价格」四个价格字段。落到实际表里我建议只保留price、total_price和明细里的subtotal单价、订单总额、明细小计。折扣不是这个系统当前阶段的重点强行留四个字段后面下单逻辑里会不断出现「这个字段到底存哪个」的争论反而容易填错。如果后续真想加优惠规范的做法不是加列而是新增coupon和order_coupon两张表订单表只留discount_amount一个汇总字段。课程设计里用不到就先不加留一个口子比留四个冗余列要干净。3.4 索引不是可选项product表的category_id、merchant_id要建普通索引。商品列表页按分类翻页是高频查询不建索引几百条数据时看不出差别数据量到几千后列表接口会明显变慢答辩演示时点一下就转圈观感很差。orders的user_id和status同理——我的订单列表和后台待发货列表是两个最高频的入口。4. 核心模块拆解登录鉴权、商品浏览和下单链路怎么走4.1 登录与拦截器三类角色怎么各走各的门文档用例图里明确画了管理员、用户、商家三套用例。用 Spring Boot 落地时最省心的做法不是写三套 Controller而是用一个登录入口加 role 路径变量再配一个按 URL 前缀切分的拦截器。先看 ControllerRestController RequestMapping(/api) public class LoginController { Autowired private AdminService adminService; Autowired private MerchantService merchantService; Autowired private UserService userService; // role 取 admin / merchant / user登录后把角色写进 session PostMapping(/login/{role}) public Result login(PathVariable String role, RequestBody LoginForm form, HttpSession session) { Object account; switch (role) { case admin: account adminService.login(form.getUsername(), form.getPassword()); break; case merchant: account merchantService.login(form.getUsername(), form.getPassword()); break; case user: account userService.login(form.getUsername(), form.getPassword()); break; default: return Result.error(未知角色); } if (account null) { return Result.error(账号或密码错误); } session.setAttribute(role, role); session.setAttribute(account, account); return Result.ok(account); } }这里的关键是session.setAttribute(role, role)。很多课程设计的登录拦截只判断「有没有登录」不判断「是哪个角色」结果商家登录后能直接访问/api/admin/user/list。解决方法是在拦截器里按路径前缀比角色public class RoleInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { String uri req.getRequestURI(); HttpSession session req.getSession(false); if (session null || session.getAttribute(role) null) { resp.setStatus(401); return false; } String role (String) session.getAttribute(role); if (uri.startsWith(/api/admin/) !admin.equals(role)) { resp.setStatus(403); return false; } if (uri.startsWith(/api/merchant/) !merchant.equals(role)) { resp.setStatus(403); return false; } return true; } }注册拦截器时把登录路径排除Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new RoleInterceptor()) .addPathPatterns(/api/admin/**, /api/merchant/**) .excludePathPatterns(/api/login/**); } }/api/user/**那部分没有放进addPathPatterns是因为用户端有些接口如商品详情允许匿名访问。如果全拦游客连商品列表都看不到。这是按场景做的取舍游客能看下单前才强制登录。4.2 商品列表与购物车分页查询和 JPA 写法商品列表按分类翻页用 Spring Data JPA 的Pageable很直接public interface ProductRepository extends JpaRepositoryProduct, Long { // 按分类和审核状态分页状态 1 表示审核通过 PageProduct findByCategoryIdAndAuditStatus(Long categoryId, Integer auditStatus, Pageable pageable); // 按商品名模糊搜供搜索框用 PageProduct findByNameContainingAndAuditStatus(String keyword, Integer auditStatus, Pageable pageable); }调用处public PageProduct listByCategory(Long categoryId, int page, int size) { // page 从 0 开始前端习惯从 1 开始这里做一次减法 Pageable pageable PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, sales)); return productRepository.findByCategoryIdAndAuditStatus(categoryId, 1, pageable); }PageRequest.of的page参数从 0 起算这点和前端分页控件默认的 1 起算差一位不转换的话第一页永远被跳过。排序按sales倒序让月售销量高的先冒头——文档里商品 E-R 图专门列了月售销量字段正好拿来做默认排序依据。购物车这块cart表只存user_id、product_id、quantity展示时再关联商品表拿名字和价格。这样商品改价后购物车实时显示新价不用同步维护冗余字段。缺点是每次查购物车要 join但数据量小换来的数据一致性更划算。4.3 下单事务库存和订单必须原子提交下单是整条链路里唯一必须开事务的地方。用户从购物车勾选商品点结算后端要做四件事读购物车行、算总价、写订单头和明细、扣库存。这四步任何一步失败前面已经写的订单都要回滚否则会出现「订单创建了但库存没扣」或者反过来。Service public class OrderService { Autowired private OrderRepository orderRepository; Autowired private OrderItemRepository orderItemRepository; Autowired private CartRepository cartRepository; Autowired private ProductRepository productRepository; // Transactional 保证库存扣减和订单写入要么全成、要么全败 Transactional(rollbackFor Exception.class) public Order placeOrder(Long userId, ListLong cartIds) { if (cartIds null || cartIds.isEmpty()) { throw new BizException(请先选择商品); } ListCart carts cartRepository.findAllById(cartIds); if (carts.isEmpty()) { throw new BizException(购物车为空); } BigDecimal total BigDecimal.ZERO; Order order new Order(); order.setOrderNo(ORD System.currentTimeMillis()); order.setUserId(userId); order.setStatus(0); // 0 待支付 order.setCreateTime(LocalDateTime.now()); for (Cart cart : carts) { // 用悲观锁读出商品避免并发下单把库存扣成负数 Product product productRepository .findByIdForUpdate(cart.getProductId()) .orElseThrow(() - new BizException(商品不存在)); if (product.getStock() cart.getQuantity()) { throw new BizException(product.getName() 库存不足); } BigDecimal subtotal product.getPrice() .multiply(BigDecimal.valueOf(cart.getQuantity())); total total.add(subtotal); // 扣库存 product.setStock(product.getStock() - cart.getQuantity()); product.setSales(product.getSales() cart.getQuantity()); // 写明细 OrderItem item new OrderItem(); item.setOrderId(order.getId()); item.setProductId(product.getId()); item.setProductName(product.getName()); item.setPrice(product.getPrice()); item.setQuantity(cart.getQuantity()); item.setSubtotal(subtotal); orderItemRepository.save(item); } order.setTotalPrice(total); orderRepository.save(order); // 下单成功后清空对应购物车行 cartRepository.deleteAll(carts); return order; } }findByIdForUpdate需要自己在 Repository 里补一个方法用Lock(LockModeType.PESSIMISTIC_WRITE)注解。它的作用是给商品行加数据库行锁防止两个用户同时下单同一件最后库存的商品时都读到stock1最后扣成负数。课程设计里并发量小但答辩现场如果被问到「两个用户同时买最后一件怎么办」能答上行锁就是一个加分项。金额的累加用BigDecimal不要图方便转double。BigDecimal.multiply之后的总价再落库到decimal(10,2)列精度链路上是闭合的。5本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →