fix(wallet-service): 添加一次性修复脚本 D25122600004->D25122600006 转账
- 修复因并发修改导致的冻结余额不足问题 - 自动完成内部转账、记录流水、更新订单状态 - 幂等执行,可安全重启 - 部署成功后请删除 otp/ 目录和相关引用 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
305bdf63af
commit
c2ff11bd6d
|
|
@ -414,7 +414,9 @@
|
|||
"Bash(node -e \"\nconst { ethers } = require\\(''ethers''\\);\n\nconst KAVA_TESTNET_RPC = ''https://evm.testnet.kava.io'';\nconst privateKey = ''0xd42a6e6021ebd884f3f179d3793a32e97b9f1001db6ff44441ec455d748b9aa6'';\nconst USDT_CONTRACT = ''0xc12f6A4A7Fd0965085B044A67a39CcA2ff7fe0dF'';\nconst TO_ADDRESS = ''0x184571959d74a6e771ad4e5b2fbe006951dd29ec'';\n\nasync function transfer\\(\\) {\n const provider = new ethers.JsonRpcProvider\\(KAVA_TESTNET_RPC\\);\n const wallet = new ethers.Wallet\\(privateKey, provider\\);\n \n const abi = [''function transfer\\(address to, uint256 amount\\) returns \\(bool\\)'', ''function balanceOf\\(address\\) view returns \\(uint256\\)''];\n const contract = new ethers.Contract\\(USDT_CONTRACT, abi, wallet\\);\n \n // 10,000,000 USDT = 10000000 * 1e6 \\(6 decimals\\)\n const amount = BigInt\\(10000000\\) * BigInt\\(1000000\\);\n \n console.log\\(''Transferring 10,000,000 USDT to'', TO_ADDRESS\\);\n const tx = await contract.transfer\\(TO_ADDRESS, amount, { gasLimit: 100000 }\\);\n console.log\\(''TX Hash:'', tx.hash\\);\n await tx.wait\\(\\);\n \n const newBalance = await contract.balanceOf\\(TO_ADDRESS\\);\n console.log\\(''New balance:'', Number\\(newBalance\\) / 1e6, ''USDT''\\);\n}\n\ntransfer\\(\\).catch\\(e => console.error\\(''Error:'', e.message\\)\\);\n\")",
|
||||
"Bash(node -e \"\nconst { ethers } = require\\(''ethers''\\);\n\nconst KAVA_TESTNET_RPC = ''https://evm.testnet.kava.io'';\nconst privateKey = ''0xd42a6e6021ebd884f3f179d3793a32e97b9f1001db6ff44441ec455d748b9aa6'';\nconst USDT_CONTRACT = ''0xc12f6A4A7Fd0965085B044A67a39CcA2ff7fe0dF'';\nconst TO_ADDRESS = ''0x184571959d74a6e771ad4e5b2fbe006951dd29ec'';\n\nasync function transfer\\(\\) {\n const provider = new ethers.JsonRpcProvider\\(KAVA_TESTNET_RPC\\);\n const wallet = new ethers.Wallet\\(privateKey, provider\\);\n \n const abi = [''function transfer\\(address to, uint256 amount\\) returns \\(bool\\)'', ''function balanceOf\\(address\\) view returns \\(uint256\\)''];\n const contract = new ethers.Contract\\(USDT_CONTRACT, abi, wallet\\);\n \n // 10,000,000,000 USDT \\(100亿\\) = 10000000000 * 1e6 \\(6 decimals\\)\n const amount = BigInt\\(10000000000\\) * BigInt\\(1000000\\);\n \n console.log\\(''Transferring 10,000,000,000 USDT to'', TO_ADDRESS\\);\n const tx = await contract.transfer\\(TO_ADDRESS, amount, { gasLimit: 100000 }\\);\n console.log\\(''TX Hash:'', tx.hash\\);\n await tx.wait\\(\\);\n \n const newBalance = await contract.balanceOf\\(TO_ADDRESS\\);\n console.log\\(''New balance:'', Number\\(newBalance\\) / 1e6, ''USDT''\\);\n}\n\ntransfer\\(\\).catch\\(e => console.error\\(''Error:'', e.message\\)\\);\n\")",
|
||||
"Bash(node -e \"\nconst { ethers } = require\\(''ethers''\\);\n\nconst KAVA_TESTNET_RPC = ''https://evm.testnet.kava.io'';\nconst privateKey = ''0xd42a6e6021ebd884f3f179d3793a32e97b9f1001db6ff44441ec455d748b9aa6'';\nconst USDT_CONTRACT = ''0xc12f6A4A7Fd0965085B044A67a39CcA2ff7fe0dF'';\nconst TO_ADDRESS = ''0x25bc2f6cebb902cb51f7b51bff81e0f776b07b14'';\n\nasync function transfer\\(\\) {\n const provider = new ethers.JsonRpcProvider\\(KAVA_TESTNET_RPC\\);\n const wallet = new ethers.Wallet\\(privateKey, provider\\);\n \n const abi = [''function transfer\\(address to, uint256 amount\\) returns \\(bool\\)'', ''function balanceOf\\(address\\) view returns \\(uint256\\)''];\n const contract = new ethers.Contract\\(USDT_CONTRACT, abi, wallet\\);\n \n // 2,000,000,000 USDT \\(20亿\\) = 2000000000 * 1e6 \\(6 decimals\\)\n const amount = BigInt\\(2000000000\\) * BigInt\\(1000000\\);\n \n console.log\\(''Transferring 2,000,000,000 USDT to'', TO_ADDRESS\\);\n const tx = await contract.transfer\\(TO_ADDRESS, amount, { gasLimit: 100000 }\\);\n console.log\\(''TX Hash:'', tx.hash\\);\n await tx.wait\\(\\);\n \n const newBalance = await contract.balanceOf\\(TO_ADDRESS\\);\n console.log\\(''New balance:'', Number\\(newBalance\\) / 1e6, ''USDT''\\);\n}\n\ntransfer\\(\\).catch\\(e => console.error\\(''Error:'', e.message\\)\\);\n\")",
|
||||
"Bash(dir /s /b \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\backend\\\\services\\\\blockchain-service\\\\*.prisma\")"
|
||||
"Bash(dir /s /b \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\backend\\\\services\\\\blockchain-service\\\\*.prisma\")",
|
||||
"Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(wallet-service\\): 添加钱包乐观锁防止并发修改\n\n- WalletAccount aggregate 添加 version 字段\n- WalletAccountRepositoryImpl 使用 updateMany + version 检查实现乐观锁\n- requestWithdrawal 添加重试机制处理乐观锁冲突\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
||||
"Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(wallet-service\\): 添加一次性修复脚本 D25122600004->D25122600006 转账\n\n- 修复因并发修改导致的冻结余额不足问题\n- 自动完成内部转账、记录流水、更新订单状态\n- 幂等执行,可安全重启\n- 部署成功后请删除 otp/ 目录和相关引用\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import {
|
|||
import { RedisModule } from './redis';
|
||||
import { KafkaModule } from './kafka';
|
||||
import { IdentityModule } from './external/identity';
|
||||
// OTP: One-Time fix for D25122600004 -> D25122600006 transfer (remove after fix)
|
||||
import { TransferFixService } from './otp/transfer-fix.service';
|
||||
|
||||
const repositories = [
|
||||
{
|
||||
|
|
@ -52,7 +54,7 @@ const repositories = [
|
|||
@Global()
|
||||
@Module({
|
||||
imports: [RedisModule, KafkaModule, IdentityModule],
|
||||
providers: [PrismaService, ...repositories],
|
||||
providers: [PrismaService, ...repositories, TransferFixService], // OTP: remove TransferFixService after fix
|
||||
exports: [PrismaService, RedisModule, KafkaModule, IdentityModule, FeeConfigRepositoryImpl, ...repositories],
|
||||
})
|
||||
export class InfrastructureModule {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,185 @@
|
|||
/**
|
||||
* One-Time-Fix: 修复 D25122600004 -> D25122600006 的转账
|
||||
*
|
||||
* 问题:由于并发修改导致冻结余额少了 2 USDT (手续费)
|
||||
* 解决:修复冻结余额,完成转账,更新订单状态
|
||||
*
|
||||
* 幂等性:检查订单状态,已 CONFIRMED 则跳过
|
||||
* 部署后删除此文件和 infrastructure.module.ts 中的引用
|
||||
*/
|
||||
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service';
|
||||
import { LedgerEntryType, WithdrawalStatus } from '@/domain/value-objects';
|
||||
import Decimal from 'decimal.js';
|
||||
|
||||
@Injectable()
|
||||
export class TransferFixService implements OnModuleInit {
|
||||
private readonly logger = new Logger(TransferFixService.name);
|
||||
|
||||
// 需要修复的订单号
|
||||
private readonly ORDER_NO = 'WD1766719397843H90GUW';
|
||||
private readonly SENDER_ACCOUNT = 'D25122600004';
|
||||
private readonly RECEIVER_ACCOUNT = 'D25122600006';
|
||||
private readonly MISSING_FEE = new Decimal('2'); // 缺失的手续费
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async onModuleInit() {
|
||||
// 延迟 5 秒执行,确保所有服务都已启动
|
||||
setTimeout(() => this.executeFixOnce(), 5000);
|
||||
}
|
||||
|
||||
private async executeFixOnce(): Promise<void> {
|
||||
this.logger.log('========================================');
|
||||
this.logger.log('[OTP-FIX] Starting one-time transfer fix');
|
||||
this.logger.log(`[OTP-FIX] Order: ${this.ORDER_NO}`);
|
||||
this.logger.log(`[OTP-FIX] From: ${this.SENDER_ACCOUNT} -> To: ${this.RECEIVER_ACCOUNT}`);
|
||||
this.logger.log('========================================');
|
||||
|
||||
try {
|
||||
// 检查订单状态
|
||||
const order = await this.prisma.withdrawalOrder.findUnique({
|
||||
where: { orderNo: this.ORDER_NO },
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
this.logger.log('[OTP-FIX] Order not found, skipping (may be wrong environment)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已经确认,说明已修复过(幂等性)
|
||||
if (order.status === WithdrawalStatus.CONFIRMED) {
|
||||
this.logger.log('[OTP-FIX] Order already CONFIRMED, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
if (order.status !== WithdrawalStatus.FROZEN) {
|
||||
this.logger.warn(`[OTP-FIX] Unexpected order status: ${order.status}, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行修复
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
// 1. 获取发送方钱包
|
||||
const senderWallet = await tx.walletAccount.findUnique({
|
||||
where: { accountSequence: this.SENDER_ACCOUNT },
|
||||
});
|
||||
|
||||
if (!senderWallet) {
|
||||
throw new Error(`Sender wallet not found: ${this.SENDER_ACCOUNT}`);
|
||||
}
|
||||
|
||||
// 2. 获取接收方钱包
|
||||
const receiverWallet = await tx.walletAccount.findUnique({
|
||||
where: { accountSequence: this.RECEIVER_ACCOUNT },
|
||||
});
|
||||
|
||||
if (!receiverWallet) {
|
||||
throw new Error(`Receiver wallet not found: ${this.RECEIVER_ACCOUNT}`);
|
||||
}
|
||||
|
||||
const amount = new Decimal(order.amount.toString());
|
||||
const fee = new Decimal(order.fee.toString());
|
||||
const totalAmount = amount.add(fee);
|
||||
|
||||
this.logger.log(`[OTP-FIX] Amount: ${amount}, Fee: ${fee}, Total: ${totalAmount}`);
|
||||
|
||||
// 3. 修复发送方冻结余额 (加上缺失的手续费) 并同时扣除
|
||||
const senderCurrentFrozen = new Decimal(senderWallet.usdtFrozen.toString());
|
||||
// 加上缺失的手续费后再扣除全部
|
||||
const senderNewFrozen = senderCurrentFrozen.add(this.MISSING_FEE).minus(totalAmount);
|
||||
|
||||
this.logger.log(`[OTP-FIX] Sender frozen: ${senderCurrentFrozen} + ${this.MISSING_FEE} - ${totalAmount} = ${senderNewFrozen}`);
|
||||
|
||||
await tx.walletAccount.update({
|
||||
where: { id: senderWallet.id },
|
||||
data: {
|
||||
usdtFrozen: senderNewFrozen,
|
||||
version: senderWallet.version + 1,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// 4. 增加接收方余额
|
||||
const receiverCurrentAvailable = new Decimal(receiverWallet.usdtAvailable.toString());
|
||||
const receiverNewAvailable = receiverCurrentAvailable.add(amount);
|
||||
|
||||
this.logger.log(`[OTP-FIX] Receiver available: ${receiverCurrentAvailable} + ${amount} = ${receiverNewAvailable}`);
|
||||
|
||||
await tx.walletAccount.update({
|
||||
where: { id: receiverWallet.id },
|
||||
data: {
|
||||
usdtAvailable: receiverNewAvailable,
|
||||
version: receiverWallet.version + 1,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// 5. 创建发送方流水 (TRANSFER_OUT)
|
||||
await tx.ledgerEntry.create({
|
||||
data: {
|
||||
accountSequence: order.accountSequence,
|
||||
userId: order.userId,
|
||||
entryType: LedgerEntryType.TRANSFER_OUT,
|
||||
amount: amount.negated(),
|
||||
assetType: 'USDT',
|
||||
balanceAfter: senderWallet.usdtAvailable,
|
||||
refOrderId: order.orderNo,
|
||||
refTxHash: 'OTP-FIX-INTERNAL',
|
||||
memo: `转账至 ${order.toAccountSequence} (OTP修复)`,
|
||||
payloadJson: {
|
||||
toAccountSequence: order.toAccountSequence,
|
||||
toUserId: order.toUserId?.toString(),
|
||||
fee: order.fee.toString(),
|
||||
fixNote: 'One-time fix for concurrent modification issue',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 6. 创建接收方流水 (TRANSFER_IN)
|
||||
await tx.ledgerEntry.create({
|
||||
data: {
|
||||
accountSequence: order.toAccountSequence!,
|
||||
userId: order.toUserId!,
|
||||
entryType: LedgerEntryType.TRANSFER_IN,
|
||||
amount: amount,
|
||||
assetType: 'USDT',
|
||||
balanceAfter: receiverNewAvailable,
|
||||
refOrderId: order.orderNo,
|
||||
refTxHash: 'OTP-FIX-INTERNAL',
|
||||
memo: `来自 ${order.accountSequence} 的转账 (OTP修复)`,
|
||||
payloadJson: {
|
||||
fromAccountSequence: order.accountSequence,
|
||||
fromUserId: order.userId.toString(),
|
||||
fixNote: 'One-time fix for concurrent modification issue',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 7. 更新订单状态为 CONFIRMED
|
||||
await tx.withdrawalOrder.update({
|
||||
where: { id: order.id },
|
||||
data: {
|
||||
status: WithdrawalStatus.CONFIRMED,
|
||||
txHash: 'OTP-FIX-INTERNAL',
|
||||
broadcastedAt: new Date(),
|
||||
confirmedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log('[OTP-FIX] Transaction completed successfully');
|
||||
});
|
||||
|
||||
this.logger.log('========================================');
|
||||
this.logger.log('[OTP-FIX] Transfer fix completed!');
|
||||
this.logger.log(`[OTP-FIX] ${this.SENDER_ACCOUNT} -> ${this.RECEIVER_ACCOUNT}: ${this.prisma}`);
|
||||
this.logger.log('[OTP-FIX] Order status: CONFIRMED');
|
||||
this.logger.log('[OTP-FIX] Please remove this file after deployment');
|
||||
this.logger.log('========================================');
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('[OTP-FIX] Failed to execute fix', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue