优化小程序支付流程,新增订单插入逻辑,确保支付成功后更新订单状态并处理佣金分配。同时,重构阅读页面,增强权限管理和阅读追踪功能,提升用户体验。
This commit is contained in:
268
app/api/admin/distribution/overview/route.ts
Normal file
268
app/api/admin/distribution/overview/route.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* 管理端分销数据概览API - 从真实数据库查询
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
import { requireAdminResponse } from '@/lib/admin-auth'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
// 验证管理员权限
|
||||
const authErr = requireAdminResponse(req)
|
||||
if (authErr) return authErr
|
||||
|
||||
try {
|
||||
const now = new Date()
|
||||
const today = now.toISOString().split('T')[0]
|
||||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString()
|
||||
const sevenDaysLater = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString()
|
||||
|
||||
// === 1. 订单数据统计 ===
|
||||
let orderStats = {
|
||||
todayOrders: 0,
|
||||
todayAmount: 0,
|
||||
monthOrders: 0,
|
||||
monthAmount: 0,
|
||||
totalOrders: 0,
|
||||
totalAmount: 0
|
||||
}
|
||||
|
||||
try {
|
||||
const orderResults = await query(`
|
||||
SELECT
|
||||
COUNT(*) as total_count,
|
||||
COALESCE(SUM(amount), 0) as total_amount,
|
||||
COALESCE(SUM(CASE WHEN DATE(created_at) = ? THEN 1 ELSE 0 END), 0) as today_count,
|
||||
COALESCE(SUM(CASE WHEN DATE(created_at) = ? THEN amount ELSE 0 END), 0) as today_amount,
|
||||
COALESCE(SUM(CASE WHEN created_at >= ? THEN 1 ELSE 0 END), 0) as month_count,
|
||||
COALESCE(SUM(CASE WHEN created_at >= ? THEN amount ELSE 0 END), 0) as month_amount
|
||||
FROM orders
|
||||
WHERE status = 'paid'
|
||||
`, [today, today, monthStart, monthStart]) as any[]
|
||||
|
||||
if (orderResults.length > 0) {
|
||||
const r = orderResults[0]
|
||||
orderStats = {
|
||||
todayOrders: parseInt(r.today_count) || 0,
|
||||
todayAmount: parseFloat(r.today_amount) || 0,
|
||||
monthOrders: parseInt(r.month_count) || 0,
|
||||
monthAmount: parseFloat(r.month_amount) || 0,
|
||||
totalOrders: parseInt(r.total_count) || 0,
|
||||
totalAmount: parseFloat(r.total_amount) || 0
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Admin Overview] 订单统计失败:', e)
|
||||
}
|
||||
|
||||
// === 2. 绑定数据统计 ===
|
||||
let bindingStats = {
|
||||
todayBindings: 0,
|
||||
todayConversions: 0,
|
||||
monthBindings: 0,
|
||||
monthConversions: 0,
|
||||
totalBindings: 0,
|
||||
totalConversions: 0,
|
||||
activeBindings: 0,
|
||||
expiredBindings: 0,
|
||||
expiringBindings: 0
|
||||
}
|
||||
|
||||
try {
|
||||
const bindingResults = await query(`
|
||||
SELECT
|
||||
COUNT(*) as total_count,
|
||||
SUM(CASE WHEN status = 'active' AND expiry_date > NOW() THEN 1 ELSE 0 END) as active_count,
|
||||
SUM(CASE WHEN status = 'converted' THEN 1 ELSE 0 END) as converted_count,
|
||||
SUM(CASE WHEN status = 'expired' OR (status = 'active' AND expiry_date <= NOW()) THEN 1 ELSE 0 END) as expired_count,
|
||||
SUM(CASE WHEN DATE(binding_date) = ? THEN 1 ELSE 0 END) as today_count,
|
||||
SUM(CASE WHEN DATE(binding_date) = ? AND status = 'converted' THEN 1 ELSE 0 END) as today_converted,
|
||||
SUM(CASE WHEN binding_date >= ? THEN 1 ELSE 0 END) as month_count,
|
||||
SUM(CASE WHEN binding_date >= ? AND status = 'converted' THEN 1 ELSE 0 END) as month_converted,
|
||||
SUM(CASE WHEN status = 'active' AND expiry_date <= ? AND expiry_date > NOW() THEN 1 ELSE 0 END) as expiring_count
|
||||
FROM referral_bindings
|
||||
`, [today, today, monthStart, monthStart, sevenDaysLater]) as any[]
|
||||
|
||||
if (bindingResults.length > 0) {
|
||||
const r = bindingResults[0]
|
||||
bindingStats = {
|
||||
todayBindings: parseInt(r.today_count) || 0,
|
||||
todayConversions: parseInt(r.today_converted) || 0,
|
||||
monthBindings: parseInt(r.month_count) || 0,
|
||||
monthConversions: parseInt(r.month_converted) || 0,
|
||||
totalBindings: parseInt(r.total_count) || 0,
|
||||
totalConversions: parseInt(r.converted_count) || 0,
|
||||
activeBindings: parseInt(r.active_count) || 0,
|
||||
expiredBindings: parseInt(r.expired_count) || 0,
|
||||
expiringBindings: parseInt(r.expiring_count) || 0
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Admin Overview] 绑定统计失败:', e)
|
||||
}
|
||||
|
||||
// === 3. 收益数据统计 ===
|
||||
let earningsStats = {
|
||||
totalEarnings: 0,
|
||||
todayEarnings: 0,
|
||||
monthEarnings: 0,
|
||||
pendingEarnings: 0
|
||||
}
|
||||
|
||||
try {
|
||||
// 从 users 表累加所有用户的收益
|
||||
const earningsResults = await query(`
|
||||
SELECT
|
||||
COALESCE(SUM(earnings), 0) as total_earnings,
|
||||
COALESCE(SUM(pending_earnings), 0) as pending_earnings
|
||||
FROM users
|
||||
`) as any[]
|
||||
|
||||
if (earningsResults.length > 0) {
|
||||
earningsStats.totalEarnings = parseFloat(earningsResults[0].total_earnings) || 0
|
||||
earningsStats.pendingEarnings = parseFloat(earningsResults[0].pending_earnings) || 0
|
||||
}
|
||||
|
||||
// 今日和本月收益:从 orders 表计算(status='paid' 的订单)
|
||||
const periodEarningsResults = await query(`
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN DATE(pay_time) = ? THEN amount * 0.9 ELSE 0 END), 0) as today_earnings,
|
||||
COALESCE(SUM(CASE WHEN pay_time >= ? THEN amount * 0.9 ELSE 0 END), 0) as month_earnings
|
||||
FROM orders
|
||||
WHERE status = 'paid'
|
||||
`, [today, monthStart]) as any[]
|
||||
|
||||
if (periodEarningsResults.length > 0) {
|
||||
earningsStats.todayEarnings = parseFloat(periodEarningsResults[0].today_earnings) || 0
|
||||
earningsStats.monthEarnings = parseFloat(periodEarningsResults[0].month_earnings) || 0
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Admin Overview] 收益统计失败:', e)
|
||||
}
|
||||
|
||||
// === 4. 提现数据统计 ===
|
||||
let withdrawalStats = {
|
||||
pendingCount: 0,
|
||||
pendingAmount: 0
|
||||
}
|
||||
|
||||
try {
|
||||
const withdrawalResults = await query(`
|
||||
SELECT
|
||||
COUNT(*) as pending_count,
|
||||
COALESCE(SUM(amount), 0) as pending_amount
|
||||
FROM withdrawals
|
||||
WHERE status = 'pending'
|
||||
`) as any[]
|
||||
|
||||
if (withdrawalResults.length > 0) {
|
||||
withdrawalStats.pendingCount = parseInt(withdrawalResults[0].pending_count) || 0
|
||||
withdrawalStats.pendingAmount = parseFloat(withdrawalResults[0].pending_amount) || 0
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Admin Overview] 提现统计失败:', e)
|
||||
}
|
||||
|
||||
// === 5. 访问数据统计 ===
|
||||
let visitStats = {
|
||||
todayVisits: 0,
|
||||
monthVisits: 0,
|
||||
totalVisits: 0
|
||||
}
|
||||
|
||||
try {
|
||||
const visitResults = await query(`
|
||||
SELECT
|
||||
COUNT(*) as total_count,
|
||||
COUNT(DISTINCT CASE WHEN DATE(created_at) = ? THEN id END) as today_count,
|
||||
COUNT(DISTINCT CASE WHEN created_at >= ? THEN id END) as month_count
|
||||
FROM referral_visits
|
||||
`, [today, monthStart]) as any[]
|
||||
|
||||
if (visitResults.length > 0) {
|
||||
visitStats.totalVisits = parseInt(visitResults[0].total_count) || 0
|
||||
visitStats.todayVisits = parseInt(visitResults[0].today_count) || 0
|
||||
visitStats.monthVisits = parseInt(visitResults[0].month_count) || 0
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Admin Overview] 访问统计失败:', e)
|
||||
// 访问表可能不存在,使用绑定数作为替代
|
||||
visitStats = {
|
||||
todayVisits: bindingStats.todayBindings,
|
||||
monthVisits: bindingStats.monthBindings,
|
||||
totalVisits: bindingStats.totalBindings
|
||||
}
|
||||
}
|
||||
|
||||
// === 6. 分销商数据统计 ===
|
||||
let distributorStats = {
|
||||
totalDistributors: 0,
|
||||
activeDistributors: 0
|
||||
}
|
||||
|
||||
try {
|
||||
const distributorResults = await query(`
|
||||
SELECT
|
||||
COUNT(*) as total_count,
|
||||
SUM(CASE WHEN earnings > 0 THEN 1 ELSE 0 END) as active_count
|
||||
FROM users
|
||||
WHERE referral_code IS NOT NULL AND referral_code != ''
|
||||
`) as any[]
|
||||
|
||||
if (distributorResults.length > 0) {
|
||||
distributorStats.totalDistributors = parseInt(distributorResults[0].total_count) || 0
|
||||
distributorStats.activeDistributors = parseInt(distributorResults[0].active_count) || 0
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Admin Overview] 分销商统计失败:', e)
|
||||
}
|
||||
|
||||
// === 7. 计算转化率 ===
|
||||
const conversionRate = visitStats.totalVisits > 0
|
||||
? ((bindingStats.totalConversions / visitStats.totalVisits) * 100).toFixed(2)
|
||||
: '0.00'
|
||||
|
||||
// 返回完整概览数据
|
||||
const overview = {
|
||||
// 今日数据
|
||||
todayClicks: visitStats.todayVisits,
|
||||
todayBindings: bindingStats.todayBindings,
|
||||
todayConversions: bindingStats.todayConversions,
|
||||
todayEarnings: earningsStats.todayEarnings,
|
||||
|
||||
// 本月数据
|
||||
monthClicks: visitStats.monthVisits,
|
||||
monthBindings: bindingStats.monthBindings,
|
||||
monthConversions: bindingStats.monthConversions,
|
||||
monthEarnings: earningsStats.monthEarnings,
|
||||
|
||||
// 总计数据
|
||||
totalClicks: visitStats.totalVisits,
|
||||
totalBindings: bindingStats.totalBindings,
|
||||
totalConversions: bindingStats.totalConversions,
|
||||
totalEarnings: earningsStats.totalEarnings,
|
||||
|
||||
// 其他统计
|
||||
expiringBindings: bindingStats.expiringBindings,
|
||||
pendingWithdrawals: withdrawalStats.pendingCount,
|
||||
pendingWithdrawAmount: withdrawalStats.pendingAmount,
|
||||
conversionRate,
|
||||
totalDistributors: distributorStats.totalDistributors,
|
||||
activeDistributors: distributorStats.activeDistributors,
|
||||
}
|
||||
|
||||
console.log('[Admin Overview] 数据统计完成:', overview)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
overview
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Admin Overview] 统计失败:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '获取分销概览失败: ' + (error as Error).message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
256
app/api/cron/sync-orders/route.ts
Normal file
256
app/api/cron/sync-orders/route.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* 订单状态同步定时任务 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 })
|
||||
}
|
||||
}
|
||||
73
app/api/db/distribution/route.ts
Normal file
73
app/api/db/distribution/route.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 绑定数据API - 从真实数据库查询
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const userId = searchParams.get('userId')
|
||||
|
||||
let sql = `
|
||||
SELECT
|
||||
rb.id,
|
||||
rb.referrer_id,
|
||||
rb.referee_id,
|
||||
rb.referral_code as referrer_code,
|
||||
rb.status,
|
||||
rb.binding_date as bound_at,
|
||||
rb.expiry_date as expires_at,
|
||||
rb.conversion_date,
|
||||
rb.commission_amount,
|
||||
u1.nickname as referrer_name,
|
||||
u2.nickname as referee_nickname,
|
||||
u2.phone as referee_phone,
|
||||
DATEDIFF(rb.expiry_date, NOW()) as days_remaining
|
||||
FROM referral_bindings rb
|
||||
LEFT JOIN users u1 ON rb.referrer_id = u1.id
|
||||
LEFT JOIN users u2 ON rb.referee_id = u2.id
|
||||
`
|
||||
|
||||
let params: any[] = []
|
||||
if (userId) {
|
||||
sql += ' WHERE rb.referrer_id = ?'
|
||||
params.push(userId)
|
||||
}
|
||||
|
||||
sql += ' ORDER BY rb.binding_date DESC LIMIT 500'
|
||||
|
||||
const bindings = await query(sql, params) as any[]
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
bindings: bindings.map((b: any) => ({
|
||||
id: b.id,
|
||||
referrer_id: b.referrer_id,
|
||||
referrer_name: b.referrer_name || '未知',
|
||||
referrer_code: b.referrer_code,
|
||||
referee_id: b.referee_id,
|
||||
referee_phone: b.referee_phone,
|
||||
referee_nickname: b.referee_nickname || '用户' + (b.referee_id || '').slice(-4),
|
||||
bound_at: b.bound_at,
|
||||
expires_at: b.expires_at,
|
||||
status: b.status,
|
||||
days_remaining: Math.max(0, parseInt(b.days_remaining) || 0),
|
||||
commission: parseFloat(b.commission_amount) || 0,
|
||||
order_amount: 0, // 需要的话可以关联 orders 表计算
|
||||
source: 'miniprogram'
|
||||
})),
|
||||
total: bindings.length
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Distribution API] 查询失败:', error)
|
||||
// 表可能不存在,返回空数组
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
bindings: [],
|
||||
total: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,27 @@ export async function POST(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// === ✅ 从 orders 表查询真实购买记录 ===
|
||||
let purchasedSections: string[] = []
|
||||
try {
|
||||
const orderRows = await query(`
|
||||
SELECT DISTINCT product_id
|
||||
FROM orders
|
||||
WHERE user_id = ?
|
||||
AND status = 'paid'
|
||||
AND product_type = 'section'
|
||||
`, [user.id]) as any[]
|
||||
|
||||
purchasedSections = orderRows.map((row: any) => row.product_id).filter(Boolean)
|
||||
console.log('[MiniLogin] 查询到已购章节:', purchasedSections.length, '个')
|
||||
} catch (e) {
|
||||
console.warn('[MiniLogin] 查询购买记录失败:', e)
|
||||
// 降级到 users.purchased_sections 字段
|
||||
purchasedSections = typeof user.purchased_sections === 'string'
|
||||
? JSON.parse(user.purchased_sections || '[]')
|
||||
: (user.purchased_sections || [])
|
||||
}
|
||||
|
||||
// 统一用户数据格式
|
||||
const responseUser = {
|
||||
id: user.id,
|
||||
@@ -126,9 +147,7 @@ export async function POST(request: Request) {
|
||||
wechatId: user.wechat_id,
|
||||
referralCode: user.referral_code,
|
||||
hasFullBook: user.has_full_book || false,
|
||||
purchasedSections: typeof user.purchased_sections === 'string'
|
||||
? JSON.parse(user.purchased_sections || '[]')
|
||||
: (user.purchased_sections || []),
|
||||
purchasedSections, // ✅ 使用从 orders 表查询的真实数据
|
||||
earnings: parseFloat(user.earnings) || 0,
|
||||
pendingEarnings: parseFloat(user.pending_earnings) || 0,
|
||||
referralCount: user.referral_count || 0,
|
||||
|
||||
@@ -125,17 +125,78 @@ export async function POST(request: Request) {
|
||||
const { productType, productId, userId } = attach
|
||||
|
||||
// 1. 更新订单状态为已支付
|
||||
let orderExists = false
|
||||
try {
|
||||
await query(`
|
||||
UPDATE orders
|
||||
SET status = 'paid',
|
||||
transaction_id = ?,
|
||||
pay_time = CURRENT_TIMESTAMP
|
||||
WHERE order_sn = ? AND status = 'pending'
|
||||
`, [transactionId, orderSn])
|
||||
console.log('[PayNotify] 订单状态已更新:', orderSn)
|
||||
// 先查询订单是否存在
|
||||
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, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'paid', ?, CURRENT_TIMESTAMP, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
`, [
|
||||
orderSn, orderSn, userId || openId, openId,
|
||||
productType || 'unknown', productId || '', totalAmount,
|
||||
'支付回调补记订单', transactionId
|
||||
])
|
||||
console.log('[PayNotify] ✅ 订单补记成功:', orderSn)
|
||||
orderExists = true
|
||||
} catch (insertErr: any) {
|
||||
if (insertErr?.message?.includes('referrer_id') || insertErr?.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, userId || openId, openId,
|
||||
productType || 'unknown', productId || '', totalAmount,
|
||||
'支付回调补记订单', transactionId
|
||||
])
|
||||
console.log('[PayNotify] ✅ 订单补记成功(无 referrer_id):', orderSn)
|
||||
orderExists = true
|
||||
} catch (e2) {
|
||||
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)
|
||||
console.error('[PayNotify] ❌ 处理订单失败:', e)
|
||||
}
|
||||
|
||||
// 2. 获取用户信息
|
||||
@@ -151,30 +212,83 @@ export async function POST(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 更新用户购买记录
|
||||
if (buyerUserId) {
|
||||
// 3. 更新用户购买记录(✅ 检查是否已有其他相同产品的已支付订单)
|
||||
if (buyerUserId && productType) {
|
||||
try {
|
||||
if (productType === 'fullbook') {
|
||||
// 全书购买
|
||||
// 全书购买:无论如何都解锁
|
||||
await query('UPDATE users SET has_full_book = TRUE WHERE id = ?', [buyerUserId])
|
||||
console.log('[PayNotify] 用户已购全书:', buyerUserId)
|
||||
console.log('[PayNotify] ✅ 用户已购全书:', buyerUserId)
|
||||
|
||||
} else if (productType === 'section' && productId) {
|
||||
// 单章购买
|
||||
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)
|
||||
// 单章购买:检查是否已有该章节的其他已支付订单
|
||||
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)
|
||||
console.error('[PayNotify] ❌ 更新用户购买记录失败:', e)
|
||||
}
|
||||
|
||||
// 4. 处理分销佣金(90%给推广者)
|
||||
// 4. 清理相同产品的无效订单(未支付的订单)
|
||||
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)
|
||||
// 清理失败不影响主流程
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 处理分销佣金(90%给推广者)
|
||||
await processReferralCommission(buyerUserId, totalAmount, orderSn)
|
||||
}
|
||||
|
||||
@@ -267,3 +381,58 @@ async function processReferralCommission(buyerUserId: string, amount: number, or
|
||||
// 分佣失败不影响主流程
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理无效订单
|
||||
* 当一个订单支付成功后,删除该用户相同产品的其他未支付订单
|
||||
*/
|
||||
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)
|
||||
// 清理失败不影响主流程
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
import crypto from 'crypto'
|
||||
import { query } from '@/lib/db'
|
||||
|
||||
// 微信支付配置 - 2026-01-25 更新
|
||||
// 小程序支付绑定状态: 审核中(申请单ID: 201554696918)
|
||||
@@ -134,6 +135,104 @@ export async function POST(request: Request) {
|
||||
productId,
|
||||
})
|
||||
|
||||
// === ✅ 1. 先插入订单到数据库(无论支付是否成功,都要有订单记录) ===
|
||||
const userId = body.userId || openId // 优先使用 userId,否则用 openId
|
||||
let orderCreated = false
|
||||
|
||||
// 查询当前用户的有效推荐人(用于订单归属与分销)
|
||||
let referrerId: string | null = null
|
||||
try {
|
||||
const bindings = await query(`
|
||||
SELECT referrer_id
|
||||
FROM referral_bindings
|
||||
WHERE referee_id = ? AND status = 'active' AND expiry_date > NOW()
|
||||
ORDER BY binding_date DESC
|
||||
LIMIT 1
|
||||
`, [userId]) as any[]
|
||||
if (bindings.length > 0) {
|
||||
referrerId = bindings[0].referrer_id || null
|
||||
console.log('[MiniPay] 订单归属推荐人(绑定):', referrerId)
|
||||
}
|
||||
// 若绑定未查到且前端传了邀请码,按邀请码解析推荐人
|
||||
if (!referrerId && body.referralCode) {
|
||||
const refUsers = await query(`
|
||||
SELECT id FROM users WHERE referral_code = ? LIMIT 1
|
||||
`, [String(body.referralCode).trim()]) as any[]
|
||||
if (refUsers.length > 0) {
|
||||
referrerId = refUsers[0].id
|
||||
console.log('[MiniPay] 订单归属推荐人(邀请码):', referrerId)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[MiniPay] 查询推荐人失败,继续创建订单:', e)
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查是否已有相同产品的已支付订单
|
||||
const existingOrders = await query(`
|
||||
SELECT id FROM orders
|
||||
WHERE user_id = ?
|
||||
AND product_type = ?
|
||||
AND product_id = ?
|
||||
AND status = 'paid'
|
||||
LIMIT 1
|
||||
`, [userId, productType, productId || 'fullbook']) as any[]
|
||||
|
||||
if (existingOrders.length > 0) {
|
||||
console.log('[MiniPay] ⚠️ 用户已购买该产品,但仍创建订单:', {
|
||||
userId,
|
||||
productType,
|
||||
productId
|
||||
})
|
||||
}
|
||||
|
||||
// 插入订单(含 referrer_id,便于分销归属与统计)
|
||||
try {
|
||||
await query(`
|
||||
INSERT INTO orders (
|
||||
id, order_sn, user_id, open_id,
|
||||
product_type, product_id, amount, description,
|
||||
status, transaction_id, referrer_id, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
`, [
|
||||
orderSn, orderSn, userId, openId,
|
||||
productType, productId || 'fullbook', amount, goodsBody,
|
||||
'created', null, referrerId
|
||||
])
|
||||
} catch (insertErr: any) {
|
||||
// 兼容:若表尚无 referrer_id 列,则用不含该字段的 INSERT
|
||||
if (insertErr?.message?.includes('referrer_id') || insertErr?.code === 'ER_BAD_FIELD_ERROR') {
|
||||
await query(`
|
||||
INSERT INTO orders (
|
||||
id, order_sn, user_id, open_id,
|
||||
product_type, product_id, amount, description,
|
||||
status, transaction_id, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
`, [
|
||||
orderSn, orderSn, userId, openId,
|
||||
productType, productId || 'fullbook', amount, goodsBody,
|
||||
'created', null
|
||||
])
|
||||
console.log('[MiniPay] 订单已插入(未含 referrer_id,请执行 scripts/add_orders_referrer_id.py)')
|
||||
} else {
|
||||
throw insertErr
|
||||
}
|
||||
}
|
||||
|
||||
orderCreated = true
|
||||
console.log('[MiniPay] ✅ 订单已插入数据库:', {
|
||||
orderSn,
|
||||
userId,
|
||||
productType,
|
||||
productId,
|
||||
amount
|
||||
})
|
||||
} catch (dbError) {
|
||||
console.error('[MiniPay] ❌ 插入订单失败:', dbError)
|
||||
// 订单创建失败,但不中断支付流程
|
||||
// 理由:微信支付成功后仍可以通过回调补记订单
|
||||
}
|
||||
|
||||
// 调用微信统一下单接口
|
||||
const xmlData = dictToXml(params)
|
||||
const response = await fetch('https://api.mch.weixin.qq.com/pay/unifiedorder', {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { PaymentFactory, SignatureError } from "@/lib/payment"
|
||||
import { query } from "@/lib/db"
|
||||
|
||||
// 确保网关已注册
|
||||
import "@/lib/payment/alipay"
|
||||
@@ -42,16 +43,96 @@ export async function POST(request: NextRequest) {
|
||||
payTime: notifyResult.payTime,
|
||||
})
|
||||
|
||||
// TODO: 更新订单状态
|
||||
// await OrderService.updateStatus(notifyResult.tradeSn, 'paid')
|
||||
// === ✅ 1. 更新订单状态 ===
|
||||
try {
|
||||
// 通过 transaction_id 查找订单
|
||||
const orderRows = await query(`
|
||||
SELECT id, user_id, amount, product_type, product_id
|
||||
FROM orders
|
||||
WHERE transaction_id = ? AND status = 'created'
|
||||
LIMIT 1
|
||||
`, [notifyResult.tradeSn]) as any[]
|
||||
|
||||
// TODO: 解锁内容/开通权限
|
||||
// await ContentService.unlockForUser(notifyResult.attach?.userId, notifyResult.attach?.productId)
|
||||
if (orderRows.length === 0) {
|
||||
console.error('[Alipay Notify] ❌ 订单不存在或已处理:', notifyResult.tradeSn)
|
||||
} else {
|
||||
const order = orderRows[0]
|
||||
const orderId = order.id
|
||||
const userId = order.user_id
|
||||
const amount = parseFloat(order.amount)
|
||||
const productType = order.product_type
|
||||
const productId = order.product_id
|
||||
|
||||
// TODO: 分配佣金(如果有推荐人)
|
||||
// if (notifyResult.attach?.referralCode) {
|
||||
// await ReferralService.distributeCommission(notifyResult)
|
||||
// }
|
||||
// 更新订单状态为已支付
|
||||
await query(`
|
||||
UPDATE orders
|
||||
SET status = 'paid',
|
||||
pay_time = ?,
|
||||
updated_at = NOW()
|
||||
WHERE id = ?
|
||||
`, [notifyResult.payTime, orderId])
|
||||
|
||||
console.log('[Alipay Notify] ✅ 订单状态已更新:', { orderId, status: 'paid' })
|
||||
|
||||
// === ✅ 2. 解锁内容/开通权限 ===
|
||||
if (productType === 'fullbook') {
|
||||
// 购买全书
|
||||
await query('UPDATE users SET has_full_book = 1 WHERE id = ?', [userId])
|
||||
console.log('[Alipay Notify] ✅ 全书权限已开通:', userId)
|
||||
} else if (productType === 'section' && productId) {
|
||||
// 购买单个章节
|
||||
console.log('[Alipay Notify] ✅ 章节权限已开通:', { userId, sectionId: productId })
|
||||
}
|
||||
|
||||
// === ✅ 3. 分配佣金(如果有推荐人) ===
|
||||
try {
|
||||
// 查询用户的推荐人
|
||||
const userRows = await query(`
|
||||
SELECT u.id, u.referred_by, rb.referrer_id, rb.status
|
||||
FROM users u
|
||||
LEFT JOIN referral_bindings rb ON rb.referee_id = u.id AND rb.status = 'active' AND rb.expiry_date > NOW()
|
||||
WHERE u.id = ?
|
||||
LIMIT 1
|
||||
`, [userId]) as any[]
|
||||
|
||||
if (userRows.length > 0 && userRows[0].referrer_id) {
|
||||
const referrerId = userRows[0].referrer_id
|
||||
const commissionRate = 0.9 // 90% 佣金比例
|
||||
const commissionAmount = parseFloat((amount * commissionRate).toFixed(2))
|
||||
|
||||
// 更新推荐人的 pending_earnings
|
||||
await query(`
|
||||
UPDATE users
|
||||
SET pending_earnings = pending_earnings + ?
|
||||
WHERE id = ?
|
||||
`, [commissionAmount, referrerId])
|
||||
|
||||
// 更新绑定状态为已转化
|
||||
await query(`
|
||||
UPDATE referral_bindings
|
||||
SET status = 'converted',
|
||||
conversion_date = NOW(),
|
||||
commission_amount = ?
|
||||
WHERE referee_id = ? AND status = 'active'
|
||||
`, [commissionAmount, userId])
|
||||
|
||||
console.log('[Alipay Notify] ✅ 佣金已分配:', {
|
||||
referrerId,
|
||||
commissionAmount,
|
||||
orderId
|
||||
})
|
||||
} else {
|
||||
console.log('[Alipay Notify] ℹ️ 该用户无推荐人,无需分配佣金')
|
||||
}
|
||||
} catch (commErr) {
|
||||
console.error('[Alipay Notify] ❌ 分配佣金失败:', commErr)
|
||||
// 不中断主流程
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Alipay Notify] ❌ 订单处理失败:', error)
|
||||
// 不中断,继续返回成功响应给支付宝(避免重复回调)
|
||||
}
|
||||
} else {
|
||||
console.log("[Alipay Notify] 非支付成功状态:", notifyResult.status)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
getNotifyUrl,
|
||||
getReturnUrl,
|
||||
} from "@/lib/payment"
|
||||
import { query } from "@/lib/db"
|
||||
|
||||
// 确保网关已注册
|
||||
import "@/lib/payment/alipay"
|
||||
@@ -52,6 +53,50 @@ export async function POST(request: NextRequest) {
|
||||
expireAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(), // 30分钟
|
||||
}
|
||||
|
||||
// === 💾 插入订单到数据库 ===
|
||||
try {
|
||||
// 获取用户 openId(如果有)
|
||||
let openId = null
|
||||
try {
|
||||
const userRows = await query('SELECT open_id FROM users WHERE id = ?', [userId]) as any[]
|
||||
if (userRows.length > 0) {
|
||||
openId = userRows[0].open_id
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Payment] 获取 openId 失败:', e)
|
||||
}
|
||||
|
||||
const productType = type === 'section' ? 'section' : 'fullbook'
|
||||
const productId = type === 'section' ? sectionId : 'fullbook'
|
||||
const description = type === 'section'
|
||||
? `购买章节: ${sectionTitle}`
|
||||
: '购买整本书'
|
||||
|
||||
await query(`
|
||||
INSERT INTO orders (
|
||||
id, order_sn, user_id, open_id,
|
||||
product_type, product_id, amount, description,
|
||||
status, transaction_id, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
`, [
|
||||
orderSn, // id
|
||||
orderSn, // order_sn
|
||||
userId, // user_id
|
||||
openId, // open_id
|
||||
productType, // product_type
|
||||
productId, // product_id
|
||||
amount, // amount
|
||||
description, // description
|
||||
'created', // status
|
||||
tradeSn // transaction_id(支付流水号)
|
||||
])
|
||||
|
||||
console.log('[Payment] ✅ 订单已插入数据库:', { orderSn, userId, amount })
|
||||
} catch (dbError) {
|
||||
console.error('[Payment] ❌ 插入订单失败:', dbError)
|
||||
// 不中断流程,继续返回支付信息
|
||||
}
|
||||
|
||||
// 获取客户端IP
|
||||
const clientIp = request.headers.get("x-forwarded-for")
|
||||
|| request.headers.get("x-real-ip")
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { PaymentFactory, SignatureError } from "@/lib/payment"
|
||||
import { query } from "@/lib/db"
|
||||
|
||||
// 确保网关已注册
|
||||
import "@/lib/payment/wechat"
|
||||
@@ -33,16 +34,97 @@ export async function POST(request: NextRequest) {
|
||||
payTime: notifyResult.payTime,
|
||||
})
|
||||
|
||||
// TODO: 更新订单状态
|
||||
// await OrderService.updateStatus(notifyResult.tradeSn, 'paid')
|
||||
// === ✅ 1. 更新订单状态 ===
|
||||
try {
|
||||
// 通过 transaction_id 查找订单
|
||||
const orderRows = await query(`
|
||||
SELECT id, user_id, amount, product_type, product_id
|
||||
FROM orders
|
||||
WHERE transaction_id = ? AND status = 'created'
|
||||
LIMIT 1
|
||||
`, [notifyResult.tradeSn]) as any[]
|
||||
|
||||
// TODO: 解锁内容/开通权限
|
||||
// await ContentService.unlockForUser(notifyResult.attach?.userId, notifyResult.attach?.productId)
|
||||
if (orderRows.length === 0) {
|
||||
console.error('[Wechat Notify] ❌ 订单不存在或已处理:', notifyResult.tradeSn)
|
||||
} else {
|
||||
const order = orderRows[0]
|
||||
const orderId = order.id
|
||||
const userId = order.user_id
|
||||
const amount = parseFloat(order.amount)
|
||||
const productType = order.product_type
|
||||
const productId = order.product_id
|
||||
|
||||
// TODO: 分配佣金(如果有推荐人)
|
||||
// if (notifyResult.attach?.referralCode) {
|
||||
// await ReferralService.distributeCommission(notifyResult)
|
||||
// }
|
||||
// 更新订单状态为已支付
|
||||
await query(`
|
||||
UPDATE orders
|
||||
SET status = 'paid',
|
||||
pay_time = ?,
|
||||
updated_at = NOW()
|
||||
WHERE id = ?
|
||||
`, [notifyResult.payTime, orderId])
|
||||
|
||||
console.log('[Wechat Notify] ✅ 订单状态已更新:', { orderId, status: 'paid' })
|
||||
|
||||
// === ✅ 2. 解锁内容/开通权限 ===
|
||||
if (productType === 'fullbook') {
|
||||
// 购买全书
|
||||
await query('UPDATE users SET has_full_book = 1 WHERE id = ?', [userId])
|
||||
console.log('[Wechat Notify] ✅ 全书权限已开通:', userId)
|
||||
} else if (productType === 'section' && productId) {
|
||||
// 购买单个章节(这里需要根据你的业务逻辑处理)
|
||||
// 可能需要在 user_purchases 表中记录,或更新 users.purchased_sections
|
||||
console.log('[Wechat Notify] ✅ 章节权限已开通:', { userId, sectionId: productId })
|
||||
}
|
||||
|
||||
// === ✅ 3. 分配佣金(如果有推荐人) ===
|
||||
try {
|
||||
// 查询用户的推荐人
|
||||
const userRows = await query(`
|
||||
SELECT u.id, u.referred_by, rb.referrer_id, rb.status
|
||||
FROM users u
|
||||
LEFT JOIN referral_bindings rb ON rb.referee_id = u.id AND rb.status = 'active' AND rb.expiry_date > NOW()
|
||||
WHERE u.id = ?
|
||||
LIMIT 1
|
||||
`, [userId]) as any[]
|
||||
|
||||
if (userRows.length > 0 && userRows[0].referrer_id) {
|
||||
const referrerId = userRows[0].referrer_id
|
||||
const commissionRate = 0.9 // 90% 佣金比例
|
||||
const commissionAmount = parseFloat((amount * commissionRate).toFixed(2))
|
||||
|
||||
// 更新推荐人的 pending_earnings
|
||||
await query(`
|
||||
UPDATE users
|
||||
SET pending_earnings = pending_earnings + ?
|
||||
WHERE id = ?
|
||||
`, [commissionAmount, referrerId])
|
||||
|
||||
// 更新绑定状态为已转化
|
||||
await query(`
|
||||
UPDATE referral_bindings
|
||||
SET status = 'converted',
|
||||
conversion_date = NOW(),
|
||||
commission_amount = ?
|
||||
WHERE referee_id = ? AND status = 'active'
|
||||
`, [commissionAmount, userId])
|
||||
|
||||
console.log('[Wechat Notify] ✅ 佣金已分配:', {
|
||||
referrerId,
|
||||
commissionAmount,
|
||||
orderId
|
||||
})
|
||||
} else {
|
||||
console.log('[Wechat Notify] ℹ️ 该用户无推荐人,无需分配佣金')
|
||||
}
|
||||
} catch (commErr) {
|
||||
console.error('[Wechat Notify] ❌ 分配佣金失败:', commErr)
|
||||
// 不中断主流程
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Wechat Notify] ❌ 订单处理失败:', error)
|
||||
// 不中断,继续返回成功响应给微信(避免重复回调)
|
||||
}
|
||||
} else {
|
||||
console.log("[Wechat Notify] 支付失败:", notifyResult)
|
||||
}
|
||||
|
||||
108
app/api/user/check-purchased/route.ts
Normal file
108
app/api/user/check-purchased/route.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 检查用户是否已购买指定章节/全书
|
||||
* 用于支付前校验,避免重复购买
|
||||
*
|
||||
* GET /api/user/check-purchased?userId=xxx&type=section&productId=xxx
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const userId = searchParams.get('userId')
|
||||
const type = searchParams.get('type') // 'section' | 'fullbook'
|
||||
const productId = searchParams.get('productId')
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '缺少 userId 参数'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// 1. 查询用户是否购买全书
|
||||
const userRows = await query(`
|
||||
SELECT has_full_book FROM users WHERE id = ?
|
||||
`, [userId]) as any[]
|
||||
|
||||
if (userRows.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '用户不存在'
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
const hasFullBook = userRows[0].has_full_book || false
|
||||
|
||||
// 如果已购全书,直接返回已购买
|
||||
if (hasFullBook) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
isPurchased: true,
|
||||
reason: 'has_full_book'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 2. 如果是购买全书,检查是否已有全书订单
|
||||
if (type === 'fullbook') {
|
||||
const orderRows = await query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM orders
|
||||
WHERE user_id = ?
|
||||
AND product_type = 'fullbook'
|
||||
AND status = 'paid'
|
||||
`, [userId]) as any[]
|
||||
|
||||
const hasPaid = orderRows[0].count > 0
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
isPurchased: hasPaid,
|
||||
reason: hasPaid ? 'fullbook_order_exists' : null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 3. 如果是购买章节,检查是否已有该章节订单
|
||||
if (type === 'section' && productId) {
|
||||
const orderRows = await query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM orders
|
||||
WHERE user_id = ?
|
||||
AND product_type = 'section'
|
||||
AND product_id = ?
|
||||
AND status = 'paid'
|
||||
`, [userId, productId]) as any[]
|
||||
|
||||
const hasPaid = orderRows[0].count > 0
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
isPurchased: hasPaid,
|
||||
reason: hasPaid ? 'section_order_exists' : null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
isPurchased: false,
|
||||
reason: null
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('[CheckPurchased] 查询失败:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '查询购买状态失败'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
72
app/api/user/purchase-status/route.ts
Normal file
72
app/api/user/purchase-status/route.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 查询用户购买状态 API
|
||||
* 用于支付成功后刷新用户的购买记录
|
||||
*
|
||||
* GET /api/user/purchase-status?userId=xxx
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const userId = searchParams.get('userId')
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '缺少 userId 参数'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// 1. 查询用户基本信息
|
||||
const userRows = await query(`
|
||||
SELECT
|
||||
id, nickname, avatar, phone, wechat_id,
|
||||
referral_code, has_full_book,
|
||||
earnings, pending_earnings, referral_count
|
||||
FROM users
|
||||
WHERE id = ?
|
||||
`, [userId]) as any[]
|
||||
|
||||
if (userRows.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '用户不存在'
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
const user = userRows[0]
|
||||
|
||||
// 2. 从 orders 表查询已购买的章节
|
||||
const orderRows = await query(`
|
||||
SELECT DISTINCT product_id
|
||||
FROM orders
|
||||
WHERE user_id = ?
|
||||
AND status = 'paid'
|
||||
AND product_type = 'section'
|
||||
`, [userId]) as any[]
|
||||
|
||||
const purchasedSections = orderRows.map((row: any) => row.product_id).filter(Boolean)
|
||||
|
||||
// 3. 返回完整购买状态
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
hasFullBook: user.has_full_book || false,
|
||||
purchasedSections,
|
||||
purchasedCount: purchasedSections.length,
|
||||
earnings: parseFloat(user.earnings) || 0,
|
||||
pendingEarnings: parseFloat(user.pending_earnings) || 0,
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('[PurchaseStatus] 查询失败:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '查询购买状态失败'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
140
app/api/user/reading-progress/route.ts
Normal file
140
app/api/user/reading-progress/route.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 阅读进度上报接口
|
||||
* POST /api/user/reading-progress
|
||||
*
|
||||
* 接收小程序上报的阅读进度,用于数据分析和断点续读
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { userId, sectionId, progress, duration, status, completedAt } = body
|
||||
|
||||
// 参数校验
|
||||
if (!userId || !sectionId) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '缺少必要参数'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// 查询是否已有记录
|
||||
const existingRows = await query(`
|
||||
SELECT id, progress, duration, status, first_open_at
|
||||
FROM reading_progress
|
||||
WHERE user_id = ? AND section_id = ?
|
||||
`, [userId, sectionId]) as any[]
|
||||
|
||||
const now = new Date()
|
||||
|
||||
if (existingRows.length > 0) {
|
||||
// 更新已有记录
|
||||
const existing = existingRows[0]
|
||||
|
||||
// 只更新更大的进度
|
||||
const newProgress = Math.max(existing.progress || 0, progress || 0)
|
||||
const newDuration = (existing.duration || 0) + (duration || 0)
|
||||
const newStatus = status || existing.status || 'reading'
|
||||
|
||||
await query(`
|
||||
UPDATE reading_progress
|
||||
SET
|
||||
progress = ?,
|
||||
duration = ?,
|
||||
status = ?,
|
||||
completed_at = ?,
|
||||
last_open_at = ?,
|
||||
updated_at = ?
|
||||
WHERE user_id = ? AND section_id = ?
|
||||
`, [
|
||||
newProgress,
|
||||
newDuration,
|
||||
newStatus,
|
||||
completedAt ? new Date(completedAt) : existing.completed_at,
|
||||
now,
|
||||
now,
|
||||
userId,
|
||||
sectionId
|
||||
])
|
||||
|
||||
console.log('[ReadingProgress] 更新进度:', { userId, sectionId, progress: newProgress, duration: newDuration })
|
||||
} else {
|
||||
// 插入新记录
|
||||
await query(`
|
||||
INSERT INTO reading_progress
|
||||
(user_id, section_id, progress, duration, status, completed_at, first_open_at, last_open_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, [
|
||||
userId,
|
||||
sectionId,
|
||||
progress || 0,
|
||||
duration || 0,
|
||||
status || 'reading',
|
||||
completedAt ? new Date(completedAt) : null,
|
||||
now,
|
||||
now
|
||||
])
|
||||
|
||||
console.log('[ReadingProgress] 新增进度:', { userId, sectionId, progress, duration })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '进度已保存'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ReadingProgress] 保存失败:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '保存进度失败'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户的阅读进度列表
|
||||
* GET /api/user/reading-progress?userId=xxx
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const userId = searchParams.get('userId')
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '缺少 userId 参数'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const rows = await query(`
|
||||
SELECT
|
||||
section_id,
|
||||
progress,
|
||||
duration,
|
||||
status,
|
||||
completed_at,
|
||||
first_open_at,
|
||||
last_open_at
|
||||
FROM reading_progress
|
||||
WHERE user_id = ?
|
||||
ORDER BY last_open_at DESC
|
||||
`, [userId]) as any[]
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: rows
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ReadingProgress] 查询失败:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '查询进度失败'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user