Spring Boot + MyBatis + MySQL 电商项目实战:从入门到完整实现
目录
前言
通过上一章的Spring Boot入门教程,你已经掌握了框架的核心用法。本章我们将在此基础上,引入MyBatis作为持久层框架,结合MySQL数据库,从一个真实的电商后端案例出发,手把手带你完成从环境搭建到功能实现的全过程。你将学习到:
-
✅ Spring Boot 整合 MyBatis 的最佳实践
-
✅ 电商核心模块(商品、用户、购物车、订单)的完整实现
-
✅ 事务管理、联表查询、动态SQL等进阶技巧
-
✅ 完整的可运行代码示例
一、项目概述与技术选型
1.1 我们将要实现的电商功能
以一个迷你商城为例,实现以下核心模块-4:
| 模块 | 功能点 |
|---|---|
| 商品模块 | 商品列表展示、商品详情查询、商品搜索 |
| 用户模块 | 用户注册、登录(密码加密)、会话管理 |
| 购物车模块 | 添加商品到购物车、修改数量、删除商品 |
| 订单模块 | 生成订单、订单列表查询 |
1.2 技术栈清单
-
后端框架:Spring Boot 2.7.x / 3.x
-
持久层框架:MyBatis + MyBatis Spring Boot Starter
-
数据库:MySQL 5.7+ / 8.0
-
项目构建:Maven
-
API测试:Postman / Swagger
二、环境搭建与项目初始化
2.1 创建Spring Boot项目
访问 Spring Initializr 或使用IDE创建项目,选择以下依赖:
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version> <!-- 根据Spring Boot版本选择 -->
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok(可选,简化代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2.2 配置数据库连接
在 application.yml 中配置数据源和MyBatis:
spring:
datasource:
url: jdbc:mysql://localhost:3306/mall?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml # Mapper XML文件位置
type-aliases-package: com.example.mall.entity # 实体类包路径
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名映射
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志(开发环境)
2.3 创建数据库表
执行以下SQL脚本,创建四张核心表:
-- 商品表
CREATE TABLE `product` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '商品名称',
`price` decimal(10,2) NOT NULL COMMENT '价格',
`stock` int(11) NOT NULL COMMENT '库存',
`description` text COMMENT '商品描述',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 用户表
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL UNIQUE,
`password` varchar(255) NOT NULL COMMENT '加密存储',
`email` varchar(100),
`phone` varchar(20),
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 订单表
CREATE TABLE `order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_no` varchar(50) NOT NULL UNIQUE COMMENT '订单号',
`user_id` bigint(20) NOT NULL,
`total_amount` decimal(10,2) NOT NULL,
`status` tinyint(4) DEFAULT 0 COMMENT '0-待支付 1-已支付 2-已发货 3-已完成',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 订单明细表
CREATE TABLE `order_item` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_id` bigint(20) NOT NULL,
`product_id` bigint(20) NOT NULL,
`product_name` varchar(100) NOT NULL,
`product_price` decimal(10,2) NOT NULL,
`quantity` int(11) NOT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_order_id` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 购物车表(简单实现,实际可用Redis)
CREATE TABLE `cart_item` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL,
`product_id` bigint(20) NOT NULL,
`quantity` int(11) NOT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_product` (`user_id`, `product_id`) -- 一个用户对一个商品只能有一条记录
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
三、商品模块开发
3.1 创建实体类
package com.example.mall.entity;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class Product {
private Long id;
private String name;
private BigDecimal price;
private Integer stock;
private String description;
private Date createTime;
private Date updateTime;
}
3.2 编写Mapper接口
package com.example.mall.mapper;
import com.example.mall.entity.Product;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ProductMapper {
/** 根据ID查询商品 */
Product selectById(@Param("id") Long id);
/** 查询所有商品(分页) */
List<Product> selectAll();
/** 根据名称模糊查询 */
List<Product> selectByName(@Param("name") String name);
/** 更新商品库存 */
int updateStock(@Param("id") Long id, @Param("stock") Integer stock);
/** 插入商品 */
int insert(Product product);
}
3.3 编写Mapper XML
在 resources/mapper/ProductMapper.xml 中:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mall.mapper.ProductMapper">
<resultMap id="BaseResultMap" type="com.example.mall.entity.Product">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="price" property="price" />
<result column="stock" property="stock" />
<result column="description" property="description" />
<result column="create_time" property="createTime" />
<result column="update_time" property="updateTime" />
</resultMap>
<sql id="Base_Column_List">
id, name, price, stock, description, create_time, update_time
</sql>
<select id="selectById" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM product
WHERE id = #{id}
</select>
<select id="selectAll" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM product
ORDER BY create_time DESC
</select>
<!-- 动态SQL示例:模糊查询 -->
<select id="selectByName" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM product
<where>
<if test="name != null and name != ''">
name LIKE CONCAT('%', #{name}, '%')
</if>
</where>
</select>
<!-- 更新库存,使用乐观锁防止超卖 -->
<update id="updateStock">
UPDATE product
SET stock = stock - #{quantity}
WHERE id = #{id} AND stock >= #{quantity}
</update>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO product (name, price, stock, description)
VALUES (#{name}, #{price}, #{stock}, #{description})
</insert>
</mapper>
3.4 编写Service层
package com.example.mall.service;
import com.example.mall.entity.Product;
import com.example.mall.mapper.ProductMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
public Product getProductById(Long id) {
return productMapper.selectById(id);
}
public List<Product> getAllProducts() {
return productMapper.selectAll();
}
public List<Product> searchProducts(String name) {
return productMapper.selectByName(name);
}
/**
* 扣减库存,返回true表示成功
*/
public boolean reduceStock(Long productId, Integer quantity) {
int affected = productMapper.updateStock(productId, quantity);
return affected > 0;
}
}
3.5 编写Controller
package com.example.mall.controller;
import com.example.mall.entity.Product;
import com.example.mall.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping
public List<Product> list() {
return productService.getAllProducts();
}
@GetMapping("/{id}")
public Product detail(@PathVariable Long id) {
return productService.getProductById(id);
}
@GetMapping("/search")
public List<Product> search(@RequestParam(required = false) String keyword) {
return productService.searchProducts(keyword);
}
}
四、用户模块开发
4.1 实体类
package com.example.mall.entity;
import lombok.Data;
import java.util.Date;
@Data
public class User {
private Long id;
private String username;
private String password; // 加密存储
private String email;
private String phone;
private Date createTime;
}
4.2 Mapper接口
@Mapper
public interface UserMapper {
User selectByUsername(@Param("username") String username);
User selectById(@Param("id") Long id);
int insert(User user);
}
4.3 Mapper XML
<mapper namespace="com.example.mall.mapper.UserMapper">
<select id="selectByUsername" resultType="User">
SELECT id, username, password, email, phone, create_time
FROM user
WHERE username = #{username}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (username, password, email, phone)
VALUES (#{username}, #{password}, #{email}, #{phone})
</insert>
</mapper>
4.4 Service实现(含密码加密)
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// Spring Security的密码加密器,也可用BCrypt
private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
/**
* 用户注册
*/
public boolean register(User user) {
// 检查用户名是否已存在
User existing = userMapper.selectByUsername(user.getUsername());
if (existing != null) {
return false;
}
// 密码加密
user.setPassword(passwordEncoder.encode(user.getPassword()));
return userMapper.insert(user) > 0;
}
/**
* 用户登录验证
*/
public User login(String username, String rawPassword) {
User user = userMapper.selectByUsername(username);
if (user != null && passwordEncoder.matches(rawPassword, user.getPassword())) {
// 登录成功,返回用户信息(注意移除密码)
user.setPassword(null);
return user;
}
return null;
}
}
五、购物车模块开发
购物车有多种实现方式:基于数据库、基于Redis、或基于客户端的本地存储。这里我们选择基于数据库的方式,便于演示MyBatis操作。
5.1 实体类
@Data
public class CartItem {
private Long id;
private Long userId;
private Long productId;
private Integer quantity;
private Date createTime;
private Date updateTime;
// 非数据库字段,用于展示时关联商品信息
private transient Product product;
}
5.2 Mapper接口
@Mapper
public interface CartMapper {
/** 查询用户的购物车列表(关联商品表) */
List<CartItem> selectByUserId(@Param("userId") Long userId);
/** 查询特定商品项 */
CartItem selectByUserAndProduct(@Param("userId") Long userId, @Param("productId") Long productId);
/** 添加或更新 */
int upsert(CartItem cartItem);
/** 删除商品项 */
int delete(@Param("id") Long id);
/** 清空用户购物车 */
int deleteByUserId(@Param("userId") Long userId);
}
5.3 Mapper XML(联表查询示例)
<mapper namespace="com.example.mall.mapper.CartMapper">
<resultMap id="CartWithProduct" type="com.example.mall.entity.CartItem">
<id column="id" property="id" />
<result column="user_id" property="userId" />
<result column="product_id" property="productId" />
<result column="quantity" property="quantity" />
<result column="create_time" property="createTime" />
<result column="update_time" property="updateTime" />
<!-- 关联商品信息 -->
<association property="product" javaType="com.example.mall.entity.Product">
<id column="pid" property="id" />
<result column="pname" property="name" />
<result column="price" property="price" />
</association>
</resultMap>
<select id="selectByUserId" resultMap="CartWithProduct">
SELECT
c.*,
p.id as pid,
p.name as pname,
p.price
FROM cart_item c
LEFT JOIN product p ON c.product_id = p.id
WHERE c.user_id = #{userId}
</select>
<!-- 使用 ON DUPLICATE KEY UPDATE 实现 upsert -->
<insert id="upsert">
INSERT INTO cart_item (user_id, product_id, quantity)
VALUES (#{userId}, #{productId}, #{quantity})
ON DUPLICATE KEY UPDATE
quantity = quantity + #{quantity}
</insert>
</mapper>
5.4 Service实现
@Service
public class CartService {
@Autowired
private CartMapper cartMapper;
@Autowired
private ProductService productService;
/**
* 添加商品到购物车
*/
public boolean addToCart(Long userId, Long productId, Integer quantity) {
// 检查商品是否存在且库存充足
Product product = productService.getProductById(productId);
if (product == null || product.getStock() < quantity) {
return false;
}
CartItem item = new CartItem();
item.setUserId(userId);
item.setProductId(productId);
item.setQuantity(quantity);
return cartMapper.upsert(item) > 0;
}
/**
* 获取购物车列表(带商品详情)
*/
public List<CartItem> getCartList(Long userId) {
return cartMapper.selectByUserId(userId);
}
/**
* 删除购物车项
*/
public boolean removeItem(Long itemId) {
return cartMapper.delete(itemId) > 0;
}
}
六、订单模块开发(事务管理)
订单创建是电商的核心业务,需要保证多表操作的一致性:扣减库存 → 创建订单 → 清空购物车。这些操作必须在同一个事务中完成。
6.1 实体类
@Data
public class Order {
private Long id;
private String orderNo; // 订单号,唯一
private Long userId;
private BigDecimal totalAmount;
private Integer status; // 0-待支付
private Date createTime;
// 订单明细列表(非数据库字段)
private transient List<OrderItem> items;
}
@Data
public class OrderItem {
private Long id;
private Long orderId;
private Long productId;
private String productName;
private BigDecimal productPrice;
private Integer quantity;
private Date createTime;
}
6.2 Mapper接口
@Mapper
public interface OrderMapper {
int insert(Order order);
int insertItem(OrderItem item);
Order selectByOrderNo(@Param("orderNo") String orderNo);
List<Order> selectByUserId(@Param("userId") Long userId);
}
6.3 Service实现(事务关键)
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private ProductService productService;
@Autowired
private CartMapper cartMapper;
/**
* 创建订单(使用@Transactional保证事务)
*/
@Transactional(rollbackFor = Exception.class)
public Order createOrder(Long userId, List<CartItem> selectedItems) {
if (selectedItems == null || selectedItems.isEmpty()) {
throw new IllegalArgumentException("购物车不能为空");
}
// 1. 生成订单号(简单示例:时间戳+随机数)
String orderNo = "ORD" + System.currentTimeMillis() + (int)(Math.random()*1000);
// 2. 计算总金额
BigDecimal total = BigDecimal.ZERO;
for (CartItem item : selectedItems) {
// 获取最新商品信息
Product product = productService.getProductById(item.getProductId());
if (product == null) {
throw new RuntimeException("商品不存在:" + item.getProductId());
}
// 扣减库存(调用ProductService的reduceStock)
boolean success = productService.reduceStock(product.getId(), item.getQuantity());
if (!success) {
throw new RuntimeException("商品库存不足:" + product.getName());
}
// 累计金额
total = total.add(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())));
}
// 3. 插入订单主表
Order order = new Order();
order.setOrderNo(orderNo);
order.setUserId(userId);
order.setTotalAmount(total);
order.setStatus(0); // 待支付
orderMapper.insert(order);
// 4. 插入订单明细
for (CartItem item : selectedItems) {
Product product = productService.getProductById(item.getProductId());
OrderItem orderItem = new OrderItem();
orderItem.setOrderId(order.getId());
orderItem.setProductId(product.getId());
orderItem.setProductName(product.getName());
orderItem.setProductPrice(product.getPrice());
orderItem.setQuantity(item.getQuantity());
orderMapper.insertItem(orderItem);
}
// 5. 清空购物车(删除已下单的商品)
for (CartItem item : selectedItems) {
cartMapper.delete(item.getId());
}
return order;
}
/**
* 查询用户订单
*/
public List<Order> getUserOrders(Long userId) {
return orderMapper.selectByUserId(userId);
}
}
6.4 Controller
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private CartService cartService;
@PostMapping("/create")
public Result createOrder(@RequestBody OrderCreateRequest request) {
// 假设已通过拦截器获取当前登录用户ID
Long userId = CurrentUser.getId();
// 获取用户选中的购物车项
List<CartItem> selectedItems = cartService.getSelectedItems(request.getCartItemIds());
Order order = orderService.createOrder(userId, selectedItems);
return Result.success(order);
}
@GetMapping("/list")
public Result listOrders() {
Long userId = CurrentUser.getId();
return Result.success(orderService.getUserOrders(userId));
}
}
七、测试与验证
7.1 单元测试示例
@SpringBootTest
@Transactional // 测试后回滚
public class OrderServiceTest {
@Autowired
private OrderService orderService;
@Autowired
private CartMapper cartMapper;
@Test
public void testCreateOrder() {
// 准备数据
Long userId = 1L;
// 先添加一个商品到购物车
CartItem item = new CartItem();
item.setUserId(userId);
item.setProductId(1L);
item.setQuantity(2);
cartMapper.upsert(item);
// 获取购物车列表
List<CartItem> cartList = cartMapper.selectByUserId(userId);
// 创建订单
Order order = orderService.createOrder(userId, cartList);
// 断言
assertNotNull(order);
assertNotNull(order.getOrderNo());
assertTrue(order.getTotalAmount().compareTo(BigDecimal.ZERO) > 0);
}
}
7.2 API测试(Postman)
| 接口 | 方法 | URL | 说明 |
|---|---|---|---|
| 商品列表 | GET | /api/products |
查看所有商品 |
| 加入购物车 | POST | /api/cart/add |
参数:productId, quantity |
| 查看购物车 | GET | /api/cart |
返回购物车列表 |
| 创建订单 | POST | /api/orders/create |
参数:cartItemIds数组 |
八、进阶优化建议
8.1 关于MyBatis的最佳实践
-
动态SQL:利用
<if>、<where>、<foreach>标签应对复杂查询-7 -
分页查询:使用PageHelper插件或手动LIMIT实现
-
批量操作:使用
<foreach>实现批量插入/更新
8.2 性能与并发
-
库存扣减的原子性:SQL中直接
stock - #{quantity}并判断stock >= #{quantity},利用数据库行锁保证原子性-6 -
乐观锁:在商品表增加
version字段,更新时检查版本号 -
索引优化:对高频查询字段(如
order.user_id、cart.user_id)建立索引
8.3 可扩展性思考
-
购物车用Redis:提高并发性能,定期同步到DB
-
订单号生成:改用雪花算法等分布式ID生成器
-
消息队列:订单创建成功后发送消息,解耦后续处理(如通知物流、发送邮件)
九、学习资源推荐
-
完整项目源码:SpringBoot+MyBatis 仿天猫商城 -8(阿里云社区,附源码)
-
MyBatis官方文档:https://mybatis.org/mybatis-3/zh/
-
实战案例:进销存系统中的库存事务设计 -6,对理解电商库存有很好参考价值
总结
通过本教程,你不仅掌握了Spring Boot整合MyBatis的核心步骤,更重要的是通过电商业务场景理解了:
-
如何设计实体与Mapper完成CRUD
-
如何用动态SQL实现复杂查询
-
如何用
@Transactional保证订单创建的一致性 -
如何实现库存扣减的原子性
电商系统涉及的知识远不止这些,但掌握了以上基础,你已经具备了开发一个可用商城后端的能力。接下来,建议你:
-
在本地运行并测试所有接口
-
尝试增加商品分类、支付回调等功能
-
学习使用Redis优化购物车
如果你在实践过程中遇到问题,欢迎留言交流。Happy Coding!
更多推荐




所有评论(0)