feat(identity/blockchain): 添加助记词挂失功能

后端:
- blockchain-service: 新增 revokeMnemonic() 方法和 POST /internal/mnemonic/revoke API
- identity-service: 新增 POST /user/mnemonic/revoke 用户端API
- 挂失后助记词状态变为 REVOKED,无法用于账户恢复

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-16 07:56:27 -08:00
parent 6fb18c6ef2
commit d3e680ea14
10 changed files with 160 additions and 2 deletions

View File

@ -3,7 +3,7 @@ import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { AddressDerivationService } from '@/application/services/address-derivation.service';
import { MnemonicVerificationService } from '@/application/services/mnemonic-verification.service';
import { MnemonicDerivationAdapter } from '@/infrastructure/blockchain';
import { DeriveAddressDto, VerifyMnemonicDto, VerifyMnemonicHashDto, MarkMnemonicBackupDto } from '../dto/request';
import { DeriveAddressDto, VerifyMnemonicDto, VerifyMnemonicHashDto, MarkMnemonicBackupDto, RevokeMnemonicDto } from '../dto/request';
import { DeriveAddressResponseDto } from '../dto/response';
/**
@ -102,4 +102,12 @@ export class InternalController {
message: 'Mnemonic marked as backed up',
};
}
@Post('mnemonic/revoke')
@ApiOperation({ summary: '挂失助记词' })
@ApiResponse({ status: 200, description: '挂失结果' })
async revokeMnemonic(@Body() dto: RevokeMnemonicDto) {
const result = await this.mnemonicVerification.revokeMnemonic(dto.accountSequence, dto.reason);
return result;
}
}

View File

@ -3,3 +3,4 @@ export * from './derive-address.dto';
export * from './verify-mnemonic.dto';
export * from './verify-mnemonic-hash.dto';
export * from './mark-mnemonic-backup.dto';
export * from './revoke-mnemonic.dto';

View File

@ -0,0 +1,15 @@
import { IsString, IsNotEmpty, MaxLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class RevokeMnemonicDto {
@ApiProperty({ example: 'D2512110001', description: '账户序列号' })
@IsString()
@IsNotEmpty()
accountSequence: string;
@ApiProperty({ example: '助记词泄露', description: '挂失原因' })
@IsString()
@IsNotEmpty()
@MaxLength(200)
reason: string;
}

View File

@ -100,4 +100,57 @@ export class MnemonicVerificationService {
});
this.logger.log(`Recovery mnemonic marked as backed up for account ${accountSequence}`);
}
/**
*
* ACTIVE REVOKED
*/
async revokeMnemonic(accountSequence: string, reason: string): Promise<{ success: boolean; message: string }> {
this.logger.log(`Revoking mnemonic for account ${accountSequence}, reason: ${reason}`);
// 查找 ACTIVE 状态的助记词
const activeRecord = await this.prisma.recoveryMnemonic.findFirst({
where: {
accountSequence,
status: 'ACTIVE',
},
});
if (!activeRecord) {
this.logger.warn(`No active mnemonic found for account ${accountSequence}`);
return {
success: false,
message: '该账户没有可挂失的助记词',
};
}
// 更新状态为 REVOKED
await this.prisma.recoveryMnemonic.update({
where: { id: activeRecord.id },
data: {
status: 'REVOKED',
revokedAt: new Date(),
revokedReason: reason,
},
});
this.logger.log(`Mnemonic revoked successfully for account ${accountSequence}`);
return {
success: true,
message: '助记词已挂失',
};
}
/**
*
*/
async isMnemonicRevoked(accountSequence: string): Promise<boolean> {
const revokedRecord = await this.prisma.recoveryMnemonic.findFirst({
where: {
accountSequence,
status: 'REVOKED',
},
});
return !!revokedRecord;
}
}

View File

