优化提现流程,新增用户确认模式以支持待用户确认的转账,更新相关API和数据库结构以确保数据一致性。同时,调整小程序界面以展示待确认收款信息,提升用户体验。
This commit is contained in:
@@ -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: '已发起转账,请通知用户在小程序内「确认收款」完成到账'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
53
app/api/withdraw/pending-confirm/route.ts
Normal file
53
app/api/withdraw/pending-confirm/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
45
app/api/withdraw/records/route.ts
Normal file
45
app/api/withdraw/records/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -158,6 +158,100 @@ export async function createTransfer(params: CreateTransferParams): Promise<Crea
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 用户确认模式:发起转账(需用户在小程序内确认收款)==========
|
||||
// 文档: https://pay.weixin.qq.com/doc/v3/merchant/4012716434
|
||||
|
||||
export interface CreateTransferUserConfirmParams {
|
||||
openid: string
|
||||
amountFen: number
|
||||
outBillNo: string
|
||||
transferRemark?: string
|
||||
}
|
||||
|
||||
export interface CreateTransferUserConfirmResult {
|
||||
success: boolean
|
||||
state?: string
|
||||
packageInfo?: string
|
||||
transferBillNo?: string
|
||||
createTime?: string
|
||||
errorCode?: string
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
/** 获取转账结果通知地址 */
|
||||
function getTransferNotifyUrl(): string {
|
||||
const base = process.env.NEXT_PUBLIC_BASE_URL || process.env.VERCEL_URL || 'http://localhost:3000'
|
||||
const host = base.startsWith('http') ? base : `https://${base}`
|
||||
return `${host}/api/payment/wechat/transfer/notify`
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户确认模式 - 发起转账
|
||||
* 返回 WAIT_USER_CONFIRM 时需将 package_info 下发给小程序,用户调 wx.requestMerchantTransfer 确认收款
|
||||
*/
|
||||
export async function createTransferUserConfirm(
|
||||
params: CreateTransferUserConfirmParams
|
||||
): Promise<CreateTransferUserConfirmResult> {
|
||||
const cfg = getConfig()
|
||||
if (!cfg.mchId || !cfg.appId || !cfg.certSerialNo) {
|
||||
return { success: false, errorCode: 'CONFIG_ERROR', errorMessage: '微信转账配置不完整' }
|
||||
}
|
||||
|
||||
const urlPath = '/v3/fund-app/mch-transfer/transfer-bills'
|
||||
const body = {
|
||||
appid: cfg.appId,
|
||||
out_bill_no: params.outBillNo,
|
||||
transfer_scene_id: '1005',
|
||||
openid: params.openid,
|
||||
transfer_amount: params.amountFen,
|
||||
transfer_remark: params.transferRemark || '提现',
|
||||
notify_url: getTransferNotifyUrl(),
|
||||
user_recv_perception: '提现',
|
||||
transfer_scene_report_infos: [
|
||||
{ info_type: '岗位类型', info_content: '兼职人员' },
|
||||
{ info_type: '报酬说明', info_content: '当日兼职费' },
|
||||
],
|
||||
}
|
||||
const bodyStr = JSON.stringify(body)
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString()
|
||||
const nonce = generateNonce()
|
||||
const signature = buildSignature('POST', urlPath, timestamp, nonce, bodyStr)
|
||||
const authorization = buildAuthorization(timestamp, nonce, signature)
|
||||
|
||||
const res = await fetch(`${BASE_URL}${urlPath}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
Authorization: authorization,
|
||||
'User-Agent': 'Soul-Withdraw/1.0',
|
||||
},
|
||||
body: bodyStr,
|
||||
})
|
||||
const data = (await res.json()) as Record<string, unknown>
|
||||
if (res.ok && res.status >= 200 && res.status < 300) {
|
||||
const state = (data.state as string) || ''
|
||||
return {
|
||||
success: true,
|
||||
state,
|
||||
packageInfo: data.package_info as string | undefined,
|
||||
transferBillNo: data.transfer_bill_no as string | undefined,
|
||||
createTime: data.create_time as string | undefined,
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
errorCode: (data.code as string) || 'UNKNOWN',
|
||||
errorMessage: (data.message as string) || (data.error as string) as string || '请求失败',
|
||||
}
|
||||
}
|
||||
|
||||
/** 供小程序调起确认收款时使用:获取商户号与 AppID */
|
||||
export function getTransferMchAndAppId(): { mchId: string; appId: string } {
|
||||
const cfg = getConfig()
|
||||
return { mchId: cfg.mchId, appId: cfg.appId }
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密回调 resource(AEAD_AES_256_GCM)
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,9 @@ App({
|
||||
|
||||
// 小程序配置 - 真实AppID
|
||||
appId: 'wxb8bbb2b10dec74aa',
|
||||
|
||||
// 订阅消息:用户点击「申请提现」→「立即提现」时会先弹出订阅授权窗
|
||||
withdrawSubscribeTmplId: 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE',
|
||||
|
||||
// 微信支付配置
|
||||
mchId: '1318592501', // 商户号
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"pages/settings/settings",
|
||||
"pages/search/search",
|
||||
"pages/addresses/addresses",
|
||||
"pages/addresses/edit"
|
||||
"pages/addresses/edit","pages/withdraw-records/withdraw-records"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
|
||||
@@ -40,9 +40,15 @@ Page({
|
||||
menuList: [
|
||||
{ id: 'orders', title: '我的订单', icon: '📦', count: 0 },
|
||||
{ id: 'referral', title: '推广中心', icon: '🎁', iconBg: 'gold', badge: '90%佣金' },
|
||||
{ id: 'withdrawRecords', title: '提现记录', icon: '📋', iconBg: 'gray' },
|
||||
{ id: 'about', title: '关于作者', icon: 'ℹ️', iconBg: 'brand' },
|
||||
{ id: 'settings', title: '设置', icon: '⚙️', iconBg: 'gray' }
|
||||
],
|
||||
|
||||
// 待确认收款(用户确认模式)
|
||||
pendingConfirmList: [],
|
||||
withdrawMchId: '',
|
||||
withdrawAppId: '',
|
||||
|
||||
// 登录弹窗
|
||||
showLoginModal: false,
|
||||
@@ -125,6 +131,7 @@ Page({
|
||||
totalReadTime: Math.floor(Math.random() * 200) + 50
|
||||
})
|
||||
this.loadReferralEarnings()
|
||||
this.loadPendingConfirm()
|
||||
} else {
|
||||
this.setData({
|
||||
isLoggedIn: false,
|
||||
@@ -139,6 +146,82 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 拉取待确认收款列表(用于「确认收款」按钮)
|
||||
async loadPendingConfirm() {
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (!app.globalData.isLoggedIn || !userInfo || !userInfo.id) return
|
||||
try {
|
||||
const res = await app.request('/api/withdraw/pending-confirm?userId=' + userInfo.id)
|
||||
if (res && res.success && res.data) {
|
||||
const list = (res.data.list || []).map(item => ({
|
||||
id: item.id,
|
||||
amount: (item.amount || 0).toFixed(2),
|
||||
package: item.package,
|
||||
created_at: item.created_at ? this.formatDateMy(item.created_at) : '--'
|
||||
}))
|
||||
this.setData({
|
||||
pendingConfirmList: list,
|
||||
withdrawMchId: res.data.mch_id || '',
|
||||
withdrawAppId: res.data.app_id || ''
|
||||
})
|
||||
} else {
|
||||
this.setData({ pendingConfirmList: [], withdrawMchId: '', withdrawAppId: '' })
|
||||
}
|
||||
} catch (e) {
|
||||
this.setData({ pendingConfirmList: [] })
|
||||
}
|
||||
},
|
||||
|
||||
formatDateMy(dateStr) {
|
||||
if (!dateStr) return '--'
|
||||
const d = new Date(dateStr)
|
||||
const m = (d.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = d.getDate().toString().padStart(2, '0')
|
||||
return `${m}-${day}`
|
||||
},
|
||||
|
||||
// 确认收款(调起微信收款页)
|
||||
confirmReceive(e) {
|
||||
const index = e.currentTarget.dataset.index
|
||||
const id = e.currentTarget.dataset.id
|
||||
const list = this.data.pendingConfirmList || []
|
||||
let item = (typeof index === 'number' || (index !== undefined && index !== '')) ? list[index] : null
|
||||
if (!item && id) item = list.find(x => x.id === id) || null
|
||||
if (!item || !item.package) {
|
||||
wx.showToast({ title: '请稍后刷新再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const mchId = this.data.withdrawMchId
|
||||
const appId = this.data.withdrawAppId
|
||||
if (!mchId || !appId) {
|
||||
wx.showToast({ title: '参数缺失,请刷新重试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!wx.canIUse('requestMerchantTransfer')) {
|
||||
wx.showToast({ title: '当前微信版本不支持,请升级后重试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '调起收款...', mask: true })
|
||||
wx.requestMerchantTransfer({
|
||||
mchId,
|
||||
appId,
|
||||
package: item.package,
|
||||
success: () => {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '收款成功', icon: 'success' })
|
||||
const newList = list.filter(x => x.id !== item.id)
|
||||
this.setData({ pendingConfirmList: newList })
|
||||
this.loadPendingConfirm()
|
||||
},
|
||||
fail: (err) => {
|
||||
wx.hideLoading()
|
||||
const msg = (err.errMsg || '').includes('cancel') ? '已取消' : (err.errMsg || '收款失败')
|
||||
wx.showToast({ title: msg, icon: 'none' })
|
||||
},
|
||||
complete: () => { wx.hideLoading() }
|
||||
})
|
||||
},
|
||||
|
||||
// 从与推广中心相同的接口拉取收益数据并更新展示(累计收益 = totalCommission,可提现 = 累计-已提现-待审核)
|
||||
async loadReferralEarnings() {
|
||||
const userInfo = app.globalData.userInfo
|
||||
@@ -438,6 +521,7 @@ Page({
|
||||
const routes = {
|
||||
orders: '/pages/purchases/purchases',
|
||||
referral: '/pages/referral/referral',
|
||||
withdrawRecords: '/pages/withdraw-records/withdraw-records',
|
||||
about: '/pages/about/about',
|
||||
settings: '/pages/settings/settings'
|
||||
}
|
||||
|
||||
@@ -109,6 +109,24 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 待确认收款(用户确认模式)- 仅登录用户显示 -->
|
||||
<view class="pending-confirm-card" wx:if="{{isLoggedIn}}">
|
||||
<view class="pending-confirm-header">
|
||||
<text class="pending-confirm-title">待确认收款</text>
|
||||
<text class="pending-confirm-desc" wx:if="{{pendingConfirmList.length > 0}}">审核已通过,点击下方按钮完成收款</text>
|
||||
<text class="pending-confirm-desc" wx:else>暂无待确认的提现,审核通过后会出现在这里</text>
|
||||
</view>
|
||||
<view class="pending-confirm-list" wx:if="{{pendingConfirmList.length > 0}}">
|
||||
<view class="pending-confirm-item" wx:for="{{pendingConfirmList}}" wx:key="id">
|
||||
<view class="pending-confirm-info">
|
||||
<text class="pending-confirm-amount">¥{{item.amount}}</text>
|
||||
<text class="pending-confirm-time">{{item.created_at}}</text>
|
||||
</view>
|
||||
<view class="pending-confirm-btn" bindtap="confirmReceive" data-index="{{index}}">确认收款</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Tab切换 - 仅登录用户显示 -->
|
||||
<view class="tab-bar-custom" wx:if="{{isLoggedIn}}">
|
||||
<view
|
||||
|
||||
@@ -1184,3 +1184,28 @@
|
||||
color: #ffffff;
|
||||
box-shadow: 0 8rpx 24rpx rgba(56, 189, 172, 0.3);
|
||||
}
|
||||
|
||||
/* 待确认收款 */
|
||||
.pending-confirm-card {
|
||||
margin: 32rpx;
|
||||
padding: 28rpx 32rpx;
|
||||
background: rgba(76, 175, 80, 0.08);
|
||||
border: 2rpx solid rgba(76, 175, 80, 0.25);
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
.pending-confirm-header { margin-bottom: 20rpx; }
|
||||
.pending-confirm-title { font-size: 28rpx; font-weight: 600; color: #fff; display: block; }
|
||||
.pending-confirm-desc { font-size: 24rpx; color: rgba(255,255,255,0.6); margin-top: 8rpx; display: block; }
|
||||
.pending-confirm-list { display: flex; flex-direction: column; gap: 16rpx; }
|
||||
.pending-confirm-item {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 20rpx 24rpx; background: rgba(28,28,30,0.6); border-radius: 16rpx;
|
||||
}
|
||||
.pending-confirm-info { display: flex; flex-direction: column; gap: 4rpx; }
|
||||
.pending-confirm-amount { font-size: 32rpx; font-weight: 600; color: #4CAF50; }
|
||||
.pending-confirm-time { font-size: 22rpx; color: rgba(255,255,255,0.5); }
|
||||
.pending-confirm-btn {
|
||||
padding: 16rpx 32rpx;
|
||||
background: linear-gradient(135deg, #4CAF50 0%, #388E3C 100%);
|
||||
color: #fff; font-size: 26rpx; font-weight: 500; border-radius: 20rpx;
|
||||
}
|
||||
|
||||
@@ -60,8 +60,9 @@ Page({
|
||||
posterReferralLink: '',
|
||||
posterNickname: '',
|
||||
posterNicknameInitial: '',
|
||||
posterCaseCount: 62
|
||||
},
|
||||
posterCaseCount: 62,
|
||||
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||
@@ -224,6 +225,7 @@ Page({
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
console.log('[Referral] ✅ 数据设置完成')
|
||||
console.log('[Referral] - 绑定中:', this.data.bindingCount)
|
||||
@@ -648,13 +650,26 @@ Page({
|
||||
content: `将提现 ¥${availableEarnings.toFixed(2)} 到您的微信零钱`,
|
||||
confirmText: '立即提现',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
if (!res.confirm) return
|
||||
const tmplId = app.globalData.withdrawSubscribeTmplId
|
||||
if (tmplId && tmplId.length > 10) {
|
||||
wx.requestSubscribeMessage({
|
||||
tmplIds: [tmplId],
|
||||
success: () => { this.doWithdraw(availableEarnings) },
|
||||
fail: () => { this.doWithdraw(availableEarnings) }
|
||||
})
|
||||
} else {
|
||||
await this.doWithdraw(availableEarnings)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
// 跳转提现记录页
|
||||
goToWithdrawRecords() {
|
||||
wx.navigateTo({ url: '/pages/withdraw-records/withdraw-records' })
|
||||
},
|
||||
|
||||
// 执行提现
|
||||
async doWithdraw(amount) {
|
||||
wx.showLoading({ title: '提现中...' })
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
{{availableEarningsNum < minWithdrawAmount ? '满' + minWithdrawAmount + '元可提现' : !hasWechatId ? '请先绑定微信号' : '申请提现 ¥' + availableEarnings}}
|
||||
</view>
|
||||
<text class="wechat-tip" wx:if="{{availableEarningsNum > 0 && !hasWechatId}}">为便于提现到账,请先到「设置」中绑定微信号</text>
|
||||
<view class="withdraw-records-link" bindtap="goToWithdrawRecords">查看提现记录</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
.withdraw-btn { padding: 28rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #fff; font-size: 32rpx; font-weight: 600; text-align: center; border-radius: 24rpx; box-shadow: 0 8rpx 24rpx rgba(0,206,209,0.3); }
|
||||
.withdraw-btn.btn-disabled { background: rgba(0,206,209,0.2); color: rgba(255,255,255,0.3); box-shadow: none; }
|
||||
.wechat-tip { display: block; font-size: 24rpx; color: rgba(255,165,0,0.9); margin-top: 16rpx; text-align: center; }
|
||||
.withdraw-records-link { display: block; margin-top: 16rpx; text-align: center; font-size: 26rpx; color: #00CED1; }
|
||||
|
||||
/* ???? - ?? Next.js 4??? */
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
|
||||
|
||||
72
miniprogram/pages/withdraw-records/withdraw-records.js
Normal file
72
miniprogram/pages/withdraw-records/withdraw-records.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 提现记录 - 独立页面
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
list: [],
|
||||
loading: true
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
async loadRecords() {
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (!app.globalData.isLoggedIn || !userInfo || !userInfo.id) {
|
||||
this.setData({ list: [], loading: false })
|
||||
return
|
||||
}
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
const res = await app.request('/api/withdraw/records?userId=' + userInfo.id)
|
||||
if (res && res.success && res.data && Array.isArray(res.data.list)) {
|
||||
const list = (res.data.list || []).map(item => ({
|
||||
id: item.id,
|
||||
amount: (item.amount != null ? item.amount : 0).toFixed(2),
|
||||
status: this.statusText(item.status),
|
||||
statusRaw: item.status,
|
||||
created_at: item.created_at ? this.formatDate(item.created_at) : '--'
|
||||
}))
|
||||
this.setData({ list, loading: false })
|
||||
} else {
|
||||
this.setData({ list: [], loading: false })
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[WithdrawRecords] 加载失败:', e)
|
||||
this.setData({ list: [], loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
statusText(status) {
|
||||
const map = {
|
||||
pending: '待审核',
|
||||
pending_confirm: '待确认收款',
|
||||
processing: '处理中',
|
||||
success: '已到账',
|
||||
failed: '已拒绝'
|
||||
}
|
||||
return map[status] || status || '--'
|
||||
},
|
||||
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '--'
|
||||
const d = new Date(dateStr)
|
||||
const y = d.getFullYear()
|
||||
const m = (d.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = d.getDate().toString().padStart(2, '0')
|
||||
return `${y}-${m}-${day}`
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
}
|
||||
})
|
||||
4
miniprogram/pages/withdraw-records/withdraw-records.json
Normal file
4
miniprogram/pages/withdraw-records/withdraw-records.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
22
miniprogram/pages/withdraw-records/withdraw-records.wxml
Normal file
22
miniprogram/pages/withdraw-records/withdraw-records.wxml
Normal file
@@ -0,0 +1,22 @@
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack">←</view>
|
||||
<text class="nav-title">提现记录</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view class="nav-placeholder-bar" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="content">
|
||||
<view class="loading-tip" wx:if="{{loading}}">加载中...</view>
|
||||
<view class="empty" wx:elif="{{list.length === 0}}">暂无提现记录</view>
|
||||
<view class="list" wx:else>
|
||||
<view class="item" wx:for="{{list}}" wx:key="id">
|
||||
<view class="item-left">
|
||||
<text class="amount">¥{{item.amount}}</text>
|
||||
<text class="time">{{item.created_at}}</text>
|
||||
</view>
|
||||
<text class="status status-{{item.statusRaw}}">{{item.status}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
57
miniprogram/pages/withdraw-records/withdraw-records.wxss
Normal file
57
miniprogram/pages/withdraw-records/withdraw-records.wxss
Normal file
@@ -0,0 +1,57 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #000;
|
||||
}
|
||||
.nav-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: rgba(0,0,0,0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24rpx;
|
||||
height: 88rpx;
|
||||
}
|
||||
.nav-back {
|
||||
color: #00CED1;
|
||||
font-size: 36rpx;
|
||||
padding: 16rpx;
|
||||
}
|
||||
.nav-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
.nav-placeholder { width: 80rpx; }
|
||||
.nav-placeholder-bar { width: 100%; }
|
||||
|
||||
.content {
|
||||
padding: 32rpx;
|
||||
}
|
||||
.loading-tip, .empty {
|
||||
text-align: center;
|
||||
color: rgba(255,255,255,0.6);
|
||||
font-size: 28rpx;
|
||||
padding: 80rpx 0;
|
||||
}
|
||||
.list { }
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 0;
|
||||
border-bottom: 2rpx solid rgba(255,255,255,0.06);
|
||||
}
|
||||
.item:last-child { border-bottom: none; }
|
||||
.item-left { display: flex; flex-direction: column; gap: 8rpx; }
|
||||
.amount { font-size: 32rpx; font-weight: 600; color: #fff; }
|
||||
.time { font-size: 24rpx; color: rgba(255,255,255,0.5); }
|
||||
.status { font-size: 26rpx; }
|
||||
.status.status-pending { color: #FFA500; }
|
||||
.status.status-pending_confirm { color: #4CAF50; }
|
||||
.status.status-success { color: #4CAF50; }
|
||||
.status.status-failed { color: rgba(255,255,255,0.5); }
|
||||
@@ -237,16 +237,18 @@ model users {
|
||||
}
|
||||
|
||||
model withdrawals {
|
||||
id String @id @db.VarChar(50)
|
||||
user_id String @db.VarChar(50)
|
||||
amount Decimal @db.Decimal(10, 2)
|
||||
status withdrawals_status? @default(pending)
|
||||
wechat_openid String? @db.VarChar(100)
|
||||
wechat_id String? @db.VarChar(100) // 用户微信号,用于后台列表展示与核对
|
||||
transaction_id String? @db.VarChar(100)
|
||||
error_message String? @db.VarChar(500)
|
||||
created_at DateTime @default(now()) @db.Timestamp(0)
|
||||
processed_at DateTime? @db.Timestamp(0)
|
||||
id String @id @db.VarChar(50)
|
||||
user_id String @db.VarChar(50)
|
||||
amount Decimal @db.Decimal(10, 2)
|
||||
status withdrawals_status? @default(pending)
|
||||
wechat_openid String? @db.VarChar(100)
|
||||
wechat_id String? @db.VarChar(100) // 用户微信号,用于后台列表展示与核对
|
||||
transaction_id String? @db.VarChar(100)
|
||||
transfer_bill_no String? @db.VarChar(100) // 微信转账单号(用户确认模式)
|
||||
package_info String? @db.VarChar(1024) // 调起 wx.requestMerchantTransfer 用
|
||||
error_message String? @db.VarChar(500)
|
||||
created_at DateTime @default(now()) @db.Timestamp(0)
|
||||
processed_at DateTime? @db.Timestamp(0)
|
||||
|
||||
@@index([status], map: "idx_status")
|
||||
@@index([user_id], map: "idx_user_id")
|
||||
@@ -261,6 +263,7 @@ enum match_records_match_type {
|
||||
|
||||
enum withdrawals_status {
|
||||
pending
|
||||
pending_confirm // 已发起转账,待用户在小程序内确认收款
|
||||
processing
|
||||
success
|
||||
failed
|
||||
|
||||
Reference in New Issue
Block a user