跳转到正文
文章目录

Next.js 商城完整结构指南


一、圆括号路由 (groupName) 是什么?

圆括号语法叫做 Route Groups(路由分组),作用是组织文件夹结构,但不影响 URL。

对比三种括号写法

写法 URL 影响 用途
[slug] ✅ 有,动态参数 /post/hello 动态路由,捕获参数
(group) ❌ 无,文件夹名不出现在 URL 逻辑分组,共享 layout
[...slug] ✅ 有,捕获多段 /a/b/c 捕获任意深度路径
[[...slug]] ✅ 有,可选捕获 参数可有可无

圆括号示例

</> PLAINTEXT
app/
├── (marketing)/          ← 括号不出现在 URL
│   ├── layout.tsx        ← 只作用于 marketing 组的 layout
│   ├── page.tsx          → /
│   └── about/
│       └── page.tsx      → /about
│
├── (shop)/               ← 括号不出现在 URL
│   ├── layout.tsx        ← 只作用于 shop 组的 layout(可加购物车栏)
│   ├── products/
│   │   └── page.tsx      → /products
│   └── cart/
│       └── page.tsx      → /cart

两个分组共享同一个域,但可以有各自独立的 layout,这是圆括号最大的价值。


二、渲染写在哪里?

渲染方式不是靠配置文件决定的,直接写在 page.tsx / 组件文件里,通过代码决定。

SSG(静态生成)— 默认行为

</> TSX
// app/products/page.tsx
// ★ 什么都不加 = 静态生成,构建时执行一次
export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products')
    .then(r => r.json())

  return (
    <ul>
      {products.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  )
}

SSR(服务端渲染)— 加 cache: 'no-store'

</> TSX
// app/orders/page.tsx
// ★ fetch 加 no-store = 每次请求都重新获取
export default async function OrdersPage() {
  const orders = await fetch('https://api.example.com/orders', {
    cache: 'no-store'   // ← 这一行决定了 SSR
  }).then(r => r.json())

  return <div>{/* 渲染订单 */}</div>
}

ISR(增量静态再生)— 加 revalidate

</> TSX
// app/products/[id]/page.tsx
// ★ 静态生成,但每 60 秒后台自动刷新
export const revalidate = 60  // ← 写在文件顶层,单位:秒

