前言
在餐厅点餐系统中,购物车是一个核心功能模块。本文将详细介绍如何使用Vue3和SpringBoot技术栈来实现一个功能完整的购物车系统。我们将从前端到后端,逐步展开讲解实现过程。
技术栈
- 前端:Vue3 + Vite + Pinia + Element Plus
- 后端:SpringBoot 2.7.x + MyBatis-Plus + MySQL
- 开发工具:IDEA、VS Code
- 依赖管理:Maven
一、数据库设计
首先,我们需要设计相关的数据表结构。主要涉及以下几个表:
sql
-- 购物车表
CREATE TABLE `cart` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`dish_id` bigint(20) NOT NULL COMMENT '菜品ID',
`quantity` int(11) NOT NULL COMMENT '数量',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车表';
-- 菜品表
CREATE TABLE `dish` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '菜品名称',
`price` decimal(10,2) NOT NULL COMMENT '价格',
`description` varchar(200) DEFAULT NULL COMMENT '描述',
`image` varchar(200) DEFAULT NULL COMMENT '图片',
`status` tinyint(4) DEFAULT '1' COMMENT '状态:1-在售 0-停售',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜品表';
二、后端实现
1. 实体类定义
java
@Data
@TableName("cart")
public class Cart {
private Long id;
private Long userId;
private Long dishId;
private Integer quantity;
private LocalDateTime createTime;
private LocalDateTime updateTime;
// 扩展字段,用于前端展示
@TableField(exist = false)
private String dishName;
@TableField(exist = false)
private BigDecimal price;
}
2. Service层实现
java
@Service
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements CartService {
@Autowired
private DishService dishService;
@Override
public void addToCart(Long userId, Long dishId, Integer quantity) {
// 查询是否已存在购物车记录
LambdaQueryWrapper<Cart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Cart::getUserId, userId)
.eq(Cart::getDishId, dishId);
Cart cart = this.getOne(queryWrapper);
if (cart != null) {
// 已存在,更新数量
cart.setQuantity(cart.getQuantity() + quantity);
this.updateById(cart);
} else {
// 不存在,新增记录
cart = new Cart();
cart.setUserId(userId);
cart.setDishId(dishId);
cart.setQuantity(quantity);
this.save(cart);
}
}
@Override
public List<CartVO> getUserCart(Long userId) {
// 查询用户购物车列表
LambdaQueryWrapper<Cart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Cart::getUserId, userId);
List<Cart> cartList = this.list(queryWrapper);
// 填充菜品信息
return cartList.stream().map(cart -> {
CartVO vo = new CartVO();
BeanUtils.copyProperties(cart, vo);
Dish dish = dishService.getById(cart.getDishId());
vo.setDishName(dish.getName());
vo.setPrice(dish.getPrice());
vo.setSubtotal(dish.getPrice().multiply(new BigDecimal(cart.getQuantity())));
return vo;
}).collect(Collectors.toList());
}
}
3. Controller层实现
java
@RestController
@RequestMapping("/api/cart")
public class CartController {
@Autowired
private CartService cartService;
@PostMapping("/add")
public Result<Void> addToCart(@RequestBody CartDTO dto) {
// 从Token中获取userId
Long userId = UserContext.getCurrentUserId();
cartService.addToCart(userId, dto.getDishId(), dto.getQuantity());
return Result.success();
}
@GetMapping("/list")
public Result<List<CartVO>> getCartList() {
Long userId = UserContext.getCurrentUserId();
List<CartVO> cartList = cartService.getUserCart(userId);
return Result.success(cartList);
}
@PutMapping("/update")
public Result<Void> updateQuantity(@RequestBody CartDTO dto) {
cartService.updateQuantity(dto.getId(), dto.getQuantity());
return Result.success();
}
@DeleteMapping("/{id}")
public Result<Void> removeFromCart(@PathVariable Long id) {
cartService.removeById(id);
return Result.success();
}
}
三、前端实现
1. Pinia状态管理
typescript
// stores/cart.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { CartItem } from '@/types'
import { getCartList, addToCart, updateCart, removeFromCart } from '@/api/cart'
export const useCartStore = defineStore('cart', () => {
const cartItems = ref<CartItem[]>([])
const loading = ref(false)
// 计算属性:总价
const total = computed(() => {
return cartItems.value.reduce((sum, item) => {
return sum + item.price * item.quantity
}, 0)
})
// 获取购物车列表
async function fetchCartList() {
loading.value = true
try {
const { data } = await getCartList()
cartItems.value = data
} finally {
loading.value = false
}
}
// 添加到购物车
async function add(dishId: number, quantity: number) {
await addToCart({ dishId, quantity })
await fetchCartList()
}
// 更新数量
async function updateQuantity(id: number, quantity: number) {
await updateCart({ id, quantity })
await fetchCartList()
}
// 移除商品
async function remove(id: number) {
await removeFromCart(id)
await fetchCartList()
}
return {
cartItems,
loading,
total,
fetchCartList,
add,
updateQuantity,
remove
}
})
2. 购物车组件实现
vue
<!-- components/ShoppingCart.vue -->
<template>
<div class="shopping-cart">
<el-card class="cart-container">
<template #header>
<div class="cart-header">
<span>购物车</span>
<span>{{ cartStore.cartItems.length }}件商品</span>
</div>
</template>
<div v-if="cartStore.loading" class="loading">
<el-skeleton :rows="3" animated />
</div>
<template v-else>
<div v-if="cartStore.cartItems.length === 0" class="empty-cart">
<el-empty description="购物车是空的" />
</div>
<div v-else class="cart-items">
<div v-for="item in cartStore.cartItems"
:key="item.id"
class="cart-item">
<div class="item-info">
<img :src="item.image" :alt="item.dishName" class="item-image">
<div class="item-details">
<h4>{{ item.dishName }}</h4>
<p class="price">¥{{ item.price }}</p>
</div>
</div>
<div class="item-actions">
<el-input-number
v-model="item.quantity"
:min="1"
:max="99"
@change="(val) => handleQuantityChange(item.id, val)"
/>
<el-button
type="danger"
circle
@click="handleRemove(item.id)"
>
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</div>
<div class="cart-footer">
<div class="total">
总计: <span class="price">¥{{ cartStore.total }}</span>
</div>
<el-button type="primary" @click="handleCheckout">
去结算
</el-button>
</div>
</template>
</el-card>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { useCartStore } from '@/stores/cart'
import { ElMessage } from 'element-plus'
const cartStore = useCartStore()
onMounted(() => {
cartStore.fetchCartList()
})
const handleQuantityChange = async (id: number, quantity: number) => {
try {
await cartStore.updateQuantity(id, quantity)
ElMessage.success('数量已更新')
} catch (error) {
ElMessage.error('更新失败')
}
}
const handleRemove = async (id: number) => {
try {
await cartStore.remove(id)
ElMessage.success('商品已移除')
} catch (error) {
ElMessage.error('移除失败')
}
}
const handleCheckout = () => {
// 跳转到结算页面
router.push('/checkout')
}
</script>
<style scoped lang="scss">
.shopping-cart {
.cart-container {
width: 100%;
max-width: 800px;
margin: 0 auto;
}
.cart-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.cart-items {
.cart-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #eee;
&:last-child {
border-bottom: none;
}
.item-info {
display: flex;
align-items: center;
gap: 12px;
.item-image {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 4px;
}
.item-details {
h4 {
margin: 0 0 8px;
}
.price {
color: #ff4d4f;
font-weight: bold;
}
}
}
.item-actions {
display: flex;
align-items: center;
gap: 12px;
}
}
}
.cart-footer {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
.total {
font-size: 16px;
.price {
color: #ff4d4f;
font-size: 20px;
font-weight: bold;
}
}
}
}
</style>
四、功能亮点
- 实时数据同步
- 使用Pinia进行状态管理
- 操作后自动刷新购物车数据
- 价格实时计算
- 性能优化
- 使用computed计算总价
- 列表使用key优化渲染
- 防抖处理频繁操作
- 用户体验
- Loading状态展示
- 操作反馈提示
- 空状态处理
- 数量输入限制
- 代码质量
- TypeScript类型检查
- 组件化开发
- 状态集中管理
- 统一的错误处理
五、扩展优化建议
- 功能扩展
- 添加商品规格选择
- 支持批量删除
- 商品库存检查
- 添加购物车商品备注
- 性能优化
- 添加接口缓存
- 大列表虚拟滚动
- 图片懒加载
- 防抖节流处理
- 体验优化
- 添加动画效果
- 草稿数据本地存储
- 移动端适配
- 快捷键支持
总结
本文详细介绍了如何使用Vue3和SpringBoot实现餐厅点餐系统的购物车功能。通过合理的技术选型、数据结构设计和代码实现,我们实现了一个功能完整、性能优良的购物车系统。在实际开发中,我们还需要根据具体业务需求进行适当的调整和扩展。