feat(zulip): Add Zulip account management and integrate with auth system

- Add ZulipAccountsEntity, repository, and module for persistent Zulip account storage
- Create ZulipAccountService in core layer for managing Zulip account lifecycle
- Integrate Zulip account creation into login flow via LoginService
- Add comprehensive test suite for Zulip account creation during user registration
- Create quick test script for validating registered user Zulip integration
- Update UsersEntity to support Zulip account associations
- Update auth module to include Zulip and ZulipAccounts dependencies
- Fix WebSocket connection protocol from ws:// to wss:// in API documentation
- Enhance LoginCoreService to coordinate Zulip account provisioning during authentication
This commit is contained in:
angjustinl
2026-01-05 17:41:54 +08:00
parent 9cb172d645
commit 6ad8d80449
14 changed files with 2698 additions and 38 deletions

View File

@@ -19,8 +19,9 @@
* @since 2025-12-17
*/
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToOne } from 'typeorm';
import { UserStatus } from '../../../business/user-mgmt/enums/user-status.enum';
import { ZulipAccounts } from '../zulip_accounts/zulip_accounts.entity';
/**
* 用户实体类
@@ -432,4 +433,25 @@ export class Users {
comment: '更新时间'
})
updated_at: Date;
/**
* 关联的Zulip账号
*
* 关系设计:
* - 类型一对一关系OneToOne
* - 外键在ZulipAccounts表中
* - 级联:不设置级联删除,保证数据安全
*
* 业务规则:
* - 每个游戏用户最多关联一个Zulip账号
* - 支持延迟加载,提高查询性能
* - 可选关联不是所有用户都有Zulip账号
*
* 使用场景:
* - 游戏内聊天功能集成
* - 跨平台消息同步
* - 用户身份验证和权限管理
*/
@OneToOne(() => ZulipAccounts, zulipAccount => zulipAccount.gameUser)
zulipAccount?: ZulipAccounts;
}

View File

@@ -0,0 +1,185 @@
/**
* Zulip账号关联实体
*
* 功能描述:
* - 存储游戏用户与Zulip账号的关联关系
* - 管理Zulip账号的基本信息和状态
* - 提供账号验证和同步功能
*
* 关联关系:
* - 与Users表建立一对一关系
* - 存储Zulip用户ID、邮箱、API Key等信息
*
* @author angjustinl
* @version 1.0.0
* @since 2025-01-05
*/
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToOne, JoinColumn, Index } from 'typeorm';
import { Users } from '../users/users.entity';
@Entity('zulip_accounts')
@Index(['zulip_user_id'], { unique: true })
@Index(['zulip_email'], { unique: true })
export class ZulipAccounts {
/**
* 主键ID
*/
@PrimaryGeneratedColumn('increment', { type: 'bigint' })
id: bigint;
/**
* 关联的游戏用户ID
*/
@Column({ type: 'bigint', name: 'game_user_id', comment: '关联的游戏用户ID' })
gameUserId: bigint;
/**
* Zulip用户ID
*/
@Column({ type: 'int', name: 'zulip_user_id', comment: 'Zulip服务器上的用户ID' })
zulipUserId: number;
/**
* Zulip用户邮箱
*/
@Column({ type: 'varchar', length: 255, name: 'zulip_email', comment: 'Zulip账号邮箱地址' })
zulipEmail: string;
/**
* Zulip用户全名
*/
@Column({ type: 'varchar', length: 100, name: 'zulip_full_name', comment: 'Zulip账号全名' })
zulipFullName: string;
/**
* Zulip API Key加密存储
*/
@Column({ type: 'text', name: 'zulip_api_key_encrypted', comment: '加密存储的Zulip API Key' })
zulipApiKeyEncrypted: string;
/**
* 账号状态
* - active: 正常激活状态
* - inactive: 未激活状态
* - suspended: 暂停状态
* - error: 错误状态
*/
@Column({
type: 'enum',
enum: ['active', 'inactive', 'suspended', 'error'],
default: 'active',
comment: '账号状态active-正常inactive-未激活suspended-暂停error-错误'
})
status: 'active' | 'inactive' | 'suspended' | 'error';
/**
* 最后验证时间
*/
@Column({ type: 'timestamp', name: 'last_verified_at', nullable: true, comment: '最后一次验证Zulip账号的时间' })
lastVerifiedAt: Date | null;
/**
* 最后同步时间
*/
@Column({ type: 'timestamp', name: 'last_synced_at', nullable: true, comment: '最后一次同步数据的时间' })
lastSyncedAt: Date | null;
/**
* 错误信息
*/
@Column({ type: 'text', name: 'error_message', nullable: true, comment: '最后一次操作的错误信息' })
errorMessage: string | null;
/**
* 重试次数
*/
@Column({ type: 'int', name: 'retry_count', default: 0, comment: '创建或同步失败的重试次数' })
retryCount: number;
/**
* 创建时间
*/
@CreateDateColumn({ name: 'created_at', comment: '记录创建时间' })
createdAt: Date;
/**
* 更新时间
*/
@UpdateDateColumn({ name: 'updated_at', comment: '记录最后更新时间' })
updatedAt: Date;
/**
* 关联的游戏用户
*/
@OneToOne(() => Users, user => user.zulipAccount)
@JoinColumn({ name: 'game_user_id' })
gameUser: Users;
/**
* 检查账号是否处于正常状态
*
* @returns boolean 是否为正常状态
*/
isActive(): boolean {
return this.status === 'active';
}
/**
* 检查账号是否需要重新验证
*
* @param maxAge 最大验证间隔毫秒默认24小时
* @returns boolean 是否需要重新验证
*/
needsVerification(maxAge: number = 24 * 60 * 60 * 1000): boolean {
if (!this.lastVerifiedAt) {
return true;
}
const now = new Date();
const timeDiff = now.getTime() - this.lastVerifiedAt.getTime();
return timeDiff > maxAge;
}
/**
* 更新验证时间
*/
updateVerificationTime(): void {
this.lastVerifiedAt = new Date();
}
/**
* 更新同步时间
*/
updateSyncTime(): void {
this.lastSyncedAt = new Date();
}
/**
* 设置错误状态
*
* @param errorMessage 错误信息
*/
setError(errorMessage: string): void {
this.status = 'error';
this.errorMessage = errorMessage;
this.retryCount += 1;
}
/**
* 清除错误状态
*/
clearError(): void {
if (this.status === 'error') {
this.status = 'active';
this.errorMessage = null;
}
}
/**
* 重置重试计数
*/
resetRetryCount(): void {
this.retryCount = 0;
}
}

