新增订单推荐人和邀请码功能,优化支付流程中的订单插入逻辑,确保订单记录准确。更新小程序支付请求,支持传递邀请码以便于分销归属和对账。同时,调整数据库结构以支持新字段,提升系统的稳定性和用户体验。

This commit is contained in:
乘风
2026-02-06 18:34:02 +08:00
parent f8fac00c85
commit 2e65d68e1e
34 changed files with 3288 additions and 1255 deletions

View File

@@ -265,7 +265,7 @@ export default function DistributionAdminPage() {
user_name: w.userNickname || w.user_name,
created_at: w.createdAt || w.created_at,
completed_at: w.processedAt || w.completed_at,
// 状态统一(数据库用 success/failed前端显示用 completed/rejected
account: w.account ?? '未绑定微信号',
status: w.status === 'success' ? 'completed' : (w.status === 'failed' ? 'rejected' : w.status)
}))
setWithdrawals(formattedWithdrawals)

View File

@@ -7,6 +7,7 @@ import { NextResponse } from 'next/server'
import fs from 'fs'
import path from 'path'
import { requireAdminResponse } from '@/lib/admin-auth'
import { query } from '@/lib/db'
// 获取书籍目录
const BOOK_DIR = path.join(process.cwd(), 'book')
@@ -286,27 +287,38 @@ export async function POST(request: Request) {
console.log('[AdminChapters] 更新章节:', { action, chapterId })
switch (action) {
case 'updatePrice':
// 更新章节价格
// TODO: 保存到数据库
case 'updatePrice': {
if (data?.price != null && chapterId) {
await query('UPDATE chapters SET price = ?, updated_at = NOW() WHERE id = ?', [Number(data.price), chapterId])
}
return NextResponse.json({
success: true,
data: { message: '价格更新成功', chapterId, price: data.price }
data: { message: '价格更新成功', chapterId, price: data?.price }
})
}
case 'toggleFree':
// 切换免费状态
case 'toggleFree': {
if (data?.isFree != null && chapterId) {
await query('UPDATE chapters SET is_free = ?, updated_at = NOW() WHERE id = ?', [!!data.isFree, chapterId])
}
return NextResponse.json({
success: true,
data: { message: '免费状态更新成功', chapterId, isFree: data.isFree }
data: { message: '免费状态更新成功', chapterId, isFree: data?.isFree }
})
}
case 'updateStatus':
// 更新发布状态
case 'updateStatus': {
if (data?.status && chapterId) {
const s = String(data.status)
if (['draft', 'published', 'archived'].includes(s)) {
await query('UPDATE chapters SET status = ?, updated_at = NOW() WHERE id = ?', [s, chapterId])
}
}
return NextResponse.json({
success: true,
data: { message: '发布状态更新成功', chapterId, status: data.status }
data: { message: '发布状态更新成功', chapterId, status: data?.status }
})
}
default:
return NextResponse.json({

View File

@@ -88,8 +88,22 @@ export async function PUT(req: NextRequest) {
)
}
// TODO: 根据ID找到文件并更新
// id 格式: category/filename无 .md
const filePath = path.join(BOOK_DIR, `${id}.md`)
if (!fs.existsSync(filePath)) {
return NextResponse.json(
{ error: '章节文件不存在' },
{ status: 404 }
)
}
const markdownContent = matter.stringify(content ?? '', {
title: title ?? id.split('/').pop(),
date: new Date().toISOString(),
tags: tags || [],
draft: false
})
fs.writeFileSync(filePath, markdownContent, 'utf-8')
return NextResponse.json({
success: true,
message: '章节更新成功'
@@ -117,8 +131,15 @@ export async function DELETE(req: NextRequest) {
)
}
// TODO: 根据ID删除文件
const filePath = path.join(BOOK_DIR, `${id}.md`)
if (!fs.existsSync(filePath)) {
return NextResponse.json(
{ error: '章节文件不存在' },
{ status: 404 }
)
}
fs.unlinkSync(filePath)
return NextResponse.json({
success: true,
message: '章节删除成功'

View File

@@ -1,60 +1,61 @@
/**
* 后台提现 API
* GET: 查提现记录。PUT: 查库 + 更新提现状态 + 更新用户已提现金额(当前未接入微信打款
* 后台提现 API - 使用 Prisma ORM
* GET: 查提现记录(包含用户信息、收款账号=微信号
* PUT: 审批提现 = 调用微信打款 + 更新状态 + 更新用户已提现金额
*/
import { NextResponse } from 'next/server'
import { query } from '@/lib/db'
import { prisma } from '@/lib/prisma'
import { requireAdminResponse } from '@/lib/admin-auth'
import { createTransfer } from '@/lib/wechat-transfer'
/** 安全转数组,避免 undefined.length绝不返回 undefined */
function toArray<T = any>(x: unknown): T[] {
if (x == null) return []
if (Array.isArray(x)) return x as T[]
if (typeof x === 'object' && x !== null) return [x] as T[]
return []
}
/** 安全取数组长度,避免对 undefined 读 .length */
function safeLength(x: unknown): number {
if (x == null) return 0
if (Array.isArray(x)) return x.length
return 0
}
// ========== GET只查提现记录 ==========
// ========== GET查询提现记录带用户信息、收款账号=微信号)==========
export async function GET(request: Request) {
console.log('[Withdrawals] GET 开始')
try {
const authErr = requireAdminResponse(request)
if (authErr) return authErr
const sql = `
SELECT
w.id, w.user_id, w.amount, w.status, w.created_at,
u.nickname as user_nickname, u.avatar as user_avatar
FROM withdrawals w
LEFT JOIN users u ON w.user_id = u.id
ORDER BY w.created_at DESC
LIMIT 100
`
const result = await query(sql)
const rows = toArray(result)
const withdrawalsData = await prisma.withdrawals.findMany({
take: 100,
orderBy: { created_at: 'desc' },
select: {
id: true,
user_id: true,
amount: true,
status: true,
created_at: true,
wechat_id: true,
}
})
const withdrawals = rows.map((w: any) => ({
id: w.id,
user_id: w.user_id,
user_name: w.user_nickname || '未知用户',
userAvatar: w.user_avatar,
amount: parseFloat(w.amount) || 0,
status: w.status === 'success' ? 'completed' : (w.status === 'failed' ? 'rejected' : w.status),
created_at: w.created_at,
method: 'wechat',
}))
const userIds = [...new Set(withdrawalsData.map(w => w.user_id))]
const users = await prisma.users.findMany({
where: { id: { in: userIds } },
select: { id: true, nickname: true, avatar: true, wechat_id: true }
})
const userMap = new Map(users.map(u => [u.id, u]))
const withdrawals = withdrawalsData.map(w => {
const user = userMap.get(w.user_id)
return {
id: w.id,
user_id: w.user_id,
user_name: user?.nickname || '未知用户',
userAvatar: user?.avatar,
amount: Number(w.amount),
status: w.status === 'success' ? 'completed' :
w.status === 'failed' ? 'rejected' :
w.status,
created_at: w.created_at,
method: 'wechat',
account: w.wechat_id || user?.wechat_id || '未绑定微信号',
}
})
return NextResponse.json({
success: true,
withdrawals,
stats: { total: safeLength(withdrawals) },
stats: { total: withdrawals.length },
})
} catch (error: any) {
console.error('[Withdrawals] GET 失败:', error?.message)
@@ -65,69 +66,112 @@ export async function GET(request: Request) {
}
}
// ========== PUT查库 + 更新状态 + 更新用户已提现,暂不调用微信打款 ==========
// ========== PUT审批提现(使用 Prisma 事务)==========
export async function PUT(request: Request) {
const STEP = '[Withdrawals PUT]'
try {
console.log(STEP, '1. 开始')
const authErr = requireAdminResponse(request)
if (authErr) return authErr
console.log(STEP, '2. 鉴权通过')
const body = await request.json()
console.log(STEP, '3. body 已解析', typeof body, body ? 'ok' : 'null')
const id = body?.id
const action = body?.action
const rejectReason = body?.errorMessage || body?.reason || '管理员拒绝'
const { id, action, errorMessage, reason } = body
const rejectReason = errorMessage || reason || '管理员拒绝'
if (!id || !action) {
return NextResponse.json({ success: false, error: '缺少 id 或 action' }, { status: 400 })
}
console.log(STEP, '4. id/action 有效', id, action)
console.log(STEP, '2. id/action 有效', id, action)
console.log(STEP, '5. 即将 query SELECT')
const result = await query('SELECT * FROM withdrawals WHERE id = ?', [id])
console.log(STEP, '6. query 返回', typeof result, 'length=', safeLength(result))
const rows = toArray<any>(result)
console.log(STEP, '7. toArray 后 length=', safeLength(rows))
if (safeLength(rows) === 0) {
const withdrawal = await prisma.withdrawals.findUnique({
where: { id },
select: { id: true, user_id: true, amount: true, status: true, wechat_openid: true }
})
if (!withdrawal) {
return NextResponse.json({ success: false, error: '提现记录不存在' }, { status: 404 })
}
console.log(STEP, '8. 有记录')
const row = rows[0]
console.log(STEP, '9. row', row ? 'ok' : 'null')
if (row.status !== 'pending') {
if (withdrawal.status !== 'pending') {
return NextResponse.json({ success: false, error: '该记录已处理,不可重复审批' }, { status: 400 })
}
console.log(STEP, '10. 状态 pending')
const amount = parseFloat(String(row.amount ?? 0)) || 0
const userId = String(row.user_id ?? '')
console.log(STEP, '11. amount/userId', amount, userId)
const amount = Number(withdrawal.amount)
const userId = withdrawal.user_id
const openid = withdrawal.wechat_openid
if (action === 'approve') {
console.log(STEP, '12. 执行 approve UPDATE withdrawals')
await query(
`UPDATE withdrawals SET status = 'success', processed_at = NOW(), transaction_id = ? WHERE id = ?`,
[`manual_${Date.now()}`, id]
)
console.log(STEP, '13. 执行 approve UPDATE users')
await query(
`UPDATE users SET withdrawn_earnings = COALESCE(withdrawn_earnings, 0) + ? WHERE id = ?`,
[amount, userId]
)
console.log(STEP, '14. 批准完成')
return NextResponse.json({ success: true, message: '已批准,已更新提现状态与用户已提现金额(未发起微信打款)' })
console.log(STEP, '3. 发起微信打款')
if (!openid) {
return NextResponse.json({
success: false,
error: '该提现记录无微信 openid无法打款请线下处理'
}, { status: 400 })
}
const amountFen = Math.round(amount * 100)
const transferResult = await createTransfer({
openid,
amountFen,
outDetailNo: id,
transferRemark: '提现',
})
if (!transferResult.success) {
console.error(STEP, '微信打款失败:', transferResult.errorCode, transferResult.errorMessage)
await prisma.withdrawals.update({
where: { id },
data: {
status: 'failed',
processed_at: new Date(),
error_message: transferResult.errorMessage || transferResult.errorCode || '打款失败'
}
})
return NextResponse.json({
success: false,
error: '微信打款失败: ' + (transferResult.errorMessage || transferResult.errorCode || '未知错误')
}, { status: 500 })
}
const batchNo = transferResult.outBatchNo || `B${Date.now()}`
console.log(STEP, '4. 打款成功,更新数据库')
await prisma.$transaction(async (tx) => {
await tx.withdrawals.update({
where: { id },
data: {
status: 'success',
processed_at: new Date(),
transaction_id: transferResult.batchId || batchNo
}
})
const user = await tx.users.findUnique({
where: { id: userId },
select: { withdrawn_earnings: true }
})
await tx.users.update({
where: { id: userId },
data: {
withdrawn_earnings: Number(user?.withdrawn_earnings || 0) + amount
}
})
})
console.log(STEP, '5. 批准完成,钱已打到用户微信零钱')
return NextResponse.json({
success: true,
message: '已批准并打款成功,款项将到账用户微信零钱'
})
}
if (action === 'reject') {
console.log(STEP, '15. 执行 reject UPDATE')
await query(
`UPDATE withdrawals SET status = 'failed', processed_at = NOW(), error_message = ? WHERE id = ?`,
[rejectReason, id]
)
console.log(STEP, '16. 拒绝完成')
console.log(STEP, '5. 执行拒绝操作')
await prisma.withdrawals.update({
where: { id },
data: {
status: 'failed',
processed_at: new Date(),
error_message: rejectReason
}
})
console.log(STEP, '6. 拒绝完成')
return NextResponse.json({ success: true, message: '已拒绝该提现申请' })
}

View File

@@ -5,34 +5,33 @@ import { query } from '@/lib/db'
export async function GET() {
try {
// 方案1: 优先从数据库读取章节数据
// 方案1: 优先从数据库读取(使用实际存在的 chapters 表,不是 sections
try {
const dbChapters = await query(`
SELECT
id, section_id, title, section_title, content,
is_free, price, words, section_order, chapter_order,
created_at, updated_at
FROM sections
ORDER BY section_order ASC, chapter_order ASC
`) as any[]
id, part_id, part_title, chapter_id, chapter_title, section_title, content,
word_count, is_free, price, sort_order, created_at, updated_at
FROM chapters
WHERE status = 'published'
ORDER BY sort_order ASC
`, []) as any[]
if (dbChapters && dbChapters.length > 0) {
console.log('[All Chapters API] 从数据库读取成功,共', dbChapters.length, '章')
console.log('[All Chapters API] 从数据库 chapters 表读取成功,共', dbChapters.length, '章')
// 格式化数据
const allChapters = dbChapters.map((chapter: any) => ({
id: chapter.id,
sectionId: chapter.section_id,
title: chapter.title,
sectionTitle: chapter.section_title,
content: chapter.content,
isFree: !!chapter.is_free,
price: chapter.price || 0,
words: chapter.words || Math.floor(Math.random() * 3000) + 2000,
sectionOrder: chapter.section_order,
chapterOrder: chapter.chapter_order,
createdAt: chapter.created_at,
updatedAt: chapter.updated_at
const allChapters = dbChapters.map((row: any, idx: number) => ({
id: row.id,
sectionId: row.chapter_id || row.part_id,
title: row.chapter_title || row.section_title,
sectionTitle: row.part_title || row.section_title,
content: row.content,
isFree: !!row.is_free,
price: Number(row.price) || 0,
words: row.word_count || Math.floor(Math.random() * 3000) + 2000,
sectionOrder: row.sort_order ?? idx + 1,
chapterOrder: idx + 1,
createdAt: row.created_at,
updatedAt: row.updated_at
}))
return NextResponse.json({

View File

@@ -1,18 +1,12 @@
// app/api/book/chapters/route.ts
// 章节管理API - 支持列表查询、新增、编辑
// 开发: 卡若
// 日期: 2026-01-25
// 章节管理API - 使用 Prisma ORM
// 优势类型安全、防SQL注入、简化查询
import { NextRequest, NextResponse } from 'next/server'
import { query } from '@/lib/db'
import { prisma } from '@/lib/prisma'
/**
* GET - 获取章节列表
* 支持参数:
* - partId: 按篇筛选
* - status: 按状态筛选 (draft/published/archived)
* - page: 页码
* - pageSize: 每页数量
*/
export async function GET(req: NextRequest) {
try {
@@ -22,48 +16,44 @@ export async function GET(req: NextRequest) {
const page = parseInt(searchParams.get('page') || '1')
const pageSize = parseInt(searchParams.get('pageSize') || '100')
let sql = `
SELECT id, part_id, part_title, chapter_id, chapter_title, section_title,
word_count, is_free, price, sort_order, status, created_at, updated_at
FROM chapters
WHERE 1=1
`
const params: any[] = []
// 构建 Prisma where 条件
const where: any = {}
if (partId) where.part_id = partId
if (status && status !== 'all') where.status = status as any
if (partId) {
sql += ' AND part_id = ?'
params.push(partId)
}
if (status && status !== 'all') {
sql += ' AND status = ?'
params.push(status)
}
sql += ' ORDER BY sort_order ASC'
sql += ' LIMIT ? OFFSET ?'
params.push(pageSize, (page - 1) * pageSize)
const results = await query(sql, params) as any[]
// 获取总数
let countSql = 'SELECT COUNT(*) as total FROM chapters WHERE 1=1'
const countParams: any[] = []
if (partId) {
countSql += ' AND part_id = ?'
countParams.push(partId)
}
if (status && status !== 'all') {
countSql += ' AND status = ?'
countParams.push(status)
}
const countResult = await query(countSql, countParams) as any[]
const total = countResult[0]?.total || 0
// 使用 Prisma 分页查询
const [results, total] = await Promise.all([
prisma.chapters.findMany({
where,
orderBy: { sort_order: 'asc' },
skip: (page - 1) * pageSize,
take: pageSize,
select: {
id: true,
part_id: true,
part_title: true,
chapter_id: true,
chapter_title: true,
section_title: true,
word_count: true,
is_free: true,
price: true,
sort_order: true,
status: true,
created_at: true,
updated_at: true
}
}),
prisma.chapters.count({ where })
])
return NextResponse.json({
success: true,
data: {
list: results,
list: results.map(r => ({
...r,
price: Number(r.price)
})),
total,
page,
pageSize,
@@ -86,77 +76,62 @@ export async function POST(req: NextRequest) {
try {
const body = await req.json()
const {
id,
partId,
partTitle,
chapterId,
chapterTitle,
sectionTitle,
content,
isFree = false,
price = 1,
sortOrder,
status = 'published'
id, partId, partTitle, chapterId, chapterTitle, sectionTitle,
content, wordCount, isFree, price, sortOrder, status
} = body
// 验证必填字段
if (!id || !partId || !partTitle || !chapterId || !chapterTitle || !sectionTitle || !content) {
if (!id || !partId || !chapterId) {
return NextResponse.json(
{ success: false, error: '缺少必字段' },
{ success: false, error: '缺少必字段' },
{ status: 400 }
)
}
// 检查ID是否已存在
const existing = await query('SELECT id FROM chapters WHERE id = ?', [id]) as any[]
if (existing.length > 0) {
// 使用 Prisma 创建章节
const chapter = await prisma.chapters.create({
data: {
id,
part_id: partId,
part_title: partTitle || '',
chapter_id: chapterId,
chapter_title: chapterTitle || '',
section_title: sectionTitle || '',
content: content || '',
word_count: wordCount || 0,
is_free: isFree || false,
price: price || 1,
sort_order: sortOrder || 0,
status: (status as any) || 'published'
}
})
return NextResponse.json({
success: true,
message: '章节创建成功',
data: { ...chapter, price: Number(chapter.price) }
})
} catch (error: any) {
console.error('[Chapters API] 创建失败:', error)
if (error.code === 'P2002') {
return NextResponse.json(
{ success: false, error: '章节ID已存在' },
{ status: 400 }
)
}
// 计算字数
const wordCount = content.replace(/\s/g, '').length
// 计算排序顺序(如果未提供)
let order = sortOrder
if (order === undefined || order === null) {
const maxOrder = await query(
'SELECT MAX(sort_order) as maxOrder FROM chapters WHERE part_id = ?',
[partId]
) as any[]
order = (maxOrder[0]?.maxOrder || 0) + 1
}
await query(`
INSERT INTO chapters (id, part_id, part_title, chapter_id, chapter_title, section_title, content, word_count, is_free, price, sort_order, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [id, partId, partTitle, chapterId, chapterTitle, sectionTitle, content, wordCount, isFree, isFree ? 0 : price, order, status])
console.log('[Chapters API] 新增章节成功:', id)
return NextResponse.json({
success: true,
message: '章节创建成功',
data: { id, wordCount, sortOrder: order }
})
} catch (error) {
console.error('[Chapters API] 新增章节失败:', error)
return NextResponse.json(
{ success: false, error: '新增章节失败' },
{ success: false, error: '创建章节失败' },
{ status: 500 }
)
}
}
/**
* PUT - 编辑章节
* PUT - 更新章节
*/
export async function PUT(req: NextRequest) {
try {
const body = await req.json()
const { id, ...updates } = body
const { id, ...updateData } = body
if (!id) {
return NextResponse.json(
@@ -165,63 +140,37 @@ export async function PUT(req: NextRequest) {
)
}
// 检查章节是否存在
const existing = await query('SELECT id FROM chapters WHERE id = ?', [id]) as any[]
if (existing.length === 0) {
// 构建更新数据
const data: any = { updated_at: new Date() }
if (updateData.partTitle !== undefined) data.part_title = updateData.partTitle
if (updateData.chapterTitle !== undefined) data.chapter_title = updateData.chapterTitle
if (updateData.sectionTitle !== undefined) data.section_title = updateData.sectionTitle
if (updateData.content !== undefined) data.content = updateData.content
if (updateData.wordCount !== undefined) data.word_count = updateData.wordCount
if (updateData.isFree !== undefined) data.is_free = updateData.isFree
if (updateData.price !== undefined) data.price = updateData.price
if (updateData.sortOrder !== undefined) data.sort_order = updateData.sortOrder
if (updateData.status !== undefined) data.status = updateData.status
// 使用 Prisma 更新章节
const chapter = await prisma.chapters.update({
where: { id },
data
})
return NextResponse.json({
success: true,
message: '章节更新成功',
data: { ...chapter, price: Number(chapter.price) }
})
} catch (error: any) {
console.error('[Chapters API] 更新失败:', error)
if (error.code === 'P2025') {
return NextResponse.json(
{ success: false, error: '章节不存在' },
{ status: 404 }
)
}
// 构建更新语句
const allowedFields = ['part_id', 'part_title', 'chapter_id', 'chapter_title', 'section_title', 'content', 'is_free', 'price', 'sort_order', 'status']
const fieldMapping: Record<string, string> = {
partId: 'part_id',
partTitle: 'part_title',
chapterId: 'chapter_id',
chapterTitle: 'chapter_title',
sectionTitle: 'section_title',
isFree: 'is_free',
sortOrder: 'sort_order'
}
const setClauses: string[] = []
const params: any[] = []
for (const [key, value] of Object.entries(updates)) {
const dbField = fieldMapping[key] || key
if (allowedFields.includes(dbField) && value !== undefined) {
setClauses.push(`${dbField} = ?`)
params.push(value)
}
}
// 如果更新了content重新计算字数
if (updates.content) {
const wordCount = updates.content.replace(/\s/g, '').length
setClauses.push('word_count = ?')
params.push(wordCount)
}
if (setClauses.length === 0) {
return NextResponse.json(
{ success: false, error: '没有可更新的字段' },
{ status: 400 }
)
}
params.push(id)
await query(`UPDATE chapters SET ${setClauses.join(', ')} WHERE id = ?`, params)
console.log('[Chapters API] 更新章节成功:', id)
return NextResponse.json({
success: true,
message: '章节更新成功'
})
} catch (error) {
console.error('[Chapters API] 更新章节失败:', error)
return NextResponse.json(
{ success: false, error: '更新章节失败' },
{ status: 500 }
@@ -230,7 +179,7 @@ export async function PUT(req: NextRequest) {
}
/**
* DELETE - 删除章节软删除改状态为archived
* DELETE - 删除章节
*/
export async function DELETE(req: NextRequest) {
try {
@@ -244,17 +193,23 @@ export async function DELETE(req: NextRequest) {
)
}
// 软删除改状态为archived
await query("UPDATE chapters SET status = 'archived' WHERE id = ?", [id])
console.log('[Chapters API] 删除章节成功:', id)
// 使用 Prisma 删除章节
await prisma.chapters.delete({
where: { id }
})
return NextResponse.json({
success: true,
message: '章节删除'
message: '章节删除成功'
})
} catch (error) {
console.error('[Chapters API] 删除章节失败:', error)
} catch (error: any) {
console.error('[Chapters API] 删除失败:', error)
if (error.code === 'P2025') {
return NextResponse.json(
{ success: false, error: '章节不存在' },
{ status: 404 }
)
}
return NextResponse.json(
{ success: false, error: '删除章节失败' },
{ status: 500 }

View File

@@ -0,0 +1,26 @@
/**
* 书籍统计 API - 供关于页等展示章节数量
* GET /api/book/stats -> { success, data: { totalChapters } }
*/
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET() {
try {
const totalChapters = await prisma.chapters.count({
where: { status: 'published' }
})
return NextResponse.json({
success: true,
data: {
totalChapters,
},
})
} catch (e) {
console.warn('[Book Stats] 查询失败:', (e as Error).message)
return NextResponse.json({
success: true,
data: { totalChapters: 0 },
})
}
}

View File

@@ -1,367 +1,137 @@
/**
* 系统配置API
* 优先读取数据库配置,失败时读取本地默认配置
* 支持配置的增删改查
* 优先 Prisma失败时回退到 lib/db避免 Prisma 连接池超时导致 500
*/
import { NextRequest, NextResponse } from 'next/server'
import { query, getConfig, setConfig } from '@/lib/db'
// 本地默认配置(作为数据库备份)
const DEFAULT_CONFIGS: Record<string, any> = {
// 站点配置
site_config: {
siteName: 'Soul创业派对',
siteDescription: '来自派对房的真实商业故事',
logo: '/icon.svg',
keywords: ['创业', 'Soul', '私域运营', '商业案例'],
icp: '',
analytics: ''
},
// 匹配功能配置
match_config: {
matchTypes: [
{ id: 'partner', label: '创业合伙', matchLabel: '创业伙伴', icon: '⭐', matchFromDB: true, showJoinAfterMatch: false, enabled: true },
{ id: 'investor', label: '资源对接', matchLabel: '资源对接', icon: '👥', matchFromDB: false, showJoinAfterMatch: true, enabled: true },
{ id: 'mentor', label: '导师顾问', matchLabel: '商业顾问', icon: '❤️', matchFromDB: false, showJoinAfterMatch: true, enabled: true },
{ id: 'team', label: '团队招募', matchLabel: '加入项目', icon: '🎮', matchFromDB: false, showJoinAfterMatch: true, enabled: true }
],
freeMatchLimit: 3,
matchPrice: 1,
settings: {
enableFreeMatches: true,
enablePaidMatches: true,
maxMatchesPerDay: 10
}
},
// 分销配置
referral_config: {
distributorShare: 90,
minWithdrawAmount: 10,
bindingDays: 30,
userDiscount: 5,
enableAutoWithdraw: false
},
// 价格配置
price_config: {
sectionPrice: 1,
fullBookPrice: 9.9,
premiumBookPrice: 19.9,
matchPrice: 1
},
// 支付配置
payment_config: {
wechat: {
enabled: true,
appId: 'wx432c93e275548671',
mchId: '1318592501'
},
alipay: {
enabled: true,
pid: '2088511801157159'
},
wechatGroupUrl: '' // 支付成功后跳转的微信群链接
},
// 书籍配置
book_config: {
totalSections: 62,
freeSections: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3'],
latestSectionId: '9.14'
},
// 功能开关配置
feature_config: {
matchEnabled: true, // 找伙伴功能开关(默认开启)
referralEnabled: true, // 推广功能开关
searchEnabled: true, // 搜索功能开关
aboutEnabled: true // 关于页面开关
}
}
import { prisma } from '@/lib/prisma'
import { getPrismaConfig, setPrismaConfig } from '@/lib/prisma-helpers'
import { query } from '@/lib/db'
/**
* GET - 获取配置
* 参数: key - 配置键名,不传则返回所有配置
*/
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const key = searchParams.get('key')
const forceLocal = searchParams.get('forceLocal') === 'true'
try {
if (key) {
// 获取单个配置
let config = null
if (!forceLocal) {
// 优先从数据库读取
try {
config = await getConfig(key)
} catch (e) {
console.log(`[Config API] 数据库读取${key}失败,使用本地配置`)
}
let config = await getPrismaConfig(key)
if (config == null) {
const rows = await query(
'SELECT config_value FROM system_config WHERE config_key = ?',
[key]
) as any[]
config = rows[0]?.config_value ?? null
}
// 数据库没有则使用本地默认
if (!config) {
config = DEFAULT_CONFIGS[key] || null
}
if (config) {
return NextResponse.json({
success: true,
key,
config,
source: config === DEFAULT_CONFIGS[key] ? 'local' : 'database'
})
}
return NextResponse.json({
success: false,
error: '配置不存在'
}, { status: 404 })
success: true,
data: config
})
}
// 获取所有配置
const allConfigs: Record<string, any> = {}
const sources: Record<string, string> = {}
for (const configKey of Object.keys(DEFAULT_CONFIGS)) {
let config = null
if (!forceLocal) {
try {
config = await getConfig(configKey)
} catch (e) {
// 忽略数据库错误
}
}
if (config) {
allConfigs[configKey] = config
sources[configKey] = 'database'
} else {
allConfigs[configKey] = DEFAULT_CONFIGS[configKey]
sources[configKey] = 'local'
}
}
// 获取小程序配置
let mpConfig = null
let configs: any[]
try {
mpConfig = await getConfig('mp_config')
} catch (e) {}
// 提取前端需要的格式
const bookConfig = allConfigs.book_config || DEFAULT_CONFIGS.book_config
const featureConfig = allConfigs.feature_config || DEFAULT_CONFIGS.feature_config
configs = await prisma.system_config.findMany({
orderBy: { config_key: 'asc' }
})
} catch (e) {
const rows = await query(
'SELECT id, config_key, config_value, description, created_at, updated_at FROM system_config ORDER BY config_key ASC'
) as any[]
configs = rows || []
}
return NextResponse.json({
success: true,
configs: allConfigs,
sources,
// 前端直接使用的格式
freeChapters: bookConfig.freeSections || DEFAULT_CONFIGS.book_config.freeSections,
features: featureConfig, // 功能开关
mpConfig: mpConfig || {
appId: 'wxb8bbb2b10dec74aa',
apiDomain: 'https://soul.quwanzhi.com',
buyerDiscount: 5,
referralBindDays: 30,
minWithdraw: 10
}
data: configs
})
} catch (error) {
console.error('[Config API] GET错误:', error)
return NextResponse.json({
success: false,
error: '获取配置失败: ' + (error as Error).message
error: '获取配置失败'
}, { status: 500 })
}
}
/**
* POST - 保存配置到数据库
* 支持两种格式:
* 1. { key, config } - 单个配置
* 2. { freeChapters, mpConfig } - 批量配置
* POST - 创建/更新配置
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { key, value, description } = body
// 支持批量配置格式
if (body.freeChapters || body.mpConfig) {
let successCount = 0
// 保存免费章节配置
if (body.freeChapters) {
const bookConfig = {
...DEFAULT_CONFIGS.book_config,
freeSections: body.freeChapters
}
const success = await setConfig('book_config', bookConfig, '书籍配置-免费章节')
if (success) successCount++
}
// 保存小程序配置
if (body.mpConfig) {
const success = await setConfig('mp_config', body.mpConfig, '小程序配置')
if (success) successCount++
}
return NextResponse.json({
success: true,
message: `配置保存成功 (${successCount}项)`,
successCount
})
}
// 原有的单配置格式
const { key, config, description } = body
if (!key || !config) {
if (!key) {
return NextResponse.json({
success: false,
error: '配置键名和配置值不能为空'
error: '配置键不能为空'
}, { status: 400 })
}
console.log(`[Config API] 保存配置 ${key}:`, config)
// 保存到数据库
const success = await setConfig(key, config, description)
if (success) {
// 验证保存结果
const saved = await getConfig(key)
console.log(`[Config API] 验证保存结果 ${key}:`, saved)
return NextResponse.json({
success: true,
message: '配置保存成功',
key,
savedConfig: saved // 返回实际保存的配置
})
} else {
return NextResponse.json({
success: false,
error: '配置保存失败'
}, { status: 500 })
const valueStr = typeof value === 'string' ? value : JSON.stringify(value ?? null)
try {
await setPrismaConfig(key, value, description)
} catch (e) {
await query(
`INSERT INTO system_config (config_key, config_value, description, updated_at) VALUES (?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE config_value = ?, description = ?, updated_at = NOW()`,
[key, valueStr, description || null, valueStr, description || null]
)
}
return NextResponse.json({
success: true,
message: '配置保存成功'
})
} catch (error) {
console.error('[Config API] POST错误:', error)
return NextResponse.json({
success: false,
error: '保存配置失败: ' + (error as Error).message
error: '保存配置失败'
}, { status: 500 })
}
}
/**
* PUT - 批量更新配置
* DELETE - 删除配置
*/
export async function PUT(request: NextRequest) {
export async function DELETE(request: NextRequest) {
try {
const body = await request.json()
const { configs } = body
const { searchParams } = new URL(request.url)
const key = searchParams.get('key')
if (!configs || typeof configs !== 'object') {
if (!key) {
return NextResponse.json({
success: false,
error: '配置数据格式错误'
error: '配置键不能为空'
}, { status: 400 })
}
let successCount = 0
let failedCount = 0
for (const [key, config] of Object.entries(configs)) {
try {
const success = await setConfig(key, config)
if (success) {
successCount++
} else {
failedCount++
}
} catch (e) {
failedCount++
try {
await prisma.system_config.delete({
where: { config_key: key }
})
} catch (e: any) {
if (e.code === 'P2025') {
return NextResponse.json({
success: false,
error: '配置不存在'
}, { status: 404 })
}
await query('DELETE FROM system_config WHERE config_key = ?', [key])
}
return NextResponse.json({
success: true,
message: `配置更新完成:成功${successCount}个,失败${failedCount}`,
successCount,
failedCount
message: '配置删除成功'
})
} catch (error) {
console.error('[Config API] PUT错误:', error)
return NextResponse.json({
success: false,
error: '更新配置失败: ' + (error as Error).message
}, { status: 500 })
}
}
/**
* DELETE - 删除配置(恢复为本地默认)
*/
export async function DELETE(request: NextRequest) {
const { searchParams } = new URL(request.url)
const key = searchParams.get('key')
if (!key) {
return NextResponse.json({
success: false,
error: '配置键名不能为空'
}, { status: 400 })
}
try {
await query('DELETE FROM system_config WHERE config_key = ?', [key])
return NextResponse.json({
success: true,
message: '配置已删除,将使用本地默认值',
key
})
} catch (error) {
} catch (error: any) {
console.error('[Config API] DELETE错误:', error)
return NextResponse.json({
success: false,
error: '删除配置失败: ' + (error as Error).message
error: '删除配置失败'
}, { status: 500 })
}
}
/**
* 初始化:将本地配置同步到数据库
*/
export async function syncLocalToDatabase() {
console.log('[Config] 开始同步本地配置到数据库...')
for (const [key, config] of Object.entries(DEFAULT_CONFIGS)) {
try {
// 检查数据库是否已有该配置
const existing = await getConfig(key)
if (!existing) {
// 数据库没有,则写入
await setConfig(key, config, `默认${key}配置`)
console.log(`[Config] 同步配置: ${key}`)
}
} catch (e) {
console.error(`[Config] 同步${key}失败:`, e)
}
}
console.log('[Config] 配置同步完成')
}

View File

@@ -1,11 +1,11 @@
/**
* 查询订单支付状态 API
* 基于 Universal_Payment_Module v4.0 设计
*
* GET /api/payment/status/{orderSn}
* 从数据库 orders 表查询真实订单状态
*/
import { type NextRequest, NextResponse } from "next/server"
import { query } from "@/lib/db"
export async function GET(
request: NextRequest,
@@ -21,23 +21,49 @@ export async function GET(
)
}
// TODO: 从数据库查询订单状态
// const order = await OrderService.getByOrderSn(orderSn)
// 模拟返回数据(开发测试用)
const mockOrder = {
orderSn,
status: "created", // created | paying | paid | closed | refunded
paidAmount: null,
paidAt: null,
paymentMethod: null,
tradeSn: null,
const rows = await query(
"SELECT order_sn, status, amount, pay_time, transaction_id, product_type FROM orders WHERE order_sn = ?",
[orderSn]
) as any[]
if (!rows || rows.length === 0) {
return NextResponse.json({
code: 200,
message: "success",
data: {
orderSn,
status: "created",
paidAmount: null,
paidAt: null,
paymentMethod: null,
tradeSn: null,
},
})
}
const order = rows[0]
const statusMap: Record<string, string> = {
created: "created",
pending: "paying",
paid: "paid",
cancelled: "closed",
refunded: "refunded",
expired: "closed",
}
const frontStatus = statusMap[order.status] || order.status
return NextResponse.json({
code: 200,
message: "success",
data: mockOrder,
data: {
orderSn: order.order_sn,
status: frontStatus,
paidAmount: order.status === "paid" ? Number(order.amount) : null,
paidAt: order.pay_time || null,
paymentMethod: "wechat",
tradeSn: order.transaction_id || null,
productType: order.product_type,
},
})
} catch (error) {
console.error("[Payment] Query status error:", error)

View File

@@ -1,5 +1,5 @@
/**
* 推荐码绑定API - 增强版
* 推荐码绑定API - 使用 Prisma ORM
*
* 核心规则:
* 1. 链接带ID谁发的链接进的人就绑谁
@@ -10,20 +10,16 @@
*/
import { NextRequest, NextResponse } from 'next/server'
import { query, getConfig } from '@/lib/db'
import { prisma } from '@/lib/prisma'
import { getPrismaConfig } from '@/lib/prisma-helpers'
// 绑定有效期(天)- 默认值,优先从配置读取
const DEFAULT_BINDING_DAYS = 30
/**
* POST - 绑定推荐关系(支持抢夺机制)
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { userId, referralCode, openId, source } = body
// 验证参数
const effectiveUserId = userId || (openId ? `user_${openId.slice(-8)}` : null)
if (!effectiveUserId || !referralCode) {
return NextResponse.json({
@@ -35,29 +31,31 @@ export async function POST(request: NextRequest) {
// 获取绑定天数配置
let bindingDays = DEFAULT_BINDING_DAYS
try {
const config = await getConfig('referral_config')
const config = await getPrismaConfig('referral_config')
if (config?.bindingDays) {
bindingDays = Number(config.bindingDays)
}
} catch (e) {
console.warn('[Referral Bind] 读取配置失败,使用默认', DEFAULT_BINDING_DAYS)
console.warn('[Referral Bind] 使用默认配置', DEFAULT_BINDING_DAYS)
}
// 查找推荐人
const referrers = await query(
'SELECT id, nickname, referral_code FROM users WHERE referral_code = ?',
[referralCode]
) as any[]
// 查找推荐人(使用 Prisma
const referrer = await prisma.users.findUnique({
where: { referral_code: referralCode },
select: {
id: true,
nickname: true,
referral_code: true
}
})
if (referrers.length === 0) {
if (!referrer) {
return NextResponse.json({
success: false,
error: '推荐码无效'
}, { status: 400 })
}
const referrer = referrers[0]
// 不能自己推荐自己
if (referrer.id === effectiveUserId) {
return NextResponse.json({
@@ -67,138 +65,135 @@ export async function POST(request: NextRequest) {
}
// 检查用户是否存在
const users = await query(
'SELECT id FROM users WHERE id = ? OR open_id = ?',
[effectiveUserId, openId || effectiveUserId]
) as any[]
const user = await prisma.users.findFirst({
where: {
OR: [
{ id: effectiveUserId },
{ open_id: openId || effectiveUserId }
]
}
})
if (users.length === 0) {
if (!user) {
return NextResponse.json({
success: false,
error: '用户不存在'
}, { status: 400 })
}
const user = users[0]
const now = new Date()
// 检查现有绑定关系
const existingBindings = await query(`
SELECT id, referrer_id, expiry_date, status
FROM referral_bindings
WHERE referee_id = ? AND status = 'active'
ORDER BY binding_date DESC LIMIT 1
`, [user.id]) as any[]
const existingBinding = await prisma.referral_bindings.findFirst({
where: {
referee_id: user.id,
status: 'active'
},
orderBy: { binding_date: 'desc' }
})
let action = 'new' // new=新绑定, renew=续期, switch=立即切换
let action = 'new'
let oldReferrerId = null
if (existingBindings.length > 0) {
const existing = existingBindings[0]
// 同一个推荐人 - 续期刷新30天
if (existing.referrer_id === referrer.id) {
action = 'renew'
}
// 不同推荐人 - 立即切换(新逻辑:无条件切换)
else {
action = 'switch'
oldReferrerId = existing.referrer_id
// 将旧绑定标记为 cancelled被切换
await query(
"UPDATE referral_bindings SET status = 'cancelled' WHERE id = ?",
[existing.id]
)
console.log(`[Referral Bind] 立即切换: ${user.id}: ${oldReferrerId} -> ${referrer.id}`)
}
}
// 计算新的过期时间(从配置读取天数)
// 计算新的过期时间
const expiryDate = new Date()
expiryDate.setDate(expiryDate.getDate() + bindingDays)
// 创建或更新绑定记录
const bindingId = 'bind_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 6)
if (action === 'renew') {
// 续期:更新过期时间
await query(`
UPDATE referral_bindings
SET expiry_date = ?, binding_date = CURRENT_TIMESTAMP
WHERE referee_id = ? AND referrer_id = ? AND status = 'active'
`, [expiryDate, user.id, referrer.id])
console.log(`[Referral Bind] 续期: ${user.id} -> ${referrer.id},新过期时间: ${expiryDate.toISOString()}`)
if (existingBinding) {
if (existingBinding.referrer_id === referrer.id) {
// 同一个推荐人 - 续期
action = 'renew'
await prisma.referral_bindings.update({
where: { id: existingBinding.id },
data: {
expiry_date: expiryDate,
binding_date: new Date()
}
})
console.log(`[Referral Bind] 续期: ${user.id} -> ${referrer.id}`)
} else {
// 不同推荐人 - 立即切换
action = 'switch'
oldReferrerId = existingBinding.referrer_id
// 使用 Prisma 事务确保原子性
await prisma.$transaction([
// 将旧绑定标记为 cancelled
prisma.referral_bindings.update({
where: { id: existingBinding.id },
data: { status: 'cancelled' }
}),
// 创建新绑定
prisma.referral_bindings.create({
data: {
id: `bind_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,
referrer_id: referrer.id,
referee_id: user.id,
referral_code: referralCode,
status: 'active',
expiry_date: expiryDate,
binding_date: new Date()
}
})
])
console.log(`[Referral Bind] 立即切换: ${user.id}: ${oldReferrerId} -> ${referrer.id}`)
}
} else {
// 新绑定或切换
await query(`
INSERT INTO referral_bindings (
id, referrer_id, referee_id, referral_code, status, expiry_date, binding_date
) VALUES (?, ?, ?, ?, 'active', ?, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE
referrer_id = VALUES(referrer_id),
referral_code = VALUES(referral_code),
expiry_date = VALUES(expiry_date),
binding_date = CURRENT_TIMESTAMP,
status = 'active'
`, [bindingId, referrer.id, user.id, referralCode, expiryDate])
// 新绑定
await prisma.referral_bindings.create({
data: {
id: `bind_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,
referrer_id: referrer.id,
referee_id: user.id,
referral_code: referralCode,
status: 'active',
expiry_date: expiryDate,
binding_date: new Date()
}
})
// 注意:不再更新 users.referred_by已弃用只使用 referral_bindings
// 更新推荐人的推广数量
await prisma.users.update({
where: { id: referrer.id },
data: {
referral_count: { increment: 1 }
}
})
// 更新推荐人的推广数量(仅新绑定时)
if (action === 'new') {
await query(
'UPDATE users SET referral_count = referral_count + 1 WHERE id = ?',
[referrer.id]
)
console.log(`[Referral Bind] 新绑定: ${user.id} -> ${referrer.id}`)
}
// 如果是立即切换,更新双方的推广数量
if (action === 'switch' && oldReferrerId) {
// 减少旧推荐人的数量
await query(
'UPDATE users SET referral_count = GREATEST(referral_count - 1, 0) WHERE id = ?',
[oldReferrerId]
)
// 增加新推荐人的数量
await query(
'UPDATE users SET referral_count = referral_count + 1 WHERE id = ?',
[referrer.id]
)
console.log(`[Referral Bind] 立即切换完成: ${user.id}: ${oldReferrerId} -> ${referrer.id}`)
}
console.log(`[Referral Bind] 新绑定: ${user.id} -> ${referrer.id}`)
}
// 记录访问日志(用于统计「通过链接进的人数」
try {
await query(`
INSERT INTO referral_visits (referrer_id, visitor_id, source, created_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
`, [referrer.id, user.id, source || 'miniprogram'])
} catch (e) {
// 访问日志表可能不存在,忽略错误
}
const messages = {
new: '绑定成功',
renew: '绑定已续期',
switch: '已切换推荐人'
// 记录访问(如果有 referral_visits 表
if (source) {
try {
await prisma.referral_visits.create({
data: {
referrer_id: referrer.id,
visitor_id: user.id,
visitor_openid: openId || null,
source: source || 'miniprogram',
page: null
}
})
} catch (e) {
console.log('[Referral Bind] 记录访问失败(表可能不存在)')
}
}
return NextResponse.json({
success: true,
message: messages[action] || '绑定成功',
action,
expiryDate: expiryDate.toISOString(),
bindingDays,
referrer: {
id: referrer.id,
nickname: referrer.nickname
},
...(oldReferrerId && { oldReferrerId })
message: action === 'renew' ? '绑定已续期' : action === 'switch' ? '推荐人已切换' : '绑定成功',
data: {
action,
referrer: {
id: referrer.id,
nickname: referrer.nickname
},
expiryDate,
bindingDays,
oldReferrerId
}
})
} catch (error) {
@@ -209,113 +204,3 @@ export async function POST(request: NextRequest) {
}, { status: 500 })
}
}
/**
* GET - 查询推荐关系
*/
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const userId = searchParams.get('userId')
const referralCode = searchParams.get('referralCode')
try {
if (referralCode) {
// 查询推荐码对应的用户
const users = await query(
'SELECT id, nickname, avatar FROM users WHERE referral_code = ?',
[referralCode]
) as any[]
if (users.length === 0) {
return NextResponse.json({
success: false,
error: '推荐码无效'
}, { status: 404 })
}
return NextResponse.json({
success: true,
referrer: users[0]
})
}
if (userId) {
// 查询用户是否存在
const users = await query(
'SELECT id FROM users WHERE id = ?',
[userId]
) as any[]
if (users.length === 0) {
return NextResponse.json({
success: false,
error: '用户不存在'
}, { status: 404 })
}
// 从 referral_bindings 查询当前有效的推荐人
let referrer = null
const activeBinding = await query(`
SELECT
rb.referrer_id,
u.nickname,
u.avatar,
rb.expiry_date,
rb.purchase_count
FROM referral_bindings rb
JOIN users u ON rb.referrer_id = u.id
WHERE rb.referee_id = ?
AND rb.status = 'active'
AND rb.expiry_date > NOW()
ORDER BY rb.binding_date DESC
LIMIT 1
`, [userId]) as any[]
if (activeBinding.length > 0) {
referrer = {
id: activeBinding[0].referrer_id,
nickname: activeBinding[0].nickname,
avatar: activeBinding[0].avatar,
expiryDate: activeBinding[0].expiry_date,
purchaseCount: activeBinding[0].purchase_count
}
}
// 获取该用户推荐的人(所有活跃绑定)
const referees = await query(`
SELECT
u.id,
u.nickname,
u.avatar,
rb.binding_date as created_at,
rb.purchase_count,
rb.total_commission
FROM referral_bindings rb
JOIN users u ON rb.referee_id = u.id
WHERE rb.referrer_id = ?
AND rb.status = 'active'
AND rb.expiry_date > NOW()
ORDER BY rb.binding_date DESC
`, [userId]) as any[]
return NextResponse.json({
success: true,
referrer,
referees,
referralCount: referees.length
})
}
return NextResponse.json({
success: false,
error: '请提供userId或referralCode参数'
}, { status: 400 })
} catch (error) {
console.error('[Referral Bind] GET错误:', error)
return NextResponse.json({
success: false,
error: '查询失败: ' + (error as Error).message
}, { status: 500 })
}
}

View File

@@ -1,31 +1,18 @@
/**
* 分销数据API - 性能优化版
* 分销数据API - 使用 Prisma ORM
*
* 优化内容
* - ⚡ 合并统计查询5个独立查询 → 1个聚合查询减少60%响应时间)
* - ⚡ 减少数据量列表从50/30条 → 20条减少55%数据传输)
* - ⚡ 优化查询添加索引提升查询效率30-50%
*
* 可见数据:
* - 绑定用户数(当前有效绑定)
* - 通过链接进的人数(总访问量)
* - 带来的付款人数(已转化购买)
* - 收益统计90%归分发者)
*
* 性能:
* - 数据库查询9个 → 5个减少44%
* - 预计响应时间500-800ms → 200-300ms
* 优
* - ✅ 完全类型安全
* - ✅ 自动防SQL注入
* - ✅ 使用 Prisma 原生聚合查询
*/
import { NextRequest, NextResponse } from 'next/server'
import { query, getConfig } from '@/lib/db'
import { prisma } from '@/lib/prisma'
import { getPrismaConfig } from '@/lib/prisma-helpers'
// 分成比例默认90%给推广者)
const DISTRIBUTOR_SHARE = 0.9
/**
* GET - 获取分销数据
*/
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const userId = searchParams.get('userId')
@@ -40,264 +27,288 @@ export async function GET(request: NextRequest) {
try {
// 获取分销配置
let distributorShare = DISTRIBUTOR_SHARE
let minWithdrawAmount = 10 // 默认最低提现金额
let minWithdrawAmount = 10
try {
const config = await getConfig('referral_config')
const config = await getPrismaConfig('referral_config')
if (config?.distributorShare) {
distributorShare = config.distributorShare / 100
}
if (config?.minWithdrawAmount) {
minWithdrawAmount = Number(config.minWithdrawAmount)
}
} catch (e) { /* 使用默认配置 */ }
} catch (e) { }
// ⚡ 优化:合并统计查询 - 添加错误处理
let statsResult: any[]
try {
statsResult = await query(`
SELECT
-- 用户基本信息
u.id, u.nickname, u.referral_code, u.earnings, u.pending_earnings,
u.withdrawn_earnings, u.referral_count,
-- 绑定关系统计
(SELECT COUNT(*) FROM referral_bindings WHERE referrer_id = u.id) as total_bindings,
(SELECT COUNT(*) FROM referral_bindings WHERE referrer_id = u.id AND status = 'active' AND expiry_date > NOW()) as active_bindings,
(SELECT COUNT(*) FROM referral_bindings WHERE referrer_id = u.id AND status = 'active' AND purchase_count > 0) as converted_bindings,
(SELECT COUNT(*) FROM referral_bindings WHERE referrer_id = u.id AND (status IN ('expired', 'cancelled') OR (status = 'active' AND expiry_date <= NOW()))) as expired_bindings,
-- 付款统计直接从orders表查询
(SELECT COUNT(DISTINCT user_id) FROM orders WHERE referrer_id = u.id AND status = 'paid') as paid_count,
(SELECT COALESCE(SUM(amount), 0) FROM orders WHERE referrer_id = u.id AND status = 'paid') as total_referral_amount
FROM users u
WHERE u.id = ?
`, [userId]) as any[]
} catch (err) {
console.error('[ReferralData] 统计查询失败:', err)
return NextResponse.json({
success: false,
error: '查询统计数据失败: ' + (err as Error).message
}, { status: 500 })
}
// 1. 查询用户基本信息
const user = await prisma.users.findUnique({
where: { id: userId },
select: {
id: true,
nickname: true,
referral_code: true,
earnings: true,
pending_earnings: true,
withdrawn_earnings: true,
referral_count: true
}
})
if (statsResult.length === 0) {
if (!user) {
return NextResponse.json({
success: false,
error: '用户不存在'
}, { status: 404 })
}
const stats = statsResult[0]
// 2. 使用 Prisma 聚合查询绑定统计
const [totalBindings, activeBindings, convertedBindings, expiredBindings] = await Promise.all([
prisma.referral_bindings.count({
where: { referrer_id: userId }
}),
prisma.referral_bindings.count({
where: {
referrer_id: userId,
status: 'active',
expiry_date: { gt: new Date() }
}
}),
prisma.referral_bindings.count({
where: {
referrer_id: userId,
status: 'active',
purchase_count: { gt: 0 }
}
}),
prisma.referral_bindings.count({
where: {
referrer_id: userId,
OR: [
{ status: { in: ['expired', 'cancelled'] } },
{ status: 'active', expiry_date: { lte: new Date() } }
]
}
})
])
// 解构统计数据
const user = {
id: stats.id,
nickname: stats.nickname,
referral_code: stats.referral_code,
earnings: stats.earnings,
pending_earnings: stats.pending_earnings,
withdrawn_earnings: stats.withdrawn_earnings,
referral_count: stats.referral_count
}
// 3. 付款统计(使用 Prisma 聚合)
const paidOrders = await prisma.orders.aggregate({
where: {
referrer_id: userId,
status: 'paid'
},
_count: { user_id: true },
_sum: { amount: true }
})
const bindingStats = {
total: parseInt(stats.total_bindings) || 0,
active: parseInt(stats.active_bindings) || 0,
converted: parseInt(stats.converted_bindings) || 0,
expired: parseInt(stats.expired_bindings) || 0
}
const paidCount = await prisma.orders.findMany({
where: {
referrer_id: userId,
status: 'paid'
},
distinct: ['user_id'],
select: { user_id: true }
})
const paymentStats = {
paidCount: parseInt(stats.paid_count) || 0,
totalAmount: parseFloat(stats.total_referral_amount) || 0
}
const totalAmount = Number(paidOrders._sum.amount || 0)
const uniquePaidCount = paidCount.length
// 获取访问统计(独立查询,带错误处理)
let totalVisits = bindingStats.total
// 4. 访问统计
let totalVisits = totalBindings
try {
const visits = await query(`
SELECT COUNT(DISTINCT visitor_id) as count
FROM referral_visits
WHERE referrer_id = ?
`, [userId]) as any[]
totalVisits = parseInt(visits[0]?.count) || bindingStats.total
const visits = await prisma.referral_visits.groupBy({
by: ['visitor_id'],
where: { referrer_id: userId }
})
totalVisits = visits.length
} catch (e) {
// referral_visits 表可能不存在,使用绑定数作为访问数
console.log('[ReferralData] 访问统计表不存在,使用绑定数')
console.log('[ReferralData] 访问统计表不存在')
}
// 获取待审核提现金额(独立查询,带错误处理
let pendingWithdrawAmount = 0
try {
const withdraws = await query(`
SELECT COALESCE(SUM(amount), 0) as pending_amount
FROM withdrawals
WHERE user_id = ? AND status = 'pending'
`, [userId]) as any[]
pendingWithdrawAmount = parseFloat(withdraws[0]?.pending_amount) || 0
} catch (e) {
console.log('[ReferralData] 提现表查询失败:', e)
}
// 5. 待审核 + 已成功提现金额(用提现表汇总,与 user.withdrawn_earnings 可能不一致时以表为准,避免可提现出现负数
const [pendingWithdraw, successWithdraw] = await Promise.all([
prisma.withdrawals.aggregate({
where: { user_id: userId, status: 'pending' },
_sum: { amount: true }
}),
prisma.withdrawals.aggregate({
where: { user_id: userId, status: 'success' },
_sum: { amount: true }
})
])
const pendingWithdrawAmount = Number(pendingWithdraw._sum.amount || 0)
const withdrawnFromTable = Number(successWithdraw._sum.amount || 0)
// ⚡ 优化减少列表数据量50条→20条减少数据传输
// 2. 获取活跃绑定用户列表
const activeBindings = await query(`
SELECT rb.id, rb.referee_id, rb.expiry_date, rb.binding_date,
u.nickname, u.avatar, u.has_full_book,
DATEDIFF(rb.expiry_date, NOW()) as days_remaining
FROM referral_bindings rb
JOIN users u ON rb.referee_id = u.id
WHERE rb.referrer_id = ? AND rb.status = 'active' AND rb.expiry_date > NOW()
ORDER BY rb.binding_date DESC
LIMIT 20
`, [userId]) as any[]
// 6. 获取活跃绑定用户列表
const activeBindingsList = await prisma.referral_bindings.findMany({
where: {
referrer_id: userId,
status: 'active',
expiry_date: { gt: new Date() }
},
take: 20,
orderBy: { binding_date: 'desc' },
include: {
users_referral_bindings_referee_idTousers: {
select: {
id: true,
nickname: true,
avatar: true,
has_full_book: true
}
}
}
})
// 3. 获取已转化用户列表(新逻辑:有购买记录的活跃绑定)
const convertedBindings = await query(`
SELECT rb.id, rb.referee_id, rb.last_purchase_date as conversion_date,
rb.total_commission as commission_amount, rb.purchase_count,
u.nickname, u.avatar,
(SELECT COALESCE(SUM(amount), 0) FROM orders WHERE user_id = rb.referee_id AND status = 'paid') as order_amount
FROM referral_bindings rb
JOIN users u ON rb.referee_id = u.id
WHERE rb.referrer_id = ? AND rb.status = 'active' AND rb.purchase_count > 0
ORDER BY rb.last_purchase_date DESC
LIMIT 20
`, [userId]) as any[]
// 7. 获取已转化用户列表
const convertedBindingsList = await prisma.referral_bindings.findMany({
where: {
referrer_id: userId,
status: 'active',
purchase_count: { gt: 0 }
},
take: 20,
orderBy: { last_purchase_date: 'desc' },
include: {
users_referral_bindings_referee_idTousers: {
select: {
id: true,
nickname: true,
avatar: true
}
}
}
})
// 4. 获取已过期用户列表
const expiredBindings = await query(`
SELECT rb.id, rb.referee_id, rb.expiry_date, rb.binding_date,
u.nickname, u.avatar
FROM referral_bindings rb
JOIN users u ON rb.referee_id = u.id
WHERE rb.referrer_id = ? AND (rb.status = 'expired' OR (rb.status = 'active' AND rb.expiry_date <= NOW()))
ORDER BY rb.expiry_date DESC
LIMIT 20
`, [userId]) as any[]
// 8. 获取已过期用户列表
const expiredBindingsList = await prisma.referral_bindings.findMany({
where: {
referrer_id: userId,
OR: [
{ status: 'expired' },
{ status: 'active', expiry_date: { lte: new Date() } }
]
},
take: 20,
orderBy: { expiry_date: 'desc' },
include: {
users_referral_bindings_referee_idTousers: {
select: {
id: true,
nickname: true,
avatar: true
}
}
}
})
// 5. 获取收益明细(包含买家信息和商品详情)
let earningsDetails: any[] = []
try {
earningsDetails = await query(`
SELECT
o.id,
o.order_sn,
o.amount,
o.product_type,
o.product_id,
o.description,
o.pay_time,
u.nickname as buyer_nickname,
u.avatar as buyer_avatar,
rb.total_commission / rb.purchase_count as commission_per_order
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN referral_bindings rb ON o.user_id = rb.referee_id AND rb.referrer_id = ?
WHERE o.status = 'paid' AND o.referrer_id = ?
ORDER BY o.pay_time DESC
LIMIT 20
`, [userId, userId]) as any[]
} catch (e) {
console.log('[ReferralData] 获取收益明细失败:', e)
}
// 9. 获取收益明细
const earningsDetailsList = await prisma.orders.findMany({
where: {
referrer_id: userId,
status: 'paid'
},
take: 20,
orderBy: { pay_time: 'desc' },
include: {
users: {
select: {
nickname: true,
avatar: true
}
}
}
})
// 6. 计算预估收益
const estimatedEarnings = paymentStats.totalAmount * distributorShare
// 计算预估收益
const estimatedEarnings = totalAmount * distributorShare
const totalCommission = totalAmount * distributorShare
// 计算即将过期用户数7天内
const now = new Date()
const sevenDaysLater = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000)
const expiringCount = activeBindingsList.filter(b => {
const expiryDate = new Date(b.expiry_date)
return expiryDate > now && expiryDate <= sevenDaysLater
}).length
return NextResponse.json({
success: true,
data: {
// === 核心可见数据 ===
// 绑定用户数(当前有效绑定)
bindingCount: bindingStats.active,
// 通过链接进的人数
// 核心可见数据
bindingCount: activeBindings,
visitCount: totalVisits,
// 带来的付款人数
paidCount: paymentStats.paidCount,
// 已过期用户数
expiredCount: bindingStats.expired,
paidCount: uniquePaidCount,
expiredCount: expiredBindings,
// === 收益数据 ===
// 累计佣金总额(直接从订单表计算:订单金额 × 分成比例)
totalCommission: Math.round((paymentStats.totalAmount * distributorShare) * 100) / 100,
// 可提现金额pending_earnings
availableEarnings: parseFloat(user.pending_earnings) || 0,
// 待审核金额(提现申请中的金额)
// 收益数据
totalCommission: Math.round(totalCommission * 100) / 100,
availableEarnings: Math.max(0, Math.round((totalCommission - withdrawnFromTable - pendingWithdrawAmount) * 100) / 100),
pendingWithdrawAmount: Math.round(pendingWithdrawAmount * 100) / 100,
// 已提现金额
withdrawnEarnings: parseFloat(user.withdrawn_earnings) || 0,
// 已结算收益(保留兼容)
earnings: parseFloat(user.earnings) || 0,
// 待结算收益(保留兼容)
pendingEarnings: parseFloat(user.pending_earnings) || 0,
// 预估总收益
withdrawnEarnings: withdrawnFromTable,
earnings: Number(user.earnings) || 0,
pendingEarnings: Number(user.pending_earnings) || 0,
estimatedEarnings: Math.round(estimatedEarnings * 100) / 100,
// 分成比例
shareRate: Math.round(distributorShare * 100),
// 最低提现金额(新增:给小程序使用)
minWithdrawAmount,
// === 推荐码 ===
// 推荐码
referralCode: user.referral_code,
referralCount: user.referral_count || bindingStats.total,
referralCount: user.referral_count || totalBindings,
// === 详细统计 ===
// 详细统计
stats: {
totalBindings: bindingStats.total,
activeBindings: bindingStats.active,
convertedBindings: bindingStats.converted,
expiredBindings: bindingStats.expired,
// 即将过期7天内
expiringCount: activeBindings.filter((b: any) => b.days_remaining <= 7 && b.days_remaining > 0).length,
// 总支付金额
totalPaymentAmount: paymentStats.totalAmount
totalBindings,
activeBindings,
convertedBindings,
expiredBindings,
expiringCount,
totalPaymentAmount: totalAmount
},
// === 用户列表 ===
activeUsers: activeBindings.map((b: any) => ({
id: b.referee_id,
nickname: b.nickname || '用户' + b.referee_id.slice(-4),
avatar: b.avatar,
daysRemaining: Math.max(0, b.days_remaining),
hasFullBook: b.has_full_book,
bindingDate: b.binding_date,
status: 'active'
})),
// 用户列表
activeUsers: activeBindingsList.map(b => {
const daysRemaining = Math.max(0, Math.floor((new Date(b.expiry_date).getTime() - Date.now()) / (24 * 60 * 60 * 1000)))
return {
id: b.referee_id,
nickname: b.users_referral_bindings_referee_idTousers.nickname || '用户' + b.referee_id.slice(-4),
avatar: b.users_referral_bindings_referee_idTousers.avatar,
daysRemaining,
hasFullBook: b.users_referral_bindings_referee_idTousers.has_full_book,
bindingDate: b.binding_date,
status: 'active'
}
}),
convertedUsers: convertedBindings.map((b: any) => ({
convertedUsers: convertedBindingsList.map(b => ({
id: b.referee_id,
nickname: b.nickname || '用户' + b.referee_id.slice(-4),
avatar: b.avatar,
commission: parseFloat(b.commission_amount) || 0,
orderAmount: parseFloat(b.order_amount) || 0,
purchaseCount: parseInt(b.purchase_count) || 0,
conversionDate: b.conversion_date,
nickname: b.users_referral_bindings_referee_idTousers.nickname || '用户' + b.referee_id.slice(-4),
avatar: b.users_referral_bindings_referee_idTousers.avatar,
commission: Number(b.total_commission) || 0,
orderAmount: Number(b.total_commission) / distributorShare || 0,
purchaseCount: b.purchase_count || 0,
conversionDate: b.last_purchase_date,
status: 'converted'
})),
// 已过期用户列表
expiredUsers: expiredBindings.map((b: any) => ({
expiredUsers: expiredBindingsList.map(b => ({
id: b.referee_id,
nickname: b.nickname || '用户' + b.referee_id.slice(-4),
avatar: b.avatar,
nickname: b.users_referral_bindings_referee_idTousers.nickname || '用户' + b.referee_id.slice(-4),
avatar: b.users_referral_bindings_referee_idTousers.avatar,
bindingDate: b.binding_date,
expiryDate: b.expiry_date,
status: 'expired'
})),
// === 收益明细 ===
earningsDetails: earningsDetails.map((e: any) => ({
// 收益明细
earningsDetails: earningsDetailsList.map(e => ({
id: e.id,
orderSn: e.order_sn,
amount: parseFloat(e.amount),
commission: parseFloat(e.commission_per_order) || parseFloat(e.amount) * distributorShare,
amount: Number(e.amount),
commission: Number(e.amount) * distributorShare,
productType: e.product_type,
productId: e.product_id,
description: e.description,
buyerNickname: e.buyer_nickname || '用户' + e.id?.toString().slice(-4),
buyerAvatar: e.buyer_avatar,
buyerNickname: e.users.nickname || '用户' + e.id.slice(-4),
buyerAvatar: e.users.avatar,
payTime: e.pay_time
}))
}

View File

@@ -1,10 +1,11 @@
/**
* 用户资料API
* 用于完善用户信息(头像、微信号、手机号)
* 使用 Prisma ORM安全防SQL注入
*/
import { NextRequest, NextResponse } from 'next/server'
import { query } from '@/lib/db'
import { prisma } from '@/lib/prisma'
/**
* GET - 获取用户资料
@@ -22,23 +23,33 @@ export async function GET(request: NextRequest) {
}
try {
const users = await query(`
SELECT id, open_id, nickname, avatar, phone, wechat_id,
referral_code, has_full_book, is_admin,
earnings, pending_earnings, referral_count, created_at
FROM users
WHERE ${userId ? 'id = ?' : 'open_id = ?'}
`, [userId || openId]) as any[]
// 使用 Prisma 查询自动防SQL注入
const user = await prisma.users.findFirst({
where: userId ? { id: userId } : { open_id: openId || '' },
select: {
id: true,
open_id: true,
nickname: true,
avatar: true,
phone: true,
wechat_id: true,
referral_code: true,
has_full_book: true,
is_admin: true,
earnings: true,
pending_earnings: true,
referral_count: true,
created_at: true
}
})
if (users.length === 0) {
if (!user) {
return NextResponse.json({
success: false,
error: '用户不存在'
}, { status: 404 })
}
const user = users[0]
// 检查资料完整度
const profileComplete = !!(user.phone || user.wechat_id)
const hasAvatar = !!user.avatar && !user.avatar.includes('picsum.photos')
@@ -91,16 +102,19 @@ export async function POST(request: NextRequest) {
}, { status: 400 })
}
// 检查用户是否存在
const users = await query(`SELECT id FROM users WHERE ${identifierField} = ?`, [identifier]) as any[]
if (users.length === 0) {
// 检查用户是否存在Prisma 自动防SQL注入
const existingUser = await prisma.users.findFirst({
where: identifierField === 'id' ? { id: identifier } : { open_id: identifier }
})
if (!existingUser) {
return NextResponse.json({
success: false,
error: '用户不存在'
}, { status: 404 })
}
const realUserId = users[0].id
const realUserId = existingUser.id
// 构建更新字段
const updates: string[] = []
@@ -137,26 +151,37 @@ export async function POST(request: NextRequest) {
}, { status: 400 })
}
// 执行更新
values.push(realUserId)
await query(`UPDATE users SET ${updates.join(', ')}, updated_at = NOW() WHERE id = ?`, values)
// 构建 Prisma 更新数据对象
const updateData: any = { updated_at: new Date() }
if (nickname !== undefined) updateData.nickname = nickname
if (avatar !== undefined) updateData.avatar = avatar
if (phone !== undefined) updateData.phone = phone
if (wechatId !== undefined) updateData.wechat_id = wechatId
// 返回更新后的用户信息
const updatedUsers = await query(`
SELECT id, nickname, avatar, phone, wechat_id, referral_code
FROM users WHERE id = ?
`, [realUserId]) as any[]
// 执行更新Prisma 自动防SQL注入
const updatedUser = await prisma.users.update({
where: { id: realUserId },
data: updateData,
select: {
id: true,
nickname: true,
avatar: true,
phone: true,
wechat_id: true,
referral_code: true
}
})
return NextResponse.json({
success: true,
message: '资料更新成功',
data: {
id: updatedUsers[0].id,
nickname: updatedUsers[0].nickname,
avatar: updatedUsers[0].avatar,
phone: updatedUsers[0].phone,
wechatId: updatedUsers[0].wechat_id,
referralCode: updatedUsers[0].referral_code
id: updatedUser.id,
nickname: updatedUser.nickname,
avatar: updatedUser.avatar,
phone: updatedUser.phone,
wechatId: updatedUser.wechat_id,
referralCode: updatedUser.referral_code
}
})

View File

@@ -1,10 +1,11 @@
/**
* 用户信息更新API
* 支持更新昵称、头像、手机号、微信号、支付宝、地址等
* 使用 Prisma ORM安全防SQL注入
*/
import { NextRequest, NextResponse } from 'next/server'
import { query } from '@/lib/db'
import { prisma } from '@/lib/prisma'
export async function POST(request: NextRequest) {
try {
@@ -15,55 +16,24 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ success: false, message: '缺少用户ID' }, { status: 400 })
}
// 构建更新字段
const updates: string[] = []
const values: any[] = []
// 构建 Prisma 更新数据对象
const updateData: any = { updated_at: new Date() }
if (nickname !== undefined) {
updates.push('nickname = ?')
values.push(nickname)
}
if (avatar !== undefined) {
updates.push('avatar = ?')
values.push(avatar)
}
if (phone !== undefined) {
updates.push('phone = ?')
values.push(phone)
}
if (wechat !== undefined) {
updates.push('wechat = ?')
values.push(wechat)
}
if (alipay !== undefined) {
updates.push('alipay = ?')
values.push(alipay)
}
if (address !== undefined) {
updates.push('address = ?')
values.push(address)
}
if (autoWithdraw !== undefined) {
updates.push('auto_withdraw = ?')
values.push(autoWithdraw ? 1 : 0)
}
if (withdrawAccount !== undefined) {
updates.push('withdraw_account = ?')
values.push(withdrawAccount)
}
if (nickname !== undefined) updateData.nickname = nickname
if (avatar !== undefined) updateData.avatar = avatar
if (phone !== undefined) updateData.phone = phone
if (wechat !== undefined) updateData.wechat_id = wechat // 映射到 wechat_id 字段
// 注意alipay, address, auto_withdraw, withdraw_account 在 schema 中不存在,需要先添加字段或移除
// 添加更新时间
updates.push('updated_at = NOW()')
if (updates.length === 1) {
if (Object.keys(updateData).length === 1) {
return NextResponse.json({ success: false, message: '没有需要更新的字段' }, { status: 400 })
}
// 执行更新
values.push(userId)
const sql = `UPDATE users SET ${updates.join(', ')} WHERE id = ?`
await query(sql, values)
// 执行更新Prisma 自动防SQL注入
await prisma.users.update({
where: { id: userId },
data: updateData
})
return NextResponse.json({
success: true,

View File

@@ -1,8 +1,9 @@
// app/api/wechat/login/route.ts
// 微信小程序登录接口
// 使用 Prisma ORM安全防SQL注入
import { NextRequest, NextResponse } from 'next/server'
import { query } from '@/lib/db'
import { prisma } from '@/lib/prisma'
// 使用真实的小程序AppID和Secret
const APPID = process.env.WECHAT_APPID || 'wxb8bbb2b10dec74aa'
@@ -45,13 +46,20 @@ export async function POST(req: NextRequest) {
let isNewUser = false
try {
// 先查询用户是否存在
const existingUsers = await query('SELECT * FROM users WHERE open_id = ?', [openid]) as any[]
// 先查询用户是否存在Prisma 自动防SQL注入
const existingUser = await prisma.users.findUnique({
where: { open_id: openid }
})
if (existingUsers.length > 0) {
if (existingUser) {
// 用户已存在更新session_key
user = existingUsers[0]
await query('UPDATE users SET session_key = ?, updated_at = NOW() WHERE open_id = ?', [session_key, openid])
user = await prisma.users.update({
where: { open_id: openid },
data: {
session_key,
updated_at: new Date()
}
})
console.log('[WechatLogin] 用户已存在:', user.id)
} else {
// 创建新用户
@@ -63,20 +71,22 @@ export async function POST(req: NextRequest) {
// 注意:推荐绑定逻辑已移至 /api/referral/bind这里只创建用户
// 如果有 referralCode会在前端调用 /api/referral/bind 建立绑定关系
await query(`
INSERT INTO users (
id, open_id, session_key, nickname, avatar, referral_code,
has_full_book, purchased_sections, earnings, pending_earnings, referral_count
) VALUES (?, ?, ?, ?, ?, ?, FALSE, '[]', 0, 0, 0)
`, [
userId, openid, session_key, nickname,
'https://picsum.photos/200/200?random=' + openid.substr(-2),
userReferralCode
])
user = await prisma.users.create({
data: {
id: userId,
open_id: openid,
session_key,
nickname,
avatar: 'https://picsum.photos/200/200?random=' + openid.substr(-2),
referral_code: userReferralCode,
has_full_book: false,
purchased_sections: [],
earnings: 0,
pending_earnings: 0,
referral_count: 0
}
})
// 获取新创建的用户
const newUsers = await query('SELECT * FROM users WHERE id = ?', [userId]) as any[]
user = newUsers[0]
console.log('[WechatLogin] 新用户创建成功:', userId)
}
} catch (dbError) {

View File

@@ -1,33 +1,27 @@
/**
* 提现API
* 用户提现到微信零钱或支付宝
* 提现API - 使用 Prisma ORM
* 用户提现到微信零钱
*
* Prisma 优势:
* - 完全类型安全
* - 自动防SQL注入
* - 简化复杂查询
*/
import { NextRequest, NextResponse } from 'next/server'
import { query, getConfig } from '@/lib/db'
import { prisma } from '@/lib/prisma'
import { Decimal } from '@/lib/generated/prisma/runtime/library'
// 确保提现表存在
async function ensureWithdrawalsTable() {
// 读取系统配置(使用 Prisma
async function getPrismaConfig(key: string): Promise<any> {
try {
await query(`
CREATE TABLE IF NOT EXISTS withdrawals (
id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
account_type VARCHAR(20) DEFAULT 'wechat',
account VARCHAR(100),
status ENUM('pending', 'processing', 'success', 'failed') DEFAULT 'pending',
wechat_openid VARCHAR(100),
transaction_id VARCHAR(100),
error_message VARCHAR(500),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
processed_at TIMESTAMP NULL,
INDEX idx_user_id (user_id),
INDEX idx_status (status)
)
`)
const config = await prisma.system_config.findUnique({
where: { config_key: key }
})
return config?.config_value
} catch (e) {
console.log('[Withdraw] 表已存在或创建失败')
console.warn(`[Config] 读取配置 ${key} 失败:`, e)
return null
}
}
@@ -44,10 +38,10 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ success: false, message: '提现金额无效' }, { status: 400 })
}
// 读取最低提现门槛
// 1. 读取最低提现门槛
let minWithdrawAmount = 10 // 默认值
try {
const config = await getConfig('referral_config')
const config = await getPrismaConfig('referral_config')
if (config?.minWithdrawAmount) {
minWithdrawAmount = Number(config.minWithdrawAmount)
}
@@ -63,37 +57,47 @@ export async function POST(request: NextRequest) {
}, { status: 400 })
}
// 确保表存在
await ensureWithdrawalsTable()
// 2. 查询用户信息Prisma 保证类型安全)
const user = await prisma.users.findUnique({
where: { id: userId },
select: {
id: true,
open_id: true,
wechat_id: true,
withdrawn_earnings: true
}
})
// 查询用户信息
const users = await query('SELECT * FROM users WHERE id = ?', [userId]) as any[]
if (!users || users.length === 0) {
if (!user) {
return NextResponse.json({ success: false, message: '用户不存在' }, { status: 404 })
}
const user = users[0]
// 微信零钱提现需要 open_id小程序/公众号登录获得)
const openId = user.open_id || ''
const wechatId = user.wechat || user.wechat_id || ''
const alipayId = user.alipay || ''
if (!openId && !alipayId) {
if (!openId) {
return NextResponse.json({
success: false,
message: '提现到微信零钱需先使用微信登录;或绑定支付宝后提现到支付宝',
message: '提现到微信零钱需先使用微信登录',
needBind: true
})
}
// ✅ 修正:从 orders 表查询累计佣金(与前端逻辑一致
// 提现需绑定微信号(用于后台展示收款账号
const wechatId = user.wechat_id?.trim() || ''
if (!wechatId) {
return NextResponse.json({
success: false,
message: '请先到「设置」中绑定微信号后再提现',
needBindWechat: true
})
}
// 3. 计算累计佣金(从 orders 表查询)
let totalCommission = 0
try {
// 读取分成比例
let distributorShare = 0.9 // 默认90%
try {
const config = await getConfig('referral_config')
const config = await getPrismaConfig('referral_config')
if (config?.distributorShare) {
distributorShare = Number(config.distributorShare)
}
@@ -101,14 +105,18 @@ export async function POST(request: NextRequest) {
console.warn('[Withdraw] 读取分成比例失败,使用默认值 90%')
}
// 查询订单总金额
const ordersResult = await query(`
SELECT COALESCE(SUM(amount), 0) as total_amount
FROM orders
WHERE referrer_id = ? AND status = 'paid'
`, [userId]) as any[]
// 使用 Prisma 聚合查询
const ordersResult = await prisma.orders.aggregate({
where: {
referrer_id: userId,
status: 'paid'
},
_sum: {
amount: true
}
})
const totalAmount = parseFloat(ordersResult[0]?.total_amount || 0)
const totalAmount = Number(ordersResult._sum.amount || 0)
totalCommission = totalAmount * distributorShare
console.log('[Withdraw] 佣金计算:')
@@ -116,41 +124,32 @@ export async function POST(request: NextRequest) {
console.log('- 分成比例:', distributorShare * 100 + '%')
console.log('- 累计佣金:', totalCommission)
} catch (e) {
// 如果表不存在收益为0
console.log('[Withdraw] 查询收益失败,可能表不存在:', e)
console.log('[Withdraw] 查询收益失败:', e)
}
// 查询已提现金额
let withdrawnEarnings = 0
try {
withdrawnEarnings = parseFloat(user.withdrawn_earnings) || 0
} catch (e) {
console.log('[Withdraw] 读取已提现金额失败:', e)
}
// 4. 已提现金额与待审核金额均以提现表为准(与分销页展示一致,避免 user.withdrawn_earnings 不同步导致负数或超额提现)
const [pendingResult, successResult] = await Promise.all([
prisma.withdrawals.aggregate({
where: { user_id: userId, status: 'pending' },
_sum: { amount: true }
}),
prisma.withdrawals.aggregate({
where: { user_id: userId, status: 'success' },
_sum: { amount: true }
})
])
const withdrawnEarnings = Number(successResult._sum.amount || 0)
const pendingWithdrawAmount = Number(pendingResult._sum.amount || 0)
// 查询待审核提现金额
let pendingWithdrawAmount = 0
try {
const pendingResult = await query(`
SELECT COALESCE(SUM(amount), 0) as pending_amount
FROM withdrawals
WHERE user_id = ? AND status = 'pending'
`, [userId]) as any[]
pendingWithdrawAmount = parseFloat(pendingResult[0]?.pending_amount || 0)
} catch (e) {
console.log('[Withdraw] 查询待审核金额失败:', e)
}
// ✅ 修正:可提现金额 = 累计佣金 - 已提现金额 - 待审核金额(三元素完整校验)
const availableAmount = totalCommission - withdrawnEarnings - pendingWithdrawAmount
// 5. 计算可提现金额(不低于 0
const availableAmount = Math.max(0, totalCommission - withdrawnEarnings - pendingWithdrawAmount)
console.log('[Withdraw] 提现验证(完整版):')
console.log('- 累计佣金 (totalCommission):', totalCommission)
console.log('- 已提现金额 (withdrawnEarnings):', withdrawnEarnings)
console.log('- 待审核金额 (pendingWithdrawAmount):', pendingWithdrawAmount)
console.log('- 可提现金额 = 累计 - 已提现 - 待审核 =', totalCommission, '-', withdrawnEarnings, '-', pendingWithdrawAmount, '=', availableAmount)
console.log('- 申请提现金额 (amount):', amount)
console.log('- 判断:', amount, '>', availableAmount, '=', amount > availableAmount)
console.log('- 累计佣金:', totalCommission)
console.log('- 已提现金额:', withdrawnEarnings)
console.log('- 待审核金额:', pendingWithdrawAmount)
console.log('- 可提现金额 =', availableAmount)
console.log('- 申请提现金额:', amount)
if (amount > availableAmount) {
return NextResponse.json({
@@ -159,37 +158,29 @@ export async function POST(request: NextRequest) {
})
}
// 创建提现记录(微信零钱需保存 wechat_openid 供后台批准时调用商家转账到零钱
// 7. 创建提现记录(使用 Prisma无SQL注入风险
const withdrawId = `W${Date.now()}`
const accountType = alipayId ? 'alipay' : 'wechat'
const account = alipayId || wechatId
try {
await query(`
INSERT INTO withdrawals (id, user_id, amount, status, wechat_openid, created_at)
VALUES (?, ?, ?, 'pending', ?, NOW())
`, [withdrawId, userId, amount, accountType === 'wechat' ? openId : null])
// 微信零钱由后台批准时调用「商家转账到零钱」;支付宝/无 openid 时仅标记成功(需线下打款)
if (accountType !== 'wechat' || !openId) {
await query(`UPDATE withdrawals SET status = 'success', processed_at = NOW() WHERE id = ?`, [withdrawId])
await query(`
UPDATE users SET withdrawn_earnings = withdrawn_earnings + ?, pending_earnings = GREATEST(0, pending_earnings - ?) WHERE id = ?
`, [amount, amount, userId])
const withdrawal = await prisma.withdrawals.create({
data: {
id: withdrawId,
user_id: userId,
amount: new Decimal(amount),
status: 'pending',
wechat_openid: openId,
wechat_id: wechatId,
created_at: new Date()
}
} catch (e) {
console.log('[Withdraw] 创建提现记录失败:', e)
}
})
return NextResponse.json({
success: true,
message: '提现申请已提交,正在审核中,通过后会自动到账您的微信零钱',
data: {
withdrawId,
amount,
account,
accountType: accountType === 'alipay' ? '支付宝' : '微信',
status: 'pending'
withdrawId: withdrawal.id,
amount: Number(withdrawal.amount),
accountType: '微信',
status: withdrawal.status
}
})

View File

@@ -1,14 +1,15 @@
import { redirect } from "next/navigation"
/** 根路径重定向到移动端首页 */
export default function RootPage({
export default async function RootPage({
searchParams,
}: {
searchParams?: Record<string, string | string[] | undefined>
searchParams?: Promise<Record<string, string | string[] | undefined>>
}) {
const resolved = await searchParams
const params = new URLSearchParams()
for (const [key, value] of Object.entries(searchParams ?? {})) {
for (const [key, value] of Object.entries(resolved ?? {})) {
if (typeof value === "string") params.set(key, value)
else if (Array.isArray(value)) value.forEach((v) => params.append(key, v))
}