本文最后更新于13 天前,其中的信息可能已经过时,如有错误请发送邮件到big_fw@foxmail.com
一、项目背景
开发一个能赚钱的网站:接入国内个人支付(无需营业执照)
为什么要做这个项目?
随着 AI 工具的普及,越来越多的人想要学习 AI 编程。但传统的编程学习曲线陡峭,让很多零基础学员望而却步。这个项目的目标是:
- 🎯 降低编程门槛 – 借助 AI 编程工具,让零基础学员也能快速上手
- 💻 实战驱动学习 – 通过实际项目练习,边做边学
- 🌐 社区化学习 – 建立学习社区,互相交流进步
- 💰 可持续运营 – 通过订阅制实现商业化,持续提供价值
二、技术栈
前端技术栈
{
"framework": "Next.js 14.2.x",
"language": "TypeScript 5.x",
"styling": "Tailwind CSS 3.x",
"animation": "AOS (Animate On Scroll)",
"font": "Inter + Cabinet Grotesk"
}
为什么选择 Next.js 14?
| 特性 | 优势 | 本项目应用 |
|---|---|---|
| App Router | 支持服务端组件,更好的性能 | 所有页面默认服务端渲染 |
| Layout 嵌套 | 复用 UI 结构 | (auth)、(default) 路由组 |
| Streaming | 流式渲染,首屏更快 | Dashboard 数据加载 |
| API Routes | 前后端一体化 | /api/checkout 支付接口 |
| Vercel 部署 | 零配置,自动优化 | 一键部署,全球 CDN |
后端技术栈
{
"database": "Supabase (PostgreSQL)",
"auth": "Supabase Auth",
"orm": "Supabase JS Client",
"payment": "ZPay 支付平台"
}
为什么选择 Supabase?
| 对比项 | Supabase | Firebase | 自建后端 |
|---|---|---|---|
| 开源 | ✅ 完全开源 | ❌ 闭源 | ✅ |
| 数据库 | PostgreSQL | NoSQL | 自选 |
| 实时订阅 | ✅ | ✅ | 需自己实现 |
| 行级权限 | ✅ RLS | ❌ | 需自己实现 |
| 国内访问 | ⚠️ 需代理 | ❌ 被墙 | ✅ |
| 价格 | 💰 免费额度高 | 💰 较贵 | 💰 服务器成本 |
三、项目架构
整体构架图

目录结构详解
happyaicoding-template-starter/
│
├── .env.local # 本地环境变量(不提交到 Git)
├── next.config.mjs # Next.js 配置
├── package.json # 依赖管理
├── tailwind.config.js # Tailwind 配置
├── tsconfig.json # TypeScript 配置
│
├── app/ # Next.js App Router
│ ├── layout.tsx # 根布局(全局字体、SEO)
│ │
│ ├── (default)/ # 默认路由组(公开页面)
│ │ ├── layout.tsx # 默认布局(Header + Footer)
│ │ ├── page.tsx # 首页
│ │ └── ... # 其他公开页面
│ │
│ ├── (auth)/ # 认证路由组(登录/注册)
│ │ ├── layout.tsx # 认证页面布局
│ │ ├── signin/
│ │ │ └── page.tsx # 登录页
│ │ ├── signup/
│ │ │ └── page.tsx # 注册页
│ │ └── reset-password/
│ │ └── page.tsx # 重置密码
│ │
│ ├── dashboard/ # 用户仪表盘(需登录)
│ │ ├── layout.tsx # 仪表盘布局
│ │ └── page.tsx # 仪表盘首页
│ │
│ ├── payment/ # 支付相关页面
│ │ ├── layout.tsx
│ │ └── success/
│ │ └── page.tsx # 支付成功页
│ │
│ ├── api/ # API 路由
│ │ ├── products/
│ │ │ └── route.ts # 产品列表接口
│ │ └── checkout/
│ │ └── providers/
│ │ └── zpay/
│ │ ├── url/
│ │ │ └── route.ts # 获取支付链接
│ │ └── webhook/
│ │ └── route.ts # 支付回调处理
│ │
│ └── auth/
│ └── callback/
│ └── route.ts # OAuth 回调处理
│
├── components/ # React 组件
│ ├── ui/ # UI 基础组件
│ │ ├── header.tsx # 头部导航
│ │ └── footer.tsx # 页脚
│ ├── hero.tsx # 首页 Hero 区域
│ ├── pricing.tsx # 定价组件
│ ├── faqs.tsx # FAQ 组件
│ └── ...
│
├── utils/
│ └── supabase/ # Supabase 工具函数
│ ├── server.ts # 服务端客户端(带 Cookie)
│ ├── client.ts # 客户端客户端
│ └── middleware.ts # 中间件认证逻辑
│
├── public/ # 静态资源
│ ├── fonts/ # 自定义字体
│ └── images/ # 图片资源
│
└── styles/
└── css/
└── style.css # 全局样式
四、核心功能实现
认证流转图

