目录

前言

一、项目概述与技术选型

1.1 我们将要实现的电商功能

1.2 技术栈清单

二、环境搭建与项目初始化

2.1 创建Spring Boot项目

2.2 配置数据库连接

2.3 创建数据库表

三、商品模块开发

3.1 创建实体类

3.2 编写Mapper接口

3.3 编写Mapper XML

3.4 编写Service层

3.5 编写Controller

四、用户模块开发

4.1 实体类

4.2 Mapper接口

4.3 Mapper XML

4.4 Service实现(含密码加密)

五、购物车模块开发

5.1 实体类

5.2 Mapper接口

5.3 Mapper XML(联表查询示例)

5.4 Service实现

六、订单模块开发(事务管理)

6.1 实体类

6.2 Mapper接口

6.3 Service实现(事务关键)

6.4 Controller

七、测试与验证

7.1 单元测试示例

7.2 API测试(Postman)

八、进阶优化建议

8.1 关于MyBatis的最佳实践

8.2 性能与并发

8.3 可扩展性思考

九、学习资源推荐

总结


前言

通过上一章的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_idcart.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保证订单创建的一致性

  • 如何实现库存扣减的原子性

电商系统涉及的知识远不止这些,但掌握了以上基础,你已经具备了开发一个可用商城后端的能力。接下来,建议你:

  1. 在本地运行并测试所有接口

  2. 尝试增加商品分类、支付回调等功能

  3. 学习使用Redis优化购物车

如果你在实践过程中遇到问题,欢迎留言交流。Happy Coding!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