Files
soul-yongping/next-project/app/api/cron/unbind-expired/route.ts
2026-02-09 14:43:35 +08:00

153 lines
5.1 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/unbind-expired?secret=YOUR_SECRET
*
* 功能:
* 1. 查询 status = 'active' 且 expiry_date < NOW() 且 purchase_count = 0 的绑定
* 2. 批量更新为 status = 'expired'
* 3. 更新推荐人的 referral_count减少
*
* 调用方式:
* - 宝塔面板计划任务每30分钟执行
* curl "https://soul.quwanzhi.com/api/cron/unbind-expired?secret=soul_cron_unbind_2026"
*
* 规则说明:
* - 只解绑「活跃状态 + 已过期 + 从未购买」的绑定关系
* - 如果用户购买过purchase_count > 0即使过期也不解绑
* - 这样可以保留有价值的推荐关系记录
*/
import { NextRequest, NextResponse } from 'next/server'
import { query } from '@/lib/db'
// 触发解绑的密钥(防止误触)
const CRON_SECRET = 'soul_cron_unbind_2026'
/**
* 主函数:自动解绑过期推荐关系
*/
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('[UnbindExpired] ========== 自动解绑任务开始 ==========')
try {
// 2. 查找需要解绑的记录
const expiredBindings = await query(`
SELECT
id,
referrer_id,
referee_id,
binding_date,
expiry_date,
purchase_count,
total_commission
FROM referral_bindings
WHERE status = 'active'
AND expiry_date < NOW()
AND purchase_count = 0
ORDER BY expiry_date ASC
`) as any[]
if (expiredBindings.length === 0) {
console.log('[UnbindExpired] 无需解绑的记录')
return NextResponse.json({
success: true,
message: '无需解绑的记录',
unbound: 0,
duration: Date.now() - startTime
})
}
console.log(`[UnbindExpired] 找到 ${expiredBindings.length} 条需要解绑的记录`)
// 3. 输出详细日志
expiredBindings.forEach((binding, index) => {
const bindingDate = new Date(binding.binding_date).toLocaleDateString('zh-CN')
const expiryDate = new Date(binding.expiry_date).toLocaleDateString('zh-CN')
const daysExpired = Math.floor((Date.now() - new Date(binding.expiry_date).getTime()) / (1000 * 60 * 60 * 24))
console.log(`[UnbindExpired] ${index + 1}. 用户 ${binding.referee_id}`)
console.log(` 推荐人: ${binding.referrer_id}`)
console.log(` 绑定时间: ${bindingDate}`)
console.log(` 过期时间: ${expiryDate} (已过期 ${daysExpired} 天)`)
console.log(` 购买次数: ${binding.purchase_count}`)
console.log(` 累计佣金: ¥${(binding.total_commission || 0).toFixed(2)}`)
})
// 4. 批量更新为 expired
const ids = expiredBindings.map(b => b.id)
const placeholders = ids.map(() => '?').join(',')
const result = await query(
`UPDATE referral_bindings SET status = 'expired' WHERE id IN (${placeholders})`,
ids
) as any
console.log(`[UnbindExpired] 已成功解绑 ${result.affectedRows || expiredBindings.length} 条记录`)
// 5. 更新推荐人的 referral_count减少
// 注意:这里需要按推荐人分组计算
const referrerUpdates = new Map<string, number>()
expiredBindings.forEach(binding => {
const count = referrerUpdates.get(binding.referrer_id) || 0
referrerUpdates.set(binding.referrer_id, count + 1)
})
let updatedReferrers = 0
for (const [referrerId, count] of referrerUpdates.entries()) {
try {
await query(`
UPDATE users
SET referral_count = GREATEST(0, referral_count - ?)
WHERE id = ?
`, [count, referrerId])
updatedReferrers++
console.log(`[UnbindExpired] 更新推荐人 ${referrerId} 的 referral_count (-${count})`)
} catch (err) {
console.error(`[UnbindExpired] 更新推荐人 ${referrerId} 失败:`, err)
}
}
const duration = Date.now() - startTime
console.log(`[UnbindExpired] 解绑完成: ${expiredBindings.length} 条记录,更新 ${updatedReferrers} 个推荐人`)
console.log(`[UnbindExpired] ========== 任务结束 (耗时 ${duration}ms) ==========`)
return NextResponse.json({
success: true,
message: '自动解绑完成',
unbound: expiredBindings.length,
updatedReferrers,
details: expiredBindings.map(b => ({
refereeId: b.referee_id,
referrerId: b.referrer_id,
bindingDate: b.binding_date,
expiryDate: b.expiry_date,
daysExpired: Math.floor((Date.now() - new Date(b.expiry_date).getTime()) / (1000 * 60 * 60 * 24))
})),
duration
})
} catch (error) {
console.error('[UnbindExpired] 解绑失败:', error)
return NextResponse.json({
success: false,
error: '自动解绑失败',
detail: error instanceof Error ? error.message : String(error)
}, { status: 500 })
}
}