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,提升性能
  • 订单创建要事务,保证数据一致
  • 库存扣减要防超卖,加锁或乐观锁

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

相关推荐
q***49862 小时前
分布式WEB应用中会话管理的变迁之路
前端·分布式
IT_陈寒2 小时前
JavaScript性能优化:10个V8引擎隐藏技巧让你的代码快30%
前端·人工智能·后端
前端加油站3 小时前
Chrome/Firefox 浏览器扩展开发完整指南
前端·chrome
码途进化论3 小时前
从Chrome跳转到IE浏览器的完整解决方案
前端·javascript
笙年3 小时前
Vue 基础配置新手总结
前端·javascript·vue.js
哆啦A梦15883 小时前
40 token
前端·vue.js·node.js
炫饭第一名3 小时前
Cursor 一年深度开发实践:前端开发的效率革命🚀
前端·程序员·ai编程
摇滚侠4 小时前
Vue 项目实战《尚医通》,获取挂号医生的信息展示,笔记43
前端·javascript·vue.js·笔记·html5
晴殇i4 小时前
关于前端基础快速跨入鸿蒙HarmonyOS开发
前端·harmonyos