
从零搭一套 Spring Boot 电子购物系统架构、编码、部署全记录做毕业设计或者自己想练手选“电子购物系统”这类题目的人非常多。原因很直接它覆盖面广从前端页面到后端接口从用户登录到订单状态流转几乎把 Web 开发的核心环节都串起来了。而且 Spring Boot 技术栈在招聘市场上是主流做完这一套简历上能写的东西也实实在在。这篇博文就围绕我最近整理的一套 Spring Boot 电子购物系统展开。它不是纯理论讲解而是带着完整源码、数据库设计、调试部署思路的实操记录。如果你是刚学完 Java 基础、想用 Spring Boot 做第一个完整项目或者正在为课程设计、毕业设计发愁这篇文章应该能帮你省下大量摸索时间。我先说明白这套系统解决的核心问题不会一上来就丢一堆文件让你自己看而是把“为什么这样设计”“关键代码怎么写”“部署时哪里容易卡住”都讲透。1. 整体设计与技术选型拆解1.1 为什么选 Spring Boot 而不是 SSH 或 Servlet 手写很多初学者会纠结这个问题。我个人的建议很明确除非学校有硬性规定必须用 SSHStruts2 Spring Hibernate或者原生 Servlet否则直接用 Spring Boot。原因有三第一Spring Boot 把配置简化到了极致。以前 SSM 整合要写一堆 XML 配置文件数据源、事务管理器、扫描路径、视图解析器每个都要手动配置。Spring Boot 的自动配置机制把这些都封装好了你只需要在application.yml里写几行配置就能跑起来。对于学习阶段来说把精力花在业务逻辑上比花在配置文件上划算得多。第二Spring Boot 自带 Tomcat 等嵌入式容器打包成 jar 后java -jar就能启动部署成本大大降低。这一点对于后面把你做的系统部署到云服务器上演示特别重要。我见过太多同学在本地 IDEA 里跑得好好的一到部署就各种问题——环境变量不对、端口冲突、容器配置缺失。Spring Boot 的 jar 包部署方式基本免疫这些问题。第三社区资源和招聘市场需求偏向明显。Spring Boot 的教程、踩坑记录、开源项目数量是其他框架的数倍遇到问题搜解决方案容易得多。你以后找工作面试Spring Boot 也几乎是必问项。1.2 系统分层架构与功能模块划分这套电子购物系统遵循经典的三层架构同时按功能拆成用户端和管理端两条线。整体结构是Controller 层接口层接收前端请求做参数校验调用 Service 层接口。Service 层业务层处理核心业务逻辑比如下单时的库存扣减、订单状态流转、购物车价格计算。Mapper 层数据访问层使用 MyBatis Plus操作 MySQL 数据库表。用户端模块包括注册登录、商品分类浏览、商品搜索、商品详情、购物车管理、订单确认、订单支付模拟、订单管理、个人中心。管理端模块包括管理员登录、商品管理增删改查、上下架、分类管理、订单管理发货、查看详情、用户管理。这种划分方式的好处是不会过度设计每个模块的代码量适中但你又能从中体会到真实项目里“模块自治、职责分离”的感觉。比如新增一个“优惠券”功能时你只需要模仿商品模块的结构新增对应的 Controller、Service、Mapper 即可不需要改动现有代码。1.3 为什么这套系统的数据库用 MySQL 而非其他MySQL 在整个电商类课程设计里几乎属于“标准答案”。它足够轻量单机部署毫无压力同时它又是生产环境中使用率极高的数据库学好它不会白费功夫。与之配套的数据库设计工具我日常用 Navicat 或 DBeaver 这类图形化客户端做表结构和数据调试比命令行效率高很多。这套系统的存储引擎选择 InnoDB原因也很直白它支持事务和外键。购物流程涉及多张表的写入操作——生成订单记录、扣减库存、清空购物车如果中间任何一步失败就必须回滚否则会出现“订单生成了但商品库存没扣”这类脏数据。InnoDB 的事务机制正是为此服务的。至于 MyISAM现在基本只出现在老项目中新项目不要选了表锁和崩溃恢复能力都不行。2. 数据库设计购物系统的地基2.1 核心表结构与字段说明我需要先强调一个经验数据库设计是一套系统的地基地基建歪了后面写业务代码时到处难受。这套系统一共设计了 8 张核心表我先挑最关键的几张说明设计思路。用户表t_user字段包括 id、username、password、nickname、avatar、phone、email、status账号状态1 正常 0 禁用、create_time。密码字段我不建议存明文用 BCrypt 加密存储。虽然课程设计阶段不太会有真正的攻击者但养成好习惯很重要——以后工作里这是硬性要求。商品表t_product字段包括 id、category_id分类外键、name、subtitle副标题、main_image主图、detail富文本详情、price价格、stock库存、status1 上架 0 下架、sales销量、create_time、update_time。这里有两个细节值得注意第一价格字段用 decimal(10,2) 而不是 double/float浮点数计算金额会产生精度问题购物车合计和订单金额计算时尤其明显第二商品表单独存一个“主图”字段而不是存图集是为了列表页性能——详情页只需要展示一张主图多图可以后续用单独的图片表或 JSON 字段扩展。分类表t_category字段包括 id、parent_id父分类 id支持多级分类、name、sort排序号、icon。用parent_id sort的经典设计既能支持一级、二级分类又能灵活调整前台展示顺序。类似京东、淘宝那种“手机数码 - 手机 - 国产手机”的多级导航都能用这个结构实现。购物车表t_cart字段包括 id、user_id、product_id、quantity、checked是否选中、create_time、update_time。这张表的设计要点是不冗余商品价格购物车展示时实时关联商品表查询当前价格。为什么因为商品价格随时可能调整如果购物车里存了旧价格用户结算时发现价格不一致会很困惑。真正下单那一刻才锁定价格此时把快照写入订单明细表。订单表t_order字段包括 id、order_no订单编号、user_id、total_amount、pay_amount实付金额、payment_type支付方式、status订单状态、receiver_name、receiver_phone、receiver_address、pay_time、deliver_time、finish_time、create_time。订单明细表t_order_item字段包括 id、order_id、order_no、product_id、product_name、product_image、current_unit_price下单时价格快照、quantity、total_price。这里的product_name、product_image、current_unit_price都是冗余字段。为什么要存冗余因为商品信息后续可能修改甚至删除但订单作为交易凭证必须保留“当时下单时的快照”。这是电商订单表设计里非常基础且重要的思路。2.2 订单状态流转设计订单表里的status字段是这套系统的业务核心之一。我设计了如下状态机状态值含义可流转到0待付款1已取消、2已支付1已取消无2已支付待发货3已发货、4退款3已发货5已完成4已退款无5已完成无这个设计看着简单但实现时有两个地方容易出问题。第一点订单取消要考虑“超时未支付自动取消”。这个功能基础版可以直接用定时任务扫表实现每分钟扫一次把创建时间超过 30 分钟且状态仍为 0 的订单改为 1同时把订单明细里锁定的库存加回去。真实项目中会用延迟队列或 Redis 过期键监听但课程设计阶段定时任务完全够用逻辑直观答辩时还能展示你对业务完整性的考虑。第二点状态更新必须带条件更新。比如用户取消订单的 SQL 应该是UPDATE t_order SET status 1 WHERE id ? AND status 0。为什么要加AND status 0因为如果用户下单后立刻支付成功状态变为 2同时又点了取消按钮后到的取消请求如果不加条件就会把已支付订单直接改成已取消造成用户货没收到、钱也退了商家亏损。这种“乐观锁思想在状态流转中的应用”是面试加分项。3. 核心功能实现与关键代码解读3.1 用户注册登录与 Token 鉴权登录模块我采用 token 鉴权方案没有引入 Spring Security 这类重量级框架原因是在课程设计阶段Security 的学习曲线陡峭而且它的过滤器链机制对初学者不友好。我用 JWTJSON Web Token实现了一个轻量级登录方案。用户注册时密码经过 BCrypt 加密后存入数据库。BCrypt 是单向哈希算法自带随机盐即使两个用户密码相同加密后的结果也不一样。登录成功后后端生成一个 JWT token 返回给前端前端后续请求在请求头里带上这个 token后端通过拦截器验证 token 合法性。核心拦截器逻辑大体是public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录、注册等无需鉴权的接口 if (request.getRequestURI().contains(/user/login) || request.getRequestURI().contains(/user/register)) { return true; } String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BusinessException(401, 未登录或登录已过期); } // 解析 token获取用户 id 存入 request Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); return true; } }这里有个特别容易踩的坑拦截器放行静态资源。首次集成时项目里放行名单只加了/user/login和/user/register结果前端页面一打开浏览器控制台全是 401 报错查了半天才发现是 CSS、JS 文件被拦截了。解决方案是把/static/**、/templates/**这类资源路径也加入放行名单或者直接统一前缀处理。3.2 商品浏览与分页查询商品列表页和搜索页是高频访问页面这里我用 MyBatis Plus 的分页插件实现。需要注意一个坑分页插件必须配置拦截器才能生效很多人漏了这一步结果分页查询返回全部数据。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }商品搜索支持关键词模糊匹配同时可以选择按分类过滤、按价格区间过滤、按销量或价格排序。因为涉及多个可选条件我建议用 LambdaQueryWrapper 构建动态 SQL代码简洁且不会出现 SQL 拼接漏洞LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); if (StringUtils.isNotBlank(keyword)) { wrapper.and(w - w.like(Product::getName, keyword).or().like(Product::getSubtitle, keyword)); } if (categoryId ! null) { wrapper.eq(Product::getCategoryId, categoryId); } if (minPrice ! null maxPrice ! null) { wrapper.between(Product::getPrice, minPrice, maxPrice); } wrapper.orderByDesc(Product::getSales);3.3 购物车Redis 还是数据库这是个技术选型问题。真实互联网项目里购物车往往先用 Redis 缓存因为它的读写性能远高于 MySQL而且天然支持过期时间。但从这套系统的定位来看我用数据库表存购物车理由是课程设计阶段数据量不大数据库方案逻辑更直观面试时你能把完整链路讲清楚更重要。购物车的核心操作加购、改数量、勾选、取消勾选、删除、清空。加购时先查该用户的购物车里是否已有同一商品有则数量累加没有则新增一条记录。这个逻辑里有个并发问题同一用户连续两次点击“加入购物车”可能出现两条重复记录。解决方案是在t_cart表上建立user_id product_id的唯一索引数据库层面兜底防重。3.4 下单流程事务与库存扣减下单是整套系统里最体现编程功力的地方。核心逻辑是校验商品是否上架、校验库存是否充足、计算订单总金额、生成订单号和订单明细、扣减库存、清空购物车对应商品。让我展开讲讲“生成订单号和扣减库存”两个细节。订单号生成我见过很多人直接拿时间戳当订单号这种方法在并发稍高时几乎必然重复。我用的是“时间戳 用户ID后四位 随机四位数字”的组合策略yyyyMMddHHmmss userId%10000 四位随机数。生成后还要查一次数据库确认不重复如果重复则重新生成。实际测试下来几千个订单没有出现过撞号。扣减库存最基础但容易出错的做法是Product product productMapper.selectById(productId); if (product.getStock() quantity) { throw new BusinessException(库存不足); } product.setStock(product.getStock() - quantity); productMapper.updateById(product);这个写法在并发高时会超卖。原因在这里两个请求同时读到库存为 1都判断库存充足都执行减 1最终库存变成 -1。解决这个问题最稳妥、也最容易向面试官解释的方案是使用乐观锁int rows productMapper.deductStock(productId, quantity, oldStock); if (rows 0) { throw new BusinessException(商品库存不足请重试); }对应的 SQLUPDATE t_product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}这条 SQL 的巧妙之处在于stock #{quantity}条件保证扣减后库存不会为负数影响行数为 0 时说明库存不足或商品已被并发更新此时直接提示用户重试。不用显式加锁也不用事务串行化性能和安全性都兼顾到了。下单整体方法上标注Transactional。这里有个需要注意的重点Spring 事务只有通过代理对象调用时才会生效也就是说——同类内部方法调用this.xxx()时事务会失效。常见场景是下单方法内部调用了同一个类里的“生成订单号”方法如果想保证生成订单号也受事务控制需要把它拆分到另一个 Service或者直接自注入调用。这个坑日常开发里我也踩过排查的时候一度以为数据库出问题了。3.5 订单管理取消、发货、确认收货订单管理模块相对简单核心是状态流转。用户端可以“取消订单”和“确认收货”管理端可以“发货”。每个操作都必须校验当前订单状态是否符合预期比如“确认收货”只能在已发货状态执行否则抛出异常。“取消订单”有个隐藏逻辑需要处理订单取消后要恢复库存。因为下单时已经扣过库存所以取消订单时要把商品库存加回去。我第一次做的时候正好漏了这一步测试时发现取消订单后商品库存没有恢复用户都下单失败了才发现问题——这种测试发现的问题往往是最有价值的教训。4. 开发环境配置与调试部署经验4.1 开发环境完整版本清单这套系统我在完整跑通后整理了版本组合这几个版本之间兼容性很好你可以直接照抄组件版本说明JDK1.88u202Spring Boot 2.x 对 JDK 8 支持最稳定避免高版本兼容问题Spring Boot2.3.12.RELEASE2.x 系列成熟稳定资料多MySQL5.7 或 8.0两个版本都验证过5.7 对老机器更友好MyBatis Plus3.4.x简化 SQL 开发的关键依赖Maven3.6.x依赖管理和构建工具Redis5.x 及以上本系统用到的地方不多可暂时不启动前端Thymeleaf Bootstrap服务端渲染模板与静态资源结合结构清晰这里特别提醒JDK 版本不要盲目追新。我试过用 JDK 17 跑 Spring Boot 2.3.x直接启动失败报了一堆反射相关的错误。如果你刚开始学就老老实实用 JDK 8后续熟练了再升级到 JDK 17 配合 Spring Boot 3.x避免一开始就陷入环境兼容性的泥潭。4.2 数据库初始化与连接配置拿到源码后首先要做的是导入数据库。我会在项目里提供一份完整的init.sql脚本里面包含建表语句和初始数据。导入时用 Navicat 或命令行执行都可以推荐用 Navicat图形化界面能直观看到表结构和数据。导入完成后打开application.yml配置数据库连接spring: datasource: url: jdbc:mysql://localhost:3306/shopping?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver这里有三个细节要注意第一serverTimezoneAsia/Shanghai必须加。否则连接 MySQL 8.x 时会报时区错误因为新版驱动要求明确指定时区。第二useUnicodetruecharacterEncodingutf8保证中文正常存储。如果漏了这个参数插入中文数据时会乱码这是初学阶段出现频率最高的一个问题。第三MySQL 8.x 的驱动类名是com.mysql.cj.jdbc.Driver5.x 是com.mysql.jdbc.Driver。如果你用的是 MySQL 8.x 但驱动配置写错启动时直接报ClassNotFoundException。4.3 本地启动与常见启动异常排查配置完成、依赖导入成功后启动 Spring Boot 项目。我整理了这段时间学员和身边朋友遇到最多的几个问题端口被占用启动后控制台报Port 8080 was already in use。处理分两步先在 IDEA 底部 Terminal 执行netstat -ano | findstr 8080查占用进程 PID再打开任务管理器结束对应进程。或者干脆改端口——在application.yml里加一行server.port: 8081就完事。数据库连接失败报Communications link failure或Access denied for user。前者多半是 MySQL 服务没启动去服务管理里把 MySQL 服务设为开机自启并手动启动后者说明用户名或密码不对认真检查配置和实际数据库密码是否一致。还有一个容易忽视的地方MySQL 8.x 默认认证插件是caching_sha2_password如果驱动版本过老连接时会报认证失败。解决方案是升级 MySQL 驱动或在 MySQL 中改回mysql_native_password认证方式。依赖下载缓慢或失败Maven 首次加载 Spring Boot 依赖数量很大如果国内网络环境不佳经常卡住。我建议直接配置阿里云 Maven 镜像在 Maven 的settings.xml中加mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror配置完重载 Maven 项目依赖下载速度会明显加快。静态资源访问 404Spring Boot 默认静态资源路径是classpath:/static/如果你把 CSS、JS 放在webapp目录下默认是访问不到的。建议按项目现有的目录结构放置静态资源不要随意改位置。5. 部署到服务器让系统可以被任何人访问本地跑通系统只能算打了 60% 的仗。为了让答辩时展示效果更好或者方便演示给朋友看我把这套系统打包部署到了云服务器上。整个过程并不复杂核心就三步。5.1 项目打包先保证本地测试通过然后在 IDEA 右侧 Maven 面板执行package命令跳过测试直接打包mvn clean package -DskipTests等待 BUILD SUCCESS 后target目录下会生成一个shopping-0.0.1-SNAPSHOT.jar文件。这个 jar 就是整个系统的可运行包。注意如果你的工程里有多个模块需要先打包依赖模块再打包入口模块单模块工程直接一条命令打到底。5.2 服务器环境准备与启动在云服务器上需要安装 JDK 和 MySQL。JDK 安装完成后把 jar 包上传到服务器任意目录执行nohup java -jar shopping-0.0.1-SNAPSHOT.jar app.log 21 nohup和的作用是让程序在后台运行日志输出到app.log文件即使你关闭 SSH 连接也不会影响系统运行。启动完成后访问http://服务器IP:8080就能看到系统首页了。如果页面打不开第一件事检查云服务器安全组的端口放行规则——我因为没在阿里云后台开放 8080 端口排查了快一个小时最后发现是安全组规则没配。这一步是最容易被忽略的。5.3 部署过程中的典型问题jar 包启动后立刻退出查看app.log的错误信息多半是数据库连接失败确认服务器的 MySQL 里已经执行过init.sql并且application.yml中密码和服务器上实际一致。页面能打开但图片加载失败项目中商品图片如果使用本地路径存储路径在服务器上就失效了。这类问题我一般处理方式是把图片上传功能改为使用绝对路径映射在配置里加自定义静态资源映射即可Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file:D:/upload/); } }这样图片请求就会映射到服务器本地目录不再依赖项目内部路径。当然你完全可以在项目里直接用网络图片 URL省去这层麻烦但实际项目里本地存储的解决方案是必须掌握的。6. 项目扩展从课程设计到更贴近生产环境这套系统做完如果想让它在简历上更有分量或者想深入学习有几个方向值得扩展。第一个建议是接入 Redis 做缓存。上面也提到商品列表和商品详情是高频访问接口目前每次请求都直接查数据库。你把热点商品数据缓存到 Redis设置 10 分钟过期数据库压力就能明显下降。在技术面试里缓存这一项是项目经验中的常客。第二个建议是引入消息队列做订单超时关闭。目前用的是定时任务扫表效率低且存在一分钟内的延迟。换成 RabbitMQ 或 RocketMQ 的延迟消息机制后下单时发送一条延迟消息30 分钟后消费检测订单未支付就自动取消。这个优化在技术深度上比定时任务高出一截。第三个建议是补充单元测试。很多课程设计项目完全不写测试但你如果对核心 Service 写下测试用例比如测试下单时库存不足的异常处理、测试重复下单的拦截效果这在答辩或面试中是非常加分的亮点。我个人的体会是完成一个基础功能完善的购物系统能让你把 Java 集合、多线程、Spring 核心、MySQL 事务、MyBatis 映射等知识点串联成体系。而真正拉开差距的是你对细节的处理——比如库存防超卖、订单状态机的严谨流转、部署时对环境的排查思路。每多琢磨一个这样的细节你的代码就不只是“能跑”而是“可靠”这大概是做项目最有价值的收获。