332 lines
13 KiB
TypeScript
332 lines
13 KiB
TypeScript
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.value}`;
|
||
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,
|
||
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.value}`, 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,
|
||
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(), 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 = [];
|
||
}
|
||
}
|