From 8e67eb5d62bb99c439812e477fcace7d53a60d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=98=E9=A3=8E?= Date: Fri, 6 Feb 2026 19:45:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8F=90=E7=8E=B0=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=EF=BC=8C=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=E6=A8=A1=E5=BC=8F=E4=BB=A5=E6=94=AF=E6=8C=81=E5=BE=85?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=A1=AE=E8=AE=A4=E7=9A=84=E8=BD=AC=E8=B4=A6?= =?UTF-8?q?=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3API=E5=92=8C?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E7=BB=93=E6=9E=84=E4=BB=A5=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E6=95=B0=E6=8D=AE=E4=B8=80=E8=87=B4=E6=80=A7=E3=80=82?= =?UTF-8?q?=E5=90=8C=E6=97=B6=EF=BC=8C=E8=B0=83=E6=95=B4=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E7=95=8C=E9=9D=A2=E4=BB=A5=E5=B1=95=E7=A4=BA=E5=BE=85?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E6=94=B6=E6=AC=BE=E4=BF=A1=E6=81=AF=EF=BC=8C?= =?UTF-8?q?=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/admin/withdrawals/route.ts | 85 ++++++++++++----- .../payment/wechat/transfer/notify/route.ts | 71 ++++++++++---- app/api/withdraw/pending-confirm/route.ts | 53 +++++++++++ app/api/withdraw/records/route.ts | 45 +++++++++ app/api/withdraw/route.ts | 61 +++++------- lib/wechat-transfer.ts | 94 +++++++++++++++++++ miniprogram/app.js | 3 + miniprogram/app.json | 2 +- miniprogram/pages/my/my.js | 84 +++++++++++++++++ miniprogram/pages/my/my.wxml | 18 ++++ miniprogram/pages/my/my.wxss | 25 +++++ miniprogram/pages/referral/referral.js | 23 ++++- miniprogram/pages/referral/referral.wxml | 1 + miniprogram/pages/referral/referral.wxss | 1 + .../withdraw-records/withdraw-records.js | 72 ++++++++++++++ .../withdraw-records/withdraw-records.json | 4 + .../withdraw-records/withdraw-records.wxml | 22 +++++ .../withdraw-records/withdraw-records.wxss | 57 +++++++++++ prisma/schema.prisma | 23 +++-- 19 files changed, 649 insertions(+), 95 deletions(-) create mode 100644 app/api/withdraw/pending-confirm/route.ts create mode 100644 app/api/withdraw/records/route.ts create mode 100644 miniprogram/pages/withdraw-records/withdraw-records.js create mode 100644 miniprogram/pages/withdraw-records/withdraw-records.json create mode 100644 miniprogram/pages/withdraw-records/withdraw-records.wxml create mode 100644 miniprogram/pages/withdraw-records/withdraw-records.wxss diff --git a/app/api/admin/withdrawals/route.ts b/app/api/admin/withdrawals/route.ts index c7cc0448..f6e3e596 100644 --- a/app/api/admin/withdrawals/route.ts +++ b/app/api/admin/withdrawals/route.ts @@ -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: '已发起转账,请通知用户在小程序内「确认收款」完成到账' }) } diff --git a/app/api/payment/wechat/transfer/notify/route.ts b/app/api/payment/wechat/transfer/notify/route.ts index c32cfa45..6c1484d8 100644 --- a/app/api/payment/wechat/transfer/notify/route.ts +++ b/app/api/payment/wechat/transfer/notify/route.ts @@ -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) diff --git a/app/api/withdraw/pending-confirm/route.ts b/app/api/withdraw/pending-confirm/route.ts new file mode 100644 index 00000000..3bc8a0dc --- /dev/null +++ b/app/api/withdraw/pending-confirm/route.ts @@ -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 } + ) + } +} diff --git a/app/api/withdraw/records/route.ts b/app/api/withdraw/records/route.ts new file mode 100644 index 00000000..6f12af29 --- /dev/null +++ b/app/api/withdraw/records/route.ts @@ -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 } + ) + } +} diff --git a/app/api/withdraw/route.ts b/app/api/withdraw/route.ts index 6ed3e02a..bd23f46c 100644 --- a/app/api/withdraw/route.ts +++ b/app/api/withdraw/route.ts @@ -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 { @@ -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' } }) diff --git a/lib/wechat-transfer.ts b/lib/wechat-transfer.ts index b8f6eff9..bfc828bf 100644 --- a/lib/wechat-transfer.ts +++ b/lib/wechat-transfer.ts @@ -158,6 +158,100 @@ export async function createTransfer(params: CreateTransferParams): Promise { + 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 + 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) */ diff --git a/miniprogram/app.js b/miniprogram/app.js index 1635a60c..7faa7ae4 100644 --- a/miniprogram/app.js +++ b/miniprogram/app.js @@ -11,6 +11,9 @@ App({ // 小程序配置 - 真实AppID appId: 'wxb8bbb2b10dec74aa', + + // 订阅消息:用户点击「申请提现」→「立即提现」时会先弹出订阅授权窗 + withdrawSubscribeTmplId: 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE', // 微信支付配置 mchId: '1318592501', // 商户号 diff --git a/miniprogram/app.json b/miniprogram/app.json index 392721cb..e3ba264d 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -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", diff --git a/miniprogram/pages/my/my.js b/miniprogram/pages/my/my.js index 819e6db7..59088eb8 100644 --- a/miniprogram/pages/my/my.js +++ b/miniprogram/pages/my/my.js @@ -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' } diff --git a/miniprogram/pages/my/my.wxml b/miniprogram/pages/my/my.wxml index 5895e8c2..c6a0c691 100644 --- a/miniprogram/pages/my/my.wxml +++ b/miniprogram/pages/my/my.wxml @@ -109,6 +109,24 @@ + + + + 待确认收款 + 审核已通过,点击下方按钮完成收款 + 暂无待确认的提现,审核通过后会出现在这里 + + + + + ¥{{item.amount}} + {{item.created_at}} + + 确认收款 + + + + { - 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: '提现中...' }) diff --git a/miniprogram/pages/referral/referral.wxml b/miniprogram/pages/referral/referral.wxml index 1e605ecb..d8978fa7 100644 --- a/miniprogram/pages/referral/referral.wxml +++ b/miniprogram/pages/referral/referral.wxml @@ -53,6 +53,7 @@ {{availableEarningsNum < minWithdrawAmount ? '满' + minWithdrawAmount + '元可提现' : !hasWechatId ? '请先绑定微信号' : '申请提现 ¥' + availableEarnings}} 为便于提现到账,请先到「设置」中绑定微信号 + 查看提现记录 diff --git a/miniprogram/pages/referral/referral.wxss b/miniprogram/pages/referral/referral.wxss index 4b52cdd0..b75eb535 100644 --- a/miniprogram/pages/referral/referral.wxss +++ b/miniprogram/pages/referral/referral.wxss @@ -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; } diff --git a/miniprogram/pages/withdraw-records/withdraw-records.js b/miniprogram/pages/withdraw-records/withdraw-records.js new file mode 100644 index 00000000..e449b89a --- /dev/null +++ b/miniprogram/pages/withdraw-records/withdraw-records.js @@ -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() + } +}) diff --git a/miniprogram/pages/withdraw-records/withdraw-records.json b/miniprogram/pages/withdraw-records/withdraw-records.json new file mode 100644 index 00000000..e90e9960 --- /dev/null +++ b/miniprogram/pages/withdraw-records/withdraw-records.json @@ -0,0 +1,4 @@ +{ + "usingComponents": {}, + "navigationStyle": "custom" +} diff --git a/miniprogram/pages/withdraw-records/withdraw-records.wxml b/miniprogram/pages/withdraw-records/withdraw-records.wxml new file mode 100644 index 00000000..49e80a83 --- /dev/null +++ b/miniprogram/pages/withdraw-records/withdraw-records.wxml @@ -0,0 +1,22 @@ + + + + 提现记录 + + + + + + 加载中... + 暂无提现记录 + + + + ¥{{item.amount}} + {{item.created_at}} + + {{item.status}} + + + + diff --git a/miniprogram/pages/withdraw-records/withdraw-records.wxss b/miniprogram/pages/withdraw-records/withdraw-records.wxss new file mode 100644 index 00000000..a70da91a --- /dev/null +++ b/miniprogram/pages/withdraw-records/withdraw-records.wxss @@ -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); } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d12ea2e2..2e467437 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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