import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, VersionColumn, Index, } from 'typeorm'; export enum UserRole { USER = 'user', ISSUER = 'issuer', MARKET_MAKER = 'market_maker', ADMIN = 'admin', } export enum UserStatus { ACTIVE = 'active', FROZEN = 'frozen', DELETED = 'deleted', } @Entity('users') export class User { @PrimaryGeneratedColumn('uuid') id: string; @Index('idx_users_phone') @Column({ type: 'varchar', length: 20, unique: true, nullable: true }) phone: string | null; @Index('idx_users_email') @Column({ type: 'varchar', length: 100, unique: true, nullable: true }) email: string | null; @Column({ name: 'password_hash', type: 'varchar', length: 255 }) passwordHash: string; @Column({ type: 'varchar', length: 50, nullable: true }) nickname: string | null; @Column({ name: 'avatar_url', type: 'varchar', length: 500, nullable: true }) avatarUrl: string | null; @Column({ name: 'kyc_level', type: 'smallint', default: 0 }) kycLevel: number; @Column({ name: 'wallet_mode', type: 'varchar', length: 10, default: 'standard', }) walletMode: 'standard' | 'external' | 'pro'; @Index('idx_users_role') @Column({ type: 'varchar', length: 20, default: UserRole.USER, }) role: UserRole; @Index('idx_users_status') @Column({ type: 'varchar', length: 20, default: UserStatus.ACTIVE, }) status: UserStatus; @Column({ name: 'residence_state', type: 'varchar', length: 5, nullable: true }) residenceState: string | null; @Column({ type: 'varchar', length: 5, nullable: true }) nationality: string | null; @Column({ name: 'login_fail_count', type: 'int', default: 0 }) loginFailCount: number; @Column({ name: 'locked_until', type: 'timestamptz', nullable: true }) lockedUntil: Date | null; @Column({ name: 'last_login_at', type: 'timestamptz', nullable: true }) lastLoginAt: Date | null; @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) createdAt: Date; @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) updatedAt: Date; @VersionColumn({ default: 1 }) version: number; /* ── Domain Methods: Account Lockout ── */ /** 账号是否处于锁定状态 */ get isLocked(): boolean { return this.lockedUntil !== null && new Date() < this.lockedUntil; } /** 是否可以登录 (状态正常且未锁定) */ get canLogin(): boolean { return this.status === UserStatus.ACTIVE && !this.isLocked; } /** 剩余锁定秒数 (0 = 未锁定) */ get lockRemainingSeconds(): number { if (!this.lockedUntil) return 0; const diff = this.lockedUntil.getTime() - Date.now(); return diff > 0 ? Math.ceil(diff / 1000) : 0; } /** * 记录登录成功 — 清除失败计数 */ recordLoginSuccess(ip?: string): void { this.loginFailCount = 0; this.lockedUntil = null; this.lastLoginAt = new Date(); } /** * 记录登录失败 — 指数退避锁定 * @param maxAttempts 触发锁定的失败次数阈值 (默认 6) * @returns 剩余尝试次数 & 锁定信息 */ recordLoginFailure(maxAttempts = 6): { remainingAttempts: number; lockMinutes?: number; } { this.loginFailCount += 1; if (this.loginFailCount < maxAttempts) { return { remainingAttempts: maxAttempts - this.loginFailCount }; } // 指数退避: 2^(failCount - maxAttempts) 分钟,最大 1440 分钟 (24h) const lockMinutes = Math.min( Math.pow(2, this.loginFailCount - maxAttempts), 1440, ); this.lockedUntil = new Date(Date.now() + lockMinutes * 60 * 1000); return { remainingAttempts: 0, lockMinutes }; } }