export default async function ProductPage({ params }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`)
    .then(r => r.json())

  return <div>{product.name}</div>
}

CSR(客户端渲染)— 加 'use client'

</> TSX
'use client'  // ← 文件第一行,决定在浏览器执行

import { useState } from 'react'

export default function AddToCartButton({ productId }) {
  const [added, setAdded] = useState(false)

  return (
    <button onClick={() => setAdded(true)}>
      {added ? '✅ 已加入购物车' : '加入购物车'}
    </button>
  )
}

渲染方式速查

决定因素 渲染方式
普通 async 函数 + fetch(默认) SSG
fetch 加 cache: 'no-store' SSR
文件顶部 export const revalidate = N ISR
文件顶部 'use client' CSR

三、Next.js 商城完整目录结构

</> PLAINTEXT
my-shop/
├── public/
│   └── images/                      # 商品图片等静态资源
│
├── src/
│   ├── app/
│   │   │
│   │   ├── (auth)/                  # 认证页面分组(独立布局,无顶部导航)
│   │   │   ├── layout.tsx           # 简洁的认证布局
│   │   │   ├── login/
│   │   │   │   └── page.tsx         → /login
│   │   │   ├── register/
│   │   │   │   └── page.tsx         → /register
│   │   │   └── forgot-password/
│   │   │       └── page.tsx         → /forgot-password
│   │   │
│   │   ├── (shop)/                  # 商城主体分组(带顶部导航 + 购物车图标)
│   │   │   ├── layout.tsx           # 含 Navbar / Footer 的主布局
│   │   │   │
│   │   │   ├── page.tsx             → /            首页(Banner + 推荐商品)
│   │   │   │
│   │   │   ├── products/
│   │   │   │   ├── page.tsx         → /products    商品列表(筛选、分页)
│   │   │   │   └── [id]/
│   │   │   │       └── page.tsx     → /products/42 商品详情
│   │   │   │
│   │   │   ├── categories/
│   │   │   │   └── [slug]/
│   │   │   │       └── page.tsx     → /categories/electronics  分类页
│   │   │   │
│   │   │   ├── cart/
│   │   │   │   └── page.tsx         → /cart        购物车页面
│   │   │   │
│   │   │   ├── checkout/
│   │   │   │   ├── page.tsx         → /checkout    填写收货信息
│   │   │   │   └── success/
│   │   │   │       └── page.tsx     → /checkout/success  支付成功页
│   │   │   │
│   │   │   └── account/             # 需要登录才能访问
│   │   │       ├── page.tsx         → /account     个人中心
│   │   │       ├── orders/
│   │   │       │   ├── page.tsx     → /account/orders      订单列表
│   │   │       │   └── [id]/
│   │   │       │       └── page.tsx → /account/orders/99   订单详情
│   │   │       └── profile/
│   │   │           └── page.tsx     → /account/profile     编辑资料
│   │   │
│   │   ├── (admin)/                 # 后台管理分组(独立布局,侧边栏导航)
│   │   │   ├── layout.tsx           # 管理后台布局
│   │   │   └── admin/
│   │   │       ├── page.tsx         → /admin       仪表盘
│   │   │       ├── products/
│   │   │       │   └── page.tsx     → /admin/products  商品管理
│   │   │       └── orders/
│   │   │           └── page.tsx     → /admin/orders    订单管理
│   │   │
│   │   ├── api/                     # 后端 API(Next.js Route Handlers)
│   │   │   ├── auth/
│   │   │   │   ├── [...nextauth]/
│   │   │   │   │   └── route.ts     # NextAuth 认证处理
│   │   │   │   └── register/
│   │   │   │       └── route.ts     # POST /api/auth/register
│   │   │   │
│   │   │   ├── products/
│   │   │   │   ├── route.ts         # GET /api/products(商品列表)
│   │   │   │   └── [id]/
│   │   │   │       └── route.ts     # GET/PUT/DELETE /api/products/[id]
│   │   │   │
│   │   │   ├── cart/
│   │   │   │   └── route.ts         # GET/POST/DELETE /api/cart
│   │   │   │
│   │   │   ├── orders/
│   │   │   │   ├── route.ts         # POST /api/orders(创建订单)
│   │   │   │   └── [id]/
│   │   │   │       └── route.ts     # GET /api/orders/[id]
│   │   │   │
│   │   │   └── payment/
│   │   │       ├── create/
│   │   │       │   └── route.ts     # POST /api/payment/create(发起支付)
│   │   │       └── webhook/
│   │   │           └── route.ts     # POST /api/payment/webhook(支付回调)
│   │   │
│   │   ├── layout.tsx               # 根布局(html/body/全局 Provider)
│   │   └── globals.css
│   │
│   ├── components/
│   │   ├── layout/
│   │   │   ├── Navbar.tsx           # 顶部导航(含购物车图标 + 用户头像)
│   │   │   └── Footer.tsx
│   │   │
│   │   ├── product/
│   │   │   ├── ProductCard.tsx      # 商品卡片(列表用)
│   │   │   ├── ProductGallery.tsx   # 商品图片轮播(详情页)
│   │   │   └── AddToCartButton.tsx  # 加入购物车按钮('use client')
│   │   │
│   │   ├── cart/
│   │   │   ├── CartDrawer.tsx       # 侧滑购物车抽屉('use client')
│   │   │   └── CartItem.tsx         # 购物车单项(修改数量/删除)
│   │   │
│   │   ├── checkout/
│   │   │   ├── AddressForm.tsx      # 收货地址表单('use client')
│   │   │   └── PaymentForm.tsx      # 支付方式选择('use client')
│   │   │
│   │   └── ui/
│   │       ├── Button.tsx           # 通用按钮
│   │       ├── Input.tsx            # 通用输入框
│   │       └── Modal.tsx            # 通用弹窗
│   │
│   ├── lib/
│   │   ├── auth.ts                  # NextAuth 配置
│   │   ├── db.ts                    # 数据库连接(Prisma)
│   │   ├── stripe.ts                # Stripe 支付 SDK 初始化
│   │   └── utils.ts                 # 工具函数(格式化价格等)
│   │
│   ├── store/
│   │   └── cartStore.ts             # Zustand 购物车全局状态
│   │
│   ├── types/
│   │   └── index.ts                 # TypeScript 类型定义
│   │
│   └── middleware.ts                # 路由守卫(保护需要登录的页面)
│
├── prisma/
│   └── schema.prisma                # 数据库模型定义
│
├── next.config.js
├── .env.local                       # 环境变量(数据库密码、API 密钥等)
└── package.json

四、关键功能实现详解

1. 登录认证(NextAuth)

src/lib/auth.ts — 配置登录方式

</> TS
import NextAuth from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import GoogleProvider from 'next-auth/providers/google'

export const authOptions = {
  providers: [
    // 账号密码登录
    CredentialsProvider({
      name: 'credentials',
      credentials: { email: {}, password: {} },
      async authorize(credentials) {
        const user = await db.user.findUnique({
          where: { email: credentials.email }
        })
        // 验证密码...
        return user
      }
    }),
    // Google 第三方登录
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    })
  ],
  pages: {
    signIn: '/login',   // 自定义登录页
  }
}

src/middleware.ts — 路由守卫,保护需要登录的页面

</> TS
import { withAuth } from 'next-auth/middleware'

export default withAuth({
  pages: { signIn: '/login' }
})

// 匹配这些路径时自动检查登录状态
export const config = {
  matcher: ['/account/:path*', '/checkout', '/admin/:path*']
}

2. 购物车(Zustand 全局状态)

src/store/cartStore.ts

</> TS
import { create } from 'zustand'
import { persist } from 'zustand/middleware'  // 持久化到 localStorage

type CartItem = {
  id: string
  name: string
  price: number
  quantity: number
  image: string
}

type CartStore = {
  items: CartItem[]
  addItem: (item: CartItem) => void
  removeItem: (id: string) => void
  updateQuantity: (id: string, quantity: number) => void
  clearCart: () => void
  totalPrice: () => number
}

export const useCartStore = create<CartStore>()(
  persist(
    (set, get) => ({
      items: [],

      addItem: (newItem) => set(state => {
        const existing = state.items.find(i => i.id === newItem.id)
        if (existing) {
          // 已有则数量 +1
          return {
            items: state.items.map(i =>
              i.id === newItem.id ? { ...i, quantity: i.quantity + 1 } : i
            )
          }
        }
        return { items: [...state.items, { ...newItem, quantity: 1 }] }
      }),

      removeItem: (id) => set(state => ({
        items: state.items.filter(i => i.id !== id)
      })),

      updateQuantity: (id, quantity) => set(state => ({
        items: state.items.map(i => i.id === id ? { ...i, quantity } : i)
      })),

      clearCart: () => set({ items: [] }),

      totalPrice: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
    }),
    { name: 'cart-storage' }  // localStorage key
  )
)

src/components/product/AddToCartButton.tsx — 加入购物车按钮

</> TSX
'use client'

import { useCartStore } from '@/store/cartStore'

export default function AddToCartButton({ product }) {
  const addItem = useCartStore(state => state.addItem)

  return (
    <button
      onClick={() => addItem({
        id: product.id,
        name: product.name,
        price: product.price,
        image: product.image,
        quantity: 1
      })}
      className="bg-blue-600 text-white px-6 py-2 rounded-lg"
    >
      加入购物车
    </button>
  )
}

3. 下单(创建订单 API)

src/app/api/orders/route.ts

</> TS
import { NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { db } from '@/lib/db'

export async function POST(request: Request) {
  // 验证登录
  const session = await getServerSession(authOptions)
  if (!session) {
    return NextResponse.json({ error: '请先登录' }, { status: 401 })
  }

  const { items, address } = await request.json()

  // 计算总价(在服务端重新计算,防止前端篡改价格)
  const productIds = items.map(i => i.id)
  const products = await db.product.findMany({
    where: { id: { in: productIds } }
  })
  const total = items.reduce((sum, item) => {
    const product = products.find(p => p.id === item.id)
    return sum + product.price * item.quantity
  }, 0)

  // 创建订单
  const order = await db.order.create({
    data: {
      userId: session.user.id,
      total,
      address: JSON.stringify(address),
      status: 'PENDING',           // 待支付
      items: {
        create: items.map(item => ({
          productId: item.id,
          quantity: item.quantity,
          price: products.find(p => p.id === item.id).price
        }))
      }
    }
  })

  return NextResponse.json({ orderId: order.id })
}

4. 支付(Stripe)

src/app/api/payment/create/route.ts — 创建支付会话

</> TS
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
import { db } from '@/lib/db'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(request: Request) {
  const { orderId } = await request.json()

  const order = await db.order.findUnique({
    where: { id: orderId },
    include: { items: { include: { product: true } } }
  })

  // 创建 Stripe 支付会话
  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: order.items.map(item => ({
      price_data: {
        currency: 'cny',
        product_data: { name: item.product.name },
        unit_amount: Math.round(item.price * 100),  // Stripe 用分
      },
      quantity: item.quantity,
    })),
    mode: 'payment',
    success_url: `${process.env.NEXT_PUBLIC_URL}/checkout/success?orderId=${orderId}`,
    cancel_url: `${process.env.NEXT_PUBLIC_URL}/cart`,
    metadata: { orderId }
  })

  return NextResponse.json({ url: session.url })  // 跳转到 Stripe 支付页
}

src/app/api/payment/webhook/route.ts — 支付回调(异步通知)

</> TS
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
import { db } from '@/lib/db'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(request: Request) {
  const body = await request.text()
  const sig = request.headers.get('stripe-signature')!

  // 验证回调来自 Stripe(防伪造)
  const event = stripe.webhooks.constructEvent(
    body, sig, process.env.STRIPE_WEBHOOK_SECRET!
  )

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object
    const orderId = session.metadata.orderId

    // 更新订单状态为已支付
    await db.order.update({
      where: { id: orderId },
      data: { status: 'PAID' }
    })
  }

  return NextResponse.json({ received: true })
}

五、数据库模型(Prisma)

prisma/schema.prisma

</> PRISMA
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  password  String?
  createdAt DateTime @default(now())
  orders    Order[]
}

model Product {
  id          String      @id @default(cuid())
  name        String
  description String
  price       Float
  stock       Int
  images      String[]
  category    String
  orderItems  OrderItem[]
}

model Order {
  id        String      @id @default(cuid())
  userId    String
  user      User        @relation(fields: [userId], references: [id])
  total     Float
  status    String      @default("PENDING")  // PENDING / PAID / SHIPPED / DONE
  address   String
  createdAt DateTime    @default(now())
  items     OrderItem[]
}

model OrderItem {
  id        String  @id @default(cuid())
  orderId   String
  order     Order   @relation(fields: [orderId], references: [id])
  productId String
  product   Product @relation(fields: [productId], references: [id])
  quantity  Int
  price     Float   // 下单时的价格快照
}

六、完整用户购物流程

</> PLAINTEXT
用户访问 /products
        ↓
浏览商品,点击「加入购物车」
→ AddToCartButton(客户端组件)
→ useCartStore.addItem()  写入全局状态(自动同步到 localStorage)
        ↓
点击购物车图标,进入 /cart
→ 展示所有 CartItem,可修改数量 / 删除
        ↓
点击「去结算」→ /checkout
→ 检查 middleware.ts 登录状态(未登录跳转 /login)
→ 填写收货地址(AddressForm)
        ↓
点击「提交订单」
→ POST /api/orders   在服务端创建订单(返回 orderId)
→ POST /api/payment/create  创建 Stripe 支付会话
→ 跳转到 Stripe 托管支付页面
        ↓
用户完成支付
→ Stripe 回调 POST /api/payment/webhook
→ 更新订单状态为 PAID
→ 跳转到 /checkout/success
        ↓
用户在 /account/orders 查看订单记录

七、主要依赖包

</> BASH
# 认证
npm install next-auth

# 数据库 ORM
npm install prisma @prisma/client

# 全局状态(购物车)
npm install zustand

# 支付
npm install stripe

# 表单验证
npm install react-hook-form zod

# UI 组件库
npm install @shadcn/ui

八、.env.local 环境变量

</> BASH
# 数据库
DATABASE_URL="postgresql://user:password@localhost:5432/myshop"

# NextAuth
NEXTAUTH_SECRET="随机字符串"
NEXTAUTH_URL="http://localhost:3000"

# Google 登录(可选)
GOOGLE_CLIENT_ID="..."
GOOGLE_CLIENT_SECRET="..."

# Stripe 支付
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..."

# 网站地址
NEXT_PUBLIC_URL="http://localhost:3000"

九、一句话总结各模块职责

模块 技术方案 职责
路由 App Router(文件即路由) 页面导航和 URL 管理
认证 NextAuth 登录 / 注册 / Session
路由守卫 middleware.ts 拦截未登录访问
购物车 Zustand + localStorage 跨页面持久化状态
下单 API Route + Prisma 服务端创建并校验订单
支付 Stripe Checkout 安全托管支付流程
数据库 PostgreSQL + Prisma 用户、商品、订单持久化

评论

搜索站内内容

输入关键词开始搜索