View File

@@ -0,0 +1,81 @@
/**
* Zulip账号关联数据模块
*
* 功能描述:
* - 提供Zulip账号关联数据的访问接口
* - 封装TypeORM实体和Repository
* - 为业务层提供数据访问服务
* - 支持数据库和内存模式的动态切换
*
* @author angjustinl
* @version 1.0.0
* @since 2025-01-05
*/
import { Module, DynamicModule, Global } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ZulipAccounts } from './zulip_accounts.entity';
import { ZulipAccountsRepository } from './zulip_accounts.repository';
import { ZulipAccountsMemoryRepository } from './zulip_accounts_memory.repository';
/**
* 检查数据库配置是否完整
*
* @returns 是否配置了数据库
*/
function isDatabaseConfigured(): boolean {
const requiredEnvVars = ['DB_HOST', 'DB_PORT', 'DB_USERNAME', 'DB_PASSWORD', 'DB_NAME'];
return requiredEnvVars.every(varName => process.env[varName]);
}
@Global()
@Module({})
export class ZulipAccountsModule {
/**
* 创建数据库模式的Zulip账号模块
*
* @returns 配置了TypeORM的动态模块
*/
static forDatabase(): DynamicModule {
return {
module: ZulipAccountsModule,
imports: [TypeOrmModule.forFeature([ZulipAccounts])],
providers: [
{
provide: 'ZulipAccountsRepository',
useClass: ZulipAccountsRepository,
},
],
exports: ['ZulipAccountsRepository', TypeOrmModule],
};
}
/**
* 创建内存模式的Zulip账号模块
*
* @returns 配置了内存存储的动态模块
*/
static forMemory(): DynamicModule {
return {
module: ZulipAccountsModule,
providers: [
{
provide: 'ZulipAccountsRepository',
useClass: ZulipAccountsMemoryRepository,
},
],
exports: ['ZulipAccountsRepository'],
};
}
/**
* 根据环境自动选择模式
*
* @returns 动态模块
*/
static forRoot(): DynamicModule {
return isDatabaseConfigured()
? ZulipAccountsModule.forDatabase()
: ZulipAccountsModule.forMemory();
}
}

