257 lines
7.5 KiB
TypeScript
257 lines
7.5 KiB
TypeScript
/**
|
||
* 订单状态同步定时任务 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<string, any>, 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<string> {
|
||
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 = '<xml>'
|
||
Object.entries({ ...params, sign }).forEach(([key, value]) => {
|
||
xmlData += `<${key}>${value}</${key}>`
|
||
})
|
||
xmlData += '</xml>'
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/xml' },
|
||
body: xmlData,
|
||
})
|
||
|
||
const respText = await response.text()
|
||
|
||
// 简单解析 XML(生产环境建议用专业库)
|
||
if (respText.includes('<return_code><![CDATA[SUCCESS]]></return_code>')) {
|
||
if (respText.includes('<trade_state><![CDATA[SUCCESS]]></trade_state>')) {
|
||
return 'SUCCESS'
|
||
} else if (respText.includes('<trade_state><![CDATA[NOTPAY]]></trade_state>')) {
|
||
return 'NOTPAY'
|
||
} else if (respText.includes('<trade_state><![CDATA[CLOSED]]></trade_state>')) {
|
||
return 'CLOSED'
|
||
} else if (respText.includes('<trade_state><![CDATA[REFUND]]></trade_state>')) {
|
||
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 })
|
||
}
|
||
}
|