微信登录&支付接入
This commit is contained in:
parent
13ce098db6
commit
8acfeec678
179
README_PAYMENT.md
Normal file
179
README_PAYMENT.md
Normal file
@ -0,0 +1,179 @@
|
||||
# 💳 支付功能集成说明
|
||||
|
||||
## 📋 功能概述
|
||||
|
||||
已实现完整的易支付集成功能,包括:
|
||||
- ✅ 订单创建和管理
|
||||
- ✅ 易支付接口对接
|
||||
- ✅ 支付回调处理
|
||||
- ✅ 支付结果页面
|
||||
- ✅ 订单状态查询
|
||||
|
||||
## 🗄️ 数据库配置
|
||||
|
||||
### 1. 创建订单表
|
||||
|
||||
执行以下 SQL 脚本创建订单表:
|
||||
|
||||
```bash
|
||||
# 如果使用 Docker PostgreSQL
|
||||
docker exec -i flnxi-postgres psql -U flnxi_user -d flnxi_db < scripts/create_orders_table.sql
|
||||
|
||||
# 如果使用系统 PostgreSQL
|
||||
psql -U flnxi_user -d flnxi_db -f scripts/create_orders_table.sql
|
||||
```
|
||||
|
||||
或者直接在数据库中执行 `scripts/create_orders_table.sql` 文件内容。
|
||||
|
||||
### 2. 表结构说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | SERIAL | 主键,自增 |
|
||||
| order_no | VARCHAR(64) | 订单号(唯一) |
|
||||
| user_id | INTEGER | 用户ID(外键) |
|
||||
| product_type | VARCHAR(32) | 产品类型:standard/deployment/custom |
|
||||
| product_name | VARCHAR(255) | 产品名称 |
|
||||
| amount | DECIMAL(10,2) | 订单金额 |
|
||||
| status | VARCHAR(32) | 订单状态:pending/paid/failed/cancelled |
|
||||
| payment_method | VARCHAR(32) | 支付方式 |
|
||||
| trade_no | VARCHAR(128) | 第三方交易号 |
|
||||
| callback_data | TEXT | 回调数据(JSON) |
|
||||
| created_at | TIMESTAMP | 创建时间 |
|
||||
| updated_at | TIMESTAMP | 更新时间 |
|
||||
| paid_at | TIMESTAMP | 支付时间 |
|
||||
|
||||
## ⚙️ 环境变量配置
|
||||
|
||||
在 `.env.local` 文件中添加以下配置:
|
||||
|
||||
```env
|
||||
# 易支付配置
|
||||
EPAY_PID=your_merchant_id # 易支付商户ID
|
||||
EPAY_KEY=your_merchant_key # 易支付商户密钥
|
||||
EPAY_API_URL=https://your-epay-domain.com/submit.php # 易支付接口地址
|
||||
|
||||
# NextAuth配置(如果还没有)
|
||||
NEXTAUTH_URL=https://your-domain.com # 网站域名
|
||||
NEXTAUTH_SECRET=your_secret_key # NextAuth密钥
|
||||
```
|
||||
|
||||
## 🔄 支付流程
|
||||
|
||||
### 1. 用户点击购买
|
||||
- 用户在产品页面点击"立即购买"
|
||||
- 系统检查登录状态,未登录则跳转登录页
|
||||
|
||||
### 2. 创建订单
|
||||
- 前端调用 `/api/payment/create`
|
||||
- 后端创建订单记录
|
||||
- 生成易支付支付参数和签名
|
||||
- 返回支付表单参数
|
||||
|
||||
### 3. 跳转支付
|
||||
- 前端自动提交表单到易支付
|
||||
- 用户完成支付
|
||||
|
||||
### 4. 支付回调
|
||||
- 易支付调用 `/api/payment/callback`
|
||||
- 验证签名
|
||||
- 更新订单状态
|
||||
- 返回 SUCCESS
|
||||
|
||||
### 5. 支付结果
|
||||
- 用户跳转到 `/payment/success`
|
||||
- 页面自动查询订单状态
|
||||
- 显示支付结果
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
app/
|
||||
├── api/
|
||||
│ └── payment/
|
||||
│ ├── create/
|
||||
│ │ └── route.ts # 创建支付订单接口
|
||||
│ ├── callback/
|
||||
│ │ └── route.ts # 支付回调接口
|
||||
│ └── status/
|
||||
│ └── route.ts # 查询订单状态接口
|
||||
├── payment/
|
||||
│ └── success/
|
||||
│ └── page.tsx # 支付结果页面
|
||||
lib/
|
||||
└── db.ts # 数据库操作(已扩展订单相关函数)
|
||||
components/
|
||||
└── Pricing.tsx # 定价组件(已集成支付功能)
|
||||
scripts/
|
||||
└── create_orders_table.sql # 订单表创建脚本
|
||||
```
|
||||
|
||||
## 🧪 测试步骤
|
||||
|
||||
### 1. 配置环境变量
|
||||
确保 `.env.local` 中配置了易支付相关信息。
|
||||
|
||||
### 2. 创建订单表
|
||||
执行数据库迁移脚本。
|
||||
|
||||
### 3. 测试支付流程
|
||||
1. 登录账户
|
||||
2. 访问首页,滚动到定价部分
|
||||
3. 点击"立即购买"(标准版或远程部署服务)
|
||||
4. 应该会跳转到易支付页面
|
||||
5. 完成支付后,会跳转回支付结果页面
|
||||
|
||||
### 4. 测试回调
|
||||
- 易支付会调用 `/api/payment/callback`
|
||||
- 检查订单状态是否更新为 `paid`
|
||||
|
||||
## 🔒 安全说明
|
||||
|
||||
1. **签名验证**:所有回调都会验证签名,防止伪造请求
|
||||
2. **订单验证**:回调时会验证订单是否存在
|
||||
3. **用户验证**:查询订单时会验证订单是否属于当前用户
|
||||
4. **幂等性**:已支付的订单不会重复处理
|
||||
|
||||
## 📝 产品配置
|
||||
|
||||
在 `app/api/payment/create/route.ts` 中可以配置产品信息:
|
||||
|
||||
```typescript
|
||||
const PRODUCTS = {
|
||||
standard: {
|
||||
name: 'FLNXI标准版',
|
||||
price: 599,
|
||||
},
|
||||
deployment: {
|
||||
name: '远程部署服务',
|
||||
price: 299,
|
||||
},
|
||||
custom: {
|
||||
name: '定制版咨询',
|
||||
price: 0, // 需要沟通报价
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### 1. 回调签名验证失败
|
||||
- 检查 `EPAY_KEY` 是否正确
|
||||
- 确认易支付平台使用的签名算法(当前使用MD5)
|
||||
|
||||
### 2. 订单状态未更新
|
||||
- 检查回调接口是否可访问
|
||||
- 查看服务器日志确认回调是否收到
|
||||
- 确认数据库连接正常
|
||||
|
||||
### 3. 支付后未跳转
|
||||
- 检查 `NEXTAUTH_URL` 配置是否正确
|
||||
- 确认易支付配置的回调地址正确
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
如有问题,请检查:
|
||||
1. 服务器日志:`/api/payment/callback` 的调用记录
|
||||
2. 数据库:订单表的数据是否正确
|
||||
3. 环境变量:易支付配置是否正确
|
||||
|
||||
229
app/api/payment/callback/route.ts
Normal file
229
app/api/payment/callback/route.ts
Normal file
@ -0,0 +1,229 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
import crypto from 'crypto'
|
||||
|
||||
// 验证易支付回调签名
|
||||
function verifySign(params: Record<string, any>, key: string): boolean {
|
||||
const sign = params.sign
|
||||
if (!sign) return false
|
||||
|
||||
// 移除sign和sign_type参数
|
||||
const filtered: Record<string, any> = {}
|
||||
Object.keys(params)
|
||||
.sort()
|
||||
.forEach((k) => {
|
||||
if (k !== 'sign' && k !== 'sign_type' && params[k] !== '' && params[k] !== null && params[k] !== undefined) {
|
||||
filtered[k] = params[k]
|
||||
}
|
||||
})
|
||||
|
||||
// 拼接字符串
|
||||
const signStr = Object.keys(filtered)
|
||||
.map((k) => `${k}=${filtered[k]}`)
|
||||
.join('&')
|
||||
const signStrWithKey = `${signStr}&key=${key}`
|
||||
|
||||
// MD5加密并转大写
|
||||
const calculatedSign = crypto.createHash('md5').update(signStrWithKey).digest('hex').toUpperCase()
|
||||
|
||||
return calculatedSign === sign.toUpperCase()
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
const params: Record<string, any> = {}
|
||||
|
||||
// 将FormData转换为对象
|
||||
formData.forEach((value, key) => {
|
||||
params[key] = value.toString()
|
||||
})
|
||||
|
||||
// 也支持JSON格式
|
||||
if (Object.keys(params).length === 0) {
|
||||
try {
|
||||
const json = await request.json()
|
||||
Object.assign(params, json)
|
||||
} catch {
|
||||
// 忽略JSON解析错误
|
||||
}
|
||||
}
|
||||
|
||||
console.log('📥 收到支付回调:', params)
|
||||
|
||||
const epayKey = process.env.EPAY_KEY
|
||||
if (!epayKey) {
|
||||
console.error('❌ 易支付密钥未配置')
|
||||
return NextResponse.text('FAIL', { status: 500 })
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
if (!verifySign(params, epayKey)) {
|
||||
console.error('❌ 签名验证失败:', params)
|
||||
return NextResponse.text('FAIL', { status: 400 })
|
||||
}
|
||||
|
||||
// 获取订单信息
|
||||
const orderNo = params.out_trade_no
|
||||
const tradeNo = params.trade_no
|
||||
const tradeStatus = params.trade_status
|
||||
const paymentMethod = params.type || params.payment_method
|
||||
|
||||
if (!orderNo) {
|
||||
console.error('❌ 订单号缺失')
|
||||
return NextResponse.text('FAIL', { status: 400 })
|
||||
}
|
||||
|
||||
// 查找订单
|
||||
const order = await db.findOrderByOrderNo(orderNo)
|
||||
if (!order) {
|
||||
console.error('❌ 订单不存在:', orderNo)
|
||||
return NextResponse.text('FAIL', { status: 404 })
|
||||
}
|
||||
|
||||
// 检查订单状态
|
||||
if (order.status === 'paid') {
|
||||
console.log('✅ 订单已支付,跳过处理:', orderNo)
|
||||
return NextResponse.text('SUCCESS')
|
||||
}
|
||||
|
||||
// 验证支付状态
|
||||
// 易支付通常 trade_status = 'TRADE_SUCCESS' 表示支付成功
|
||||
if (tradeStatus === 'TRADE_SUCCESS' || params.status === 'success' || params.code === '1') {
|
||||
// 更新订单状态
|
||||
await db.updateOrderStatus(
|
||||
orderNo,
|
||||
'paid',
|
||||
tradeNo,
|
||||
paymentMethod,
|
||||
JSON.stringify(params)
|
||||
)
|
||||
|
||||
console.log('✅ 订单支付成功:', {
|
||||
orderNo,
|
||||
tradeNo,
|
||||
amount: order.amount,
|
||||
paymentMethod,
|
||||
})
|
||||
|
||||
// TODO: 这里可以添加其他业务逻辑,比如:
|
||||
// - 发送邮件通知
|
||||
// - 更新用户权限
|
||||
// - 记录日志等
|
||||
|
||||
return NextResponse.text('SUCCESS')
|
||||
} else {
|
||||
// 支付失败
|
||||
await db.updateOrderStatus(
|
||||
orderNo,
|
||||
'failed',
|
||||
tradeNo,
|
||||
paymentMethod,
|
||||
JSON.stringify(params)
|
||||
)
|
||||
|
||||
console.log('❌ 订单支付失败:', {
|
||||
orderNo,
|
||||
tradeStatus,
|
||||
params,
|
||||
})
|
||||
|
||||
return NextResponse.text('SUCCESS') // 即使失败也要返回SUCCESS,避免重复通知
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理支付回调错误:', error)
|
||||
return NextResponse.text('FAIL', { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// 也支持GET请求(某些支付平台可能使用GET)
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const params: Record<string, any> = {}
|
||||
|
||||
searchParams.forEach((value, key) => {
|
||||
params[key] = value
|
||||
})
|
||||
|
||||
console.log('📥 收到支付回调(GET):', params)
|
||||
|
||||
const epayKey = process.env.EPAY_KEY
|
||||
if (!epayKey) {
|
||||
console.error('❌ 易支付密钥未配置')
|
||||
return NextResponse.text('FAIL', { status: 500 })
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
if (!verifySign(params, epayKey)) {
|
||||
console.error('❌ 签名验证失败:', params)
|
||||
return NextResponse.text('FAIL', { status: 400 })
|
||||
}
|
||||
|
||||
// 获取订单信息
|
||||
const orderNo = params.out_trade_no
|
||||
const tradeNo = params.trade_no
|
||||
const tradeStatus = params.trade_status
|
||||
const paymentMethod = params.type || params.payment_method
|
||||
|
||||
if (!orderNo) {
|
||||
console.error('❌ 订单号缺失')
|
||||
return NextResponse.text('FAIL', { status: 400 })
|
||||
}
|
||||
|
||||
// 查找订单
|
||||
const order = await db.findOrderByOrderNo(orderNo)
|
||||
if (!order) {
|
||||
console.error('❌ 订单不存在:', orderNo)
|
||||
return NextResponse.text('FAIL', { status: 404 })
|
||||
}
|
||||
|
||||
// 检查订单状态
|
||||
if (order.status === 'paid') {
|
||||
console.log('✅ 订单已支付,跳过处理:', orderNo)
|
||||
return NextResponse.text('SUCCESS')
|
||||
}
|
||||
|
||||
// 验证支付状态
|
||||
if (tradeStatus === 'TRADE_SUCCESS' || params.status === 'success' || params.code === '1') {
|
||||
// 更新订单状态
|
||||
await db.updateOrderStatus(
|
||||
orderNo,
|
||||
'paid',
|
||||
tradeNo,
|
||||
paymentMethod,
|
||||
JSON.stringify(params)
|
||||
)
|
||||
|
||||
console.log('✅ 订单支付成功:', {
|
||||
orderNo,
|
||||
tradeNo,
|
||||
amount: order.amount,
|
||||
paymentMethod,
|
||||
})
|
||||
|
||||
return NextResponse.text('SUCCESS')
|
||||
} else {
|
||||
// 支付失败
|
||||
await db.updateOrderStatus(
|
||||
orderNo,
|
||||
'failed',
|
||||
tradeNo,
|
||||
paymentMethod,
|
||||
JSON.stringify(params)
|
||||
)
|
||||
|
||||
console.log('❌ 订单支付失败:', {
|
||||
orderNo,
|
||||
tradeStatus,
|
||||
params,
|
||||
})
|
||||
|
||||
return NextResponse.text('SUCCESS')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理支付回调错误:', error)
|
||||
return NextResponse.text('FAIL', { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
153
app/api/payment/create/route.ts
Normal file
153
app/api/payment/create/route.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth/next'
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route'
|
||||
import { db } from '@/lib/db'
|
||||
import crypto from 'crypto'
|
||||
|
||||
// 产品配置
|
||||
const PRODUCTS = {
|
||||
standard: {
|
||||
name: 'FLNXI标准版',
|
||||
price: 599,
|
||||
},
|
||||
deployment: {
|
||||
name: '远程部署服务',
|
||||
price: 299,
|
||||
},
|
||||
custom: {
|
||||
name: '定制版咨询',
|
||||
price: 0, // 需要沟通报价
|
||||
},
|
||||
}
|
||||
|
||||
// 生成订单号
|
||||
function generateOrderNo(): string {
|
||||
const timestamp = Date.now()
|
||||
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0')
|
||||
return `FLNXI${timestamp}${random}`
|
||||
}
|
||||
|
||||
// 生成易支付签名
|
||||
function generateSign(params: Record<string, any>, key: string): string {
|
||||
// 移除空值和sign参数
|
||||
const filtered: Record<string, any> = {}
|
||||
Object.keys(params)
|
||||
.sort()
|
||||
.forEach((k) => {
|
||||
if (params[k] !== '' && params[k] !== null && params[k] !== undefined && k !== 'sign') {
|
||||
filtered[k] = params[k]
|
||||
}
|
||||
})
|
||||
|
||||
// 拼接字符串
|
||||
const signStr = Object.keys(filtered)
|
||||
.map((k) => `${k}=${filtered[k]}`)
|
||||
.join('&')
|
||||
const signStrWithKey = `${signStr}&key=${key}`
|
||||
|
||||
// MD5加密并转大写
|
||||
return crypto.createHash('md5').update(signStrWithKey).digest('hex').toUpperCase()
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// 验证登录状态
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session || !session.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ message: '请先登录' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { productType, paymentMethod = 'alipay' } = body
|
||||
|
||||
// 验证产品类型
|
||||
if (!productType || !PRODUCTS[productType as keyof typeof PRODUCTS]) {
|
||||
return NextResponse.json(
|
||||
{ message: '无效的产品类型' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const product = PRODUCTS[productType as keyof typeof PRODUCTS]
|
||||
|
||||
// 定制版需要特殊处理
|
||||
if (productType === 'custom') {
|
||||
return NextResponse.json(
|
||||
{ message: '定制版请联系客服咨询报价', redirect: '#contact' },
|
||||
{ status: 200 }
|
||||
)
|
||||
}
|
||||
|
||||
// 检查易支付配置
|
||||
const epayPid = process.env.EPAY_PID
|
||||
const epayKey = process.env.EPAY_KEY
|
||||
const epayApiUrl = process.env.EPAY_API_URL || 'https://your-epay-domain.com/submit.php'
|
||||
|
||||
if (!epayPid || !epayKey || !epayApiUrl) {
|
||||
return NextResponse.json(
|
||||
{ message: '支付系统未配置,请联系管理员' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
// 生成订单号
|
||||
const orderNo = generateOrderNo()
|
||||
const userId = parseInt(session.user.id)
|
||||
|
||||
// 创建订单
|
||||
const order = await db.createOrder(
|
||||
orderNo,
|
||||
userId,
|
||||
productType,
|
||||
product.name,
|
||||
product.price
|
||||
)
|
||||
|
||||
// 获取回调地址
|
||||
const baseUrl = process.env.NEXTAUTH_URL || request.headers.get('origin') || 'http://localhost:23000'
|
||||
const notifyUrl = `${baseUrl}/api/payment/callback`
|
||||
const returnUrl = `${baseUrl}/payment/success?order_no=${orderNo}`
|
||||
|
||||
// 构建易支付参数
|
||||
const epayParams = {
|
||||
pid: epayPid,
|
||||
type: paymentMethod, // alipay, wxpay, qqpay等
|
||||
out_trade_no: orderNo,
|
||||
notify_url: notifyUrl,
|
||||
return_url: returnUrl,
|
||||
name: product.name,
|
||||
money: product.price.toFixed(2),
|
||||
}
|
||||
|
||||
// 生成签名
|
||||
const sign = generateSign(epayParams, epayKey)
|
||||
epayParams.sign = sign
|
||||
epayParams.sign_type = 'MD5'
|
||||
|
||||
console.log('✅ 创建支付订单:', {
|
||||
orderNo,
|
||||
userId,
|
||||
product: product.name,
|
||||
amount: product.price,
|
||||
})
|
||||
|
||||
// 返回支付参数,前端提交表单到易支付
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
orderNo,
|
||||
orderId: order.id,
|
||||
paymentUrl: epayApiUrl,
|
||||
paymentParams: epayParams,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('创建支付订单错误:', error)
|
||||
return NextResponse.json(
|
||||
{ message: '创建订单失败,请稍后重试' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
68
app/api/payment/status/route.ts
Normal file
68
app/api/payment/status/route.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth/next'
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// 验证登录状态
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session || !session.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ message: '请先登录' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const orderNo = searchParams.get('order_no')
|
||||
|
||||
if (!orderNo) {
|
||||
return NextResponse.json(
|
||||
{ message: '订单号不能为空' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// 查找订单
|
||||
const order = await db.findOrderByOrderNo(orderNo)
|
||||
if (!order) {
|
||||
return NextResponse.json(
|
||||
{ message: '订单不存在' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// 验证订单是否属于当前用户
|
||||
const userId = parseInt(session.user.id)
|
||||
if (order.user_id !== userId) {
|
||||
return NextResponse.json(
|
||||
{ message: '无权访问此订单' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
order: {
|
||||
id: order.id,
|
||||
order_no: order.order_no,
|
||||
product_type: order.product_type,
|
||||
product_name: order.product_name,
|
||||
amount: order.amount,
|
||||
status: order.status,
|
||||
payment_method: order.payment_method,
|
||||
trade_no: order.trade_no,
|
||||
created_at: order.created_at,
|
||||
paid_at: order.paid_at,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('查询订单状态错误:', error)
|
||||
return NextResponse.json(
|
||||
{ message: '查询失败,请稍后重试' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { signIn } from 'next-auth/react'
|
||||
import { useState } from 'react'
|
||||
import { Mail, Lock, ArrowLeft, AlertCircle } from 'lucide-react'
|
||||
import { Mail, Lock, ArrowLeft, AlertCircle, QrCode } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
|
||||
@ -10,6 +10,7 @@ export default function SignInPage() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [wechatLoading, setWeChatLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
@ -37,6 +38,19 @@ export default function SignInPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleWeChatLogin = async () => {
|
||||
setError('')
|
||||
setWeChatLoading(true)
|
||||
try {
|
||||
await signIn('wechat', {
|
||||
callbackUrl: '/',
|
||||
})
|
||||
} catch (err) {
|
||||
setError('唤起微信登录失败,请稍后重试')
|
||||
setWeChatLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-apple-gray-50 to-white flex items-center justify-center px-4 py-12">
|
||||
<div className="max-w-md w-full">
|
||||
@ -154,6 +168,27 @@ export default function SignInPage() {
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* 第三方登录 */}
|
||||
<div className="mt-6 space-y-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-1 h-px bg-apple-gray-100" />
|
||||
<span className="text-xs text-apple-gray-400">或使用第三方登录</span>
|
||||
<div className="flex-1 h-px bg-apple-gray-100" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={wechatLoading}
|
||||
onClick={handleWeChatLogin}
|
||||
className="w-full py-3 flex items-center justify-center space-x-3 border-2 border-apple-gray-200 rounded-xl hover:border-apple-blue hover:text-apple-blue transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<QrCode className="w-5 h-5" />
|
||||
<span>{wechatLoading ? '正在拉起微信...' : '微信扫码登录'}</span>
|
||||
</button>
|
||||
<p className="text-xs text-apple-gray-500 text-center">
|
||||
请确保已在微信开放平台配置回调域名:`/api/auth/callback/wechat`
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 底部说明 */}
|
||||
<div className="mt-6 pt-6 border-t border-apple-gray-100 text-center text-sm text-apple-gray-600">
|
||||
<p>
|
||||
|
||||
191
app/payment/success/page.tsx
Normal file
191
app/payment/success/page.tsx
Normal file
@ -0,0 +1,191 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import { CheckCircle, XCircle, Loader2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
|
||||
export default function PaymentSuccessPage() {
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const orderNo = searchParams.get('order_no')
|
||||
const [orderStatus, setOrderStatus] = useState<'loading' | 'success' | 'failed' | 'pending'>('loading')
|
||||
const [orderInfo, setOrderInfo] = useState<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!orderNo) {
|
||||
setOrderStatus('failed')
|
||||
return
|
||||
}
|
||||
|
||||
// 查询订单状态
|
||||
const checkOrderStatus = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/payment/status?order_no=${orderNo}`)
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setOrderInfo(data.order)
|
||||
setOrderStatus(data.order.status === 'paid' ? 'success' : data.order.status === 'failed' ? 'failed' : 'pending')
|
||||
} else {
|
||||
setOrderStatus('failed')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询订单状态错误:', error)
|
||||
setOrderStatus('failed')
|
||||
}
|
||||
}
|
||||
|
||||
checkOrderStatus()
|
||||
|
||||
// 如果订单还在pending状态,每3秒轮询一次(最多轮询10次)
|
||||
let pollCount = 0
|
||||
const pollInterval = setInterval(() => {
|
||||
if (pollCount >= 10) {
|
||||
clearInterval(pollInterval)
|
||||
return
|
||||
}
|
||||
pollCount++
|
||||
checkOrderStatus()
|
||||
}, 3000)
|
||||
|
||||
return () => clearInterval(pollInterval)
|
||||
}, [orderNo])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-apple-gray-50 to-white flex items-center justify-center px-4 py-12">
|
||||
<div className="max-w-md w-full">
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="relative w-16 h-16">
|
||||
<Image
|
||||
src="/images/logo.png"
|
||||
alt="FLNXI Logo"
|
||||
fill
|
||||
className="object-contain"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 支付结果卡片 */}
|
||||
<div className="bg-white rounded-3xl apple-shadow-lg p-8">
|
||||
{orderStatus === 'loading' && (
|
||||
<div className="text-center py-8">
|
||||
<Loader2 className="w-16 h-16 text-apple-blue animate-spin mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-apple-gray-900 mb-2">
|
||||
正在确认支付结果...
|
||||
</h2>
|
||||
<p className="text-apple-gray-600">
|
||||
请稍候,我们正在验证您的支付信息
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{orderStatus === 'success' && (
|
||||
<div className="text-center py-8">
|
||||
<div className="w-16 h-16 bg-gradient-to-r from-green-500 to-emerald-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-apple-gray-900 mb-2">
|
||||
支付成功!
|
||||
</h2>
|
||||
<p className="text-apple-gray-600 mb-6">
|
||||
感谢您的购买,订单已确认
|
||||
</p>
|
||||
|
||||
{orderInfo && (
|
||||
<div className="bg-apple-gray-50 rounded-xl p-4 mb-6 text-left">
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-apple-gray-600">订单号:</span>
|
||||
<span className="font-mono font-semibold">{orderInfo.order_no}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-apple-gray-600">商品名称:</span>
|
||||
<span className="font-semibold">{orderInfo.product_name}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-apple-gray-600">支付金额:</span>
|
||||
<span className="font-bold text-green-600">¥{orderInfo.amount}</span>
|
||||
</div>
|
||||
{orderInfo.trade_no && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-apple-gray-600">交易号:</span>
|
||||
<span className="font-mono text-xs">{orderInfo.trade_no}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<Link
|
||||
href="/"
|
||||
className="block w-full py-3 bg-gradient-to-r from-apple-blue to-blue-600 text-white rounded-full font-semibold button-hover apple-shadow"
|
||||
>
|
||||
返回首页
|
||||
</Link>
|
||||
<p className="text-xs text-apple-gray-500">
|
||||
我们将在1-3个工作日内为您开通服务
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{orderStatus === 'pending' && (
|
||||
<div className="text-center py-8">
|
||||
<Loader2 className="w-16 h-16 text-orange-500 animate-spin mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-apple-gray-900 mb-2">
|
||||
支付处理中
|
||||
</h2>
|
||||
<p className="text-apple-gray-600 mb-6">
|
||||
您的订单已提交,正在等待支付确认
|
||||
</p>
|
||||
{orderNo && (
|
||||
<p className="text-xs text-apple-gray-500 font-mono mb-6">
|
||||
订单号:{orderNo}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-sm text-apple-gray-500">
|
||||
如果已完成支付,请稍候片刻,系统会自动更新订单状态
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{orderStatus === 'failed' && (
|
||||
<div className="text-center py-8">
|
||||
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<XCircle className="w-8 h-8 text-red-600" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-apple-gray-900 mb-2">
|
||||
支付失败
|
||||
</h2>
|
||||
<p className="text-apple-gray-600 mb-6">
|
||||
订单处理失败,请检查支付信息或联系客服
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<Link
|
||||
href="/#pricing"
|
||||
className="block w-full py-3 bg-gradient-to-r from-apple-blue to-blue-600 text-white rounded-full font-semibold button-hover apple-shadow"
|
||||
>
|
||||
重新购买
|
||||
</Link>
|
||||
<Link
|
||||
href="/#contact"
|
||||
className="block w-full py-3 border-2 border-apple-gray-200 text-apple-gray-700 rounded-full font-semibold hover:border-apple-blue hover:text-apple-blue transition-all"
|
||||
>
|
||||
联系客服
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { Check, Star, Sparkles, Zap, TrendingUp, AlertCircle } from 'lucide-react'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
|
||||
const freeFeatures = [
|
||||
{ text: '✅ 永久免费使用,0成本体验', highlight: true },
|
||||
@ -45,8 +46,9 @@ const customFeatures = [
|
||||
export default function Pricing() {
|
||||
const { data: session, status } = useSession()
|
||||
const router = useRouter()
|
||||
const [purchasing, setPurchasing] = useState<string | null>(null)
|
||||
|
||||
const handlePurchaseClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
const handlePurchaseClick = async (e: React.MouseEvent<HTMLAnchorElement>, productType: string) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (status === 'loading') {
|
||||
@ -55,10 +57,72 @@ export default function Pricing() {
|
||||
|
||||
if (!session) {
|
||||
// 未登录,跳转到登录页
|
||||
router.push('/auth/signin?callbackUrl=' + encodeURIComponent('/#contact'))
|
||||
} else {
|
||||
// 已登录,跳转到联系部分
|
||||
router.push('/auth/signin?callbackUrl=' + encodeURIComponent('/#pricing'))
|
||||
return
|
||||
}
|
||||
|
||||
// 定制版跳转到联系部分
|
||||
if (productType === 'custom') {
|
||||
window.location.href = '#contact'
|
||||
return
|
||||
}
|
||||
|
||||
// 创建支付订单
|
||||
setPurchasing(productType)
|
||||
try {
|
||||
const response = await fetch('/api/payment/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
productType,
|
||||
paymentMethod: 'alipay', // 默认支付宝,可以后续添加选择
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
if (data.redirect) {
|
||||
// 定制版等需要跳转的情况
|
||||
window.location.href = data.redirect
|
||||
return
|
||||
}
|
||||
alert(data.message || '创建订单失败,请稍后重试')
|
||||
setPurchasing(null)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.success && data.paymentUrl && data.paymentParams) {
|
||||
// 创建表单并提交到易支付
|
||||
const form = document.createElement('form')
|
||||
form.method = 'POST'
|
||||
form.action = data.paymentUrl
|
||||
form.target = '_blank'
|
||||
|
||||
// 添加支付参数
|
||||
Object.keys(data.paymentParams).forEach((key) => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'hidden'
|
||||
input.name = key
|
||||
input.value = data.paymentParams[key]
|
||||
form.appendChild(input)
|
||||
})
|
||||
|
||||
document.body.appendChild(form)
|
||||
form.submit()
|
||||
document.body.removeChild(form)
|
||||
|
||||
// 跳转到支付结果页面
|
||||
setTimeout(() => {
|
||||
router.push(`/payment/success?order_no=${data.orderNo}`)
|
||||
}, 1000)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建支付订单错误:', error)
|
||||
alert('创建订单失败,请稍后重试')
|
||||
setPurchasing(null)
|
||||
}
|
||||
}
|
||||
|
||||
@ -226,10 +290,24 @@ export default function Pricing() {
|
||||
{/* CTA Button */}
|
||||
<a
|
||||
href="#contact"
|
||||
onClick={handlePurchaseClick}
|
||||
className="block w-full py-4 bg-gradient-to-r from-apple-blue to-blue-600 text-white text-center rounded-full font-bold text-lg button-hover apple-shadow-lg cursor-pointer"
|
||||
onClick={(e) => handlePurchaseClick(e, 'standard')}
|
||||
className="block w-full py-4 bg-gradient-to-r from-apple-blue to-blue-600 text-white text-center rounded-full font-bold text-lg button-hover apple-shadow-lg cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{status === 'loading' ? '加载中...' : session ? '立即购买' : '登录后购买'}
|
||||
{purchasing === 'standard' ? (
|
||||
<span className="flex items-center justify-center">
|
||||
<svg className="animate-spin h-5 w-5 mr-3" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||
</svg>
|
||||
正在创建订单...
|
||||
</span>
|
||||
) : status === 'loading' ? (
|
||||
'加载中...'
|
||||
) : session ? (
|
||||
'立即购买'
|
||||
) : (
|
||||
'登录后购买'
|
||||
)}
|
||||
</a>
|
||||
|
||||
<p className="text-center text-sm text-apple-gray-500 mt-4">
|
||||
@ -277,7 +355,7 @@ export default function Pricing() {
|
||||
{/* CTA Button */}
|
||||
<a
|
||||
href="#contact"
|
||||
onClick={handlePurchaseClick}
|
||||
onClick={(e) => handlePurchaseClick(e, 'custom')}
|
||||
className="block w-full py-4 bg-gradient-to-r from-purple-500 to-pink-600 text-white text-center rounded-full font-bold text-lg button-hover apple-shadow-lg cursor-pointer"
|
||||
>
|
||||
{status === 'loading' ? '加载中...' : session ? '咨询定制方案' : '登录后咨询'}
|
||||
@ -347,10 +425,10 @@ export default function Pricing() {
|
||||
</div>
|
||||
<a
|
||||
href="#contact"
|
||||
onClick={handlePurchaseClick}
|
||||
className="px-6 py-2 bg-gradient-to-r from-orange-500 to-amber-600 text-white rounded-full font-semibold text-sm button-hover apple-shadow whitespace-nowrap"
|
||||
onClick={(e) => handlePurchaseClick(e, 'deployment')}
|
||||
className="px-6 py-2 bg-gradient-to-r from-orange-500 to-amber-600 text-white rounded-full font-semibold text-sm button-hover apple-shadow whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
立即购买
|
||||
{purchasing === 'deployment' ? '处理中...' : '立即购买'}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
94
lib/db.ts
94
lib/db.ts
@ -30,6 +30,23 @@ export interface User {
|
||||
updated_at: Date
|
||||
}
|
||||
|
||||
// 订单接口
|
||||
export interface Order {
|
||||
id: number
|
||||
order_no: string
|
||||
user_id: number
|
||||
product_type: string // 'standard' | 'deployment' | 'custom'
|
||||
product_name: string
|
||||
amount: number
|
||||
status: string // 'pending' | 'paid' | 'failed' | 'cancelled'
|
||||
payment_method?: string
|
||||
trade_no?: string
|
||||
callback_data?: string
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
paid_at?: Date
|
||||
}
|
||||
|
||||
// 数据库查询函数
|
||||
export const db = {
|
||||
// 根据邮箱查找用户
|
||||
@ -107,5 +124,82 @@ export const db = {
|
||||
)
|
||||
return result.rows
|
||||
},
|
||||
|
||||
// ========== 订单相关操作 ==========
|
||||
|
||||
// 创建订单
|
||||
async createOrder(
|
||||
orderNo: string,
|
||||
userId: number,
|
||||
productType: string,
|
||||
productName: string,
|
||||
amount: number
|
||||
): Promise<Order> {
|
||||
const result = await pool.query(
|
||||
'INSERT INTO orders (order_no, user_id, product_type, product_name, amount, status, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW()) RETURNING *',
|
||||
[orderNo, userId, productType, productName, amount, 'pending']
|
||||
)
|
||||
return result.rows[0]
|
||||
},
|
||||
|
||||
// 根据订单号查找订单
|
||||
async findOrderByOrderNo(orderNo: string): Promise<Order | null> {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM orders WHERE order_no = $1',
|
||||
[orderNo]
|
||||
)
|
||||
return result.rows[0] || null
|
||||
},
|
||||
|
||||
// 根据ID查找订单
|
||||
async findOrderById(id: number): Promise<Order | null> {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM orders WHERE id = $1',
|
||||
[id]
|
||||
)
|
||||
return result.rows[0] || null
|
||||
},
|
||||
|
||||
// 更新订单状态
|
||||
async updateOrderStatus(
|
||||
orderNo: string,
|
||||
status: string,
|
||||
tradeNo?: string,
|
||||
paymentMethod?: string,
|
||||
callbackData?: string
|
||||
): Promise<Order> {
|
||||
const updates = ['status = $2', 'updated_at = NOW()']
|
||||
const values: any[] = [orderNo, status]
|
||||
let index = 3
|
||||
|
||||
if (tradeNo) {
|
||||
updates.push(`trade_no = $${index++}`)
|
||||
values.push(tradeNo)
|
||||
}
|
||||
if (paymentMethod) {
|
||||
updates.push(`payment_method = $${index++}`)
|
||||
values.push(paymentMethod)
|
||||
}
|
||||
if (callbackData) {
|
||||
updates.push(`callback_data = $${index++}`)
|
||||
values.push(callbackData)
|
||||
}
|
||||
if (status === 'paid') {
|
||||
updates.push('paid_at = NOW()')
|
||||
}
|
||||
|
||||
const query = `UPDATE orders SET ${updates.join(', ')} WHERE order_no = $1 RETURNING *`
|
||||
const result = await pool.query(query, values)
|
||||
return result.rows[0]
|
||||
},
|
||||
|
||||
// 获取用户的订单列表
|
||||
async getUserOrders(userId: number): Promise<Order[]> {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM orders WHERE user_id = $1 ORDER BY created_at DESC',
|
||||
[userId]
|
||||
)
|
||||
return result.rows
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
2132
package-lock.json
generated
2132
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
scripts/create_orders_table.sql
Normal file
35
scripts/create_orders_table.sql
Normal file
@ -0,0 +1,35 @@
|
||||
-- 创建订单表
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id SERIAL PRIMARY KEY,
|
||||
order_no VARCHAR(64) UNIQUE NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
product_type VARCHAR(32) NOT NULL,
|
||||
product_name VARCHAR(255) NOT NULL,
|
||||
amount DECIMAL(10, 2) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
||||
payment_method VARCHAR(32),
|
||||
trade_no VARCHAR(128),
|
||||
callback_data TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
paid_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_order_no ON orders(order_no);
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_user_id ON orders(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_created_at ON orders(created_at);
|
||||
|
||||
-- 添加注释
|
||||
COMMENT ON TABLE orders IS '订单表';
|
||||
COMMENT ON COLUMN orders.order_no IS '订单号';
|
||||
COMMENT ON COLUMN orders.user_id IS '用户ID';
|
||||
COMMENT ON COLUMN orders.product_type IS '产品类型:standard/deployment/custom';
|
||||
COMMENT ON COLUMN orders.product_name IS '产品名称';
|
||||
COMMENT ON COLUMN orders.amount IS '订单金额';
|
||||
COMMENT ON COLUMN orders.status IS '订单状态:pending/paid/failed/cancelled';
|
||||
COMMENT ON COLUMN orders.payment_method IS '支付方式';
|
||||
COMMENT ON COLUMN orders.trade_no IS '第三方交易号';
|
||||
COMMENT ON COLUMN orders.callback_data IS '回调数据(JSON格式)';
|
||||
|
||||
10
types/next-auth.d.ts
vendored
10
types/next-auth.d.ts
vendored
@ -8,8 +8,9 @@ declare module "next-auth" {
|
||||
interface Session {
|
||||
user: {
|
||||
id: string
|
||||
email: string
|
||||
name: string
|
||||
email?: string | null
|
||||
name?: string | null
|
||||
provider?: string
|
||||
} & DefaultSession["user"]
|
||||
}
|
||||
|
||||
@ -18,6 +19,7 @@ declare module "next-auth" {
|
||||
*/
|
||||
interface User extends DefaultUser {
|
||||
id: string
|
||||
provider?: string
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +29,10 @@ declare module "next-auth/jwt" {
|
||||
*/
|
||||
interface JWT {
|
||||
id: string
|
||||
provider?: string
|
||||
picture?: string | null
|
||||
email?: string | null
|
||||
name?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user