Files
soul-yongping/app/api/miniprogram/pay/notify/route.ts

487 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

/**
* 小程序支付回调通知处理
* 微信支付成功后会调用此接口
*
* 分销规则:
* - 约90%给分发者可在system_config配置
* - 一级分销,只算直接推荐人
*/
import { NextResponse } from 'next/server'
import crypto from 'crypto'
import { query, getConfig } from '@/lib/db'
const WECHAT_PAY_CONFIG = {
appId: 'wxb8bbb2b10dec74aa',
mchId: '1318592501',
mchKey: 'wx3e31b068be59ddc131b068be59ddc2',
}
// 默认分成比例90%给推广者)
const DEFAULT_DISTRIBUTOR_SHARE = 0.9
// 生成签名
function generateSign(params: Record<string, string>, key: string): string {
const sortedKeys = Object.keys(params).sort()
const signString = sortedKeys
.filter((k) => params[k] && k !== 'sign')
.map((k) => `${k}=${params[k]}`)
.join('&')
const signWithKey = `${signString}&key=${key}`
return crypto.createHash('md5').update(signWithKey, 'utf8').digest('hex').toUpperCase()
}
// 验证签名
function verifySign(params: Record<string, string>, key: string): boolean {
const receivedSign = params.sign
if (!receivedSign) return false
const data = { ...params }
delete data.sign
const calculatedSign = generateSign(data, key)
return receivedSign === calculatedSign
}
// XML转对象
function xmlToDict(xml: string): Record<string, string> {
const result: Record<string, string> = {}
const regex = /<(\w+)><!\[CDATA\[(.*?)\]\]><\/\1>/g
let match
while ((match = regex.exec(xml)) !== null) {
result[match[1]] = match[2]
}
const simpleRegex = /<(\w+)>([^<]*)<\/\1>/g
while ((match = simpleRegex.exec(xml)) !== null) {
if (!result[match[1]]) {
result[match[1]] = match[2]
}
}
return result
}
// 成功响应
const SUCCESS_RESPONSE = `<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
</xml>`
// 失败响应
const FAIL_RESPONSE = `<xml>
<return_code><![CDATA[FAIL]]></return_code>
<return_msg><![CDATA[ERROR]]></return_msg>
</xml>`
/**
* POST - 接收微信支付回调
*/
export async function POST(request: Request) {
try {
const xmlData = await request.text()
console.log('[PayNotify] 收到支付回调')
const data = xmlToDict(xmlData)
// 验证签名
if (!verifySign(data, WECHAT_PAY_CONFIG.mchKey)) {
console.error('[PayNotify] 签名验证失败')
return new Response(FAIL_RESPONSE, {
headers: { 'Content-Type': 'application/xml' }
})
}
// 检查支付结果
if (data.return_code !== 'SUCCESS' || data.result_code !== 'SUCCESS') {
console.log('[PayNotify] 支付未成功:', data.err_code, data.err_code_des)
return new Response(SUCCESS_RESPONSE, {
headers: { 'Content-Type': 'application/xml' }
})
}
const orderSn = data.out_trade_no
const transactionId = data.transaction_id
const totalFee = parseInt(data.total_fee || '0', 10)
const totalAmount = totalFee / 100 // 转为元
const openId = data.openid
console.log('[PayNotify] 支付成功:', {
orderSn,
transactionId,
totalFee,
openId: openId?.slice(0, 10) + '...',
})
// 解析附加数据
let attach: Record<string, string> = {}
if (data.attach) {
try {
attach = JSON.parse(data.attach)
} catch (e) {
// 忽略解析错误
}
}
const { productType, productId, userId: attachUserId } = attach
// 买家身份必须以微信 openId 为准(不可伪造),避免客户端伪造 userId 导致错误归属/分佣
let buyerUserId: string | undefined = attachUserId
if (openId) {
try {
const usersByOpenId = await query('SELECT id FROM users WHERE open_id = ?', [openId]) as any[]
if (usersByOpenId.length > 0) {
const resolvedId = usersByOpenId[0].id
if (attachUserId && resolvedId !== attachUserId) {
console.warn('[PayNotify] 买家身份校验: attach.userId 与 openId 解析不一致,以 openId 为准', {
attachUserId,
resolvedId,
orderSn,
})
}
buyerUserId = resolvedId
}
} catch (e) {
console.error('[PayNotify] 按 openId 解析买家失败:', e)
}
}
if (!buyerUserId && attachUserId) {
buyerUserId = attachUserId
}
// 1. 更新订单状态为已支付
let orderExists = false
try {
// 先查询订单是否存在
const orderRows = await query(`
SELECT id, user_id, product_type, product_id, status
FROM orders
WHERE order_sn = ?
`, [orderSn]) as any[]
if (orderRows.length === 0) {
console.warn('[PayNotify] ⚠️ 订单不存在,尝试补记:', orderSn)
// 订单不存在时,补记订单(可能是创建订单时失败了)
try {
await query(`
INSERT INTO orders (
id, order_sn, user_id, open_id,
product_type, product_id, amount, description,
status, transaction_id, pay_time, referrer_id, referral_code, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'paid', ?, CURRENT_TIMESTAMP, NULL, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, [
orderSn, orderSn, buyerUserId || openId, openId,
productType || 'unknown', productId || '', totalAmount,
'支付回调补记订单', transactionId
])
console.log('[PayNotify] ✅ 订单补记成功:', orderSn)
orderExists = true
} catch (insertErr: any) {
const msg = insertErr?.message || ''
const code = insertErr?.code || ''
if (msg.includes('referrer_id') || msg.includes('referral_code') || code === 'ER_BAD_FIELD_ERROR') {
try {
await query(`
INSERT INTO orders (
id, order_sn, user_id, open_id,
product_type, product_id, amount, description,
status, transaction_id, pay_time, referrer_id, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'paid', ?, CURRENT_TIMESTAMP, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, [
orderSn, orderSn, buyerUserId || openId, openId,
productType || 'unknown', productId || '', totalAmount,
'支付回调补记订单', transactionId
])
console.log('[PayNotify] ✅ 订单补记成功(无 referral_code):', orderSn)
orderExists = true
} catch (e2: any) {
if (e2?.message?.includes('referrer_id') || e2?.code === 'ER_BAD_FIELD_ERROR') {
try {
await query(`
INSERT INTO orders (
id, order_sn, user_id, open_id,
product_type, product_id, amount, description,
status, transaction_id, pay_time, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'paid', ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, [
orderSn, orderSn, buyerUserId || openId, openId,
productType || 'unknown', productId || '', totalAmount,
'支付回调补记订单', transactionId
])
console.log('[PayNotify] ✅ 订单补记成功(无 referrer_id/referral_code):', orderSn)
orderExists = true
} catch (e3) {
console.error('[PayNotify] ❌ 补记订单失败:', e3)
}
} else {
console.error('[PayNotify] ❌ 补记订单失败:', e2)
}
}
} else {
console.error('[PayNotify] ❌ 补记订单失败:', insertErr)
}
}
} else {
const order = orderRows[0]
orderExists = true
if (order.status === 'paid') {
console.log('[PayNotify] 订单已支付,跳过更新:', orderSn)
} else {
// 更新订单状态
await query(`
UPDATE orders
SET status = 'paid',
transaction_id = ?,
pay_time = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE order_sn = ?
`, [transactionId, orderSn])
console.log('[PayNotify] ✅ 订单状态已更新为已支付:', orderSn)
}
}
} catch (e) {
console.error('[PayNotify] ❌ 处理订单失败:', e)
}
// 2. 更新用户购买记录buyerUserId 已在上面以 openId 为准解析)(✅ 检查是否已有其他相同产品的已支付订单)
if (buyerUserId && productType) {
try {
if (productType === 'fullbook') {
// 全书购买:无论如何都解锁
await query('UPDATE users SET has_full_book = TRUE WHERE id = ?', [buyerUserId])
console.log('[PayNotify] ✅ 用户已购全书:', buyerUserId)
} else if (productType === 'section' && productId) {
// 单章购买:检查是否已有该章节的其他已支付订单
const existingPaidOrders = await query(`
SELECT COUNT(*) as count
FROM orders
WHERE user_id = ?
AND product_type = 'section'
AND product_id = ?
AND status = 'paid'
AND order_sn != ?
`, [buyerUserId, productId, orderSn]) as any[]
const hasOtherPaidOrder = existingPaidOrders[0].count > 0
if (hasOtherPaidOrder) {
console.log('[PayNotify] 用户已有该章节的其他已支付订单,无需重复解锁:', {
userId: buyerUserId,
productId
})
} else {
// 第一次支付该章节,解锁权限
await query(`
UPDATE users
SET purchased_sections = JSON_ARRAY_APPEND(
COALESCE(purchased_sections, '[]'),
'$', ?
)
WHERE id = ? AND NOT JSON_CONTAINS(COALESCE(purchased_sections, '[]'), ?)
`, [productId, buyerUserId, JSON.stringify(productId)])
console.log('[PayNotify] ✅ 用户首次购买章节,已解锁:', buyerUserId, productId)
}
}
} catch (e) {
console.error('[PayNotify] ❌ 更新用户购买记录失败:', e)
}
// 3. 清理相同产品的无效订单(未支付的订单)
if (productType && (productType === 'fullbook' || productId)) {
try {
const deleteResult = await query(`
DELETE FROM orders
WHERE user_id = ?
AND product_type = ?
AND product_id = ?
AND status = 'created'
AND order_sn != ?
`, [
buyerUserId,
productType,
productId || 'fullbook',
orderSn // 保留当前已支付的订单
])
const deletedCount = (deleteResult as any).affectedRows || 0
if (deletedCount > 0) {
console.log('[PayNotify] ✅ 已清理无效订单:', {
userId: buyerUserId,
productType,
productId: productId || 'fullbook',
deletedCount
})
}
} catch (deleteErr) {
console.error('[PayNotify] ❌ 清理无效订单失败:', deleteErr)
// 清理失败不影响主流程
}
}
// 4. 处理分销佣金90%给推广者)
await processReferralCommission(buyerUserId, totalAmount, orderSn)
}
console.log('[PayNotify] 订单处理完成:', {
orderSn,
productType,
productId,
userId: buyerUserId,
})
// 返回成功响应给微信
return new Response(SUCCESS_RESPONSE, {
headers: { 'Content-Type': 'application/xml' }
})
} catch (error) {
console.error('[PayNotify] 处理回调失败:', error)
return new Response(FAIL_RESPONSE, {
headers: { 'Content-Type': 'application/xml' }
})
}
}
/**
* 处理分销佣金
* 规则约90%给分发者(一级分销)
*/
async function processReferralCommission(buyerUserId: string, amount: number, orderSn: string) {
try {
// 获取分成配置
let distributorShare = DEFAULT_DISTRIBUTOR_SHARE
try {
const config = await getConfig('referral_config')
if (config?.distributorShare) {
distributorShare = config.distributorShare / 100
}
} catch (e) { /* 使用默认配置 */ }
// 查找当前有效的推广绑定关系(新逻辑:购买时的绑定关系)
const bindings = await query(`
SELECT rb.id, rb.referrer_id, rb.referee_id, rb.expiry_date, rb.status,
rb.purchase_count, rb.total_commission
FROM referral_bindings rb
WHERE rb.referee_id = ?
AND rb.status = 'active'
ORDER BY rb.binding_date DESC
LIMIT 1
`, [buyerUserId]) as any[]
if (bindings.length === 0) {
console.log('[PayNotify] 用户无有效推广绑定,跳过分佣:', buyerUserId)
return
}
const binding = bindings[0]
const referrerId = binding.referrer_id
// 检查是否已过期(过期也不分佣)
const expiryDate = new Date(binding.expiry_date)
const now = new Date()
if (expiryDate < now) {
console.log('[PayNotify] 绑定已过期,跳过分佣:', {
buyerUserId,
referrerId,
expiryDate: expiryDate.toISOString()
})
return
}
// 计算佣金
const commission = Math.round(amount * distributorShare * 100) / 100
const newPurchaseCount = (binding.purchase_count || 0) + 1
const newTotalCommission = (binding.total_commission || 0) + commission
console.log('[PayNotify] 处理分佣:', {
referrerId,
buyerUserId,
amount,
commission,
shareRate: `${distributorShare * 100}%`,
purchaseCount: `${binding.purchase_count || 0} -> ${newPurchaseCount}`,
totalCommission: `${binding.total_commission || 0} -> ${newTotalCommission.toFixed(2)}`
})
// 更新推广者的待结算收益
await query(`
UPDATE users
SET pending_earnings = pending_earnings + ?
WHERE id = ?
`, [commission, referrerId])
// 更新绑定记录:累加购买次数和佣金,记录最后购买时间(保持 active 状态)
await query(`
UPDATE referral_bindings
SET last_purchase_date = CURRENT_TIMESTAMP,
purchase_count = purchase_count + 1,
total_commission = total_commission + ?
WHERE id = ?
`, [commission, binding.id])
console.log('[PayNotify] 分佣完成: 推广者', referrerId, '获得', commission, '元(第', newPurchaseCount, '次购买,累计', newTotalCommission.toFixed(2), '元)')
} catch (error) {
console.error('[PayNotify] 处理分佣失败:', error)
// 分佣失败不影响主流程
}
}
/**
* 清理无效订单
* 当一个订单支付成功后,删除该用户相同产品的其他未支付订单
*/
async function cleanupUnpaidOrders(
userId: string,
productType: string | undefined,
productId: string | undefined,
paidOrderSn: string
) {
try {
if (!userId || !productType) {
return
}
// 查询相同产品的其他未支付订单
const unpaidOrders = await query(`
SELECT id, order_sn, status, created_at
FROM orders
WHERE user_id = ?
AND product_type = ?
AND product_id = ?
AND status IN ('created', 'pending')
AND order_sn != ?
`, [userId, productType, productId || 'fullbook', paidOrderSn]) as any[]
if (unpaidOrders.length === 0) {
console.log('[PayNotify] 没有需要清理的无效订单')
return
}
// 删除这些无效订单
await query(`
DELETE FROM orders
WHERE user_id = ?
AND product_type = ?
AND product_id = ?
AND status IN ('created', 'pending')
AND order_sn != ?
`, [userId, productType, productId || 'fullbook', paidOrderSn])
console.log('[PayNotify] ✅ 已清理无效订单:', {
userId,
productType,
productId,
deletedCount: unpaidOrders.length,
deletedOrders: unpaidOrders.map(o => o.order_sn)
})
} catch (error) {
console.error('[PayNotify] ❌ 清理无效订单失败:', error)
// 清理失败不影响主流程
}
}