从零搭一套 Spring Boot 电子购物系统:架构、编码、部署全记录
做毕业设计或者自己想练手,选“电子购物系统”这类题目的人非常多。原因很直接:它覆盖面广,从前端页面到后端接口,从用户登录到订单状态流转,几乎把 Web 开发的核心环节都串起来了。而且 Spring Boot 技术栈在招聘市场上是主流,做完这一套,简历上能写的东西也实实在在。
这篇博文就围绕我最近整理的一套 Spring Boot 电子购物系统展开。它不是纯理论讲解,而是带着完整源码、数据库设计、调试部署思路的实操记录。如果你是刚学完 Java 基础、想用 Spring Boot 做第一个完整项目,或者正在为课程设计、毕业设计发愁,这篇文章应该能帮你省下大量摸索时间。我先说明白这套系统解决的核心问题:不会一上来就丢一堆文件让你自己看,而是把“为什么这样设计”“关键代码怎么写”“部署时哪里容易卡住”都讲透。
1. 整体设计与技术选型拆解
1.1 为什么选 Spring Boot 而不是 SSH 或 Servlet 手写
很多初学者会纠结这个问题。我个人的建议很明确:除非学校有硬性规定必须用 SSH(Struts2 + 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(库存)、status(1 上架 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 的学习曲线陡峭,而且它的过滤器链机制对初学者不友好。我用 JWT(JSON 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 拼接漏洞:
LambdaQueryWrapper<Product> 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("商品库存不足,请重试"); }对应的 SQL:
UPDATE 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 开发环境完整版本清单
这套系统我在完整跑通后整理了版本组合,这几个版本之间兼容性很好,你可以直接照抄:
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8(8u202+) | Spring Boot 2.x 对 JDK 8 支持最稳定,避免高版本兼容问题 |
| Spring Boot | 2.3.12.RELEASE | 2.x 系列成熟稳定,资料多 |
| MySQL | 5.7 或 8.0 | 两个版本都验证过,5.7 对老机器更友好 |
| MyBatis Plus | 3.4.x | 简化 SQL 开发的关键依赖 |
| Maven | 3.6.x | 依赖管理和构建工具 |
| Redis | 5.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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver这里有三个细节要注意:
第一,serverTimezone=Asia/Shanghai必须加。否则连接 MySQL 8.x 时会报时区错误,因为新版驱动要求明确指定时区。
第二,useUnicode=true&characterEncoding=utf8保证中文正常存储。如果漏了这个参数,插入中文数据时会乱码,这是初学阶段出现频率最高的一个问题。
第三,MySQL 8.x 的驱动类名是com.mysql.cj.jdbc.Driver,5.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> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>配置完重载 Maven 项目,依赖下载速度会明显加快。
静态资源访问 404:Spring 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 2>&1 &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 映射等知识点串联成体系。而真正拉开差距的,是你对细节的处理——比如库存防超卖、订单状态机的严谨流转、部署时对环境的排查思路。每多琢磨一个这样的细节,你的代码就不只是“能跑”,而是“可靠”,这大概是做项目最有价值的收获。