Next.js全栈电商平台
功能完整的电商系统,集成Stripe支付、购物车管理、商品搜索筛选和管理员后台,使用Zustand进行状态管理
2024年2月25日
源代码Next.js电商Stripe全栈状态管理
项目简介
这是一个功能完整的全栈电商平台,包含用户端购物体验和管理员后台管理系统。项目使用Next.js 14 App Router构建,集成Stripe支付网关,实现了从商品浏览、购物车管理到订单支付的完整购物流程。
核心功能
1. 商品管理
- 商品列表展示
- 商品详情页面
- 商品搜索和筛选
- 商品分类管理
- 库存状态追踪
2. 购物车系统
- 添加/移除商品
- 数量调整
- 价格计算
- 购物车持久化
- 优惠券支持
3. 支付集成
- Stripe支付集成
- 多种支付方式
- 支付状态追踪
- 订单确认邮件
4. 用户系统
- 用户注册登录
- 个人信息管理
- 订单历史查看
- 收货地址管理
5. 管理员后台
- 商品管理(CRUD)
- 订单管理
- 用户管理
- 数据统计分析
技术架构
前端
- Next.js 14 (App Router)
- TypeScript
- TailwindCSS
- Shadcn/ui
- Zustand (状态管理)
- React Hook Form
- Zod (表单验证)
后端
- Next.js API Routes
- Prisma ORM
- PostgreSQL
- Stripe API
- NextAuth.js
- Resend (邮件服务)
数据库设计
model Product {
id String @id @default(cuid())
name String
description String
price Decimal @db.Decimal(10, 2)
stock Int
images String[]
categoryId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
category Category @relation(fields: [categoryId], references: [id])
orderItems OrderItem[]
reviews Review[]
@@index([categoryId])
@@index([name])
}
model Category {
id String @id @default(cuid())
name String @unique
slug String @unique
products Product[]
}
model Cart {
id String @id @default(cuid())
userId String? @unique
items CartItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User? @relation(fields: [userId], references: [id])
}
model CartItem {
id String @id @default(cuid())
quantity Int
cartId String
productId String
cart Cart @relation(fields: [cartId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id])
@@unique([cartId, productId])
}
model Order {
id String @id @default(cuid())
orderNumber String @unique
status OrderStatus @default(PENDING)
total Decimal @db.Decimal(10, 2)
stripePaymentId String? @unique
userId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
items OrderItem[]
@@index([userId])
@@index([orderNumber])
}
model OrderItem {
id String @id @default(cuid())
quantity Int
price Decimal @db.Decimal(10, 2)
orderId String
productId String
order Order @relation(fields: [orderId], references: [id])
product Product @relation(fields: [productId], references: [id])
}
enum OrderStatus {
PENDING
PAID
PROCESSING
SHIPPED
DELIVERED
CANCELLED
}核心实现
1. Zustand购物车状态管理
// stores/useCartStore.ts
import create from 'zustand'
import { persist } from 'zustand/middleware'
interface CartItem {
id: string
productId: string
name: string
price: number
quantity: number
image: string
}
interface CartStore {
items: CartItem[]
addItem: (item: CartItem) => void
removeItem: (productId: string) => void
updateQuantity: (productId: string, quantity: number) => void
clearCart: () => void
getTotalItems: () => number
getTotalPrice: () => number
}
export const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
addItem: (item) => {
set((state) => {
const existingItem = state.items.find(
(i) => i.productId === item.productId
)
if (existingItem) {
// 更新数量
return {
items: state.items.map((i) =>
i.productId === item.productId
? { ...i, quantity: i.quantity + item.quantity }
: i
),
}
} else {
// 添加新商品
return {
items: [...state.items, item],
}
}
})
},
removeItem: (productId) => {
set((state) => ({
items: state.items.filter((i) => i.productId !== productId),
}))
},
updateQuantity: (productId, quantity) => {
set((state) => ({
items: state.items.map((i) =>
i.productId === productId ? { ...i, quantity } : i
),
}))
},
clearCart: () => {
set({ items: [] })
},
getTotalItems: () => {
return get().items.reduce((total, item) => total + item.quantity, 0)
},
getTotalPrice: () => {
return get().items.reduce(
(total, item) => total + item.price * item.quantity,
0
)
},
}),
{
name: 'cart-storage', // localStorage key
}
)
)2. Stripe支付集成
// app/api/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
import { prisma } from '@/lib/prisma'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
})
export async function POST(req: NextRequest) {
try {
const { items, userId } = await req.json()
// 创建订单
const order = await prisma.order.create({
data: {
orderNumber: generateOrderNumber(),
userId,
total: calculateTotal(items),
status: 'PENDING',
items: {
create: items.map((item: any) => ({
productId: item.productId,
quantity: item.quantity,
price: item.price,
})),
},
},
})
// 创建Stripe Checkout Session
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: items.map((item: any) => ({
price_data: {
currency: 'cny',
product_data: {
name: item.name,
images: [item.image],
},
unit_amount: Math.round(item.price * 100), // 转换为分
},
quantity: item.quantity,
})),
mode: 'payment',
success_url: `${process.env.NEXT_PUBLIC_URL}/order/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/cart`,
metadata: {
orderId: order.id,
},
})
// 更新订单的Stripe支付ID
await prisma.order.update({
where: { id: order.id },
data: { stripePaymentId: session.id },
})
return NextResponse.json({ sessionId: session.id })
} catch (error) {
console.error('Checkout error:', error)
return NextResponse.json(
{ error: 'Checkout failed' },
{ status: 500 }
)
}
}3. Stripe Webhook处理
// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
import { prisma } from '@/lib/prisma'
import { sendOrderConfirmationEmail } from '@/lib/email'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!
export async function POST(req: NextRequest) {
const body = await req.text()
const signature = req.headers.get('stripe-signature')!
let event: Stripe.Event
try {
// 验证webhook签名
event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
} catch (err) {
console.error('Webhook signature verification failed')
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
}
// 处理支付成功事件
if (event.type === 'checkout.session.completed') {
const session = event.data.object as Stripe.Checkout.Session
const orderId = session.metadata?.orderId
if (orderId) {
// 更新订单状态
const order = await prisma.order.update({
where: { id: orderId },
data: { status: 'PAID' },
include: {
user: true,
items: {
include: {
product: true,
},
},
},
})
// 更新商品库存
for (const item of order.items) {
await prisma.product.update({
where: { id: item.productId },
data: {
stock: {
decrement: item.quantity,
},
},
})
}
// 发送确认邮件
await sendOrderConfirmationEmail(order)
}
}
return NextResponse.json({ received: true })
}4. 商品搜索和筛选
// app/api/products/search/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET(req: NextRequest) {
const searchParams = req.nextUrl.searchParams
const query = searchParams.get('q') || ''
const category = searchParams.get('category')
const minPrice = Number(searchParams.get('minPrice')) || 0
const maxPrice = Number(searchParams.get('maxPrice')) || 999999
const sortBy = searchParams.get('sortBy') || 'createdAt'
const order = searchParams.get('order') || 'desc'
const page = Number(searchParams.get('page')) || 1
const limit = Number(searchParams.get('limit')) || 20
const where = {
AND: [
// 文本搜索
query
? {
OR: [
{ name: { contains: query, mode: 'insensitive' as const } },
{ description: { contains: query, mode: 'insensitive' as const } },
],
}
: {},
// 分类筛选
category ? { categoryId: category } : {},
// 价格范围
{
price: {
gte: minPrice,
lte: maxPrice,
},
},
// 只显示有库存的商品
{ stock: { gt: 0 } },
],
}
const [products, total] = await Promise.all([
prisma.product.findMany({
where,
include: {
category: true,
reviews: {
select: {
rating: true,
},
},
},
orderBy: {
[sortBy]: order,
},
skip: (page - 1) * limit,
take: limit,
}),
prisma.product.count({ where }),
])
// 计算平均评分
const productsWithRating = products.map((product) => ({
...product,
averageRating:
product.reviews.length > 0
? product.reviews.reduce((sum, r) => sum + r.rating, 0) /
product.reviews.length
: 0,
}))
return NextResponse.json({
products: productsWithRating,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
})
}5. 购物车组件
// components/cart/cart-dropdown.tsx
'use client'
import { useCartStore } from '@/stores/useCartStore'
import { Button } from '@/components/ui/button'
import { useRouter } from 'next/navigation'
import Image from 'next/image'
export function CartDropdown() {
const { items, removeItem, getTotalItems, getTotalPrice } = useCartStore()
const router = useRouter()
if (items.length === 0) {
return (
<div className="p-4 text-center">
<p className="text-gray-500">购物车是空的</p>
</div>
)
}
return (
<div className="w-80">
<div className="max-h-96 overflow-y-auto">
{items.map((item) => (
<div key={item.productId} className="flex gap-3 p-4 border-b">
<Image
src={item.image}
alt={item.name}
width={60}
height={60}
className="rounded object-cover"
/>
<div className="flex-1">
<h4 className="text-sm font-medium">{item.name}</h4>
<p className="text-sm text-gray-500">
¥{item.price} × {item.quantity}
</p>
</div>
<button
onClick={() => removeItem(item.productId)}
className="text-red-500 text-sm hover:text-red-700"
>
删除
</button>
</div>
))}
</div>
<div className="p-4 border-t">
<div className="flex justify-between mb-3">
<span>总计 ({getTotalItems()} 件):</span>
<span className="font-bold text-lg">¥{getTotalPrice().toFixed(2)}</span>
</div>
<Button
onClick={() => router.push('/checkout')}
className="w-full"
>
去结算
</Button>
</div>
</div>
)
}遇到的挑战
问题1:库存并发控制
描述:多用户同时购买同一商品时,可能出现超卖现象
解决方案:
- 使用数据库事务
- 乐观锁机制
- 库存预扣减
// 使用Prisma事务确保库存一致性
async function createOrder(items: CartItem[], userId: string) {
return await prisma.$transaction(async (tx) => {
// 检查库存并锁定
for (const item of items) {
const product = await tx.product.findUnique({
where: { id: item.productId },
})
if (!product || product.stock < item.quantity) {
throw new Error(`商品 ${item.name} 库存不足`)
}
// 扣减库存
await tx.product.update({
where: { id: item.productId },
data: {
stock: {
decrement: item.quantity,
},
},
})
}
// 创建订单
return await tx.order.create({
data: {
// ... 订单数据
},
})
})
}问题2:Stripe测试环境配置
描述:开发环境中测试支付流程复杂
解决方案:
- 使用Stripe测试模式
- 配置本地Webhook转发
- 使用Stripe CLI
# 安装Stripe CLI
brew install stripe/stripe-cli/stripe
# 登录
stripe login
# 转发webhook到本地
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# 使用测试卡号
# 4242 4242 4242 4242 (成功)
# 4000 0000 0000 0002 (失败)性能优化
-
图片优化
- 使用Next.js Image组件
- 响应式图片
- 懒加载
-
数据缓存
- React Query缓存商品数据
- 服务端缓存热门商品
- CDN缓存静态资源
-
数据库优化
- 为常用查询添加索引
- 使用数据库连接池
- 分页查询避免全表扫描
部署
# 环境变量配置
DATABASE_URL="postgresql://..."
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
NEXTAUTH_SECRET="..."
NEXTAUTH_URL="https://your-domain.com"使用Vercel部署,PostgreSQL使用Supabase托管。
后续计划
- 商品评论和评分系统
- 优惠券和促销活动
- 推荐算法
- 订单物流追踪
- 移动端优化
总结
这个项目实现了一个功能完整的电商平台,从商品管理到支付流程都有完整的实现。通过Stripe集成,学习了支付系统的设计;通过Zustand,实现了简洁的状态管理;通过Prisma事务,解决了库存并发问题。是一个非常好的全栈开发实践项目。