简介这份资源是面向计算机及相关专业本科生、研究生的MySQL网上购物系统课程设计文档基于Spring Boot框架、Java技术与MySQL数据库采用B/S结构实现完整的在线购物平台。内容覆盖需求分析、系统设计、编码实现与系统测试全过程重点阐述用户、商家、管理员三大功能模块并给出E-R图与数据库表结构设计测试部分从功能、性能、安全三方面展开适合作为课程设计参考或电商系统入门案例。资源包共1个docx文件约3.8MB为完整论文式文档含中英文摘要、目录、技术选型与用例分析等章节结构清晰便于直接借鉴写作框架与模块划分思路。目前已有76人学习下载可帮助读者快速理解Spring Boot在实际项目中的落地方式掌握从需求到测试的完整开发流程并为后续系统维护与功能扩展提供参考。1. 从课程设计到能跑通的购物系统这套 Spring Boot MySQL 方案到底解决什么问题很多同学做课程设计时第一反应是去搜“网上购物系统源码”下载下来发现跑不起来或者跑起来了但答辩时被问“库存扣减怎么保证不超卖”就卡壳。这套基于 Spring Boot 的网上购物商城系统核心要解决的不是“能不能下单”而是“多人同时下单时数据不乱、订单状态可追溯、前后端职责清晰”。它适合两类人一是需要交课程设计但不想只交个空壳的在校生二是想用一个小型电商项目练手 Spring Boot 分层架构和 MySQL 事务的开发者。我见过太多人把购物系统做成“能登录、能加购物车、能下单”就收工结果一压测就翻车——库存扣成负数、订单重复提交、金额对不上。这套方案的重点就是把这些坑提前填上。2. 技术选型与数据库设计为什么用 Spring Boot 而不是 Servlet表结构怎么定2.1 选型理由Spring Boot 省掉的是配置不是逻辑课程设计里常见两种做法纯 Servlet JSP或者 Spring Boot 前后端分离。前者写起来“看得见底层”但光是 web.xml、数据库连接池、事务管理就能耗掉一半时间最后业务逻辑反而写得潦草。Spring Boot 的价值在于自动配置和起步依赖把连接池、事务、JSON 序列化这些基础设施默认搭好让你把精力放在购物车合并、订单状态机、库存扣减这些真正会被答辩追问的地方。我一般会选 Spring Boot 2.7.x 或 3.x 搭配 MySQL 8JDK 用 8 或 17。持久层用 MyBatis-Plus 而不是纯 JPA原因是购物系统里“条件分页查询商品”“按状态筛选订单”这类动态 SQL 很常见MyBatis-Plus 的 LambdaQueryWrapper 写起来直观也方便你在答辩时讲清楚每条 SQL 对应什么业务。前端如果是课程设计用 Thymeleaf 做服务端渲染最省事如果想显得现代一点Vue Axios 前后端分离也行但要多处理跨域和登录态传递。2.2 数据库表结构六张核心表撑起整个商城购物系统的表不用多但字段要想清楚。下面是我常用的最小可用表结构直接给建表语句。-- 用户表区分普通用户和管理员 CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, role TINYINT DEFAULT 0 COMMENT 0普通用户 1管理员, phone VARCHAR(20) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 商品表库存字段是并发核心 CREATE TABLE product ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0 COMMENT 库存扣减时必须加行锁, category_id BIGINT DEFAULT NULL, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 购物车表同一用户同一商品只存一条数量累加 CREATE TABLE cart_item ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_product (user_id,product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单主表状态机是答辩重点 CREATE TABLE orders ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号, user_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成 4已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表下单时快照商品价格和名称 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, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 收货地址表 CREATE TABLE address ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, receiver VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, detail VARCHAR(200) NOT NULL, is_default TINYINT DEFAULT 0, PRIMARY KEY (id), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个设计决策值得在答辩时主动讲第一order_item里冗余了product_name和price因为商品可能改价或下架订单必须保留成交时的快照否则历史订单金额会变。第二cart_item加了uk_user_product唯一索引加购时用INSERT ... ON DUPLICATE KEY UPDATE quantity quantity 1避免同一商品出现多条购物车记录。第三orders.status用数字枚举而不是字符串查询效率高但要在代码里定义常量类别到处写魔法数字。2.3 项目分层Controller 不写业务Service 不碰 HttpServletRequest标准分层是 controller / service / mapper / entity / dto / vo。Controller 只负责参数校验和调用 Service返回统一结果包装类Service 里写业务逻辑和事务边界Mapper 只做数据库操作。我见过把库存扣减写在 Controller 里的结果事务不生效因为 Spring 的事务是基于代理的Controller 里的方法没被代理就白搭。所以记住Transactional要加在 Service 的 public 方法上且该方法必须通过 Spring 容器注入的代理对象调用类内部自调用不生效。3. 核心业务实现下单、库存扣减、购物车合并的代码怎么写3.1 下单接口一个 Transactional 方法里做完五件事下单是整个系统最容易出并发问题的地方。常见做法是校验库存、扣库存、写订单主表、写订单明细、清购物车。这五步必须在一个事务里任何一步失败全部回滚。Service public class OrderServiceImpl implements OrderService { Autowired private ProductMapper productMapper; Autowired private OrdersMapper ordersMapper; Autowired private OrderItemMapper orderItemMapper; Autowired private CartItemMapper cartItemMapper; Override Transactional(rollbackFor Exception.class) public String createOrder(Long userId, ListLong cartItemIds) { // 1. 查出购物车项校验归属 ListCartItem items cartItemMapper.selectByIds(cartItemIds); if (items.isEmpty()) { throw new BizException(购物车为空); } BigDecimal total BigDecimal.ZERO; ListOrderItem orderItems new ArrayList(); // 2. 逐项扣库存用乐观锁或行锁 for (CartItem item : items) { // update product set stock stock - #{qty} where id #{id} and stock #{qty} int affected productMapper.deductStock(item.getProductId(), item.getQuantity()); if (affected 0) { throw new BizException(商品库存不足 item.getProductId()); } Product p productMapper.selectById(item.getProductId()); total total.add(p.getPrice().multiply(new BigDecimal(item.getQuantity()))); OrderItem oi new OrderItem(); oi.setProductId(p.getId()); oi.setProductName(p.getName()); // 快照 oi.setPrice(p.getPrice()); // 快照 oi.setQuantity(item.getQuantity()); orderItems.add(oi); } // 3. 写订单主表 Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(0); // 待支付 ordersMapper.insert(order); // 4. 写订单明细 for (OrderItem oi : orderItems) { oi.setOrderId(order.getId()); orderItemMapper.insert(oi); } // 5. 清购物车 cartItemMapper.deleteBatchIds(cartItemIds); return order.getOrderNo(); } }逻辑说明deductStock对应的 SQL 是UPDATE product SET stock stock - #{qty} WHERE id #{id} AND stock #{qty}这条语句在 InnoDB 里会对该行加排他锁并且stock qty条件保证了不会扣成负数。如果返回影响行数为 0说明库存不够直接抛异常触发回滚。参数上rollbackFor Exception.class确保受检异常也回滚别只写Transactional默认只回滚运行时异常。3.2 购物车合并加购时用 ON DUPLICATE KEY UPDATE用户重复加购同一商品不应该产生两条记录而是数量累加。利用唯一索引uk_user_product配合 MySQL 的ON DUPLICATE KEY UPDATE一条 SQL 搞定。!-- CartItemMapper.xml -- insert idaddOrUpdate INSERT INTO cart_item (user_id, product_id, quantity) VALUES (#{userId}, #{productId}, #{quantity}) ON DUPLICATE KEY UPDATE quantity quantity #{quantity} /insert逻辑说明当(user_id, product_id)冲突时执行 UPDATE 把原数量加上新数量。参数quantity默认传 1如果用户在前端选了数量就传实际值。注意这个语法是 MySQL 特有的换成 PostgreSQL 要用ON CONFLICT。另外如果业务要求加购时校验库存要在 Service 里先查一次库存再调用别把校验逻辑塞进 SQL。3.3 订单状态流转别让用户直接改 status订单状态从待支付到已完成必须由后端根据操作类型驱动不能前端传什么就改什么。常见做法是定义状态机常量并提供明确的接口支付接口把 0 改成 1发货接口把 1 改成 2确认收货把 2 改成 3取消订单把 0 改成 4。public class OrderStatus { public static final int UNPAID 0; public static final int PAID 1; public static final int SHIPPED 2; public static final int FINISHED 3; public static final int CANCELED 4; } // 支付回调里 Transactional public void paySuccess(String orderNo) { Orders order ordersMapper.selectByOrderNo(orderNo); if (order.getStatus() ! OrderStatus.UNPAID) { throw new BizException(订单状态不允许支付); } order.setStatus(OrderStatus.PAID); order.setPayTime(new Date()); ordersMapper.updateById(order); }逻辑说明每次状态变更前先校验当前状态是否允许该操作防止重复支付或已取消订单被支付。参数上orderNo是业务订单号不是主键 id对外暴露订单号更安全。payTime用new Date()记录支付时间方便后续对账。4. 避坑与排查库存超卖、事务失效、跨域、密码明文这些坑我都踩过4.1 库存扣成负数现象是压测时 stock 出现 -1原因是先查后扣没加锁现象两个人同时买最后一件商品都查到库存为 1都执行扣减结果库存变成 -1。原因Service 里先select stock判断再update stock stock - 1这两步之间没有锁。解决直接用UPDATE ... WHERE stock qty原子操作靠数据库行锁保证。如果业务复杂到需要先查再算就用SELECT ... FOR UPDATE手动加行锁但要注意锁的范围和顺序避免死锁。4.2 Transactional 不生效现象是抛异常后数据没回滚原因是方法自调用或异常被吞现象下单失败抛了异常但库存已经扣了订单也写了。原因要么是同类内部方法直接调用this.createOrder()没走代理要么是 catch 了异常没重新抛出Spring 以为方法正常结束。解决确保事务方法被外部类调用catch 块里要么throw new RuntimeException(e)要么用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()手动标记回滚。4.3 前后端分离跨域现象是浏览器报 CORS 错误原因是端口不同且没配跨域现象前端跑在 8080后端跑在 8081Axios 请求被浏览器拦截。原因同源策略限制。解决在后端加全局跨域配置别在每个 Controller 上写CrossOrigin。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns和allowedOrigins的区别当allowCredentials(true)时不能用allowedOrigins(*)要用allowedOriginPatterns(*)否则启动报错。4.4 密码明文存库现象是数据库里能看到 123456原因是没加密现象用户表 password 字段直接存明文。原因图省事。解决用 BCrypt 加密Spring Security 提供了BCryptPasswordEncoder即使不引入完整 Security 也可以单独用这个类。注册时encode登录时matches。别用 MD5MD5 已经被彩虹表攻破答辩时被问到会减分。4.5 订单号重复现象是并发下单时 order_no 唯一索引冲突原因是用了时间戳现象高并发下两个订单生成同一个订单号插入报唯一键冲突。原因用System.currentTimeMillis()做订单号毫秒级并发会重复。解决用“时间戳 用户id后四位 随机数”或者引入雪花算法。课程设计里简单点可以用UUID去掉横杠但 UUID 无序作为主键插入会影响 InnoDB 性能所以只作为业务订单号主键仍用自增 id。5. 进阶技巧用 JMeter 压测验证库存安全用状态机图讲清答辩逻辑5.1 压测验证200 并发抢 100 件库存看最终 stock 是否为 0课程设计答辩时如果你能拿出压测数据说服力完全不一样。用 JMeter 建一个线程组200 个线程同时调用下单接口目标商品库存设为 100。跑完后查数据库SELECT stock FROM product WHERE id ?正确结果是 0且订单明细里该商品总数量正好 100。如果 stock 是负数说明扣减逻辑有并发漏洞如果订单数量超过 100说明有超卖。这个测试不需要复杂工具JMeter 下载解压就能用线程组里加 HTTP 请求参数用 CSV 文件传不同用户 id 和购物车 id。5.2 用状态机图讲清订单流转答辩时画在白板上订单状态是答辩老师最爱问的点。我一般会准备一张状态流转表比画图更快当前状态允许操作目标状态触发接口待支付 0支付已支付 1/order/pay待支付 0取消已取消 4/order/cancel已支付 1发货已发货 2/admin/order/ship已发货 2确认收货已完成 3/order/confirm已完成 3无——已取消 4无——这张表能直接回答“为什么已取消订单不能支付”“为什么已完成订单不能取消”。代码里对应一个canTransfer(int from, int to)方法每次变更前调用不满足就抛业务异常。5.3 一个我常犯的错忘了给订单明细加索引早期做购物系统时订单明细表只建了主键没建order_id索引。数据量小的时候没感觉后来模拟数据插了 10 万条订单明细查“某订单的所有商品”时全表扫描页面加载要 3 秒。加上KEY idx_order (order_id)后降到毫秒级。这个坑在课程设计阶段很容易被忽略因为测试数据就几十条但答辩时如果老师让你解释“订单详情页怎么查的”你能说出索引设计就是加分项。5.4 最后说一个习惯所有金额用 BigDecimal别用 double我见过用 double 算总价的0.1 0.2 得到 0.30000000000000004订单金额显示成 0.30000000000000004 元用户直接懵。Java 里金额一律用BigDecimal数据库用DECIMAL(10,2)。BigDecimal 比较用compareTo而不是equals因为equals会比较精度new BigDecimal(1.0).equals(new BigDecimal(1.00))返回 false。这个细节写进代码里答辩时被问到就是亮点。希望帮到你。本文还有配套的精品资源点击获取