feat(deposit): add accountSequence correlation and testnet support
blockchain-service: - Add accountSequence to monitored_addresses and deposit_transactions - Support BSC/KAVA testnet via NETWORK_MODE environment variable - Add chain config service with testnet RPC endpoints - Update deposit detection with accountSequence propagation wallet-service: - Add accountSequence to wallet_accounts and ledger_entries - Fix JWT strategy to match identity-service token format - Update deposit handling with accountSequence correlation - Add repository methods for accountSequence-based queries 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
6b85401d5c
commit
3c2144ad7c
|
|
@ -20,7 +20,10 @@ model MonitoredAddress {
|
|||
chainType String @map("chain_type") @db.VarChar(20) // KAVA, BSC
|
||||
address String @db.VarChar(42) // 0x地址
|
||||
|
||||
userId BigInt @map("user_id") // 关联用户ID
|
||||
// 使用 accountSequence 作为跨服务关联标识 (全局唯一业务ID)
|
||||
accountSequence BigInt @map("account_sequence")
|
||||
// 保留 userId 用于兼容,但主要使用 accountSequence
|
||||
userId BigInt @map("user_id")
|
||||
|
||||
isActive Boolean @default(true) @map("is_active") // 是否激活监听
|
||||
|
||||
|
|
@ -30,6 +33,7 @@ model MonitoredAddress {
|
|||
deposits DepositTransaction[]
|
||||
|
||||
@@unique([chainType, address], name: "uk_chain_address")
|
||||
@@index([accountSequence], name: "idx_account_sequence")
|
||||
@@index([userId], name: "idx_user")
|
||||
@@index([chainType, isActive], name: "idx_chain_active")
|
||||
@@map("monitored_addresses")
|
||||
|
|
@ -60,9 +64,10 @@ model DepositTransaction {
|
|||
confirmations Int @default(0)
|
||||
status String @default("DETECTED") @db.VarChar(20) // DETECTED, CONFIRMING, CONFIRMED, NOTIFIED
|
||||
|
||||
// 关联
|
||||
addressId BigInt @map("address_id")
|
||||
userId BigInt @map("user_id")
|
||||
// 关联 - 使用 accountSequence 作为跨服务主键
|
||||
addressId BigInt @map("address_id")
|
||||
accountSequence BigInt @map("account_sequence") // 跨服务关联标识
|
||||
userId BigInt @map("user_id") // 保留兼容
|
||||
|
||||
// 通知状态
|
||||
notifiedAt DateTime? @map("notified_at")
|
||||
|
|
@ -75,6 +80,7 @@ model DepositTransaction {
|
|||
monitoredAddress MonitoredAddress @relation(fields: [addressId], references: [id])
|
||||
|
||||
@@index([chainType, status], name: "idx_chain_status")
|
||||
@@index([accountSequence], name: "idx_deposit_account")
|
||||
@@index([userId], name: "idx_deposit_user")
|
||||
@@index([blockNumber], name: "idx_block")
|
||||
@@index([status, notifiedAt], name: "idx_pending_notify")
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ApplicationModule } from '@/application/application.module';
|
||||
import { DomainModule } from '@/domain/domain.module';
|
||||
import { HealthController, BalanceController, InternalController } from './controllers';
|
||||
|
||||
@Module({
|
||||
imports: [ApplicationModule],
|
||||
imports: [ApplicationModule, DomainModule],
|
||||
controllers: [HealthController, BalanceController, InternalController],
|
||||
})
|
||||
export class ApiModule {}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { ChainConfigService } from '@/domain/services/chain-config.service';
|
||||
import { ChainType } from '@/domain/value-objects';
|
||||
|
||||
@ApiTags('Health')
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
constructor(private readonly chainConfig: ChainConfigService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '健康检查' })
|
||||
@ApiResponse({ status: 200, description: '服务健康' })
|
||||
|
|
@ -25,4 +29,31 @@ export class HealthController {
|
|||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('network')
|
||||
@ApiOperation({ summary: '网络配置信息' })
|
||||
@ApiResponse({ status: 200, description: '返回当前网络配置' })
|
||||
network() {
|
||||
const supportedChains = this.chainConfig.getSupportedChains();
|
||||
const chains: Record<string, unknown> = {};
|
||||
|
||||
for (const chainTypeEnum of supportedChains) {
|
||||
const chainType = ChainType.fromEnum(chainTypeEnum);
|
||||
const config = this.chainConfig.getConfig(chainType);
|
||||
chains[chainTypeEnum] = {
|
||||
chainId: config.chainId,
|
||||
rpcUrl: config.rpcUrl,
|
||||
usdtContract: config.usdtContract,
|
||||
isTestnet: config.isTestnet,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
service: 'blockchain-service',
|
||||
networkMode: this.chainConfig.getNetworkMode(),
|
||||
isTestnet: this.chainConfig.isTestnetMode(),
|
||||
chains,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export class AddressDerivationService {
|
|||
// 2. 只为 EVM 链注册监控地址 (用于充值检测)
|
||||
for (const derived of derivedAddresses) {
|
||||
if (this.evmChains.has(derived.chainType)) {
|
||||
await this.registerEvmAddressForMonitoring(userId, derived);
|
||||
await this.registerEvmAddressForMonitoring(userId, accountSequence, derived);
|
||||
} else {
|
||||
this.logger.log(`[DERIVE] Skipping monitoring registration for Cosmos chain: ${derived.chainType} - ${derived.address}`);
|
||||
}
|
||||
|
|
@ -144,17 +144,22 @@ export class AddressDerivationService {
|
|||
/**
|
||||
* 注册 EVM 地址用于充值监控
|
||||
*/
|
||||
private async registerEvmAddressForMonitoring(userId: bigint, derived: DerivedAddress): Promise<void> {
|
||||
private async registerEvmAddressForMonitoring(
|
||||
userId: bigint,
|
||||
accountSequence: number,
|
||||
derived: DerivedAddress,
|
||||
): Promise<void> {
|
||||
const chainType = ChainType.fromEnum(derived.chainType);
|
||||
const address = EvmAddress.create(derived.address);
|
||||
|
||||
// 检查是否已存在
|
||||
const exists = await this.monitoredAddressRepo.existsByChainAndAddress(chainType, address);
|
||||
if (!exists) {
|
||||
// 创建监控地址
|
||||
// 创建监控地址 - 使用 accountSequence 作为跨服务关联键
|
||||
const monitored = MonitoredAddress.create({
|
||||
chainType,
|
||||
address,
|
||||
accountSequence: BigInt(accountSequence),
|
||||
userId,
|
||||
});
|
||||
|
||||
|
|
@ -163,7 +168,7 @@ export class AddressDerivationService {
|
|||
// 添加到缓存
|
||||
await this.addressCache.addAddress(chainType, address.lowercase);
|
||||
|
||||
this.logger.log(`[MONITOR] Registered EVM address for monitoring: ${derived.chainType} - ${derived.address}`);
|
||||
this.logger.log(`[MONITOR] Registered EVM address for monitoring: ${derived.chainType} - ${derived.address} (account ${accountSequence})`);
|
||||
} else {
|
||||
this.logger.debug(`[MONITOR] Address already registered: ${derived.chainType} - ${derived.address}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ export class DepositDetectionService implements OnModuleInit {
|
|||
return;
|
||||
}
|
||||
|
||||
// 创建充值记录
|
||||
// 创建充值记录 - 使用 accountSequence 作为跨服务关联键
|
||||
const deposit = DepositTransaction.create({
|
||||
chainType,
|
||||
txHash,
|
||||
|
|
@ -148,6 +148,7 @@ export class DepositDetectionService implements OnModuleInit {
|
|||
blockTimestamp: event.blockTimestamp,
|
||||
logIndex: event.logIndex,
|
||||
addressId: monitoredAddress.id,
|
||||
accountSequence: monitoredAddress.accountSequence,
|
||||
userId: monitoredAddress.userId,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,64 @@
|
|||
import { registerAs } from '@nestjs/config';
|
||||
|
||||
export default registerAs('blockchain', () => ({
|
||||
// 通用配置
|
||||
scanIntervalMs: parseInt(process.env.BLOCK_SCAN_INTERVAL_MS || '5000', 10),
|
||||
confirmationsRequired: parseInt(process.env.BLOCK_CONFIRMATIONS_REQUIRED || '12', 10),
|
||||
scanBatchSize: parseInt(process.env.BLOCK_SCAN_BATCH_SIZE || '100', 10),
|
||||
/**
|
||||
* 区块链配置
|
||||
*
|
||||
* 支持主网和测试网切换,通过 NETWORK_MODE 环境变量控制:
|
||||
* - NETWORK_MODE=mainnet (默认): 使用主网配置
|
||||
* - NETWORK_MODE=testnet: 使用测试网配置
|
||||
*
|
||||
* 测试网配置:
|
||||
* - BSC Testnet: Chain ID 97, 水龙头: https://testnet.bnbchain.org/faucet-smart
|
||||
* - KAVA Testnet: Chain ID 2221, 水龙头: https://faucet.kava.io
|
||||
*/
|
||||
export default registerAs('blockchain', () => {
|
||||
const networkMode = process.env.NETWORK_MODE || 'mainnet';
|
||||
const isTestnet = networkMode === 'testnet';
|
||||
|
||||
// KAVA 配置
|
||||
kava: {
|
||||
rpcUrl: process.env.KAVA_RPC_URL || 'https://evm.kava.io',
|
||||
chainId: parseInt(process.env.KAVA_CHAIN_ID || '2222', 10),
|
||||
usdtContract: process.env.KAVA_USDT_CONTRACT || '0x919C1c267BC06a7039e03fcc2eF738525769109c',
|
||||
confirmations: parseInt(process.env.KAVA_CONFIRMATIONS || '12', 10),
|
||||
},
|
||||
return {
|
||||
// 网络模式
|
||||
networkMode,
|
||||
isTestnet,
|
||||
|
||||
// BSC 配置
|
||||
bsc: {
|
||||
rpcUrl: process.env.BSC_RPC_URL || 'https://bsc-dataseed.binance.org',
|
||||
chainId: parseInt(process.env.BSC_CHAIN_ID || '56', 10),
|
||||
usdtContract: process.env.BSC_USDT_CONTRACT || '0x55d398326f99059fF775485246999027B3197955',
|
||||
confirmations: parseInt(process.env.BSC_CONFIRMATIONS || '15', 10),
|
||||
},
|
||||
}));
|
||||
// 通用配置
|
||||
scanIntervalMs: parseInt(process.env.BLOCK_SCAN_INTERVAL_MS || '5000', 10),
|
||||
confirmationsRequired: parseInt(process.env.BLOCK_CONFIRMATIONS_REQUIRED || (isTestnet ? '3' : '12'), 10),
|
||||
scanBatchSize: parseInt(process.env.BLOCK_SCAN_BATCH_SIZE || '100', 10),
|
||||
|
||||
// KAVA 配置
|
||||
kava: isTestnet
|
||||
? {
|
||||
// KAVA Testnet
|
||||
rpcUrl: process.env.KAVA_RPC_URL || 'https://evm.testnet.kava.io',
|
||||
chainId: parseInt(process.env.KAVA_CHAIN_ID || '2221', 10),
|
||||
// 测试网 USDT 合约 (需要部署或使用已有的)
|
||||
usdtContract: process.env.KAVA_USDT_CONTRACT || '0x0000000000000000000000000000000000000000',
|
||||
confirmations: parseInt(process.env.KAVA_CONFIRMATIONS || '3', 10),
|
||||
}
|
||||
: {
|
||||
// KAVA Mainnet
|
||||
rpcUrl: process.env.KAVA_RPC_URL || 'https://evm.kava.io',
|
||||
chainId: parseInt(process.env.KAVA_CHAIN_ID || '2222', 10),
|
||||
usdtContract: process.env.KAVA_USDT_CONTRACT || '0x919C1c267BC06a7039e03fcc2eF738525769109c',
|
||||
confirmations: parseInt(process.env.KAVA_CONFIRMATIONS || '12', 10),
|
||||
},
|
||||
|
||||
// BSC 配置
|
||||
bsc: isTestnet
|
||||
? {
|
||||
// BSC Testnet (BNB Smart Chain Testnet)
|
||||
rpcUrl: process.env.BSC_RPC_URL || 'https://data-seed-prebsc-1-s1.binance.org:8545',
|
||||
chainId: parseInt(process.env.BSC_CHAIN_ID || '97', 10),
|
||||
// BSC Testnet 官方测试 USDT 合约
|
||||
usdtContract: process.env.BSC_USDT_CONTRACT || '0x337610d27c682E347C9cD60BD4b3b107C9d34dDd',
|
||||
confirmations: parseInt(process.env.BSC_CONFIRMATIONS || '3', 10),
|
||||
}
|
||||
: {
|
||||
// BSC Mainnet
|
||||
rpcUrl: process.env.BSC_RPC_URL || 'https://bsc-dataseed.binance.org',
|
||||
chainId: parseInt(process.env.BSC_CHAIN_ID || '56', 10),
|
||||
usdtContract: process.env.BSC_USDT_CONTRACT || '0x55d398326f99059fF775485246999027B3197955',
|
||||
confirmations: parseInt(process.env.BSC_CONFIRMATIONS || '15', 10),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ export interface DepositTransactionProps {
|
|||
confirmations: number;
|
||||
status: DepositStatus;
|
||||
addressId: bigint;
|
||||
userId: bigint;
|
||||
accountSequence: bigint; // 跨服务关联标识
|
||||
userId: bigint; // 保留兼容
|
||||
notifiedAt?: Date;
|
||||
notifyAttempts: number;
|
||||
lastNotifyError?: string;
|
||||
|
|
@ -73,6 +74,9 @@ export class DepositTransaction extends AggregateRoot<bigint> {
|
|||
get addressId(): bigint {
|
||||
return this.props.addressId;
|
||||
}
|
||||
get accountSequence(): bigint {
|
||||
return this.props.accountSequence;
|
||||
}
|
||||
get userId(): bigint {
|
||||
return this.props.userId;
|
||||
}
|
||||
|
|
@ -113,6 +117,7 @@ export class DepositTransaction extends AggregateRoot<bigint> {
|
|||
blockTimestamp: Date;
|
||||
logIndex: number;
|
||||
addressId: bigint;
|
||||
accountSequence: bigint;
|
||||
userId: bigint;
|
||||
}): DepositTransaction {
|
||||
const deposit = new DepositTransaction({
|
||||
|
|
@ -134,6 +139,7 @@ export class DepositTransaction extends AggregateRoot<bigint> {
|
|||
amountFormatted: params.amount.toFixed(8),
|
||||
blockNumber: params.blockNumber.toString(),
|
||||
blockTimestamp: params.blockTimestamp.toISOString(),
|
||||
accountSequence: params.accountSequence.toString(),
|
||||
userId: params.userId.toString(),
|
||||
}),
|
||||
);
|
||||
|
|
@ -180,6 +186,7 @@ export class DepositTransaction extends AggregateRoot<bigint> {
|
|||
amount: this.props.amount.raw.toString(),
|
||||
amountFormatted: this.props.amount.toFixed(8),
|
||||
confirmations: this.props.confirmations,
|
||||
accountSequence: this.props.accountSequence.toString(),
|
||||
userId: this.props.userId.toString(),
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ export interface MonitoredAddressProps {
|
|||
id?: bigint;
|
||||
chainType: ChainType;
|
||||
address: EvmAddress;
|
||||
userId: bigint;
|
||||
accountSequence: bigint; // 跨服务关联标识 (全局唯一业务ID)
|
||||
userId: bigint; // 保留兼容
|
||||
isActive: boolean;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
|
|
@ -29,6 +30,9 @@ export class MonitoredAddress extends AggregateRoot<bigint> {
|
|||
get address(): EvmAddress {
|
||||
return this.props.address;
|
||||
}
|
||||
get accountSequence(): bigint {
|
||||
return this.props.accountSequence;
|
||||
}
|
||||
get userId(): bigint {
|
||||
return this.props.userId;
|
||||
}
|
||||
|
|
@ -48,6 +52,7 @@ export class MonitoredAddress extends AggregateRoot<bigint> {
|
|||
static create(params: {
|
||||
chainType: ChainType;
|
||||
address: EvmAddress;
|
||||
accountSequence: bigint;
|
||||
userId: bigint;
|
||||
}): MonitoredAddress {
|
||||
return new MonitoredAddress({
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ export interface DepositConfirmedPayload {
|
|||
amount: string;
|
||||
amountFormatted: string;
|
||||
confirmations: number;
|
||||
userId: string;
|
||||
accountSequence: string; // 跨服务关联标识 (全局唯一业务ID)
|
||||
userId: string; // 保留兼容
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ export interface DepositDetectedPayload {
|
|||
amountFormatted: string;
|
||||
blockNumber: string;
|
||||
blockTimestamp: string;
|
||||
userId: string;
|
||||
accountSequence: string; // 跨服务关联标识 (全局唯一业务ID)
|
||||
userId: string; // 保留兼容
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ChainType } from '@/domain/value-objects';
|
||||
import { ChainTypeEnum } from '@/domain/enums';
|
||||
|
|
@ -10,49 +10,79 @@ export interface ChainConfig {
|
|||
usdtContract: string;
|
||||
nativeSymbol: string;
|
||||
blockTime: number; // 平均出块时间(秒)
|
||||
isTestnet: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 链配置服务
|
||||
*
|
||||
* 支持主网/测试网切换,通过 NETWORK_MODE 环境变量控制
|
||||
*/
|
||||
@Injectable()
|
||||
export class ChainConfigService {
|
||||
private readonly logger = new Logger(ChainConfigService.name);
|
||||
private readonly configs: Map<ChainTypeEnum, ChainConfig>;
|
||||
private readonly isTestnet: boolean;
|
||||
private readonly networkMode: string;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.configs = new Map();
|
||||
this.networkMode = this.configService.get<string>('blockchain.networkMode', 'mainnet');
|
||||
this.isTestnet = this.networkMode === 'testnet';
|
||||
this.initializeConfigs();
|
||||
this.logger.log(`[INIT] Network mode: ${this.networkMode} (testnet=${this.isTestnet})`);
|
||||
}
|
||||
|
||||
private initializeConfigs(): void {
|
||||
// KAVA 配置
|
||||
this.configs.set(ChainTypeEnum.KAVA, {
|
||||
chainType: ChainTypeEnum.KAVA,
|
||||
chainId: this.configService.get<number>('blockchain.kava.chainId', 2222),
|
||||
rpcUrl: this.configService.get<string>('blockchain.kava.rpcUrl', 'https://evm.kava.io'),
|
||||
chainId: this.configService.get<number>('blockchain.kava.chainId', this.isTestnet ? 2221 : 2222),
|
||||
rpcUrl: this.configService.get<string>(
|
||||
'blockchain.kava.rpcUrl',
|
||||
this.isTestnet ? 'https://evm.testnet.kava.io' : 'https://evm.kava.io',
|
||||
),
|
||||
usdtContract: this.configService.get<string>(
|
||||
'blockchain.kava.usdtContract',
|
||||
'0x919C1c267BC06a7039e03fcc2eF738525769109c',
|
||||
this.isTestnet ? '0x0000000000000000000000000000000000000000' : '0x919C1c267BC06a7039e03fcc2eF738525769109c',
|
||||
),
|
||||
nativeSymbol: 'KAVA',
|
||||
blockTime: 6,
|
||||
isTestnet: this.isTestnet,
|
||||
});
|
||||
this.logger.log(`[INIT] KAVA: chainId=${this.configs.get(ChainTypeEnum.KAVA)?.chainId}, rpc=${this.configs.get(ChainTypeEnum.KAVA)?.rpcUrl}`);
|
||||
|
||||
// BSC 配置
|
||||
this.configs.set(ChainTypeEnum.BSC, {
|
||||
chainType: ChainTypeEnum.BSC,
|
||||
chainId: this.configService.get<number>('blockchain.bsc.chainId', 56),
|
||||
chainId: this.configService.get<number>('blockchain.bsc.chainId', this.isTestnet ? 97 : 56),
|
||||
rpcUrl: this.configService.get<string>(
|
||||
'blockchain.bsc.rpcUrl',
|
||||
'https://bsc-dataseed.binance.org',
|
||||
this.isTestnet ? 'https://data-seed-prebsc-1-s1.binance.org:8545' : 'https://bsc-dataseed.binance.org',
|
||||
),
|
||||
usdtContract: this.configService.get<string>(
|
||||
'blockchain.bsc.usdtContract',
|
||||
'0x55d398326f99059fF775485246999027B3197955',
|
||||
this.isTestnet ? '0x337610d27c682E347C9cD60BD4b3b107C9d34dDd' : '0x55d398326f99059fF775485246999027B3197955',
|
||||
),
|
||||
nativeSymbol: 'BNB',
|
||||
blockTime: 3,
|
||||
isTestnet: this.isTestnet,
|
||||
});
|
||||
this.logger.log(`[INIT] BSC: chainId=${this.configs.get(ChainTypeEnum.BSC)?.chainId}, rpc=${this.configs.get(ChainTypeEnum.BSC)?.rpcUrl}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前网络模式
|
||||
*/
|
||||
getNetworkMode(): string {
|
||||
return this.networkMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为测试网
|
||||
*/
|
||||
isTestnetMode(): boolean {
|
||||
return this.isTestnet;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export class DepositTransactionMapper {
|
|||
confirmations: prisma.confirmations,
|
||||
status: prisma.status as DepositStatus,
|
||||
addressId: prisma.addressId,
|
||||
accountSequence: prisma.accountSequence,
|
||||
userId: prisma.userId,
|
||||
notifiedAt: prisma.notifiedAt ?? undefined,
|
||||
notifyAttempts: prisma.notifyAttempts,
|
||||
|
|
@ -51,6 +52,7 @@ export class DepositTransactionMapper {
|
|||
confirmations: domain.confirmations,
|
||||
status: domain.status,
|
||||
addressId: domain.addressId,
|
||||
accountSequence: domain.accountSequence,
|
||||
userId: domain.userId,
|
||||
notifiedAt: domain.notifiedAt ?? null,
|
||||
notifyAttempts: domain.notifyAttempts,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export class MonitoredAddressMapper {
|
|||
id: prisma.id,
|
||||
chainType: ChainType.create(prisma.chainType),
|
||||
address: EvmAddress.fromUnchecked(prisma.address),
|
||||
accountSequence: prisma.accountSequence,
|
||||
userId: prisma.userId,
|
||||
isActive: prisma.isActive,
|
||||
createdAt: prisma.createdAt,
|
||||
|
|
@ -24,6 +25,7 @@ export class MonitoredAddressMapper {
|
|||
id: domain.id,
|
||||
chainType: domain.chainType.toString(),
|
||||
address: domain.address.lowercase,
|
||||
accountSequence: domain.accountSequence,
|
||||
userId: domain.userId,
|
||||
isActive: domain.isActive,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ datasource db {
|
|||
// 钱包账户表 (状态表)
|
||||
// ============================================
|
||||
model WalletAccount {
|
||||
id BigInt @id @default(autoincrement()) @map("wallet_id")
|
||||
userId BigInt @unique @map("user_id")
|
||||
id BigInt @id @default(autoincrement()) @map("wallet_id")
|
||||
accountSequence BigInt @unique @map("account_sequence") // 跨服务关联标识 (全局唯一业务ID)
|
||||
userId BigInt @unique @map("user_id") // 保留兼容
|
||||
|
||||
// USDT 余额
|
||||
usdtAvailable Decimal @default(0) @map("usdt_available") @db.Decimal(20, 8)
|
||||
|
|
@ -71,8 +72,9 @@ model WalletAccount {
|
|||
// 账本流水表 (行为表, append-only)
|
||||
// ============================================
|
||||
model LedgerEntry {
|
||||
id BigInt @id @default(autoincrement()) @map("entry_id")
|
||||
userId BigInt @map("user_id")
|
||||
id BigInt @id @default(autoincrement()) @map("entry_id")
|
||||
accountSequence BigInt @map("account_sequence") // 跨服务关联标识
|
||||
userId BigInt @map("user_id") // 保留兼容
|
||||
|
||||
// 流水类型
|
||||
entryType String @map("entry_type") @db.VarChar(50)
|
||||
|
|
@ -97,6 +99,7 @@ model LedgerEntry {
|
|||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@map("wallet_ledger_entries")
|
||||
@@index([accountSequence, createdAt(sort: Desc)])
|
||||
@@index([userId, createdAt(sort: Desc)])
|
||||
@@index([entryType])
|
||||
@@index([assetType])
|
||||
|
|
@ -109,8 +112,9 @@ model LedgerEntry {
|
|||
// 充值订单表
|
||||
// ============================================
|
||||
model DepositOrder {
|
||||
id BigInt @id @default(autoincrement()) @map("order_id")
|
||||
userId BigInt @map("user_id")
|
||||
id BigInt @id @default(autoincrement()) @map("order_id")
|
||||
accountSequence BigInt @map("account_sequence") // 跨服务关联标识
|
||||
userId BigInt @map("user_id") // 保留兼容
|
||||
|
||||
// 充值信息
|
||||
chainType String @map("chain_type") @db.VarChar(20)
|
||||
|
|
@ -124,6 +128,7 @@ model DepositOrder {
|
|||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@map("deposit_orders")
|
||||
@@index([accountSequence])
|
||||
@@index([userId])
|
||||
@@index([txHash])
|
||||
@@index([status])
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export class DepositController {
|
|||
@ApiResponse({ status: 200, description: '入账成功' })
|
||||
async handleDeposit(@Body() dto: HandleDepositDTO): Promise<{ message: string }> {
|
||||
const command = new HandleDepositCommand(
|
||||
dto.accountSequence,
|
||||
dto.userId,
|
||||
dto.amount,
|
||||
dto.chainType,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,10 @@ export class WalletController {
|
|||
@ApiOperation({ summary: '查询我的钱包', description: '获取当前用户的钱包余额、算力和奖励信息' })
|
||||
@ApiResponse({ status: 200, type: WalletResponseDTO })
|
||||
async getMyWallet(@CurrentUser() user: CurrentUserPayload): Promise<WalletResponseDTO> {
|
||||
const query = new GetMyWalletQuery(user.userId);
|
||||
const query = new GetMyWalletQuery(
|
||||
user.accountSequence.toString(),
|
||||
user.userId,
|
||||
);
|
||||
return this.walletService.getMyWallet(query);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,11 @@ import { ApiProperty } from '@nestjs/swagger';
|
|||
import { ChainType } from '@/domain/value-objects';
|
||||
|
||||
export class HandleDepositDTO {
|
||||
@ApiProperty({ description: '账户序列号 (跨服务关联标识)' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
accountSequence: string;
|
||||
|
||||
@ApiProperty({ description: '用户ID' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import { ChainType } from '@/domain/value-objects';
|
|||
|
||||
export class HandleDepositCommand {
|
||||
constructor(
|
||||
public readonly userId: string,
|
||||
public readonly accountSequence: string, // 跨服务关联标识 (全局唯一业务ID)
|
||||
public readonly userId: string, // 保留兼容
|
||||
public readonly amount: number,
|
||||
public readonly chainType: ChainType,
|
||||
public readonly txHash: string,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export class GetMyWalletQuery {
|
||||
constructor(
|
||||
public readonly userId: string,
|
||||
public readonly accountSequence: string, // 跨服务关联标识 (全局唯一业务ID)
|
||||
public readonly userId: string, // 保留兼容
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,14 +87,16 @@ export class WalletApplicationService {
|
|||
throw new DuplicateTransactionError(command.txHash);
|
||||
}
|
||||
|
||||
const accountSequence = BigInt(command.accountSequence);
|
||||
const userId = BigInt(command.userId);
|
||||
const amount = Money.USDT(command.amount);
|
||||
|
||||
// Get or create wallet
|
||||
const wallet = await this.walletRepo.getOrCreate(userId);
|
||||
// Get or create wallet by accountSequence (跨服务关联标识)
|
||||
const wallet = await this.walletRepo.getOrCreate(accountSequence, userId);
|
||||
|
||||
// Create deposit order
|
||||
const depositOrder = DepositOrder.create({
|
||||
accountSequence,
|
||||
userId: UserId.create(userId),
|
||||
chainType: command.chainType,
|
||||
amount,
|
||||
|
|
@ -107,12 +109,13 @@ export class WalletApplicationService {
|
|||
wallet.deposit(amount, command.chainType, command.txHash);
|
||||
await this.walletRepo.save(wallet);
|
||||
|
||||
// Record ledger entry
|
||||
// Record ledger entry (append-only, 可审计)
|
||||
const entryType = command.chainType === ChainType.KAVA
|
||||
? LedgerEntryType.DEPOSIT_KAVA
|
||||
: LedgerEntryType.DEPOSIT_BSC;
|
||||
|
||||
const ledgerEntry = LedgerEntry.create({
|
||||
accountSequence,
|
||||
userId: UserId.create(userId),
|
||||
entryType,
|
||||
amount,
|
||||
|
|
@ -139,8 +142,9 @@ export class WalletApplicationService {
|
|||
wallet.deduct(amount, 'Plant payment', command.orderId);
|
||||
await this.walletRepo.save(wallet);
|
||||
|
||||
// Record ledger entry
|
||||
// Record ledger entry (使用 wallet.accountSequence 作为跨服务关联键)
|
||||
const ledgerEntry = LedgerEntry.create({
|
||||
accountSequence: wallet.accountSequence,
|
||||
userId: UserId.create(userId),
|
||||
entryType: LedgerEntryType.PLANT_PAYMENT,
|
||||
amount: Money.signed(-command.amount, 'USDT'), // Negative for deduction
|
||||
|
|
@ -157,7 +161,11 @@ export class WalletApplicationService {
|
|||
async addRewards(command: AddRewardsCommand): Promise<void> {
|
||||
const userId = BigInt(command.userId);
|
||||
|
||||
const wallet = await this.walletRepo.getOrCreate(userId);
|
||||
// 先通过 userId 查找钱包(addRewards 是内部调用,钱包应该已存在)
|
||||
const wallet = await this.walletRepo.findByUserId(userId);
|
||||
if (!wallet) {
|
||||
throw new WalletNotFoundError(`userId: ${command.userId}`);
|
||||
}
|
||||
|
||||
const usdtAmount = Money.USDT(command.usdtAmount);
|
||||
const hashpowerAmount = Hashpower.create(command.hashpowerAmount);
|
||||
|
|
@ -165,9 +173,10 @@ export class WalletApplicationService {
|
|||
wallet.addPendingReward(usdtAmount, hashpowerAmount, command.expireAt, command.refOrderId);
|
||||
await this.walletRepo.save(wallet);
|
||||
|
||||
// Record ledger entry for USDT reward
|
||||
// Record ledger entry for USDT reward (使用 wallet.accountSequence)
|
||||
if (command.usdtAmount > 0) {
|
||||
const usdtEntry = LedgerEntry.create({
|
||||
accountSequence: wallet.accountSequence,
|
||||
userId: UserId.create(userId),
|
||||
entryType: LedgerEntryType.REWARD_PENDING,
|
||||
amount: usdtAmount,
|
||||
|
|
@ -181,6 +190,7 @@ export class WalletApplicationService {
|
|||
// Record ledger entry for hashpower reward
|
||||
if (command.hashpowerAmount > 0) {
|
||||
const hpEntry = LedgerEntry.create({
|
||||
accountSequence: wallet.accountSequence,
|
||||
userId: UserId.create(userId),
|
||||
entryType: LedgerEntryType.REWARD_PENDING,
|
||||
amount: Money.create(command.hashpowerAmount, 'HASHPOWER'),
|
||||
|
|
@ -209,9 +219,10 @@ export class WalletApplicationService {
|
|||
wallet.movePendingToSettleable();
|
||||
await this.walletRepo.save(wallet);
|
||||
|
||||
// Record ledger entries
|
||||
// Record ledger entries (使用 wallet.accountSequence)
|
||||
if (pendingUsdt > 0) {
|
||||
const usdtEntry = LedgerEntry.create({
|
||||
accountSequence: wallet.accountSequence,
|
||||
userId: UserId.create(userId),
|
||||
entryType: LedgerEntryType.REWARD_TO_SETTLEABLE,
|
||||
amount: Money.USDT(pendingUsdt),
|
||||
|
|
@ -222,6 +233,7 @@ export class WalletApplicationService {
|
|||
|
||||
if (pendingHashpower > 0) {
|
||||
const hpEntry = LedgerEntry.create({
|
||||
accountSequence: wallet.accountSequence,
|
||||
userId: UserId.create(userId),
|
||||
entryType: LedgerEntryType.REWARD_TO_SETTLEABLE,
|
||||
amount: Money.create(pendingHashpower, 'HASHPOWER'),
|
||||
|
|
@ -270,8 +282,9 @@ export class WalletApplicationService {
|
|||
settlementOrder.complete(swapTxHash, receivedAmount);
|
||||
await this.settlementRepo.save(settlementOrder);
|
||||
|
||||
// Record ledger entry
|
||||
// Record ledger entry (使用 wallet.accountSequence)
|
||||
const ledgerEntry = LedgerEntry.create({
|
||||
accountSequence: wallet.accountSequence,
|
||||
userId: UserId.create(userId),
|
||||
entryType: LedgerEntryType.REWARD_SETTLED,
|
||||
amount: receivedAmount,
|
||||
|
|
@ -291,6 +304,7 @@ export class WalletApplicationService {
|
|||
// =============== Queries ===============
|
||||
|
||||
async getMyWallet(query: GetMyWalletQuery): Promise<WalletDTO> {
|
||||
const accountSequence = BigInt(query.accountSequence);
|
||||
const userId = BigInt(query.userId);
|
||||
|
||||
// Try to get from cache first
|
||||
|
|
@ -307,8 +321,8 @@ export class WalletApplicationService {
|
|||
};
|
||||
}
|
||||
|
||||
// Cache miss - fetch from database
|
||||
const wallet = await this.walletRepo.getOrCreate(userId);
|
||||
// Cache miss - fetch from database (by accountSequence)
|
||||
const wallet = await this.walletRepo.getOrCreate(accountSequence, userId);
|
||||
|
||||
const walletDTO: WalletDTO = {
|
||||
walletId: wallet.walletId.toString(),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import { DomainError } from '@/shared/exceptions/domain.exception';
|
|||
|
||||
export class DepositOrder {
|
||||
private readonly _id: bigint;
|
||||
private readonly _userId: UserId;
|
||||
private readonly _accountSequence: bigint; // 跨服务关联标识 (全局唯一业务ID)
|
||||
private readonly _userId: UserId; // 保留兼容
|
||||
private readonly _chainType: ChainType;
|
||||
private readonly _amount: Money;
|
||||
private readonly _txHash: string;
|
||||
|
|
@ -14,6 +15,7 @@ export class DepositOrder {
|
|||
|
||||
private constructor(
|
||||
id: bigint,
|
||||
accountSequence: bigint,
|
||||
userId: UserId,
|
||||
chainType: ChainType,
|
||||
amount: Money,
|
||||
|
|
@ -23,6 +25,7 @@ export class DepositOrder {
|
|||
createdAt: Date,
|
||||
) {
|
||||
this._id = id;
|
||||
this._accountSequence = accountSequence;
|
||||
this._userId = userId;
|
||||
this._chainType = chainType;
|
||||
this._amount = amount;
|
||||
|
|
@ -34,6 +37,7 @@ export class DepositOrder {
|
|||
|
||||
// Getters
|
||||
get id(): bigint { return this._id; }
|
||||
get accountSequence(): bigint { return this._accountSequence; }
|
||||
get userId(): UserId { return this._userId; }
|
||||
get chainType(): ChainType { return this._chainType; }
|
||||
get amount(): Money { return this._amount; }
|
||||
|
|
@ -45,6 +49,7 @@ export class DepositOrder {
|
|||
get isConfirmed(): boolean { return this._status === DepositStatus.CONFIRMED; }
|
||||
|
||||
static create(params: {
|
||||
accountSequence: bigint;
|
||||
userId: UserId;
|
||||
chainType: ChainType;
|
||||
amount: Money;
|
||||
|
|
@ -52,6 +57,7 @@ export class DepositOrder {
|
|||
}): DepositOrder {
|
||||
return new DepositOrder(
|
||||
BigInt(0), // Will be set by database
|
||||
params.accountSequence,
|
||||
params.userId,
|
||||
params.chainType,
|
||||
params.amount,
|
||||
|
|
@ -64,6 +70,7 @@ export class DepositOrder {
|
|||
|
||||
static reconstruct(params: {
|
||||
id: bigint;
|
||||
accountSequence: bigint;
|
||||
userId: bigint;
|
||||
chainType: string;
|
||||
amount: Decimal;
|
||||
|
|
@ -74,6 +81,7 @@ export class DepositOrder {
|
|||
}): DepositOrder {
|
||||
return new DepositOrder(
|
||||
params.id,
|
||||
params.accountSequence,
|
||||
UserId.create(params.userId),
|
||||
params.chainType as ChainType,
|
||||
Money.USDT(params.amount),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { UserId, AssetType, LedgerEntryType, Money } from '@/domain/value-object
|
|||
|
||||
export class LedgerEntry {
|
||||
private readonly _id: bigint;
|
||||
private readonly _userId: UserId;
|
||||
private readonly _accountSequence: bigint; // 跨服务关联标识 (全局唯一业务ID)
|
||||
private readonly _userId: UserId; // 保留兼容
|
||||
private readonly _entryType: LedgerEntryType;
|
||||
private readonly _amount: Money;
|
||||
private readonly _balanceAfter: Money | null;
|
||||
|
|
@ -15,6 +16,7 @@ export class LedgerEntry {
|
|||
|
||||
private constructor(
|
||||
id: bigint,
|
||||
accountSequence: bigint,
|
||||
userId: UserId,
|
||||
entryType: LedgerEntryType,
|
||||
amount: Money,
|
||||
|
|
@ -26,6 +28,7 @@ export class LedgerEntry {
|
|||
createdAt: Date,
|
||||
) {
|
||||
this._id = id;
|
||||
this._accountSequence = accountSequence;
|
||||
this._userId = userId;
|
||||
this._entryType = entryType;
|
||||
this._amount = amount;
|
||||
|
|
@ -39,6 +42,7 @@ export class LedgerEntry {
|
|||
|
||||
// Getters
|
||||
get id(): bigint { return this._id; }
|
||||
get accountSequence(): bigint { return this._accountSequence; }
|
||||
get userId(): UserId { return this._userId; }
|
||||
get entryType(): LedgerEntryType { return this._entryType; }
|
||||
get amount(): Money { return this._amount; }
|
||||
|
|
@ -51,6 +55,7 @@ export class LedgerEntry {
|
|||
get createdAt(): Date { return this._createdAt; }
|
||||
|
||||
static create(params: {
|
||||
accountSequence: bigint;
|
||||
userId: UserId;
|
||||
entryType: LedgerEntryType;
|
||||
amount: Money;
|
||||
|
|
@ -62,6 +67,7 @@ export class LedgerEntry {
|
|||
}): LedgerEntry {
|
||||
return new LedgerEntry(
|
||||
BigInt(0), // Will be set by database
|
||||
params.accountSequence,
|
||||
params.userId,
|
||||
params.entryType,
|
||||
params.amount,
|
||||
|
|
@ -76,6 +82,7 @@ export class LedgerEntry {
|
|||
|
||||
static reconstruct(params: {
|
||||
id: bigint;
|
||||
accountSequence: bigint;
|
||||
userId: bigint;
|
||||
entryType: string;
|
||||
amount: Decimal;
|
||||
|
|
@ -89,6 +96,7 @@ export class LedgerEntry {
|
|||
}): LedgerEntry {
|
||||
return new LedgerEntry(
|
||||
params.id,
|
||||
params.accountSequence,
|
||||
UserId.create(params.userId),
|
||||
params.entryType as LedgerEntryType,
|
||||
Money.create(params.amount, params.assetType),
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ export interface WalletRewards {
|
|||
|
||||
export class WalletAccount {
|
||||
private readonly _walletId: WalletId;
|
||||
private readonly _userId: UserId;
|
||||
private readonly _accountSequence: bigint; // 跨服务关联标识 (全局唯一业务ID)
|
||||
private readonly _userId: UserId; // 保留兼容
|
||||
private _balances: WalletBalances;
|
||||
private _hashpower: Hashpower;
|
||||
private _rewards: WalletRewards;
|
||||
|
|
@ -44,6 +45,7 @@ export class WalletAccount {
|
|||
|
||||
private constructor(
|
||||
walletId: WalletId,
|
||||
accountSequence: bigint,
|
||||
userId: UserId,
|
||||
balances: WalletBalances,
|
||||
hashpower: Hashpower,
|
||||
|
|
@ -53,6 +55,7 @@ export class WalletAccount {
|
|||
updatedAt: Date,
|
||||
) {
|
||||
this._walletId = walletId;
|
||||
this._accountSequence = accountSequence;
|
||||
this._userId = userId;
|
||||
this._balances = balances;
|
||||
this._hashpower = hashpower;
|
||||
|
|
@ -64,6 +67,7 @@ export class WalletAccount {
|
|||
|
||||
// Getters
|
||||
get walletId(): WalletId { return this._walletId; }
|
||||
get accountSequence(): bigint { return this._accountSequence; }
|
||||
get userId(): UserId { return this._userId; }
|
||||
get balances(): WalletBalances { return this._balances; }
|
||||
get hashpower(): Hashpower { return this._hashpower; }
|
||||
|
|
@ -74,10 +78,11 @@ export class WalletAccount {
|
|||
get isActive(): boolean { return this._status === WalletStatus.ACTIVE; }
|
||||
get domainEvents(): DomainEvent[] { return [...this._domainEvents]; }
|
||||
|
||||
static createNew(userId: UserId): WalletAccount {
|
||||
static createNew(accountSequence: bigint, userId: UserId): WalletAccount {
|
||||
const now = new Date();
|
||||
return new WalletAccount(
|
||||
WalletId.create(0), // Will be set by database
|
||||
accountSequence,
|
||||
userId,
|
||||
{
|
||||
usdt: Balance.zero('USDT'),
|
||||
|
|
@ -106,6 +111,7 @@ export class WalletAccount {
|
|||
|
||||
static reconstruct(params: {
|
||||
walletId: bigint;
|
||||
accountSequence: bigint;
|
||||
userId: bigint;
|
||||
usdtAvailable: Decimal;
|
||||
usdtFrozen: Decimal;
|
||||
|
|
@ -133,6 +139,7 @@ export class WalletAccount {
|
|||
}): WalletAccount {
|
||||
return new WalletAccount(
|
||||
WalletId.create(params.walletId),
|
||||
params.accountSequence,
|
||||
UserId.create(params.userId),
|
||||
{
|
||||
usdt: Balance.create(Money.USDT(params.usdtAvailable), Money.USDT(params.usdtFrozen)),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export interface IDepositOrderRepository {
|
|||
findById(orderId: bigint): Promise<DepositOrder | null>;
|
||||
findByTxHash(txHash: string): Promise<DepositOrder | null>;
|
||||
findByUserId(userId: bigint, status?: DepositStatus): Promise<DepositOrder[]>;
|
||||
findByAccountSequence(accountSequence: bigint, status?: DepositStatus): Promise<DepositOrder[]>;
|
||||
existsByTxHash(txHash: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export interface ILedgerEntryRepository {
|
|||
save(entry: LedgerEntry): Promise<LedgerEntry>;
|
||||
saveAll(entries: LedgerEntry[]): Promise<void>;
|
||||
findByUserId(userId: bigint, filters?: LedgerFilters, pagination?: Pagination): Promise<PaginatedResult<LedgerEntry>>;
|
||||
findByAccountSequence(accountSequence: bigint, filters?: LedgerFilters, pagination?: Pagination): Promise<PaginatedResult<LedgerEntry>>;
|
||||
findByRefOrderId(refOrderId: string): Promise<LedgerEntry[]>;
|
||||
findByRefTxHash(refTxHash: string): Promise<LedgerEntry[]>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ export interface IWalletAccountRepository {
|
|||
save(wallet: WalletAccount): Promise<WalletAccount>;
|
||||
findById(walletId: bigint): Promise<WalletAccount | null>;
|
||||
findByUserId(userId: bigint): Promise<WalletAccount | null>;
|
||||
getOrCreate(userId: bigint): Promise<WalletAccount>;
|
||||
findByAccountSequence(accountSequence: bigint): Promise<WalletAccount | null>;
|
||||
getOrCreate(accountSequence: bigint, userId: bigint): Promise<WalletAccount>;
|
||||
findByUserIds(userIds: bigint[]): Promise<Map<string, WalletAccount>>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export class DepositOrderRepositoryImpl implements IDepositOrderRepository {
|
|||
|
||||
async save(order: DepositOrder): Promise<DepositOrder> {
|
||||
const data = {
|
||||
accountSequence: order.accountSequence,
|
||||
userId: order.userId.value,
|
||||
chainType: order.chainType,
|
||||
amount: order.amount.toDecimal(),
|
||||
|
|
@ -65,8 +66,22 @@ export class DepositOrderRepositoryImpl implements IDepositOrderRepository {
|
|||
return count > 0;
|
||||
}
|
||||
|
||||
async findByAccountSequence(accountSequence: bigint, status?: DepositStatus): Promise<DepositOrder[]> {
|
||||
const where: Record<string, unknown> = { accountSequence };
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
const records = await this.prisma.depositOrder.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
return records.map(r => this.toDomain(r));
|
||||
}
|
||||
|
||||
private toDomain(record: {
|
||||
id: bigint;
|
||||
accountSequence: bigint;
|
||||
userId: bigint;
|
||||
chainType: string;
|
||||
amount: Decimal;
|
||||
|
|
@ -77,6 +92,7 @@ export class DepositOrderRepositoryImpl implements IDepositOrderRepository {
|
|||
}): DepositOrder {
|
||||
return DepositOrder.reconstruct({
|
||||
id: record.id,
|
||||
accountSequence: record.accountSequence,
|
||||
userId: record.userId,
|
||||
chainType: record.chainType,
|
||||
amount: new Decimal(record.amount.toString()),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export class LedgerEntryRepositoryImpl implements ILedgerEntryRepository {
|
|||
async save(entry: LedgerEntry): Promise<LedgerEntry> {
|
||||
const created = await this.prisma.ledgerEntry.create({
|
||||
data: {
|
||||
accountSequence: entry.accountSequence,
|
||||
userId: entry.userId.value,
|
||||
entryType: entry.entryType,
|
||||
amount: entry.amount.toDecimal(),
|
||||
|
|
@ -31,6 +32,7 @@ export class LedgerEntryRepositoryImpl implements ILedgerEntryRepository {
|
|||
async saveAll(entries: LedgerEntry[]): Promise<void> {
|
||||
await this.prisma.ledgerEntry.createMany({
|
||||
data: entries.map(entry => ({
|
||||
accountSequence: entry.accountSequence,
|
||||
userId: entry.userId.value,
|
||||
entryType: entry.entryType,
|
||||
amount: entry.amount.toDecimal(),
|
||||
|
|
@ -106,8 +108,55 @@ export class LedgerEntryRepositoryImpl implements ILedgerEntryRepository {
|
|||
return records.map(r => this.toDomain(r));
|
||||
}
|
||||
|
||||
async findByAccountSequence(
|
||||
accountSequence: bigint,
|
||||
filters?: LedgerFilters,
|
||||
pagination?: Pagination,
|
||||
): Promise<PaginatedResult<LedgerEntry>> {
|
||||
const where: Record<string, unknown> = { accountSequence };
|
||||
|
||||
if (filters?.entryType) {
|
||||
where.entryType = filters.entryType;
|
||||
}
|
||||
if (filters?.assetType) {
|
||||
where.assetType = filters.assetType;
|
||||
}
|
||||
if (filters?.startDate || filters?.endDate) {
|
||||
where.createdAt = {};
|
||||
if (filters.startDate) {
|
||||
(where.createdAt as Record<string, unknown>).gte = filters.startDate;
|
||||
}
|
||||
if (filters.endDate) {
|
||||
(where.createdAt as Record<string, unknown>).lte = filters.endDate;
|
||||
}
|
||||
}
|
||||
|
||||
const page = pagination?.page ?? 1;
|
||||
const pageSize = pagination?.pageSize ?? 20;
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
const [records, total] = await Promise.all([
|
||||
this.prisma.ledgerEntry.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.ledgerEntry.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
data: records.map(r => this.toDomain(r)),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
};
|
||||
}
|
||||
|
||||
private toDomain(record: {
|
||||
id: bigint;
|
||||
accountSequence: bigint;
|
||||
userId: bigint;
|
||||
entryType: string;
|
||||
amount: Decimal;
|
||||
|
|
@ -121,6 +170,7 @@ export class LedgerEntryRepositoryImpl implements ILedgerEntryRepository {
|
|||
}): LedgerEntry {
|
||||
return LedgerEntry.reconstruct({
|
||||
id: record.id,
|
||||
accountSequence: record.accountSequence,
|
||||
userId: record.userId,
|
||||
entryType: record.entryType,
|
||||
amount: new Decimal(record.amount.toString()),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export class WalletAccountRepositoryImpl implements IWalletAccountRepository {
|
|||
|
||||
async save(wallet: WalletAccount): Promise<WalletAccount> {
|
||||
const data = {
|
||||
accountSequence: wallet.accountSequence,
|
||||
userId: wallet.userId.value,
|
||||
usdtAvailable: wallet.balances.usdt.available.toDecimal(),
|
||||
usdtFrozen: wallet.balances.usdt.frozen.toDecimal(),
|
||||
|
|
@ -63,13 +64,20 @@ export class WalletAccountRepositoryImpl implements IWalletAccountRepository {
|
|||
return record ? this.toDomain(record) : null;
|
||||
}
|
||||
|
||||
async getOrCreate(userId: bigint): Promise<WalletAccount> {
|
||||
const existing = await this.findByUserId(userId);
|
||||
async findByAccountSequence(accountSequence: bigint): Promise<WalletAccount | null> {
|
||||
const record = await this.prisma.walletAccount.findUnique({
|
||||
where: { accountSequence },
|
||||
});
|
||||
return record ? this.toDomain(record) : null;
|
||||
}
|
||||
|
||||
async getOrCreate(accountSequence: bigint, userId: bigint): Promise<WalletAccount> {
|
||||
const existing = await this.findByAccountSequence(accountSequence);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const newWallet = WalletAccount.createNew(UserId.create(userId));
|
||||
const newWallet = WalletAccount.createNew(accountSequence, UserId.create(userId));
|
||||
return this.save(newWallet);
|
||||
}
|
||||
|
||||
|
|
@ -87,6 +95,7 @@ export class WalletAccountRepositoryImpl implements IWalletAccountRepository {
|
|||
|
||||
private toDomain(record: {
|
||||
id: bigint;
|
||||
accountSequence: bigint;
|
||||
userId: bigint;
|
||||
usdtAvailable: Decimal;
|
||||
usdtFrozen: Decimal;
|
||||
|
|
@ -114,6 +123,7 @@ export class WalletAccountRepositoryImpl implements IWalletAccountRepository {
|
|||
}): WalletAccount {
|
||||
return WalletAccount.reconstruct({
|
||||
walletId: record.id,
|
||||
accountSequence: record.accountSequence,
|
||||
userId: record.userId,
|
||||
usdtAvailable: new Decimal(record.usdtAvailable.toString()),
|
||||
usdtFrozen: new Decimal(record.usdtFrozen.toString()),
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ import { PassportStrategy } from '@nestjs/passport';
|
|||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
// JWT payload 格式与 identity-service 生成的 token 一致
|
||||
interface JwtPayload {
|
||||
sub: string;
|
||||
seq: number;
|
||||
userId: string;
|
||||
accountSequence: number;
|
||||
deviceId: string;
|
||||
type: 'access' | 'refresh';
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
|
@ -22,8 +25,9 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||
|
||||
async validate(payload: JwtPayload) {
|
||||
return {
|
||||
userId: payload.sub,
|
||||
accountSequence: payload.seq,
|
||||
userId: payload.userId,
|
||||
accountSequence: payload.accountSequence,
|
||||
deviceId: payload.deviceId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue