登录&支付调试优化
This commit is contained in:
parent
8acfeec678
commit
c06c4f2299
@ -1,105 +1,7 @@
|
|||||||
import NextAuth, { AuthOptions } from "next-auth"
|
import NextAuth from "next-auth"
|
||||||
import CredentialsProvider from "next-auth/providers/credentials"
|
import { authOptions } from "@/lib/authOptions"
|
||||||
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',
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = NextAuth(authOptions)
|
const handler = NextAuth(authOptions)
|
||||||
|
|
||||||
export { handler as GET, handler as POST }
|
export { handler as GET, handler as POST }
|
||||||
|
|
||||||
|
|||||||
@ -2,31 +2,44 @@ import { NextRequest, NextResponse } from 'next/server'
|
|||||||
import { db } from '@/lib/db'
|
import { db } from '@/lib/db'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
|
|
||||||
// 验证易支付回调签名
|
// 验证嘟噜支付回调MD5签名
|
||||||
function verifySign(params: Record<string, any>, key: string): boolean {
|
function verifySign(params: Record<string, any>, key: string): boolean {
|
||||||
const sign = params.sign
|
const sign = params.sign
|
||||||
if (!sign) return false
|
if (!sign) return false
|
||||||
|
|
||||||
// 移除sign和sign_type参数
|
// 1. 获取所有非空请求参数,剔除sign和sign_type字段
|
||||||
const filtered: Record<string, any> = {}
|
const filtered: Record<string, any> = {}
|
||||||
Object.keys(params)
|
Object.keys(params)
|
||||||
.sort()
|
.sort() // 按照ASCII码递增排序
|
||||||
.forEach((k) => {
|
.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]
|
filtered[k] = params[k]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 拼接字符串
|
// 2. 将排序后的参数组合成"参数=参数值"的格式,用&连接
|
||||||
|
// 注意:参数值不要进行URL编码(根据官方文档)
|
||||||
const signStr = Object.keys(filtered)
|
const signStr = Object.keys(filtered)
|
||||||
.map((k) => `${k}=${filtered[k]}`)
|
.map((k) => `${k}=${filtered[k]}`)
|
||||||
.join('&')
|
.join('&')
|
||||||
const signStrWithKey = `${signStr}&key=${key}`
|
|
||||||
|
|
||||||
// MD5加密并转大写
|
// 3. 直接拼接密钥(无&key=前缀)
|
||||||
const calculatedSign = crypto.createHash('md5').update(signStrWithKey).digest('hex').toUpperCase()
|
// 根据官方文档:sign = md5(a=b&c=d&e=f + KEY)
|
||||||
|
const signStrWithKey = `${signStr}${key}`
|
||||||
|
|
||||||
return calculatedSign === sign.toUpperCase()
|
// 4. MD5加密(结果为小写,根据官方文档)
|
||||||
|
const calculatedSign = crypto.createHash('md5').update(signStrWithKey, 'utf8').digest('hex')
|
||||||
|
|
||||||
|
return calculatedSign === sign.toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
@ -51,16 +64,16 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
console.log('📥 收到支付回调:', params)
|
console.log('📥 收到支付回调:', params)
|
||||||
|
|
||||||
const epayKey = process.env.EPAY_KEY
|
const epayKey = process.env.EPAY_KEY // MD5密钥
|
||||||
if (!epayKey) {
|
if (!epayKey) {
|
||||||
console.error('❌ 易支付密钥未配置')
|
console.error('❌ 嘟噜支付MD5密钥未配置')
|
||||||
return NextResponse.text('FAIL', { status: 500 })
|
return new NextResponse('fail', { status: 500 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证签名
|
// 验证签名
|
||||||
if (!verifySign(params, epayKey)) {
|
if (!verifySign(params, epayKey)) {
|
||||||
console.error('❌ 签名验证失败:', params)
|
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) {
|
if (!orderNo) {
|
||||||
console.error('❌ 订单号缺失')
|
console.error('❌ 订单号缺失')
|
||||||
return NextResponse.text('FAIL', { status: 400 })
|
return new NextResponse('fail', { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找订单
|
// 查找订单
|
||||||
const order = await db.findOrderByOrderNo(orderNo)
|
const order = await db.findOrderByOrderNo(orderNo)
|
||||||
if (!order) {
|
if (!order) {
|
||||||
console.error('❌ 订单不存在:', orderNo)
|
console.error('❌ 订单不存在:', orderNo)
|
||||||
return NextResponse.text('FAIL', { status: 404 })
|
return new NextResponse('fail', { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查订单状态
|
// 检查订单状态
|
||||||
if (order.status === 'paid') {
|
if (order.status === 'paid') {
|
||||||
console.log('✅ 订单已支付,跳过处理:', orderNo)
|
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 {
|
} else {
|
||||||
// 支付失败
|
// 支付失败
|
||||||
await db.updateOrderStatus(
|
await db.updateOrderStatus(
|
||||||
@ -128,11 +141,11 @@ export async function POST(request: NextRequest) {
|
|||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
|
|
||||||
return NextResponse.text('SUCCESS') // 即使失败也要返回SUCCESS,避免重复通知
|
return new NextResponse('success') // 即使失败也要返回SUCCESS,避免重复通知
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('处理支付回调错误:', 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)
|
console.log('📥 收到支付回调(GET):', params)
|
||||||
|
|
||||||
const epayKey = process.env.EPAY_KEY
|
const epayKey = process.env.EPAY_KEY // MD5密钥
|
||||||
if (!epayKey) {
|
if (!epayKey) {
|
||||||
console.error('❌ 易支付密钥未配置')
|
console.error('❌ 嘟噜支付MD5密钥未配置')
|
||||||
return NextResponse.text('FAIL', { status: 500 })
|
return new NextResponse('fail', { status: 500 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证签名
|
// 验证签名
|
||||||
if (!verifySign(params, epayKey)) {
|
if (!verifySign(params, epayKey)) {
|
||||||
console.error('❌ 签名验证失败:', params)
|
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) {
|
if (!orderNo) {
|
||||||
console.error('❌ 订单号缺失')
|
console.error('❌ 订单号缺失')
|
||||||
return NextResponse.text('FAIL', { status: 400 })
|
return new NextResponse('fail', { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找订单
|
// 查找订单
|
||||||
const order = await db.findOrderByOrderNo(orderNo)
|
const order = await db.findOrderByOrderNo(orderNo)
|
||||||
if (!order) {
|
if (!order) {
|
||||||
console.error('❌ 订单不存在:', orderNo)
|
console.error('❌ 订单不存在:', orderNo)
|
||||||
return NextResponse.text('FAIL', { status: 404 })
|
return new NextResponse('fail', { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查订单状态
|
// 检查订单状态
|
||||||
if (order.status === 'paid') {
|
if (order.status === 'paid') {
|
||||||
console.log('✅ 订单已支付,跳过处理:', orderNo)
|
console.log('✅ 订单已支付,跳过处理:', orderNo)
|
||||||
return NextResponse.text('SUCCESS')
|
return new NextResponse('success')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证支付状态
|
// 验证支付状态
|
||||||
@ -202,7 +215,7 @@ export async function GET(request: NextRequest) {
|
|||||||
paymentMethod,
|
paymentMethod,
|
||||||
})
|
})
|
||||||
|
|
||||||
return NextResponse.text('SUCCESS')
|
return new NextResponse('success')
|
||||||
} else {
|
} else {
|
||||||
// 支付失败
|
// 支付失败
|
||||||
await db.updateOrderStatus(
|
await db.updateOrderStatus(
|
||||||
@ -219,11 +232,11 @@ export async function GET(request: NextRequest) {
|
|||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
|
|
||||||
return NextResponse.text('SUCCESS')
|
return new NextResponse('success')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('处理支付回调错误:', 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 { NextRequest, NextResponse } from 'next/server'
|
||||||
import { getServerSession } from 'next-auth/next'
|
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 { db } from '@/lib/db'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
|
|
||||||
@ -18,6 +18,10 @@ const PRODUCTS = {
|
|||||||
name: '定制版咨询',
|
name: '定制版咨询',
|
||||||
price: 0, // 需要沟通报价
|
price: 0, // 需要沟通报价
|
||||||
},
|
},
|
||||||
|
test: {
|
||||||
|
name: '支付测试',
|
||||||
|
price: 0.1, // 测试用,金额0.1元
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成订单号
|
// 生成订单号
|
||||||
@ -27,26 +31,98 @@ function generateOrderNo(): string {
|
|||||||
return `FLNXI${timestamp}${random}`
|
return `FLNXI${timestamp}${random}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成易支付签名
|
// 生成嘟噜支付MD5签名
|
||||||
function generateSign(params: Record<string, any>, key: string): string {
|
function generateSign(params: Record<string, any>, key: string): string {
|
||||||
// 移除空值和sign参数
|
// 1. 获取所有非空请求参数,剔除sign和sign_type字段
|
||||||
const filtered: Record<string, any> = {}
|
const filtered: Record<string, string> = {}
|
||||||
Object.keys(params)
|
const sortedKeys = Object.keys(params).sort() // 按照ASCII码递增排序
|
||||||
.sort()
|
|
||||||
.forEach((k) => {
|
sortedKeys.forEach((k) => {
|
||||||
if (params[k] !== '' && params[k] !== null && params[k] !== undefined && k !== 'sign') {
|
// 排除空值、数组、字节类型参数,以及sign和sign_type字段
|
||||||
filtered[k] = params[k]
|
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])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 拼接字符串
|
// 2. 将排序后的参数组合成"参数=参数值"的格式,用&连接
|
||||||
const signStr = Object.keys(filtered)
|
// 注意:签名时使用原始参数值(不编码),因为表单提交时浏览器会自动处理编码
|
||||||
|
// 但支付平台验证签名时使用的是收到的原始值(浏览器解码后的值)
|
||||||
|
const sortedFilteredKeys = Object.keys(filtered).sort()
|
||||||
|
|
||||||
|
// 使用原始参数值(不编码)进行签名
|
||||||
|
const signStr = sortedFilteredKeys
|
||||||
.map((k) => `${k}=${filtered[k]}`)
|
.map((k) => `${k}=${filtered[k]}`)
|
||||||
.join('&')
|
.join('&')
|
||||||
const signStrWithKey = `${signStr}&key=${key}`
|
|
||||||
|
|
||||||
// MD5加密并转大写
|
// 3. 在末尾加上密钥(易支付常见方式:直接拼接密钥,没有&key=前缀)
|
||||||
return crypto.createHash('md5').update(signStrWithKey).digest('hex').toUpperCase()
|
const signStrWithKey = `${signStr}${key}`
|
||||||
|
|
||||||
|
// 对比:如果使用 &key=密钥 的方式
|
||||||
|
const signStrWithKeyPrefix = `${signStr}&key=${key}`
|
||||||
|
|
||||||
|
|
||||||
|
// 详细调试日志
|
||||||
|
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) {
|
export async function POST(request: NextRequest) {
|
||||||
@ -81,10 +157,10 @@ export async function POST(request: NextRequest) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查易支付配置
|
// 检查嘟噜支付配置
|
||||||
const epayPid = process.env.EPAY_PID
|
const epayPid = process.env.EPAY_PID
|
||||||
const epayKey = process.env.EPAY_KEY
|
const epayKey = process.env.EPAY_KEY // MD5密钥
|
||||||
const epayApiUrl = process.env.EPAY_API_URL || 'https://your-epay-domain.com/submit.php'
|
const epayApiUrl = process.env.EPAY_API_URL || 'https://www.dulupay.com/api/pay'
|
||||||
|
|
||||||
if (!epayPid || !epayKey || !epayApiUrl) {
|
if (!epayPid || !epayKey || !epayApiUrl) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@ -111,21 +187,33 @@ export async function POST(request: NextRequest) {
|
|||||||
const notifyUrl = `${baseUrl}/api/payment/callback`
|
const notifyUrl = `${baseUrl}/api/payment/callback`
|
||||||
const returnUrl = `${baseUrl}/payment/success?order_no=${orderNo}`
|
const returnUrl = `${baseUrl}/payment/success?order_no=${orderNo}`
|
||||||
|
|
||||||
// 构建易支付参数
|
// 构建嘟噜支付参数(根据官方文档)
|
||||||
const epayParams = {
|
// 注意:参数名需要与易支付平台要求的一致
|
||||||
pid: epayPid,
|
const epayParams: Record<string, string> = {
|
||||||
type: paymentMethod, // alipay, wxpay, qqpay等
|
pid: String(epayPid), // 商户ID
|
||||||
out_trade_no: orderNo,
|
type: String(paymentMethod), // 支付方式:alipay, wxpay, qqpay等
|
||||||
notify_url: notifyUrl,
|
out_trade_no: orderNo, // 商户订单号
|
||||||
return_url: returnUrl,
|
notify_url: notifyUrl, // 异步通知地址
|
||||||
name: product.name,
|
return_url: returnUrl, // 同步跳转地址
|
||||||
money: product.price.toFixed(2),
|
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)
|
const sign = generateSign(epayParams, epayKey)
|
||||||
epayParams.sign = sign
|
epayParams.sign = sign
|
||||||
epayParams.sign_type = 'MD5'
|
epayParams.sign_type = 'MD5' // 嘟噜支付使用MD5签名
|
||||||
|
|
||||||
console.log('✅ 创建支付订单:', {
|
console.log('✅ 创建支付订单:', {
|
||||||
orderNo,
|
orderNo,
|
||||||
@ -133,8 +221,23 @@ export async function POST(request: NextRequest) {
|
|||||||
product: product.name,
|
product: product.name,
|
||||||
amount: product.price,
|
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({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
orderNo,
|
orderNo,
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { getServerSession } from 'next-auth/next'
|
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 { db } from '@/lib/db'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// 验证登录状态
|
// 验证登录状态
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
import SessionProvider from '@/components/SessionProvider'
|
import SessionProvider from '@/components/SessionProvider'
|
||||||
|
import { getServerSession } from 'next-auth/next'
|
||||||
|
import { authOptions } from '@/lib/authOptions'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'FLNXI电商素材系统 - 企业级素材管理解决方案',
|
title: 'FLNXI电商素材系统 - 企业级素材管理解决方案',
|
||||||
@ -14,15 +16,17 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
|
const session = await getServerSession(authOptions)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<body className="font-sf-pro">
|
<body className="font-sf-pro">
|
||||||
<SessionProvider session={null}>
|
<SessionProvider session={session}>
|
||||||
{children}
|
{children}
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { Suspense, useEffect, useState } from 'react'
|
||||||
import { useSearchParams, useRouter } from 'next/navigation'
|
import { useSearchParams } from 'next/navigation'
|
||||||
import { CheckCircle, XCircle, Loader2 } from 'lucide-react'
|
import { CheckCircle, XCircle, Loader2 } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
|
||||||
export default function PaymentSuccessPage() {
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
function PaymentSuccessContent() {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const router = useRouter()
|
|
||||||
const orderNo = searchParams.get('order_no')
|
const orderNo = searchParams.get('order_no')
|
||||||
const [orderStatus, setOrderStatus] = useState<'loading' | 'success' | 'failed' | 'pending'>('loading')
|
const [orderStatus, setOrderStatus] = useState<'loading' | 'success' | 'failed' | 'pending'>('loading')
|
||||||
const [orderInfo, setOrderInfo] = useState<any>(null)
|
const [orderInfo, setOrderInfo] = useState<any>(null)
|
||||||
@ -19,7 +20,6 @@ export default function PaymentSuccessPage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询订单状态
|
|
||||||
const checkOrderStatus = async () => {
|
const checkOrderStatus = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/payment/status?order_no=${orderNo}`)
|
const response = await fetch(`/api/payment/status?order_no=${orderNo}`)
|
||||||
@ -27,7 +27,13 @@ export default function PaymentSuccessPage() {
|
|||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setOrderInfo(data.order)
|
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 {
|
} else {
|
||||||
setOrderStatus('failed')
|
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])
|
}, [mobileMenuOpen])
|
||||||
|
|
||||||
|
// 调试:输出会话状态和用户
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('🔍 useSession status:', status)
|
||||||
|
console.log('🔍 useSession user:', session?.user)
|
||||||
|
}, [status, session])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header
|
<header
|
||||||
@ -73,10 +79,7 @@ export default function Header({ scrolled }: HeaderProps) {
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
{/* Login/User Info */}
|
{/* Login/User Info */}
|
||||||
{status === 'loading' ? (
|
{status === 'authenticated' && session ? (
|
||||||
<div className="w-9 h-9 rounded-full bg-apple-gray-200 animate-pulse" />
|
|
||||||
) : session ? (
|
|
||||||
// 已登录 - 显示用户信息
|
|
||||||
<div className="flex items-center space-x-3">
|
<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">
|
<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" />
|
<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" />
|
<LogOut className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<div className="flex items-center space-x-3">
|
||||||
<a
|
<a
|
||||||
href="/auth/signup"
|
href="/auth/signup"
|
||||||
@ -207,7 +211,15 @@ export default function Header({ scrolled }: HeaderProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session) {
|
if (status !== 'authenticated' || !session) {
|
||||||
// 未登录,跳转到登录页
|
// 未登录,跳转到登录页
|
||||||
router.push('/auth/signin?callbackUrl=' + encodeURIComponent('/#pricing'))
|
router.push('/auth/signin?callbackUrl=' + encodeURIComponent('/#pricing'))
|
||||||
return
|
return
|
||||||
@ -301,13 +301,7 @@ export default function Pricing() {
|
|||||||
</svg>
|
</svg>
|
||||||
正在创建订单...
|
正在创建订单...
|
||||||
</span>
|
</span>
|
||||||
) : status === 'loading' ? (
|
) : status === 'authenticated' ? '立即购买' : status === 'loading' ? '加载中...' : '登录后购买'}
|
||||||
'加载中...'
|
|
||||||
) : session ? (
|
|
||||||
'立即购买'
|
|
||||||
) : (
|
|
||||||
'登录后购买'
|
|
||||||
)}
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<p className="text-center text-sm text-apple-gray-500 mt-4">
|
<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="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="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">
|
<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 { Pool } from 'pg'
|
||||||
|
import crypto from 'crypto'
|
||||||
|
|
||||||
// 创建数据库连接池
|
// 创建数据库连接池
|
||||||
const pool = new Pool({
|
const pool = new Pool({
|
||||||
@ -28,6 +29,9 @@ export interface User {
|
|||||||
email_verified: boolean
|
email_verified: boolean
|
||||||
created_at: Date
|
created_at: Date
|
||||||
updated_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
|
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