import { AccountSequence, Password, Phone } from '../value-objects'; export enum UserStatus { ACTIVE = 'ACTIVE', DISABLED = 'DISABLED', DELETED = 'DELETED', } export enum KycStatus { PENDING = 'PENDING', SUBMITTED = 'SUBMITTED', VERIFIED = 'VERIFIED', REJECTED = 'REJECTED', } export interface UserProps { id?: bigint; phone: Phone; passwordHash: string; tradePasswordHash?: string; // 支付密码(独立于登录密码) accountSequence: AccountSequence; status: UserStatus; kycStatus: KycStatus; realName?: string; idCardNo?: string; idCardFront?: string; idCardBack?: string; kycSubmittedAt?: Date; kycVerifiedAt?: Date; kycRejectReason?: string; loginFailCount: number; lockedUntil?: Date; lastLoginAt?: Date; lastLoginIp?: string; createdAt?: Date; updatedAt?: Date; } /** * 用户聚合根 */ export class UserAggregate { private _id?: bigint; private _phone: Phone; private _passwordHash: string; private _tradePasswordHash?: string; // 支付密码哈希 private _accountSequence: AccountSequence; private _status: UserStatus; private _kycStatus: KycStatus; private _realName?: string; private _idCardNo?: string; private _idCardFront?: string; private _idCardBack?: string; private _kycSubmittedAt?: Date; private _kycVerifiedAt?: Date; private _kycRejectReason?: string; private _loginFailCount: number; private _lockedUntil?: Date; private _lastLoginAt?: Date; private _lastLoginIp?: string; private _createdAt?: Date; private _updatedAt?: Date; private constructor(props: UserProps) { this._id = props.id; this._phone = props.phone; this._passwordHash = props.passwordHash; this._tradePasswordHash = props.tradePasswordHash; this._accountSequence = props.accountSequence; this._status = props.status; this._kycStatus = props.kycStatus; this._realName = props.realName; this._idCardNo = props.idCardNo; this._idCardFront = props.idCardFront; this._idCardBack = props.idCardBack; this._kycSubmittedAt = props.kycSubmittedAt; this._kycVerifiedAt = props.kycVerifiedAt; this._kycRejectReason = props.kycRejectReason; this._loginFailCount = props.loginFailCount; this._lockedUntil = props.lockedUntil; this._lastLoginAt = props.lastLoginAt; this._lastLoginIp = props.lastLoginIp; this._createdAt = props.createdAt; this._updatedAt = props.updatedAt; } /** * 创建新用户(注册) */ static async create( phone: Phone, plainPassword: string, accountSequence: AccountSequence, ): Promise { const password = await Password.create(plainPassword); return new UserAggregate({ phone, passwordHash: password.hash, accountSequence, status: UserStatus.ACTIVE, kycStatus: KycStatus.PENDING, loginFailCount: 0, }); } /** * 从数据库重建 */ static reconstitute(props: UserProps): UserAggregate { return new UserAggregate(props); } // Getters get id(): bigint | undefined { return this._id; } get phone(): Phone { return this._phone; } get passwordHash(): string { return this._passwordHash; } get tradePasswordHash(): string | undefined { return this._tradePasswordHash; } /** * 是否已设置支付密码 */ get hasTradePassword(): boolean { return this._tradePasswordHash !== undefined && this._tradePasswordHash !== null; } get accountSequence(): AccountSequence { return this._accountSequence; } get status(): UserStatus { return this._status; } get kycStatus(): KycStatus { return this._kycStatus; } get realName(): string | undefined { return this._realName; } get idCardNo(): string | undefined { return this._idCardNo; } get idCardFront(): string | undefined { return this._idCardFront; } get idCardBack(): string | undefined { return this._idCardBack; } get kycSubmittedAt(): Date | undefined { return this._kycSubmittedAt; } get kycVerifiedAt(): Date | undefined { return this._kycVerifiedAt; } get kycRejectReason(): string | undefined { return this._kycRejectReason; } get loginFailCount(): number { return this._loginFailCount; } get lockedUntil(): Date | undefined { return this._lockedUntil; } get lastLoginAt(): Date | undefined { return this._lastLoginAt; } get lastLoginIp(): string | undefined { return this._lastLoginIp; } get createdAt(): Date | undefined { return this._createdAt; } get updatedAt(): Date | undefined { return this._updatedAt; } /** * 判断用户来源 */ get source(): 'V1' | 'V2' { return this._accountSequence.source; } /** * 是否为 V1 迁移用户 */ get isLegacyUser(): boolean { return this._accountSequence.isV1; } /** * 是否已锁定 */ get isLocked(): boolean { return this._lockedUntil !== undefined && this._lockedUntil > new Date(); } /** * 是否可登录 */ get canLogin(): boolean { return this._status === UserStatus.ACTIVE && !this.isLocked; } /** * 是否已完成 KYC */ get isKycVerified(): boolean { return this._kycStatus === KycStatus.VERIFIED; } /** * 验证密码 */ async verifyPassword(plainPassword: string): Promise { const password = Password.fromHash(this._passwordHash); return password.verify(plainPassword); } /** * 修改密码 */ async changePassword(newPlainPassword: string): Promise { const password = await Password.create(newPlainPassword); this._passwordHash = password.hash; this._updatedAt = new Date(); } /** * 设置支付密码 */ async setTradePassword(newPlainPassword: string): Promise { const password = await Password.create(newPlainPassword); this._tradePasswordHash = password.hash; this._updatedAt = new Date(); } /** * 验证支付密码 */ async verifyTradePassword(plainPassword: string): Promise { if (!this._tradePasswordHash) { return false; } const password = Password.fromHash(this._tradePasswordHash); return password.verify(plainPassword); } /** * 清除支付密码 */ clearTradePassword(): void { this._tradePasswordHash = undefined; this._updatedAt = new Date(); } /** * 记录登录成功 */ recordLoginSuccess(ip?: string): void { this._loginFailCount = 0; this._lockedUntil = undefined; this._lastLoginAt = new Date(); this._lastLoginIp = ip; this._updatedAt = new Date(); } /** * 计算指数退避锁定时间(分钟) * 第6次失败: 1分钟 * 第7次失败: 2分钟 * 第8次失败: 4分钟 * 第9次失败: 8分钟 * 第10次失败: 16分钟 * ...以此类推,最长24小时 */ private calculateLockMinutes(failCount: number, maxAttempts: number): number { const excessAttempts = failCount - maxAttempts; // 2^(excessAttempts) 分钟,最长 1440 分钟(24小时) const lockMinutes = Math.pow(2, excessAttempts); return Math.min(lockMinutes, 1440); } /** * 记录登录失败 * @param maxAttempts 最大尝试次数,默认6次 * @returns 返回剩余尝试次数和锁定信息 */ recordLoginFailure(maxAttempts: number = 6): { remainingAttempts: number; lockedUntil?: Date; lockMinutes?: number } { this._loginFailCount += 1; this._updatedAt = new Date(); if (this._loginFailCount >= maxAttempts) { const lockMinutes = this.calculateLockMinutes(this._loginFailCount, maxAttempts); this._lockedUntil = new Date(Date.now() + lockMinutes * 60 * 1000); return { remainingAttempts: 0, lockedUntil: this._lockedUntil, lockMinutes, }; } return { remainingAttempts: maxAttempts - this._loginFailCount, }; } /** * 获取剩余尝试次数 */ getRemainingAttempts(maxAttempts: number = 6): number { return Math.max(0, maxAttempts - this._loginFailCount); } /** * 获取锁定剩余时间(秒) */ getLockRemainingSeconds(): number { if (!this._lockedUntil) return 0; const remaining = this._lockedUntil.getTime() - Date.now(); return Math.max(0, Math.ceil(remaining / 1000)); } /** * 解锁账户 */ unlock(): void { this._loginFailCount = 0; this._lockedUntil = undefined; this._updatedAt = new Date(); } /** * 提交 KYC 资料 */ submitKyc( realName: string, idCardNo: string, idCardFront: string, idCardBack: string, ): void { if (this._kycStatus === KycStatus.VERIFIED) { throw new Error('已完成实名认证,无法重新提交'); } this._realName = realName; this._idCardNo = idCardNo; this._idCardFront = idCardFront; this._idCardBack = idCardBack; this._kycStatus = KycStatus.SUBMITTED; this._kycSubmittedAt = new Date(); this._kycRejectReason = undefined; this._updatedAt = new Date(); } /** * KYC 审核通过 */ approveKyc(): void { if (this._kycStatus !== KycStatus.SUBMITTED) { throw new Error('当前状态无法审核'); } this._kycStatus = KycStatus.VERIFIED; this._kycVerifiedAt = new Date(); this._updatedAt = new Date(); } /** * KYC 审核拒绝 */ rejectKyc(reason: string): void { if (this._kycStatus !== KycStatus.SUBMITTED) { throw new Error('当前状态无法审核'); } this._kycStatus = KycStatus.REJECTED; this._kycRejectReason = reason; this._updatedAt = new Date(); } /** * 禁用用户 */ disable(): void { this._status = UserStatus.DISABLED; this._updatedAt = new Date(); } /** * 启用用户 */ enable(): void { this._status = UserStatus.ACTIVE; this._updatedAt = new Date(); } /** * 删除用户(软删除) */ delete(): void { this._status = UserStatus.DELETED; this._updatedAt = new Date(); } /** * 更换手机号 */ changePhone(newPhone: Phone): void { this._phone = newPhone; this._updatedAt = new Date(); } toSnapshot(): UserProps { return { id: this._id, phone: this._phone, passwordHash: this._passwordHash, tradePasswordHash: this._tradePasswordHash, accountSequence: this._accountSequence, status: this._status, kycStatus: this._kycStatus, realName: this._realName, idCardNo: this._idCardNo, idCardFront: this._idCardFront, idCardBack: this._idCardBack, kycSubmittedAt: this._kycSubmittedAt, kycVerifiedAt: this._kycVerifiedAt, kycRejectReason: this._kycRejectReason, loginFailCount: this._loginFailCount, lockedUntil: this._lockedUntil, lastLoginAt: this._lastLoginAt, lastLoginIp: this._lastLoginIp, createdAt: this._createdAt, updatedAt: this._updatedAt, }; } }