diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 596eb53..0d23595 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -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 { - 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 } + diff --git a/app/api/payment/callback/route.ts b/app/api/payment/callback/route.ts index cc2ca9f..58404cf 100644 --- a/app/api/payment/callback/route.ts +++ b/app/api/payment/callback/route.ts @@ -2,31 +2,44 @@ import { NextRequest, NextResponse } from 'next/server' import { db } from '@/lib/db' import crypto from 'crypto' -// 验证易支付回调签名 +// 验证嘟噜支付回调MD5签名 function verifySign(params: Record, key: string): boolean { const sign = params.sign if (!sign) return false - // 移除sign和sign_type参数 + // 1. 获取所有非空请求参数,剔除sign和sign_type字段 const filtered: Record = {} 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 }) } } diff --git a/app/api/payment/create/route.ts b/app/api/payment/create/route.ts index c65aaed..b807a1d 100644 --- a/app/api/payment/create/route.ts +++ b/app/api/payment/create/route.ts @@ -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, key: string): string { - // 移除空值和sign参数 - const filtered: Record = {} - 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 = {} + 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 = { + 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, diff --git a/app/api/payment/status/route.ts b/app/api/payment/status/route.ts index 35a5491..d796a08 100644 --- a/app/api/payment/status/route.ts +++ b/app/api/payment/status/route.ts @@ -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 { // 验证登录状态 diff --git a/app/layout.tsx b/app/layout.tsx index 86e5d1e..5743152 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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 ( - + {children} diff --git a/app/payment/success/page.tsx b/app/payment/success/page.tsx index 0a00698..254ac4e 100644 --- a/app/payment/success/page.tsx +++ b/app/payment/success/page.tsx @@ -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(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 ( + +
+ +

页面加载中...

+
+ + } + > + +
+ ) +} + diff --git a/components/Header.tsx b/components/Header.tsx index aec14aa..7e5f7b4 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -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 ( <>
{/* Login/User Info */} - {status === 'loading' ? ( -
- ) : session ? ( - // 已登录 - 显示用户信息 + {status === 'authenticated' && session ? (
@@ -92,8 +95,9 @@ export default function Header({ scrolled }: HeaderProps) {
+ ) : status === 'loading' ? ( +
) : ( - // 未登录 - 显示注册和登录按钮
+ {/* Debug 浮层:直接展示会话状态,方便确认登录(始终显示) */} +
+
DEBUG Session
+
status: {status}
+
user: {session?.user?.email || session?.user?.name || 'none'}
+
) } +Header.displayName = 'HeaderWithDebug' + diff --git a/components/Pricing.tsx b/components/Pricing.tsx index 4414d71..a7f1521 100644 --- a/components/Pricing.tsx +++ b/components/Pricing.tsx @@ -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() { 正在创建订单... - ) : status === 'loading' ? ( - '加载中...' - ) : session ? ( - '立即购买' - ) : ( - '登录后购买' - )} + ) : status === 'authenticated' ? '立即购买' : status === 'loading' ? '加载中...' : '登录后购买'}

@@ -369,6 +363,61 @@ export default function Pricing() { {/* 增值服务 */}

+ {/* 支付测试 */} +
+
+ {/* 左侧图标和价格 */} +
+
+ + + +
+
¥0.1
+
测试专用
+
+ + {/* 右侧内容 */} + +
+
+ {/* 远程部署服务 */}
diff --git a/lib/authOptions.ts b/lib/authOptions.ts new file mode 100644 index 0000000..d14a3c2 --- /dev/null +++ b/lib/authOptions.ts @@ -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 + provider: any + }) { + const url = `${provider.token!.url}?${new URLSearchParams( + params as Record + ).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 { + 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', +} + diff --git a/lib/db.ts b/lib/db.ts index 1890af0..95b2fcd 100644 --- a/lib/db.ts +++ b/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 { + 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 { + 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] + }, + // ========== 订单相关操作 ========== // 创建订单 diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..6b6a076 --- /dev/null +++ b/middleware.ts @@ -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).*)'], +} +