feat: 实现ID-to-ID内部转账功能
- 添加内部转账标识字段:is_internal_transfer, to_account_sequence, to_user_id - 提现时自动检测目标地址是否为内部用户 - 内部转账确认后创建双向流水:发送方TRANSFER_OUT,接收方TRANSFER_IN - 新增identity-service钱包地址查询API支持内部用户识别 🤖 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
3f5203c142
commit
f9222fed50
|
|
@ -572,6 +572,30 @@ export class UserAccountController {
|
||||||
return { address };
|
return { address };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('internal/users/by-wallet-address')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '通过钱包地址查询用户信息(内部调用)',
|
||||||
|
description: '通过区块链钱包地址查询用户的 accountSequence 和 userId',
|
||||||
|
})
|
||||||
|
@ApiQuery({ name: 'chainType', required: true, description: '链类型 (KAVA/BSC)' })
|
||||||
|
@ApiQuery({ name: 'address', required: true, description: '钱包地址' })
|
||||||
|
@ApiResponse({ status: 200, description: '返回用户信息' })
|
||||||
|
@ApiResponse({ status: 404, description: '找不到用户' })
|
||||||
|
async getUserByWalletAddress(
|
||||||
|
@Query('chainType') chainType: string,
|
||||||
|
@Query('address') address: string,
|
||||||
|
) {
|
||||||
|
const result = await this.userService.findUserByWalletAddress(chainType, address);
|
||||||
|
if (!result) {
|
||||||
|
return { found: false, accountSequence: null, userId: null };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
found: true,
|
||||||
|
accountSequence: result.accountSequence,
|
||||||
|
userId: result.userId.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Post('upload-avatar')
|
@Post('upload-avatar')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOperation({ summary: '上传用户头像' })
|
@ApiOperation({ summary: '上传用户头像' })
|
||||||
|
|
|
||||||
|
|
@ -2086,6 +2086,57 @@ export class UserApplicationService {
|
||||||
return walletAddress.address;
|
return walletAddress.address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过钱包地址查询用户信息
|
||||||
|
* 用于内部转账时判断目标地址是否属于系统内用户
|
||||||
|
*/
|
||||||
|
async findUserByWalletAddress(
|
||||||
|
chainType: string,
|
||||||
|
address: string,
|
||||||
|
): Promise<{ accountSequence: string; userId: bigint } | null> {
|
||||||
|
this.logger.log(
|
||||||
|
`Finding user by wallet address: ${chainType} ${address}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 查询钱包地址
|
||||||
|
const walletAddress = await this.prisma.walletAddress.findFirst({
|
||||||
|
where: {
|
||||||
|
chainType: chainType.toUpperCase(),
|
||||||
|
address: address.toLowerCase(),
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!walletAddress) {
|
||||||
|
this.logger.debug(`No user found for wallet address: ${address}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询用户的 accountSequence
|
||||||
|
const user = await this.prisma.userAccount.findUnique({
|
||||||
|
where: { userId: walletAddress.userId },
|
||||||
|
select: {
|
||||||
|
accountSequence: true,
|
||||||
|
userId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
this.logger.warn(`User not found for userId: ${walletAddress.userId}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Found user ${user.accountSequence} for wallet address: ${address}`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
accountSequence: user.accountSequence,
|
||||||
|
userId: user.userId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证用户登录密码
|
* 验证用户登录密码
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
-- 添加内部转账标识字段
|
||||||
|
-- 用于区分 ID 转 ID 的内部转账和其他类型的提现
|
||||||
|
|
||||||
|
-- 是否为内部转账
|
||||||
|
ALTER TABLE "withdrawal_orders" ADD COLUMN "is_internal_transfer" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
-- 接收方 accountSequence(内部转账时有值)
|
||||||
|
ALTER TABLE "withdrawal_orders" ADD COLUMN "to_account_sequence" VARCHAR(20);
|
||||||
|
|
||||||
|
-- 接收方 userId(内部转账时有值)
|
||||||
|
ALTER TABLE "withdrawal_orders" ADD COLUMN "to_user_id" BIGINT;
|
||||||
|
|
@ -187,6 +187,11 @@ model WithdrawalOrder {
|
||||||
// 交易信息
|
// 交易信息
|
||||||
txHash String? @map("tx_hash") @db.VarChar(100) // 链上交易哈希
|
txHash String? @map("tx_hash") @db.VarChar(100) // 链上交易哈希
|
||||||
|
|
||||||
|
// 内部转账标识
|
||||||
|
isInternalTransfer Boolean @default(false) @map("is_internal_transfer") // 是否为内部转账(ID转ID)
|
||||||
|
toAccountSequence String? @map("to_account_sequence") @db.VarChar(20) // 接收方ID(内部转账时有值)
|
||||||
|
toUserId BigInt? @map("to_user_id") // 接收方用户ID(内部转账时有值)
|
||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
status String @default("PENDING") @map("status") @db.VarChar(20)
|
status String @default("PENDING") @map("status") @db.VarChar(20)
|
||||||
errorMessage String? @map("error_message") @db.VarChar(500)
|
errorMessage String? @map("error_message") @db.VarChar(500)
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ import {
|
||||||
WITHDRAWAL_ORDER_REPOSITORY,
|
WITHDRAWAL_ORDER_REPOSITORY,
|
||||||
IWalletAccountRepository,
|
IWalletAccountRepository,
|
||||||
WALLET_ACCOUNT_REPOSITORY,
|
WALLET_ACCOUNT_REPOSITORY,
|
||||||
|
ILedgerEntryRepository,
|
||||||
|
LEDGER_ENTRY_REPOSITORY,
|
||||||
} from '@/domain/repositories';
|
} from '@/domain/repositories';
|
||||||
import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service';
|
import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service';
|
||||||
import { WithdrawalOrder, WalletAccount } from '@/domain/aggregates';
|
import { WithdrawalOrder, WalletAccount, LedgerEntry } from '@/domain/aggregates';
|
||||||
import { WithdrawalStatus, Money, UserId } from '@/domain/value-objects';
|
import { WithdrawalStatus, Money, UserId, LedgerEntryType } from '@/domain/value-objects';
|
||||||
import { OptimisticLockError } from '@/shared/exceptions/domain.exception';
|
import { OptimisticLockError } from '@/shared/exceptions/domain.exception';
|
||||||
import Decimal from 'decimal.js';
|
import Decimal from 'decimal.js';
|
||||||
|
|
||||||
|
|
@ -39,6 +41,8 @@ export class WithdrawalStatusHandler implements OnModuleInit {
|
||||||
private readonly withdrawalRepo: IWithdrawalOrderRepository,
|
private readonly withdrawalRepo: IWithdrawalOrderRepository,
|
||||||
@Inject(WALLET_ACCOUNT_REPOSITORY)
|
@Inject(WALLET_ACCOUNT_REPOSITORY)
|
||||||
private readonly walletRepo: IWalletAccountRepository,
|
private readonly walletRepo: IWalletAccountRepository,
|
||||||
|
@Inject(LEDGER_ENTRY_REPOSITORY)
|
||||||
|
private readonly ledgerRepo: ILedgerEntryRepository,
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|
@ -184,6 +188,110 @@ export class WithdrawalStatusHandler implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`[CONFIRMED] Deducted ${totalAmount.toString()} USDT from frozen balance for ${orderRecord.accountSequence} (version: ${currentVersion} -> ${currentVersion + 1})`);
|
this.logger.log(`[CONFIRMED] Deducted ${totalAmount.toString()} USDT from frozen balance for ${orderRecord.accountSequence} (version: ${currentVersion} -> ${currentVersion + 1})`);
|
||||||
|
|
||||||
|
// 记录流水:根据是否内部转账决定流水类型
|
||||||
|
if (orderRecord.isInternalTransfer && orderRecord.toAccountSequence) {
|
||||||
|
// 内部转账:给转出方记录 TRANSFER_OUT
|
||||||
|
await tx.ledgerEntry.create({
|
||||||
|
data: {
|
||||||
|
accountSequence: orderRecord.accountSequence,
|
||||||
|
userId: orderRecord.userId,
|
||||||
|
entryType: LedgerEntryType.TRANSFER_OUT,
|
||||||
|
amount: new Decimal(orderRecord.amount.toString()).negated(),
|
||||||
|
assetType: 'USDT',
|
||||||
|
balanceAfter: walletRecord.usdtAvailable, // 冻结余额扣除后可用余额不变
|
||||||
|
refOrderId: orderRecord.orderNo,
|
||||||
|
refTxHash: payload.txHash,
|
||||||
|
memo: `转账至 ${orderRecord.toAccountSequence}`,
|
||||||
|
payloadJson: {
|
||||||
|
toAccountSequence: orderRecord.toAccountSequence,
|
||||||
|
toUserId: orderRecord.toUserId?.toString(),
|
||||||
|
fee: orderRecord.fee.toString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 内部转账:给接收方记录 TRANSFER_IN 并增加余额
|
||||||
|
if (orderRecord.toUserId) {
|
||||||
|
// 查找接收方钱包
|
||||||
|
let toWalletRecord = await tx.walletAccount.findUnique({
|
||||||
|
where: { accountSequence: orderRecord.toAccountSequence },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!toWalletRecord) {
|
||||||
|
toWalletRecord = await tx.walletAccount.findUnique({
|
||||||
|
where: { userId: orderRecord.toUserId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toWalletRecord) {
|
||||||
|
const transferAmount = new Decimal(orderRecord.amount.toString());
|
||||||
|
const toCurrentAvailable = new Decimal(toWalletRecord.usdtAvailable.toString());
|
||||||
|
const toNewAvailable = toCurrentAvailable.add(transferAmount);
|
||||||
|
const toCurrentVersion = toWalletRecord.version;
|
||||||
|
|
||||||
|
// 更新接收方余额
|
||||||
|
const toUpdateResult = await tx.walletAccount.updateMany({
|
||||||
|
where: {
|
||||||
|
id: toWalletRecord.id,
|
||||||
|
version: toCurrentVersion,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
usdtAvailable: toNewAvailable,
|
||||||
|
version: toCurrentVersion + 1,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (toUpdateResult.count === 0) {
|
||||||
|
throw new OptimisticLockError(`Optimistic lock conflict for receiver wallet ${toWalletRecord.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 给接收方记录 TRANSFER_IN 流水
|
||||||
|
await tx.ledgerEntry.create({
|
||||||
|
data: {
|
||||||
|
accountSequence: orderRecord.toAccountSequence,
|
||||||
|
userId: orderRecord.toUserId,
|
||||||
|
entryType: LedgerEntryType.TRANSFER_IN,
|
||||||
|
amount: transferAmount,
|
||||||
|
assetType: 'USDT',
|
||||||
|
balanceAfter: toNewAvailable,
|
||||||
|
refOrderId: orderRecord.orderNo,
|
||||||
|
refTxHash: payload.txHash,
|
||||||
|
memo: `来自 ${orderRecord.accountSequence} 的转账`,
|
||||||
|
payloadJson: {
|
||||||
|
fromAccountSequence: orderRecord.accountSequence,
|
||||||
|
fromUserId: orderRecord.userId.toString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`[CONFIRMED] Internal transfer: ${orderRecord.accountSequence} -> ${orderRecord.toAccountSequence}, amount: ${transferAmount.toString()}`);
|
||||||
|
} else {
|
||||||
|
this.logger.error(`[CONFIRMED] Receiver wallet not found: ${orderRecord.toAccountSequence}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 普通提现:记录 WITHDRAWAL
|
||||||
|
await tx.ledgerEntry.create({
|
||||||
|
data: {
|
||||||
|
accountSequence: orderRecord.accountSequence,
|
||||||
|
userId: orderRecord.userId,
|
||||||
|
entryType: LedgerEntryType.WITHDRAWAL,
|
||||||
|
amount: new Decimal(orderRecord.amount.toString()).negated(),
|
||||||
|
assetType: 'USDT',
|
||||||
|
balanceAfter: walletRecord.usdtAvailable,
|
||||||
|
refOrderId: orderRecord.orderNo,
|
||||||
|
refTxHash: payload.txHash,
|
||||||
|
memo: `提现至 ${orderRecord.toAddress}`,
|
||||||
|
payloadJson: {
|
||||||
|
toAddress: orderRecord.toAddress,
|
||||||
|
chainType: orderRecord.chainType,
|
||||||
|
fee: orderRecord.fee.toString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.logger.error(`[CONFIRMED] Wallet not found for accountSequence: ${orderRecord.accountSequence}, userId: ${orderRecord.userId}`);
|
this.logger.error(`[CONFIRMED] Wallet not found for accountSequence: ${orderRecord.accountSequence}, userId: ${orderRecord.userId}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { EventPublisherService } from '@/infrastructure/kafka';
|
||||||
import { WithdrawalRequestedEvent } from '@/domain/events';
|
import { WithdrawalRequestedEvent } from '@/domain/events';
|
||||||
import { FeeConfigRepositoryImpl } from '@/infrastructure/persistence/repositories';
|
import { FeeConfigRepositoryImpl } from '@/infrastructure/persistence/repositories';
|
||||||
import { FeeType } from '@/api/dto/response';
|
import { FeeType } from '@/api/dto/response';
|
||||||
|
import { IdentityClientService } from '@/infrastructure/external/identity/identity-client.service';
|
||||||
|
|
||||||
export interface WalletDTO {
|
export interface WalletDTO {
|
||||||
walletId: string;
|
walletId: string;
|
||||||
|
|
@ -92,6 +93,7 @@ export class WalletApplicationService {
|
||||||
private readonly eventPublisher: EventPublisherService,
|
private readonly eventPublisher: EventPublisherService,
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly feeConfigRepo: FeeConfigRepositoryImpl,
|
private readonly feeConfigRepo: FeeConfigRepositoryImpl,
|
||||||
|
private readonly identityClient: IdentityClientService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// =============== Commands ===============
|
// =============== Commands ===============
|
||||||
|
|
@ -1341,6 +1343,26 @@ export class WalletApplicationService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查目标地址是否为系统内用户(内部转账)
|
||||||
|
let isInternalTransfer = false;
|
||||||
|
let toAccountSequence: string | undefined;
|
||||||
|
let toUserId: UserId | undefined;
|
||||||
|
|
||||||
|
const targetUser = await this.identityClient.findUserByWalletAddress(
|
||||||
|
command.chainType,
|
||||||
|
command.toAddress,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (targetUser) {
|
||||||
|
// 目标地址属于系统内用户,标记为内部转账
|
||||||
|
isInternalTransfer = true;
|
||||||
|
toAccountSequence = targetUser.accountSequence;
|
||||||
|
toUserId = UserId.create(BigInt(targetUser.userId));
|
||||||
|
this.logger.log(
|
||||||
|
`Internal transfer detected: ${wallet.accountSequence} -> ${toAccountSequence}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 创建提现订单
|
// 创建提现订单
|
||||||
const withdrawalOrder = WithdrawalOrder.create({
|
const withdrawalOrder = WithdrawalOrder.create({
|
||||||
accountSequence: wallet.accountSequence,
|
accountSequence: wallet.accountSequence,
|
||||||
|
|
@ -1349,6 +1371,9 @@ export class WalletApplicationService {
|
||||||
fee,
|
fee,
|
||||||
chainType: command.chainType,
|
chainType: command.chainType,
|
||||||
toAddress: command.toAddress,
|
toAddress: command.toAddress,
|
||||||
|
isInternalTransfer,
|
||||||
|
toAccountSequence,
|
||||||
|
toUserId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 冻结用户余额 (金额 + 手续费)
|
// 冻结用户余额 (金额 + 手续费)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,10 @@ export class WithdrawalOrder {
|
||||||
private readonly _chainType: ChainType;
|
private readonly _chainType: ChainType;
|
||||||
private readonly _toAddress: string; // 提现目标地址
|
private readonly _toAddress: string; // 提现目标地址
|
||||||
private _txHash: string | null;
|
private _txHash: string | null;
|
||||||
|
// 内部转账标识
|
||||||
|
private readonly _isInternalTransfer: boolean; // 是否为内部转账(ID转ID)
|
||||||
|
private readonly _toAccountSequence: string | null; // 接收方ID(内部转账时有值)
|
||||||
|
private readonly _toUserId: UserId | null; // 接收方用户ID(内部转账时有值)
|
||||||
private _status: WithdrawalStatus;
|
private _status: WithdrawalStatus;
|
||||||
private _errorMessage: string | null;
|
private _errorMessage: string | null;
|
||||||
private _frozenAt: Date | null;
|
private _frozenAt: Date | null;
|
||||||
|
|
@ -41,6 +45,9 @@ export class WithdrawalOrder {
|
||||||
chainType: ChainType,
|
chainType: ChainType,
|
||||||
toAddress: string,
|
toAddress: string,
|
||||||
txHash: string | null,
|
txHash: string | null,
|
||||||
|
isInternalTransfer: boolean,
|
||||||
|
toAccountSequence: string | null,
|
||||||
|
toUserId: UserId | null,
|
||||||
status: WithdrawalStatus,
|
status: WithdrawalStatus,
|
||||||
errorMessage: string | null,
|
errorMessage: string | null,
|
||||||
frozenAt: Date | null,
|
frozenAt: Date | null,
|
||||||
|
|
@ -57,6 +64,9 @@ export class WithdrawalOrder {
|
||||||
this._chainType = chainType;
|
this._chainType = chainType;
|
||||||
this._toAddress = toAddress;
|
this._toAddress = toAddress;
|
||||||
this._txHash = txHash;
|
this._txHash = txHash;
|
||||||
|
this._isInternalTransfer = isInternalTransfer;
|
||||||
|
this._toAccountSequence = toAccountSequence;
|
||||||
|
this._toUserId = toUserId;
|
||||||
this._status = status;
|
this._status = status;
|
||||||
this._errorMessage = errorMessage;
|
this._errorMessage = errorMessage;
|
||||||
this._frozenAt = frozenAt;
|
this._frozenAt = frozenAt;
|
||||||
|
|
@ -76,6 +86,9 @@ export class WithdrawalOrder {
|
||||||
get chainType(): ChainType { return this._chainType; }
|
get chainType(): ChainType { return this._chainType; }
|
||||||
get toAddress(): string { return this._toAddress; }
|
get toAddress(): string { return this._toAddress; }
|
||||||
get txHash(): string | null { return this._txHash; }
|
get txHash(): string | null { return this._txHash; }
|
||||||
|
get isInternalTransfer(): boolean { return this._isInternalTransfer; }
|
||||||
|
get toAccountSequence(): string | null { return this._toAccountSequence; }
|
||||||
|
get toUserId(): UserId | null { return this._toUserId; }
|
||||||
get status(): WithdrawalStatus { return this._status; }
|
get status(): WithdrawalStatus { return this._status; }
|
||||||
get errorMessage(): string | null { return this._errorMessage; }
|
get errorMessage(): string | null { return this._errorMessage; }
|
||||||
get frozenAt(): Date | null { return this._frozenAt; }
|
get frozenAt(): Date | null { return this._frozenAt; }
|
||||||
|
|
@ -114,6 +127,9 @@ export class WithdrawalOrder {
|
||||||
fee: Money;
|
fee: Money;
|
||||||
chainType: ChainType;
|
chainType: ChainType;
|
||||||
toAddress: string;
|
toAddress: string;
|
||||||
|
isInternalTransfer?: boolean;
|
||||||
|
toAccountSequence?: string;
|
||||||
|
toUserId?: UserId;
|
||||||
}): WithdrawalOrder {
|
}): WithdrawalOrder {
|
||||||
// 验证金额
|
// 验证金额
|
||||||
if (params.amount.value <= 0) {
|
if (params.amount.value <= 0) {
|
||||||
|
|
@ -145,6 +161,9 @@ export class WithdrawalOrder {
|
||||||
params.chainType,
|
params.chainType,
|
||||||
params.toAddress,
|
params.toAddress,
|
||||||
null,
|
null,
|
||||||
|
params.isInternalTransfer ?? false,
|
||||||
|
params.toAccountSequence ?? null,
|
||||||
|
params.toUserId ?? null,
|
||||||
WithdrawalStatus.PENDING,
|
WithdrawalStatus.PENDING,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
|
@ -167,6 +186,9 @@ export class WithdrawalOrder {
|
||||||
chainType: string;
|
chainType: string;
|
||||||
toAddress: string;
|
toAddress: string;
|
||||||
txHash: string | null;
|
txHash: string | null;
|
||||||
|
isInternalTransfer: boolean;
|
||||||
|
toAccountSequence: string | null;
|
||||||
|
toUserId: bigint | null;
|
||||||
status: string;
|
status: string;
|
||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
frozenAt: Date | null;
|
frozenAt: Date | null;
|
||||||
|
|
@ -184,6 +206,9 @@ export class WithdrawalOrder {
|
||||||
params.chainType as ChainType,
|
params.chainType as ChainType,
|
||||||
params.toAddress,
|
params.toAddress,
|
||||||
params.txHash,
|
params.txHash,
|
||||||
|
params.isInternalTransfer,
|
||||||
|
params.toAccountSequence,
|
||||||
|
params.toUserId ? UserId.create(params.toUserId) : null,
|
||||||
params.status as WithdrawalStatus,
|
params.status as WithdrawalStatus,
|
||||||
params.errorMessage,
|
params.errorMessage,
|
||||||
params.frozenAt,
|
params.frozenAt,
|
||||||
|
|
|
||||||
|
|
@ -260,4 +260,47 @@ export class IdentityClientService {
|
||||||
throw new HttpException('无法解析充值ID', HttpStatus.SERVICE_UNAVAILABLE);
|
throw new HttpException('无法解析充值ID', HttpStatus.SERVICE_UNAVAILABLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过钱包地址查询用户信息(内部调用,无需认证)
|
||||||
|
*
|
||||||
|
* @param chainType 链类型 (KAVA, BSC)
|
||||||
|
* @param address 钱包地址
|
||||||
|
* @returns 用户信息,如果找不到则返回 null
|
||||||
|
*/
|
||||||
|
async findUserByWalletAddress(
|
||||||
|
chainType: string,
|
||||||
|
address: string,
|
||||||
|
): Promise<{ accountSequence: string; userId: string } | null> {
|
||||||
|
try {
|
||||||
|
this.logger.log(`查询钱包地址对应用户: chainType=${chainType}, address=${address}`);
|
||||||
|
|
||||||
|
const response = await this.httpClient.get(
|
||||||
|
'/user/internal/users/by-wallet-address',
|
||||||
|
{
|
||||||
|
params: { chainType, address },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// identity-service 响应格式: { success: true, data: { found: true, accountSequence: '...', userId: '...' } }
|
||||||
|
const data = response.data?.data;
|
||||||
|
if (!data?.found) {
|
||||||
|
this.logger.debug(`未找到钱包地址对应用户: ${address}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`钱包地址对应用户: ${address} -> ${data.accountSequence}`);
|
||||||
|
return {
|
||||||
|
accountSequence: data.accountSequence,
|
||||||
|
userId: data.userId,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(
|
||||||
|
`查询钱包地址对应用户失败: ${address}, error=${error.message}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 查询失败时返回 null,不影响正常流程
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ export class WithdrawalOrderRepositoryImpl implements IWithdrawalOrderRepository
|
||||||
chainType: order.chainType,
|
chainType: order.chainType,
|
||||||
toAddress: order.toAddress,
|
toAddress: order.toAddress,
|
||||||
txHash: order.txHash,
|
txHash: order.txHash,
|
||||||
|
isInternalTransfer: order.isInternalTransfer,
|
||||||
|
toAccountSequence: order.toAccountSequence,
|
||||||
|
toUserId: order.toUserId?.value ?? null,
|
||||||
status: order.status,
|
status: order.status,
|
||||||
errorMessage: order.errorMessage,
|
errorMessage: order.errorMessage,
|
||||||
frozenAt: order.frozenAt,
|
frozenAt: order.frozenAt,
|
||||||
|
|
@ -99,6 +102,9 @@ export class WithdrawalOrderRepositoryImpl implements IWithdrawalOrderRepository
|
||||||
chainType: string;
|
chainType: string;
|
||||||
toAddress: string;
|
toAddress: string;
|
||||||
txHash: string | null;
|
txHash: string | null;
|
||||||
|
isInternalTransfer: boolean;
|
||||||
|
toAccountSequence: string | null;
|
||||||
|
toUserId: bigint | null;
|
||||||
status: string;
|
status: string;
|
||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
frozenAt: Date | null;
|
frozenAt: Date | null;
|
||||||
|
|
@ -116,6 +122,9 @@ export class WithdrawalOrderRepositoryImpl implements IWithdrawalOrderRepository
|
||||||
chainType: record.chainType,
|
chainType: record.chainType,
|
||||||
toAddress: record.toAddress,
|
toAddress: record.toAddress,
|
||||||
txHash: record.txHash,
|
txHash: record.txHash,
|
||||||
|
isInternalTransfer: record.isInternalTransfer,
|
||||||
|
toAccountSequence: record.toAccountSequence,
|
||||||
|
toUserId: record.toUserId,
|
||||||
status: record.status,
|
status: record.status,
|
||||||
errorMessage: record.errorMessage,
|
errorMessage: record.errorMessage,
|
||||||
frozenAt: record.frozenAt,
|
frozenAt: record.frozenAt,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue