From 3c2144ad7c89e20bfe00409157ccdb4dc3537be4 Mon Sep 17 00:00:00 2001 From: hailin Date: Mon, 8 Dec 2025 10:26:01 -0800 Subject: [PATCH] feat(deposit): add accountSequence correlation and testnet support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../blockchain-service/prisma/schema.prisma | 14 +++- .../blockchain-service/src/api/api.module.ts | 3 +- .../src/api/controllers/health.controller.ts | 31 +++++++ .../services/address-derivation.service.ts | 13 ++- .../services/deposit-detection.service.ts | 3 +- .../src/config/blockchain.config.ts | 80 ++++++++++++++----- .../deposit-transaction.aggregate.ts | 9 ++- .../monitored-address.aggregate.ts | 7 +- .../domain/events/deposit-confirmed.event.ts | 3 +- .../domain/events/deposit-detected.event.ts | 3 +- .../domain/services/chain-config.service.ts | 44 ++++++++-- .../mappers/deposit-transaction.mapper.ts | 2 + .../mappers/monitored-address.mapper.ts | 2 + .../wallet-service/prisma/schema.prisma | 17 ++-- .../src/api/controllers/deposit.controller.ts | 1 + .../src/api/controllers/wallet.controller.ts | 5 +- .../src/api/dto/request/deposit.dto.ts | 5 ++ .../commands/handle-deposit.command.ts | 3 +- .../queries/get-my-wallet.query.ts | 3 +- .../services/wallet-application.service.ts | 34 +++++--- .../aggregates/deposit-order.aggregate.ts | 10 ++- .../aggregates/ledger-entry.aggregate.ts | 10 ++- .../aggregates/wallet-account.aggregate.ts | 11 ++- .../deposit-order.repository.interface.ts | 1 + .../ledger-entry.repository.interface.ts | 1 + .../wallet-account.repository.interface.ts | 3 +- .../deposit-order.repository.impl.ts | 16 ++++ .../ledger-entry.repository.impl.ts | 50 ++++++++++++ .../wallet-account.repository.impl.ts | 16 +++- .../src/shared/strategies/jwt.strategy.ts | 12 ++- 30 files changed, 340 insertions(+), 72 deletions(-) diff --git a/backend/services/blockchain-service/prisma/schema.prisma b/backend/services/blockchain-service/prisma/schema.prisma index 1eb905b4..4fa82cf1 100644 --- a/backend/services/blockchain-service/prisma/schema.prisma +++ b/backend/services/blockchain-service/prisma/schema.prisma @@ -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") diff --git a/backend/services/blockchain-service/src/api/api.module.ts b/backend/services/blockchain-service/src/api/api.module.ts index dc6a8666..e19922dd 100644 --- a/backend/services/blockchain-service/src/api/api.module.ts +++ b/backend/services/blockchain-service/src/api/api.module.ts @@ -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 {} diff --git a/backend/services/blockchain-service/src/api/controllers/health.controller.ts b/backend/services/blockchain-service/src/api/controllers/health.controller.ts index 717f15bd..b30c6324 100644 --- a/backend/services/blockchain-service/src/api/controllers/health.controller.ts +++ b/backend/services/blockchain-service/src/api/controllers/health.controller.ts @@ -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 = {}; + + 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(), + }; + } } diff --git a/backend/services/blockchain-service/src/application/services/address-derivation.service.ts b/backend/services/blockchain-service/src/application/services/address-derivation.service.ts index 61d46a8b..9b9893bb 100644 --- a/backend/services/blockchain-service/src/application/services/address-derivation.service.ts +++ b/backend/services/blockchain-service/src/application/services/address-derivation.service.ts @@ -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 { + private async registerEvmAddressForMonitoring( + userId: bigint, + accountSequence: number, + derived: DerivedAddress, + ): Promise { 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}`); } diff --git a/backend/services/blockchain-service/src/application/services/deposit-detection.service.ts b/backend/services/blockchain-service/src/application/services/deposit-detection.service.ts index 1372f8e5..11ecb6c2 100644 --- a/backend/services/blockchain-service/src/application/services/deposit-detection.service.ts +++ b/backend/services/blockchain-service/src/application/services/deposit-detection.service.ts @@ -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, }); diff --git a/backend/services/blockchain-service/src/config/blockchain.config.ts b/backend/services/blockchain-service/src/config/blockchain.config.ts index 86681e4b..aadc027f 100644 --- a/backend/services/blockchain-service/src/config/blockchain.config.ts +++ b/backend/services/blockchain-service/src/config/blockchain.config.ts @@ -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), + }, + }; +}); diff --git a/backend/services/blockchain-service/src/domain/aggregates/deposit-transaction/deposit-transaction.aggregate.ts b/backend/services/blockchain-service/src/domain/aggregates/deposit-transaction/deposit-transaction.aggregate.ts index c3b97da3..6c7f6cc3 100644 --- a/backend/services/blockchain-service/src/domain/aggregates/deposit-transaction/deposit-transaction.aggregate.ts +++ b/backend/services/blockchain-service/src/domain/aggregates/deposit-transaction/deposit-transaction.aggregate.ts @@ -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 { 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 { blockTimestamp: Date; logIndex: number; addressId: bigint; + accountSequence: bigint; userId: bigint; }): DepositTransaction { const deposit = new DepositTransaction({ @@ -134,6 +139,7 @@ export class DepositTransaction extends AggregateRoot { 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 { 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(), }), ); diff --git a/backend/services/blockchain-service/src/domain/aggregates/monitored-address/monitored-address.aggregate.ts b/backend/services/blockchain-service/src/domain/aggregates/monitored-address/monitored-address.aggregate.ts index 0a38d3ae..cdecd0d4 100644 --- a/backend/services/blockchain-service/src/domain/aggregates/monitored-address/monitored-address.aggregate.ts +++ b/backend/services/blockchain-service/src/domain/aggregates/monitored-address/monitored-address.aggregate.ts @@ -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 { 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 { static create(params: { chainType: ChainType; address: EvmAddress; + accountSequence: bigint; userId: bigint; }): MonitoredAddress { return new MonitoredAddress({ diff --git a/backend/services/blockchain-service/src/domain/events/deposit-confirmed.event.ts b/backend/services/blockchain-service/src/domain/events/deposit-confirmed.event.ts index 4c655f7d..b3b44b12 100644 --- a/backend/services/blockchain-service/src/domain/events/deposit-confirmed.event.ts +++ b/backend/services/blockchain-service/src/domain/events/deposit-confirmed.event.ts @@ -8,7 +8,8 @@ export interface DepositConfirmedPayload { amount: string; amountFormatted: string; confirmations: number; - userId: string; + accountSequence: string; // 跨服务关联标识 (全局唯一业务ID) + userId: string; // 保留兼容 [key: string]: unknown; } diff --git a/backend/services/blockchain-service/src/domain/events/deposit-detected.event.ts b/backend/services/blockchain-service/src/domain/events/deposit-detected.event.ts index 14e2a15f..0a7b8e2b 100644 --- a/backend/services/blockchain-service/src/domain/events/deposit-detected.event.ts +++ b/backend/services/blockchain-service/src/domain/events/deposit-detected.event.ts @@ -11,7 +11,8 @@ export interface DepositDetectedPayload { amountFormatted: string; blockNumber: string; blockTimestamp: string; - userId: string; + accountSequence: string; // 跨服务关联标识 (全局唯一业务ID) + userId: string; // 保留兼容 [key: string]: unknown; } diff --git a/backend/services/blockchain-service/src/domain/services/chain-config.service.ts b/backend/services/blockchain-service/src/domain/services/chain-config.service.ts index 9fa75bb7..91d7a0f0 100644 --- a/backend/services/blockchain-service/src/domain/services/chain-config.service.ts +++ b/backend/services/blockchain-service/src/domain/services/chain-config.service.ts @@ -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; + private readonly isTestnet: boolean; + private readonly networkMode: string; constructor(private readonly configService: ConfigService) { this.configs = new Map(); + this.networkMode = this.configService.get('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('blockchain.kava.chainId', 2222), - rpcUrl: this.configService.get('blockchain.kava.rpcUrl', 'https://evm.kava.io'), + chainId: this.configService.get('blockchain.kava.chainId', this.isTestnet ? 2221 : 2222), + rpcUrl: this.configService.get( + 'blockchain.kava.rpcUrl', + this.isTestnet ? 'https://evm.testnet.kava.io' : 'https://evm.kava.io', + ), usdtContract: this.configService.get( '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('blockchain.bsc.chainId', 56), + chainId: this.configService.get('blockchain.bsc.chainId', this.isTestnet ? 97 : 56), rpcUrl: this.configService.get( '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( '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; } /** diff --git a/backend/services/blockchain-service/src/infrastructure/persistence/mappers/deposit-transaction.mapper.ts b/backend/services/blockchain-service/src/infrastructure/persistence/mappers/deposit-transaction.mapper.ts index 639e7e5e..d8a70ec5 100644 --- a/backend/services/blockchain-service/src/infrastructure/persistence/mappers/deposit-transaction.mapper.ts +++ b/backend/services/blockchain-service/src/infrastructure/persistence/mappers/deposit-transaction.mapper.ts @@ -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, diff --git a/backend/services/blockchain-service/src/infrastructure/persistence/mappers/monitored-address.mapper.ts b/backend/services/blockchain-service/src/infrastructure/persistence/mappers/monitored-address.mapper.ts index 842aa202..1d47b76d 100644 --- a/backend/services/blockchain-service/src/infrastructure/persistence/mappers/monitored-address.mapper.ts +++ b/backend/services/blockchain-service/src/infrastructure/persistence/mappers/monitored-address.mapper.ts @@ -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, }; diff --git a/backend/services/wallet-service/prisma/schema.prisma b/backend/services/wallet-service/prisma/schema.prisma index cc8c9e7e..dd435f6a 100644 --- a/backend/services/wallet-service/prisma/schema.prisma +++ b/backend/services/wallet-service/prisma/schema.prisma @@ -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]) diff --git a/backend/services/wallet-service/src/api/controllers/deposit.controller.ts b/backend/services/wallet-service/src/api/controllers/deposit.controller.ts index 54597025..444f4d47 100644 --- a/backend/services/wallet-service/src/api/controllers/deposit.controller.ts +++ b/backend/services/wallet-service/src/api/controllers/deposit.controller.ts @@ -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, diff --git a/backend/services/wallet-service/src/api/controllers/wallet.controller.ts b/backend/services/wallet-service/src/api/controllers/wallet.controller.ts index 821ab67e..b9958ea5 100644 --- a/backend/services/wallet-service/src/api/controllers/wallet.controller.ts +++ b/backend/services/wallet-service/src/api/controllers/wallet.controller.ts @@ -19,7 +19,10 @@ export class WalletController { @ApiOperation({ summary: '查询我的钱包', description: '获取当前用户的钱包余额、算力和奖励信息' }) @ApiResponse({ status: 200, type: WalletResponseDTO }) async getMyWallet(@CurrentUser() user: CurrentUserPayload): Promise { - const query = new GetMyWalletQuery(user.userId); + const query = new GetMyWalletQuery( + user.accountSequence.toString(), + user.userId, + ); return this.walletService.getMyWallet(query); } diff --git a/backend/services/wallet-service/src/api/dto/request/deposit.dto.ts b/backend/services/wallet-service/src/api/dto/request/deposit.dto.ts index ab35df04..e2623ceb 100644 --- a/backend/services/wallet-service/src/api/dto/request/deposit.dto.ts +++ b/backend/services/wallet-service/src/api/dto/request/deposit.dto.ts @@ -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() diff --git a/backend/services/wallet-service/src/application/commands/handle-deposit.command.ts b/backend/services/wallet-service/src/application/commands/handle-deposit.command.ts index d96d7914..19d57bbc 100644 --- a/backend/services/wallet-service/src/application/commands/handle-deposit.command.ts +++ b/backend/services/wallet-service/src/application/commands/handle-deposit.command.ts @@ -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, diff --git a/backend/services/wallet-service/src/application/queries/get-my-wallet.query.ts b/backend/services/wallet-service/src/application/queries/get-my-wallet.query.ts index 81b733f7..b013c3c1 100644 --- a/backend/services/wallet-service/src/application/queries/get-my-wallet.query.ts +++ b/backend/services/wallet-service/src/application/queries/get-my-wallet.query.ts @@ -1,5 +1,6 @@ export class GetMyWalletQuery { constructor( - public readonly userId: string, + public readonly accountSequence: string, // 跨服务关联标识 (全局唯一业务ID) + public readonly userId: string, // 保留兼容 ) {} } diff --git a/backend/services/wallet-service/src/application/services/wallet-application.service.ts b/backend/services/wallet-service/src/application/services/wallet-application.service.ts index 1a412fa1..9a0d2e72 100644 --- a/backend/services/wallet-service/src/application/services/wallet-application.service.ts +++ b/backend/services/wallet-service/src/application/services/wallet-application.service.ts @@ -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 { 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 { + 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(), diff --git a/backend/services/wallet-service/src/domain/aggregates/deposit-order.aggregate.ts b/backend/services/wallet-service/src/domain/aggregates/deposit-order.aggregate.ts index fad6aeeb..e199e965 100644 --- a/backend/services/wallet-service/src/domain/aggregates/deposit-order.aggregate.ts +++ b/backend/services/wallet-service/src/domain/aggregates/deposit-order.aggregate.ts @@ -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), diff --git a/backend/services/wallet-service/src/domain/aggregates/ledger-entry.aggregate.ts b/backend/services/wallet-service/src/domain/aggregates/ledger-entry.aggregate.ts index 4d0452be..c802b641 100644 --- a/backend/services/wallet-service/src/domain/aggregates/ledger-entry.aggregate.ts +++ b/backend/services/wallet-service/src/domain/aggregates/ledger-entry.aggregate.ts @@ -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), diff --git a/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts b/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts index afa5fad5..701fe3f0 100644 --- a/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts +++ b/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts @@ -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)), diff --git a/backend/services/wallet-service/src/domain/repositories/deposit-order.repository.interface.ts b/backend/services/wallet-service/src/domain/repositories/deposit-order.repository.interface.ts index d17d713f..83fdc22f 100644 --- a/backend/services/wallet-service/src/domain/repositories/deposit-order.repository.interface.ts +++ b/backend/services/wallet-service/src/domain/repositories/deposit-order.repository.interface.ts @@ -6,6 +6,7 @@ export interface IDepositOrderRepository { findById(orderId: bigint): Promise; findByTxHash(txHash: string): Promise; findByUserId(userId: bigint, status?: DepositStatus): Promise; + findByAccountSequence(accountSequence: bigint, status?: DepositStatus): Promise; existsByTxHash(txHash: string): Promise; } diff --git a/backend/services/wallet-service/src/domain/repositories/ledger-entry.repository.interface.ts b/backend/services/wallet-service/src/domain/repositories/ledger-entry.repository.interface.ts index 1f7c6dac..ae98f792 100644 --- a/backend/services/wallet-service/src/domain/repositories/ledger-entry.repository.interface.ts +++ b/backend/services/wallet-service/src/domain/repositories/ledger-entry.repository.interface.ts @@ -25,6 +25,7 @@ export interface ILedgerEntryRepository { save(entry: LedgerEntry): Promise; saveAll(entries: LedgerEntry[]): Promise; findByUserId(userId: bigint, filters?: LedgerFilters, pagination?: Pagination): Promise>; + findByAccountSequence(accountSequence: bigint, filters?: LedgerFilters, pagination?: Pagination): Promise>; findByRefOrderId(refOrderId: string): Promise; findByRefTxHash(refTxHash: string): Promise; } diff --git a/backend/services/wallet-service/src/domain/repositories/wallet-account.repository.interface.ts b/backend/services/wallet-service/src/domain/repositories/wallet-account.repository.interface.ts index 8cb3f118..048d6c64 100644 --- a/backend/services/wallet-service/src/domain/repositories/wallet-account.repository.interface.ts +++ b/backend/services/wallet-service/src/domain/repositories/wallet-account.repository.interface.ts @@ -4,7 +4,8 @@ export interface IWalletAccountRepository { save(wallet: WalletAccount): Promise; findById(walletId: bigint): Promise; findByUserId(userId: bigint): Promise; - getOrCreate(userId: bigint): Promise; + findByAccountSequence(accountSequence: bigint): Promise; + getOrCreate(accountSequence: bigint, userId: bigint): Promise; findByUserIds(userIds: bigint[]): Promise>; } diff --git a/backend/services/wallet-service/src/infrastructure/persistence/repositories/deposit-order.repository.impl.ts b/backend/services/wallet-service/src/infrastructure/persistence/repositories/deposit-order.repository.impl.ts index 40ecd08e..708233fd 100644 --- a/backend/services/wallet-service/src/infrastructure/persistence/repositories/deposit-order.repository.impl.ts +++ b/backend/services/wallet-service/src/infrastructure/persistence/repositories/deposit-order.repository.impl.ts @@ -11,6 +11,7 @@ export class DepositOrderRepositoryImpl implements IDepositOrderRepository { async save(order: DepositOrder): Promise { 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 { + const where: Record = { 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()), diff --git a/backend/services/wallet-service/src/infrastructure/persistence/repositories/ledger-entry.repository.impl.ts b/backend/services/wallet-service/src/infrastructure/persistence/repositories/ledger-entry.repository.impl.ts index d4a5877d..1a43fc5e 100644 --- a/backend/services/wallet-service/src/infrastructure/persistence/repositories/ledger-entry.repository.impl.ts +++ b/backend/services/wallet-service/src/infrastructure/persistence/repositories/ledger-entry.repository.impl.ts @@ -14,6 +14,7 @@ export class LedgerEntryRepositoryImpl implements ILedgerEntryRepository { async save(entry: LedgerEntry): Promise { 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 { 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> { + const where: Record = { 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).gte = filters.startDate; + } + if (filters.endDate) { + (where.createdAt as Record).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()), diff --git a/backend/services/wallet-service/src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts b/backend/services/wallet-service/src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts index e98db4fd..60dd3184 100644 --- a/backend/services/wallet-service/src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts +++ b/backend/services/wallet-service/src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts @@ -11,6 +11,7 @@ export class WalletAccountRepositoryImpl implements IWalletAccountRepository { async save(wallet: WalletAccount): Promise { 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 { - const existing = await this.findByUserId(userId); + async findByAccountSequence(accountSequence: bigint): Promise { + const record = await this.prisma.walletAccount.findUnique({ + where: { accountSequence }, + }); + return record ? this.toDomain(record) : null; + } + + async getOrCreate(accountSequence: bigint, userId: bigint): Promise { + 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()), diff --git a/backend/services/wallet-service/src/shared/strategies/jwt.strategy.ts b/backend/services/wallet-service/src/shared/strategies/jwt.strategy.ts index 738f8f3d..a19d75d7 100644 --- a/backend/services/wallet-service/src/shared/strategies/jwt.strategy.ts +++ b/backend/services/wallet-service/src/shared/strategies/jwt.strategy.ts @@ -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, }; } }