上周,实习生小周问我:
"马哥,我想做一个完整的电商项目练手,但网上教程要么太简单(只有后端),要么太复杂(直接上微服务)。有没有一个前后端分离、功能完整的商城 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,提升性能
- 订单创建要事务,保证数据一致
- 库存扣减要防超卖,加锁或乐观锁
学会电商项目,你就掌握了 真实业务场景的完整开发流程!