View File

@@ -0,0 +1,323 @@
/**
* Zulip账号关联数据访问层
*
* 功能描述:
* - 提供Zulip账号关联数据的CRUD操作
* - 封装复杂查询逻辑和数据库交互
* - 实现数据访问层的业务逻辑抽象
*
* 主要功能:
* - 账号关联的创建、查询、更新、删除
* - 支持按游戏用户ID、Zulip用户ID、邮箱查询
* - 提供账号状态管理和批量操作
*
* @author angjustinl
* @version 1.0.0
* @since 2025-01-05
*/
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindOptionsWhere } from 'typeorm';
import { ZulipAccounts } from './zulip_accounts.entity';
/**
* 创建Zulip账号关联的数据传输对象
*/
export interface CreateZulipAccountDto {
gameUserId: bigint;
zulipUserId: number;
zulipEmail: string;
zulipFullName: string;
zulipApiKeyEncrypted: string;
status?: 'active' | 'inactive' | 'suspended' | 'error';
}
/**
* 更新Zulip账号关联的数据传输对象
*/
export interface UpdateZulipAccountDto {
zulipFullName?: string;
zulipApiKeyEncrypted?: string;
status?: 'active' | 'inactive' | 'suspended' | 'error';
lastVerifiedAt?: Date;
lastSyncedAt?: Date;
errorMessage?: string;
retryCount?: number;
}
/**
* Zulip账号查询条件
*/
export interface ZulipAccountQueryOptions {
gameUserId?: bigint;
zulipUserId?: number;
zulipEmail?: string;
status?: 'active' | 'inactive' | 'suspended' | 'error';
includeGameUser?: boolean;
}
@Injectable()
export class ZulipAccountsRepository {
constructor(
@InjectRepository(ZulipAccounts)
private readonly repository: Repository<ZulipAccounts>,
) {}
/**
* 创建新的Zulip账号关联
*
* @param createDto 创建数据
* @returns Promise<ZulipAccounts> 创建的关联记录
*/
async create(createDto: CreateZulipAccountDto): Promise<ZulipAccounts> {
const zulipAccount = this.repository.create(createDto);
return await this.repository.save(zulipAccount);
}
/**
* 根据游戏用户ID查找Zulip账号关联
*
* @param gameUserId 游戏用户ID
* @param includeGameUser 是否包含游戏用户信息
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findByGameUserId(gameUserId: bigint, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
const relations = includeGameUser ? ['gameUser'] : [];
return await this.repository.findOne({
where: { gameUserId },
relations,
});
}
/**
* 根据Zulip用户ID查找账号关联
*
* @param zulipUserId Zulip用户ID
* @param includeGameUser 是否包含游戏用户信息
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findByZulipUserId(zulipUserId: number, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
const relations = includeGameUser ? ['gameUser'] : [];
return await this.repository.findOne({
where: { zulipUserId },
relations,
});
}
/**
* 根据Zulip邮箱查找账号关联
*
* @param zulipEmail Zulip邮箱
* @param includeGameUser 是否包含游戏用户信息
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findByZulipEmail(zulipEmail: string, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
const relations = includeGameUser ? ['gameUser'] : [];
return await this.repository.findOne({
where: { zulipEmail },
relations,
});
}
/**
* 根据ID查找Zulip账号关联
*
* @param id 关联记录ID
* @param includeGameUser 是否包含游戏用户信息
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findById(id: bigint, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
const relations = includeGameUser ? ['gameUser'] : [];
return await this.repository.findOne({
where: { id },
relations,
});
}
/**
* 更新Zulip账号关联
*
* @param id 关联记录ID
* @param updateDto 更新数据
* @returns Promise<ZulipAccounts | null> 更新后的记录或null
*/
async update(id: bigint, updateDto: UpdateZulipAccountDto): Promise<ZulipAccounts | null> {
await this.repository.update({ id }, updateDto);
return await this.findById(id);
}
/**
* 根据游戏用户ID更新Zulip账号关联
*
* @param gameUserId 游戏用户ID
* @param updateDto 更新数据
* @returns Promise<ZulipAccounts | null> 更新后的记录或null
*/
async updateByGameUserId(gameUserId: bigint, updateDto: UpdateZulipAccountDto): Promise<ZulipAccounts | null> {
await this.repository.update({ gameUserId }, updateDto);
return await this.findByGameUserId(gameUserId);
}
/**
* 删除Zulip账号关联
*
* @param id 关联记录ID
* @returns Promise<boolean> 是否删除成功
*/
async delete(id: bigint): Promise<boolean> {
const result = await this.repository.delete({ id });
return result.affected > 0;
}
/**
* 根据游戏用户ID删除Zulip账号关联
*
* @param gameUserId 游戏用户ID
* @returns Promise<boolean> 是否删除成功
*/
async deleteByGameUserId(gameUserId: bigint): Promise<boolean> {
const result = await this.repository.delete({ gameUserId });
return result.affected > 0;
}
/**
* 查询多个Zulip账号关联
*
* @param options 查询选项
* @returns Promise<ZulipAccounts[]> 关联记录列表
*/
async findMany(options: ZulipAccountQueryOptions = {}): Promise<ZulipAccounts[]> {
const { includeGameUser, ...whereOptions } = options;
const relations = includeGameUser ? ['gameUser'] : [];
// 构建查询条件
const where: FindOptionsWhere<ZulipAccounts> = {};
if (whereOptions.gameUserId) where.gameUserId = whereOptions.gameUserId;
if (whereOptions.zulipUserId) where.zulipUserId = whereOptions.zulipUserId;
if (whereOptions.zulipEmail) where.zulipEmail = whereOptions.zulipEmail;
if (whereOptions.status) where.status = whereOptions.status;
return await this.repository.find({
where,
relations,
order: { createdAt: 'DESC' },
});
}
/**
* 获取需要验证的账号列表
*
* @param maxAge 最大验证间隔毫秒默认24小时
* @returns Promise<ZulipAccounts[]> 需要验证的账号列表
*/
async findAccountsNeedingVerification(maxAge: number = 24 * 60 * 60 * 1000): Promise<ZulipAccounts[]> {
const cutoffTime = new Date(Date.now() - maxAge);
return await this.repository
.createQueryBuilder('zulip_accounts')
.where('zulip_accounts.status = :status', { status: 'active' })
.andWhere(
'(zulip_accounts.last_verified_at IS NULL OR zulip_accounts.last_verified_at < :cutoffTime)',
{ cutoffTime }
)
.orderBy('zulip_accounts.last_verified_at', 'ASC', 'NULLS FIRST')
.getMany();
}
/**
* 获取错误状态的账号列表
*
* @param maxRetryCount 最大重试次数默认3次
* @returns Promise<ZulipAccounts[]> 错误状态的账号列表
*/
async findErrorAccounts(maxRetryCount: number = 3): Promise<ZulipAccounts[]> {
return await this.repository.find({
where: { status: 'error' },
order: { updatedAt: 'ASC' },
});
}
/**
* 批量更新账号状态
*
* @param ids 账号ID列表
* @param status 新状态
* @returns Promise<number> 更新的记录数
*/
async batchUpdateStatus(ids: bigint[], status: 'active' | 'inactive' | 'suspended' | 'error'): Promise<number> {
const result = await this.repository
.createQueryBuilder()
.update(ZulipAccounts)
.set({ status })
.whereInIds(ids)
.execute();
return result.affected || 0;
}
/**
* 统计各状态的账号数量
*
* @returns Promise<Record<string, number>> 状态统计
*/
async getStatusStatistics(): Promise<Record<string, number>> {
const result = await this.repository
.createQueryBuilder('zulip_accounts')
.select('zulip_accounts.status', 'status')
.addSelect('COUNT(*)', 'count')
.groupBy('zulip_accounts.status')
.getRawMany();
const statistics: Record<string, number> = {};
result.forEach(row => {
statistics[row.status] = parseInt(row.count, 10);
});
return statistics;
}
/**
* 检查邮箱是否已存在
*
* @param zulipEmail Zulip邮箱
* @param excludeId 排除的记录ID用于更新时检查
* @returns Promise<boolean> 是否已存在
*/
async existsByEmail(zulipEmail: string, excludeId?: bigint): Promise<boolean> {
const queryBuilder = this.repository
.createQueryBuilder('zulip_accounts')
.where('zulip_accounts.zulip_email = :zulipEmail', { zulipEmail });
if (excludeId) {
queryBuilder.andWhere('zulip_accounts.id != :excludeId', { excludeId });
}
const count = await queryBuilder.getCount();
return count > 0;
}
/**
* 检查Zulip用户ID是否已存在
*
* @param zulipUserId Zulip用户ID
* @param excludeId 排除的记录ID用于更新时检查
* @returns Promise<boolean> 是否已存在
*/
async existsByZulipUserId(zulipUserId: number, excludeId?: bigint): Promise<boolean> {
const queryBuilder = this.repository
.createQueryBuilder('zulip_accounts')
.where('zulip_accounts.zulip_user_id = :zulipUserId', { zulipUserId });
if (excludeId) {
queryBuilder.andWhere('zulip_accounts.id != :excludeId', { excludeId });
}
const count = await queryBuilder.getCount();
return count > 0;
}
}

