From d614d18e97a139be9e5475bf42f8f748c595ebd0 Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 3 Jan 2026 05:44:43 -0800 Subject: [PATCH] Revert "feat(withdrawal): implement fiat withdrawal with bank/alipay/wechat" This reverts commit 288d89474632a5e88a25f24badc6fcb6eafde5b9. --- .../migration.sql | 77 -- .../wallet-service/prisma/schema.prisma | 105 +- .../wallet-service/src/api/api.module.ts | 2 + .../controllers/internal-wallet.controller.ts | 76 -- .../src/api/controllers/wallet.controller.ts | 86 +- .../src/api/dto/request/withdrawal.dto.ts | 81 +- .../src/api/dto/response/withdrawal.dto.ts | 88 +- .../commands/request-withdrawal.command.ts | 68 +- .../withdrawal-status.handler.ts | 461 ++++++++ .../services/wallet-application.service.ts | 448 ++----- .../aggregates/withdrawal-order.aggregate.ts | 461 ++------ .../withdrawal-order.repository.interface.ts | 5 +- .../value-objects/ledger-entry-type.enum.ts | 1 - .../value-objects/withdrawal-status.enum.ts | 38 +- .../withdrawal-order.repository.impl.ts | 183 +-- .../src/app/(dashboard)/withdrawals/page.tsx | 682 ----------- .../withdrawals/withdrawals.module.scss | 457 ------- .../src/components/layout/Sidebar/Sidebar.tsx | 1 - .../admin-web/src/hooks/useWithdrawals.ts | 110 -- .../src/infrastructure/api/endpoints.ts | 10 - .../src/services/withdrawalService.ts | 92 -- .../admin-web/src/types/withdrawal.types.ts | 127 -- .../lib/core/services/wallet_service.dart | 148 +-- .../pages/withdraw_fiat_confirm_page.dart | 894 -------------- .../pages/withdraw_fiat_page.dart | 1046 ----------------- .../mobile-app/lib/routes/app_router.dart | 19 - .../mobile-app/lib/routes/route_names.dart | 2 - .../mobile-app/lib/routes/route_paths.dart | 2 - 28 files changed, 815 insertions(+), 4955 deletions(-) delete mode 100644 backend/services/wallet-service/prisma/migrations/20260103000000_add_fiat_withdrawal/migration.sql create mode 100644 backend/services/wallet-service/src/application/event-handlers/withdrawal-status.handler.ts delete mode 100644 frontend/admin-web/src/app/(dashboard)/withdrawals/page.tsx delete mode 100644 frontend/admin-web/src/app/(dashboard)/withdrawals/withdrawals.module.scss delete mode 100644 frontend/admin-web/src/hooks/useWithdrawals.ts delete mode 100644 frontend/admin-web/src/services/withdrawalService.ts delete mode 100644 frontend/admin-web/src/types/withdrawal.types.ts delete mode 100644 frontend/mobile-app/lib/features/withdraw/presentation/pages/withdraw_fiat_confirm_page.dart delete mode 100644 frontend/mobile-app/lib/features/withdraw/presentation/pages/withdraw_fiat_page.dart diff --git a/backend/services/wallet-service/prisma/migrations/20260103000000_add_fiat_withdrawal/migration.sql b/backend/services/wallet-service/prisma/migrations/20260103000000_add_fiat_withdrawal/migration.sql deleted file mode 100644 index c5035baa..00000000 --- a/backend/services/wallet-service/prisma/migrations/20260103000000_add_fiat_withdrawal/migration.sql +++ /dev/null @@ -1,77 +0,0 @@ --- 法币提现功能迁移 --- 将原来的区块链提现改为法币提现(银行卡/支付宝/微信) - --- 1. 添加新字段到 withdrawal_orders 表 -ALTER TABLE "withdrawal_orders" ADD COLUMN IF NOT EXISTS "withdrawal_type" VARCHAR(20) DEFAULT 'FIAT'; - --- 银行卡信息 -ALTER TABLE "withdrawal_orders" ADD COLUMN IF NOT EXISTS "bank_name" VARCHAR(100); -ALTER TABLE "withdrawal_orders" ADD COLUMN IF NOT EXISTS "bank_card_no" VARCHAR(30); -ALTER TABLE "withdrawal_orders" ADD COLUMN IF NOT EXISTS "card_holder_name" VARCHAR(50); - --- 支付宝信息 -ALTER TABLE "withdrawal_orders" ADD COLUMN IF NOT EXISTS "alipay_account" VARCHAR(100); -ALTER TABLE "withdrawal_orders" ADD COLUMN IF NOT EXISTS "alipay_real_name" VARCHAR(50); - --- 微信信息 -ALTER TABLE "withdrawal_orders" ADD COLUMN IF NOT EXISTS "wechat_account" VARCHAR(100); -ALTER TABLE "withdrawal_orders" ADD COLUMN IF NOT EXISTS "wechat_real_name" VARCHAR(50); - --- 收款方式: BANK_CARD / ALIPAY / WECHAT -ALTER TABLE "withdrawal_orders" ADD COLUMN IF NOT EXISTS "payment_method" VARCHAR(20); - --- 审核相关 -ALTER TABLE "withdrawal_orders" ADD COLUMN IF NOT EXISTS "reviewed_by" VARCHAR(50); -ALTER TABLE "withdrawal_orders" ADD COLUMN IF NOT EXISTS "reviewed_at" TIMESTAMP(6); -ALTER TABLE "withdrawal_orders" ADD COLUMN IF NOT EXISTS "review_remark" VARCHAR(500); - --- 打款相关 -ALTER TABLE "withdrawal_orders" ADD COLUMN IF NOT EXISTS "paid_by" VARCHAR(50); -ALTER TABLE "withdrawal_orders" ADD COLUMN IF NOT EXISTS "paid_at" TIMESTAMP(6); -ALTER TABLE "withdrawal_orders" ADD COLUMN IF NOT EXISTS "payment_proof" VARCHAR(500); -ALTER TABLE "withdrawal_orders" ADD COLUMN IF NOT EXISTS "payment_remark" VARCHAR(500); - --- 详细备注(记录完整的提现过程) -ALTER TABLE "withdrawal_orders" ADD COLUMN IF NOT EXISTS "detail_memo" TEXT; - --- 2. 更新索引 -CREATE INDEX IF NOT EXISTS "idx_withdrawal_payment_method" ON "withdrawal_orders" ("payment_method"); -CREATE INDEX IF NOT EXISTS "idx_withdrawal_reviewed_at" ON "withdrawal_orders" ("reviewed_at"); -CREATE INDEX IF NOT EXISTS "idx_withdrawal_paid_at" ON "withdrawal_orders" ("paid_at"); - --- 3. 创建用户收款账户表(保存用户的常用收款信息) -CREATE TABLE IF NOT EXISTS "user_payment_accounts" ( - "account_id" BIGSERIAL PRIMARY KEY, - "account_sequence" VARCHAR(20) NOT NULL, - "user_id" BIGINT NOT NULL, - - -- 收款方式 - "payment_method" VARCHAR(20) NOT NULL, - - -- 银行卡信息 - "bank_name" VARCHAR(100), - "bank_card_no" VARCHAR(30), - "card_holder_name" VARCHAR(50), - - -- 支付宝信息 - "alipay_account" VARCHAR(100), - "alipay_real_name" VARCHAR(50), - - -- 微信信息 - "wechat_account" VARCHAR(100), - "wechat_real_name" VARCHAR(50), - - -- 是否为默认账户 - "is_default" BOOLEAN DEFAULT false, - - -- 状态 - "is_active" BOOLEAN DEFAULT true, - - "created_at" TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS "idx_payment_account_sequence" ON "user_payment_accounts" ("account_sequence"); -CREATE INDEX IF NOT EXISTS "idx_payment_user_id" ON "user_payment_accounts" ("user_id"); -CREATE INDEX IF NOT EXISTS "idx_payment_method_type" ON "user_payment_accounts" ("payment_method"); -CREATE INDEX IF NOT EXISTS "idx_payment_is_default" ON "user_payment_accounts" ("is_default"); diff --git a/backend/services/wallet-service/prisma/schema.prisma b/backend/services/wallet-service/prisma/schema.prisma index ca8b45c7..02ebf63e 100644 --- a/backend/services/wallet-service/prisma/schema.prisma +++ b/backend/services/wallet-service/prisma/schema.prisma @@ -170,7 +170,7 @@ model SettlementOrder { } // ============================================ -// 提现订单表 (法币提现: 银行卡/支付宝/微信) +// 提现订单表 // ============================================ model WithdrawalOrder { id BigInt @id @default(autoincrement()) @map("order_id") @@ -179,111 +179,38 @@ model WithdrawalOrder { userId BigInt @map("user_id") // 提现信息 - amount Decimal @map("amount") @db.Decimal(20, 8) // 提现金额 (绿积分, 1:1人民币) + amount Decimal @map("amount") @db.Decimal(20, 8) // 提现金额 fee Decimal @map("fee") @db.Decimal(20, 8) // 手续费 + chainType String @map("chain_type") @db.VarChar(20) // 目标链 (BSC/KAVA) + toAddress String @map("to_address") @db.VarChar(100) // 提现目标地址 - // 提现类型: FIAT (法币) - withdrawalType String @default("FIAT") @map("withdrawal_type") @db.VarChar(20) + // 交易信息 + txHash String? @map("tx_hash") @db.VarChar(100) // 链上交易哈希 - // 收款方式: BANK_CARD / ALIPAY / WECHAT - paymentMethod String? @map("payment_method") @db.VarChar(20) + // 内部转账标识 + isInternalTransfer Boolean @default(false) @map("is_internal_transfer") // 是否为内部转账(ID转ID) + toAccountSequence String? @map("to_account_sequence") @db.VarChar(20) // 接收方ID(内部转账时有值) + toUserId BigInt? @map("to_user_id") // 接收方用户ID(内部转账时有值) - // 银行卡信息 - bankName String? @map("bank_name") @db.VarChar(100) - bankCardNo String? @map("bank_card_no") @db.VarChar(30) - cardHolderName String? @map("card_holder_name") @db.VarChar(50) - - // 支付宝信息 - alipayAccount String? @map("alipay_account") @db.VarChar(100) - alipayRealName String? @map("alipay_real_name") @db.VarChar(50) - - // 微信信息 - wechatAccount String? @map("wechat_account") @db.VarChar(100) - wechatRealName String? @map("wechat_real_name") @db.VarChar(50) - - // 状态: PENDING -> REVIEWING -> APPROVED -> PAYING -> COMPLETED / REJECTED / FAILED + // 状态 status String @default("PENDING") @map("status") @db.VarChar(20) errorMessage String? @map("error_message") @db.VarChar(500) - // 审核信息 - reviewedBy String? @map("reviewed_by") @db.VarChar(50) // 审核人 - reviewedAt DateTime? @map("reviewed_at") // 审核时间 - reviewRemark String? @map("review_remark") @db.VarChar(500) // 审核备注 - - // 打款信息 - paidBy String? @map("paid_by") @db.VarChar(50) // 打款人 - paidAt DateTime? @map("paid_at") // 打款时间 - paymentProof String? @map("payment_proof") @db.VarChar(500) // 打款凭证(截图URL等) - paymentRemark String? @map("payment_remark") @db.VarChar(500) // 打款备注 - - // 详细备注(记录完整的提现过程) - detailMemo String? @map("detail_memo") @db.Text - // 时间戳 - frozenAt DateTime? @map("frozen_at") // 资金冻结时间 - completedAt DateTime? @map("completed_at") // 完成时间 + frozenAt DateTime? @map("frozen_at") + broadcastedAt DateTime? @map("broadcasted_at") + confirmedAt DateTime? @map("confirmed_at") createdAt DateTime @default(now()) @map("created_at") - // 兼容旧字段(已弃用,保留用于迁移) - chainType String? @map("chain_type") @db.VarChar(20) - toAddress String? @map("to_address") @db.VarChar(100) - txHash String? @map("tx_hash") @db.VarChar(100) - isInternalTransfer Boolean @default(false) @map("is_internal_transfer") - toAccountSequence String? @map("to_account_sequence") @db.VarChar(20) - toUserId BigInt? @map("to_user_id") - broadcastedAt DateTime? @map("broadcasted_at") - confirmedAt DateTime? @map("confirmed_at") - @@map("withdrawal_orders") @@index([accountSequence]) @@index([userId]) @@index([status]) - @@index([paymentMethod]) - @@index([reviewedAt]) - @@index([paidAt]) + @@index([chainType]) + @@index([txHash]) @@index([createdAt]) } -// ============================================ -// 用户收款账户表(保存用户的常用收款信息) -// ============================================ -model UserPaymentAccount { - id BigInt @id @default(autoincrement()) @map("account_id") - accountSequence String @map("account_sequence") @db.VarChar(20) - userId BigInt @map("user_id") - - // 收款方式: BANK_CARD / ALIPAY / WECHAT - paymentMethod String @map("payment_method") @db.VarChar(20) - - // 银行卡信息 - bankName String? @map("bank_name") @db.VarChar(100) - bankCardNo String? @map("bank_card_no") @db.VarChar(30) - cardHolderName String? @map("card_holder_name") @db.VarChar(50) - - // 支付宝信息 - alipayAccount String? @map("alipay_account") @db.VarChar(100) - alipayRealName String? @map("alipay_real_name") @db.VarChar(50) - - // 微信信息 - wechatAccount String? @map("wechat_account") @db.VarChar(100) - wechatRealName String? @map("wechat_real_name") @db.VarChar(50) - - // 是否为默认账户 - isDefault Boolean @default(false) @map("is_default") - - // 状态 - isActive Boolean @default(true) @map("is_active") - - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - - @@map("user_payment_accounts") - @@index([accountSequence]) - @@index([userId]) - @@index([paymentMethod]) - @@index([isDefault]) -} - // ============================================ // 待领取奖励表 (逐笔记录) // 每笔待领取奖励独立跟踪过期时间 diff --git a/backend/services/wallet-service/src/api/api.module.ts b/backend/services/wallet-service/src/api/api.module.ts index 132b6c16..3e5b5809 100644 --- a/backend/services/wallet-service/src/api/api.module.ts +++ b/backend/services/wallet-service/src/api/api.module.ts @@ -11,6 +11,7 @@ import { import { InternalWalletController } from './controllers/internal-wallet.controller'; import { WalletApplicationService } from '@/application/services'; import { DepositConfirmedHandler, PlantingCreatedHandler } from '@/application/event-handlers'; +import { WithdrawalStatusHandler } from '@/application/event-handlers/withdrawal-status.handler'; import { ExpiredRewardsScheduler } from '@/application/schedulers'; import { JwtStrategy } from '@/shared/strategies/jwt.strategy'; @@ -36,6 +37,7 @@ import { JwtStrategy } from '@/shared/strategies/jwt.strategy'; WalletApplicationService, DepositConfirmedHandler, PlantingCreatedHandler, + WithdrawalStatusHandler, ExpiredRewardsScheduler, JwtStrategy, ], diff --git a/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts b/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts index 3c3e9a74..e2255fce 100644 --- a/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts +++ b/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts @@ -9,9 +9,6 @@ import { FreezeForPlantingCommand, ConfirmPlantingDeductionCommand, UnfreezeForPlantingCommand, - ReviewWithdrawalCommand, - StartPaymentCommand, - CompletePaymentCommand, } from '@/application/commands'; import { Public } from '@/shared/decorators'; @@ -197,77 +194,4 @@ export class InternalWalletController { this.logger.log(`结算结果: ${JSON.stringify(result)}`); return result; } - - // =============== 提现审核管理 (管理后台调用) =============== - - @Get('withdrawals/reviewing') - @Public() - @ApiOperation({ summary: '查询待审核提现订单(内部API)' }) - @ApiResponse({ status: 200, description: '待审核提现订单列表' }) - async getReviewingWithdrawals() { - return this.walletService.getReviewingWithdrawals(); - } - - @Get('withdrawals/approved') - @Public() - @ApiOperation({ summary: '查询待打款提现订单(内部API)' }) - @ApiResponse({ status: 200, description: '待打款提现订单列表' }) - async getApprovedWithdrawals() { - return this.walletService.getApprovedWithdrawals(); - } - - @Get('withdrawals/paying') - @Public() - @ApiOperation({ summary: '查询打款中提现订单(内部API)' }) - @ApiResponse({ status: 200, description: '打款中提现订单列表' }) - async getPayingWithdrawals() { - return this.walletService.getPayingWithdrawals(); - } - - @Post('withdrawals/:orderNo/review') - @Public() - @ApiOperation({ summary: '审核提现订单(内部API)' }) - @ApiParam({ name: 'orderNo', description: '提现订单号' }) - @ApiResponse({ status: 200, description: '审核结果' }) - async reviewWithdrawal( - @Param('orderNo') orderNo: string, - @Body() dto: { approved: boolean; reviewedBy: string; remark?: string }, - ) { - this.logger.log(`审核提现订单: ${orderNo}, approved=${dto.approved}, by=${dto.reviewedBy}`); - const command = new ReviewWithdrawalCommand( - orderNo, - dto.approved, - dto.reviewedBy, - dto.remark, - ); - return this.walletService.reviewWithdrawal(command); - } - - @Post('withdrawals/:orderNo/start-payment') - @Public() - @ApiOperation({ summary: '开始打款(内部API)' }) - @ApiParam({ name: 'orderNo', description: '提现订单号' }) - @ApiResponse({ status: 200, description: '开始打款结果' }) - async startPayment( - @Param('orderNo') orderNo: string, - @Body() dto: { paidBy: string }, - ) { - this.logger.log(`开始打款: ${orderNo}, by=${dto.paidBy}`); - const command = new StartPaymentCommand(orderNo, dto.paidBy); - return this.walletService.startPayment(command); - } - - @Post('withdrawals/:orderNo/complete-payment') - @Public() - @ApiOperation({ summary: '完成打款(内部API)' }) - @ApiParam({ name: 'orderNo', description: '提现订单号' }) - @ApiResponse({ status: 200, description: '完成打款结果' }) - async completePayment( - @Param('orderNo') orderNo: string, - @Body() dto: { paymentProof?: string; remark?: string }, - ) { - this.logger.log(`完成打款: ${orderNo}`); - const command = new CompletePaymentCommand(orderNo, dto.paymentProof, dto.remark); - return this.walletService.completePayment(command); - } } 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 41e35411..ff1616fa 100644 --- a/backend/services/wallet-service/src/api/controllers/wallet.controller.ts +++ b/backend/services/wallet-service/src/api/controllers/wallet.controller.ts @@ -1,16 +1,14 @@ -import { Controller, Get, Post, Body, Query, UseGuards, Headers, HttpException, HttpStatus, Param } from '@nestjs/common'; +import { Controller, Get, Post, Body, Query, UseGuards, Headers, HttpException, HttpStatus } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse, ApiQuery } from '@nestjs/swagger'; import { WalletApplicationService } from '@/application/services'; import { GetMyWalletQuery } from '@/application/queries'; -import { ClaimRewardsCommand, SettleRewardsCommand, RequestWithdrawalCommand, CancelWithdrawalCommand } from '@/application/commands'; -import { PaymentAccountDto } from '@/application/commands/request-withdrawal.command'; +import { ClaimRewardsCommand, SettleRewardsCommand, RequestWithdrawalCommand } from '@/application/commands'; import { CurrentUser, CurrentUserPayload } from '@/shared/decorators'; import { JwtAuthGuard } from '@/shared/guards/jwt-auth.guard'; -import { SettleRewardsDTO, RequestWithdrawalDTO, CancelWithdrawalDTO } from '@/api/dto/request'; +import { SettleRewardsDTO, RequestWithdrawalDTO } from '@/api/dto/request'; import { WalletResponseDTO, WithdrawalResponseDTO, WithdrawalListItemDTO, WithdrawalFeeConfigResponseDTO, CalculateFeeResponseDTO } from '@/api/dto/response'; import { IdentityClientService } from '@/infrastructure/external/identity'; import { FeeConfigRepositoryImpl } from '@/infrastructure/persistence/repositories'; -import { PaymentMethod } from '@/domain/value-objects/withdrawal-status.enum'; @ApiTags('Wallet') @Controller('wallet') @@ -74,7 +72,7 @@ export class WalletController { } @Post('withdraw') - @ApiOperation({ summary: '申请提现 (法币)', description: '将绿积分提现到银行卡/支付宝/微信,需要短信验证和密码验证' }) + @ApiOperation({ summary: '申请提现', description: '将USDT提现到指定地址,需要短信验证和密码验证' }) @ApiResponse({ status: 201, type: WithdrawalResponseDTO }) async requestWithdrawal( @CurrentUser() user: CurrentUserPayload, @@ -104,68 +102,30 @@ export class WalletController { throw new HttpException('登录密码错误,请重试', HttpStatus.BAD_REQUEST); } - // 验证收款账户信息完整性 - this.validatePaymentAccount(dto); - - // 构建收款账户信息 - const paymentAccount: PaymentAccountDto = { - paymentMethod: dto.paymentMethod, - bankName: dto.bankName, - bankCardNo: dto.bankCardNo, - cardHolderName: dto.cardHolderName, - alipayAccount: dto.alipayAccount, - alipayRealName: dto.alipayRealName, - wechatAccount: dto.wechatAccount, - wechatRealName: dto.wechatRealName, - }; + // 处理 toAddress: 如果是 accountSequence 格式,转换为区块链地址 + let actualAddress = dto.toAddress; + if (dto.toAddress.startsWith('D') && dto.toAddress.length === 12) { + // accountSequence 格式,需要查询对应的区块链地址 + const resolvedAddress = await this.identityClient.resolveAccountSequenceToAddress( + dto.toAddress, + dto.chainType, + token, + ); + if (!resolvedAddress) { + throw new HttpException('无效的充值ID,未找到对应地址', HttpStatus.BAD_REQUEST); + } + actualAddress = resolvedAddress; + } const command = new RequestWithdrawalCommand( - user.accountSequence, user.userId, dto.amount, - paymentAccount, + actualAddress, + dto.chainType, ); return this.walletService.requestWithdrawal(command); } - /** - * 验证收款账户信息完整性 - */ - private validatePaymentAccount(dto: RequestWithdrawalDTO): void { - switch (dto.paymentMethod) { - case PaymentMethod.BANK_CARD: - if (!dto.bankName || !dto.bankCardNo || !dto.cardHolderName) { - throw new HttpException('请填写完整的银行卡信息', HttpStatus.BAD_REQUEST); - } - break; - case PaymentMethod.ALIPAY: - if (!dto.alipayAccount || !dto.alipayRealName) { - throw new HttpException('请填写完整的支付宝信息', HttpStatus.BAD_REQUEST); - } - break; - case PaymentMethod.WECHAT: - if (!dto.wechatAccount || !dto.wechatRealName) { - throw new HttpException('请填写完整的微信信息', HttpStatus.BAD_REQUEST); - } - break; - default: - throw new HttpException('不支持的收款方式', HttpStatus.BAD_REQUEST); - } - } - - @Post('withdraw/:orderNo/cancel') - @ApiOperation({ summary: '取消提现', description: '取消未审核的提现订单' }) - @ApiResponse({ status: 200, description: '取消成功' }) - async cancelWithdrawal( - @CurrentUser() user: CurrentUserPayload, - @Param('orderNo') orderNo: string, - @Body() dto: CancelWithdrawalDTO, - ): Promise<{ orderNo: string; status: string }> { - // TODO: 验证订单属于当前用户 - const command = new CancelWithdrawalCommand(orderNo, dto.reason); - return this.walletService.cancelWithdrawal(command); - } - @Get('withdrawals') @ApiOperation({ summary: '查询提现记录', description: '获取用户的提现订单列表' }) @ApiResponse({ status: 200, type: [WithdrawalListItemDTO] }) @@ -235,8 +195,8 @@ export class WalletController { } @Get('calculate-fee') - @ApiOperation({ summary: '计算提现手续费', description: '根据提现金额计算手续费' }) - @ApiQuery({ name: 'amount', type: Number, description: '提现金额 (绿积分)' }) + @ApiOperation({ summary: '计算提取手续费', description: '根据提取金额计算手续费' }) + @ApiQuery({ name: 'amount', type: Number, description: '提取金额' }) @ApiResponse({ status: 200, type: CalculateFeeResponseDTO }) async calculateFee( @Query('amount') amountStr: string, @@ -252,7 +212,7 @@ export class WalletController { amount, fee, totalRequired: amount + fee, - receiverGets: amount - fee, // 实际到账金额(扣除手续费) + receiverGets: amount, // 接收方收到完整金额 feeType, feeValue, }; diff --git a/backend/services/wallet-service/src/api/dto/request/withdrawal.dto.ts b/backend/services/wallet-service/src/api/dto/request/withdrawal.dto.ts index b61e4c21..6bcdaf37 100644 --- a/backend/services/wallet-service/src/api/dto/request/withdrawal.dto.ts +++ b/backend/services/wallet-service/src/api/dto/request/withdrawal.dto.ts @@ -1,64 +1,31 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsNumber, IsString, IsEnum, Min, IsOptional, Length, ValidateIf, Matches } from 'class-validator'; -import { PaymentMethod } from '@/domain/value-objects/withdrawal-status.enum'; +import { IsNumber, IsString, IsEnum, Min, Matches, IsOptional, Length } from 'class-validator'; +import { ChainType } from '@/domain/value-objects'; -/** - * 法币提现请求 DTO - */ export class RequestWithdrawalDTO { - @ApiProperty({ description: '提现金额 (绿积分,1:1人民币)', example: 100 }) + @ApiProperty({ description: '提现金额 (USDT)', example: 100 }) @IsNumber() - @Min(100, { message: '最小提现金额为 100 绿积分' }) + @Min(10, { message: '最小提现金额为 10 USDT' }) amount: number; @ApiProperty({ - description: '收款方式', - enum: PaymentMethod, - example: PaymentMethod.BANK_CARD, + description: '提现目标地址 (EVM地址或充值ID)', + example: '0x1234567890abcdef1234567890abcdef12345678', }) - @IsEnum(PaymentMethod, { message: '请选择有效的收款方式' }) - paymentMethod: PaymentMethod; - - // 银行卡相关字段 - @ApiPropertyOptional({ description: '银行名称', example: '中国工商银行' }) - @ValidateIf(o => o.paymentMethod === PaymentMethod.BANK_CARD) @IsString() - bankName?: string; + @Matches(/^(0x[a-fA-F0-9]{40}|D\d{11})$/, { + message: '无效的地址格式,请输入EVM地址(0x...)或充值ID(D...)', + }) + toAddress: string; - @ApiPropertyOptional({ description: '银行卡号', example: '6222021234567890123' }) - @ValidateIf(o => o.paymentMethod === PaymentMethod.BANK_CARD) - @IsString() - @Matches(/^\d{16,19}$/, { message: '请输入正确的银行卡号(16-19位数字)' }) - bankCardNo?: string; + @ApiProperty({ + description: '目标链类型', + enum: ChainType, + example: 'KAVA', + }) + @IsEnum(ChainType) + chainType: ChainType; - @ApiPropertyOptional({ description: '持卡人姓名', example: '张三' }) - @ValidateIf(o => o.paymentMethod === PaymentMethod.BANK_CARD) - @IsString() - cardHolderName?: string; - - // 支付宝相关字段 - @ApiPropertyOptional({ description: '支付宝账号', example: '13800138000' }) - @ValidateIf(o => o.paymentMethod === PaymentMethod.ALIPAY) - @IsString() - alipayAccount?: string; - - @ApiPropertyOptional({ description: '支付宝实名', example: '张三' }) - @ValidateIf(o => o.paymentMethod === PaymentMethod.ALIPAY) - @IsString() - alipayRealName?: string; - - // 微信相关字段 - @ApiPropertyOptional({ description: '微信账号', example: 'wxid_abc123' }) - @ValidateIf(o => o.paymentMethod === PaymentMethod.WECHAT) - @IsString() - wechatAccount?: string; - - @ApiPropertyOptional({ description: '微信实名', example: '张三' }) - @ValidateIf(o => o.paymentMethod === PaymentMethod.WECHAT) - @IsString() - wechatRealName?: string; - - // 验证字段 @ApiProperty({ description: '短信验证码', example: '123456', @@ -76,17 +43,3 @@ export class RequestWithdrawalDTO { @Length(6, 32, { message: '密码长度必须在6-32位之间' }) password: string; } - -/** - * 取消提现请求 DTO - */ -export class CancelWithdrawalDTO { - @ApiProperty({ description: '提现订单号', example: 'WD1234567890ABCD' }) - @IsString() - orderNo: string; - - @ApiPropertyOptional({ description: '取消原因', example: '不想提现了' }) - @IsOptional() - @IsString() - reason?: string; -} diff --git a/backend/services/wallet-service/src/api/dto/response/withdrawal.dto.ts b/backend/services/wallet-service/src/api/dto/response/withdrawal.dto.ts index 2e4bab23..9e95d137 100644 --- a/backend/services/wallet-service/src/api/dto/response/withdrawal.dto.ts +++ b/backend/services/wallet-service/src/api/dto/response/withdrawal.dto.ts @@ -1,103 +1,47 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiProperty } from '@nestjs/swagger'; -/** - * 提现响应 DTO - */ export class WithdrawalResponseDTO { @ApiProperty({ description: '提现订单号', example: 'WD1234567890ABCD' }) orderNo: string; - @ApiProperty({ description: '提现金额 (绿积分)', example: 100 }) + @ApiProperty({ description: '提现金额', example: 100 }) amount: number; - @ApiProperty({ description: '手续费 (绿积分)', example: 1 }) + @ApiProperty({ description: '手续费', example: 1 }) fee: number; - @ApiProperty({ description: '实际到账金额 (元人民币)', example: 99 }) + @ApiProperty({ description: '实际到账金额', example: 99 }) netAmount: number; - @ApiProperty({ description: '订单状态', example: 'REVIEWING' }) + @ApiProperty({ description: '订单状态', example: 'FROZEN' }) status: string; - - @ApiProperty({ description: '收款方式', example: 'BANK_CARD' }) - paymentMethod: string; } -/** - * 提现列表项 DTO (用户端) - */ export class WithdrawalListItemDTO { @ApiProperty({ description: '提现订单号', example: 'WD1234567890ABCD' }) orderNo: string; - @ApiProperty({ description: '提现金额 (绿积分)', example: 100 }) + @ApiProperty({ description: '提现金额', example: 100 }) amount: number; - @ApiProperty({ description: '手续费 (绿积分)', example: 1 }) + @ApiProperty({ description: '手续费', example: 1 }) fee: number; - @ApiProperty({ description: '实际到账金额 (元人民币)', example: 99 }) + @ApiProperty({ description: '实际到账金额', example: 99 }) netAmount: number; - @ApiProperty({ description: '收款方式', example: 'BANK_CARD' }) - paymentMethod: string; + @ApiProperty({ description: '目标链', example: 'BSC' }) + chainType: string; - @ApiProperty({ description: '收款账户显示', example: '工商银行 ****1234 (张三)' }) - paymentAccountDisplay: string; + @ApiProperty({ description: '提现地址', example: '0x1234...' }) + toAddress: string; - @ApiProperty({ description: '订单状态', example: 'COMPLETED' }) + @ApiProperty({ description: '链上交易哈希', nullable: true }) + txHash: string | null; + + @ApiProperty({ description: '订单状态', example: 'CONFIRMED' }) status: string; @ApiProperty({ description: '创建时间' }) createdAt: string; - - @ApiPropertyOptional({ description: '完成时间' }) - completedAt: string | null; -} - -/** - * 提现订单详情 DTO (管理后台) - */ -export class WithdrawalOrderDetailDTO { - @ApiProperty({ description: '提现订单号' }) - orderNo: string; - - @ApiProperty({ description: '用户账号' }) - accountSequence: string; - - @ApiProperty({ description: '用户ID' }) - userId: string; - - @ApiProperty({ description: '提现金额 (绿积分)' }) - amount: number; - - @ApiProperty({ description: '手续费 (绿积分)' }) - fee: number; - - @ApiProperty({ description: '实际到账金额 (元人民币)' }) - netAmount: number; - - @ApiProperty({ description: '收款方式' }) - paymentMethod: string; - - @ApiProperty({ description: '收款账户显示' }) - paymentAccountDisplay: string; - - @ApiProperty({ description: '订单状态' }) - status: string; - - @ApiPropertyOptional({ description: '审核人' }) - reviewedBy: string | null; - - @ApiPropertyOptional({ description: '审核时间' }) - reviewedAt: string | null; - - @ApiPropertyOptional({ description: '打款人' }) - paidBy: string | null; - - @ApiProperty({ description: '创建时间' }) - createdAt: string; - - @ApiPropertyOptional({ description: '详细备注' }) - detailMemo: string | null; } diff --git a/backend/services/wallet-service/src/application/commands/request-withdrawal.command.ts b/backend/services/wallet-service/src/application/commands/request-withdrawal.command.ts index 1c2a7aeb..88ea0747 100644 --- a/backend/services/wallet-service/src/application/commands/request-withdrawal.command.ts +++ b/backend/services/wallet-service/src/application/commands/request-withdrawal.command.ts @@ -1,73 +1,25 @@ -import { PaymentMethod } from '@/domain/value-objects/withdrawal-status.enum'; +import { ChainType } from '@/domain/value-objects'; /** - * 收款账户信息 - */ -export interface PaymentAccountDto { - paymentMethod: PaymentMethod; - // 银行卡 - bankName?: string; - bankCardNo?: string; - cardHolderName?: string; - // 支付宝 - alipayAccount?: string; - alipayRealName?: string; - // 微信 - wechatAccount?: string; - wechatRealName?: string; -} - -/** - * 请求提现命令 (法币提现) + * 请求提现命令 */ export class RequestWithdrawalCommand { constructor( - public readonly accountSequence: string, public readonly userId: string, - public readonly amount: number, // 提现金额 (绿积分, 1:1人民币) - public readonly paymentAccount: PaymentAccountDto, // 收款账户 + public readonly amount: number, // 提现金额 (USDT) + public readonly toAddress: string, // 目标地址 + public readonly chainType: ChainType, // 目标链 (BSC/KAVA) ) {} } /** - * 审核提现命令 + * 更新提现状态命令 (内部使用) */ -export class ReviewWithdrawalCommand { +export class UpdateWithdrawalStatusCommand { constructor( public readonly orderNo: string, - public readonly approved: boolean, // true=通过, false=驳回 - public readonly reviewedBy: string, // 审核人 - public readonly remark?: string, // 审核备注 - ) {} -} - -/** - * 开始打款命令 - */ -export class StartPaymentCommand { - constructor( - public readonly orderNo: string, - public readonly paidBy: string, // 打款人 - ) {} -} - -/** - * 完成打款命令 - */ -export class CompletePaymentCommand { - constructor( - public readonly orderNo: string, - public readonly paymentProof?: string, // 打款凭证 - public readonly remark?: string, // 打款备注 - ) {} -} - -/** - * 取消提现命令 - */ -export class CancelWithdrawalCommand { - constructor( - public readonly orderNo: string, - public readonly reason?: string, // 取消原因 + public readonly status: 'BROADCASTED' | 'CONFIRMED' | 'FAILED', + public readonly txHash?: string, + public readonly errorMessage?: string, ) {} } diff --git a/backend/services/wallet-service/src/application/event-handlers/withdrawal-status.handler.ts b/backend/services/wallet-service/src/application/event-handlers/withdrawal-status.handler.ts new file mode 100644 index 00000000..161906a7 --- /dev/null +++ b/backend/services/wallet-service/src/application/event-handlers/withdrawal-status.handler.ts @@ -0,0 +1,461 @@ +import { Injectable, Logger, OnModuleInit, Inject } from '@nestjs/common'; +import { + WithdrawalEventConsumerService, + WithdrawalConfirmedPayload, + WithdrawalFailedPayload, +} from '@/infrastructure/kafka/withdrawal-event-consumer.service'; +import { + IWithdrawalOrderRepository, + WITHDRAWAL_ORDER_REPOSITORY, + IWalletAccountRepository, + WALLET_ACCOUNT_REPOSITORY, + ILedgerEntryRepository, + LEDGER_ENTRY_REPOSITORY, +} from '@/domain/repositories'; +import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service'; +import { WithdrawalOrder, WalletAccount, LedgerEntry } from '@/domain/aggregates'; +import { WithdrawalStatus, Money, UserId, LedgerEntryType } from '@/domain/value-objects'; +import { OptimisticLockError } from '@/shared/exceptions/domain.exception'; +import Decimal from 'decimal.js'; + +/** + * Withdrawal Status Handler + * + * Handles withdrawal status events from blockchain-service. + * Updates withdrawal order status and handles fund refunds on failure. + * + * IMPORTANT: + * - All operations use database transactions for atomicity. + * - Wallet balance updates use optimistic locking to prevent concurrent modification issues. + */ +@Injectable() +export class WithdrawalStatusHandler implements OnModuleInit { + private readonly logger = new Logger(WithdrawalStatusHandler.name); + + // Max retry count for optimistic lock conflicts + private readonly MAX_RETRIES = 3; + + constructor( + private readonly withdrawalEventConsumer: WithdrawalEventConsumerService, + @Inject(WITHDRAWAL_ORDER_REPOSITORY) + private readonly withdrawalRepo: IWithdrawalOrderRepository, + @Inject(WALLET_ACCOUNT_REPOSITORY) + private readonly walletRepo: IWalletAccountRepository, + @Inject(LEDGER_ENTRY_REPOSITORY) + private readonly ledgerRepo: ILedgerEntryRepository, + private readonly prisma: PrismaService, + ) {} + + onModuleInit() { + this.withdrawalEventConsumer.onWithdrawalConfirmed( + this.handleWithdrawalConfirmed.bind(this), + ); + this.withdrawalEventConsumer.onWithdrawalFailed( + this.handleWithdrawalFailed.bind(this), + ); + this.logger.log(`[INIT] WithdrawalStatusHandler registered`); + } + + /** + * Handle withdrawal confirmed event + * Update order status to CONFIRMED, store txHash, and deduct frozen balance + * + * Uses database transaction + optimistic locking to ensure atomicity and prevent race conditions. + */ + private async handleWithdrawalConfirmed( + payload: WithdrawalConfirmedPayload, + ): Promise { + this.logger.log(`[CONFIRMED] Processing withdrawal confirmation`); + this.logger.log(`[CONFIRMED] orderNo: ${payload.orderNo}`); + this.logger.log(`[CONFIRMED] txHash: ${payload.txHash}`); + + let retries = 0; + while (retries < this.MAX_RETRIES) { + try { + await this.executeWithdrawalConfirmed(payload); + return; // Success, exit + } catch (error) { + if (this.isOptimisticLockError(error)) { + retries++; + this.logger.warn(`[CONFIRMED] Optimistic lock conflict for ${payload.orderNo}, retry ${retries}/${this.MAX_RETRIES}`); + if (retries >= this.MAX_RETRIES) { + this.logger.error(`[CONFIRMED] Max retries exceeded for ${payload.orderNo}`); + throw error; + } + // Brief delay before retry + await this.sleep(50 * retries); + } else { + throw error; + } + } + } + } + + /** + * Execute the withdrawal confirmed logic within a transaction + */ + private async executeWithdrawalConfirmed( + payload: WithdrawalConfirmedPayload, + ): Promise { + try { + // Use transaction to ensure atomicity + await this.prisma.$transaction(async (tx) => { + // Find the withdrawal order + const orderRecord = await tx.withdrawalOrder.findUnique({ + where: { orderNo: payload.orderNo }, + }); + + if (!orderRecord) { + this.logger.error(`[CONFIRMED] Order not found: ${payload.orderNo}`); + return; + } + + // Check if already confirmed (idempotency) + if (orderRecord.status === WithdrawalStatus.CONFIRMED) { + this.logger.log(`[CONFIRMED] Order ${payload.orderNo} already confirmed, skipping`); + return; + } + + // Determine new status based on current status + let newStatus = orderRecord.status; + let txHash = orderRecord.txHash; + let broadcastedAt = orderRecord.broadcastedAt; + let confirmedAt = orderRecord.confirmedAt; + + // FROZEN -> BROADCASTED -> CONFIRMED + if (orderRecord.status === WithdrawalStatus.FROZEN) { + newStatus = WithdrawalStatus.BROADCASTED; + txHash = payload.txHash; + broadcastedAt = new Date(); + } + + if (newStatus === WithdrawalStatus.BROADCASTED || orderRecord.status === WithdrawalStatus.BROADCASTED) { + newStatus = WithdrawalStatus.CONFIRMED; + confirmedAt = new Date(); + } + + // Update order status + await tx.withdrawalOrder.update({ + where: { id: orderRecord.id }, + data: { + status: newStatus, + txHash, + broadcastedAt, + confirmedAt, + }, + }); + + // Find wallet and deduct frozen balance with optimistic lock + let walletRecord = await tx.walletAccount.findUnique({ + where: { accountSequence: orderRecord.accountSequence }, + }); + + if (!walletRecord) { + walletRecord = await tx.walletAccount.findUnique({ + where: { userId: orderRecord.userId }, + }); + } + + if (walletRecord) { + // Deduct the total frozen amount (amount + fee) + const totalAmount = new Decimal(orderRecord.amount.toString()).add(new Decimal(orderRecord.fee.toString())); + const currentFrozen = new Decimal(walletRecord.usdtFrozen.toString()); + + if (currentFrozen.lessThan(totalAmount)) { + this.logger.error(`[CONFIRMED] Insufficient frozen balance: have ${currentFrozen}, need ${totalAmount}`); + throw new Error(`Insufficient frozen balance for withdrawal ${payload.orderNo}`); + } + + const newFrozen = currentFrozen.minus(totalAmount); + const currentVersion = walletRecord.version; + + // Optimistic lock: update only if version matches + const updateResult = await tx.walletAccount.updateMany({ + where: { + id: walletRecord.id, + version: currentVersion, // Optimistic lock condition + }, + data: { + usdtFrozen: newFrozen, + version: currentVersion + 1, // Increment version + updatedAt: new Date(), + }, + }); + + if (updateResult.count === 0) { + // Version mismatch - another transaction modified the record + throw new OptimisticLockError(`Optimistic lock conflict for wallet ${walletRecord.id}`); + } + + this.logger.log(`[CONFIRMED] Deducted ${totalAmount.toString()} USDT from frozen balance for ${orderRecord.accountSequence} (version: ${currentVersion} -> ${currentVersion + 1})`); + + // 记录流水:根据是否内部转账决定流水类型 + if (orderRecord.isInternalTransfer && orderRecord.toAccountSequence) { + // 内部转账:给转出方记录 TRANSFER_OUT + await tx.ledgerEntry.create({ + data: { + accountSequence: orderRecord.accountSequence, + userId: orderRecord.userId, + entryType: LedgerEntryType.TRANSFER_OUT, + amount: new Decimal(orderRecord.amount.toString()).negated(), + assetType: 'USDT', + balanceAfter: walletRecord.usdtAvailable, // 冻结余额扣除后可用余额不变 + refOrderId: orderRecord.orderNo, + refTxHash: payload.txHash, + memo: `转账至 ${orderRecord.toAccountSequence}`, + payloadJson: { + toAccountSequence: orderRecord.toAccountSequence, + toUserId: orderRecord.toUserId?.toString(), + fee: orderRecord.fee.toString(), + }, + }, + }); + + // 内部转账:给接收方记录 TRANSFER_IN 并增加余额 + if (orderRecord.toUserId) { + // 查找接收方钱包 + let toWalletRecord = await tx.walletAccount.findUnique({ + where: { accountSequence: orderRecord.toAccountSequence }, + }); + + if (!toWalletRecord) { + toWalletRecord = await tx.walletAccount.findUnique({ + where: { userId: orderRecord.toUserId }, + }); + } + + if (toWalletRecord) { + const transferAmount = new Decimal(orderRecord.amount.toString()); + const toCurrentAvailable = new Decimal(toWalletRecord.usdtAvailable.toString()); + const toNewAvailable = toCurrentAvailable.add(transferAmount); + const toCurrentVersion = toWalletRecord.version; + + // 更新接收方余额 + const toUpdateResult = await tx.walletAccount.updateMany({ + where: { + id: toWalletRecord.id, + version: toCurrentVersion, + }, + data: { + usdtAvailable: toNewAvailable, + version: toCurrentVersion + 1, + updatedAt: new Date(), + }, + }); + + if (toUpdateResult.count === 0) { + throw new OptimisticLockError(`Optimistic lock conflict for receiver wallet ${toWalletRecord.id}`); + } + + // 给接收方记录 TRANSFER_IN 流水 + await tx.ledgerEntry.create({ + data: { + accountSequence: orderRecord.toAccountSequence, + userId: orderRecord.toUserId, + entryType: LedgerEntryType.TRANSFER_IN, + amount: transferAmount, + assetType: 'USDT', + balanceAfter: toNewAvailable, + refOrderId: orderRecord.orderNo, + refTxHash: payload.txHash, + memo: `来自 ${orderRecord.accountSequence} 的转账`, + payloadJson: { + fromAccountSequence: orderRecord.accountSequence, + fromUserId: orderRecord.userId.toString(), + }, + }, + }); + + this.logger.log(`[CONFIRMED] Internal transfer: ${orderRecord.accountSequence} -> ${orderRecord.toAccountSequence}, amount: ${transferAmount.toString()}`); + } else { + this.logger.error(`[CONFIRMED] Receiver wallet not found: ${orderRecord.toAccountSequence}`); + } + } + } else { + // 普通提现:记录 WITHDRAWAL + await tx.ledgerEntry.create({ + data: { + accountSequence: orderRecord.accountSequence, + userId: orderRecord.userId, + entryType: LedgerEntryType.WITHDRAWAL, + amount: new Decimal(orderRecord.amount.toString()).negated(), + assetType: 'USDT', + balanceAfter: walletRecord.usdtAvailable, + refOrderId: orderRecord.orderNo, + refTxHash: payload.txHash, + memo: `提现至 ${orderRecord.toAddress}`, + payloadJson: { + toAddress: orderRecord.toAddress, + chainType: orderRecord.chainType, + fee: orderRecord.fee.toString(), + }, + }, + }); + } + } else { + this.logger.error(`[CONFIRMED] Wallet not found for accountSequence: ${orderRecord.accountSequence}, userId: ${orderRecord.userId}`); + } + }); + + this.logger.log(`[CONFIRMED] Order ${payload.orderNo} confirmed successfully`); + } catch (error) { + this.logger.error(`[CONFIRMED] Failed to process confirmation for ${payload.orderNo}`, error); + throw error; + } + } + + /** + * Handle withdrawal failed event + * Update order status to FAILED and refund frozen funds (amount + fee) + * + * Uses database transaction + optimistic locking to ensure atomicity and prevent race conditions. + */ + private async handleWithdrawalFailed( + payload: WithdrawalFailedPayload, + ): Promise { + this.logger.log(`[FAILED] Processing withdrawal failure`); + this.logger.log(`[FAILED] orderNo: ${payload.orderNo}`); + this.logger.log(`[FAILED] error: ${payload.error}`); + + let retries = 0; + while (retries < this.MAX_RETRIES) { + try { + await this.executeWithdrawalFailed(payload); + return; // Success, exit + } catch (error) { + if (this.isOptimisticLockError(error)) { + retries++; + this.logger.warn(`[FAILED] Optimistic lock conflict for ${payload.orderNo}, retry ${retries}/${this.MAX_RETRIES}`); + if (retries >= this.MAX_RETRIES) { + this.logger.error(`[FAILED] Max retries exceeded for ${payload.orderNo}`); + throw error; + } + // Brief delay before retry + await this.sleep(50 * retries); + } else { + throw error; + } + } + } + } + + /** + * Execute the withdrawal failed logic within a transaction + */ + private async executeWithdrawalFailed( + payload: WithdrawalFailedPayload, + ): Promise { + try { + // Use transaction to ensure atomicity + await this.prisma.$transaction(async (tx) => { + // Find the withdrawal order + const orderRecord = await tx.withdrawalOrder.findUnique({ + where: { orderNo: payload.orderNo }, + }); + + if (!orderRecord) { + this.logger.error(`[FAILED] Order not found: ${payload.orderNo}`); + return; + } + + // Check if already in terminal state (idempotency) + if (orderRecord.status === WithdrawalStatus.CONFIRMED || + orderRecord.status === WithdrawalStatus.FAILED || + orderRecord.status === WithdrawalStatus.CANCELLED) { + this.logger.log(`[FAILED] Order ${payload.orderNo} already in terminal state: ${orderRecord.status}, skipping`); + return; + } + + // Check if needs unfreeze (was frozen) + const needsUnfreeze = orderRecord.frozenAt !== null; + + // Update order status to FAILED + await tx.withdrawalOrder.update({ + where: { id: orderRecord.id }, + data: { + status: WithdrawalStatus.FAILED, + errorMessage: payload.error, + }, + }); + + // Refund frozen funds back to available balance if needed + if (needsUnfreeze) { + let walletRecord = await tx.walletAccount.findUnique({ + where: { accountSequence: orderRecord.accountSequence }, + }); + + if (!walletRecord) { + walletRecord = await tx.walletAccount.findUnique({ + where: { userId: orderRecord.userId }, + }); + } + + if (walletRecord) { + // Unfreeze the total amount (amount + fee) + const totalAmount = new Decimal(orderRecord.amount.toString()).add(new Decimal(orderRecord.fee.toString())); + const currentFrozen = new Decimal(walletRecord.usdtFrozen.toString()); + const currentAvailable = new Decimal(walletRecord.usdtAvailable.toString()); + const currentVersion = walletRecord.version; + + // Validate frozen balance + let newFrozen: Decimal; + let newAvailable: Decimal; + + if (currentFrozen.lessThan(totalAmount)) { + this.logger.warn(`[FAILED] Frozen balance (${currentFrozen}) less than refund amount (${totalAmount}), refunding what's available`); + // Refund whatever is frozen (shouldn't happen in normal flow) + const refundAmount = Decimal.min(currentFrozen, totalAmount); + newFrozen = currentFrozen.minus(refundAmount); + newAvailable = currentAvailable.add(refundAmount); + } else { + newFrozen = currentFrozen.minus(totalAmount); + newAvailable = currentAvailable.add(totalAmount); + } + + // Optimistic lock: update only if version matches + const updateResult = await tx.walletAccount.updateMany({ + where: { + id: walletRecord.id, + version: currentVersion, // Optimistic lock condition + }, + data: { + usdtFrozen: newFrozen, + usdtAvailable: newAvailable, + version: currentVersion + 1, // Increment version + updatedAt: new Date(), + }, + }); + + if (updateResult.count === 0) { + // Version mismatch - another transaction modified the record + throw new OptimisticLockError(`Optimistic lock conflict for wallet ${walletRecord.id}`); + } + + this.logger.log(`[FAILED] Refunded ${totalAmount.toString()} USDT (amount + fee) to account ${orderRecord.accountSequence} (version: ${currentVersion} -> ${currentVersion + 1})`); + } else { + this.logger.error(`[FAILED] Wallet not found for accountSequence: ${orderRecord.accountSequence}, userId: ${orderRecord.userId}`); + } + } + }); + + this.logger.log(`[FAILED] Order ${payload.orderNo} marked as failed`); + } catch (error) { + this.logger.error(`[FAILED] Failed to process failure for ${payload.orderNo}`, error); + throw error; + } + } + + /** + * Check if error is an optimistic lock error + */ + private isOptimisticLockError(error: unknown): boolean { + return error instanceof OptimisticLockError; + } + + /** + * Sleep for specified milliseconds + */ + private sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } +} 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 ff2b74b0..86decef1 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 @@ -15,16 +15,17 @@ import { import { HandleDepositCommand, DeductForPlantingCommand, AddRewardsCommand, ClaimRewardsCommand, SettleRewardsCommand, AllocateFundsCommand, FundAllocationItem, - RequestWithdrawalCommand, ReviewWithdrawalCommand, StartPaymentCommand, CompletePaymentCommand, CancelWithdrawalCommand, + RequestWithdrawalCommand, UpdateWithdrawalStatusCommand, FreezeForPlantingCommand, ConfirmPlantingDeductionCommand, UnfreezeForPlantingCommand, } from '@/application/commands'; -import { PaymentMethod, WithdrawalStatus } from '@/domain/value-objects/withdrawal-status.enum'; import { GetMyWalletQuery, GetMyLedgerQuery } from '@/application/queries'; import { DuplicateTransactionError, WalletNotFoundError, OptimisticLockError } from '@/shared/exceptions/domain.exception'; import { WalletCacheService } from '@/infrastructure/redis'; import { EventPublisherService } from '@/infrastructure/kafka'; +import { WithdrawalRequestedEvent } from '@/domain/events'; import { FeeConfigRepositoryImpl } from '@/infrastructure/persistence/repositories'; import { FeeType } from '@/api/dto/response'; +import { IdentityClientService } from '@/infrastructure/external/identity/identity-client.service'; export interface WalletDTO { walletId: string; @@ -93,6 +94,7 @@ export class WalletApplicationService { private readonly eventPublisher: EventPublisherService, private readonly prisma: PrismaService, private readonly feeConfigRepo: FeeConfigRepositoryImpl, + private readonly identityClient: IdentityClientService, ) {} // =============== Commands =============== @@ -1454,7 +1456,7 @@ export class WalletApplicationService { * 3. 创建提现订单 * 4. 冻结用户余额 * 5. 记录流水 - * 6. 提交审核 + * 6. 发布事件通知 blockchain-service */ async requestWithdrawal(command: RequestWithdrawalCommand): Promise<{ orderNo: string; @@ -1462,7 +1464,6 @@ export class WalletApplicationService { fee: number; netAmount: number; status: string; - paymentMethod: string; }> { const MAX_RETRIES = 3; let retries = 0; @@ -1473,9 +1474,9 @@ export class WalletApplicationService { } catch (error) { if (this.isOptimisticLockError(error)) { retries++; - this.logger.warn(`[requestWithdrawal] Optimistic lock conflict for ${command.accountSequence}, retry ${retries}/${MAX_RETRIES}`); + this.logger.warn(`[requestWithdrawal] Optimistic lock conflict for ${command.userId}, retry ${retries}/${MAX_RETRIES}`); if (retries >= MAX_RETRIES) { - this.logger.error(`[requestWithdrawal] Max retries exceeded for ${command.accountSequence}`); + this.logger.error(`[requestWithdrawal] Max retries exceeded for ${command.userId}`); throw error; } await this.sleep(50 * retries); @@ -1489,7 +1490,7 @@ export class WalletApplicationService { } /** - * Execute the withdrawal request logic (法币提现) + * Execute the withdrawal request logic */ private async executeRequestWithdrawal(command: RequestWithdrawalCommand): Promise<{ orderNo: string; @@ -1497,7 +1498,6 @@ export class WalletApplicationService { fee: number; netAmount: number; status: string; - paymentMethod: string; }> { const userId = BigInt(command.userId); const amount = Money.USDT(command.amount); @@ -1511,20 +1511,20 @@ export class WalletApplicationService { ? `固定 ${feeValue} 绿积分` : `${(feeValue * 100).toFixed(2)}%`; - this.logger.log(`Processing fiat withdrawal request for user ${command.accountSequence}: ${command.amount} 绿积分, fee: ${feeAmount} (${feeDescription}), payment method: ${command.paymentAccount.paymentMethod}`); + this.logger.log(`Processing withdrawal request for user ${userId}: ${command.amount} USDT to ${command.toAddress}, fee: ${feeAmount} (${feeDescription})`); // 验证最小提现金额 if (command.amount < this.MIN_WITHDRAWAL_AMOUNT) { - throw new Error(`最小提现金额为 ${this.MIN_WITHDRAWAL_AMOUNT} 绿积分`); + throw new Error(`最小提现金额为 ${this.MIN_WITHDRAWAL_AMOUNT} USDT`); } // 优先按 accountSequence 查找,如果未找到则按 userId 查找 - let wallet = await this.walletRepo.findByAccountSequence(command.accountSequence); + let wallet = await this.walletRepo.findByAccountSequence(command.userId); if (!wallet) { wallet = await this.walletRepo.findByUserId(userId); } if (!wallet) { - throw new WalletNotFoundError(`userId/accountSequence: ${command.accountSequence}`); + throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`); } // 验证余额是否足够 @@ -1534,31 +1534,45 @@ export class WalletApplicationService { ); } - // 创建提现订单 (法币提现) + // 检查目标地址是否为系统内用户(内部转账) + let isInternalTransfer = false; + let toAccountSequence: string | undefined; + let toUserId: UserId | undefined; + + const targetUser = await this.identityClient.findUserByWalletAddress( + command.chainType, + command.toAddress, + ); + + if (targetUser) { + // 目标地址属于系统内用户,标记为内部转账 + isInternalTransfer = true; + toAccountSequence = targetUser.accountSequence; + toUserId = UserId.create(BigInt(targetUser.userId)); + this.logger.log( + `Internal transfer detected: ${wallet.accountSequence} -> ${toAccountSequence}`, + ); + } + + // 创建提现订单 const withdrawalOrder = WithdrawalOrder.create({ accountSequence: wallet.accountSequence, userId: UserId.create(userId), amount, fee, - paymentAccount: { - paymentMethod: command.paymentAccount.paymentMethod, - bankName: command.paymentAccount.bankName, - bankCardNo: command.paymentAccount.bankCardNo, - cardHolderName: command.paymentAccount.cardHolderName, - alipayAccount: command.paymentAccount.alipayAccount, - alipayRealName: command.paymentAccount.alipayRealName, - wechatAccount: command.paymentAccount.wechatAccount, - wechatRealName: command.paymentAccount.wechatRealName, - }, + chainType: command.chainType, + toAddress: command.toAddress, + isInternalTransfer, + toAccountSequence, + toUserId, }); // 冻结用户余额 (金额 + 手续费) wallet.freeze(totalRequired); await this.walletRepo.save(wallet); - // 标记订单已冻结并提交审核 + // 标记订单已冻结 withdrawalOrder.markAsFrozen(); - withdrawalOrder.submitForReview(); const savedOrder = await this.withdrawalRepo.save(withdrawalOrder); // 记录流水 - 冻结 @@ -1569,14 +1583,35 @@ export class WalletApplicationService { amount: Money.signed(-totalRequired.value, 'USDT'), balanceAfter: wallet.balances.usdt.available, refOrderId: savedOrder.orderNo, - memo: `提现冻结: ${command.amount} 绿积分 + ${feeAmount.toFixed(2)} 绿积分手续费 (${feeDescription}), 收款方式: ${savedOrder.getPaymentMethodName()}`, + memo: `提取冻结: ${command.amount} 绿积分 + ${feeAmount.toFixed(2)} 绿积分手续费 (${feeDescription})`, }); await this.ledgerRepo.save(freezeEntry); + // 发布事件通知 blockchain-service + const event = new WithdrawalRequestedEvent({ + orderNo: savedOrder.orderNo, + accountSequence: wallet.accountSequence, + userId: userId.toString(), + walletId: wallet.walletId.toString(), + amount: command.amount.toString(), + fee: feeAmount.toString(), + netAmount: command.amount.toString(), // 接收方收到完整金额,手续费由发送方额外承担 + assetType: 'USDT', + chainType: command.chainType, + toAddress: command.toAddress, + }); + + // 发布到 Kafka 通知 blockchain-service + await this.eventPublisher.publish({ + eventType: 'wallet.withdrawal.requested', + payload: event.getPayload() as unknown as { [key: string]: unknown }, + }); + this.logger.log(`Withdrawal event published: ${savedOrder.orderNo}`); + // 清除钱包缓存 await this.walletCacheService.invalidateWallet(userId); - this.logger.log(`Fiat withdrawal order created: ${savedOrder.orderNo}, status: ${savedOrder.status}`); + this.logger.log(`Withdrawal order created: ${savedOrder.orderNo}`); return { orderNo: savedOrder.orderNo, @@ -1584,19 +1619,14 @@ export class WalletApplicationService { fee: savedOrder.fee.value, netAmount: savedOrder.netAmount.value, status: savedOrder.status, - paymentMethod: savedOrder.paymentMethod, }; } /** - * 审核提现订单 (管理员) + * 更新提现状态 (内部调用,由 blockchain-service 事件触发) */ - async reviewWithdrawal(command: ReviewWithdrawalCommand): Promise<{ - orderNo: string; - status: string; - reviewedBy: string; - }> { - this.logger.log(`Reviewing withdrawal ${command.orderNo}: approved=${command.approved} by ${command.reviewedBy}`); + async updateWithdrawalStatus(command: UpdateWithdrawalStatusCommand): Promise { + this.logger.log(`Updating withdrawal ${command.orderNo} to status ${command.status}`); const order = await this.withdrawalRepo.findByOrderNo(command.orderNo); if (!order) { @@ -1608,189 +1638,69 @@ export class WalletApplicationService { throw new WalletNotFoundError(`userId: ${order.userId.value}`); } - if (command.approved) { - // 审核通过 - order.approve(command.reviewedBy, command.remark); - await this.withdrawalRepo.save(order); - this.logger.log(`Withdrawal ${order.orderNo} approved by ${command.reviewedBy}`); - } else { - // 审核驳回 - order.reject(command.reviewedBy, command.remark || '审核不通过'); - await this.withdrawalRepo.save(order); - - // 解冻资金 - const totalFrozen = order.amount.add(order.fee); - wallet.unfreeze(totalFrozen); - await this.walletRepo.save(wallet); - - // 记录解冻流水 - const unfreezeEntry = LedgerEntry.create({ - accountSequence: wallet.accountSequence, - userId: order.userId, - entryType: LedgerEntryType.UNFREEZE, - amount: totalFrozen, - balanceAfter: wallet.balances.usdt.available, - refOrderId: order.orderNo, - memo: `提现审核驳回,资金解冻: ${command.remark || '审核不通过'}`, - }); - await this.ledgerRepo.save(unfreezeEntry); - - this.logger.log(`Withdrawal ${order.orderNo} rejected by ${command.reviewedBy}`); - } - - await this.walletCacheService.invalidateWallet(order.userId.value); - - return { - orderNo: order.orderNo, - status: order.status, - reviewedBy: command.reviewedBy, - }; - } - - /** - * 开始打款 (管理员) - */ - async startPayment(command: StartPaymentCommand): Promise<{ - orderNo: string; - status: string; - paidBy: string; - }> { - this.logger.log(`Starting payment for withdrawal ${command.orderNo} by ${command.paidBy}`); - - const order = await this.withdrawalRepo.findByOrderNo(command.orderNo); - if (!order) { - throw new Error(`Withdrawal order not found: ${command.orderNo}`); - } - - order.startPayment(command.paidBy); - await this.withdrawalRepo.save(order); - - this.logger.log(`Withdrawal ${order.orderNo} payment started by ${command.paidBy}`); - - return { - orderNo: order.orderNo, - status: order.status, - paidBy: command.paidBy, - }; - } - - /** - * 完成打款 (管理员) - */ - async completePayment(command: CompletePaymentCommand): Promise<{ - orderNo: string; - status: string; - completedAt: string; - }> { - this.logger.log(`Completing payment for withdrawal ${command.orderNo}`); - - const order = await this.withdrawalRepo.findByOrderNo(command.orderNo); - if (!order) { - throw new Error(`Withdrawal order not found: ${command.orderNo}`); - } - - const wallet = await this.walletRepo.findByUserId(order.userId.value); - if (!wallet) { - throw new WalletNotFoundError(`userId: ${order.userId.value}`); - } - - // 完成打款 - order.completePayment(command.paymentProof, command.remark); - await this.withdrawalRepo.save(order); - - // 解冻并扣除余额 const totalFrozen = order.amount.add(order.fee); - wallet.unfreeze(totalFrozen); - wallet.deduct(totalFrozen, `提现完成: ${order.orderNo}`, order.orderNo); - await this.walletRepo.save(wallet); - // 记录提现完成流水 - const withdrawEntry = LedgerEntry.create({ - accountSequence: wallet.accountSequence, - userId: order.userId, - entryType: LedgerEntryType.WITHDRAWAL, - amount: Money.signed(-order.amount.value, 'USDT'), - balanceAfter: wallet.balances.usdt.available, - refOrderId: order.orderNo, - memo: `提现完成: ${order.netAmount.value.toFixed(2)} 元人民币 到 ${order.getPaymentAccountDisplay()}`, - }); - await this.ledgerRepo.save(withdrawEntry); + switch (command.status) { + case 'BROADCASTED': + if (!command.txHash) { + throw new Error('txHash is required for BROADCASTED status'); + } + order.markAsBroadcasted(command.txHash); + await this.withdrawalRepo.save(order); + break; - // 手续费流水 (单独记录) - if (order.fee.value > 0) { - const feeEntry = LedgerEntry.create({ - accountSequence: wallet.accountSequence, - userId: order.userId, - entryType: LedgerEntryType.WITHDRAWAL_FEE, - amount: Money.signed(-order.fee.value, 'USDT'), - balanceAfter: wallet.balances.usdt.available, - refOrderId: order.orderNo, - memo: `提现手续费`, - }); - await this.ledgerRepo.save(feeEntry); + case 'CONFIRMED': + order.markAsConfirmed(); + await this.withdrawalRepo.save(order); + + // 解冻并扣除 + wallet.unfreeze(totalFrozen); + wallet.deduct(totalFrozen, 'Withdrawal completed', order.orderNo); + await this.walletRepo.save(wallet); + + // 记录提现完成流水 + const withdrawEntry = LedgerEntry.create({ + accountSequence: wallet.accountSequence, + userId: order.userId, + entryType: LedgerEntryType.WITHDRAWAL, + amount: Money.signed(-order.amount.value, 'USDT'), + balanceAfter: wallet.balances.usdt.available, + refOrderId: order.orderNo, + refTxHash: order.txHash ?? undefined, + memo: `Withdrawal to ${order.toAddress}`, + }); + await this.ledgerRepo.save(withdrawEntry); + + this.logger.log(`Withdrawal ${order.orderNo} confirmed, txHash: ${order.txHash}`); + break; + + case 'FAILED': + order.markAsFailed(command.errorMessage || 'Unknown error'); + await this.withdrawalRepo.save(order); + + // 解冻资金 + if (order.needsUnfreeze()) { + wallet.unfreeze(totalFrozen); + await this.walletRepo.save(wallet); + + // 记录解冻流水 + const unfreezeEntry = LedgerEntry.create({ + accountSequence: wallet.accountSequence, + userId: order.userId, + entryType: LedgerEntryType.UNFREEZE, + amount: totalFrozen, + balanceAfter: wallet.balances.usdt.available, + refOrderId: order.orderNo, + memo: `Withdrawal failed, funds unfrozen: ${command.errorMessage}`, + }); + await this.ledgerRepo.save(unfreezeEntry); + } + + this.logger.warn(`Withdrawal ${order.orderNo} failed: ${command.errorMessage}`); + break; } await this.walletCacheService.invalidateWallet(order.userId.value); - - this.logger.log(`Withdrawal ${order.orderNo} completed`); - - return { - orderNo: order.orderNo, - status: order.status, - completedAt: order.completedAt?.toISOString() || new Date().toISOString(), - }; - } - - /** - * 取消提现 (用户) - */ - async cancelWithdrawal(command: CancelWithdrawalCommand): Promise<{ - orderNo: string; - status: string; - }> { - this.logger.log(`Cancelling withdrawal ${command.orderNo}`); - - const order = await this.withdrawalRepo.findByOrderNo(command.orderNo); - if (!order) { - throw new Error(`Withdrawal order not found: ${command.orderNo}`); - } - - const wallet = await this.walletRepo.findByUserId(order.userId.value); - if (!wallet) { - throw new WalletNotFoundError(`userId: ${order.userId.value}`); - } - - // 取消订单 - order.cancel(command.reason); - await this.withdrawalRepo.save(order); - - // 如果已冻结,解冻资金 - if (order.needsUnfreeze()) { - const totalFrozen = order.amount.add(order.fee); - wallet.unfreeze(totalFrozen); - await this.walletRepo.save(wallet); - - // 记录解冻流水 - const unfreezeEntry = LedgerEntry.create({ - accountSequence: wallet.accountSequence, - userId: order.userId, - entryType: LedgerEntryType.UNFREEZE, - amount: totalFrozen, - balanceAfter: wallet.balances.usdt.available, - refOrderId: order.orderNo, - memo: `用户取消提现,资金解冻${command.reason ? `: ${command.reason}` : ''}`, - }); - await this.ledgerRepo.save(unfreezeEntry); - } - - await this.walletCacheService.invalidateWallet(order.userId.value); - - this.logger.log(`Withdrawal ${order.orderNo} cancelled`); - - return { - orderNo: order.orderNo, - status: order.status, - }; } /** @@ -1801,11 +1711,11 @@ export class WalletApplicationService { amount: number; fee: number; netAmount: number; - paymentMethod: string; - paymentAccountDisplay: string; + chainType: string; + toAddress: string; + txHash: string | null; status: string; createdAt: string; - completedAt: string | null; }>> { const orders = await this.withdrawalRepo.findByUserId(BigInt(userId)); return orders.map(order => ({ @@ -1813,113 +1723,11 @@ export class WalletApplicationService { amount: order.amount.value, fee: order.fee.value, netAmount: order.netAmount.value, - paymentMethod: order.paymentMethod, - paymentAccountDisplay: order.getPaymentAccountDisplay(), + chainType: order.chainType, + toAddress: order.toAddress, + txHash: order.txHash, status: order.status, createdAt: order.createdAt.toISOString(), - completedAt: order.completedAt?.toISOString() || null, - })); - } - - /** - * 查询待审核的提现订单 (管理员) - */ - async getReviewingWithdrawals(): Promise> { - const orders = await this.withdrawalRepo.findReviewingOrders(); - return orders.map(order => ({ - orderNo: order.orderNo, - accountSequence: order.accountSequence, - userId: order.userId.value.toString(), - amount: order.amount.value, - fee: order.fee.value, - netAmount: order.netAmount.value, - paymentMethod: order.paymentMethod, - paymentAccountDisplay: order.getPaymentAccountDisplay(), - status: order.status, - createdAt: order.createdAt.toISOString(), - detailMemo: order.detailMemo, - })); - } - - /** - * 查询待打款的提现订单 (管理员) - */ - async getApprovedWithdrawals(): Promise> { - const orders = await this.withdrawalRepo.findApprovedOrders(); - return orders.map(order => ({ - orderNo: order.orderNo, - accountSequence: order.accountSequence, - userId: order.userId.value.toString(), - amount: order.amount.value, - fee: order.fee.value, - netAmount: order.netAmount.value, - paymentMethod: order.paymentMethod, - paymentAccountDisplay: order.getPaymentAccountDisplay(), - status: order.status, - reviewedBy: order.reviewedBy, - reviewedAt: order.reviewedAt?.toISOString() || null, - createdAt: order.createdAt.toISOString(), - detailMemo: order.detailMemo, - })); - } - - /** - * 查询打款中的提现订单 (管理员) - */ - async getPayingWithdrawals(): Promise> { - const orders = await this.withdrawalRepo.findPayingOrders(); - return orders.map(order => ({ - orderNo: order.orderNo, - accountSequence: order.accountSequence, - userId: order.userId.value.toString(), - amount: order.amount.value, - fee: order.fee.value, - netAmount: order.netAmount.value, - paymentMethod: order.paymentMethod, - paymentAccountDisplay: order.getPaymentAccountDisplay(), - status: order.status, - paidBy: order.paidBy, - createdAt: order.createdAt.toISOString(), - detailMemo: order.detailMemo, })); } diff --git a/backend/services/wallet-service/src/domain/aggregates/withdrawal-order.aggregate.ts b/backend/services/wallet-service/src/domain/aggregates/withdrawal-order.aggregate.ts index 18b02ca1..a7ae8559 100644 --- a/backend/services/wallet-service/src/domain/aggregates/withdrawal-order.aggregate.ts +++ b/backend/services/wallet-service/src/domain/aggregates/withdrawal-order.aggregate.ts @@ -1,55 +1,16 @@ import Decimal from 'decimal.js'; -import { UserId, AssetType, Money } from '@/domain/value-objects'; -import { WithdrawalStatus, PaymentMethod, WithdrawalType } from '@/domain/value-objects/withdrawal-status.enum'; +import { UserId, ChainType, AssetType, Money } from '@/domain/value-objects'; +import { WithdrawalStatus } from '@/domain/value-objects/withdrawal-status.enum'; import { DomainError } from '@/shared/exceptions/domain.exception'; /** - * 收款账户信息 - */ -export interface PaymentAccountInfo { - paymentMethod: PaymentMethod; - // 银行卡 - bankName?: string; - bankCardNo?: string; - cardHolderName?: string; - // 支付宝 - alipayAccount?: string; - alipayRealName?: string; - // 微信 - wechatAccount?: string; - wechatRealName?: string; -} - -/** - * 审核信息 - */ -export interface ReviewInfo { - reviewedBy: string; - reviewedAt: Date; - reviewRemark?: string; -} - -/** - * 打款信息 - */ -export interface PaymentInfo { - paidBy: string; - paidAt: Date; - paymentProof?: string; - paymentRemark?: string; -} - -/** - * 提现订单聚合根 (法币提现) + * 提现订单聚合根 * * 提现流程: * 1. 用户发起提现请求 -> PENDING * 2. 冻结用户余额 -> FROZEN - * 3. 提交审核 -> REVIEWING - * 4. 审核通过 -> APPROVED (等待打款) - * 审核驳回 -> REJECTED (资金解冻) - * 5. 开始打款 -> PAYING - * 6. 打款完成 -> COMPLETED + * 3. blockchain-service 签名并广播 -> BROADCASTED + * 4. 链上确认 -> CONFIRMED * * 失败/取消时解冻资金 */ @@ -58,44 +19,20 @@ export class WithdrawalOrder { private readonly _orderNo: string; private readonly _accountSequence: string; private readonly _userId: UserId; - private readonly _amount: Money; // 提现金额 (绿积分, 1:1人民币) - private readonly _fee: Money; // 手续费 - private readonly _withdrawalType: WithdrawalType; - - // 收款方式 - private readonly _paymentMethod: PaymentMethod; - // 银行卡信息 - private readonly _bankName: string | null; - private readonly _bankCardNo: string | null; - private readonly _cardHolderName: string | null; - // 支付宝信息 - private readonly _alipayAccount: string | null; - private readonly _alipayRealName: string | null; - // 微信信息 - private readonly _wechatAccount: string | null; - private readonly _wechatRealName: string | null; - - // 状态 + private readonly _amount: Money; + private readonly _fee: Money; // 手续费 + private readonly _chainType: ChainType; + private readonly _toAddress: string; // 提现目标地址 + private _txHash: string | null; + // 内部转账标识 + private readonly _isInternalTransfer: boolean; // 是否为内部转账(ID转ID) + private readonly _toAccountSequence: string | null; // 接收方ID(内部转账时有值) + private readonly _toUserId: UserId | null; // 接收方用户ID(内部转账时有值) private _status: WithdrawalStatus; private _errorMessage: string | null; - - // 审核信息 - private _reviewedBy: string | null; - private _reviewedAt: Date | null; - private _reviewRemark: string | null; - - // 打款信息 - private _paidBy: string | null; - private _paidAt: Date | null; - private _paymentProof: string | null; - private _paymentRemark: string | null; - - // 详细备注 - private _detailMemo: string | null; - - // 时间戳 private _frozenAt: Date | null; - private _completedAt: Date | null; + private _broadcastedAt: Date | null; + private _confirmedAt: Date | null; private readonly _createdAt: Date; private constructor( @@ -105,27 +42,17 @@ export class WithdrawalOrder { userId: UserId, amount: Money, fee: Money, - withdrawalType: WithdrawalType, - paymentMethod: PaymentMethod, - bankName: string | null, - bankCardNo: string | null, - cardHolderName: string | null, - alipayAccount: string | null, - alipayRealName: string | null, - wechatAccount: string | null, - wechatRealName: string | null, + chainType: ChainType, + toAddress: string, + txHash: string | null, + isInternalTransfer: boolean, + toAccountSequence: string | null, + toUserId: UserId | null, status: WithdrawalStatus, errorMessage: string | null, - reviewedBy: string | null, - reviewedAt: Date | null, - reviewRemark: string | null, - paidBy: string | null, - paidAt: Date | null, - paymentProof: string | null, - paymentRemark: string | null, - detailMemo: string | null, frozenAt: Date | null, - completedAt: Date | null, + broadcastedAt: Date | null, + confirmedAt: Date | null, createdAt: Date, ) { this._id = id; @@ -134,27 +61,17 @@ export class WithdrawalOrder { this._userId = userId; this._amount = amount; this._fee = fee; - this._withdrawalType = withdrawalType; - this._paymentMethod = paymentMethod; - this._bankName = bankName; - this._bankCardNo = bankCardNo; - this._cardHolderName = cardHolderName; - this._alipayAccount = alipayAccount; - this._alipayRealName = alipayRealName; - this._wechatAccount = wechatAccount; - this._wechatRealName = wechatRealName; + this._chainType = chainType; + this._toAddress = toAddress; + this._txHash = txHash; + this._isInternalTransfer = isInternalTransfer; + this._toAccountSequence = toAccountSequence; + this._toUserId = toUserId; this._status = status; this._errorMessage = errorMessage; - this._reviewedBy = reviewedBy; - this._reviewedAt = reviewedAt; - this._reviewRemark = reviewRemark; - this._paidBy = paidBy; - this._paidAt = paidAt; - this._paymentProof = paymentProof; - this._paymentRemark = paymentRemark; - this._detailMemo = detailMemo; this._frozenAt = frozenAt; - this._completedAt = completedAt; + this._broadcastedAt = broadcastedAt; + this._confirmedAt = confirmedAt; this._createdAt = createdAt; } @@ -165,43 +82,28 @@ export class WithdrawalOrder { get userId(): UserId { return this._userId; } get amount(): Money { return this._amount; } get fee(): Money { return this._fee; } - get netAmount(): Money { return Money.USDT(new Decimal(this._amount.value).minus(this._fee.value)); } - get withdrawalType(): WithdrawalType { return this._withdrawalType; } - get paymentMethod(): PaymentMethod { return this._paymentMethod; } - get bankName(): string | null { return this._bankName; } - get bankCardNo(): string | null { return this._bankCardNo; } - get cardHolderName(): string | null { return this._cardHolderName; } - get alipayAccount(): string | null { return this._alipayAccount; } - get alipayRealName(): string | null { return this._alipayRealName; } - get wechatAccount(): string | null { return this._wechatAccount; } - get wechatRealName(): string | null { return this._wechatRealName; } + get netAmount(): Money { return this._amount; } // 接收方收到完整金额,手续费由发送方额外承担 + get chainType(): ChainType { return this._chainType; } + get toAddress(): string { return this._toAddress; } + get txHash(): string | null { return this._txHash; } + get isInternalTransfer(): boolean { return this._isInternalTransfer; } + get toAccountSequence(): string | null { return this._toAccountSequence; } + get toUserId(): UserId | null { return this._toUserId; } get status(): WithdrawalStatus { return this._status; } get errorMessage(): string | null { return this._errorMessage; } - get reviewedBy(): string | null { return this._reviewedBy; } - get reviewedAt(): Date | null { return this._reviewedAt; } - get reviewRemark(): string | null { return this._reviewRemark; } - get paidBy(): string | null { return this._paidBy; } - get paidAt(): Date | null { return this._paidAt; } - get paymentProof(): string | null { return this._paymentProof; } - get paymentRemark(): string | null { return this._paymentRemark; } - get detailMemo(): string | null { return this._detailMemo; } get frozenAt(): Date | null { return this._frozenAt; } - get completedAt(): Date | null { return this._completedAt; } + get broadcastedAt(): Date | null { return this._broadcastedAt; } + get confirmedAt(): Date | null { return this._confirmedAt; } get createdAt(): Date { return this._createdAt; } - // 状态判断 get isPending(): boolean { return this._status === WithdrawalStatus.PENDING; } get isFrozen(): boolean { return this._status === WithdrawalStatus.FROZEN; } - get isReviewing(): boolean { return this._status === WithdrawalStatus.REVIEWING; } - get isApproved(): boolean { return this._status === WithdrawalStatus.APPROVED; } - get isPaying(): boolean { return this._status === WithdrawalStatus.PAYING; } - get isCompleted(): boolean { return this._status === WithdrawalStatus.COMPLETED; } - get isRejected(): boolean { return this._status === WithdrawalStatus.REJECTED; } + get isBroadcasted(): boolean { return this._status === WithdrawalStatus.BROADCASTED; } + get isConfirmed(): boolean { return this._status === WithdrawalStatus.CONFIRMED; } get isFailed(): boolean { return this._status === WithdrawalStatus.FAILED; } get isCancelled(): boolean { return this._status === WithdrawalStatus.CANCELLED; } get isFinished(): boolean { - return this._status === WithdrawalStatus.COMPLETED || - this._status === WithdrawalStatus.REJECTED || + return this._status === WithdrawalStatus.CONFIRMED || this._status === WithdrawalStatus.FAILED || this._status === WithdrawalStatus.CANCELLED; } @@ -216,131 +118,58 @@ export class WithdrawalOrder { } /** - * 获取收款账户显示信息 - */ - getPaymentAccountDisplay(): string { - switch (this._paymentMethod) { - case PaymentMethod.BANK_CARD: - const maskedCardNo = this._bankCardNo - ? `****${this._bankCardNo.slice(-4)}` - : ''; - return `${this._bankName || ''} ${maskedCardNo} (${this._cardHolderName || ''})`; - case PaymentMethod.ALIPAY: - return `支付宝: ${this._alipayAccount || ''} (${this._alipayRealName || ''})`; - case PaymentMethod.WECHAT: - return `微信: ${this._wechatAccount || ''} (${this._wechatRealName || ''})`; - default: - return '未知收款方式'; - } - } - - /** - * 获取收款方式中文名称 - */ - getPaymentMethodName(): string { - switch (this._paymentMethod) { - case PaymentMethod.BANK_CARD: - return '银行卡'; - case PaymentMethod.ALIPAY: - return '支付宝'; - case PaymentMethod.WECHAT: - return '微信'; - default: - return '未知'; - } - } - - /** - * 创建提现订单 (法币提现) + * 创建提现订单 */ static create(params: { accountSequence: string; userId: UserId; amount: Money; fee: Money; - paymentAccount: PaymentAccountInfo; + chainType: ChainType; + toAddress: string; + isInternalTransfer?: boolean; + toAccountSequence?: string; + toUserId?: UserId; }): WithdrawalOrder { // 验证金额 if (params.amount.value <= 0) { - throw new DomainError('提现金额必须大于0'); + throw new DomainError('Withdrawal amount must be positive'); } // 验证手续费 if (params.fee.value < 0) { - throw new DomainError('手续费不能为负数'); + throw new DomainError('Withdrawal fee cannot be negative'); } // 验证净额大于0 - const netAmount = new Decimal(params.amount.value).minus(params.fee.value); - if (netAmount.lte(0)) { - throw new DomainError('提现金额必须大于手续费'); + if (params.amount.value <= params.fee.value) { + throw new DomainError('Withdrawal amount must be greater than fee'); } - // 根据收款方式验证必填信息 - const { paymentAccount } = params; - switch (paymentAccount.paymentMethod) { - case PaymentMethod.BANK_CARD: - if (!paymentAccount.bankName || !paymentAccount.bankCardNo || !paymentAccount.cardHolderName) { - throw new DomainError('银行卡信息不完整'); - } - break; - case PaymentMethod.ALIPAY: - if (!paymentAccount.alipayAccount || !paymentAccount.alipayRealName) { - throw new DomainError('支付宝信息不完整'); - } - break; - case PaymentMethod.WECHAT: - if (!paymentAccount.wechatAccount || !paymentAccount.wechatRealName) { - throw new DomainError('微信信息不完整'); - } - break; - default: - throw new DomainError('不支持的收款方式'); + // 验证地址格式 (简单的EVM地址检查) + if (!params.toAddress.match(/^0x[a-fA-F0-9]{40}$/)) { + throw new DomainError('Invalid withdrawal address format'); } - const now = new Date(); - const orderNo = this.generateOrderNo(); - - // 构建初始备注 - const detailMemo = [ - `[${now.toLocaleString('zh-CN')}] 用户发起提现申请`, - ` 订单号: ${orderNo}`, - ` 提现金额: ${params.amount.value} 绿积分`, - ` 手续费: ${params.fee.value} 绿积分`, - ` 实际到账: ${netAmount.toFixed(2)} 元人民币`, - ` 收款方式: ${paymentAccount.paymentMethod === PaymentMethod.BANK_CARD ? '银行卡' : - paymentAccount.paymentMethod === PaymentMethod.ALIPAY ? '支付宝' : '微信'}`, - ].join('\n'); - return new WithdrawalOrder( BigInt(0), // Will be set by database - orderNo, + this.generateOrderNo(), params.accountSequence, params.userId, params.amount, params.fee, - WithdrawalType.FIAT, - paymentAccount.paymentMethod, - paymentAccount.bankName || null, - paymentAccount.bankCardNo || null, - paymentAccount.cardHolderName || null, - paymentAccount.alipayAccount || null, - paymentAccount.alipayRealName || null, - paymentAccount.wechatAccount || null, - paymentAccount.wechatRealName || null, + params.chainType, + params.toAddress, + null, + params.isInternalTransfer ?? false, + params.toAccountSequence ?? null, + params.toUserId ?? null, WithdrawalStatus.PENDING, null, null, null, null, - null, - null, - null, - null, - detailMemo, - null, - null, - now, + new Date(), ); } @@ -354,27 +183,17 @@ export class WithdrawalOrder { userId: bigint; amount: Decimal; fee: Decimal; - withdrawalType?: string; - paymentMethod?: string; - bankName?: string | null; - bankCardNo?: string | null; - cardHolderName?: string | null; - alipayAccount?: string | null; - alipayRealName?: string | null; - wechatAccount?: string | null; - wechatRealName?: string | null; + chainType: string; + toAddress: string; + txHash: string | null; + isInternalTransfer: boolean; + toAccountSequence: string | null; + toUserId: bigint | null; status: string; - errorMessage?: string | null; - reviewedBy?: string | null; - reviewedAt?: Date | null; - reviewRemark?: string | null; - paidBy?: string | null; - paidAt?: Date | null; - paymentProof?: string | null; - paymentRemark?: string | null; - detailMemo?: string | null; - frozenAt?: Date | null; - completedAt?: Date | null; + errorMessage: string | null; + frozenAt: Date | null; + broadcastedAt: Date | null; + confirmedAt: Date | null; createdAt: Date; }): WithdrawalOrder { return new WithdrawalOrder( @@ -384,123 +203,53 @@ export class WithdrawalOrder { UserId.create(params.userId), Money.USDT(params.amount), Money.USDT(params.fee), - (params.withdrawalType as WithdrawalType) || WithdrawalType.FIAT, - (params.paymentMethod as PaymentMethod) || PaymentMethod.BANK_CARD, - params.bankName || null, - params.bankCardNo || null, - params.cardHolderName || null, - params.alipayAccount || null, - params.alipayRealName || null, - params.wechatAccount || null, - params.wechatRealName || null, + params.chainType as ChainType, + params.toAddress, + params.txHash, + params.isInternalTransfer, + params.toAccountSequence, + params.toUserId ? UserId.create(params.toUserId) : null, params.status as WithdrawalStatus, - params.errorMessage || null, - params.reviewedBy || null, - params.reviewedAt || null, - params.reviewRemark || null, - params.paidBy || null, - params.paidAt || null, - params.paymentProof || null, - params.paymentRemark || null, - params.detailMemo || null, - params.frozenAt || null, - params.completedAt || null, + params.errorMessage, + params.frozenAt, + params.broadcastedAt, + params.confirmedAt, params.createdAt, ); } - /** - * 添加备注 - */ - private appendMemo(message: string): void { - const now = new Date(); - const newLine = `[${now.toLocaleString('zh-CN')}] ${message}`; - this._detailMemo = this._detailMemo - ? `${this._detailMemo}\n${newLine}` - : newLine; - } - /** * 标记为已冻结 (资金已从可用余额冻结) */ markAsFrozen(): void { if (this._status !== WithdrawalStatus.PENDING) { - throw new DomainError('只有待处理的提现订单可以冻结资金'); + throw new DomainError('Only pending withdrawals can be frozen'); } this._status = WithdrawalStatus.FROZEN; this._frozenAt = new Date(); - this.appendMemo('资金已冻结,等待审核'); } /** - * 提交审核 + * 标记为已广播 */ - submitForReview(): void { + markAsBroadcasted(txHash: string): void { if (this._status !== WithdrawalStatus.FROZEN) { - throw new DomainError('只有已冻结的提现订单可以提交审核'); + throw new DomainError('Only frozen withdrawals can be broadcasted'); } - this._status = WithdrawalStatus.REVIEWING; - this.appendMemo('已提交审核'); + this._status = WithdrawalStatus.BROADCASTED; + this._txHash = txHash; + this._broadcastedAt = new Date(); } /** - * 审核通过 + * 标记为已确认 (链上确认) */ - approve(reviewedBy: string, remark?: string): void { - if (this._status !== WithdrawalStatus.REVIEWING && this._status !== WithdrawalStatus.FROZEN) { - throw new DomainError('只有审核中或已冻结的订单可以通过审核'); + markAsConfirmed(): void { + if (this._status !== WithdrawalStatus.BROADCASTED) { + throw new DomainError('Only broadcasted withdrawals can be confirmed'); } - this._status = WithdrawalStatus.APPROVED; - this._reviewedBy = reviewedBy; - this._reviewedAt = new Date(); - this._reviewRemark = remark || null; - this.appendMemo(`审核通过 - 审核人: ${reviewedBy}${remark ? `, 备注: ${remark}` : ''}`); - } - - /** - * 审核驳回 - */ - reject(reviewedBy: string, remark: string): void { - if (this._status !== WithdrawalStatus.REVIEWING && this._status !== WithdrawalStatus.FROZEN) { - throw new DomainError('只有审核中或已冻结的订单可以驳回'); - } - this._status = WithdrawalStatus.REJECTED; - this._reviewedBy = reviewedBy; - this._reviewedAt = new Date(); - this._reviewRemark = remark; - this.appendMemo(`审核驳回 - 审核人: ${reviewedBy}, 原因: ${remark}`); - } - - /** - * 开始打款 - */ - startPayment(paidBy: string): void { - if (this._status !== WithdrawalStatus.APPROVED) { - throw new DomainError('只有审核通过的订单可以开始打款'); - } - this._status = WithdrawalStatus.PAYING; - this._paidBy = paidBy; - this.appendMemo(`开始打款 - 操作人: ${paidBy}`); - } - - /** - * 完成打款 - */ - completePayment(paymentProof?: string, remark?: string): void { - if (this._status !== WithdrawalStatus.PAYING) { - throw new DomainError('只有打款中的订单可以完成打款'); - } - this._status = WithdrawalStatus.COMPLETED; - this._paidAt = new Date(); - this._completedAt = new Date(); - this._paymentProof = paymentProof || null; - this._paymentRemark = remark || null; - - const proofInfo = paymentProof ? `, 凭证: ${paymentProof}` : ''; - const remarkInfo = remark ? `, 备注: ${remark}` : ''; - this.appendMemo(`打款完成${proofInfo}${remarkInfo}`); - this.appendMemo(` 收款账户: ${this.getPaymentAccountDisplay()}`); - this.appendMemo(` 到账金额: ${this.netAmount.value.toFixed(2)} 元人民币`); + this._status = WithdrawalStatus.CONFIRMED; + this._confirmedAt = new Date(); } /** @@ -508,31 +257,27 @@ export class WithdrawalOrder { */ markAsFailed(errorMessage: string): void { if (this.isFinished) { - throw new DomainError('已完成的提现订单无法标记为失败'); + throw new DomainError('Cannot fail a finished withdrawal'); } this._status = WithdrawalStatus.FAILED; this._errorMessage = errorMessage; - this.appendMemo(`提现失败 - 原因: ${errorMessage}`); } /** * 取消提现 */ - cancel(reason?: string): void { + cancel(): void { if (this._status !== WithdrawalStatus.PENDING && this._status !== WithdrawalStatus.FROZEN) { - throw new DomainError('只有待处理或已冻结的提现订单可以取消'); + throw new DomainError('Only pending or frozen withdrawals can be cancelled'); } this._status = WithdrawalStatus.CANCELLED; - this.appendMemo(`用户取消提现${reason ? ` - 原因: ${reason}` : ''}`); } /** - * 是否需要解冻资金 (驳回/失败/取消且已冻结) + * 是否需要解冻资金 (失败或取消且已冻结) */ needsUnfreeze(): boolean { - return (this._status === WithdrawalStatus.REJECTED || - this._status === WithdrawalStatus.FAILED || - this._status === WithdrawalStatus.CANCELLED) + return (this._status === WithdrawalStatus.FAILED || this._status === WithdrawalStatus.CANCELLED) && this._frozenAt !== null; } } diff --git a/backend/services/wallet-service/src/domain/repositories/withdrawal-order.repository.interface.ts b/backend/services/wallet-service/src/domain/repositories/withdrawal-order.repository.interface.ts index 4b31dcee..ed116d9e 100644 --- a/backend/services/wallet-service/src/domain/repositories/withdrawal-order.repository.interface.ts +++ b/backend/services/wallet-service/src/domain/repositories/withdrawal-order.repository.interface.ts @@ -8,10 +8,7 @@ export interface IWithdrawalOrderRepository { findByUserId(userId: bigint, status?: WithdrawalStatus): Promise; findPendingOrders(): Promise; findFrozenOrders(): Promise; - // 法币提现相关查询 - findReviewingOrders(): Promise; - findApprovedOrders(): Promise; - findPayingOrders(): Promise; + findBroadcastedOrders(): Promise; } export const WITHDRAWAL_ORDER_REPOSITORY = Symbol('IWithdrawalOrderRepository'); diff --git a/backend/services/wallet-service/src/domain/value-objects/ledger-entry-type.enum.ts b/backend/services/wallet-service/src/domain/value-objects/ledger-entry-type.enum.ts index 12b624af..70443210 100644 --- a/backend/services/wallet-service/src/domain/value-objects/ledger-entry-type.enum.ts +++ b/backend/services/wallet-service/src/domain/value-objects/ledger-entry-type.enum.ts @@ -11,7 +11,6 @@ export enum LedgerEntryType { TRANSFER_TO_POOL = 'TRANSFER_TO_POOL', SWAP_EXECUTED = 'SWAP_EXECUTED', WITHDRAWAL = 'WITHDRAWAL', - WITHDRAWAL_FEE = 'WITHDRAWAL_FEE', // 提现手续费 TRANSFER_IN = 'TRANSFER_IN', TRANSFER_OUT = 'TRANSFER_OUT', FREEZE = 'FREEZE', diff --git a/backend/services/wallet-service/src/domain/value-objects/withdrawal-status.enum.ts b/backend/services/wallet-service/src/domain/value-objects/withdrawal-status.enum.ts index b6a81fd2..037b20e9 100644 --- a/backend/services/wallet-service/src/domain/value-objects/withdrawal-status.enum.ts +++ b/backend/services/wallet-service/src/domain/value-objects/withdrawal-status.enum.ts @@ -1,34 +1,8 @@ -/** - * 提现订单状态 (法币提现流程) - * - * 流程: PENDING -> FROZEN -> REVIEWING -> APPROVED -> PAYING -> COMPLETED - * -> REJECTED (驳回) - * -> FAILED (失败,退款) - */ export enum WithdrawalStatus { - PENDING = 'PENDING', // 待处理(用户刚提交) - FROZEN = 'FROZEN', // 已冻结资金,等待审核 - REVIEWING = 'REVIEWING', // 审核中 - APPROVED = 'APPROVED', // 审核通过,等待打款 - PAYING = 'PAYING', // 打款中 - COMPLETED = 'COMPLETED', // 已完成(打款成功) - REJECTED = 'REJECTED', // 审核驳回(资金已退回) - FAILED = 'FAILED', // 失败(资金已退回) - CANCELLED = 'CANCELLED', // 已取消(用户取消,资金已退回) -} - -/** - * 收款方式 - */ -export enum PaymentMethod { - BANK_CARD = 'BANK_CARD', // 银行卡 - ALIPAY = 'ALIPAY', // 支付宝 - WECHAT = 'WECHAT', // 微信支付 -} - -/** - * 提现类型 - */ -export enum WithdrawalType { - FIAT = 'FIAT', // 法币提现(人民币) + PENDING = 'PENDING', // 待处理 + FROZEN = 'FROZEN', // 已冻结资金,等待签名 + BROADCASTED = 'BROADCASTED', // 已广播到链上 + CONFIRMED = 'CONFIRMED', // 链上确认完成 + FAILED = 'FAILED', // 失败 + CANCELLED = 'CANCELLED', // 已取消 } diff --git a/backend/services/wallet-service/src/infrastructure/persistence/repositories/withdrawal-order.repository.impl.ts b/backend/services/wallet-service/src/infrastructure/persistence/repositories/withdrawal-order.repository.impl.ts index 49bbd619..c6bb4c58 100644 --- a/backend/services/wallet-service/src/infrastructure/persistence/repositories/withdrawal-order.repository.impl.ts +++ b/backend/services/wallet-service/src/infrastructure/persistence/repositories/withdrawal-order.repository.impl.ts @@ -16,33 +16,17 @@ export class WithdrawalOrderRepositoryImpl implements IWithdrawalOrderRepository userId: order.userId.value, amount: order.amount.toDecimal(), fee: order.fee.toDecimal(), - // 法币提现字段 - withdrawalType: order.withdrawalType, - paymentMethod: order.paymentMethod, - bankName: order.bankName, - bankCardNo: order.bankCardNo, - cardHolderName: order.cardHolderName, - alipayAccount: order.alipayAccount, - alipayRealName: order.alipayRealName, - wechatAccount: order.wechatAccount, - wechatRealName: order.wechatRealName, - // 状态 + chainType: order.chainType, + toAddress: order.toAddress, + txHash: order.txHash, + isInternalTransfer: order.isInternalTransfer, + toAccountSequence: order.toAccountSequence, + toUserId: order.toUserId?.value ?? null, status: order.status, errorMessage: order.errorMessage, - // 审核信息 - reviewedBy: order.reviewedBy, - reviewedAt: order.reviewedAt, - reviewRemark: order.reviewRemark, - // 打款信息 - paidBy: order.paidBy, - paidAt: order.paidAt, - paymentProof: order.paymentProof, - paymentRemark: order.paymentRemark, - // 详细备注 - detailMemo: order.detailMemo, - // 时间戳 frozenAt: order.frozenAt, - completedAt: order.completedAt, + broadcastedAt: order.broadcastedAt, + confirmedAt: order.confirmedAt, }; if (order.id === BigInt(0)) { @@ -84,19 +68,6 @@ export class WithdrawalOrderRepositoryImpl implements IWithdrawalOrderRepository return records.map(r => this.toDomain(r)); } - async findByAccountSequence(accountSequence: string, status?: WithdrawalStatus): Promise { - const where: Record = { accountSequence }; - if (status) { - where.status = status; - } - - const records = await this.prisma.withdrawalOrder.findMany({ - where, - orderBy: { createdAt: 'desc' }, - }); - return records.map(r => this.toDomain(r)); - } - async findPendingOrders(): Promise { const records = await this.prisma.withdrawalOrder.findMany({ where: { status: WithdrawalStatus.PENDING }, @@ -113,87 +84,14 @@ export class WithdrawalOrderRepositoryImpl implements IWithdrawalOrderRepository return records.map(r => this.toDomain(r)); } - /** - * 查找待审核的提现订单 - */ - async findReviewingOrders(): Promise { + async findBroadcastedOrders(): Promise { const records = await this.prisma.withdrawalOrder.findMany({ - where: { - status: { - in: [WithdrawalStatus.FROZEN, WithdrawalStatus.REVIEWING], - }, - }, + where: { status: WithdrawalStatus.BROADCASTED }, orderBy: { createdAt: 'asc' }, }); return records.map(r => this.toDomain(r)); } - /** - * 查找待打款的提现订单 - */ - async findApprovedOrders(): Promise { - const records = await this.prisma.withdrawalOrder.findMany({ - where: { status: WithdrawalStatus.APPROVED }, - orderBy: { createdAt: 'asc' }, - }); - return records.map(r => this.toDomain(r)); - } - - /** - * 查找打款中的提现订单 - */ - async findPayingOrders(): Promise { - const records = await this.prisma.withdrawalOrder.findMany({ - where: { status: WithdrawalStatus.PAYING }, - orderBy: { createdAt: 'asc' }, - }); - return records.map(r => this.toDomain(r)); - } - - /** - * 分页查询提现订单(管理后台使用) - */ - async findWithPagination(params: { - status?: WithdrawalStatus | WithdrawalStatus[]; - paymentMethod?: string; - accountSequence?: string; - page: number; - pageSize: number; - }): Promise<{ orders: WithdrawalOrder[]; total: number }> { - const where: Record = {}; - - if (params.status) { - if (Array.isArray(params.status)) { - where.status = { in: params.status }; - } else { - where.status = params.status; - } - } - - if (params.paymentMethod) { - where.paymentMethod = params.paymentMethod; - } - - if (params.accountSequence) { - where.accountSequence = params.accountSequence; - } - - const [records, total] = await Promise.all([ - this.prisma.withdrawalOrder.findMany({ - where, - orderBy: { createdAt: 'desc' }, - skip: (params.page - 1) * params.pageSize, - take: params.pageSize, - }), - this.prisma.withdrawalOrder.count({ where }), - ]); - - return { - orders: records.map(r => this.toDomain(r)), - total, - }; - } - private toDomain(record: { id: bigint; orderNo: string; @@ -201,37 +99,18 @@ export class WithdrawalOrderRepositoryImpl implements IWithdrawalOrderRepository userId: bigint; amount: Decimal; fee: Decimal; - withdrawalType: string | null; - paymentMethod: string | null; - bankName: string | null; - bankCardNo: string | null; - cardHolderName: string | null; - alipayAccount: string | null; - alipayRealName: string | null; - wechatAccount: string | null; - wechatRealName: string | null; + chainType: string; + toAddress: string; + txHash: string | null; + isInternalTransfer: boolean; + toAccountSequence: string | null; + toUserId: bigint | null; status: string; errorMessage: string | null; - reviewedBy: string | null; - reviewedAt: Date | null; - reviewRemark: string | null; - paidBy: string | null; - paidAt: Date | null; - paymentProof: string | null; - paymentRemark: string | null; - detailMemo: string | null; frozenAt: Date | null; - completedAt: Date | null; + broadcastedAt: Date | null; + confirmedAt: Date | null; createdAt: Date; - // 兼容旧字段 - chainType?: string | null; - toAddress?: string | null; - txHash?: string | null; - isInternalTransfer?: boolean; - toAccountSequence?: string | null; - toUserId?: bigint | null; - broadcastedAt?: Date | null; - confirmedAt?: Date | null; }): WithdrawalOrder { return WithdrawalOrder.reconstruct({ id: record.id, @@ -240,27 +119,17 @@ export class WithdrawalOrderRepositoryImpl implements IWithdrawalOrderRepository userId: record.userId, amount: new Decimal(record.amount.toString()), fee: new Decimal(record.fee.toString()), - withdrawalType: record.withdrawalType || undefined, - paymentMethod: record.paymentMethod || undefined, - bankName: record.bankName, - bankCardNo: record.bankCardNo, - cardHolderName: record.cardHolderName, - alipayAccount: record.alipayAccount, - alipayRealName: record.alipayRealName, - wechatAccount: record.wechatAccount, - wechatRealName: record.wechatRealName, + chainType: record.chainType, + toAddress: record.toAddress, + txHash: record.txHash, + isInternalTransfer: record.isInternalTransfer, + toAccountSequence: record.toAccountSequence, + toUserId: record.toUserId, status: record.status, errorMessage: record.errorMessage, - reviewedBy: record.reviewedBy, - reviewedAt: record.reviewedAt, - reviewRemark: record.reviewRemark, - paidBy: record.paidBy, - paidAt: record.paidAt, - paymentProof: record.paymentProof, - paymentRemark: record.paymentRemark, - detailMemo: record.detailMemo, frozenAt: record.frozenAt, - completedAt: record.completedAt, + broadcastedAt: record.broadcastedAt, + confirmedAt: record.confirmedAt, createdAt: record.createdAt, }); } diff --git a/frontend/admin-web/src/app/(dashboard)/withdrawals/page.tsx b/frontend/admin-web/src/app/(dashboard)/withdrawals/page.tsx deleted file mode 100644 index e04df258..00000000 --- a/frontend/admin-web/src/app/(dashboard)/withdrawals/page.tsx +++ /dev/null @@ -1,682 +0,0 @@ -'use client'; - -import { useState, useCallback } from 'react'; -import { Modal, toast, Button } from '@/components/common'; -import { PageContainer } from '@/components/layout'; -import { cn } from '@/utils/helpers'; -import { formatDateTime } from '@/utils/formatters'; -import { - useReviewingWithdrawals, - useApprovedWithdrawals, - usePayingWithdrawals, - useReviewWithdrawal, - useStartPayment, - useCompletePayment, -} from '@/hooks/useWithdrawals'; -import { - WithdrawalOrder, - getWithdrawalStatusInfo, - getPaymentMethodInfo, - formatAmount, - maskBankCardNo, - maskAccount, -} from '@/types/withdrawal.types'; -import styles from './withdrawals.module.scss'; - -type TabType = 'reviewing' | 'approved' | 'paying'; - -/** - * 提现审核管理页面 - */ -export default function WithdrawalsPage() { - const [activeTab, setActiveTab] = useState('reviewing'); - const [viewingOrder, setViewingOrder] = useState(null); - const [reviewingOrder, setReviewingOrder] = useState(null); - const [payingOrder, setPayingOrder] = useState(null); - const [completeOrder, setCompleteOrder] = useState(null); - - // 审核表单 - const [reviewForm, setReviewForm] = useState({ - approved: true, - remark: '', - }); - - // 完成打款表单 - const [completeForm, setCompleteForm] = useState({ - paymentProof: '', - remark: '', - }); - - // 数据查询 - const { - data: reviewingData, - isLoading: reviewingLoading, - error: reviewingError, - refetch: refetchReviewing, - } = useReviewingWithdrawals(); - - const { - data: approvedData, - isLoading: approvedLoading, - error: approvedError, - refetch: refetchApproved, - } = useApprovedWithdrawals(); - - const { - data: payingData, - isLoading: payingLoading, - error: payingError, - refetch: refetchPaying, - } = usePayingWithdrawals(); - - // Mutations - const reviewMutation = useReviewWithdrawal(); - const startPaymentMutation = useStartPayment(); - const completePaymentMutation = useCompletePayment(); - - // 获取当前 Tab 数据 - const getCurrentData = () => { - switch (activeTab) { - case 'reviewing': - return { data: reviewingData, loading: reviewingLoading, error: reviewingError, refetch: refetchReviewing }; - case 'approved': - return { data: approvedData, loading: approvedLoading, error: approvedError, refetch: refetchApproved }; - case 'paying': - return { data: payingData, loading: payingLoading, error: payingError, refetch: refetchPaying }; - } - }; - - const { data: currentData, loading, error, refetch } = getCurrentData(); - - // 刷新所有数据 - const handleRefreshAll = useCallback(() => { - refetchReviewing(); - refetchApproved(); - refetchPaying(); - toast.success('数据已刷新'); - }, [refetchReviewing, refetchApproved, refetchPaying]); - - // 打开审核弹窗 - const handleOpenReview = (order: WithdrawalOrder) => { - setReviewingOrder(order); - setReviewForm({ approved: true, remark: '' }); - }; - - // 提交审核 - const handleSubmitReview = async () => { - if (!reviewingOrder) return; - - try { - await reviewMutation.mutateAsync({ - orderNo: reviewingOrder.orderNo, - data: { - approved: reviewForm.approved, - reviewedBy: 'admin', // TODO: 从登录用户获取 - remark: reviewForm.remark || undefined, - }, - }); - toast.success(reviewForm.approved ? '审核通过' : '已拒绝提现'); - setReviewingOrder(null); - } catch (err) { - toast.error((err as Error).message || '操作失败'); - } - }; - - // 开始打款 - const handleStartPayment = async (order: WithdrawalOrder) => { - setPayingOrder(order); - try { - await startPaymentMutation.mutateAsync({ - orderNo: order.orderNo, - data: { - paidBy: 'admin', // TODO: 从登录用户获取 - }, - }); - toast.success('已开始打款'); - setPayingOrder(null); - } catch (err) { - toast.error((err as Error).message || '操作失败'); - setPayingOrder(null); - } - }; - - // 打开完成打款弹窗 - const handleOpenComplete = (order: WithdrawalOrder) => { - setCompleteOrder(order); - setCompleteForm({ paymentProof: '', remark: '' }); - }; - - // 完成打款 - const handleSubmitComplete = async () => { - if (!completeOrder) return; - - try { - await completePaymentMutation.mutateAsync({ - orderNo: completeOrder.orderNo, - data: { - paymentProof: completeForm.paymentProof || undefined, - remark: completeForm.remark || undefined, - }, - }); - toast.success('打款完成'); - setCompleteOrder(null); - } catch (err) { - toast.error((err as Error).message || '操作失败'); - } - }; - - // 获取收款账户显示信息 - const getPaymentAccountDisplay = (order: WithdrawalOrder) => { - switch (order.paymentMethod) { - case 'BANK_CARD': - return { - label: order.bankName || '银行卡', - account: order.bankCardNo ? maskBankCardNo(order.bankCardNo) : '-', - name: order.cardHolderName, - }; - case 'ALIPAY': - return { - label: '支付宝', - account: order.alipayAccount ? maskAccount(order.alipayAccount) : '-', - name: order.alipayRealName, - }; - case 'WECHAT': - return { - label: '微信', - account: order.wechatAccount ? maskAccount(order.wechatAccount) : '-', - name: order.wechatRealName, - }; - default: - return { label: '-', account: '-', name: '-' }; - } - }; - - // 渲染表格行 - const renderTableRow = (order: WithdrawalOrder) => { - const statusInfo = getWithdrawalStatusInfo(order.status); - const methodInfo = getPaymentMethodInfo(order.paymentMethod); - const accountInfo = getPaymentAccountDisplay(order); - - return ( - - - {order.orderNo} - - -
- {order.accountSequence} - ID: {order.userId} -
- - -
- {formatAmount(order.amount)} - (手续费: {formatAmount(order.fee)}) -
- - -
- {methodInfo.icon} -
- {accountInfo.label} - {accountInfo.account} -
-
- - - - {statusInfo.label} - - - {formatDateTime(order.createdAt)} - -
- - {activeTab === 'reviewing' && ( - - )} - {activeTab === 'approved' && ( - - )} - {activeTab === 'paying' && ( - - )} -
- - - ); - }; - - return ( - -
- {/* 页面标题 */} -
-

提现审核管理

-

- 审核用户提现申请,处理打款操作 -

-
- - {/* Tab 切换 */} -
- - - -
- - {/* 主内容卡片 */} -
-
- - 共 {currentData?.length ?? 0} 条记录 - - -
- - {/* 表格 */} - {loading ? ( -
加载中...
- ) : error ? ( -
- {(error as Error).message || '加载失败'} - -
- ) : !currentData || currentData.length === 0 ? ( -
- - {activeTab === 'reviewing' ? '📋' : activeTab === 'approved' ? '💰' : '⏳'} - - - {activeTab === 'reviewing' - ? '暂无待审核的提现订单' - : activeTab === 'approved' - ? '暂无待打款的提现订单' - : '暂无打款中的提现订单'} - -
- ) : ( - - - - - - - - - - - - - {currentData.map(renderTableRow)} -
订单号用户金额 (绿积分)收款方式状态申请时间操作
- )} -
- - {/* 详情弹窗 */} - setViewingOrder(null)} - footer={ -
- -
- } - width={700} - > - {viewingOrder && ( -
- {/* 基本信息 */} -
-

基本信息

-
-
- 订单号: - - {viewingOrder.orderNo} - -
-
- 状态: - - {getWithdrawalStatusInfo(viewingOrder.status).label} - -
-
- 用户: - - {viewingOrder.accountSequence} (ID: {viewingOrder.userId}) - -
-
- 申请时间: - {formatDateTime(viewingOrder.createdAt)} -
-
-
- - {/* 金额信息 */} -
-

金额信息

-
-
- 提现金额: - - {formatAmount(viewingOrder.amount)} 绿积分 - -
-
- 手续费: - {formatAmount(viewingOrder.fee)} 绿积分 -
-
- 实际到账: - {formatAmount(viewingOrder.netAmount)} 元 -
-
-
- - {/* 收款信息 */} -
-

收款信息

-
-
- 收款方式: - - {getPaymentMethodInfo(viewingOrder.paymentMethod).icon}{' '} - {getPaymentMethodInfo(viewingOrder.paymentMethod).label} - -
- {viewingOrder.paymentMethod === 'BANK_CARD' && ( - <> -
- 开户行: - {viewingOrder.bankName || '-'} -
-
- 卡号: - - {viewingOrder.bankCardNo || '-'} - -
-
- 持卡人: - {viewingOrder.cardHolderName || '-'} -
- - )} - {viewingOrder.paymentMethod === 'ALIPAY' && ( - <> -
- 支付宝账号: - {viewingOrder.alipayAccount || '-'} -
-
- 真实姓名: - {viewingOrder.alipayRealName || '-'} -
- - )} - {viewingOrder.paymentMethod === 'WECHAT' && ( - <> -
- 微信账号: - {viewingOrder.wechatAccount || '-'} -
-
- 真实姓名: - {viewingOrder.wechatRealName || '-'} -
- - )} -
-
- - {/* 处理记录 */} - {(viewingOrder.reviewedBy || viewingOrder.paidBy) && ( -
-

处理记录

-
- {viewingOrder.reviewedBy && ( -
-
- 审核{viewingOrder.status === 'REJECTED' ? '拒绝' : '通过'} -
-
- 审核人: {viewingOrder.reviewedBy} - {viewingOrder.reviewedAt && ` | 时间: ${formatDateTime(viewingOrder.reviewedAt)}`} - {viewingOrder.reviewRemark && ` | 备注: ${viewingOrder.reviewRemark}`} -
-
- )} - {viewingOrder.paidBy && ( -
-
- {viewingOrder.status === 'COMPLETED' ? '打款完成' : '打款中'} -
-
- 操作人: {viewingOrder.paidBy} - {viewingOrder.paidAt && ` | 时间: ${formatDateTime(viewingOrder.paidAt)}`} - {viewingOrder.paymentRemark && ` | 备注: ${viewingOrder.paymentRemark}`} -
-
- )} -
-
- )} -
- )} -
- - {/* 审核弹窗 */} - setReviewingOrder(null)} - footer={ -
- - -
- } - width={500} - > - {reviewingOrder && ( -
-
- 订单号: - {reviewingOrder.orderNo} -
-
- 用户: - {reviewingOrder.accountSequence} -
-
- 金额: - - {formatAmount(reviewingOrder.amount)} 绿积分 - -
- -
- -
- - -
-
- -
- -