Spring Boot + Vue 实现一个在线商城(商品展示、购物车、订单)!从零到一完整项目

上周,实习生小周问我:

"马哥,我想做一个完整的电商项目练手,但网上教程要么太简单(只有后端),要么太复杂(直接上微服务)。有没有一个前后端分离、功能完整的商城 demo,从商品展示到购物车到订单都讲一遍?"

我笑了:"电商项目,核心就三点:商品、购物车、订单。今天我就带你用 Spring Boot + Vue,一步步搭建一个能跑的商城!"

✅ 后端:Spring Boot + MyBatis + MySQL + Redis

✅ 前端:Vue 3 + Element Plus + Axios

✅ 功能:商品列表 → 加入购物车 → 生成订单

🛠️ 一、准备工作(只需 3 分钟)

你需要:

  • JDK 17
  • Node.js 18+(用于 Vue)
  • MySQL 8.0
  • IDEA + VS Code(或 WebStorm)
  • Redis(用于购物车缓存)

💻 二、后端项目搭建(Spring Boot 部分)

步骤 1:创建 Spring Boot 项目(pom.xml)

XML 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>3.0.2</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
</dependencies>

步骤 2:配置文件(application.yml)

XML 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mall?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

  redis:
    host: localhost
    port: 6379
    database: 1
    timeout: 2000ms
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.mall.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# 跨域配置(开发用)
server:
  port: 8080

步骤 3:创建数据库表(SQL)

sql 复制代码
-- 创建数据库
CREATE DATABASE mall CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

USE mall;

-- 商品表
CREATE TABLE product (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    description TEXT,
    image_url VARCHAR(255),
    stock INT NOT NULL DEFAULT 0
);

-- 订单表
CREATE TABLE `order` (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT NOT NULL,
    total_amount DECIMAL(10,2) NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 订单项表
CREATE TABLE order_item (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    order_id BIGINT NOT NULL,
    product_id BIGINT NOT NULL,
    quantity INT NOT NULL,
    price DECIMAL(10,2) NOT NULL
);

-- 插入测试数据
INSERT INTO product (name, price, description, image_url, stock) VALUES
('iPhone 15', 5999.00, '最新款苹果手机', 'https://example.com/iphone15.jpg', 100),
('MacBook Pro', 12999.00, '专业笔记本电脑', 'https://example.com/macbook.jpg', 50),
('AirPods Pro', 1999.00, '无线降噪耳机', 'https://example.com/airpods.jpg', 200);

步骤 4:创建实体类(entity/)

java 复制代码
// Product.java
package com.example.mall.entity;

public class Product {
    private Long id;
    private String name;
    private Double price;
    private String description;
    private String imageUrl;
    private Integer stock;

    // getter/setter...
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Double getPrice() { return price; }
    public void setPrice(Double price) { this.price = price; }
    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }
    public String getImageUrl() { return imageUrl; }
    public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; }
    public Integer getStock() { return stock; }
    public void setStock(Integer stock) { this.stock = stock; }
}

// Order.java
package com.example.mall.entity;

import java.util.Date;
import java.util.List;

public class Order {
    private Long id;
    private Long userId;
    private Double totalAmount;
    private String status;
    private Date createdAt;
    private List<OrderItem> items; // 关联订单项

    // getter/setter...
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public Long getUserId() { return userId; }
    public void setUserId(Long userId) { this.userId = userId; }
    public Double getTotalAmount() { return totalAmount; }
    public void setTotalAmount(Double totalAmount) { this.totalAmount = totalAmount; }
    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }
    public Date getCreatedAt() { return createdAt; }
    public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
    public List<OrderItem> getItems() { return items; }
    public void setItems(List<OrderItem> items) { this.items = items; }
}

// OrderItem.java
package com.example.mall.entity;

public class OrderItem {
    private Long id;
    private Long orderId;
    private Long productId;
    private Integer quantity;
    private Double price;

    // getter/setter...
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public Long getOrderId() { return orderId; }
    public void setOrderId(Long orderId) { this.orderId = orderId; }
    public Long getProductId() { return productId; }
    public void setProductId(Long productId) { this.productId = productId; }
    public Integer getQuantity() { return quantity; }
    public void setQuantity(Integer quantity) { this.quantity = quantity; }
    public Double getPrice() { return price; }
    public void setPrice(Double price) { this.price = price; }
}

