优化小程序支付流程,新增订单插入逻辑,确保支付成功后更新订单状态并处理佣金分配。同时,重构阅读页面,增强权限管理和阅读追踪功能,提升用户体验。

This commit is contained in:
乘风
2026-02-04 21:36:26 +08:00
parent 25fd3190b2
commit 67ef87095f
48 changed files with 9619 additions and 1218 deletions

View 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 })
}
}

View 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 })
}
}

View 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
})
}
}

View File

@@ -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,

View File

@@ -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)
// 清理失败不影响主流程
}
}

View File

@@ -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', {

View File

@@ -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)
}

View File

@@ -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")

View File

@@ -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)
}

View 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 })
}
}

View 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 })
}
}

View 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 })
}
}