Files
soul-yongping/app/api/cron/sync-orders/route.ts

257 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 订单状态同步定时任务 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 })
}
}