步骤 5:创建 Mapper 接口和 XML

java 复制代码
// ProductMapper.java
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 {
    List<Product> findAll();
    Product findById(@Param("id") Long id);
    void updateStock(@Param("id") Long id, @Param("quantity") Integer quantity);
}
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="ProductResultMap" type="Product">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="price" column="price"/>
        <result property="description" column="description"/>
        <result property="imageUrl" column="image_url"/>
        <result property="stock" column="stock"/>
    </resultMap>

    <select id="findAll" resultMap="ProductResultMap">
        SELECT id, name, price, description, image_url, stock FROM product
    </select>

    <select id="findById" parameterType="java.lang.Long" resultMap="ProductResultMap">
        SELECT id, name, price, description, image_url, stock FROM product WHERE id = #{id}
    </select>

    <update id="updateStock" parameterType="map">
        UPDATE product SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity}
    </update>
</mapper>

步骤 6:创建购物车服务(Redis 缓存)

java 复制代码
// CartService.java
package com.example.mall.service;

import com.example.mall.entity.Product;
import com.example.mall.mapper.ProductMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Service
public class CartService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 添加商品到购物车
     */
    public void addToCart(Long userId, Long productId, Integer quantity) {
        String key = "cart:" + userId;
        
        // 获取当前购物车
        ValueOperations<String, Object> ops = redisTemplate.opsForValue();
        Object cartObj = ops.get(key);
        
        Map<Long, Integer> cart = new HashMap<>();
        if (cartObj != null) {
            cart = (Map<Long, Integer>) cartObj;
        }
        
        // 添加或更新商品数量
        cart.put(productId, cart.getOrDefault(productId, 0) + quantity);
        
        // 保存到 Redis,30分钟过期
        ops.set(key, cart, 30, TimeUnit.MINUTES);
    }

    /**
     * 获取购物车商品列表
     */
    public List<Map<String, Object>> getCartItems(Long userId) {
        String key = "cart:" + userId;
        ValueOperations<String, Object> ops = redisTemplate.opsForValue();
        Object cartObj = ops.get(key);
        
        if (cartObj == null) {
            return new ArrayList<>();
        }
        
        @SuppressWarnings("unchecked")
        Map<Long, Integer> cart = (Map<Long, Integer>) cartObj;
        List<Map<String, Object>> items = new ArrayList<>();
        
        for (Map.Entry<Long, Integer> entry : cart.entrySet()) {
            Long productId = entry.getKey();
            Integer quantity = entry.getValue();
            
            Product product = productMapper.findById(productId);
            if (product != null) {
                Map<String, Object> item = new HashMap<>();
                item.put("product", product);
                item.put("quantity", quantity);
                item.put("totalPrice", product.getPrice() * quantity);
                items.add(item);
            }
        }
        
        return items;
    }

    /**
     * 清空购物车
     */
    public void clearCart(Long userId) {
        String key = "cart:" + userId;
        redisTemplate.delete(key);
    }
}

步骤 7:创建订单服务

java 复制代码
// OrderService.java
package com.example.mall.service;

import com.example.mall.entity.*;
import com.example.mall.mapper.ProductMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;
import java.util.Map;

@Service
public class OrderService {

    @Autowired
    private CartService cartService;

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private OrderMapper orderMapper;

    @Transactional
    public Long createOrder(Long userId) {
        // 获取购物车商品
        List<Map<String, Object>> cartItems = cartService.getCartItems(userId);
        if (cartItems.isEmpty()) {
            throw new RuntimeException("购物车为空");
        }

        // 计算总金额
        Double totalAmount = 0.0;
        for (Map<String, Object> item : cartItems) {
            totalAmount += (Double) item.get("totalPrice");
        }

        // 创建订单
        Order order = new Order();
        order.setUserId(userId);
        order.setTotalAmount(totalAmount);
        order.setStatus("PENDING");
        order.setCreatedAt(new Date());
        orderMapper.insert(order);

        // 创建订单项并扣减库存
        for (Map<String, Object> item : cartItems) {
            Product product = (Product) item.get("product");
            Integer quantity = (Integer) item.get("quantity");

            OrderItem orderItem = new OrderItem();
            orderItem.setOrderId(order.getId());
            orderItem.setProductId(product.getId());
            orderItem.setQuantity(quantity);
            orderItem.setPrice(product.getPrice());
            orderMapper.insertOrderItem(orderItem);

            // 扣减库存
            productMapper.updateStock(product.getId(), quantity);
        }

        // 清空购物车
        cartService.clearCart(userId);

        return order.getId();
    }
}

