ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot购物商城源码解析:从数据库设计到订单流程实战

Spring Boot购物商城源码解析:从数据库设计到订单流程实战 简介一份适合Java初/中级学习者和毕业设计开发的Spring Boot购物商城系统源码包覆盖前后台核心业务模块。前台包含商品展示、搜索、分类筛选、购物车、在线购买、个人中心及收藏等后台提供用户、商品分类、颜色、评价、轮播图、资讯、订单等管理功能并集成客服模块。技术栈采用JavaSpring BootMaven前端使用Layui数据库适用MySQL 5/8开发工具为IDEA部署灵活。压缩包约24.44MB内含完整工程源码、SQL数据库脚本、配套论文和开题报告可直接导入IDEA运行调试。已有315人学习下载适合用于Java课程设计、毕业设计以及Spring Boot商城项目实战入门。1. 拿到 springboot 购物商城源码之后先别急着跑很多第一次接触 Java 购物商城系统源码的人拿到压缩包后习惯先点开项目结构然后直接在 IDEA 里用 Maven 构建结果十有八九卡在数据库连接上。这套基于 Spring Boot 2.x、Layui 和 MySQL 的商城源码覆盖了前台商品搜索、收藏、购物车、立即购买以及后台用户、商品分类、订单管理和客服管理等场景代码量不算大但表结构和请求链路比较完整适合用来说明一个典型电商后端的骨架是怎么搭出来的。无论是做数据库课程设计还是想基于一个可运行的商城源码二次开发都需要先搞清楚配置、脚本和启动顺序而不是先盯着页面看。2. springboot 商城源码的后端骨架配置文件、Maven 依赖与分层结构2.1 application.yml 中 MySQL 连接配置为什么总出问题先看项目里最常见的配置文件server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/springbootw3o95?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 servlet: multipart: max-file-size: 10MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.shop.entity这段配置是大多数 Spring Boot 商城源码的默认形态数据库名必须建为 springbootw3o95否则后面执行 SQL 脚本时会因为表不存在而报错。很多人在 MySQL 5 上运行没问题换到 MySQL 8 后出现连接失败往往是因为驱动从旧版本换成了 com.mysql.cj.jdbc.Driver并且缺少 serverTimezone 参数反过来在 MySQL 5 上如果用了高版本驱动反而可能触发认证插件问题。解决思路是先确认本地 MySQL 版本再决定是否保留 useSSLfalse 和 serverTimezone。我的习惯是写一个 spring.profiles.active 切换多环境配置把本地开发和生产环境的数据库连接分别放到 application-dev.yml 和 application-prod.yml这样不会因为改密码而污染主配置。下面这张表列出了启动时最常见的三类连接问题现象原因处理Access denied for user用户名或密码错误检查 application.yml 中 username/passwordUnknown database springbootw3o95未创建数据库先执行 create database springbootw3o95The server time zone valueMySQL 8 时区校验不通过在 url 后加 serverTimezoneAsia/Shanghai2.2 pom.xml 依赖与 JDK 版本的匹配逻辑继续看 Maven 依赖核心依赖一般长这样parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.x/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.2.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies这里把 Spring Boot 版本写成 2.7.x因为这套商城源码里大量使用 javax.servlet 相关 API一旦升级到 Spring Boot 3.x 需要把所有 javax 换成 jakarta包名整体变动改造成本大于收益。Spring Boot 2.7 对应 JDK 8 或 11如果你在 IDEA 里用的是 JDK 17Maven 编译期可能直接报“程序包不存在”或 “cannot access javax.servlet”。此时不需要换 Spring Boot 版本只需要在 Project Structure 里把 Project SDK 指定为 1.8再把 Maven 的 Runner JRE 选成对应的 JDK。另外因为网络原因下载 Spring 依赖经常超时可以在 Maven 的 settings.xml 里配置国内镜像如果没配镜像启动时会刷出大量 [ERROR] The artifact ... 提示这是构建环境问题不是源码问题。2.3 Controller-Service-Mapper 分层与接口设计源码包的目录结构大致如下src/main/java/com/shop ├── controller │ ├── LoginController.java │ ├── GoodsController.java │ └── OrderController.java ├── service │ ├── GoodsService.java │ ├── OrderService.java │ └── impl │ └── OrderServiceImpl.java ├── mapper │ ├── GoodsMapper.java │ └── OrderMapper.java ├── entity │ ├── Goods.java │ └── Order.java └── config └── WebConfig.java很多初学者会把业务逻辑写在 Controller 里但这套源码的分层相对标准Controller 只负责接收参数和做页面跳转Service 层处理库存扣减、订单生成等事务Mapper 是 MyBatis 的数据访问接口SQL 写在 resources/mapper 对应的 XML 里。以提交订单为例Controller 接收前端 JSON 后只调一个方法事务注解 Transactional 放在 Service 实现类上这样异常回滚才有效。如果直接把 Transactional 写在 Controller 上虽然也能截获异常但职责边界会变模糊后续做服务拆分时很难理清业务入口。另外我一般会让 Service 返回自定义 DTO 而不是直接把数据库实体暴露给前端避免表结构调整后接口字段跟着变动造成前端页面不可控。3. 商城数据库设计商品、分类、订单与购物车表怎么落库3.1 商品信息表与分类表字段设计和索引策略核心表结构可以从 SQL 脚本里直接看到这里拆出两张最关键的CREATE TABLE goods ( id bigint(20) NOT NULL AUTO_INCREMENT, category_id int(11) NOT NULL COMMENT 分类ID, name varchar(200) NOT NULL COMMENT 商品名称, price decimal(10,2) NOT NULL COMMENT 售价, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存, image varchar(500) DEFAULT NULL COMMENT 主图, brand varchar(100) DEFAULT NULL COMMENT 品牌, tags varchar(255) DEFAULT NULL COMMENT 标签逗号分隔, status tinyint(4) DEFAULT 1 COMMENT 1上架 0下架, PRIMARY KEY (id), KEY idx_category_status (category_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表; CREATE TABLE category ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL, sort int(11) DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;商品表里的 category_id 不建物理外键只建普通索引因为订单量上来后外键会影响插入性能。价格字段用 decimal 而不是 double避免浮点误差。库存字段在后端更新时要做原子扣减常见 SQL 是UPDATE goods SET stock stock - 1 WHERE id ? AND stock 0这样并发情况下不会把库存扣成负数。商品搜索通常会按商品名称、标签、品牌三个条件过滤如果数据量不大直接用 LIKE 也能接受如果后期商品到了几十万条建议把搜索拆成标签精确匹配加名称模糊匹配或者引入全文索引而不是在查询里写多个无索引的 LIKE。3.2 订单表与订单项表状态字段和事务边界订单部分涉及两张表订单主表和订单项表。CREATE TABLE orders ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id bigint(20) NOT NULL, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0未支付 1已支付 2已发货 3已完成 4已取消, total_amount decimal(10,2) NOT NULL, pay_time datetime DEFAULT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id bigint(20) NOT NULL AUTO_INCREMENT, order_id bigint(20) NOT NULL, goods_id bigint(20) NOT NULL, goods_name varchar(200) DEFAULT NULL, price decimal(10,2) NOT NULL, num int(11) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单状态用数字表示未支付、已支付、已发货、已完成、已取消这几种状态一旦固定后期不要随意调整数值含义否则历史数据迁移会很麻烦。生成订单时接口要同时写 orders 和 order_item两张表的写入必须放在同一个数据库事务里否则会出现订单主表有数据、订单项表没数据的情况。这里有一个容易忽略的点支付回调只负责更新订单状态和支付时间不要在回调里再次扣减库存。扣减库存应该发生在订单生成阶段并且与订单表写入保持事务一致性。后台管理里看到“未支付订单”“已发货订单”本质都是同一个订单列表接口只是传入的 status 条件不同。3.3 执行数据库脚本 springbootw3o95 时的常见坑拿到源码后第一步是建库再导入 SQL 脚本CREATE DATABASE springbootw3o95 DEFAULT CHARACTER SET utf8mb4; USE springbootw3o95; SOURCE /path/to/shop.sql;在 MySQL 命令行里用 SOURCE 执行脚本比在图形化工具里复制粘贴更稳尤其当脚本文件较大时复制粘贴容易因为特殊字符中断。如果使用 MySQL 8要注意脚本里的建表语句是否用了 utf8mb4否则中文搜索可能出现乱码。如果脚本中有外键关联先执行主表建表语句再执行子表或者让脚本自带DROP TABLE IF EXISTS来保证可重复执行。执行完成后可以用SHOW TABLES;确认表数量。这套商城一般会包含用户表、商品表、分类表、订单表、订单项表、购物车表、收藏表、评价表、资讯表、客服表等十几张表如果少了某一张基本可以判断是脚本执行中断了。4. 前台购物流程拆解搜索、收藏、加购与下单4.1 基于 Layui 的商品列表与搜索框实现前台页面使用 Layui 作为前端框架首页搜索框提交商品名称到后端接口。列表接口可以这样设计RestController RequestMapping(/goods) public class GoodsController { Autowired private GoodsService goodsService; GetMapping(/search) public MapString, Object search(RequestParam(required false) String keyword, RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 12) Integer limit) { MapString, Object result new HashMap(); PageInfoGoods pageInfo goodsService.searchByKeyword(keyword, page, limit); result.put(code, 0); result.put(msg, ); result.put(count, pageInfo.getTotal()); result.put(data, pageInfo.getList()); return result; } }这里的 page 和 limit 是 Layui 表格组件的标准参数前端通过 table.render 请求接口后端返回{code:0, msg:, count:总条数, data:当前页数据}Layui 就能自动渲染商品列表。搜索逻辑通常写在 Service 层MyBatis XML 里的 SQL 大体如下select idsearchByKeyword resultTypecom.shop.entity.Goods SELECT * FROM goods WHERE status 1 if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR tags LIKE CONCAT(%, #{keyword}, %) OR brand LIKE CONCAT(%, #{keyword}, %)) /if ORDER BY id DESC /select这段 SQL 使用 MyBatis 的动态 where 条件keyword 为空时不加过滤非空时匹配名称、标签、品牌三个字段。如果是商品推荐和首页轮播通常可以通过 status 和 sort 字段控制显示顺序。Layui 前端需要特别注意表格的 cols 配置必须和返回的 data 字段名对应比如商品名称字段是 name前端就不能写 title 去接收。4.2 商品详情与“点我收藏”的后端接口设计商品详情页会展示商品分类、图片、价格、评论等信息评论模块挂在商品 ID 下。收藏按钮会把当前用户和商品写入一张收藏表常见表结构是 user_id 和 goods_id 联合唯一避免重复收藏。PostMapping(/collect) ResponseBody public String collect(RequestParam Long userId, RequestParam Long goodsId) { Collect collect new Collect(); collect.setUserId(userId); collect.setGoodsId(goodsId); int count collectMapper.insertIfAbsent(collect); return count 0 ? 收藏成功 : 已经收藏过了; }insertIfAbsent 可以用 MySQL 的INSERT IGNORE或者先查再插。如果使用先查再插必须确保 user_id 和 goods_id 有联合唯一索引否则两个并发请求可能插入重复数据。个人中心里“我的收藏”列表只需要按 user_id 查收藏表再关联商品表读出当前价格和图片。这里不建议在收藏表里冗余商品价格因为商品价格会变动冗余只会增加后期维护成本。4.3 从加入购物车到立即购买订单生成流程购物车是前台临时容器用户可以勾选商品后批量结算也可以直接点“立即购买”跳过购物车。两种方式的终点都是生成订单。购物车表关键字段如下字段类型说明idbigint主键user_idbigint用户IDgoods_idbigint商品IDnumint数量checkedtinyint是否选中订单创建接口的后端逻辑大致如下Override Transactional(rollbackFor Exception.class) public Long createOrder(Long userId, ListCartItem items) { // 1. 校验商品是否存在且上架 // 2. 遍历 items计算总价 // 3. 扣减库存UPDATE goods SET stock stock - #{num} WHERE id #{id} AND stock #{num} // 4. 生成订单号插入 orders // 5. 批量插入 order_item // 6. 如果是从购物车结算清空对应的购物车条目 }最关键的是第 3 步的更新语句必须带上stock num条件让数据库在更新时做并发控制。如果 update 影响行数为 0说明库存不足直接抛出异常事务回滚购物车也不会被误清空。订单号建议用日期加随机数方式生成比如yyyyMMddHHmmss 6 位随机数不要使用数据库自增 ID 作为对外单号否则容易暴露日单量。初始订单状态为 0也就是未支付用户点击模拟支付后后端把 status 改成 1后台商品评价、发货等都依赖这个状态值。5. 后台管理模块用户管理、订单状态与轮播图配置5.1 后台登录与个人中心密码修改和用户管理后台登录通常复用同一套用户表只是登录后会按角色跳转到不同首页。个人中心支持修改密码和更新个人信息修改密码时不能直接把明文传进数据库。常见做法是对密码做一次加密存储再在修改时比对旧密码。PostMapping(/admin/updatePwd) ResponseBody public String updatePwd(RequestParam Long userId, RequestParam String oldPwd, RequestParam String newPwd) { User user userService.getById(userId); if (!user.getPassword().equals(MD5Utils.encode(oldPwd))) { return 原密码错误; } user.setPassword(MD5Utils.encode(newPwd)); userService.updateById(user); return 修改成功; }代码里的 MD5Utils 是项目里的一个加密工具类。MD5 不加盐容易被撞库生产环境建议换成 BCrypt改造点集中在 User 实体和登录校验处。用户管理列表在后台就是常见的 CRUD要注意两点查询用户时不要返回密码字段可以在实体类的 password 属性上加 JsonIgnore或者在 SQL 中直接忽略该列删除用户前要检查该用户是否有关联订单避免外键约束导致删除失败。5.2 订单管理未支付、已发货、已完成的状态流转后台订单管理页面会按状态分组比如未支付订单、已发货订单。状态之间不能随意跳转需要做状态机校验。状态值含义允许的操作0未支付取消、模拟支付1已支付发货2已发货确认收货3已完成无4已取消无可以在 Service 层定义一个校验方法private boolean canTransit(int from, int to) { // 0未支付 1已支付 2已发货 3已完成 4已取消 if (from 0 (to 1 || to 4)) return true; if (from 1 (to 2 || to 4)) return true; if (from 2 to 3) return true; return false; }状态机校验放在 Service 层能防止后续加接口时漏掉分支。订单列表接口需要支持分页和状态筛选使用 PageHelper 时的顺序极其重要PageHelper.startPage(page, limit); ListOrder orders orderMapper.selectByStatus(status); PageInfoOrder pageInfo new PageInfo(orders);PageHelper 的分页只会作用于紧接着的第一条 SQL 查询中间不能插入其他查询。如果订单查询要关联用户表或商品表尽量用一条多表 JOIN 的 SQL 一次性查出数据不要先用订单表查分页再逐条查用户详情那样会出现分页结果错乱或者 N1 查询问题。5.3 轮播图与商品资讯文件上传和富文本处理后台的商品分类管理、轮播图管理属于常规的增删改查其中轮播图涉及图片文件上传。Spring Boot 默认限制上传文件大小为 1MB所以要在配置里调大前面 application.yml 里的 multipart 配置就是做这个的。上传接口一般使用 MultipartFilePostMapping(/admin/upload) ResponseBody public String upload(RequestParam(file) MultipartFile file) throws IOException { String originName file.getOriginalFilename(); String ext originName.substring(originName.lastIndexOf(.)); String fileName UUID.randomUUID().toString() ext; file.transferTo(new File(uploadDir / fileName)); return /files/ fileName; }文件名用 UUID 重命名避免中文名和同名覆盖问题。图片返回后是否能访问取决于是否配置了静态资源映射常见的做法是把 uploadDir 映射到/files/**。商品资讯表的 content 字段一般是富文本编辑器提交的 HTML 内容保存到 text 类型字段中前台展示时注意要用th:utext而不是th:text否则 HTML 会被转义输出。促销活动如“服装优惠活动”就是通过资讯表发布后台编辑完成后在首页按发布时间倒序取前几条即可。6. 部署到 Tomcat 的排错清单与性能调优技巧6.1 数据库连接失败、时区报错的处理本地 IDEA 启动项目时最常见的三个错误是 Access denied、Unknown database、The server time zone value。前两个检查账号密码和库名时区错误需要在 JDBC URL 上加serverTimezoneAsia/Shanghai。MySQL 是 8.0 时驱动类必须改成com.mysql.cj.jdbc.Driver老项目里写的com.mysql.jdbc.Driver在 MySQL 8 下会抛 ClassNotFoundException。另外在 Linux 上部署时 MySQL 默认区分表名大小写从 Windows 导出的脚本经常因为表名大小写不一致报错建议在 my.cnf 里配置lower_case_table_names1后重启 MySQL让表名统一按小写处理。6.2 用 curl 和日志验证商城接口是否正常部署完成后先不要急着打开浏览器用 curl 验证核心接口更直接。验证后台登录curl -X POST http://localhost:8080/login \ -d usernameadminpassword123456 \ -H Content-Type: application/x-www-form-urlencoded如果返回 JSON 中有成功标识说明后端服务、数据库连接、用户表数据都是正常的。接着验证商品列表curl http://localhost:8080/goods/search?page1limit10返回的 JSON 里 count 字段应该是商品总数。如果返回 500去查看 Spring Boot 控制台日志重点看 Caused by 后面的异常信息。出现 BadSqlGrammarException 大多是因为 SQL 脚本没执行完整出现 BindingException 则是 MyBatis 的 mapper XML 路径配置错误需要检查 application.yml 中的 mapper-locations 是否为classpath:mapper/*.xml。6.3 优化点把订单查询从模糊搜索改造成索引覆盖最后给一个实用优化技巧。前台的商品搜索用了LIKE %keyword%商品量小的时候没感觉数据量到几十万以后搜索页会明显变慢。最简单的方案不是引入 Elasticsearch而是给商品表增加一个search_keyword字段在商品写入时把名称、品牌、标签拼接成一段文本然后对name字段建LIKE keyword%前缀索引让 MySQL 走索引扫描。对纯中文商品名MySQL 默认全文索引不支持中文分词所以更务实的做法是先用标签或者品牌字段做等值查询缩小范围再用name LIKE %keyword%做最后一层过滤。这样既能保留原搜索体验又不会因为一条模糊查询把整个商品的首页推荐拖慢。本文还有配套的精品资源点击获取
返回列表