/** * 一次性修复脚本:补录内部转账入账 * * ⚠️ 警告:此脚本涉及资金操作,请务必仔细核对! * * 用于修复因接收方钱包未创建导致入账失败的内部转账。 * * 执行前必须确认: * 1. 订单号正确 * 2. 订单状态是 CONFIRMED(链上已完成) * 3. 接收方确实没有收到这笔转账 * 4. 数据库中没有对应的 TRANSFER_IN 流水 * * 安全机制: * 1. DRY_RUN 模式:默认只检查不执行,需要手动改为 false 才会真正执行 * 2. 幂等性:检查 refOrderId + accountSequence + entryType 是否已存在 * 3. 事务性:所有操作在同一个数据库事务中 * 4. 乐观锁:使用 version 字段防止并发修改 * 5. 审计追踪:payloadJson.dataFix=true 标记为修复操作 * * 使用方法: * 在 wallet-service 容器内执行: * npx ts-node scripts/fix-missing-transfer-in.ts * * 环境变量要求: * - DATABASE_URL: 数据库连接字符串 */ import { PrismaClient } from '@prisma/client'; import Decimal from 'decimal.js'; // ========== 配置 ========== // 只需提供订单号,其他信息自动从数据库获取 const ORDER_NO = 'WD1767599904858VG01WF'; // ⚠️ 安全开关:设为 true 时只检查不执行,设为 false 才会真正修复 const DRY_RUN = true; // ========================== function log(level: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG', message: string, data?: unknown) { const timestamp = new Date().toISOString(); const prefix = `[${timestamp}] [${level}]`; if (data !== undefined) { console.log(`${prefix} ${message}`, JSON.stringify(data, null, 2)); } else { console.log(`${prefix} ${message}`); } } async function main() { log('INFO', '========================================'); log('INFO', '内部转账入账修复脚本'); log('INFO', '========================================'); log('INFO', `订单号: ${ORDER_NO}`); log('INFO', `模式: ${DRY_RUN ? '🔍 DRY_RUN (只检查不执行)' : '⚡ LIVE (真正执行)'}`); log('INFO', ''); if (DRY_RUN) { log('WARN', '⚠️ DRY_RUN 模式:以下操作不会真正执行'); log('WARN', '⚠️ 确认无误后,将 DRY_RUN 改为 false 再次执行'); log('INFO', ''); } const prisma = new PrismaClient({ log: ['warn', 'error'], }); try { await prisma.$transaction(async (tx) => { // ==================== Step 1: 查询订单信息 ==================== log('INFO', '[Step 1/7] 查询订单信息...'); const order = await tx.withdrawalOrder.findUnique({ where: { orderNo: ORDER_NO }, }); if (!order) { throw new Error(`❌ 订单不存在: ${ORDER_NO}`); } log('DEBUG', '订单原始数据:', { id: order.id, orderNo: order.orderNo, status: order.status, isInternalTransfer: order.isInternalTransfer, accountSequence: order.accountSequence, userId: order.userId.toString(), toAccountSequence: order.toAccountSequence, toUserId: order.toUserId?.toString(), amount: order.amount.toString(), fee: order.fee.toString(), txHash: order.txHash, confirmedAt: order.confirmedAt?.toISOString(), createdAt: order.createdAt.toISOString(), }); // ==================== Step 2: 验证订单状态 ==================== log('INFO', '[Step 2/7] 验证订单状态...'); if (order.status !== 'CONFIRMED') { throw new Error(`❌ 订单状态不是 CONFIRMED,当前状态: ${order.status}`); } log('INFO', ` ✓ 订单状态: CONFIRMED`); if (!order.isInternalTransfer) { throw new Error(`❌ 订单不是内部转账,isInternalTransfer=${order.isInternalTransfer}`); } log('INFO', ` ✓ 订单类型: 内部转账`); if (!order.toAccountSequence) { throw new Error(`❌ 订单缺少接收方账号 toAccountSequence`); } if (!order.toUserId) { throw new Error(`❌ 订单缺少接收方用户ID toUserId`); } log('INFO', ` ✓ 接收方信息完整`); if (!order.txHash) { throw new Error(`❌ 订单缺少交易哈希 txHash,链上交易可能未完成`); } log('INFO', ` ✓ 链上交易已完成: ${order.txHash}`); const toAccountSequence = order.toAccountSequence; const toUserId = order.toUserId; const transferAmount = new Decimal(order.amount.toString()); log('INFO', ''); log('INFO', '📋 订单摘要:'); log('INFO', ` 订单号: ${order.orderNo}`); log('INFO', ` 转出方: ${order.accountSequence} (userId=${order.userId})`); log('INFO', ` 接收方: ${toAccountSequence} (userId=${toUserId})`); log('INFO', ` 金额: ${transferAmount.toString()} USDT`); log('INFO', ` 手续费: ${order.fee.toString()} USDT`); log('INFO', ` TxHash: ${order.txHash}`); log('INFO', ` 确认时间: ${order.confirmedAt?.toISOString() || 'N/A'}`); log('INFO', ''); // ==================== Step 3: 检查转出方流水 ==================== log('INFO', '[Step 3/7] 检查转出方 TRANSFER_OUT 流水...'); const transferOutEntry = await tx.ledgerEntry.findFirst({ where: { refOrderId: ORDER_NO, accountSequence: order.accountSequence, entryType: 'TRANSFER_OUT', }, }); if (!transferOutEntry) { log('WARN', ` ⚠️ 转出方没有 TRANSFER_OUT 流水!这可能表示整个转账都没有正常处理`); log('WARN', ` 请先确认转出方是否已扣款`); } else { log('INFO', ` ✓ 转出方 TRANSFER_OUT 流水存在 (id=${transferOutEntry.id})`); log('DEBUG', '转出方流水:', { id: transferOutEntry.id, amount: transferOutEntry.amount.toString(), balanceAfter: transferOutEntry.balanceAfter?.toString() ?? 'null', createdAt: transferOutEntry.createdAt.toISOString(), }); } // ==================== Step 4: 幂等性检查 ==================== log('INFO', '[Step 4/7] 幂等性检查 - 接收方 TRANSFER_IN 流水...'); const existingTransferIn = await tx.ledgerEntry.findFirst({ where: { refOrderId: ORDER_NO, accountSequence: toAccountSequence, entryType: 'TRANSFER_IN', }, }); if (existingTransferIn) { log('ERROR', ` ❌ TRANSFER_IN 流水已存在!`); log('ERROR', '流水详情:', { id: existingTransferIn.id, amount: existingTransferIn.amount.toString(), balanceAfter: existingTransferIn.balanceAfter?.toString() ?? 'null', createdAt: existingTransferIn.createdAt.toISOString(), }); log('ERROR', ''); log('ERROR', '========================================'); log('ERROR', '❌ 中止:接收方已有入账流水,不能重复入账!'); log('ERROR', '========================================'); return; } log('INFO', ` ✓ 未找到 TRANSFER_IN 流水,可以安全入账`); // ==================== Step 5: 查找接收方钱包 ==================== log('INFO', '[Step 5/7] 查找接收方钱包...'); let wallet = await tx.walletAccount.findUnique({ where: { accountSequence: toAccountSequence }, }); if (!wallet) { wallet = await tx.walletAccount.findUnique({ where: { userId: toUserId }, }); } if (wallet) { log('INFO', ` ✓ 钱包已存在`); log('DEBUG', '钱包信息:', { id: wallet.id, accountSequence: wallet.accountSequence, userId: wallet.userId.toString(), usdtAvailable: wallet.usdtAvailable.toString(), usdtFrozen: wallet.usdtFrozen.toString(), version: wallet.version, createdAt: wallet.createdAt.toISOString(), }); } else { log('WARN', ` ⚠️ 钱包不存在,需要创建`); } // ==================== Step 6: 计算新余额 ==================== log('INFO', '[Step 6/7] 计算新余额...'); const currentBalance = wallet ? new Decimal(wallet.usdtAvailable.toString()) : new Decimal(0); const newBalance = currentBalance.add(transferAmount); const currentVersion = wallet?.version ?? 0; log('INFO', ` 当前余额: ${currentBalance.toString()} USDT`); log('INFO', ` 转账金额: +${transferAmount.toString()} USDT`); log('INFO', ` 新余额: ${newBalance.toString()} USDT`); log('INFO', ''); // ==================== DRY_RUN 检查 ==================== if (DRY_RUN) { log('INFO', '========================================'); log('INFO', '🔍 DRY_RUN 模式 - 检查完成'); log('INFO', '========================================'); log('INFO', ''); log('INFO', '以上检查全部通过,可以安全执行修复。'); log('INFO', ''); log('INFO', '将要执行的操作:'); if (!wallet) { log('INFO', ` 1. 创建钱包: ${toAccountSequence} (userId=${toUserId})`); log('INFO', ` 2. 设置余额: ${newBalance.toString()} USDT`); } else { log('INFO', ` 1. 更新钱包余额: ${currentBalance.toString()} -> ${newBalance.toString()} USDT`); log('INFO', ` 2. 更新版本号: ${currentVersion} -> ${currentVersion + 1}`); } log('INFO', ` 3. 创建 TRANSFER_IN 流水记录`); log('INFO', ''); log('INFO', '确认无误后,请将 DRY_RUN 改为 false 再次执行。'); return; } // ==================== Step 7: 执行修复 ==================== log('INFO', '[Step 7/7] 执行修复...'); // 7a. 创建或更新钱包 if (!wallet) { log('INFO', ' 创建钱包...'); wallet = await tx.walletAccount.create({ data: { accountSequence: toAccountSequence, userId: toUserId, usdtAvailable: newBalance, // 直接设置为转账金额 usdtFrozen: new Decimal(0), dstAvailable: new Decimal(0), dstFrozen: new Decimal(0), bnbAvailable: new Decimal(0), bnbFrozen: new Decimal(0), ogAvailable: new Decimal(0), ogFrozen: new Decimal(0), rwadAvailable: new Decimal(0), rwadFrozen: new Decimal(0), hashpower: new Decimal(0), pendingUsdt: new Decimal(0), pendingHashpower: new Decimal(0), settleableUsdt: new Decimal(0), settleableHashpower: new Decimal(0), settledTotalUsdt: new Decimal(0), settledTotalHashpower: new Decimal(0), expiredTotalUsdt: new Decimal(0), expiredTotalHashpower: new Decimal(0), status: 'ACTIVE', hasPlanted: false, version: 1, }, }); log('INFO', ` ✓ 钱包创建成功 (id=${wallet.id}, balance=${newBalance.toString()})`); } else { log('INFO', ' 更新钱包余额...'); const updateResult = await tx.walletAccount.updateMany({ where: { id: wallet.id, version: currentVersion, }, data: { usdtAvailable: newBalance, version: currentVersion + 1, updatedAt: new Date(), }, }); if (updateResult.count === 0) { throw new Error('❌ 乐观锁冲突:钱包数据已被其他操作修改,请重试'); } log('INFO', ` ✓ 余额更新成功 (version: ${currentVersion} -> ${currentVersion + 1})`); } // 7b. 创建流水记录 log('INFO', ' 创建流水记录...'); const entry = await tx.ledgerEntry.create({ data: { accountSequence: toAccountSequence, userId: toUserId, entryType: 'TRANSFER_IN', amount: transferAmount, assetType: 'USDT', balanceAfter: newBalance, refOrderId: ORDER_NO, refTxHash: order.txHash, memo: `来自 ${order.accountSequence} 的转账(数据修复)`, payloadJson: { fromAccountSequence: order.accountSequence, fromUserId: order.userId.toString(), originalOrderNo: ORDER_NO, dataFix: true, fixedAt: new Date().toISOString(), fixReason: 'Receiver wallet not existed when transfer confirmed', scriptVersion: '1.0.0', }, }, }); log('INFO', ` ✓ 流水记录创建成功 (id=${entry.id})`); // ==================== 完成 ==================== log('INFO', ''); log('INFO', '========================================'); log('INFO', '✅ 修复成功!'); log('INFO', '========================================'); log('INFO', `接收方: ${toAccountSequence}`); log('INFO', `入账金额: ${transferAmount.toString()} USDT`); log('INFO', `新余额: ${newBalance.toString()} USDT`); log('INFO', `流水ID: ${entry.id}`); log('INFO', ''); log('INFO', '验证命令:'); log('INFO', ` SELECT account_sequence, usdt_available, version FROM wallet_accounts WHERE account_sequence = '${toAccountSequence}';`); log('INFO', ` SELECT id, entry_type, amount, balance_after, ref_order_id, created_at FROM wallet_ledger_entries WHERE ref_order_id = '${ORDER_NO}';`); }); } catch (error) { log('ERROR', ''); log('ERROR', '========================================'); log('ERROR', '❌ 修复失败'); log('ERROR', '========================================'); if (error instanceof Error) { log('ERROR', `错误信息: ${error.message}`); log('DEBUG', `错误堆栈: ${error.stack}`); } else { log('ERROR', '未知错误:', error); } process.exit(1); } finally { await prisma.$disconnect(); } } main();