@ -17,7 +17,7 @@ import {
import {
AutoCreateAccountDto, RecoverByMnemonicDto, RecoverByPhoneDto, AutoLoginDto,
SendSmsCodeDto, RegisterDto, LoginDto, BindPhoneDto, UpdateProfileDto,
BindWalletDto, SubmitKYCDto, RemoveDeviceDto,
BindWalletDto, SubmitKYCDto, RemoveDeviceDto, RevokeMnemonicDto,
AutoCreateAccountResponseDto, RecoverAccountResponseDto, LoginResponseDto,
UserProfileResponseDto, DeviceResponseDto,
WalletStatusReadyResponseDto, WalletStatusGeneratingResponseDto,
@ -198,6 +198,14 @@ export class UserAccountController {
return { message: '已标记为已备份' };
}
@Post('mnemonic/revoke')
@ApiBearerAuth()
@ApiOperation({ summary: '挂失助记词', description: '用户主动挂失助记词,挂失后该助记词将无法用于账户恢复' })
@ApiResponse({ status: 200, description: '挂失结果' })
async revokeMnemonic(@CurrentUser() user: CurrentUserData, @Body() dto: RevokeMnemonicDto) {
return this.userService.revokeMnemonic(user.userId, dto.reason);
}
@Post('upload-avatar')
@ApiBearerAuth()
@ApiOperation({ summary: '上传用户头像' })

View File

@ -3,3 +3,4 @@ export * from './recover-by-mnemonic.dto';
export * from './recover-by-phone.dto';
export * from './bind-phone.dto';
export * from './submit-kyc.dto';
export * from './revoke-mnemonic.dto';

View File

@ -0,0 +1,10 @@
import { IsString, IsNotEmpty, MaxLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class RevokeMnemonicDto {
@ApiProperty({ example: '助记词泄露', description: '挂失原因' })
@IsString()
@IsNotEmpty({ message: '请填写挂失原因' })
@MaxLength(200, { message: '挂失原因不能超过200字' })
reason: string;
}

View File

@ -0,0 +1,7 @@
export class RevokeMnemonicCommand {
constructor(
public readonly userId: string,
public readonly accountSequence: string,
public readonly reason: string,
) {}
}

View File

@ -775,4 +775,33 @@ export class UserApplicationService {
}
}
}
// ============ 助记词挂失相关 ============
/**
* (POST /user/mnemonic/revoke)
*
*
*
*/
async revokeMnemonic(userId: string, reason: string): Promise<{ success: boolean; message: string }> {
this.logger.log(`[REVOKE] Revoking mnemonic for user: ${userId}, reason: ${reason}`);
// 1. 获取用户的 accountSequence
const account = await this.userRepository.findById(UserId.create(userId));
if (!account) {
this.logger.warn(`[REVOKE] User not found: ${userId}`);
return { success: false, message: '用户不存在' };
}
// 2. 调用 blockchain-service 挂失助记词
try {
const result = await this.blockchainClient.revokeMnemonic(account.accountSequence.value, reason);
this.logger.log(`[REVOKE] Mnemonic revoke result: ${JSON.stringify(result)}`);
return result;
} catch (error) {
this.logger.error(`[REVOKE] Failed to revoke mnemonic`, error);
return { success: false, message: '挂失失败,请稍后重试' };
}
}
}

View File

@ -165,4 +165,30 @@ export class BlockchainClientService {
throw error;
}
}
/**
*
*/
async revokeMnemonic(accountSequence: string, reason: string): Promise<{ success: boolean; message: string }> {
this.logger.log(`Revoking mnemonic for account ${accountSequence}, reason: ${reason}`);
try {
const response = await firstValueFrom(
this.httpService.post<{ success: boolean; message: string }>(
`${this.blockchainServiceUrl}/internal/mnemonic/revoke`,
{ accountSequence, reason },
{
headers: { 'Content-Type': 'application/json' },
timeout: 30000,
},
),
);
this.logger.log(`Mnemonic revoke result: success=${response.data.success}`);
return response.data;
} catch (error) {
this.logger.error('Failed to revoke mnemonic', error);
throw error;
}
}
}