/** * 订单状态同步定时任务 API * GET /api/cron/sync-orders?secret=YOUR_SECRET * * 功能: * 1. 查询 'created' 状态的订单 * 2. 调用微信支付接口查询真实状态 * 3. 同步订单状态(paid / expired) * 4. 更新用户购买记录 * * 部署方式: * - 方式1: 使用 cron 定时调用此接口(推荐) * - 方式2: 使用 Vercel Cron(如果部署在 Vercel) * - 方式3: 使用 node-cron 在服务端定时执行 */ import { NextRequest, NextResponse } from 'next/server' import { query } from '@/lib/db' import crypto from 'crypto' // 触发同步的密钥(写死,仅用于防止误触,非高安全场景) const CRON_SECRET = 'soul_cron_sync_orders_2026' // 微信支付配置 const WECHAT_PAY_CONFIG = { appid: process.env.WECHAT_APPID || 'wxb8bbb2b10dec74aa', mchId: process.env.WECHAT_MCH_ID || '1318592501', apiKey: process.env.WECHAT_API_KEY || '', // 需要配置真实的 API Key } // 订单超时时间(分钟) const ORDER_TIMEOUT_MINUTES = 30 /** * 生成随机字符串 */ function generateNonceStr(length: number = 32): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' let result = '' for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)) } return result } /** * 生成微信支付签名 */ function createSign(params: Record, apiKey: string): string { // 1. 参数排序 const sortedKeys = Object.keys(params).sort() // 2. 拼接字符串 const stringA = sortedKeys .filter(key => params[key] !== undefined && params[key] !== '') .map(key => `${key}=${params[key]}`) .join('&') const stringSignTemp = `${stringA}&key=${apiKey}` // 3. MD5 加密并转大写 return crypto.createHash('md5').update(stringSignTemp, 'utf8').digest('hex').toUpperCase() } /** * 查询微信支付订单状态 */ async function queryWechatOrderStatus(outTradeNo: string): Promise { const url = 'https://api.mch.weixin.qq.com/pay/orderquery' const params = { appid: WECHAT_PAY_CONFIG.appid, mch_id: WECHAT_PAY_CONFIG.mchId, out_trade_no: outTradeNo, nonce_str: generateNonceStr(), } // 生成签名 const sign = createSign(params, WECHAT_PAY_CONFIG.apiKey) // 构建 XML 请求体 let xmlData = '' Object.entries({ ...params, sign }).forEach(([key, value]) => { xmlData += `<${key}>${value}` }) xmlData += '' try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/xml' }, body: xmlData, }) const respText = await response.text() // 简单解析 XML(生产环境建议用专业库) if (respText.includes('')) { if (respText.includes('')) { return 'SUCCESS' } else if (respText.includes('')) { return 'NOTPAY' } else if (respText.includes('')) { return 'CLOSED' } else if (respText.includes('')) { return 'REFUND' } } return 'UNKNOWN' } catch (error) { console.error('[SyncOrders] 查询微信订单失败:', error) return 'ERROR' } } /** * 主函数:同步订单状态 */ export async function GET(request: NextRequest) { const startTime = Date.now() // 1. 验证密钥 const { searchParams } = new URL(request.url) const secret = searchParams.get('secret') if (secret !== CRON_SECRET) { return NextResponse.json({ success: false, error: '未授权访问' }, { status: 401 }) } console.log('[SyncOrders] ========== 订单状态同步任务开始 ==========') try { // 2. 查询所有 'created' 状态的订单(最近 2 小时内) const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000) const pendingOrders = await query(` SELECT id, order_sn, user_id, product_type, product_id, amount, created_at FROM orders WHERE status = 'created' AND created_at >= ? ORDER BY created_at DESC `, [twoHoursAgo]) as any[] if (pendingOrders.length === 0) { console.log('[SyncOrders] 没有需要同步的订单') return NextResponse.json({ success: true, message: '没有需要同步的订单', synced: 0, expired: 0, duration: Date.now() - startTime }) } console.log(`[SyncOrders] 找到 ${pendingOrders.length} 个待同步订单`) let syncedCount = 0 let expiredCount = 0 let errorCount = 0 for (const order of pendingOrders) { const orderSn = order.order_sn const createdAt = new Date(order.created_at) const timeDiff = Date.now() - createdAt.getTime() const minutesDiff = Math.floor(timeDiff / (1000 * 60)) // 3. 判断订单是否超时 if (minutesDiff > ORDER_TIMEOUT_MINUTES) { console.log(`[SyncOrders] 订单 ${orderSn} 超时 (${minutesDiff} 分钟),标记为 expired`) await query(` UPDATE orders SET status = 'expired', updated_at = NOW() WHERE order_sn = ? `, [orderSn]) expiredCount++ continue } // 4. 查询微信支付状态(需要配置 API Key) if (!WECHAT_PAY_CONFIG.apiKey) { console.log(`[SyncOrders] 跳过订单 ${orderSn}(未配置 API Key)`) continue } const wechatStatus = await queryWechatOrderStatus(orderSn) if (wechatStatus === 'SUCCESS') { // 微信支付成功,更新为 paid console.log(`[SyncOrders] 订单 ${orderSn} 微信支付成功,更新为 paid`) await query(` UPDATE orders SET status = 'paid', updated_at = NOW() WHERE order_sn = ? `, [orderSn]) // 更新用户购买记录 if (order.product_type === 'fullbook') { await query(` UPDATE users SET has_full_book = 1 WHERE id = ? `, [order.user_id]) } syncedCount++ } else if (wechatStatus === 'NOTPAY') { console.log(`[SyncOrders] 订单 ${orderSn} 尚未支付`) } else if (wechatStatus === 'CLOSED') { console.log(`[SyncOrders] 订单 ${orderSn} 已关闭,标记为 cancelled`) await query(` UPDATE orders SET status = 'cancelled', updated_at = NOW() WHERE order_sn = ? `, [orderSn]) } else { console.log(`[SyncOrders] 订单 ${orderSn} 查询失败: ${wechatStatus}`) errorCount++ } } const duration = Date.now() - startTime console.log(`[SyncOrders] 同步完成: 同步 ${syncedCount} 个,超时 ${expiredCount} 个,失败 ${errorCount} 个`) console.log(`[SyncOrders] ========== 任务结束 (耗时 ${duration}ms) ==========`) return NextResponse.json({ success: true, message: '订单状态同步完成', total: pendingOrders.length, synced: syncedCount, expired: expiredCount, error: errorCount, duration }) } catch (error) { console.error('[SyncOrders] 同步失败:', error) return NextResponse.json({ success: false, error: '订单状态同步失败', detail: error instanceof Error ? error.message : String(error) }, { status: 500 }) } }