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

463 lines
11 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 { 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<UserAggregate> {
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<boolean> {
const password = Password.fromHash(this._passwordHash);
return password.verify(plainPassword);
}
/**
* 修改密码
*/
async changePassword(newPlainPassword: string): Promise<void> {
const password = await Password.create(newPlainPassword);
this._passwordHash = password.hash;
this._updatedAt = new Date();
}
/**
* 设置支付密码
*/
async setTradePassword(newPlainPassword: string): Promise<void> {
const password = await Password.create(newPlainPassword);
this._tradePasswordHash = password.hash;
this._updatedAt = new Date();
}
/**
* 验证支付密码
*/
async verifyTradePassword(plainPassword: string): Promise<boolean> {
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,
};
}
}