360 lines
14 KiB
TypeScript
360 lines
14 KiB
TypeScript
/**
|
||
* 一次性修复脚本:补录内部转账入账
|
||
*
|
||
* ⚠️ 警告:此脚本涉及资金操作,请务必仔细核对!
|
||
*
|
||
* 用于修复因接收方钱包未创建导致入账失败的内部转账。
|
||
*
|
||
* 执行前必须确认:
|
||
* 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();
|