优化提现流程,新增用户确认模式以支持待用户确认的转账,更新相关API和数据库结构以确保数据一致性。同时,调整小程序界面以展示待确认收款信息,提升用户体验。

This commit is contained in:
乘风
2026-02-06 19:45:24 +08:00
parent 2e65d68e1e
commit 8e67eb5d62
19 changed files with 649 additions and 95 deletions

View File

@@ -6,7 +6,7 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { requireAdminResponse } from '@/lib/admin-auth'
import { createTransfer } from '@/lib/wechat-transfer'
import { createTransferUserConfirm } from '@/lib/wechat-transfer'
// ========== GET查询提现记录带用户信息、收款账号=微信号)==========
export async function GET(request: Request) {
@@ -43,8 +43,9 @@ export async function GET(request: Request) {
user_name: user?.nickname || '未知用户',
userAvatar: user?.avatar,
amount: Number(w.amount),
status: w.status === 'success' ? 'completed' :
w.status === 'failed' ? 'rejected' :
status: w.status === 'success' ? 'completed' :
w.status === 'failed' ? 'rejected' :
w.status === 'pending_confirm' ? 'pending_confirm' :
w.status,
created_at: w.created_at,
method: 'wechat',
@@ -101,7 +102,7 @@ export async function PUT(request: Request) {
const openid = withdrawal.wechat_openid
if (action === 'approve') {
console.log(STEP, '3. 发起微信打款')
console.log(STEP, '3. 发起转账(用户确认模式)')
if (!openid) {
return NextResponse.json({
success: false,
@@ -109,53 +110,85 @@ export async function PUT(request: Request) {
}, { status: 400 })
}
const amountFen = Math.round(amount * 100)
const transferResult = await createTransfer({
const transferResult = await createTransferUserConfirm({
openid,
amountFen,
outDetailNo: id,
outBillNo: id,
transferRemark: '提现',
})
if (!transferResult.success) {
console.error(STEP, '微信打款失败:', transferResult.errorCode, transferResult.errorMessage)
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 || '打款失败'
error_message: transferResult.errorMessage || transferResult.errorCode || '发起失败'
}
})
return NextResponse.json({
success: false,
error: '微信打款失败: ' + (transferResult.errorMessage || transferResult.errorCode || '未知错误')
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({
const state = transferResult.state || ''
const hasPackage = !!(transferResult.packageInfo && (state === 'WAIT_USER_CONFIRM' || state === 'TRANSFERING' || state === 'ACCEPTED' || state === 'PROCESSING'))
if (hasPackage && transferResult.packageInfo) {
console.log(STEP, '4. 待用户确认收款,保存 package_info')
await prisma.withdrawals.update({
where: { id },
data: {
status: 'success',
processed_at: new Date(),
transaction_id: transferResult.batchId || batchNo
status: 'pending_confirm',
transfer_bill_no: transferResult.transferBillNo || null,
package_info: transferResult.packageInfo,
transaction_id: transferResult.transferBillNo || undefined,
}
})
const user = await tx.users.findUnique({
where: { id: userId },
select: { withdrawn_earnings: true }
return NextResponse.json({
success: true,
message: '已发起转账,请通知用户在小程序「分销中心」或「我的」中点击「确认收款」完成到账'
})
await tx.users.update({
where: { id: userId },
data: {
withdrawn_earnings: Number(user?.withdrawn_earnings || 0) + amount
}
}
if (state === 'SUCCESS') {
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.transferBillNo || undefined,
transfer_bill_no: transferResult.transferBillNo || undefined,
}
})
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
}
})
})
return NextResponse.json({
success: true,
message: '已批准并打款成功,款项将到账用户微信零钱'
})
}
await prisma.withdrawals.update({
where: { id },
data: {
status: 'pending_confirm',
transfer_bill_no: transferResult.transferBillNo || null,
package_info: transferResult.packageInfo || null,
transaction_id: transferResult.transferBillNo || undefined,
}
})
console.log(STEP, '5. 批准完成,钱已打到用户微信零钱')
return NextResponse.json({
success: true,
message: '已批准并打款成功,款项将到账用户微信零钱'
message: '已发起转账,请通知用户在小程序内「确认收款」完成到账'
})
}

View File

@@ -1,11 +1,12 @@
/**
* 微信支付 - 商家转账到零钱 结果通知
* 文档: 开发文档/提现功能完整技术文档.md
* 支持processing原批量转账、pending_confirm用户确认模式终态后更新为 success 并增加用户已提现
*/
import { NextRequest, NextResponse } from 'next/server'
import { decryptResource } from '@/lib/wechat-transfer'
import { query } from '@/lib/db'
import { prisma } from '@/lib/prisma'
const cfg = {
apiV3Key: process.env.WECHAT_API_V3_KEY || process.env.WECHAT_MCH_KEY || '',
@@ -34,29 +35,65 @@ export async function POST(request: NextRequest) {
if (!outBillNo) {
return NextResponse.json({ code: 'SUCCESS' })
}
const rows = await query('SELECT id, user_id, amount, status FROM withdrawals WHERE id = ?', [outBillNo]) as any[]
if (rows.length === 0) {
const w = await prisma.withdrawals.findUnique({
where: { id: outBillNo },
select: { id: true, user_id: true, amount: true, status: true },
})
if (!w) {
return NextResponse.json({ code: 'SUCCESS' })
}
const w = rows[0]
if (w.status !== 'processing') {
// 仅处理「处理中」或「待用户确认」的终态回调
if (w.status !== 'processing' && w.status !== 'pending_confirm') {
return NextResponse.json({ code: 'SUCCESS' })
}
const amount = Number(w.amount)
if (state === 'SUCCESS') {
await query(`
UPDATE withdrawals SET status = 'success', processed_at = NOW(), transaction_id = ? WHERE id = ?
`, [transferBillNo, outBillNo])
await query(`
UPDATE users SET withdrawn_earnings = withdrawn_earnings + ?, pending_earnings = GREATEST(0, pending_earnings - ?) WHERE id = ?
`, [w.amount, w.amount, w.user_id])
await prisma.$transaction(async (tx) => {
await tx.withdrawals.update({
where: { id: outBillNo },
data: {
status: 'success',
processed_at: new Date(),
transaction_id: transferBillNo,
},
})
const user = await tx.users.findUnique({
where: { id: w.user_id },
select: { withdrawn_earnings: true, pending_earnings: true },
})
const curWithdrawn = Number(user?.withdrawn_earnings ?? 0)
const curPending = Number(user?.pending_earnings ?? 0)
await tx.users.update({
where: { id: w.user_id },
data: {
withdrawn_earnings: curWithdrawn + amount,
pending_earnings: Math.max(0, curPending - amount),
},
})
})
} else {
await query(`
UPDATE withdrawals SET status = 'failed', processed_at = NOW(), error_message = ? WHERE id = ?
`, [state || '转账失败', outBillNo])
await query(`
UPDATE users SET pending_earnings = pending_earnings + ? WHERE id = ?
`, [w.amount, w.user_id])
await prisma.withdrawals.update({
where: { id: outBillNo },
data: {
status: 'failed',
processed_at: new Date(),
error_message: state || '转账失败',
},
})
const user = await prisma.users.findUnique({
where: { id: w.user_id },
select: { pending_earnings: true },
})
const curPending = Number(user?.pending_earnings ?? 0)
await prisma.users.update({
where: { id: w.user_id },
data: { pending_earnings: curPending + amount },
})
}
return NextResponse.json({ code: 'SUCCESS' })
} catch (e) {
console.error('[WechatTransferNotify]', e)

View File

@@ -0,0 +1,53 @@
/**
* 待确认收款列表 - 供小程序调起 wx.requestMerchantTransfer 使用
* GET ?userId=xxx 返回当前用户 status=pending_confirm 且含 package_info 的提现记录,及 mch_id、app_id
*/
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { getTransferMchAndAppId } from '@/lib/wechat-transfer'
export async function GET(request: NextRequest) {
try {
const userId = request.nextUrl.searchParams.get('userId')
if (!userId) {
return NextResponse.json({ success: false, message: '缺少 userId' }, { status: 400 })
}
// 原始查询(数据库 ENUM 已包含 pending_confirm
const list = await prisma.$queryRaw<
{ id: string; amount: unknown; package_info: string | null; created_at: Date }[]
>`
SELECT id, amount, package_info, created_at
FROM withdrawals
WHERE user_id = ${userId}
AND status = 'pending_confirm'
AND package_info IS NOT NULL
ORDER BY created_at DESC
`
const { mchId, appId } = getTransferMchAndAppId()
const items = list.map((w) => ({
id: w.id,
amount: Number(w.amount),
package: w.package_info ?? '',
created_at: w.created_at,
}))
return NextResponse.json({
success: true,
data: {
list: items,
mch_id: mchId,
app_id: appId,
},
})
} catch (e) {
console.error('[Withdraw pending-confirm]', e)
return NextResponse.json(
{ success: false, message: '获取待确认列表失败' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,45 @@
/**
* 提现记录列表 - 当前用户的全部提现记录(供小程序「提现记录」展示)
* GET ?userId=xxx
*/
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET(request: NextRequest) {
try {
const userId = request.nextUrl.searchParams.get('userId')
if (!userId) {
return NextResponse.json({ success: false, message: '缺少 userId' }, { status: 400 })
}
const rows = await prisma.$queryRaw<
{ id: string; amount: unknown; status: string; created_at: Date; processed_at: Date | null }[]
>`
SELECT id, amount, status, created_at, processed_at
FROM withdrawals
WHERE user_id = ${userId}
ORDER BY created_at DESC
LIMIT 100
`
const list = rows.map((r) => ({
id: r.id,
amount: Number(r.amount),
status: r.status,
created_at: r.created_at,
processed_at: r.processed_at,
}))
return NextResponse.json({
success: true,
data: { list },
})
} catch (e) {
console.error('[Withdraw records]', e)
return NextResponse.json(
{ success: false, message: '获取提现记录失败' },
{ status: 500 }
)
}
}

View File

@@ -1,16 +1,10 @@
/**
* 提现API - 使用 Prisma ORM
* 提现API - 使用 Prisma ORM + 原始查询(避免枚举未同步导致 500
* 用户提现到微信零钱
*
* Prisma 优势:
* - 完全类型安全
* - 自动防SQL注入
* - 简化复杂查询
*/
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { Decimal } from '@/lib/generated/prisma/runtime/library'
// 读取系统配置(使用 Prisma
async function getPrismaConfig(key: string): Promise<any> {
@@ -127,19 +121,19 @@ export async function POST(request: NextRequest) {
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 }
})
// 4. 已提现金额与待审核金额(原始查询避免 withdrawals_status 枚举校验
const [pendingRows, successRows] = await Promise.all([
prisma.$queryRaw<[{ sum_amount: unknown }]>`
SELECT COALESCE(SUM(amount), 0) AS sum_amount FROM withdrawals
WHERE user_id = ${userId} AND status = 'pending'
`,
prisma.$queryRaw<[{ sum_amount: unknown }]>`
SELECT COALESCE(SUM(amount), 0) AS sum_amount FROM withdrawals
WHERE user_id = ${userId} AND status = 'success'
`
])
const withdrawnEarnings = Number(successResult._sum.amount || 0)
const pendingWithdrawAmount = Number(pendingResult._sum.amount || 0)
const pendingWithdrawAmount = Number((pendingRows[0] as { sum_amount: unknown })?.sum_amount ?? 0)
const withdrawnEarnings = Number((successRows[0] as { sum_amount: unknown })?.sum_amount ?? 0)
// 5. 计算可提现金额(不低于 0
const availableAmount = Math.max(0, totalCommission - withdrawnEarnings - pendingWithdrawAmount)
@@ -158,29 +152,22 @@ export async function POST(request: NextRequest) {
})
}
// 7. 创建提现记录(使用 Prisma无SQL注入风险
// 7. 创建提现记录(原始插入避免 status 枚举校验
const withdrawId = `W${Date.now()}`
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()
}
})
const amountNum = Number(amount)
await prisma.$executeRaw`
INSERT INTO withdrawals (id, user_id, amount, status, wechat_openid, wechat_id, created_at)
VALUES (${withdrawId}, ${userId}, ${amountNum}, 'pending', ${openId}, ${wechatId}, NOW())
`
return NextResponse.json({
success: true,
message: '提现申请已提交,正在审核中,通过后会自动到账您的微信零钱',
message: '提现申请已提交,通过后会在我的页面提醒您收款!',
data: {
withdrawId: withdrawal.id,
amount: Number(withdrawal.amount),
withdrawId,
amount: amountNum,
accountType: '微信',
status: withdrawal.status
status: 'pending'
}
})