View File

@@ -0,0 +1,299 @@
/**
* Zulip账号关联内存数据访问层
*
* 功能描述:
* - 提供Zulip账号关联数据的内存存储实现
* - 用于开发和测试环境
* - 实现与数据库版本相同的接口
*
* @author angjustinl
* @version 1.0.0
* @since 2025-01-05
*/
import { Injectable } from '@nestjs/common';
import { ZulipAccounts } from './zulip_accounts.entity';
import {
CreateZulipAccountDto,
UpdateZulipAccountDto,
ZulipAccountQueryOptions,
} from './zulip_accounts.repository';
@Injectable()
export class ZulipAccountsMemoryRepository {
private accounts: Map<bigint, ZulipAccounts> = new Map();
private currentId: bigint = BigInt(1);
/**
* 创建新的Zulip账号关联
*
* @param createDto 创建数据
* @returns Promise<ZulipAccounts> 创建的关联记录
*/
async create(createDto: CreateZulipAccountDto): Promise<ZulipAccounts> {
const account = new ZulipAccounts();
account.id = this.currentId++;
account.gameUserId = createDto.gameUserId;
account.zulipUserId = createDto.zulipUserId;
account.zulipEmail = createDto.zulipEmail;
account.zulipFullName = createDto.zulipFullName;
account.zulipApiKeyEncrypted = createDto.zulipApiKeyEncrypted;
account.status = createDto.status || 'active';
account.createdAt = new Date();
account.updatedAt = new Date();
this.accounts.set(account.id, account);
return account;
}
/**
* 根据游戏用户ID查找Zulip账号关联
*
* @param gameUserId 游戏用户ID
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findByGameUserId(gameUserId: bigint, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
for (const account of this.accounts.values()) {
if (account.gameUserId === gameUserId) {
return account;
}
}
return null;
}
/**
* 根据Zulip用户ID查找账号关联
*
* @param zulipUserId Zulip用户ID
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findByZulipUserId(zulipUserId: number, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
for (const account of this.accounts.values()) {
if (account.zulipUserId === zulipUserId) {
return account;
}
}
return null;
}
/**
* 根据Zulip邮箱查找账号关联
*
* @param zulipEmail Zulip邮箱
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findByZulipEmail(zulipEmail: string, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
for (const account of this.accounts.values()) {
if (account.zulipEmail === zulipEmail) {
return account;
}
}
return null;
}
/**
* 根据ID查找Zulip账号关联
*
* @param id 关联记录ID
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findById(id: bigint, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
return this.accounts.get(id) || null;
}
/**
* 更新Zulip账号关联
*
* @param id 关联记录ID
* @param updateDto 更新数据
* @returns Promise<ZulipAccounts | null> 更新后的记录或null
*/
async update(id: bigint, updateDto: UpdateZulipAccountDto): Promise<ZulipAccounts | null> {
const account = this.accounts.get(id);
if (!account) {
return null;
}
Object.assign(account, updateDto);
account.updatedAt = new Date();
return account;
}
/**
* 根据游戏用户ID更新Zulip账号关联
*
* @param gameUserId 游戏用户ID
* @param updateDto 更新数据
* @returns Promise<ZulipAccounts | null> 更新后的记录或null
*/
async updateByGameUserId(gameUserId: bigint, updateDto: UpdateZulipAccountDto): Promise<ZulipAccounts | null> {
const account = await this.findByGameUserId(gameUserId);
if (!account) {
return null;
}
Object.assign(account, updateDto);
account.updatedAt = new Date();
return account;
}
/**
* 删除Zulip账号关联
*
* @param id 关联记录ID
* @returns Promise<boolean> 是否删除成功
*/
async delete(id: bigint): Promise<boolean> {
return this.accounts.delete(id);
}
/**
* 根据游戏用户ID删除Zulip账号关联
*
* @param gameUserId 游戏用户ID
* @returns Promise<boolean> 是否删除成功
*/
async deleteByGameUserId(gameUserId: bigint): Promise<boolean> {
for (const [id, account] of this.accounts.entries()) {
if (account.gameUserId === gameUserId) {
return this.accounts.delete(id);
}
}
return false;
}
/**
* 查询多个Zulip账号关联
*
* @param options 查询选项
* @returns Promise<ZulipAccounts[]> 关联记录列表
*/
async findMany(options: ZulipAccountQueryOptions = {}): Promise<ZulipAccounts[]> {
let results = Array.from(this.accounts.values());
if (options.gameUserId) {
results = results.filter(a => a.gameUserId === options.gameUserId);
}
if (options.zulipUserId) {
results = results.filter(a => a.zulipUserId === options.zulipUserId);
}
if (options.zulipEmail) {
results = results.filter(a => a.zulipEmail === options.zulipEmail);
}
if (options.status) {
results = results.filter(a => a.status === options.status);
}
// 按创建时间降序排序
results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
return results;
}
/**
* 获取需要验证的账号列表
*
* @param maxAge 最大验证间隔毫秒默认24小时
* @returns Promise<ZulipAccounts[]> 需要验证的账号列表
*/
async findAccountsNeedingVerification(maxAge: number = 24 * 60 * 60 * 1000): Promise<ZulipAccounts[]> {
const cutoffTime = new Date(Date.now() - maxAge);
return Array.from(this.accounts.values())
.filter(account =>
account.status === 'active' &&
(!account.lastVerifiedAt || account.lastVerifiedAt < cutoffTime)
)
.sort((a, b) => {
if (!a.lastVerifiedAt) return -1;
if (!b.lastVerifiedAt) return 1;
return a.lastVerifiedAt.getTime() - b.lastVerifiedAt.getTime();
});
}
/**
* 获取错误状态的账号列表
*
* @param maxRetryCount 最大重试次数(内存模式忽略)
* @returns Promise<ZulipAccounts[]> 错误状态的账号列表
*/
async findErrorAccounts(maxRetryCount: number = 3): Promise<ZulipAccounts[]> {
return Array.from(this.accounts.values())
.filter(account => account.status === 'error')
.sort((a, b) => a.updatedAt.getTime() - b.updatedAt.getTime());
}
/**
* 批量更新账号状态
*
* @param ids 账号ID列表
* @param status 新状态
* @returns Promise<number> 更新的记录数
*/
async batchUpdateStatus(ids: bigint[], status: 'active' | 'inactive' | 'suspended' | 'error'): Promise<number> {
let count = 0;
for (const id of ids) {
const account = this.accounts.get(id);
if (account) {
account.status = status;
account.updatedAt = new Date();
count++;
}
}
return count;
}
/**
* 统计各状态的账号数量
*
* @returns Promise<Record<string, number>> 状态统计
*/
async getStatusStatistics(): Promise<Record<string, number>> {
const statistics: Record<string, number> = {};
for (const account of this.accounts.values()) {
const status = account.status;
statistics[status] = (statistics[status] || 0) + 1;
}
return statistics;
}
/**
* 检查邮箱是否已存在
*
* @param zulipEmail Zulip邮箱
* @param excludeId 排除的记录ID用于更新时检查
* @returns Promise<boolean> 是否已存在
*/
async existsByEmail(zulipEmail: string, excludeId?: bigint): Promise<boolean> {
for (const [id, account] of this.accounts.entries()) {
if (account.zulipEmail === zulipEmail && (!excludeId || id !== excludeId)) {
return true;
}
}
return false;
}
/**
* 检查Zulip用户ID是否已存在
*
* @param zulipUserId Zulip用户ID
* @param excludeId 排除的记录ID用于更新时检查
* @returns Promise<boolean> 是否已存在
*/
async existsByZulipUserId(zulipUserId: number, excludeId?: bigint): Promise<boolean> {
for (const [id, account] of this.accounts.entries()) {
if (account.zulipUserId === zulipUserId && (!excludeId || id !== excludeId)) {
return true;
}
}
return false;
}
}