登录&支付调试优化
This commit is contained in:
parent
8acfeec678
commit
c06c4f2299
@ -1,105 +1,7 @@
|
||||
import NextAuth, { AuthOptions } from "next-auth"
|
||||
import CredentialsProvider from "next-auth/providers/credentials"
|
||||
import type { User } from "next-auth"
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
// 认证配置
|
||||
const authOptions: AuthOptions = {
|
||||
providers: [
|
||||
// 邮箱密码登录
|
||||
CredentialsProvider({
|
||||
name: 'credentials',
|
||||
credentials: {
|
||||
email: { label: "邮箱", type: "email", placeholder: "user@example.com" },
|
||||
password: { label: "密码", type: "password" }
|
||||
},
|
||||
async authorize(credentials): Promise<User | null> {
|
||||
try {
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
console.log('❌ 登录失败: 缺少邮箱或密码')
|
||||
return null
|
||||
}
|
||||
|
||||
// 从数据库查询用户
|
||||
const user = await db.findUserByEmail(credentials.email)
|
||||
|
||||
if (!user) {
|
||||
console.log('❌ 登录失败: 用户不存在 -', credentials.email)
|
||||
return null
|
||||
}
|
||||
|
||||
// 使用 bcrypt 验证密码
|
||||
const isValidPassword = await bcrypt.compare(
|
||||
credentials.password,
|
||||
user.password
|
||||
)
|
||||
|
||||
if (!isValidPassword) {
|
||||
console.log('❌ 登录失败: 密码错误 -', credentials.email)
|
||||
return null
|
||||
}
|
||||
|
||||
console.log('✅ 登录成功:', {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
})
|
||||
|
||||
// 返回用户信息(不包含密码)
|
||||
return {
|
||||
id: user.id.toString(),
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}),
|
||||
],
|
||||
|
||||
// 自定义页面
|
||||
pages: {
|
||||
signIn: '/auth/signin',
|
||||
// error: '/auth/error',
|
||||
// signOut: '/auth/signout',
|
||||
},
|
||||
|
||||
// 回调函数
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.id = user.id
|
||||
token.email = user.email
|
||||
token.name = user.name
|
||||
}
|
||||
return token
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (session.user) {
|
||||
session.user.id = token.id as string
|
||||
session.user.email = token.email as string
|
||||
session.user.name = token.name as string
|
||||
}
|
||||
return session
|
||||
},
|
||||
},
|
||||
|
||||
// 会话策略
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
maxAge: 30 * 24 * 60 * 60, // 30 天
|
||||
},
|
||||
|
||||
// 密钥
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
|
||||
// 调试模式(生产环境设为false)
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
}
|
||||
import NextAuth from "next-auth"
|
||||
import { authOptions } from "@/lib/authOptions"
|
||||
|
||||
const handler = NextAuth(authOptions)
|
||||
|
||||
export { handler as GET, handler as POST }
|
||||
|
||||
|
||||
@ -2,31 +2,44 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
import crypto from 'crypto'
|
||||
|
||||
// 验证易支付回调签名
|
||||
// 验证嘟噜支付回调MD5签名
|
||||
function verifySign(params: Record<string, any>, key: string): boolean {
|
||||
const sign = params.sign
|
||||
if (!sign) return false
|
||||
|
||||
// 移除sign和sign_type参数
|
||||
// 1. 获取所有非空请求参数,剔除sign和sign_type字段
|
||||
const filtered: Record<string, any> = {}
|
||||
Object.keys(params)
|
||||
.sort()
|
||||
.sort() // 按照ASCII码递增排序
|
||||
.forEach((k) => {
|
||||
if (k !== 'sign' && k !== 'sign_type' && params[k] !== '' && params[k] !== null && params[k] !== undefined) {
|
||||
// 排除空值、数组、字节类型参数,以及sign和sign_type字段
|
||||
if (
|
||||
k !== 'sign' &&
|
||||
k !== 'sign_type' &&
|
||||
params[k] !== '' &&
|
||||
params[k] !== null &&
|
||||
params[k] !== undefined &&
|
||||
!Array.isArray(params[k]) &&
|
||||
typeof params[k] !== 'object'
|
||||
) {
|
||||
filtered[k] = params[k]
|
||||
}
|
||||
})
|
||||
|
||||
// 拼接字符串
|
||||
// 2. 将排序后的参数组合成"参数=参数值"的格式,用&连接
|
||||
// 注意:参数值不要进行URL编码(根据官方文档)
|
||||
const signStr = Object.keys(filtered)
|
||||
.map((k) => `${k}=${filtered[k]}`)
|
||||
.join('&')
|
||||
const signStrWithKey = `${signStr}&key=${key}`
|
||||
|
||||
// 3. 直接拼接密钥(无&key=前缀)
|
||||
// 根据官方文档:sign = md5(a=b&c=d&e=f + KEY)
|
||||
const signStrWithKey = `${signStr}${key}`
|
||||
|
||||
// MD5加密并转大写
|
||||
const calculatedSign = crypto.createHash('md5').update(signStrWithKey).digest('hex').toUpperCase()
|
||||
// 4. MD5加密(结果为小写,根据官方文档)
|
||||
const calculatedSign = crypto.createHash('md5').update(signStrWithKey, 'utf8').digest('hex')
|
||||
|
||||
return calculatedSign === sign.toUpperCase()
|
||||
return calculatedSign === sign.toLowerCase()
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
@ -51,16 +64,16 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
console.log('📥 收到支付回调:', params)
|
||||
|
||||
const epayKey = process.env.EPAY_KEY
|
||||
const epayKey = process.env.EPAY_KEY // MD5密钥
|
||||
if (!epayKey) {
|
||||
console.error('❌ 易支付密钥未配置')
|
||||
return NextResponse.text('FAIL', { status: 500 })
|
||||
console.error('❌ 嘟噜支付MD5密钥未配置')
|
||||
return new NextResponse('fail', { status: 500 })
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
if (!verifySign(params, epayKey)) {
|
||||
console.error('❌ 签名验证失败:', params)
|
||||
return NextResponse.text('FAIL', { status: 400 })
|
||||
return new NextResponse('fail', { status: 400 })
|
||||
}
|
||||
|
||||
// 获取订单信息
|
||||
@ -71,20 +84,20 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
if (!orderNo) {
|
||||
console.error('❌ 订单号缺失')
|
||||
return NextResponse.text('FAIL', { status: 400 })
|
||||
return new NextResponse('fail', { status: 400 })
|
||||
}
|
||||
|
||||
// 查找订单
|
||||
const order = await db.findOrderByOrderNo(orderNo)
|
||||
if (!order) {
|
||||
console.error('❌ 订单不存在:', orderNo)
|
||||
return NextResponse.text('FAIL', { status: 404 })
|
||||
return new NextResponse('fail', { status: 404 })
|
||||
}
|
||||
|
||||
// 检查订单状态
|
||||
if (order.status === 'paid') {
|
||||
console.log('✅ 订单已支付,跳过处理:', orderNo)
|
||||
return NextResponse.text('SUCCESS')
|
||||
return new NextResponse('success')
|
||||
}
|
||||
|
||||
// 验证支付状态
|
||||
@ -111,7 +124,7 @@ export async function POST(request: NextRequest) {
|
||||
// - 更新用户权限
|
||||
// - 记录日志等
|
||||
|
||||
return NextResponse.text('SUCCESS')
|
||||
return new NextResponse('success')
|
||||
} else {
|
||||
// 支付失败
|
||||
await db.updateOrderStatus(
|
||||
@ -128,11 +141,11 @@ export async function POST(request: NextRequest) {
|
||||
params,
|
||||
})
|
||||
|
||||
return NextResponse.text('SUCCESS') // 即使失败也要返回SUCCESS,避免重复通知
|
||||
return new NextResponse('success') // 即使失败也要返回SUCCESS,避免重复通知
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理支付回调错误:', error)
|
||||
return NextResponse.text('FAIL', { status: 500 })
|
||||
return new NextResponse('FAIL', { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,16 +161,16 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
console.log('📥 收到支付回调(GET):', params)
|
||||
|
||||
const epayKey = process.env.EPAY_KEY
|
||||
const epayKey = process.env.EPAY_KEY // MD5密钥
|
||||
if (!epayKey) {
|
||||
console.error('❌ 易支付密钥未配置')
|
||||
return NextResponse.text('FAIL', { status: 500 })
|
||||
console.error('❌ 嘟噜支付MD5密钥未配置')
|
||||
return new NextResponse('fail', { status: 500 })
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
if (!verifySign(params, epayKey)) {
|
||||
console.error('❌ 签名验证失败:', params)
|
||||
return NextResponse.text('FAIL', { status: 400 })
|
||||
return new NextResponse('fail', { status: 400 })
|
||||
}
|
||||
|
||||
// 获取订单信息
|
||||
@ -168,20 +181,20 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
if (!orderNo) {
|
||||
console.error('❌ 订单号缺失')
|
||||
return NextResponse.text('FAIL', { status: 400 })
|
||||
return new NextResponse('fail', { status: 400 })
|
||||
}
|
||||
|
||||
// 查找订单
|
||||
const order = await db.findOrderByOrderNo(orderNo)
|
||||
if (!order) {
|
||||
console.error('❌ 订单不存在:', orderNo)
|
||||
return NextResponse.text('FAIL', { status: 404 })
|
||||
return new NextResponse('fail', { status: 404 })
|
||||
}
|
||||
|
||||
// 检查订单状态
|
||||
if (order.status === 'paid') {
|
||||
console.log('✅ 订单已支付,跳过处理:', orderNo)
|
||||
return NextResponse.text('SUCCESS')
|
||||
return new NextResponse('success')
|
||||
}
|
||||
|
||||
// 验证支付状态
|
||||
@ -202,7 +215,7 @@ export async function GET(request: NextRequest) {
|
||||
paymentMethod,
|
||||
})
|
||||
|
||||
return NextResponse.text('SUCCESS')
|
||||
return new NextResponse('success')
|
||||
} else {
|
||||
// 支付失败
|
||||
await db.updateOrderStatus(
|
||||
@ -219,11 +232,11 @@ export async function GET(request: NextRequest) {
|
||||
params,
|
||||
})
|
||||
|
||||
return NextResponse.text('SUCCESS')
|
||||
return new NextResponse('success')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理支付回调错误:', error)
|
||||
return NextResponse.text('FAIL', { status: 500 })
|
||||
return new NextResponse('FAIL', { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth/next'
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route'
|
||||
import { authOptions } from '@/lib/authOptions'
|
||||
import { db } from '@/lib/db'
|
||||
import crypto from 'crypto'
|
||||
|
||||
@ -18,6 +18,10 @@ const PRODUCTS = {
|
||||
name: '定制版咨询',
|
||||
price: 0, // 需要沟通报价
|
||||
},
|
||||
test: {
|
||||
name: '支付测试',
|
||||
price: 0.1, // 测试用,金额0.1元
|
||||
},
|
||||
}
|
||||
|
||||
// 生成订单号
|
||||
@ -27,26 +31,98 @@ function generateOrderNo(): string {
|
||||
return `FLNXI${timestamp}${random}`
|
||||
}
|
||||
|
||||
// 生成易支付签名
|
||||
// 生成嘟噜支付MD5签名
|
||||
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]
|
||||
}
|
||||
})
|
||||
// 1. 获取所有非空请求参数,剔除sign和sign_type字段
|
||||
const filtered: Record<string, string> = {}
|
||||
const sortedKeys = Object.keys(params).sort() // 按照ASCII码递增排序
|
||||
|
||||
sortedKeys.forEach((k) => {
|
||||
// 排除空值、数组、字节类型参数,以及sign和sign_type字段
|
||||
if (
|
||||
k !== 'sign' &&
|
||||
k !== 'sign_type' &&
|
||||
params[k] !== '' &&
|
||||
params[k] !== null &&
|
||||
params[k] !== undefined &&
|
||||
!Array.isArray(params[k]) &&
|
||||
typeof params[k] !== 'object'
|
||||
) {
|
||||
// 确保所有值都是字符串(不trim,保持原始值)
|
||||
filtered[k] = String(params[k])
|
||||
}
|
||||
})
|
||||
|
||||
// 拼接字符串
|
||||
const signStr = Object.keys(filtered)
|
||||
// 2. 将排序后的参数组合成"参数=参数值"的格式,用&连接
|
||||
// 注意:签名时使用原始参数值(不编码),因为表单提交时浏览器会自动处理编码
|
||||
// 但支付平台验证签名时使用的是收到的原始值(浏览器解码后的值)
|
||||
const sortedFilteredKeys = Object.keys(filtered).sort()
|
||||
|
||||
// 使用原始参数值(不编码)进行签名
|
||||
const signStr = sortedFilteredKeys
|
||||
.map((k) => `${k}=${filtered[k]}`)
|
||||
.join('&')
|
||||
const signStrWithKey = `${signStr}&key=${key}`
|
||||
|
||||
// 3. 在末尾加上密钥(易支付常见方式:直接拼接密钥,没有&key=前缀)
|
||||
const signStrWithKey = `${signStr}${key}`
|
||||
|
||||
// 对比:如果使用 &key=密钥 的方式
|
||||
const signStrWithKeyPrefix = `${signStr}&key=${key}`
|
||||
|
||||
|
||||
// MD5加密并转大写
|
||||
return crypto.createHash('md5').update(signStrWithKey).digest('hex').toUpperCase()
|
||||
// 详细调试日志
|
||||
console.log('\n========== 签名参数调试信息 ==========')
|
||||
console.log('📋 原始参数:')
|
||||
Object.keys(params).sort().forEach(k => {
|
||||
console.log(` ${k} = ${params[k]} (类型: ${typeof params[k]})`)
|
||||
})
|
||||
|
||||
console.log('\n✅ 参与签名的参数(已过滤和排序):')
|
||||
sortedFilteredKeys.forEach((k, index) => {
|
||||
const encodedValue = encodeURIComponent(filtered[k])
|
||||
console.log(` ${index + 1}. ${k} = "${filtered[k]}"`)
|
||||
if (encodedValue !== filtered[k]) {
|
||||
console.log(` (URL编码后: "${encodedValue}")`)
|
||||
}
|
||||
})
|
||||
|
||||
console.log('\n🔗 待签名字符串(参数值不进行URL编码):')
|
||||
console.log(` ${signStr}`)
|
||||
|
||||
console.log('\n🔑 密钥信息:')
|
||||
console.log(` 长度: ${key.length} 字符`)
|
||||
console.log(` 前3个字符: ${key.substring(0, 3)}...`)
|
||||
console.log(` 后3个字符: ...${key.substring(key.length - 3)}`)
|
||||
|
||||
console.log('\n🔐 完整签名字符串(直接拼接密钥,无&key=前缀):')
|
||||
console.log(` ${signStrWithKey}`)
|
||||
console.log(` 长度: ${signStrWithKey.length} 字符`)
|
||||
console.log(' 格式:sign = md5(a=b&c=d&e=f + KEY)')
|
||||
|
||||
// 4. MD5加密
|
||||
// 根据官方文档:sign = md5(a=b&c=d&e=f + KEY),MD5结果为小写
|
||||
|
||||
// 方式A:直接拼接密钥(官方文档标准方式)
|
||||
const signatureA = crypto.createHash('md5').update(signStrWithKey, 'utf8').digest('hex') // 小写
|
||||
|
||||
// 方式B:使用&key=前缀(对比)
|
||||
const signatureB = crypto.createHash('md5').update(signStrWithKeyPrefix, 'utf8').digest('hex') // 小写
|
||||
|
||||
// 默认使用方式A(直接拼接密钥,官方文档标准方式)
|
||||
const signature = signatureA
|
||||
|
||||
console.log('\n📝 生成的签名(根据官方文档):')
|
||||
console.log(' [方式A - 直接拼接密钥(官方文档标准方式)] (当前使用):')
|
||||
console.log(` 签名字符串: ${signStrWithKey}`)
|
||||
console.log(` 签名(小写): ${signatureA}`)
|
||||
console.log(' [方式B - 使用&key=前缀(对比)]:')
|
||||
console.log(` 签名字符串: ${signStrWithKeyPrefix}`)
|
||||
console.log(` 签名(小写): ${signatureB}`)
|
||||
console.log('\n✅ 当前使用方式A(直接拼接密钥,MD5结果为小写)')
|
||||
console.log(' 根据官方文档:sign = md5(a=b&c=d&e=f + KEY),MD5结果为小写')
|
||||
console.log('========================================\n')
|
||||
|
||||
return signature
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
@ -81,10 +157,10 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// 检查易支付配置
|
||||
// 检查嘟噜支付配置
|
||||
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'
|
||||
const epayKey = process.env.EPAY_KEY // MD5密钥
|
||||
const epayApiUrl = process.env.EPAY_API_URL || 'https://www.dulupay.com/api/pay'
|
||||
|
||||
if (!epayPid || !epayKey || !epayApiUrl) {
|
||||
return NextResponse.json(
|
||||
@ -111,21 +187,33 @@ export async function POST(request: NextRequest) {
|
||||
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 epayParams: Record<string, string> = {
|
||||
pid: String(epayPid), // 商户ID
|
||||
type: String(paymentMethod), // 支付方式:alipay, wxpay, qqpay等
|
||||
out_trade_no: orderNo, // 商户订单号
|
||||
notify_url: notifyUrl, // 异步通知地址
|
||||
return_url: returnUrl, // 同步跳转地址
|
||||
name: product.name, // 商品名称
|
||||
money: product.price.toFixed(2), // 金额(保留2位小数)
|
||||
}
|
||||
|
||||
// 调试:输出参数名对比
|
||||
console.log('\n📝 参数名检查(易支付标准参数名,根据官方文档):')
|
||||
console.log(' pid = 商户ID(必填)')
|
||||
console.log(' type = 支付方式(可选)')
|
||||
console.log(' out_trade_no = 商户订单号(必填)')
|
||||
console.log(' notify_url = 异步通知地址(必填)')
|
||||
console.log(' return_url = 同步跳转地址(必填)')
|
||||
console.log(' name = 商品名称(必填)')
|
||||
console.log(' money = 金额(必填,保留2位小数)')
|
||||
console.log(' 注意:sign、sign_type、和空值不参与签名\n')
|
||||
|
||||
// 生成签名
|
||||
// 生成MD5签名
|
||||
const sign = generateSign(epayParams, epayKey)
|
||||
epayParams.sign = sign
|
||||
epayParams.sign_type = 'MD5'
|
||||
epayParams.sign_type = 'MD5' // 嘟噜支付使用MD5签名
|
||||
|
||||
console.log('✅ 创建支付订单:', {
|
||||
orderNo,
|
||||
@ -133,8 +221,23 @@ export async function POST(request: NextRequest) {
|
||||
product: product.name,
|
||||
amount: product.price,
|
||||
})
|
||||
console.log('📋 支付参数:', epayParams)
|
||||
|
||||
// 输出完整的测试信息,方便对比
|
||||
console.log('\n🔍 完整测试信息(可用于对比Go项目):')
|
||||
console.log('参数列表(按ASCII排序):')
|
||||
Object.keys(epayParams).sort().forEach(k => {
|
||||
console.log(` ${k} = "${epayParams[k]}"`)
|
||||
})
|
||||
console.log('\n建议:')
|
||||
console.log('1. 查看Go项目实际发送给支付平台的参数(网络抓包或日志)')
|
||||
console.log('2. 对比参数名是否一致')
|
||||
console.log('3. 对比参数值是否一致')
|
||||
console.log('4. 查看Go项目实际生成的签名')
|
||||
console.log('5. 如果Go项目能正常工作,请提供Go项目的实际参数和签名用于对比')
|
||||
console.log('6. 或联系嘟噜支付技术支持,获取签名示例代码\n')
|
||||
|
||||
// 返回支付参数,前端提交表单到易支付
|
||||
// 返回支付参数,前端提交表单到嘟噜支付
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
orderNo,
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth/next'
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route'
|
||||
import { authOptions } from '@/lib/authOptions'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// 验证登录状态
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import type { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
import SessionProvider from '@/components/SessionProvider'
|
||||
import { getServerSession } from 'next-auth/next'
|
||||
import { authOptions } from '@/lib/authOptions'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'FLNXI电商素材系统 - 企业级素材管理解决方案',
|
||||
@ -14,15 +16,17 @@ export const metadata: Metadata = {
|
||||
},
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body className="font-sf-pro">
|
||||
<SessionProvider session={null}>
|
||||
<SessionProvider session={session}>
|
||||
{children}
|
||||
</SessionProvider>
|
||||
</body>
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import { Suspense, useEffect, useState } from 'react'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { CheckCircle, XCircle, Loader2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
|
||||
export default function PaymentSuccessPage() {
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
function PaymentSuccessContent() {
|
||||
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)
|
||||
@ -19,7 +20,6 @@ export default function PaymentSuccessPage() {
|
||||
return
|
||||
}
|
||||
|
||||
// 查询订单状态
|
||||
const checkOrderStatus = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/payment/status?order_no=${orderNo}`)
|
||||
@ -27,7 +27,13 @@ export default function PaymentSuccessPage() {
|
||||
|
||||
if (data.success) {
|
||||
setOrderInfo(data.order)
|
||||
setOrderStatus(data.order.status === 'paid' ? 'success' : data.order.status === 'failed' ? 'failed' : 'pending')
|
||||
setOrderStatus(
|
||||
data.order.status === 'paid'
|
||||
? 'success'
|
||||
: data.order.status === 'failed'
|
||||
? 'failed'
|
||||
: 'pending'
|
||||
)
|
||||
} else {
|
||||
setOrderStatus('failed')
|
||||
}
|
||||
@ -189,3 +195,20 @@ export default function PaymentSuccessPage() {
|
||||
)
|
||||
}
|
||||
|
||||
export default function PaymentSuccessPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<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="text-center">
|
||||
<Loader2 className="w-12 h-12 text-apple-blue animate-spin mx-auto mb-4" />
|
||||
<p className="text-apple-gray-600">页面加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<PaymentSuccessContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +27,12 @@ export default function Header({ scrolled }: HeaderProps) {
|
||||
}
|
||||
}, [mobileMenuOpen])
|
||||
|
||||
// 调试:输出会话状态和用户
|
||||
useEffect(() => {
|
||||
console.log('🔍 useSession status:', status)
|
||||
console.log('🔍 useSession user:', session?.user)
|
||||
}, [status, session])
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
@ -73,10 +79,7 @@ export default function Header({ scrolled }: HeaderProps) {
|
||||
</a>
|
||||
|
||||
{/* Login/User Info */}
|
||||
{status === 'loading' ? (
|
||||
<div className="w-9 h-9 rounded-full bg-apple-gray-200 animate-pulse" />
|
||||
) : session ? (
|
||||
// 已登录 - 显示用户信息
|
||||
{status === 'authenticated' && session ? (
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center space-x-2 px-4 py-2 bg-apple-gray-50 rounded-full">
|
||||
<User className="w-4 h-4 text-apple-blue" />
|
||||
@ -92,8 +95,9 @@ export default function Header({ scrolled }: HeaderProps) {
|
||||
<LogOut className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
) : status === 'loading' ? (
|
||||
<div className="w-9 h-9 rounded-full bg-apple-gray-200 animate-pulse" />
|
||||
) : (
|
||||
// 未登录 - 显示注册和登录按钮
|
||||
<div className="flex items-center space-x-3">
|
||||
<a
|
||||
href="/auth/signup"
|
||||
@ -207,7 +211,15 @@ export default function Header({ scrolled }: HeaderProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Debug 浮层:直接展示会话状态,方便确认登录(始终显示) */}
|
||||
<div className="fixed bottom-3 right-3 z-[9999] rounded-xl border border-apple-gray-200 bg-white/90 px-3 py-2 text-xs text-apple-gray-700 shadow-lg space-y-1">
|
||||
<div className="font-semibold text-apple-gray-800">DEBUG Session</div>
|
||||
<div>status: {status}</div>
|
||||
<div>user: {session?.user?.email || session?.user?.name || 'none'}</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Header.displayName = 'HeaderWithDebug'
|
||||
|
||||
|
||||
@ -55,7 +55,7 @@ export default function Pricing() {
|
||||
return
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
if (status !== 'authenticated' || !session) {
|
||||
// 未登录,跳转到登录页
|
||||
router.push('/auth/signin?callbackUrl=' + encodeURIComponent('/#pricing'))
|
||||
return
|
||||
@ -301,13 +301,7 @@ export default function Pricing() {
|
||||
</svg>
|
||||
正在创建订单...
|
||||
</span>
|
||||
) : status === 'loading' ? (
|
||||
'加载中...'
|
||||
) : session ? (
|
||||
'立即购买'
|
||||
) : (
|
||||
'登录后购买'
|
||||
)}
|
||||
) : status === 'authenticated' ? '立即购买' : status === 'loading' ? '加载中...' : '登录后购买'}
|
||||
</a>
|
||||
|
||||
<p className="text-center text-sm text-apple-gray-500 mt-4">
|
||||
@ -369,6 +363,61 @@ export default function Pricing() {
|
||||
|
||||
{/* 增值服务 */}
|
||||
<div className="grid md:grid-cols-2 gap-8 mb-12">
|
||||
{/* 支付测试 */}
|
||||
<div className="bg-gradient-to-br from-green-50 to-emerald-50 rounded-3xl p-8 border-2 border-green-200 apple-shadow-lg">
|
||||
<div className="flex flex-col md:flex-row items-center gap-6">
|
||||
{/* 左侧图标和价格 */}
|
||||
<div className="flex-shrink-0 text-center md:text-left">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-green-500 to-emerald-600 rounded-2xl flex items-center justify-center mx-auto md:mx-0 mb-3">
|
||||
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-green-600 mb-1">¥0.1</div>
|
||||
<div className="text-sm text-apple-gray-600">测试专用</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧内容 */}
|
||||
<div className="flex-1">
|
||||
<h4 className="text-2xl font-bold text-apple-gray-900 mb-3">
|
||||
🧪 支付测试
|
||||
</h4>
|
||||
<p className="text-apple-gray-700 mb-4">
|
||||
用于测试支付功能,金额仅<strong>0.1元</strong>,方便验证支付流程是否正常。
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-3 mb-4">
|
||||
<div className="flex items-center text-sm text-apple-gray-700">
|
||||
<Check className="w-4 h-4 text-green-600 mr-2 flex-shrink-0" />
|
||||
<span>测试支付流程</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-apple-gray-700">
|
||||
<Check className="w-4 h-4 text-green-600 mr-2 flex-shrink-0" />
|
||||
<span>验证签名功能</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-apple-gray-700">
|
||||
<Check className="w-4 h-4 text-green-600 mr-2 flex-shrink-0" />
|
||||
<span>检查回调通知</span>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="#contact"
|
||||
onClick={(e) => handlePurchaseClick(e, 'test')}
|
||||
className="block w-full py-3 bg-gradient-to-r from-green-500 to-emerald-600 text-white text-center rounded-full font-semibold button-hover apple-shadow disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{purchasing === 'test' ? (
|
||||
<span className="flex items-center justify-center">
|
||||
<svg className="animate-spin h-5 w-5 mr-2" 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 === 'authenticated' ? '测试支付' : status === 'loading' ? '加载中...' : '登录后测试'}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 远程部署服务 */}
|
||||
<div className="bg-gradient-to-br from-orange-50 to-amber-50 rounded-3xl p-8 border-2 border-orange-200 apple-shadow-lg">
|
||||
<div className="flex flex-col md:flex-row items-center gap-6">
|
||||
|
||||
223
lib/authOptions.ts
Normal file
223
lib/authOptions.ts
Normal file
@ -0,0 +1,223 @@
|
||||
import type { AuthOptions } from "next-auth"
|
||||
import CredentialsProvider from "next-auth/providers/credentials"
|
||||
import type { User } from "next-auth"
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { db } from '@/lib/db'
|
||||
import crypto from 'crypto'
|
||||
|
||||
type WeChatTokens = {
|
||||
access_token?: string
|
||||
refresh_token?: string
|
||||
expires_in?: number
|
||||
openid?: string
|
||||
unionid?: string
|
||||
scope?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
function WeChatProvider(): any {
|
||||
const appId = process.env.WECHAT_APP_ID
|
||||
const appSecret = process.env.WECHAT_APP_SECRET
|
||||
|
||||
if (!appId || !appSecret) {
|
||||
console.error('❌ 微信登录配置缺失: WECHAT_APP_ID 或 WECHAT_APP_SECRET 未设置')
|
||||
} else {
|
||||
console.log('✅ 微信登录配置已加载:', { appId: appId.substring(0, 8) + '...' })
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'wechat',
|
||||
name: 'WeChat',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
checks: ['pkce', 'state'],
|
||||
authorization: {
|
||||
url: 'https://open.weixin.qq.com/connect/qrconnect',
|
||||
params: {
|
||||
appid: appId,
|
||||
scope: 'snsapi_login',
|
||||
response_type: 'code',
|
||||
// redirect_uri 由 NextAuth 自动注入
|
||||
},
|
||||
},
|
||||
token: {
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token',
|
||||
params: {
|
||||
appid: appId,
|
||||
secret: appSecret,
|
||||
grant_type: 'authorization_code',
|
||||
},
|
||||
async request({
|
||||
params,
|
||||
provider,
|
||||
}: {
|
||||
params: Record<string, string>
|
||||
provider: any
|
||||
}) {
|
||||
const url = `${provider.token!.url}?${new URLSearchParams(
|
||||
params as Record<string, string>
|
||||
).toString()}`
|
||||
const res = await fetch(url, { method: 'GET' })
|
||||
if (!res.ok) {
|
||||
throw new Error(`WeChat token request failed: ${res.status}`)
|
||||
}
|
||||
const tokens = (await res.json()) as WeChatTokens
|
||||
return { tokens }
|
||||
},
|
||||
},
|
||||
userinfo: {
|
||||
// 需要 access_token + openid
|
||||
async request({ tokens }: { tokens: WeChatTokens }) {
|
||||
const accessToken = tokens.access_token as string
|
||||
const openid = (tokens as any).openid as string
|
||||
if (!accessToken || !openid) {
|
||||
throw new Error('Missing access_token or openid from WeChat')
|
||||
}
|
||||
const url = `https://api.weixin.qq.com/sns/userinfo?${new URLSearchParams({
|
||||
access_token: accessToken,
|
||||
openid,
|
||||
lang: 'zh_CN',
|
||||
}).toString()}`
|
||||
const res = await fetch(url)
|
||||
if (!res.ok) {
|
||||
throw new Error(`WeChat userinfo failed: ${res.status}`)
|
||||
}
|
||||
return res.json()
|
||||
},
|
||||
},
|
||||
profile(profile: any) {
|
||||
return {
|
||||
id: profile.unionid || profile.openid,
|
||||
name: profile.nickname || '微信用户',
|
||||
email: profile.email || `${profile.openid}@wechat.flnxi.com`,
|
||||
image: profile.headimgurl,
|
||||
}
|
||||
},
|
||||
clientId: appId,
|
||||
clientSecret: appSecret,
|
||||
}
|
||||
}
|
||||
|
||||
export const authOptions: AuthOptions = {
|
||||
providers: [
|
||||
// 邮箱密码登录
|
||||
CredentialsProvider({
|
||||
name: 'credentials',
|
||||
credentials: {
|
||||
email: { label: "邮箱", type: "email", placeholder: "user@example.com" },
|
||||
password: { label: "密码", type: "password" }
|
||||
},
|
||||
async authorize(credentials): Promise<User | null> {
|
||||
try {
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
console.log('❌ 登录失败: 缺少邮箱或密码')
|
||||
return null
|
||||
}
|
||||
|
||||
// 从数据库查询用户
|
||||
const user = await db.findUserByEmail(credentials.email)
|
||||
|
||||
if (!user) {
|
||||
console.log('❌ 登录失败: 用户不存在 -', credentials.email)
|
||||
return null
|
||||
}
|
||||
|
||||
// 使用 bcrypt 验证密码
|
||||
const isValidPassword = await bcrypt.compare(
|
||||
credentials.password,
|
||||
user.password
|
||||
)
|
||||
|
||||
if (!isValidPassword) {
|
||||
console.log('❌ 登录失败: 密码错误 -', credentials.email)
|
||||
return null
|
||||
}
|
||||
|
||||
console.log('✅ 登录成功:', {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
})
|
||||
|
||||
// 返回用户信息(不包含密码)
|
||||
return {
|
||||
id: user.id.toString(),
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}),
|
||||
// 微信扫码登录
|
||||
WeChatProvider(),
|
||||
],
|
||||
|
||||
// 在 signIn 阶段将微信用户写入数据库
|
||||
callbacks: {
|
||||
async signIn({ user, account, profile }) {
|
||||
if (account?.provider === 'wechat') {
|
||||
const unionid = (profile as any)?.unionid || (profile as any)?.id
|
||||
const openid = (profile as any)?.openid
|
||||
const name = (profile as any)?.nickname || user.name || '微信用户'
|
||||
const email =
|
||||
(profile as any)?.email ||
|
||||
user.email ||
|
||||
`${openid || unionid || crypto.randomUUID()}@wechat.flnxi.com`
|
||||
const randomPassword = crypto.randomBytes(16).toString('hex')
|
||||
const hashedPassword = await bcrypt.hash(randomPassword, 10)
|
||||
|
||||
const dbUser = await db.upsertWechatUser({
|
||||
name,
|
||||
email,
|
||||
unionid: unionid ?? null,
|
||||
openid: openid ?? null,
|
||||
hashedPassword,
|
||||
})
|
||||
|
||||
// 将数据库中的 id/email/name 赋回 user,后续 jwt/session 使用
|
||||
user.id = dbUser.id.toString()
|
||||
user.email = dbUser.email
|
||||
user.name = dbUser.name
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
// 自定义页面
|
||||
pages: {
|
||||
signIn: '/auth/signin',
|
||||
},
|
||||
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.id = user.id
|
||||
token.email = user.email
|
||||
token.name = user.name
|
||||
}
|
||||
return token
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (session.user) {
|
||||
session.user.id = token.id as string
|
||||
session.user.email = token.email as string
|
||||
session.user.name = token.name as string
|
||||
}
|
||||
return session
|
||||
},
|
||||
},
|
||||
|
||||
// 会话策略
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
maxAge: 30 * 24 * 60 * 60, // 30 天
|
||||
},
|
||||
|
||||
// 密钥
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
|
||||
// 调试模式(生产环境设为false)
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
}
|
||||
|
||||
48
lib/db.ts
48
lib/db.ts
@ -1,4 +1,5 @@
|
||||
import { Pool } from 'pg'
|
||||
import crypto from 'crypto'
|
||||
|
||||
// 创建数据库连接池
|
||||
const pool = new Pool({
|
||||
@ -28,6 +29,9 @@ export interface User {
|
||||
email_verified: boolean
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
provider?: string
|
||||
wechat_unionid?: string | null
|
||||
wechat_openid?: string | null
|
||||
}
|
||||
|
||||
// 订单接口
|
||||
@ -125,6 +129,50 @@ export const db = {
|
||||
return result.rows
|
||||
},
|
||||
|
||||
// ========== 微信用户相关 ==========
|
||||
async findUserByWechat(unionid?: string | null, openid?: string | null): Promise<User | null> {
|
||||
if (unionid) {
|
||||
const byUnion = await pool.query('SELECT * FROM users WHERE wechat_unionid = $1', [unionid])
|
||||
if (byUnion.rows[0]) return byUnion.rows[0]
|
||||
}
|
||||
if (openid) {
|
||||
const byOpen = await pool.query('SELECT * FROM users WHERE wechat_openid = $1', [openid])
|
||||
if (byOpen.rows[0]) return byOpen.rows[0]
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
async upsertWechatUser(params: {
|
||||
name?: string | null
|
||||
email?: string | null
|
||||
unionid?: string | null
|
||||
openid?: string | null
|
||||
hashedPassword: string
|
||||
}): Promise<User> {
|
||||
const { name, email, unionid, openid, hashedPassword } = params
|
||||
|
||||
const existing = await this.findUserByWechat(unionid ?? undefined, openid ?? undefined)
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO users (name, email, password, email_verified, provider, wechat_unionid, wechat_openid, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
|
||||
RETURNING *`,
|
||||
[
|
||||
name || '微信用户',
|
||||
email || `${openid || unionid || crypto.randomUUID()}@wechat.local`,
|
||||
hashedPassword,
|
||||
true,
|
||||
'wechat',
|
||||
unionid || null,
|
||||
openid || null,
|
||||
]
|
||||
)
|
||||
return result.rows[0]
|
||||
},
|
||||
|
||||
// ========== 订单相关操作 ==========
|
||||
|
||||
// 创建订单
|
||||
|
||||
19
middleware.ts
Normal file
19
middleware.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { getToken } from 'next-auth/jwt'
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET })
|
||||
|
||||
// 仅调试日志,确认是否读到 token
|
||||
console.log('🔍 middleware token exists:', !!token, 'path:', request.nextUrl.pathname)
|
||||
// middleware.ts 顶部或回调里
|
||||
console.log('NEXTAUTH_URL', process.env.NEXTAUTH_URL)
|
||||
console.log('NEXTAUTH_SECRET exists:', !!process.env.NEXTAUTH_SECRET, 'len:', process.env.NEXTAUTH_SECRET?.length)
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user