rwadurian/backend/services/identity-service/src/domain/aggregates/user-account/user-account.aggregate.ts

524 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { DomainError } from '@/shared/exceptions/domain.exception';
import {
UserId,
AccountSequence,
PhoneNumber,
ReferralCode,
DeviceInfo,
ChainType,
KYCInfo,
KYCStatus,
AccountStatus,
} from '@/domain/value-objects';
import { WalletAddress } from '@/domain/entities/wallet-address.entity';
import {
DomainEvent,
UserAccountAutoCreatedEvent,
UserAccountCreatedEvent,
DeviceAddedEvent,
DeviceRemovedEvent,
PhoneNumberBoundEvent,
WalletAddressBoundEvent,
MultipleWalletAddressesBoundEvent,
KYCSubmittedEvent,
KYCVerifiedEvent,
KYCRejectedEvent,
UserAccountFrozenEvent,
UserAccountDeactivatedEvent,
} from '@/domain/events';
export class UserAccount {
private readonly _userId: UserId;
private readonly _accountSequence: AccountSequence;
private _devices: Map<string, DeviceInfo>;
private _phoneNumber: PhoneNumber | null;
private _nickname: string;
private _avatarUrl: string | null;
private readonly _inviterSequence: AccountSequence | null;
private readonly _referralCode: ReferralCode;
private _walletAddresses: Map<ChainType, WalletAddress>;
private _kycInfo: KYCInfo | null;
private _kycStatus: KYCStatus;
private _status: AccountStatus;
private readonly _registeredAt: Date;
private _lastLoginAt: Date | null;
private _updatedAt: Date;
private _domainEvents: DomainEvent[] = [];
// Getters
get userId(): UserId {
return this._userId;
}
get accountSequence(): AccountSequence {
return this._accountSequence;
}
get phoneNumber(): PhoneNumber | null {
return this._phoneNumber;
}
get nickname(): string {
return this._nickname;
}
get avatarUrl(): string | null {
return this._avatarUrl;
}
get inviterSequence(): AccountSequence | null {
return this._inviterSequence;
}
get referralCode(): ReferralCode {
return this._referralCode;
}
get kycInfo(): KYCInfo | null {
return this._kycInfo;
}
get kycStatus(): KYCStatus {
return this._kycStatus;
}
get status(): AccountStatus {
return this._status;
}
get registeredAt(): Date {
return this._registeredAt;
}
get lastLoginAt(): Date | null {
return this._lastLoginAt;
}
get updatedAt(): Date {
return this._updatedAt;
}
get isActive(): boolean {
return this._status === AccountStatus.ACTIVE;
}
get isKYCVerified(): boolean {
return this._kycStatus === KYCStatus.VERIFIED;
}
get domainEvents(): DomainEvent[] {
return [...this._domainEvents];
}
private constructor(
userId: UserId,
accountSequence: AccountSequence,
devices: Map<string, DeviceInfo>,
phoneNumber: PhoneNumber | null,
nickname: string,
avatarUrl: string | null,
inviterSequence: AccountSequence | null,
referralCode: ReferralCode,
walletAddresses: Map<ChainType, WalletAddress>,
kycInfo: KYCInfo | null,
kycStatus: KYCStatus,
status: AccountStatus,
registeredAt: Date,
lastLoginAt: Date | null,
updatedAt: Date,
) {
this._userId = userId;
this._accountSequence = accountSequence;
this._devices = devices;
this._phoneNumber = phoneNumber;
this._nickname = nickname;
this._avatarUrl = avatarUrl;
this._inviterSequence = inviterSequence;
this._referralCode = referralCode;
this._walletAddresses = walletAddresses;
this._kycInfo = kycInfo;
this._kycStatus = kycStatus;
this._status = status;
this._registeredAt = registeredAt;
this._lastLoginAt = lastLoginAt;
this._updatedAt = updatedAt;
}
static createAutomatic(params: {
accountSequence: AccountSequence;
initialDeviceId: string;
deviceName?: string;
deviceInfo?: Record<string, unknown>; // 完整的设备信息 JSON
inviterSequence: AccountSequence | null;
nickname?: string;
avatarSvg?: string;
}): UserAccount {
const devices = new Map<string, DeviceInfo>();
devices.set(
params.initialDeviceId,
new DeviceInfo(
params.initialDeviceId,
params.deviceName || '未命名设备',
new Date(),
new Date(),
params.deviceInfo, // 传递完整的 JSON
),
);
// UserID将由数据库自动生成(autoincrement)这里使用临时值0
const nickname =
params.nickname || `用户${params.accountSequence.dailySequence}`;
const avatarUrl = params.avatarSvg || null;
const account = new UserAccount(
UserId.create(0),
params.accountSequence,
devices,
null,
nickname,
avatarUrl,
params.inviterSequence,
ReferralCode.generate(),
new Map(),
null,
KYCStatus.NOT_VERIFIED,
AccountStatus.ACTIVE,
new Date(),
null,
new Date(),
);
account.addDomainEvent(
new UserAccountAutoCreatedEvent({
userId: account.userId.toString(),
accountSequence: params.accountSequence.value,
referralCode: account._referralCode.value, // 用户的推荐码
initialDeviceId: params.initialDeviceId,
inviterSequence: params.inviterSequence?.value || null,
registeredAt: account._registeredAt,
}),
);
return account;
}
static create(params: {
accountSequence: AccountSequence;
phoneNumber: PhoneNumber;
initialDeviceId: string;
deviceName?: string;
deviceInfo?: Record<string, unknown>; // 完整的设备信息 JSON
inviterSequence: AccountSequence | null;
}): UserAccount {
const devices = new Map<string, DeviceInfo>();
devices.set(
params.initialDeviceId,
new DeviceInfo(
params.initialDeviceId,
params.deviceName || '未命名设备',
new Date(),
new Date(),
params.deviceInfo,
),
);
// UserID将由数据库自动生成(autoincrement)这里使用临时值0
const account = new UserAccount(
UserId.create(0),
params.accountSequence,
devices,
params.phoneNumber,
`用户${params.accountSequence.dailySequence}`,
null,
params.inviterSequence,
ReferralCode.generate(),
new Map(),
null,
KYCStatus.NOT_VERIFIED,
AccountStatus.ACTIVE,
new Date(),
null,
new Date(),
);
account.addDomainEvent(
new UserAccountCreatedEvent({
userId: account.userId.toString(),
accountSequence: params.accountSequence.value,
referralCode: account._referralCode.value, // 用户的推荐码
phoneNumber: params.phoneNumber.value,
initialDeviceId: params.initialDeviceId,
inviterSequence: params.inviterSequence?.value || null,
registeredAt: account._registeredAt,
}),
);
return account;
}
static reconstruct(params: {
userId: string;
accountSequence: string;
devices: DeviceInfo[];
phoneNumber: string | null;
nickname: string;
avatarUrl: string | null;
inviterSequence: string | null;
referralCode: string;
walletAddresses: WalletAddress[];
kycInfo: KYCInfo | null;
kycStatus: KYCStatus;
status: AccountStatus;
registeredAt: Date;
lastLoginAt: Date | null;
updatedAt: Date;
}): UserAccount {
const deviceMap = new Map<string, DeviceInfo>();
params.devices.forEach((d) => deviceMap.set(d.deviceId, d));
const walletMap = new Map<ChainType, WalletAddress>();
params.walletAddresses.forEach((w) => walletMap.set(w.chainType, w));
return new UserAccount(
UserId.create(params.userId),
AccountSequence.create(params.accountSequence),
deviceMap,
params.phoneNumber ? PhoneNumber.create(params.phoneNumber) : null,
params.nickname,
params.avatarUrl,
params.inviterSequence
? AccountSequence.create(params.inviterSequence)
: null,
ReferralCode.create(params.referralCode),
walletMap,
params.kycInfo,
params.kycStatus,
params.status,
params.registeredAt,
params.lastLoginAt,
params.updatedAt,
);
}
addDevice(
deviceId: string,
deviceName?: string,
deviceInfo?: Record<string, unknown>,
): void {
this.ensureActive();
if (this._devices.size >= 5 && !this._devices.has(deviceId)) {
throw new DomainError('最多允许5个设备同时登录');
}
if (this._devices.has(deviceId)) {
const device = this._devices.get(deviceId)!;
device.updateActivity();
if (deviceInfo) {
device.updateDeviceInfo(deviceInfo);
}
} else {
this._devices.set(
deviceId,
new DeviceInfo(
deviceId,
deviceName || '未命名设备',
new Date(),
new Date(),
deviceInfo,
),
);
this.addDomainEvent(
new DeviceAddedEvent({
userId: this.userId.toString(),
accountSequence: this.accountSequence.value,
deviceId,
deviceName: deviceName || '未命名设备',
}),
);
}
this._updatedAt = new Date();
}
removeDevice(deviceId: string): void {
this.ensureActive();
if (!this._devices.has(deviceId)) throw new DomainError('设备不存在');
if (this._devices.size <= 1) throw new DomainError('至少保留一个设备');
this._devices.delete(deviceId);
this._updatedAt = new Date();
this.addDomainEvent(
new DeviceRemovedEvent({ userId: this.userId.toString(), deviceId }),
);
}
isDeviceAuthorized(deviceId: string): boolean {
return this._devices.has(deviceId);
}
getAllDevices(): DeviceInfo[] {
return Array.from(this._devices.values());
}
updateProfile(params: { nickname?: string; avatarUrl?: string }): void {
this.ensureActive();
if (params.nickname) this._nickname = params.nickname;
if (params.avatarUrl !== undefined) this._avatarUrl = params.avatarUrl;
this._updatedAt = new Date();
}
bindPhoneNumber(phoneNumber: PhoneNumber): void {
this.ensureActive();
if (this._phoneNumber) throw new DomainError('已绑定手机号,不可重复绑定');
this._phoneNumber = phoneNumber;
this._updatedAt = new Date();
this.addDomainEvent(
new PhoneNumberBoundEvent({
userId: this.userId.toString(),
phoneNumber: phoneNumber.value,
}),
);
}
bindWalletAddress(chainType: ChainType, address: string): void {
this.ensureActive();
if (this._walletAddresses.has(chainType))
throw new DomainError(`已绑定${chainType}地址`);
const walletAddress = WalletAddress.create({
userId: this.userId,
chainType,
address,
});
this._walletAddresses.set(chainType, walletAddress);
this._updatedAt = new Date();
this.addDomainEvent(
new WalletAddressBoundEvent({
userId: this.userId.toString(),
chainType,
address,
}),
);
}
bindMultipleWalletAddresses(wallets: Map<ChainType, WalletAddress>): void {
this.ensureActive();
for (const [chainType, wallet] of wallets) {
if (this._walletAddresses.has(chainType))
throw new DomainError(`已绑定${chainType}地址`);
this._walletAddresses.set(chainType, wallet);
}
this._updatedAt = new Date();
this.addDomainEvent(
new MultipleWalletAddressesBoundEvent({
userId: this.userId.toString(),
addresses: Array.from(wallets.entries()).map(([chainType, wallet]) => ({
chainType,
address: wallet.address,
})),
}),
);
}
submitKYC(kycInfo: KYCInfo): void {
this.ensureActive();
if (this._kycStatus === KYCStatus.VERIFIED)
throw new DomainError('已通过KYC认证,不可重复提交');
this._kycInfo = kycInfo;
this._kycStatus = KYCStatus.PENDING;
this._updatedAt = new Date();
this.addDomainEvent(
new KYCSubmittedEvent({
userId: this.userId.toString(),
realName: kycInfo.realName,
idCardNumber: kycInfo.idCardNumber,
}),
);
}
approveKYC(): void {
if (this._kycStatus !== KYCStatus.PENDING)
throw new DomainError('只有待审核状态才能通过KYC');
this._kycStatus = KYCStatus.VERIFIED;
this._updatedAt = new Date();
this.addDomainEvent(
new KYCVerifiedEvent({
userId: this.userId.toString(),
accountSequence: this.accountSequence.value,
verifiedAt: new Date(),
}),
);
}
rejectKYC(reason: string): void {
if (this._kycStatus !== KYCStatus.PENDING)
throw new DomainError('只有待审核状态才能拒绝KYC');
this._kycStatus = KYCStatus.REJECTED;
this._updatedAt = new Date();
this.addDomainEvent(
new KYCRejectedEvent({ userId: this.userId.toString(), reason }),
);
}
recordLogin(): void {
this.ensureActive();
this._lastLoginAt = new Date();
this._updatedAt = new Date();
}
freeze(reason: string): void {
if (this._status === AccountStatus.FROZEN)
throw new DomainError('账户已冻结');
this._status = AccountStatus.FROZEN;
this._updatedAt = new Date();
this.addDomainEvent(
new UserAccountFrozenEvent({ userId: this.userId.toString(), reason }),
);
}
unfreeze(): void {
if (this._status !== AccountStatus.FROZEN)
throw new DomainError('账户未冻结');
this._status = AccountStatus.ACTIVE;
this._updatedAt = new Date();
}
deactivate(): void {
if (this._status === AccountStatus.DEACTIVATED)
throw new DomainError('账户已注销');
this._status = AccountStatus.DEACTIVATED;
this._updatedAt = new Date();
this.addDomainEvent(
new UserAccountDeactivatedEvent({
userId: this.userId.toString(),
deactivatedAt: new Date(),
}),
);
}
getWalletAddress(chainType: ChainType): WalletAddress | null {
return this._walletAddresses.get(chainType) || null;
}
getAllWalletAddresses(): WalletAddress[] {
return Array.from(this._walletAddresses.values());
}
private ensureActive(): void {
if (this._status !== AccountStatus.ACTIVE)
throw new DomainError('账户已冻结或注销');
}
private addDomainEvent(event: DomainEvent): void {
this._domainEvents.push(event);
}
clearDomainEvents(): void {
this._domainEvents = [];
}
/**
* 创建钱包生成事件(用于重试)
*
* 重新发布 UserAccountCreatedEvent 以触发 MPC 钱包生成流程
* 这个方法是幂等的,可以安全地多次调用
*/
createWalletGenerationEvent(): UserAccountCreatedEvent {
// 获取第一个设备的信息
const firstDevice = this._devices.values().next().value as
| DeviceInfo
| undefined;
return new UserAccountCreatedEvent({
userId: this._userId.toString(),
accountSequence: this._accountSequence.value,
referralCode: this._referralCode.value,
phoneNumber: this._phoneNumber?.value || null,
initialDeviceId: firstDevice?.deviceId || 'retry-unknown',
inviterSequence: null, // 重试时不需要邀请人信息
registeredAt: this._registeredAt,
});
}
}