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