4.1 用户登录/注册认证
1. 功能描述
实现用户邮箱密码登录、注册功能,通过 Supabase Auth 管理用户身份,登录状态持久化存储,支持跨页面状态同步。
2. 实现思路
用户输入凭证 → Supabase Auth 验证 → 返回 JWT Token
→ 写入 HttpOnly Cookie → 客户端监听 auth 状态变化
→ 更新 UI 显示登录状态 → 受保护路由自动放行
关键点:
- 使用
createClient()创建客户端 Supabase 实例 - 通过
onAuthStateChange监听登录状态变化 - 登录成功后调用
router.refresh()刷新服务端 session
3. 关键代码
// app/(auth)/signin/page.tsx
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { createClient } from '@/utils/supabase/client'
export default function SignIn() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const router = useRouter()
const supabase = createClient()
const handleSignIn = async (e: React.FormEvent) => {
e.preventDefault()
setError(null)
setLoading(true)
try {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) throw error
// 登录成功,刷新页面获取新的 session
router.refresh()
router.push('/')
} catch (error: any) {
setError(error.message || '登录失败,请检查您的邮箱和密码')
} finally {
setLoading(false)
}
}
return (
<form onSubmit={handleSignIn}>
{error && <div className="error">{error}</div>}
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="邮箱" />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="密码" />
<button type="submit" disabled={loading}>
{loading ? '登录中...' : '登录账号'}
</button>
</form>
)
}
4. 效果展示
- 用户输入邮箱密码点击登录
- 登录中按钮显示加载状态
- 登录成功后跳转首页,Header 显示”个人中心”
- 登录失败时红色错误提示框显示错误信息
4.2 路由权限保护(Middleware)
1. 功能描述
通过 Next.js Middleware 在请求到达页面前拦截,检查用户登录状态,未登录用户自动重定向到登录页,已登录用户访问登录页自动跳转首页。
2. 实现思路
请求进入 → Middleware 拦截 → 创建 Supabase 客户端
→ 获取当前用户 Session → 判断路由类型
├─ 保护路由 (/dashboard) 且未登录 → 重定向 /signin
├─ 认证页面 (/signin) 且已登录 → 重定向 /
└─ 其他情况 → 放行
3. 关键代码
// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let response = NextResponse.next({ request: { headers: request.headers } })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) { return request.cookies.get(name)?.value },
set(name: string, value: string, options: any) {
request.cookies.set({ name, value, ...options })
response = NextResponse.next({ request: { headers: request.headers } })
response.cookies.set({ name, value, ...options })
},
},
}
)
// 刷新 session
await supabase.auth.getSession()
const { data: { user } } = await supabase.auth.getUser()
const protectedPaths = ['/dashboard', '/payment']
const authPaths = ['/signin', '/signup', '/reset-password']
// 已登录访问认证页面 → 跳转首页
if (authPaths.some(p => request.nextUrl.pathname.startsWith(p)) && user) {
return NextResponse.redirect(new URL('/', request.url))
}
// 未登录访问保护路由 → 跳转登录页
if (protectedPaths.some(p => request.nextUrl.pathname.startsWith(p)) && !user) {
return NextResponse.redirect(new URL('/signin', request.url))
}
return response
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
}
4. 效果展示
- 未登录访问
/dashboard→ 自动跳转到/signin - 登录后访问
/signin→ 自动跳转到/ - 保护路由无需在每个页面重复验证逻辑
4.3 支付链接获取接口
1. 功能描述
用户点击购买后,调用此接口生成支付订单,返回 ZPay 支付链接。支持订阅产品时间累加计算。
2. 实现思路
接收请求 → 验证用户登录 → 查询产品信息 → 生成订单号
→ 计算订阅时间(如有活跃订阅则累加)→ 构建支付参数
→ 生成 MD5 签名 → 保存交易记录 → 返回支付 URL
3. 关键代码
// app/api/checkout/providers/zpay/url/route.ts
export async function POST(request: NextRequest) {
const { productId, paymentMethod } = await request.json()
// 1. 验证用户登录
const supabase = createServerSupabaseClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ msg: '请先登录' }, { status: 401 })
}
// 2. 获取产品信息
const product = products[productId]
if (!product) {
return NextResponse.json({ msg: '产品不存在' }, { status: 404 })
}
// 3. 生成订单号
const outTradeNo = generateOutTradeNo()
// 4. 计算订阅时间(订阅产品自动累加)
let subscriptionEndDate: Date | null = null
if (product.isSubscription) {
const { data: existing } = await supabase
.from('zpay_transactions')
.select('subscription_end_date')
.eq('user_id', user.id)
.eq('trade_status', 'TRADE_SUCCESS')
.gt('subscription_end_date', new Date().toISOString())
.order('subscription_end_date', { ascending: false })
.limit(1)
if (existing && existing.length > 0) {
// 已有订阅,从结束日期累加
const currentEndDate = new Date(existing[0].subscription_end_date)
subscriptionEndDate = new Date(currentEndDate)
subscriptionEndDate.setMonth(subscriptionEndDate.getMonth() + 1)
} else {
// 新订阅,从现在开始
subscriptionEndDate = new Date()
subscriptionEndDate.setMonth(subscriptionEndDate.getMonth() + 1)
}
}
// 5. 生成签名
const signParams = { money, name: product.name, out_trade_no: outTradeNo, ... }
const sign = generateSign(signParams, process.env.ZPAY_KEY!)
// 6. 保存交易记录
await supabase.from('zpay_transactions').insert({
out_trade_no: outTradeNo,
user_id: user.id,
product_id: productId,
trade_status: 'WAIT_BUYER_PAY',
subscription_end_date: subscriptionEndDate?.toISOString(),
})
// 7. 返回支付链接
const paymentUrl = `https://zpayz.cn/submit.php?${params.toString()}`
return NextResponse.json({ code: 'success', paymentUrl })
}
4. 效果展示
- 用户在定价页点击”立即购买”
- 选择支付方式(支付宝/微信)
- 前端调用接口获取支付 URL
- 自动跳转/打开二维码完成支付
4.4 支付回调 Webhook
1. 功能描述
接收 ZPay 支付平台的异步通知,验证签名、更新订单状态、处理订阅时间,确保支付结果安全可靠。
2. 实现思路
收到回调 → 提取参数 → 验证签名 → 验证商户 ID
→ 检查支付状态 → 查询订单记录 → 验证金额一致性
→ 防止重复处理 → 更新订单状态 → 处理订阅逻辑
→ 返回 success 确认接收
3. 关键代码
// app/api/checkout/providers/zpay/webhook/route.ts
// 验证签名函数
function verifySign(params: Record<string, string>, key: string): boolean {
const { sign, ...restParams } = params
const filteredParams = Object.entries(restParams)
.filter(([_, v]) => v && v !== '')
.sort((a, b) => a[0].localeCompare(b[0]))
const prestr = filteredParams.map(([k, v]) => `${k}=${v}`).join('&')
const calculatedSign = crypto.createHash('md5').update(prestr + key).digest('hex')
return calculatedSign === sign.toLowerCase()
}
export async function GET(request: NextRequest) {
const params = Object.fromEntries(request.nextUrl.searchParams)
// 1. 验证签名
if (!verifySign(params, process.env.ZPAY_KEY!)) {
console.error('签名验证失败')
return new NextResponse('fail', { status: 200 })
}
// 2. 验证商户 ID
if (params.pid !== process.env.ZPAY_PID) {
return new NextResponse('fail', { status: 200 })
}
// 3. 检查支付状态
if (params.trade_status !== 'TRADE_SUCCESS') {
return new NextResponse('success', { status: 200 })
}
// 4. 查询订单
const supabase = createServerAdminClient()
const { data: order } = await supabase
.from('zpay_transactions')
.select('*')
.eq('out_trade_no', params.out_trade_no)
.single()
// 5. 验证金额一致性(防止"假通知")
const expectedMoney = parseFloat(order.money.toString())
const receivedMoney = parseFloat(params.money)
if (Math.abs(expectedMoney - receivedMoney) > 0.01) {
return new NextResponse('fail', { status: 200 })
}
// 6. 防止重复处理
if (order.notify_success && order.trade_status === 'TRADE_SUCCESS') {
return new NextResponse('success', { status: 200 })
}
// 7. 更新订单状态
await supabase.from('zpay_transactions').update({
zpay_trade_no: params.trade_no,
trade_status: 'TRADE_SUCCESS',
notify_success: true,
paid_at: new Date().toISOString(),
}).eq('out_trade_no', params.out_trade_no)
// 8. 处理订阅(更新用户权限)
if (order.is_subscription) {
// 可在此更新 user_subscriptions 表或 users.premium_expire_at
}
return new NextResponse('success', { status: 200 })
}
4. 效果展示
- 用户完成支付后,ZPay 后台自动发送回调通知
- 系统验证签名、更新订单状态
- Dashboard 显示”已支付”状态
- 订阅产品自动延长会员到期时间
4.5 Header 用户状态同步
1. 功能描述
全局 Header 组件实时显示用户登录状态,未登录显示”登录/注册”按钮,已登录显示”个人中心”入口,支持状态自动同步。
2. 实现思路
组件挂载 → 检查当前用户状态 → 监听 auth 状态变化
→ 状态变化自动更新 UI → 清理订阅(防止内存泄漏)
→ 路由预加载优化体验 → 导航时显示加载动画
3. 关键代码
// components/ui/header.tsx
"use client"
import { useState, useEffect } from 'react'
import { createClient } from '@/utils/supabase/client'
import { useRouter } from 'next/navigation'
export default function Header() {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
const [isNavigating, setIsNavigating] = useState(false)
const supabase = createClient()
const router = useRouter()
useEffect(() => {
// 检查登录状态
const checkUser = async () => {
const { data: { user } } = await supabase.auth.getUser()
setUser(user || null)
setLoading(false)
// 监听状态变化
const { data: { subscription } } = await supabase.auth.onAuthStateChange((_event, session) => {
setUser(session?.user || null)
})
return () => subscription.unsubscribe()
}
checkUser()
}, [])
const handleDashboardClick = (e: React.MouseEvent) => {
e.preventDefault()
setIsNavigating(true)
router.prefetch('/dashboard')
setTimeout(() => router.push('/dashboard'), 100)
}
return (
<header>
{loading ? (
// 骨架屏加载
<div className="h-8 w-20 bg-gray-200 rounded-md animate-pulse" />
) : user ? (
// 已登录
<a href="/dashboard" onClick={handleDashboardClick}>
{isNavigating ? <Spinner /> : '个人中心'}
</a>
) : (
// 未登录
<>
<Link href="/signin">登录</Link>
<Link href="/signup">注册</Link>
</>
)}
</header>
)
}
4. 效果展示
- 页面加载时 Header 显示骨架屏动画
- 加载完成后显示登录/注册按钮
- 登录后实时显示”个人中心”
- 点击个人中心显示加载 Spinner,预加载路由
4.6 订阅时间累加计算
1. 功能描述
用户购买订阅产品时,如果已有活跃订阅,新订阅时间从当前订阅结束日期开始累加,而不是从当前时间计算。
2. 实现思路
用户购买订阅 → 查询用户是否有未过期订阅
├─ 有 → 从订阅结束日期开始累加(月付 +1 月,年付 +1 年)
└─ 无 → 从现在开始计算
→ 保存新的订阅结束日期 → 返回支付链接
3. 关键代码
// 计算订阅结束日期
async function calculateSubscriptionEndDate(
userId: string,
productId: string,
subscriptionPeriod: 'monthly' | 'yearly'
) {
const supabase = createServerAdminClient()
// 查询是否有活跃订阅
const { data: existingTransactions } = await supabase
.from('zpay_transactions')
.select('subscription_end_date, trade_status')
.eq('user_id', userId)
.eq('product_id', productId)
.eq('trade_status', 'TRADE_SUCCESS')
.gt('subscription_end_date', new Date().toISOString())
.order('subscription_end_date', { ascending: false })
.limit(1)
let subscriptionEndDate: Date
if (existingTransactions && existingTransactions.length > 0) {
// 已有活跃订阅,从结束日期累加
const currentEndDate = new Date(existingTransactions[0].subscription_end_date)
subscriptionEndDate = new Date(currentEndDate)
if (subscriptionPeriod === 'monthly') {
subscriptionEndDate.setMonth(subscriptionEndDate.getMonth() + 1)
} else if (subscriptionPeriod === 'yearly') {
subscriptionEndDate.setFullYear(subscriptionEndDate.getFullYear() + 1)
}
} else {
// 新订阅,从现在开始
subscriptionEndDate = new Date()
if (subscriptionPeriod === 'monthly') {
subscriptionEndDate.setMonth(subscriptionEndDate.getMonth() + 1)
} else if (subscriptionPeriod === 'yearly') {
subscriptionEndDate.setFullYear(subscriptionEndDate.getFullYear() + 1)
}
}
return subscriptionEndDate
}
4. 效果展示
- 用户 1 月 1 日购买月卡,到期时间 2 月 1 日
- 用户 1 月 15 日再次购买月卡,到期时间自动变为 3 月 1 日(而非 2 月 15 日)
- Dashboard 显示”会员到期时间:2026-03-01″
五、安全机制
1. 认证安全
| 措施 | 说明 | 实现 |
|---|---|---|
| HttpOnly Cookie | 防止 XSS 攻击读取 | Supabase SSR 自动设置 |
| JWT 过期时间 | 降低令牌泄露风险 | 默认 1 小时 |
| Refresh Token | 无感知续期 | onAuthStateChange 监听 |
| 密码哈希 | 数据库不存明文 | Supabase Auth 内置 |
2. 支付安全
// 多层验证
1. 签名验证 → 确保请求来自 ZPay
2. 商户 ID 验证 → 确保通知发送给正确的商户
3. 金额验证 → 防止篡改支付金额
4. 订单号验证 → 确保订单存在
5. 防重复处理 → 避免重复发放权益
3. RLS 行级权限
-- 用户只能访问自己的数据
CREATE POLICY "select_own_data" ON users
FOR SELECT USING (auth.uid() = id);
CREATE POLICY "update_own_data" ON users
FOR UPDATE USING (auth.uid() = id);
-- 交易记录:用户可查看自己的记录
CREATE POLICY "select_own_transactions" ON zpay_transactions
FOR SELECT USING (auth.uid() = user_id);
4. 环境变量管理
# .env.local(本地开发)
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...
NEXT_PUBLIC_BASE_URL=http://localhost:3000
ZPAY_PID=xxx
ZPAY_KEY=xxx
# Vercel 环境变量(生产环境)
# 在 Dashboard → Settings → Environment Variables 中配置
# SUPABASE_SERVICE_ROLE_KEY 仅服务端可访问
六、部署与运维
Vercel 部署步骤
1. 安装 Vercel CLI
npm i -g vercel
2. 登录 Vercel
vercel login
3. 首次部署
vercel
4. 生产部署
vercel --prod
环境变量配置
| 变量名 | 环境 | 说明 |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | All | Supabase 项目 URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY | All | 匿名密钥(可暴露) |
SUPABASE_SERVICE_ROLE_KEY | Server Only | 管理员密钥(绝不可暴露) |
NEXT_PUBLIC_BASE_URL | All | 项目基础 URL |
ZPAY_PID | Server Only | 支付商户 ID |
ZPAY_KEY | Server Only | 支付签名密钥 |
数据库迁移
# 使用 Supabase CLI 进行迁移
supabase migration new create_users_table
supabase migration new create_transactions_table
supabase db push
监控与日志
// 错误日志记录
console.error('支付回调错误:', error);
// 性能监控(可接入)
// - Vercel Analytics
// - Supabase Query Stats
// - Sentry 错误追踪
七、性能优化
1. 路由预加载
// Dashboard 链接预加载
<a
href="/dashboard"
onMouseEnter={() => router.prefetch('/dashboard')}
onClick={() => router.push('/dashboard')}
>
个人中心
</a>
2. 组件懒加载
// 客户端组件按需加载
const DashboardClient = dynamic(() => import('./dashboard/client'), {
loading: () => <DashboardSkeleton />
});
3. 数据缓存
// API 响应缓存
const response = await fetch(url, {
cache: 'force-cache', // SSG
// next: { revalidate: 3600 } // ISR
});
4. 图片优化
// 使用 Next.js Image 组件
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={630}
priority
sizes="(max-width: 768px) 100vw, 1200px"
/>
5. 字体优化
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap', // 防止 FOIT
});
八、总结与展望
项目亮点总结
| 方面 | 实现 |
|---|---|
| 技术栈 | Next.js 14 + Supabase + TypeScript |
| 认证系统 | JWT + Cookie + RLS 三重保护 |
| 支付系统 | 完整的支付流程 + 安全验证 |
| 订阅管理 | 自动累加订阅时间 |
| 部署 | Vercel 一键部署 + 全球 CDN |
待优化功能
- 添加邮件通知(支付成功/订阅到期)
- 集成 Stripe 作为备选支付
- 添加管理员后台
- 数据看板(收入/用户统计)
- 博客系统(CMS 集成)
- 单元测试 + E2E 测试
遇到的问题
1.订阅逻辑错误,如果多次订阅,没有将最远的一次订阅作为开始时间?
查询用户是否有未过期的活跃订阅 --> 已有活跃订阅,从结束日期累加 --> 已有活跃订阅,从结束日期累加
GitHub: (https://github.com/qiuxuezhe345/template-payment-toturial)
作者: houzhibin
博客: (houzhibin.top)
发布时间: 2026 年 3 月
