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

257 lines
7.5 KiB
TypeScript
Raw Normal View History

/**
* 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 })
}
}