步骤 8:创建控制器

java 复制代码
// ProductController.java
package com.example.mall.controller;

import com.example.mall.entity.Product;
import com.example.mall.mapper.ProductMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/products")
@CrossOrigin(origins = "*") // 开发用
public class ProductController {

    @Autowired
    private ProductMapper productMapper;

    @GetMapping
    public List<Product> getProducts() {
        return productMapper.findAll();
    }

    @GetMapping("/{id}")
    public Product getProduct(@PathVariable Long id) {
        return productMapper.findById(id);
    }
}

// CartController.java
package com.example.mall.controller;

import com.example.mall.service.CartService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/cart")
@CrossOrigin(origins = "*")
public class CartController {

    @Autowired
    private CartService cartService;

    @PostMapping("/add")
    public String addToCart(@RequestParam Long userId, @RequestParam Long productId, @RequestParam Integer quantity) {
        cartService.addToCart(userId, productId, quantity);
        return "OK";
    }

    @GetMapping("/{userId}")
    public List<Map<String, Object>> getCart(@PathVariable Long userId) {
        return cartService.getCartItems(userId);
    }

    @DeleteMapping("/clear/{userId}")
    public String clearCart(@PathVariable Long userId) {
        cartService.clearCart(userId);
        return "OK";
    }
}

// OrderController.java
package com.example.mall.controller;

import com.example.mall.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/orders")
@CrossOrigin(origins = "*")
public class OrderController {

    @Autowired
    private OrderService orderService;

    @PostMapping("/create")
    public String createOrder(@RequestParam Long userId) {
        Long orderId = orderService.createOrder(userId);
        return "订单创建成功,ID: " + orderId;
    }
}

🌐 三、前端项目搭建(Vue 3 部分)

步骤 1:创建 Vue 项目

bash 复制代码
npm create vue@latest mall-frontend
cd mall-frontend
npm install
npm install axios element-plus

步骤 2:创建前端页面(App.vue)

javascript 复制代码
<template>
  <div id="app">
    <el-container>
      <el-header>
        <h1>在线商城</h1>
        <div class="cart-info">
          <el-button type="primary" @click="showCart = true">购物车 ({{ cartItems.length }})</el-button>
        </div>
      </el-header>
      
      <el-main>
        <!-- 商品列表 -->
        <el-row :gutter="20">
          <el-col :span="6" v-for="product in products" :key="product.id">
            <el-card class="product-card">
              <img :src="product.imageUrl" class="product-image" />
              <div class="product-info">
                <h3>{{ product.name }}</h3>
                <p class="price">¥{{ product.price }}</p>
                <p class="stock">库存: {{ product.stock }}</p>
                <el-button 
                  type="primary" 
                  @click="addToCart(product.id, 1)"
                  :disabled="product.stock <= 0"
                >
                  加入购物车
                </el-button>
              </div>
            </el-card>
          </el-col>
        </el-row>
      </el-main>
    </el-container>

    <!-- 购物车弹窗 -->
    <el-drawer v-model="showCart" title="购物车" size="40%">
      <div v-if="cartItems.length === 0">
        <p>购物车是空的</p>
      </div>
      <div v-else>
        <div v-for="item in cartItems" :key="item.product.id" class="cart-item">
          <img :src="item.product.imageUrl" class="cart-image" />
          <div class="cart-details">
            <h4>{{ item.product.name }}</h4>
            <p>数量: {{ item.quantity }}</p>
            <p>小计: ¥{{ item.totalPrice }}</p>
          </div>
        </div>
        <div class="cart-total">
          <h3>总计: ¥{{ cartTotal }}</h3>
          <el-button type="success" @click="createOrder" :disabled="cartItems.length === 0">生成订单</el-button>
        </div>
      </div>
    </el-drawer>
  </div>
</template>

<script>
import axios from 'axios'

export default {
  name: 'App',
  data() {
    return {
      products: [],
      cartItems: [],
      showCart: false,
      userId: 1 // 模拟用户ID
    }
  },
  computed: {
    cartTotal() {
      return this.cartItems.reduce((total, item) => total + item.totalPrice, 0).toFixed(2)
    }
  },
  mounted() {
    this.loadProducts()
    this.loadCart()
  },
  methods: {
    async loadProducts() {
      try {
        const response = await axios.get('http://localhost:8080/api/products')
        this.products = response.data
      } catch (error) {
        console.error('加载商品失败:', error)
      }
    },
    async loadCart() {
      try {
        const response = await axios.get(`http://localhost:8080/api/cart/${this.userId}`)
        this.cartItems = response.data
      } catch (error) {
        console.error('加载购物车失败:', error)
      }
    },
    async addToCart(productId, quantity) {
      try {
        await axios.post('http://localhost:8080/api/cart/add', null, {
          params: { userId: this.userId, productId, quantity }
        })
        this.$message.success('已加入购物车')
        this.loadCart() // 刷新购物车
      } catch (error) {
        console.error('添加购物车失败:', error)
        this.$message.error('添加失败')
      }
    },
    async createOrder() {
      try {
        const response = await axios.post('http://localhost:8080/api/orders/create', null, {
          params: { userId: this.userId }
        })
        this.$message.success(response.data)
        this.showCart = false
        this.loadCart() // 清空购物车
      } catch (error) {
        console.error('创建订单失败:', error)
        this.$message.error('创建订单失败')
      }
    }
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.el-header {
  background-color: #409EFF;
  color: white;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.product-card {
  margin-bottom: 20px;
}

.product-image {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.product-info {
  padding: 10px 0;
}

.price {
  color: #e74c3c;
  font-size: 18px;
  font-weight: bold;
}

.stock {
  color: #999;
}

.cart-image {
  width: 60px;
  height: 60px;
  object-fit: cover;
  margin-right: 10px;
}

.cart-item {
  display: flex;
  align-items: center;
  padding: 10px 0;
  border-bottom: 1px solid #eee;
}

.cart-total {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 20px;
  background: white;
  border-top: 1px solid #eee;
  text-align: right;
}
</style>

▶️ 四、运行验证(2 分钟搞定)

1. 启动后端

bash 复制代码
cd mall-backend
mvn spring-boot:run

2. 启动前端

bash 复制代码
cd mall-frontend
npm run dev

3. 访问页面

浏览器打开:http://localhost:5173

4. 测试功能

  • 看到商品列表
  • 点"加入购物车" → 购物车数量增加
  • 点"购物车"图标 → 查看购物车内容
  • 点"生成订单" → 订单创建成功

✅ 后端端口 8080,前端端口 5173,跨域已配置

💡 五、Bonus:电商项目常见坑点

坑 1:库存超卖

当前实现 :用 Redis 缓存购物车,数据库扣减库存
生产建议:加分布式锁或数据库悲观锁

坑 2:订单幂等性

建议:生成订单前检查用户是否有未支付订单

坑 3:购物车过期

当前实现 :Redis 30分钟过期
生产建议:根据业务调整,可考虑持久化


💬六、写在最后

电商项目,看似复杂,实则是 商品、购物车、订单 三个模块的组合。

记住三句话:

  • 购物车用 Redis,提升性能
  • 订单创建要事务,保证数据一致
  • 库存扣减要防超卖,加锁或乐观锁

学会电商项目,你就掌握了 真实业务场景的完整开发流程

相关推荐
于慨14 小时前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz14 小时前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
swg32132114 小时前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
从前慢丶14 小时前
前端交互规范(Web 端)
前端
像我这样帅的人丶你还14 小时前
别再让JS耽误你进步了。
css·vue.js
gelald14 小时前
SpringBoot - 自动配置原理
java·spring boot·后端
@yanyu66614 小时前
07-引入element布局及spring boot完善后端
javascript·vue.js·spring boot
CHU72903514 小时前
便捷约玩,沉浸推理:线上剧本杀APP功能版块设计详解
前端·小程序
GISer_Jing14 小时前
Page-agent MCP结构
前端·人工智能
王霸天14 小时前
💥别再抄网上的Scale缩放代码了!50行源码教你写一个永不翻车的大屏适配
前端·vue.js·数据可视化