Initial WhaleTown V2 backend
This commit is contained in:
28
src/core/db/player_assets/create-player-assets-tables.sql
Normal file
28
src/core/db/player_assets/create-player-assets-tables.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
CREATE TABLE IF NOT EXISTS `user_assets` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`user_id` BIGINT NOT NULL COMMENT '关联users.id',
|
||||
`asset_type` VARCHAR(40) NOT NULL COMMENT '资产类型:skin/room_decor/item/badge等',
|
||||
`asset_id` VARCHAR(100) NOT NULL COMMENT '资产ID',
|
||||
`source` VARCHAR(50) NOT NULL DEFAULT 'system' COMMENT '发放来源',
|
||||
`metadata` JSON NULL COMMENT '资产扩展数据',
|
||||
`acquired_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '获得时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_user_assets_user_type_asset_unique` (`user_id`, `asset_type`, `asset_id`),
|
||||
KEY `idx_user_assets_user_id` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_decor_placements` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`user_id` BIGINT NOT NULL COMMENT '关联users.id',
|
||||
`decor_id` VARCHAR(100) NOT NULL COMMENT '房间摆件ID',
|
||||
`placed` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否已摆放',
|
||||
`position_x` FLOAT NULL COMMENT '房间内X坐标',
|
||||
`position_y` FLOAT NULL COMMENT '房间内Y坐标',
|
||||
`scale` FLOAT NOT NULL DEFAULT 1 COMMENT '摆件缩放',
|
||||
`z_index` INT NOT NULL DEFAULT 0 COMMENT '摆放层级',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_room_decor_placements_user_decor_unique` (`user_id`, `decor_id`),
|
||||
KEY `idx_room_decor_placements_user_id` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
42
src/core/db/player_assets/migrate-legacy-assets.sql
Normal file
42
src/core/db/player_assets/migrate-legacy-assets.sql
Normal file
@@ -0,0 +1,42 @@
|
||||
INSERT IGNORE INTO `user_assets` (`user_id`, `asset_type`, `asset_id`, `source`, `metadata`, `acquired_at`)
|
||||
SELECT
|
||||
`user_id`,
|
||||
'skin' AS `asset_type`,
|
||||
`skin_id` AS `asset_id`,
|
||||
`source`,
|
||||
NULL AS `metadata`,
|
||||
`created_at` AS `acquired_at`
|
||||
FROM `user_skins`;
|
||||
|
||||
INSERT IGNORE INTO `user_assets` (`user_id`, `asset_type`, `asset_id`, `source`, `metadata`, `acquired_at`)
|
||||
SELECT
|
||||
`user_id`,
|
||||
'room_decor' AS `asset_type`,
|
||||
`decor_id` AS `asset_id`,
|
||||
`source`,
|
||||
NULL AS `metadata`,
|
||||
`created_at` AS `acquired_at`
|
||||
FROM `user_room_decors`;
|
||||
|
||||
INSERT IGNORE INTO `room_decor_placements` (
|
||||
`user_id`,
|
||||
`decor_id`,
|
||||
`placed`,
|
||||
`position_x`,
|
||||
`position_y`,
|
||||
`scale`,
|
||||
`z_index`,
|
||||
`created_at`,
|
||||
`updated_at`
|
||||
)
|
||||
SELECT
|
||||
`user_id`,
|
||||
`decor_id`,
|
||||
`placed`,
|
||||
`position_x`,
|
||||
`position_y`,
|
||||
`scale`,
|
||||
`z_index`,
|
||||
`created_at`,
|
||||
`updated_at`
|
||||
FROM `user_room_decors`;
|
||||
29
src/core/db/player_assets/player_assets.entity.ts
Normal file
29
src/core/db/player_assets/player_assets.entity.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
export type PlayerAssetType = 'skin' | 'room_decor';
|
||||
|
||||
@Entity('user_assets')
|
||||
@Index('idx_user_assets_user_id', ['user_id'])
|
||||
@Index('idx_user_assets_user_type_asset_unique', ['user_id', 'asset_type', 'asset_id'], { unique: true })
|
||||
export class PlayerAssets {
|
||||
@PrimaryGeneratedColumn({ type: 'bigint', comment: '主键ID' })
|
||||
id: bigint;
|
||||
|
||||
@Column({ type: 'bigint', nullable: false, comment: '关联users.id' })
|
||||
user_id: bigint;
|
||||
|
||||
@Column({ type: 'varchar', length: 40, nullable: false, comment: '资产类型:skin/room_decor/item/badge等' })
|
||||
asset_type: PlayerAssetType;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, nullable: false, comment: '资产ID' })
|
||||
asset_id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false, default: 'system', comment: '发放来源' })
|
||||
source: string;
|
||||
|
||||
@Column({ type: 'json', nullable: true, comment: '资产扩展数据' })
|
||||
metadata?: Record<string, unknown> | null;
|
||||
|
||||
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', comment: '获得时间' })
|
||||
acquired_at: Date;
|
||||
}
|
||||
48
src/core/db/player_assets/player_assets.module.ts
Normal file
48
src/core/db/player_assets/player_assets.module.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { DynamicModule, Global, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { PlayerAssets } from './player_assets.entity';
|
||||
import { PlayerAssetsMemoryService } from './player_assets_memory.service';
|
||||
import { PlayerAssetsService } from './player_assets.service';
|
||||
import { RoomDecorPlacements } from './room_decor_placements.entity';
|
||||
import { RoomDecorPlacementsMemoryService } from './room_decor_placements_memory.service';
|
||||
import { RoomDecorPlacementsService } from './room_decor_placements.service';
|
||||
|
||||
@Global()
|
||||
@Module({})
|
||||
export class PlayerAssetsModule {
|
||||
static forDatabase(): DynamicModule {
|
||||
return {
|
||||
module: PlayerAssetsModule,
|
||||
imports: [TypeOrmModule.forFeature([PlayerAssets, RoomDecorPlacements])],
|
||||
providers: [
|
||||
PlayerAssetsService,
|
||||
RoomDecorPlacementsService,
|
||||
{ provide: 'IPlayerAssetsService', useClass: PlayerAssetsService },
|
||||
{ provide: 'IRoomDecorPlacementsService', useClass: RoomDecorPlacementsService },
|
||||
],
|
||||
exports: [PlayerAssetsService, RoomDecorPlacementsService, 'IPlayerAssetsService', 'IRoomDecorPlacementsService'],
|
||||
};
|
||||
}
|
||||
|
||||
static forMemory(): DynamicModule {
|
||||
return {
|
||||
module: PlayerAssetsModule,
|
||||
providers: [
|
||||
PlayerAssetsMemoryService,
|
||||
RoomDecorPlacementsMemoryService,
|
||||
{ provide: 'IPlayerAssetsService', useClass: PlayerAssetsMemoryService },
|
||||
{ provide: 'IRoomDecorPlacementsService', useClass: RoomDecorPlacementsMemoryService },
|
||||
],
|
||||
exports: [PlayerAssetsMemoryService, RoomDecorPlacementsMemoryService, 'IPlayerAssetsService', 'IRoomDecorPlacementsService'],
|
||||
};
|
||||
}
|
||||
|
||||
static forRoot(useMemory?: boolean): DynamicModule {
|
||||
const shouldUseMemory = useMemory ?? (
|
||||
process.env.NODE_ENV === 'test' ||
|
||||
process.env.USE_MEMORY_STORAGE === 'true' ||
|
||||
!process.env.DB_HOST
|
||||
);
|
||||
return shouldUseMemory ? this.forMemory() : this.forDatabase();
|
||||
}
|
||||
}
|
||||
73
src/core/db/player_assets/player_assets.service.ts
Normal file
73
src/core/db/player_assets/player_assets.service.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { PlayerAssets, PlayerAssetType } from './player_assets.entity';
|
||||
|
||||
@Injectable()
|
||||
export class PlayerAssetsService {
|
||||
constructor(
|
||||
@InjectRepository(PlayerAssets)
|
||||
private readonly playerAssetsRepository: Repository<PlayerAssets>,
|
||||
) {}
|
||||
|
||||
async grantAsset(userId: bigint, assetType: PlayerAssetType, assetId: string, source = 'system', metadata?: Record<string, unknown>): Promise<PlayerAssets> {
|
||||
const normalizedAssetId = this.normalizeAssetId(assetId);
|
||||
const existing = await this.playerAssetsRepository.findOne({
|
||||
where: { user_id: userId, asset_type: assetType, asset_id: normalizedAssetId },
|
||||
});
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const row = new PlayerAssets();
|
||||
row.user_id = userId;
|
||||
row.asset_type = assetType;
|
||||
row.asset_id = normalizedAssetId;
|
||||
row.source = source || 'system';
|
||||
row.metadata = metadata ?? null;
|
||||
row.acquired_at = new Date();
|
||||
return await this.playerAssetsRepository.save(row);
|
||||
}
|
||||
|
||||
async hasAsset(userId: bigint, assetType: PlayerAssetType, assetId: string): Promise<boolean> {
|
||||
const normalizedAssetId = (assetId || '').trim();
|
||||
if (!normalizedAssetId) {
|
||||
return false;
|
||||
}
|
||||
const count = await this.playerAssetsRepository.count({
|
||||
where: { user_id: userId, asset_type: assetType, asset_id: normalizedAssetId },
|
||||
});
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
async listAssets(userId: bigint, assetType?: PlayerAssetType): Promise<PlayerAssets[]> {
|
||||
return await this.playerAssetsRepository.find({
|
||||
where: assetType ? { user_id: userId, asset_type: assetType } : { user_id: userId },
|
||||
order: { acquired_at: 'ASC', id: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async listAssetIds(userId: bigint, assetType: PlayerAssetType): Promise<string[]> {
|
||||
const rows = await this.listAssets(userId, assetType);
|
||||
return rows.map((row) => row.asset_id);
|
||||
}
|
||||
|
||||
async hasAssetFromSource(userId: bigint, assetType: PlayerAssetType, source: string): Promise<boolean> {
|
||||
const normalizedSource = (source || '').trim();
|
||||
if (!normalizedSource) {
|
||||
return false;
|
||||
}
|
||||
const count = await this.playerAssetsRepository.count({
|
||||
where: { user_id: userId, asset_type: assetType, source: normalizedSource },
|
||||
});
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
private normalizeAssetId(assetId: string): string {
|
||||
const normalized = (assetId || '').trim();
|
||||
if (!/^[A-Za-z0-9_:-]{1,100}$/.test(normalized)) {
|
||||
throw new BadRequestException('资产ID格式不正确');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
73
src/core/db/player_assets/player_assets_memory.service.ts
Normal file
73
src/core/db/player_assets/player_assets_memory.service.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { PlayerAssets, PlayerAssetType } from './player_assets.entity';
|
||||
|
||||
@Injectable()
|
||||
export class PlayerAssetsMemoryService {
|
||||
private assets: Map<bigint, PlayerAssets> = new Map();
|
||||
private userAssetIndex: Map<string, bigint> = new Map();
|
||||
private currentId: bigint = BigInt(1);
|
||||
|
||||
async grantAsset(userId: bigint, assetType: PlayerAssetType, assetId: string, source = 'system', metadata?: Record<string, unknown>): Promise<PlayerAssets> {
|
||||
const normalizedAssetId = this.normalizeAssetId(assetId);
|
||||
const key = this.indexKey(userId, assetType, normalizedAssetId);
|
||||
const existingId = this.userAssetIndex.get(key);
|
||||
if (existingId) {
|
||||
return this.assets.get(existingId) as PlayerAssets;
|
||||
}
|
||||
|
||||
const row = new PlayerAssets();
|
||||
row.id = this.currentId++;
|
||||
row.user_id = userId;
|
||||
row.asset_type = assetType;
|
||||
row.asset_id = normalizedAssetId;
|
||||
row.source = source || 'system';
|
||||
row.metadata = metadata ?? null;
|
||||
row.acquired_at = new Date();
|
||||
this.assets.set(row.id, row);
|
||||
this.userAssetIndex.set(key, row.id);
|
||||
return row;
|
||||
}
|
||||
|
||||
async hasAsset(userId: bigint, assetType: PlayerAssetType, assetId: string): Promise<boolean> {
|
||||
const normalizedAssetId = (assetId || '').trim();
|
||||
return normalizedAssetId ? this.userAssetIndex.has(this.indexKey(userId, assetType, normalizedAssetId)) : false;
|
||||
}
|
||||
|
||||
async listAssets(userId: bigint, assetType?: PlayerAssetType): Promise<PlayerAssets[]> {
|
||||
return Array.from(this.assets.values())
|
||||
.filter((row) => row.user_id === userId && (!assetType || row.asset_type === assetType))
|
||||
.sort((a, b) => {
|
||||
const acquiredDiff = a.acquired_at.getTime() - b.acquired_at.getTime();
|
||||
return acquiredDiff !== 0 ? acquiredDiff : Number(a.id - b.id);
|
||||
});
|
||||
}
|
||||
|
||||
async listAssetIds(userId: bigint, assetType: PlayerAssetType): Promise<string[]> {
|
||||
const rows = await this.listAssets(userId, assetType);
|
||||
return rows.map((row) => row.asset_id);
|
||||
}
|
||||
|
||||
async hasAssetFromSource(userId: bigint, assetType: PlayerAssetType, source: string): Promise<boolean> {
|
||||
const normalizedSource = (source || '').trim();
|
||||
if (!normalizedSource) {
|
||||
return false;
|
||||
}
|
||||
return Array.from(this.assets.values()).some((row) => (
|
||||
row.user_id === userId &&
|
||||
row.asset_type === assetType &&
|
||||
row.source === normalizedSource
|
||||
));
|
||||
}
|
||||
|
||||
private indexKey(userId: bigint, assetType: PlayerAssetType, assetId: string): string {
|
||||
return `${userId.toString()}:${assetType}:${assetId}`;
|
||||
}
|
||||
|
||||
private normalizeAssetId(assetId: string): string {
|
||||
const normalized = (assetId || '').trim();
|
||||
if (!/^[A-Za-z0-9_:-]{1,100}$/.test(normalized)) {
|
||||
throw new BadRequestException('资产ID格式不正确');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
36
src/core/db/player_assets/room_decor_placements.entity.ts
Normal file
36
src/core/db/player_assets/room_decor_placements.entity.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity('room_decor_placements')
|
||||
@Index('idx_room_decor_placements_user_id', ['user_id'])
|
||||
@Index('idx_room_decor_placements_user_decor_unique', ['user_id', 'decor_id'], { unique: true })
|
||||
export class RoomDecorPlacements {
|
||||
@PrimaryGeneratedColumn({ type: 'bigint', comment: '主键ID' })
|
||||
id: bigint;
|
||||
|
||||
@Column({ type: 'bigint', nullable: false, comment: '关联users.id' })
|
||||
user_id: bigint;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, nullable: false, comment: '房间摆件ID' })
|
||||
decor_id: string;
|
||||
|
||||
@Column({ type: 'boolean', nullable: false, default: false, comment: '是否已摆放' })
|
||||
placed: boolean;
|
||||
|
||||
@Column({ type: 'float', nullable: true, comment: '房间内X坐标' })
|
||||
position_x: number | null;
|
||||
|
||||
@Column({ type: 'float', nullable: true, comment: '房间内Y坐标' })
|
||||
position_y: number | null;
|
||||
|
||||
@Column({ type: 'float', nullable: false, default: 1, comment: '摆件缩放' })
|
||||
scale: number;
|
||||
|
||||
@Column({ type: 'int', nullable: false, default: 0, comment: '摆放层级' })
|
||||
z_index: number;
|
||||
|
||||
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', comment: '创建时间' })
|
||||
created_at: Date;
|
||||
|
||||
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP', comment: '更新时间' })
|
||||
updated_at: Date;
|
||||
}
|
||||
48
src/core/db/player_assets/room_decor_placements.service.ts
Normal file
48
src/core/db/player_assets/room_decor_placements.service.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { SaveRoomDecorPlacementDto } from '../../../business/room_decor/dto/save_room_decor_placement.dto';
|
||||
import { RoomDecorPlacements } from './room_decor_placements.entity';
|
||||
|
||||
@Injectable()
|
||||
export class RoomDecorPlacementsService {
|
||||
constructor(
|
||||
@InjectRepository(RoomDecorPlacements)
|
||||
private readonly placementsRepository: Repository<RoomDecorPlacements>,
|
||||
) {}
|
||||
|
||||
async listPlacements(userId: bigint): Promise<RoomDecorPlacements[]> {
|
||||
return await this.placementsRepository.find({
|
||||
where: { user_id: userId },
|
||||
order: { created_at: 'ASC', id: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async savePlacement(userId: bigint, placement: SaveRoomDecorPlacementDto): Promise<RoomDecorPlacements> {
|
||||
const decorId = this.normalizeDecorId(placement.decor_id);
|
||||
let row = await this.placementsRepository.findOne({
|
||||
where: { user_id: userId, decor_id: decorId },
|
||||
});
|
||||
if (!row) {
|
||||
row = new RoomDecorPlacements();
|
||||
row.user_id = userId;
|
||||
row.decor_id = decorId;
|
||||
row.created_at = new Date();
|
||||
}
|
||||
row.placed = placement.placed;
|
||||
row.position_x = placement.placed ? Number(placement.position_x ?? row.position_x ?? 0) : null;
|
||||
row.position_y = placement.placed ? Number(placement.position_y ?? row.position_y ?? 0) : null;
|
||||
row.scale = Number(placement.scale ?? row.scale ?? 1);
|
||||
row.z_index = Number(placement.z_index ?? row.z_index ?? 0);
|
||||
row.updated_at = new Date();
|
||||
return await this.placementsRepository.save(row);
|
||||
}
|
||||
|
||||
private normalizeDecorId(decorId: string): string {
|
||||
const normalized = (decorId || '').trim();
|
||||
if (!/^[A-Za-z0-9_:-]{1,100}$/.test(normalized)) {
|
||||
throw new BadRequestException('摆件ID格式不正确');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { SaveRoomDecorPlacementDto } from '../../../business/room_decor/dto/save_room_decor_placement.dto';
|
||||
import { RoomDecorPlacements } from './room_decor_placements.entity';
|
||||
|
||||
@Injectable()
|
||||
export class RoomDecorPlacementsMemoryService {
|
||||
private placements: Map<bigint, RoomDecorPlacements> = new Map();
|
||||
private userDecorIndex: Map<string, bigint> = new Map();
|
||||
private currentId: bigint = BigInt(1);
|
||||
|
||||
async listPlacements(userId: bigint): Promise<RoomDecorPlacements[]> {
|
||||
return Array.from(this.placements.values())
|
||||
.filter((row) => row.user_id === userId)
|
||||
.sort((a, b) => {
|
||||
const createdDiff = a.created_at.getTime() - b.created_at.getTime();
|
||||
return createdDiff !== 0 ? createdDiff : Number(a.id - b.id);
|
||||
});
|
||||
}
|
||||
|
||||
async savePlacement(userId: bigint, placement: SaveRoomDecorPlacementDto): Promise<RoomDecorPlacements> {
|
||||
const decorId = this.normalizeDecorId(placement.decor_id);
|
||||
const key = this.indexKey(userId, decorId);
|
||||
const existingId = this.userDecorIndex.get(key);
|
||||
const row = existingId ? this.placements.get(existingId) as RoomDecorPlacements : new RoomDecorPlacements();
|
||||
if (!existingId) {
|
||||
row.id = this.currentId++;
|
||||
row.user_id = userId;
|
||||
row.decor_id = decorId;
|
||||
row.created_at = new Date();
|
||||
this.userDecorIndex.set(key, row.id);
|
||||
this.placements.set(row.id, row);
|
||||
}
|
||||
row.placed = placement.placed;
|
||||
row.position_x = placement.placed ? Number(placement.position_x ?? row.position_x ?? 0) : null;
|
||||
row.position_y = placement.placed ? Number(placement.position_y ?? row.position_y ?? 0) : null;
|
||||
row.scale = Number(placement.scale ?? row.scale ?? 1);
|
||||
row.z_index = Number(placement.z_index ?? row.z_index ?? 0);
|
||||
row.updated_at = new Date();
|
||||
return row;
|
||||
}
|
||||
|
||||
private indexKey(userId: bigint, decorId: string): string {
|
||||
return `${userId.toString()}:${decorId}`;
|
||||
}
|
||||
|
||||
private normalizeDecorId(decorId: string): string {
|
||||
const normalized = (decorId || '').trim();
|
||||
if (!/^[A-Za-z0-9_:-]{1,100}$/.test(normalized)) {
|
||||
throw new BadRequestException('摆件ID格式不正确');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
424
src/core/db/user_profiles/base_user_profiles.service.ts
Normal file
424
src/core/db/user_profiles/base_user_profiles.service.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
/**
|
||||
* 用户档案基础服务类
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供用户档案服务的基础功能和通用方法
|
||||
* - 定义日志记录和性能监控的标准模式
|
||||
* - 实现错误处理和异常管理的统一规范
|
||||
* - 支持双模式运行的基础架构
|
||||
*
|
||||
* 职责分离:
|
||||
* - 日志管理:统一的日志记录格式和级别
|
||||
* - 性能监控:操作耗时统计和性能指标
|
||||
* - 错误处理:标准化的异常处理模式
|
||||
* - 工具方法:通用的辅助功能和验证逻辑
|
||||
*
|
||||
* 继承关系:
|
||||
* - UserProfilesService extends BaseUserProfilesService (MySQL实现)
|
||||
* - UserProfilesMemoryService extends BaseUserProfilesService (内存实现)
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 功能新增 - 创建用户档案基础服务类 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-08
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* 用户档案基础服务抽象类
|
||||
*
|
||||
* 职责:
|
||||
* - 提供所有用户档案服务的通用基础功能
|
||||
* - 定义标准的日志记录和性能监控模式
|
||||
* - 实现统一的错误处理和异常管理
|
||||
* - 支持MySQL和内存两种存储模式
|
||||
*
|
||||
* 设计模式:
|
||||
* - 模板方法模式:定义通用的操作流程
|
||||
* - 策略模式:支持不同的存储实现策略
|
||||
* - 观察者模式:统一的日志和监控机制
|
||||
*
|
||||
* 使用场景:
|
||||
* - 作为具体用户档案服务的基类
|
||||
* - 提供标准化的日志和监控功能
|
||||
* - 实现通用的工具方法和验证逻辑
|
||||
*/
|
||||
export abstract class BaseUserProfilesService {
|
||||
/**
|
||||
* 日志记录器
|
||||
*
|
||||
* 功能:
|
||||
* - 记录用户档案操作的详细日志
|
||||
* - 支持不同级别的日志输出
|
||||
* - 提供结构化的日志格式
|
||||
* - 便于问题排查和性能分析
|
||||
*/
|
||||
protected readonly logger = new Logger(BaseUserProfilesService.name);
|
||||
|
||||
/**
|
||||
* 记录操作开始日志
|
||||
*
|
||||
* 功能描述:
|
||||
* 统一记录操作开始的日志信息,包含操作类型、参数和时间戳
|
||||
*
|
||||
* @param operation 操作名称
|
||||
* @param params 操作参数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* this.logStart('创建用户档案', {
|
||||
* userId: '123',
|
||||
* currentMap: 'plaza'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
protected logStart(operation: string, params: Record<string, any>): void {
|
||||
this.logger.log(`开始${operation}`, {
|
||||
operation: this.formatOperationName(operation),
|
||||
...params,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录操作成功日志
|
||||
*
|
||||
* 功能描述:
|
||||
* 统一记录操作成功的日志信息,包含结果数据和性能指标
|
||||
*
|
||||
* @param operation 操作名称
|
||||
* @param result 操作结果
|
||||
* @param duration 操作耗时(毫秒)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* this.logSuccess('创建用户档案', {
|
||||
* profileId: '456'
|
||||
* }, 150);
|
||||
* ```
|
||||
*/
|
||||
protected logSuccess(operation: string, result: Record<string, any>, duration: number): void {
|
||||
this.logger.log(`${operation}成功`, {
|
||||
operation: this.formatOperationName(operation),
|
||||
...result,
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录操作警告日志
|
||||
*
|
||||
* 功能描述:
|
||||
* 统一记录操作警告的日志信息,用于记录非致命性问题
|
||||
*
|
||||
* @param operation 操作名称
|
||||
* @param warning 警告信息
|
||||
* @param params 相关参数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* this.logWarning('更新用户位置', '用户档案不存在', {
|
||||
* userId: '123'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
protected logWarning(operation: string, warning: string, params: Record<string, any>): void {
|
||||
this.logger.warn(`${operation}警告:${warning}`, {
|
||||
operation: this.formatOperationName(operation),
|
||||
warning,
|
||||
...params,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录操作错误日志
|
||||
*
|
||||
* 功能描述:
|
||||
* 统一记录操作错误的日志信息,包含错误详情和堆栈信息
|
||||
*
|
||||
* @param operation 操作名称
|
||||
* @param error 错误信息
|
||||
* @param params 相关参数
|
||||
* @param duration 操作耗时(毫秒)
|
||||
* @param stack 错误堆栈(可选)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* this.logError('创建用户档案', '数据库连接失败', {
|
||||
* userId: '123'
|
||||
* }, 500, error.stack);
|
||||
* ```
|
||||
*/
|
||||
protected logError(
|
||||
operation: string,
|
||||
error: string,
|
||||
params: Record<string, any>,
|
||||
duration: number,
|
||||
stack?: string
|
||||
): void {
|
||||
this.logger.error(`${operation}失败:${error}`, {
|
||||
operation: this.formatOperationName(operation),
|
||||
error,
|
||||
...params,
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
}, stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理搜索异常
|
||||
*
|
||||
* 功能描述:
|
||||
* 专门处理搜索操作的异常,返回空结果而不抛出异常
|
||||
*
|
||||
* 设计理念:
|
||||
* - 搜索失败不应该影响用户体验
|
||||
* - 返回空结果比抛出异常更友好
|
||||
* - 记录错误日志便于问题排查
|
||||
*
|
||||
* @param error 异常对象
|
||||
* @param operation 操作名称
|
||||
* @param params 操作参数
|
||||
* @returns 空数组
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* try {
|
||||
* return await this.searchProfiles(keyword);
|
||||
* } catch (error) {
|
||||
* return this.handleSearchError(error, '搜索用户档案', { keyword });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected handleSearchError<T>(
|
||||
error: any,
|
||||
operation: string,
|
||||
params: Record<string, any>
|
||||
): T[] {
|
||||
this.logError(
|
||||
operation,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
params,
|
||||
0, // 搜索异常不计算耗时
|
||||
error instanceof Error ? error.stack : undefined
|
||||
);
|
||||
|
||||
// 搜索异常返回空数组,不影响用户体验
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化操作名称
|
||||
*
|
||||
* 功能描述:
|
||||
* 将中文操作名称转换为英文标识符,便于日志分析和监控
|
||||
*
|
||||
* @param operation 中文操作名称
|
||||
* @returns 英文操作标识符
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* this.formatOperationName('创建用户档案'); // 返回: 'createUserProfile'
|
||||
* this.formatOperationName('更新用户位置'); // 返回: 'updateUserPosition'
|
||||
* ```
|
||||
*/
|
||||
private formatOperationName(operation: string): string {
|
||||
const operationMap: Record<string, string> = {
|
||||
'创建用户档案': 'createUserProfile',
|
||||
'查询用户档案': 'findUserProfile',
|
||||
'更新用户档案': 'updateUserProfile',
|
||||
'更新用户位置': 'updateUserPosition',
|
||||
'删除用户档案': 'removeUserProfile',
|
||||
'搜索用户档案': 'searchUserProfiles',
|
||||
'查询地图用户': 'findUsersByMap',
|
||||
'批量更新状态': 'batchUpdateStatus',
|
||||
'统计用户数量': 'countUserProfiles'
|
||||
};
|
||||
|
||||
return operationMap[operation] || operation.toLowerCase().replace(/\s+/g, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证用户ID格式
|
||||
*
|
||||
* 功能描述:
|
||||
* 验证用户ID是否为有效的bigint格式
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @returns 是否有效
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* if (!this.isValidUserId(userId)) {
|
||||
* throw new BadRequestException('用户ID格式无效');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected isValidUserId(userId: any): userId is bigint {
|
||||
try {
|
||||
const id = BigInt(userId);
|
||||
return id > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证坐标格式
|
||||
*
|
||||
* 功能描述:
|
||||
* 验证位置坐标是否为有效的数字格式
|
||||
*
|
||||
* @param coordinate 坐标值
|
||||
* @returns 是否有效
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* if (!this.isValidCoordinate(posX) || !this.isValidCoordinate(posY)) {
|
||||
* throw new BadRequestException('坐标格式无效');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected isValidCoordinate(coordinate: any): coordinate is number {
|
||||
return typeof coordinate === 'number' &&
|
||||
!isNaN(coordinate) &&
|
||||
isFinite(coordinate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证地图名称格式
|
||||
*
|
||||
* 功能描述:
|
||||
* 验证地图名称是否符合规范要求
|
||||
*
|
||||
* @param mapName 地图名称
|
||||
* @returns 是否有效
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* if (!this.isValidMapName(currentMap)) {
|
||||
* throw new BadRequestException('地图名称格式无效');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected isValidMapName(mapName: any): mapName is string {
|
||||
return typeof mapName === 'string' &&
|
||||
mapName.length > 0 &&
|
||||
mapName.length <= 50 &&
|
||||
/^[a-zA-Z0-9_-]+$/.test(mapName); // 只允许字母、数字、下划线、连字符
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理敏感数据
|
||||
*
|
||||
* 功能描述:
|
||||
* 从日志数据中移除敏感信息,保护用户隐私
|
||||
*
|
||||
* @param data 原始数据
|
||||
* @returns 清理后的数据
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const safeData = this.sanitizeLogData({
|
||||
* userId: '123',
|
||||
* email: 'user@example.com',
|
||||
* password: 'secret123'
|
||||
* });
|
||||
* // 返回: { userId: '123', email: 'u***@example.com', password: '***' }
|
||||
* ```
|
||||
*/
|
||||
protected sanitizeLogData(data: Record<string, any>): Record<string, any> {
|
||||
const sensitiveFields = ['password', 'token', 'secret', 'key'];
|
||||
const emailFields = ['email'];
|
||||
|
||||
const sanitized = { ...data };
|
||||
|
||||
for (const [key, value] of Object.entries(sanitized)) {
|
||||
const lowerKey = key.toLowerCase();
|
||||
|
||||
// 完全隐藏敏感字段
|
||||
if (sensitiveFields.some(field => lowerKey.includes(field))) {
|
||||
sanitized[key] = '***';
|
||||
}
|
||||
// 部分隐藏邮箱字段
|
||||
else if (emailFields.some(field => lowerKey.includes(field)) && typeof value === 'string') {
|
||||
sanitized[key] = this.maskEmail(value);
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮箱脱敏处理
|
||||
*
|
||||
* 功能描述:
|
||||
* 对邮箱地址进行脱敏处理,保护用户隐私
|
||||
*
|
||||
* @param email 邮箱地址
|
||||
* @returns 脱敏后的邮箱
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* this.maskEmail('user@example.com'); // 返回: 'u***@example.com'
|
||||
* this.maskEmail('longusername@test.org'); // 返回: 'l***@test.org'
|
||||
* ```
|
||||
*/
|
||||
private maskEmail(email: string): string {
|
||||
if (!email || !email.includes('@')) {
|
||||
return '***';
|
||||
}
|
||||
|
||||
const [username, domain] = email.split('@');
|
||||
if (username.length <= 1) {
|
||||
return `***@${domain}`;
|
||||
}
|
||||
|
||||
return `${username[0]}***@${domain}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算操作耗时
|
||||
*
|
||||
* 功能描述:
|
||||
* 计算操作的执行时间,用于性能监控
|
||||
*
|
||||
* @param startTime 开始时间戳
|
||||
* @returns 耗时(毫秒)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const startTime = Date.now();
|
||||
* // ... 执行操作
|
||||
* const duration = this.calculateDuration(startTime);
|
||||
* this.logSuccess('操作完成', { result }, duration);
|
||||
* ```
|
||||
*/
|
||||
protected calculateDuration(startTime: number): number {
|
||||
return Date.now() - startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成操作ID
|
||||
*
|
||||
* 功能描述:
|
||||
* 生成唯一的操作ID,用于跟踪和关联日志
|
||||
*
|
||||
* @returns 操作ID
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const operationId = this.generateOperationId();
|
||||
* this.logger.log('开始操作', { operationId, ...params });
|
||||
* ```
|
||||
*/
|
||||
protected generateOperationId(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||
}
|
||||
}
|
||||
491
src/core/db/user_profiles/user_profiles.dto.ts
Normal file
491
src/core/db/user_profiles/user_profiles.dto.ts
Normal file
@@ -0,0 +1,491 @@
|
||||
/**
|
||||
* 用户档案数据传输对象模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义用户档案相关的数据传输对象
|
||||
* - 提供数据验证和类型约束
|
||||
* - 支持位置信息的创建和更新操作
|
||||
* - 实现完整的数据传输层抽象
|
||||
*
|
||||
* 职责分离:
|
||||
* - 数据验证:使用class-validator进行输入验证
|
||||
* - 类型定义:TypeScript类型安全保证
|
||||
* - 数据转换:支持前端到后端的数据映射
|
||||
* - 接口规范:统一的API数据格式
|
||||
*
|
||||
* 依赖模块:
|
||||
* - class-validator: 数据验证装饰器
|
||||
* - class-transformer: 数据转换装饰器
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 功能新增 - 创建用户档案DTO,支持位置广播系统 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-08
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import { IsString, IsNumber, IsOptional, IsNotEmpty, IsObject, IsInt, Min, Max, Length } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* 创建用户档案DTO
|
||||
*
|
||||
* 职责:
|
||||
* - 定义创建用户档案时的必需和可选字段
|
||||
* - 提供完整的数据验证规则
|
||||
* - 支持位置信息的初始化
|
||||
*
|
||||
* 验证规则:
|
||||
* - user_id: 必需,正整数
|
||||
* - current_map: 必需,非空字符串,长度1-50
|
||||
* - pos_x, pos_y: 必需,数字类型
|
||||
* - 其他字段: 可选,有相应的格式验证
|
||||
*/
|
||||
export class CreateUserProfileDto {
|
||||
/**
|
||||
* 关联用户ID
|
||||
*
|
||||
* 验证规则:
|
||||
* - 必需字段,不能为空
|
||||
* - 必须是正整数
|
||||
* - 用于关联users表的主键
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '关联的用户ID',
|
||||
example: 1,
|
||||
type: 'integer'
|
||||
})
|
||||
@IsNotEmpty({ message: '用户ID不能为空' })
|
||||
@Type(() => Number)
|
||||
user_id: bigint;
|
||||
|
||||
/**
|
||||
* 用户简介
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段
|
||||
* - 字符串类型,最大长度500
|
||||
* - 支持多语言和特殊字符
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: '用户自我介绍',
|
||||
example: '热爱编程的全栈开发者,喜欢探索新技术',
|
||||
maxLength: 500
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString({ message: '简介必须是字符串' })
|
||||
@Length(0, 500, { message: '简介长度不能超过500个字符' })
|
||||
bio?: string;
|
||||
|
||||
/**
|
||||
* 简历内容
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段
|
||||
* - 字符串类型,支持长文本
|
||||
* - 可以包含结构化信息
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: '详细简历内容',
|
||||
example: '5年全栈开发经验,精通React、Node.js、Python等技术栈...'
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString({ message: '简历内容必须是字符串' })
|
||||
resume_content?: string;
|
||||
|
||||
/**
|
||||
* 标签信息
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段
|
||||
* - 对象类型,支持嵌套结构
|
||||
* - 用于存储兴趣、技能等标签
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: '用户标签信息',
|
||||
example: {
|
||||
interests: ['游戏', '编程', '音乐'],
|
||||
skills: ['JavaScript', 'Python', 'React'],
|
||||
personality: ['外向', '创新', '团队合作']
|
||||
}
|
||||
})
|
||||
@IsOptional()
|
||||
@IsObject({ message: '标签信息必须是对象格式' })
|
||||
tags?: Record<string, any>;
|
||||
|
||||
/**
|
||||
* 社交链接
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段
|
||||
* - 对象类型,键值对格式
|
||||
* - 值必须是字符串(URL格式)
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: '社交媒体链接',
|
||||
example: {
|
||||
github: 'https://github.com/username',
|
||||
twitter: 'https://twitter.com/username',
|
||||
linkedin: 'https://linkedin.com/in/username'
|
||||
}
|
||||
})
|
||||
@IsOptional()
|
||||
@IsObject({ message: '社交链接必须是对象格式' })
|
||||
social_links?: Record<string, string>;
|
||||
|
||||
/**
|
||||
* 皮肤ID
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段
|
||||
* - 整数类型,范围1-999999
|
||||
* - 关联皮肤资源库
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: '角色皮肤ID',
|
||||
example: 'classic_whale',
|
||||
maxLength: 100
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString({ message: '皮肤ID必须是字符串' })
|
||||
@Length(1, 100, { message: '皮肤ID长度需在1-100字符之间' })
|
||||
skin_id?: string;
|
||||
|
||||
/**
|
||||
* 当前地图
|
||||
*
|
||||
* 验证规则:
|
||||
* - 必需字段,默认值'plaza'
|
||||
* - 字符串类型,长度1-50
|
||||
* - 不能为空字符串
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '当前所在地图',
|
||||
example: 'plaza',
|
||||
default: 'plaza',
|
||||
minLength: 1,
|
||||
maxLength: 50
|
||||
})
|
||||
@IsString({ message: '地图名称必须是字符串' })
|
||||
@IsNotEmpty({ message: '地图名称不能为空' })
|
||||
@Length(1, 50, { message: '地图名称长度必须在1-50个字符之间' })
|
||||
current_map: string = 'plaza';
|
||||
|
||||
/**
|
||||
* X坐标
|
||||
*
|
||||
* 验证规则:
|
||||
* - 必需字段,默认值0
|
||||
* - 数字类型,支持小数
|
||||
* - 坐标范围由具体地图决定
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'X轴坐标位置',
|
||||
example: 100.5,
|
||||
default: 0,
|
||||
type: 'number'
|
||||
})
|
||||
@IsNumber({}, { message: 'X坐标必须是数字' })
|
||||
@Type(() => Number)
|
||||
pos_x: number = 0;
|
||||
|
||||
/**
|
||||
* Y坐标
|
||||
*
|
||||
* 验证规则:
|
||||
* - 必需字段,默认值0
|
||||
* - 数字类型,支持小数
|
||||
* - 坐标范围由具体地图决定
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'Y轴坐标位置',
|
||||
example: 200.3,
|
||||
default: 0,
|
||||
type: 'number'
|
||||
})
|
||||
@IsNumber({}, { message: 'Y坐标必须是数字' })
|
||||
@Type(() => Number)
|
||||
pos_y: number = 0;
|
||||
|
||||
/**
|
||||
* 用户状态
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段,默认值0(离线)
|
||||
* - 整数类型,范围0-255
|
||||
* - 0: 离线,1: 在线,2: 忙碌,3: 隐身
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: '用户状态',
|
||||
example: 1,
|
||||
default: 0,
|
||||
minimum: 0,
|
||||
maximum: 255,
|
||||
enum: [0, 1, 2, 3],
|
||||
enumName: 'UserProfileStatus'
|
||||
})
|
||||
@IsOptional()
|
||||
@IsInt({ message: '用户状态必须是整数' })
|
||||
@Min(0, { message: '用户状态不能小于0' })
|
||||
@Max(255, { message: '用户状态不能大于255' })
|
||||
status?: number = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户档案DTO
|
||||
*
|
||||
* 职责:
|
||||
* - 定义更新用户档案时的可选字段
|
||||
* - 继承创建DTO的验证规则
|
||||
* - 支持部分字段更新
|
||||
*
|
||||
* 特点:
|
||||
* - 所有字段都是可选的
|
||||
* - 保持与创建DTO相同的验证规则
|
||||
* - 支持灵活的部分更新操作
|
||||
*/
|
||||
export class UpdateUserProfileDto {
|
||||
/**
|
||||
* 用户简介(可选更新)
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: '用户自我介绍',
|
||||
example: '更新后的自我介绍',
|
||||
maxLength: 500
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString({ message: '简介必须是字符串' })
|
||||
@Length(0, 500, { message: '简介长度不能超过500个字符' })
|
||||
bio?: string;
|
||||
|
||||
/**
|
||||
* 简历内容(可选更新)
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: '详细简历内容',
|
||||
example: '更新后的简历内容'
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString({ message: '简历内容必须是字符串' })
|
||||
resume_content?: string;
|
||||
|
||||
/**
|
||||
* 标签信息(可选更新)
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: '用户标签信息',
|
||||
example: {
|
||||
interests: ['新的兴趣'],
|
||||
skills: ['新的技能']
|
||||
}
|
||||
})
|
||||
@IsOptional()
|
||||
@IsObject({ message: '标签信息必须是对象格式' })
|
||||
tags?: Record<string, any>;
|
||||
|
||||
/**
|
||||
* 社交链接(可选更新)
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: '社交媒体链接',
|
||||
example: {
|
||||
github: 'https://github.com/newusername'
|
||||
}
|
||||
})
|
||||
@IsOptional()
|
||||
@IsObject({ message: '社交链接必须是对象格式' })
|
||||
social_links?: Record<string, string>;
|
||||
|
||||
/**
|
||||
* 皮肤ID(可选更新)
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: '角色皮肤ID',
|
||||
example: 'human_whale_directional_v2_8x4',
|
||||
maxLength: 100
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString({ message: '皮肤ID必须是字符串' })
|
||||
@Length(1, 100, { message: '皮肤ID长度需在1-100字符之间' })
|
||||
skin_id?: string;
|
||||
|
||||
/**
|
||||
* 当前地图(可选更新)
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: '当前所在地图',
|
||||
example: 'forest',
|
||||
minLength: 1,
|
||||
maxLength: 50
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString({ message: '地图名称必须是字符串' })
|
||||
@IsNotEmpty({ message: '地图名称不能为空' })
|
||||
@Length(1, 50, { message: '地图名称长度必须在1-50个字符之间' })
|
||||
current_map?: string;
|
||||
|
||||
/**
|
||||
* X坐标(可选更新)
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'X轴坐标位置',
|
||||
example: 150.7,
|
||||
type: 'number'
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber({}, { message: 'X坐标必须是数字' })
|
||||
@Type(() => Number)
|
||||
pos_x?: number;
|
||||
|
||||
/**
|
||||
* Y坐标(可选更新)
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'Y轴坐标位置',
|
||||
example: 250.9,
|
||||
type: 'number'
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber({}, { message: 'Y坐标必须是数字' })
|
||||
@Type(() => Number)
|
||||
pos_y?: number;
|
||||
|
||||
/**
|
||||
* 用户状态(可选更新)
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: '用户状态',
|
||||
example: 2,
|
||||
minimum: 0,
|
||||
maximum: 255,
|
||||
enum: [0, 1, 2, 3]
|
||||
})
|
||||
@IsOptional()
|
||||
@IsInt({ message: '用户状态必须是整数' })
|
||||
@Min(0, { message: '用户状态不能小于0' })
|
||||
@Max(255, { message: '用户状态不能大于255' })
|
||||
status?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 位置更新DTO
|
||||
*
|
||||
* 职责:
|
||||
* - 专门用于位置广播系统的位置更新
|
||||
* - 只包含位置相关的核心字段
|
||||
* - 提供高性能的位置数据传输
|
||||
*
|
||||
* 使用场景:
|
||||
* - WebSocket位置更新消息
|
||||
* - 批量位置同步操作
|
||||
* - 位置广播系统的核心数据结构
|
||||
*/
|
||||
export class UpdatePositionDto {
|
||||
/**
|
||||
* 当前地图
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '当前所在地图',
|
||||
example: 'plaza',
|
||||
minLength: 1,
|
||||
maxLength: 50
|
||||
})
|
||||
@IsString({ message: '地图名称必须是字符串' })
|
||||
@IsNotEmpty({ message: '地图名称不能为空' })
|
||||
@Length(1, 50, { message: '地图名称长度必须在1-50个字符之间' })
|
||||
current_map: string;
|
||||
|
||||
/**
|
||||
* X坐标
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'X轴坐标位置',
|
||||
example: 100.5,
|
||||
type: 'number'
|
||||
})
|
||||
@IsNumber({}, { message: 'X坐标必须是数字' })
|
||||
@Type(() => Number)
|
||||
pos_x: number;
|
||||
|
||||
/**
|
||||
* Y坐标
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'Y轴坐标位置',
|
||||
example: 200.3,
|
||||
type: 'number'
|
||||
})
|
||||
@IsNumber({}, { message: 'Y坐标必须是数字' })
|
||||
@Type(() => Number)
|
||||
pos_y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户档案查询DTO
|
||||
*
|
||||
* 职责:
|
||||
* - 定义查询用户档案时的过滤条件
|
||||
* - 支持分页和排序参数
|
||||
* - 提供灵活的查询选项
|
||||
*/
|
||||
export class QueryUserProfileDto {
|
||||
/**
|
||||
* 地图过滤
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: '按地图过滤用户',
|
||||
example: 'plaza'
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString({ message: '地图名称必须是字符串' })
|
||||
current_map?: string;
|
||||
|
||||
/**
|
||||
* 状态过滤
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: '按状态过滤用户',
|
||||
example: 1,
|
||||
enum: [0, 1, 2, 3]
|
||||
})
|
||||
@IsOptional()
|
||||
@IsInt({ message: '状态必须是整数' })
|
||||
@Min(0, { message: '状态不能小于0' })
|
||||
@Max(255, { message: '状态不能大于255' })
|
||||
status?: number;
|
||||
|
||||
/**
|
||||
* 分页大小
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: '每页数量',
|
||||
example: 20,
|
||||
default: 20,
|
||||
minimum: 1,
|
||||
maximum: 100
|
||||
})
|
||||
@IsOptional()
|
||||
@IsInt({ message: '分页大小必须是整数' })
|
||||
@Min(1, { message: '分页大小不能小于1' })
|
||||
@Max(100, { message: '分页大小不能超过100' })
|
||||
@Type(() => Number)
|
||||
limit?: number = 20;
|
||||
|
||||
/**
|
||||
* 偏移量
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: '偏移量',
|
||||
example: 0,
|
||||
default: 0,
|
||||
minimum: 0
|
||||
})
|
||||
@IsOptional()
|
||||
@IsInt({ message: '偏移量必须是整数' })
|
||||
@Min(0, { message: '偏移量不能小于0' })
|
||||
@Type(() => Number)
|
||||
offset?: number = 0;
|
||||
}
|
||||
403
src/core/db/user_profiles/user_profiles.entity.ts
Normal file
403
src/core/db/user_profiles/user_profiles.entity.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
/**
|
||||
* 用户档案数据实体模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义用户档案表的实体映射和字段约束
|
||||
* - 提供用户档案数据的持久化存储结构
|
||||
* - 支持用户位置信息和档案数据存储
|
||||
* - 实现完整的用户档案数据模型和关系映射
|
||||
*
|
||||
* 职责分离:
|
||||
* - 数据映射:TypeORM实体与数据库表的映射关系
|
||||
* - 约束定义:字段类型、长度、唯一性等约束规则
|
||||
* - 关系管理:与其他实体的关联关系定义
|
||||
* - 索引优化:数据库查询性能优化策略
|
||||
*
|
||||
* 依赖模块:
|
||||
* - TypeORM: ORM框架,提供数据库映射功能
|
||||
* - MySQL: 底层数据库存储
|
||||
*
|
||||
* 数据库表:user_profiles
|
||||
* 存储引擎:InnoDB
|
||||
* 字符集:utf8mb4
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 功能新增 - 创建用户档案实体,支持位置广播系统 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-08
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
/**
|
||||
* 用户档案实体类
|
||||
*
|
||||
* 职责:
|
||||
* - 映射数据库user_profiles表的结构和约束
|
||||
* - 定义用户档案数据的字段类型和验证规则
|
||||
* - 提供用户位置信息和档案数据的完整数据模型
|
||||
*
|
||||
* 主要功能:
|
||||
* - 用户基础档案信息存储
|
||||
* - 用户位置信息管理(current_map, pos_x, pos_y)
|
||||
* - 用户状态和活跃度跟踪
|
||||
* - 自动时间戳记录和更新
|
||||
*
|
||||
* 数据完整性:
|
||||
* - 主键约束:id字段自增主键
|
||||
* - 外键约束:user_id关联users表
|
||||
* - 非空约束:user_id, current_map, pos_x, pos_y
|
||||
* - 默认值:current_map='plaza', pos_x=0, pos_y=0
|
||||
*
|
||||
* 使用场景:
|
||||
* - 用户档案信息查询和更新
|
||||
* - 位置广播系统的位置数据存储
|
||||
* - 用户活跃度统计和分析
|
||||
* - 游戏内用户状态管理
|
||||
*
|
||||
* 索引策略:
|
||||
* - 主键索引:id (自动创建)
|
||||
* - 唯一索引:user_id (用户唯一档案)
|
||||
* - 普通索引:current_map (用于地图查询)
|
||||
* - 复合索引:current_map + status (用于活跃用户查询)
|
||||
*/
|
||||
@Entity('user_profiles')
|
||||
export class UserProfiles {
|
||||
/**
|
||||
* 档案主键ID
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:BIGINT,支持大量档案数据
|
||||
* - 约束:主键、非空、自增
|
||||
* - 范围:1 ~ 9,223,372,036,854,775,807
|
||||
*
|
||||
* 业务规则:
|
||||
* - 系统自动生成,不可手动指定
|
||||
* - 全局唯一标识符,用于档案关联
|
||||
* - 作为其他表的外键引用
|
||||
*/
|
||||
@PrimaryGeneratedColumn({
|
||||
type: 'bigint',
|
||||
comment: '主键ID'
|
||||
})
|
||||
id: bigint;
|
||||
|
||||
/**
|
||||
* 关联用户ID
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:BIGINT,与users表id字段对应
|
||||
* - 约束:非空、唯一索引
|
||||
* - 外键:关联users表的主键
|
||||
*
|
||||
* 业务规则:
|
||||
* - 每个用户只能有一个档案记录
|
||||
* - 用于关联用户基础信息和档案信息
|
||||
* - 删除用户时需要同步处理档案数据
|
||||
*
|
||||
* 性能考虑:
|
||||
* - 建立唯一索引,确保一对一关系
|
||||
* - 用于JOIN查询用户完整信息
|
||||
*/
|
||||
@Column({
|
||||
type: 'bigint',
|
||||
nullable: false,
|
||||
unique: true,
|
||||
comment: '关联users.id'
|
||||
})
|
||||
user_id: bigint;
|
||||
|
||||
/**
|
||||
* 用户简介
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:VARCHAR(500),支持较长的自我介绍
|
||||
* - 约束:允许空,无唯一性要求
|
||||
* - 字符集:utf8mb4,支持emoji表情
|
||||
*
|
||||
* 业务规则:
|
||||
* - 用户自定义的个人简介信息
|
||||
* - 支持多语言和特殊字符
|
||||
* - 长度限制:最多500个字符
|
||||
* - 可用于用户搜索和推荐
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 500,
|
||||
nullable: true,
|
||||
comment: '自我介绍'
|
||||
})
|
||||
bio?: string;
|
||||
|
||||
/**
|
||||
* 简历内容
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:TEXT,支持大量文本内容
|
||||
* - 约束:允许空,无长度限制
|
||||
* - 存储:适合存储结构化的简历信息
|
||||
*
|
||||
* 业务规则:
|
||||
* - 用户的详细简历或经历信息
|
||||
* - 支持富文本或结构化数据
|
||||
* - 可用于职业匹配和推荐
|
||||
* - 隐私敏感,需要权限控制
|
||||
*/
|
||||
@Column({
|
||||
type: 'text',
|
||||
nullable: true,
|
||||
comment: '个人详细简历'
|
||||
})
|
||||
resume_content?: string;
|
||||
|
||||
/**
|
||||
* 标签信息
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:JSON,支持结构化标签数据
|
||||
* - 约束:允许空,灵活的数据结构
|
||||
* - 存储:JSON格式,便于查询和过滤
|
||||
*
|
||||
* 业务规则:
|
||||
* - 用户的兴趣标签、技能标签等
|
||||
* - 支持多维度标签分类
|
||||
* - 用于用户匹配和内容推荐
|
||||
* - 支持动态添加和删除标签
|
||||
*
|
||||
* 数据格式示例:
|
||||
* ```json
|
||||
* {
|
||||
* "interests": ["游戏", "编程", "音乐"],
|
||||
* "skills": ["JavaScript", "Python", "React"],
|
||||
* "personality": ["外向", "创新", "团队合作"]
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Column({
|
||||
type: 'json',
|
||||
nullable: true,
|
||||
comment: '身份标签信息'
|
||||
})
|
||||
tags?: Record<string, any>;
|
||||
|
||||
/**
|
||||
* 社交链接
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:JSON,支持多个社交平台链接
|
||||
* - 约束:允许空,灵活的数据结构
|
||||
* - 存储:JSON格式,便于扩展新平台
|
||||
*
|
||||
* 业务规则:
|
||||
* - 用户的各种社交媒体链接
|
||||
* - 支持GitHub、Twitter、LinkedIn等平台
|
||||
* - 用于用户社交网络建立
|
||||
* - 需要验证链接的有效性
|
||||
*
|
||||
* 数据格式示例:
|
||||
* ```json
|
||||
* {
|
||||
* "github": "https://github.com/username",
|
||||
* "twitter": "https://twitter.com/username",
|
||||
* "linkedin": "https://linkedin.com/in/username",
|
||||
* "website": "https://personal-website.com"
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Column({
|
||||
type: 'json',
|
||||
nullable: true,
|
||||
comment: '社交链接信息'
|
||||
})
|
||||
social_links?: Record<string, string>;
|
||||
|
||||
/**
|
||||
* 皮肤ID
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:INT,整数类型
|
||||
* - 约束:允许空,默认值null
|
||||
* - 范围:支持大量皮肤选择
|
||||
*
|
||||
* 业务规则:
|
||||
* - 用户选择的游戏皮肤或主题
|
||||
* - 关联皮肤资源库的ID
|
||||
* - 影响游戏内角色外观
|
||||
* - 支持皮肤商城和个性化定制
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 100,
|
||||
nullable: true,
|
||||
comment: '角色外观皮肤ID'
|
||||
})
|
||||
skin_id?: string;
|
||||
|
||||
/**
|
||||
* 当前地图
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:VARCHAR(50),支持地图名称
|
||||
* - 约束:非空、默认值'plaza'
|
||||
* - 索引:用于地图用户查询
|
||||
*
|
||||
* 业务规则:
|
||||
* - 用户当前所在的游戏地图
|
||||
* - 用于位置广播系统的地图过滤
|
||||
* - 影响用户可见性和交互范围
|
||||
* - 默认为广场(plaza),新用户的起始位置
|
||||
*
|
||||
* 位置广播系统:
|
||||
* - 核心字段,用于确定用户所在区域
|
||||
* - 同一地图的用户可以相互看到位置
|
||||
* - 切换地图时需要更新此字段
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 50,
|
||||
nullable: false,
|
||||
default: 'plaza',
|
||||
comment: '当前所在地图'
|
||||
})
|
||||
current_map: string;
|
||||
|
||||
/**
|
||||
* X坐标位置
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:FLOAT,支持小数坐标
|
||||
* - 约束:非空、默认值0
|
||||
* - 精度:单精度浮点数,满足游戏精度需求
|
||||
*
|
||||
* 业务规则:
|
||||
* - 用户在当前地图的X轴坐标
|
||||
* - 用于位置广播系统的精确定位
|
||||
* - 坐标范围由具体地图决定
|
||||
* - 默认值0表示地图中心或起始点
|
||||
*
|
||||
* 位置广播系统:
|
||||
* - 核心字段,用于计算用户间距离
|
||||
* - 实时更新,频繁读写操作
|
||||
* - 需要与Redis缓存保持同步
|
||||
*/
|
||||
@Column({
|
||||
type: 'float',
|
||||
nullable: false,
|
||||
default: 0,
|
||||
comment: 'X坐标(横轴)'
|
||||
})
|
||||
pos_x: number;
|
||||
|
||||
/**
|
||||
* Y坐标位置
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:FLOAT,支持小数坐标
|
||||
* - 约束:非空、默认值0
|
||||
* - 精度:单精度浮点数,满足游戏精度需求
|
||||
*
|
||||
* 业务规则:
|
||||
* - 用户在当前地图的Y轴坐标
|
||||
* - 用于位置广播系统的精确定位
|
||||
* - 坐标范围由具体地图决定
|
||||
* - 默认值0表示地图中心或起始点
|
||||
*
|
||||
* 位置广播系统:
|
||||
* - 核心字段,用于计算用户间距离
|
||||
* - 实时更新,频繁读写操作
|
||||
* - 需要与Redis缓存保持同步
|
||||
*/
|
||||
@Column({
|
||||
type: 'float',
|
||||
nullable: false,
|
||||
default: 0,
|
||||
comment: 'Y坐标(纵轴)'
|
||||
})
|
||||
pos_y: number;
|
||||
|
||||
/**
|
||||
* 用户状态
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:TINYINT,节省存储空间
|
||||
* - 约束:非空、默认值0
|
||||
* - 范围:0-255,支持多种状态
|
||||
*
|
||||
* 业务规则:
|
||||
* - 用户当前的活动状态
|
||||
* - 0: 离线,1: 在线,2: 忙碌,3: 隐身等
|
||||
* - 影响位置广播的可见性
|
||||
* - 用于用户活跃度统计
|
||||
*
|
||||
* 位置广播系统:
|
||||
* - 影响位置信息的广播范围
|
||||
* - 隐身用户不参与位置广播
|
||||
* - 离线用户需要清理位置缓存
|
||||
*/
|
||||
@Column({
|
||||
type: 'tinyint',
|
||||
nullable: false,
|
||||
default: 0,
|
||||
comment: '状态:0-离线,1-在线,2-忙碌,3-隐身'
|
||||
})
|
||||
status: number;
|
||||
|
||||
/**
|
||||
* 最后登录时间
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:DATETIME,精确到秒
|
||||
* - 约束:允许空,新用户可能为空
|
||||
* - 时区:使用系统时区,建议UTC
|
||||
*
|
||||
* 业务规则:
|
||||
* - 记录用户最后一次登录的时间
|
||||
* - 用于用户活跃度分析
|
||||
* - 支持长时间未登录用户的清理
|
||||
* - 影响位置数据的有效性判断
|
||||
*
|
||||
* 位置广播系统:
|
||||
* - 用于判断位置数据的时效性
|
||||
* - 长时间未登录的用户位置数据可能过期
|
||||
* - 支持基于登录时间的数据清理策略
|
||||
*/
|
||||
@Column({
|
||||
type: 'datetime',
|
||||
nullable: true,
|
||||
comment: '最后登录时间'
|
||||
})
|
||||
last_login_at?: Date;
|
||||
|
||||
/**
|
||||
* 最后位置更新时间
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:DATETIME,精确到秒
|
||||
* - 约束:允许空,默认值null
|
||||
* - 时区:使用系统时区,建议UTC
|
||||
*
|
||||
* 业务规则:
|
||||
* - 记录用户位置最后更新的时间
|
||||
* - 用于位置数据的缓存失效判断
|
||||
* - 支持位置更新频率的统计分析
|
||||
* - 用于清理过期的位置缓存数据
|
||||
*
|
||||
* 位置广播系统:
|
||||
* - 核心字段,用于缓存同步策略
|
||||
* - 判断Redis中位置数据是否需要更新
|
||||
* - 支持增量同步和数据一致性保证
|
||||
* - 用于性能监控和优化
|
||||
*
|
||||
* 注意:此字段需要通过ALTER TABLE添加到现有表中
|
||||
*/
|
||||
@Column({
|
||||
type: 'datetime',
|
||||
nullable: true,
|
||||
default: null,
|
||||
comment: '最后位置更新时间,用于位置广播系统'
|
||||
})
|
||||
last_position_update?: Date;
|
||||
}
|
||||
225
src/core/db/user_profiles/user_profiles.module.ts
Normal file
225
src/core/db/user_profiles/user_profiles.module.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* 用户档案模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供用户档案数据访问的完整模块配置
|
||||
* - 支持MySQL和内存两种存储模式的动态切换
|
||||
* - 集成TypeORM实体和服务的依赖注入
|
||||
* - 为位置广播系统提供数据持久化支持
|
||||
*
|
||||
* 职责分离:
|
||||
* - 模块配置:定义模块的导入、提供者和导出
|
||||
* - 依赖注入:配置服务和存储库的注入关系
|
||||
* - 存储模式:支持数据库和内存两种存储实现
|
||||
* - 接口抽象:提供统一的服务接口供业务层使用
|
||||
*
|
||||
* 存储模式:
|
||||
* - 数据库模式:使用TypeORM连接MySQL数据库
|
||||
* - 内存模式:使用Map存储,适用于开发和测试
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 功能新增 - 创建用户档案模块,支持位置广播系统 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-08
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import { Module, DynamicModule, Global } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UserProfiles } from './user_profiles.entity';
|
||||
import { UserProfilesService } from './user_profiles.service';
|
||||
import { UserProfilesMemoryService } from './user_profiles_memory.service';
|
||||
|
||||
/**
|
||||
* 用户档案模块类
|
||||
*
|
||||
* 职责:
|
||||
* - 配置用户档案相关的服务和实体
|
||||
* - 提供数据库和内存两种存储模式
|
||||
* - 支持动态模块配置和依赖注入
|
||||
* - 为位置广播系统提供数据访问层
|
||||
*
|
||||
* 模块特性:
|
||||
* - 动态模块:支持运行时配置选择
|
||||
* - 双模式支持:数据库模式和内存模式
|
||||
* - 接口统一:提供一致的服务接口
|
||||
* - 可测试性:内存模式便于单元测试
|
||||
*
|
||||
* 使用场景:
|
||||
* - 生产环境:使用数据库模式,数据持久化
|
||||
* - 开发测试:使用内存模式,快速启动
|
||||
* - 单元测试:使用内存模式,隔离测试
|
||||
* - 故障降级:数据库故障时切换到内存模式
|
||||
*/
|
||||
@Global()
|
||||
@Module({})
|
||||
export class UserProfilesModule {
|
||||
|
||||
/**
|
||||
* 配置数据库模式的用户档案模块
|
||||
*
|
||||
* 功能描述:
|
||||
* 创建使用MySQL数据库的用户档案模块配置
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 导入TypeORM模块并注册UserProfiles实体
|
||||
* 2. 提供UserProfilesService作为数据访问服务
|
||||
* 3. 导出服务供其他模块使用
|
||||
* 4. 配置依赖注入关系
|
||||
*
|
||||
* 适用场景:
|
||||
* - 生产环境部署
|
||||
* - 需要数据持久化的场景
|
||||
* - 多实例部署的数据共享
|
||||
* - 大数据量的用户档案管理
|
||||
*
|
||||
* @returns 配置了数据库模式的动态模块
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 在AppModule中使用数据库模式
|
||||
* @Module({
|
||||
* imports: [
|
||||
* UserProfilesModule.forDatabase(),
|
||||
* // 其他模块...
|
||||
* ],
|
||||
* })
|
||||
* export class AppModule {}
|
||||
* ```
|
||||
*/
|
||||
static forDatabase(): DynamicModule {
|
||||
return {
|
||||
module: UserProfilesModule,
|
||||
imports: [
|
||||
// 导入TypeORM模块,注册UserProfiles实体
|
||||
TypeOrmModule.forFeature([UserProfiles])
|
||||
],
|
||||
providers: [
|
||||
// 提供MySQL数据库实现的用户档案服务
|
||||
UserProfilesService,
|
||||
{
|
||||
// 使用接口名称作为注入令牌,便于依赖注入
|
||||
provide: 'IUserProfilesService',
|
||||
useClass: UserProfilesService,
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
// 导出服务供其他模块使用
|
||||
UserProfilesService,
|
||||
'IUserProfilesService',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置内存模式的用户档案模块
|
||||
*
|
||||
* 功能描述:
|
||||
* 创建使用内存存储的用户档案模块配置
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 提供UserProfilesMemoryService作为内存存储服务
|
||||
* 2. 使用Map数据结构进行内存数据管理
|
||||
* 3. 导出服务供其他模块使用
|
||||
* 4. 配置统一的服务接口
|
||||
*
|
||||
* 适用场景:
|
||||
* - 开发环境快速启动
|
||||
* - 单元测试和集成测试
|
||||
* - 演示和原型开发
|
||||
* - 数据库故障时的降级方案
|
||||
*
|
||||
* 性能特点:
|
||||
* - 启动速度快,无需数据库连接
|
||||
* - 读写性能高,直接内存访问
|
||||
* - 数据易失,重启后数据丢失
|
||||
* - 内存占用,大数据量时需注意
|
||||
*
|
||||
* @returns 配置了内存模式的动态模块
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 在测试模块中使用内存模式
|
||||
* @Module({
|
||||
* imports: [
|
||||
* UserProfilesModule.forMemory(),
|
||||
* // 其他测试模块...
|
||||
* ],
|
||||
* })
|
||||
* export class TestModule {}
|
||||
* ```
|
||||
*/
|
||||
static forMemory(): DynamicModule {
|
||||
return {
|
||||
module: UserProfilesModule,
|
||||
providers: [
|
||||
// 提供内存存储实现的用户档案服务
|
||||
UserProfilesMemoryService,
|
||||
{
|
||||
// 使用接口名称作为注入令牌,保持接口一致性
|
||||
provide: 'IUserProfilesService',
|
||||
useClass: UserProfilesMemoryService,
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
// 导出服务供其他模块使用
|
||||
UserProfilesMemoryService,
|
||||
'IUserProfilesService',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置自动选择存储模式
|
||||
*
|
||||
* 功能描述:
|
||||
* 根据环境变量或配置参数自动选择数据库或内存模式
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 读取环境变量或配置参数
|
||||
* 2. 根据配置选择对应的存储模式
|
||||
* 3. 返回相应的动态模块配置
|
||||
* 4. 支持运行时模式切换
|
||||
*
|
||||
* 配置规则:
|
||||
* - DB_HOST存在且不为空:使用数据库模式
|
||||
* - DB_HOST不存在或为空:使用内存模式
|
||||
* - NODE_ENV=test:强制使用内存模式
|
||||
* - USE_MEMORY_STORAGE=true:强制使用内存模式
|
||||
*
|
||||
* @param useMemory 是否强制使用内存模式(可选)
|
||||
* @returns 自动选择的动态模块配置
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 在AppModule中使用自动模式选择
|
||||
* @Module({
|
||||
* imports: [
|
||||
* UserProfilesModule.forRoot(),
|
||||
* // 其他模块...
|
||||
* ],
|
||||
* })
|
||||
* export class AppModule {}
|
||||
*
|
||||
* // 强制使用内存模式
|
||||
* UserProfilesModule.forRoot(true);
|
||||
* ```
|
||||
*/
|
||||
static forRoot(useMemory?: boolean): DynamicModule {
|
||||
// 自动检测存储模式
|
||||
const shouldUseMemory = useMemory ?? (
|
||||
process.env.NODE_ENV === 'test' ||
|
||||
process.env.USE_MEMORY_STORAGE === 'true' ||
|
||||
!process.env.DB_HOST
|
||||
);
|
||||
|
||||
// 根据检测结果选择对应的模块配置
|
||||
if (shouldUseMemory) {
|
||||
return this.forMemory();
|
||||
} else {
|
||||
return this.forDatabase();
|
||||
}
|
||||
}
|
||||
}
|
||||
621
src/core/db/user_profiles/user_profiles.service.ts
Normal file
621
src/core/db/user_profiles/user_profiles.service.ts
Normal file
@@ -0,0 +1,621 @@
|
||||
/**
|
||||
* 用户档案服务类
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供用户档案数据的增删改查技术实现
|
||||
* - 处理位置信息的持久化和存储操作
|
||||
* - 数据格式验证和约束检查
|
||||
* - 支持完整的用户档案生命周期管理
|
||||
*
|
||||
* 职责分离:
|
||||
* - 数据持久化:通过TypeORM操作MySQL数据库
|
||||
* - 数据验证:数据格式和约束完整性检查
|
||||
* - 异常处理:统一的错误处理和日志记录
|
||||
* - 性能监控:操作耗时统计和性能优化
|
||||
*
|
||||
* 位置广播系统集成:
|
||||
* - 位置数据的持久化存储
|
||||
* - 支持位置更新时间戳管理
|
||||
* - 提供地图用户查询功能
|
||||
* - 实现位置数据的批量操作
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 功能新增 - 创建用户档案服务,支持位置广播系统 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-08
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, FindOptionsWhere } from 'typeorm';
|
||||
import { UserProfiles } from './user_profiles.entity';
|
||||
import { CreateUserProfileDto, UpdateUserProfileDto, UpdatePositionDto, QueryUserProfileDto } from './user_profiles.dto';
|
||||
import { validate } from 'class-validator';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { BaseUserProfilesService } from './base_user_profiles.service';
|
||||
|
||||
@Injectable()
|
||||
export class UserProfilesService extends BaseUserProfilesService {
|
||||
|
||||
constructor(
|
||||
@InjectRepository(UserProfiles)
|
||||
private readonly userProfilesRepository: Repository<UserProfiles>,
|
||||
) {
|
||||
super(); // 调用基类构造函数
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新用户档案
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 验证输入数据的格式和完整性
|
||||
* 2. 使用class-validator进行DTO数据验证
|
||||
* 3. 检查用户ID的唯一性约束
|
||||
* 4. 创建用户档案实体并设置默认值
|
||||
* 5. 保存用户档案数据到数据库
|
||||
* 6. 记录操作日志和性能指标
|
||||
* 7. 返回创建成功的用户档案实体
|
||||
*
|
||||
* @param createUserProfileDto 创建用户档案的数据传输对象
|
||||
* @returns 创建成功的用户档案实体,包含自动生成的ID和时间戳
|
||||
* @throws BadRequestException 当数据验证失败或输入格式错误时
|
||||
* @throws ConflictException 当用户ID已存在档案时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const newProfile = await userProfilesService.create({
|
||||
* user_id: BigInt(1),
|
||||
* current_map: 'plaza',
|
||||
* pos_x: 0,
|
||||
* pos_y: 0,
|
||||
* bio: '新用户'
|
||||
* });
|
||||
* console.log(`用户档案创建成功,ID: ${newProfile.id}`);
|
||||
* ```
|
||||
*/
|
||||
async create(createUserProfileDto: CreateUserProfileDto): Promise<UserProfiles> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logger.log('开始创建用户档案', {
|
||||
operation: 'create',
|
||||
userId: createUserProfileDto.user_id.toString(),
|
||||
currentMap: createUserProfileDto.current_map,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
// 验证DTO
|
||||
const dto = plainToClass(CreateUserProfileDto, createUserProfileDto);
|
||||
const validationErrors = await validate(dto);
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
const errorMessages = validationErrors.map(error =>
|
||||
Object.values(error.constraints || {}).join(', ')
|
||||
).join('; ');
|
||||
|
||||
this.logger.warn('用户档案创建失败:数据验证失败', {
|
||||
operation: 'create',
|
||||
userId: createUserProfileDto.user_id.toString(),
|
||||
validationErrors: errorMessages
|
||||
});
|
||||
|
||||
throw new BadRequestException(`数据验证失败: ${errorMessages}`);
|
||||
}
|
||||
|
||||
// 检查用户ID是否已存在档案
|
||||
const existingProfile = await this.userProfilesRepository.findOne({
|
||||
where: { user_id: createUserProfileDto.user_id }
|
||||
});
|
||||
|
||||
if (existingProfile) {
|
||||
this.logger.warn('用户档案创建失败:用户ID已存在档案', {
|
||||
operation: 'create',
|
||||
userId: createUserProfileDto.user_id.toString(),
|
||||
existingProfileId: existingProfile.id.toString()
|
||||
});
|
||||
|
||||
throw new ConflictException('该用户已存在档案记录');
|
||||
}
|
||||
|
||||
// 创建用户档案实体
|
||||
const userProfile = new UserProfiles();
|
||||
userProfile.user_id = createUserProfileDto.user_id;
|
||||
userProfile.bio = createUserProfileDto.bio || null;
|
||||
userProfile.resume_content = createUserProfileDto.resume_content || null;
|
||||
userProfile.tags = createUserProfileDto.tags || null;
|
||||
userProfile.social_links = createUserProfileDto.social_links || null;
|
||||
userProfile.skin_id = createUserProfileDto.skin_id || null;
|
||||
userProfile.current_map = createUserProfileDto.current_map || 'plaza';
|
||||
userProfile.pos_x = createUserProfileDto.pos_x || 0;
|
||||
userProfile.pos_y = createUserProfileDto.pos_y || 0;
|
||||
userProfile.status = createUserProfileDto.status || 0;
|
||||
userProfile.last_position_update = new Date(); // 设置初始位置更新时间
|
||||
|
||||
// 保存到数据库
|
||||
const savedProfile = await this.userProfilesRepository.save(userProfile);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.log('用户档案创建成功', {
|
||||
operation: 'create',
|
||||
profileId: savedProfile.id.toString(),
|
||||
userId: savedProfile.user_id.toString(),
|
||||
currentMap: savedProfile.current_map,
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return savedProfile;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (error instanceof BadRequestException || error instanceof ConflictException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.error('用户档案创建系统异常', {
|
||||
operation: 'create',
|
||||
userId: createUserProfileDto.user_id.toString(),
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
}, error instanceof Error ? error.stack : undefined);
|
||||
|
||||
throw new BadRequestException('用户档案创建失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询用户档案
|
||||
*
|
||||
* @param id 档案ID
|
||||
* @returns 用户档案实体
|
||||
* @throws NotFoundException 当档案不存在时
|
||||
*/
|
||||
async findOne(id: bigint): Promise<UserProfiles> {
|
||||
const profile = await this.userProfilesRepository.findOne({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!profile) {
|
||||
throw new NotFoundException(`ID为 ${id} 的用户档案不存在`);
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID查询用户档案
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @returns 用户档案实体或null
|
||||
*/
|
||||
async findByUserId(userId: bigint): Promise<UserProfiles | null> {
|
||||
return await this.userProfilesRepository.findOne({
|
||||
where: { user_id: userId }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据地图查询用户档案列表
|
||||
*
|
||||
* 功能描述:
|
||||
* 查询指定地图中的所有用户档案,支持状态过滤和分页
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 构建查询条件(地图、状态)
|
||||
* 2. 应用分页参数
|
||||
* 3. 按最后位置更新时间排序
|
||||
* 4. 返回查询结果
|
||||
*
|
||||
* 位置广播系统应用:
|
||||
* - 获取同一地图的所有在线用户
|
||||
* - 支持位置广播的目标用户筛选
|
||||
* - 提供地图用户统计功能
|
||||
*
|
||||
* @param mapId 地图ID
|
||||
* @param status 用户状态过滤(可选)
|
||||
* @param limit 限制数量,默认50
|
||||
* @param offset 偏移量,默认0
|
||||
* @returns 用户档案列表
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 获取plaza地图中的所有在线用户
|
||||
* const onlineUsers = await userProfilesService.findByMap('plaza', 1, 20, 0);
|
||||
*
|
||||
* // 获取forest地图中的所有用户(不限状态)
|
||||
* const allUsers = await userProfilesService.findByMap('forest');
|
||||
* ```
|
||||
*/
|
||||
async findByMap(mapId: string, status?: number, limit: number = 50, offset: number = 0): Promise<UserProfiles[]> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logger.log('开始查询地图用户档案', {
|
||||
operation: 'findByMap',
|
||||
mapId,
|
||||
status,
|
||||
limit,
|
||||
offset,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
// 构建查询条件
|
||||
const whereCondition: FindOptionsWhere<UserProfiles> = {
|
||||
current_map: mapId
|
||||
};
|
||||
|
||||
// 添加状态过滤
|
||||
if (status !== undefined) {
|
||||
whereCondition.status = status;
|
||||
}
|
||||
|
||||
const profiles = await this.userProfilesRepository.find({
|
||||
where: whereCondition,
|
||||
take: limit,
|
||||
skip: offset,
|
||||
order: { last_position_update: 'DESC' }
|
||||
});
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.log('地图用户档案查询成功', {
|
||||
operation: 'findByMap',
|
||||
mapId,
|
||||
status,
|
||||
resultCount: profiles.length,
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return profiles;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.error('地图用户档案查询异常', {
|
||||
operation: 'findByMap',
|
||||
mapId,
|
||||
status,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
}, error instanceof Error ? error.stack : undefined);
|
||||
|
||||
// 查询异常返回空数组而不抛出异常
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户档案信息
|
||||
*
|
||||
* @param id 档案ID
|
||||
* @param updateData 更新的数据
|
||||
* @returns 更新后的用户档案实体
|
||||
* @throws NotFoundException 当档案不存在时
|
||||
*/
|
||||
async update(id: bigint, updateData: UpdateUserProfileDto): Promise<UserProfiles> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logger.log('开始更新用户档案信息', {
|
||||
operation: 'update',
|
||||
profileId: id.toString(),
|
||||
updateFields: Object.keys(updateData),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
// 检查档案是否存在
|
||||
const existingProfile = await this.findOne(id);
|
||||
|
||||
// 合并更新数据
|
||||
Object.assign(existingProfile, updateData);
|
||||
|
||||
// 保存更新后的档案信息
|
||||
const updatedProfile = await this.userProfilesRepository.save(existingProfile);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.log('用户档案信息更新成功', {
|
||||
operation: 'update',
|
||||
profileId: id.toString(),
|
||||
userId: updatedProfile.user_id.toString(),
|
||||
updateFields: Object.keys(updateData),
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return updatedProfile;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.error('用户档案更新系统异常', {
|
||||
operation: 'update',
|
||||
profileId: id.toString(),
|
||||
updateData,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
}, error instanceof Error ? error.stack : undefined);
|
||||
|
||||
throw new BadRequestException('用户档案更新失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户位置信息
|
||||
*
|
||||
* 功能描述:
|
||||
* 专门用于位置广播系统的位置更新操作,高性能优化
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 根据用户ID查找档案记录
|
||||
* 2. 更新位置相关字段(地图、坐标)
|
||||
* 3. 自动更新位置更新时间戳
|
||||
* 4. 执行数据库更新操作
|
||||
* 5. 记录位置更新日志
|
||||
*
|
||||
* 性能优化:
|
||||
* - 只更新位置相关字段,减少数据传输
|
||||
* - 使用部分更新,避免全量数据操作
|
||||
* - 批量操作支持,提高并发性能
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param positionData 位置数据
|
||||
* @returns 更新后的用户档案实体
|
||||
* @throws NotFoundException 当用户档案不存在时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 更新用户位置
|
||||
* const updatedProfile = await userProfilesService.updatePosition(
|
||||
* BigInt(1),
|
||||
* {
|
||||
* current_map: 'forest',
|
||||
* pos_x: 150.5,
|
||||
* pos_y: 200.3
|
||||
* }
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
async updatePosition(userId: bigint, positionData: UpdatePositionDto): Promise<UserProfiles> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logger.log('开始更新用户位置', {
|
||||
operation: 'updatePosition',
|
||||
userId: userId.toString(),
|
||||
currentMap: positionData.current_map,
|
||||
posX: positionData.pos_x,
|
||||
posY: positionData.pos_y,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
// 查找用户档案
|
||||
const profile = await this.userProfilesRepository.findOne({
|
||||
where: { user_id: userId }
|
||||
});
|
||||
|
||||
if (!profile) {
|
||||
this.logger.warn('用户位置更新失败:档案不存在', {
|
||||
operation: 'updatePosition',
|
||||
userId: userId.toString()
|
||||
});
|
||||
|
||||
throw new NotFoundException(`用户ID ${userId} 的档案不存在`);
|
||||
}
|
||||
|
||||
// 更新位置信息
|
||||
profile.current_map = positionData.current_map;
|
||||
profile.pos_x = positionData.pos_x;
|
||||
profile.pos_y = positionData.pos_y;
|
||||
profile.last_position_update = new Date(); // 更新位置更新时间
|
||||
|
||||
// 保存更新
|
||||
const updatedProfile = await this.userProfilesRepository.save(profile);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.log('用户位置更新成功', {
|
||||
operation: 'updatePosition',
|
||||
profileId: updatedProfile.id.toString(),
|
||||
userId: userId.toString(),
|
||||
currentMap: updatedProfile.current_map,
|
||||
posX: updatedProfile.pos_x,
|
||||
posY: updatedProfile.pos_y,
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return updatedProfile;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.error('用户位置更新系统异常', {
|
||||
operation: 'updatePosition',
|
||||
userId: userId.toString(),
|
||||
positionData,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
}, error instanceof Error ? error.stack : undefined);
|
||||
|
||||
throw new BadRequestException('用户位置更新失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新用户状态
|
||||
*
|
||||
* 功能描述:
|
||||
* 批量更新多个用户的状态,用于系统维护和状态同步
|
||||
*
|
||||
* @param userIds 用户ID列表
|
||||
* @param status 目标状态
|
||||
* @returns 更新的记录数量
|
||||
*/
|
||||
async batchUpdateStatus(userIds: bigint[], status: number): Promise<number> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logger.log('开始批量更新用户状态', {
|
||||
operation: 'batchUpdateStatus',
|
||||
userCount: userIds.length,
|
||||
targetStatus: status,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await this.userProfilesRepository.update(
|
||||
{ user_id: { $in: userIds } as any },
|
||||
{ status }
|
||||
);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.log('批量更新用户状态成功', {
|
||||
operation: 'batchUpdateStatus',
|
||||
userCount: userIds.length,
|
||||
targetStatus: status,
|
||||
affectedRows: result.affected || 0,
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return result.affected || 0;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.error('批量更新用户状态异常', {
|
||||
operation: 'batchUpdateStatus',
|
||||
userCount: userIds.length,
|
||||
targetStatus: status,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
}, error instanceof Error ? error.stack : undefined);
|
||||
|
||||
throw new BadRequestException('批量更新用户状态失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户档案列表
|
||||
*
|
||||
* @param queryDto 查询条件
|
||||
* @returns 用户档案列表
|
||||
*/
|
||||
async findAll(queryDto: QueryUserProfileDto = {}): Promise<UserProfiles[]> {
|
||||
const { current_map, status, limit = 20, offset = 0 } = queryDto;
|
||||
|
||||
// 构建查询条件
|
||||
const whereCondition: FindOptionsWhere<UserProfiles> = {};
|
||||
|
||||
if (current_map) {
|
||||
whereCondition.current_map = current_map;
|
||||
}
|
||||
|
||||
if (status !== undefined) {
|
||||
whereCondition.status = status;
|
||||
}
|
||||
|
||||
return await this.userProfilesRepository.find({
|
||||
where: whereCondition,
|
||||
take: limit,
|
||||
skip: offset,
|
||||
order: { last_position_update: 'DESC' }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计用户档案数量
|
||||
*
|
||||
* @param conditions 查询条件
|
||||
* @returns 档案数量
|
||||
*/
|
||||
async count(conditions?: FindOptionsWhere<UserProfiles>): Promise<number> {
|
||||
return await this.userProfilesRepository.count({ where: conditions });
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户档案
|
||||
*
|
||||
* @param id 档案ID
|
||||
* @returns 删除操作结果
|
||||
* @throws NotFoundException 当档案不存在时
|
||||
*/
|
||||
async remove(id: bigint): Promise<{ affected: number; message: string }> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logger.log('开始删除用户档案', {
|
||||
operation: 'remove',
|
||||
profileId: id.toString(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
// 检查档案是否存在
|
||||
await this.findOne(id);
|
||||
|
||||
// 执行删除操作
|
||||
const result = await this.userProfilesRepository.delete({ id });
|
||||
|
||||
const deleteResult = {
|
||||
affected: result.affected || 0,
|
||||
message: `成功删除ID为 ${id} 的用户档案`
|
||||
};
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.log('用户档案删除成功', {
|
||||
operation: 'remove',
|
||||
profileId: id.toString(),
|
||||
affected: deleteResult.affected,
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return deleteResult;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.error('用户档案删除系统异常', {
|
||||
operation: 'remove',
|
||||
profileId: id.toString(),
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
}, error instanceof Error ? error.stack : undefined);
|
||||
|
||||
throw new BadRequestException('用户档案删除失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户档案是否存在
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @returns 是否存在
|
||||
*/
|
||||
async existsByUserId(userId: bigint): Promise<boolean> {
|
||||
const count = await this.userProfilesRepository.count({
|
||||
where: { user_id: userId }
|
||||
});
|
||||
return count > 0;
|
||||
}
|
||||
}
|
||||
697
src/core/db/user_profiles/user_profiles_memory.service.ts
Normal file
697
src/core/db/user_profiles/user_profiles_memory.service.ts
Normal file
@@ -0,0 +1,697 @@
|
||||
/**
|
||||
* 用户档案内存服务类
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供用户档案数据的内存存储实现
|
||||
* - 使用Map数据结构进行高性能数据管理
|
||||
* - 支持完整的CRUD操作和位置信息管理
|
||||
* - 为开发测试环境提供零依赖的数据存储方案
|
||||
*
|
||||
* 职责分离:
|
||||
* - 数据存储:使用Map进行内存数据管理
|
||||
* - ID生成:线程安全的自增ID生成机制
|
||||
* - 数据验证:数据完整性和唯一性约束检查
|
||||
* - 性能监控:操作耗时统计和日志记录
|
||||
*
|
||||
* 技术特点:
|
||||
* - 高性能:直接内存访问,无IO开销
|
||||
* - 零依赖:无需数据库连接,快速启动
|
||||
* - 完整功能:实现与数据库服务相同的接口
|
||||
* - 易测试:便于单元测试和集成测试
|
||||
*
|
||||
* 使用场景:
|
||||
* - 开发环境快速启动和调试
|
||||
* - 单元测试和集成测试
|
||||
* - 演示和原型开发
|
||||
* - 数据库故障时的降级方案
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 功能新增 - 创建用户档案内存服务,支持位置广播系统 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-08
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { UserProfiles } from './user_profiles.entity';
|
||||
import { CreateUserProfileDto, UpdateUserProfileDto, UpdatePositionDto, QueryUserProfileDto } from './user_profiles.dto';
|
||||
import { validate } from 'class-validator';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { BaseUserProfilesService } from './base_user_profiles.service';
|
||||
|
||||
@Injectable()
|
||||
export class UserProfilesMemoryService extends BaseUserProfilesService {
|
||||
/**
|
||||
* 内存数据存储
|
||||
*
|
||||
* 数据结构:
|
||||
* - Key: bigint类型的档案ID
|
||||
* - Value: UserProfiles实体对象
|
||||
* - 特点:支持快速查找和更新操作
|
||||
*/
|
||||
private profiles: Map<bigint, UserProfiles> = new Map();
|
||||
|
||||
/**
|
||||
* 用户ID到档案ID的映射
|
||||
*
|
||||
* 数据结构:
|
||||
* - Key: bigint类型的用户ID
|
||||
* - Value: bigint类型的档案ID
|
||||
* - 用途:支持根据用户ID快速查找档案
|
||||
*/
|
||||
private userIdToProfileId: Map<bigint, bigint> = new Map();
|
||||
|
||||
/**
|
||||
* 当前ID计数器
|
||||
*
|
||||
* 功能:
|
||||
* - 生成唯一的档案ID
|
||||
* - 自增机制,确保ID唯一性
|
||||
* - 线程安全的ID生成
|
||||
*/
|
||||
private CURRENT_ID: bigint = BigInt(1);
|
||||
|
||||
/**
|
||||
* ID生成锁
|
||||
*
|
||||
* 功能:
|
||||
* - 防止并发ID生成冲突
|
||||
* - 简单的锁机制实现
|
||||
* - 确保ID生成的原子性
|
||||
*/
|
||||
private readonly ID_LOCK = new Set<string>();
|
||||
|
||||
/**
|
||||
* 创建新用户档案
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 验证输入数据的格式和完整性
|
||||
* 2. 检查用户ID的唯一性约束
|
||||
* 3. 生成唯一的档案ID
|
||||
* 4. 创建用户档案实体对象
|
||||
* 5. 存储到内存Map中
|
||||
* 6. 建立用户ID到档案ID的映射
|
||||
* 7. 记录操作日志和性能指标
|
||||
*
|
||||
* @param createUserProfileDto 创建用户档案的数据传输对象
|
||||
* @returns 创建成功的用户档案实体
|
||||
* @throws BadRequestException 当数据验证失败时
|
||||
* @throws ConflictException 当用户ID已存在档案时
|
||||
*/
|
||||
async create(createUserProfileDto: CreateUserProfileDto): Promise<UserProfiles> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logStart('创建用户档案', {
|
||||
userId: createUserProfileDto.user_id.toString(),
|
||||
currentMap: createUserProfileDto.current_map
|
||||
});
|
||||
|
||||
try {
|
||||
// 验证DTO
|
||||
const dto = plainToClass(CreateUserProfileDto, createUserProfileDto);
|
||||
const validationErrors = await validate(dto);
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
const errorMessages = validationErrors.map(error =>
|
||||
Object.values(error.constraints || {}).join(', ')
|
||||
).join('; ');
|
||||
|
||||
this.logWarning('创建用户档案', '数据验证失败', {
|
||||
userId: createUserProfileDto.user_id.toString(),
|
||||
validationErrors: errorMessages
|
||||
});
|
||||
|
||||
throw new BadRequestException(`数据验证失败: ${errorMessages}`);
|
||||
}
|
||||
|
||||
// 检查用户ID是否已存在档案
|
||||
if (this.userIdToProfileId.has(createUserProfileDto.user_id)) {
|
||||
const existingProfileId = this.userIdToProfileId.get(createUserProfileDto.user_id);
|
||||
|
||||
this.logWarning('创建用户档案', '用户ID已存在档案', {
|
||||
userId: createUserProfileDto.user_id.toString(),
|
||||
existingProfileId: existingProfileId?.toString()
|
||||
});
|
||||
|
||||
throw new ConflictException('该用户已存在档案记录');
|
||||
}
|
||||
|
||||
// 生成唯一ID
|
||||
const profileId = this.generateUniqueId();
|
||||
|
||||
// 创建用户档案实体
|
||||
const userProfile = new UserProfiles();
|
||||
userProfile.id = profileId;
|
||||
userProfile.user_id = createUserProfileDto.user_id;
|
||||
userProfile.bio = createUserProfileDto.bio || null;
|
||||
userProfile.resume_content = createUserProfileDto.resume_content || null;
|
||||
userProfile.tags = createUserProfileDto.tags || null;
|
||||
userProfile.social_links = createUserProfileDto.social_links || null;
|
||||
userProfile.skin_id = createUserProfileDto.skin_id || null;
|
||||
userProfile.current_map = createUserProfileDto.current_map || 'plaza';
|
||||
userProfile.pos_x = createUserProfileDto.pos_x || 0;
|
||||
userProfile.pos_y = createUserProfileDto.pos_y || 0;
|
||||
userProfile.status = createUserProfileDto.status || 0;
|
||||
userProfile.last_position_update = new Date();
|
||||
|
||||
// 存储到内存
|
||||
this.profiles.set(profileId, userProfile);
|
||||
this.userIdToProfileId.set(createUserProfileDto.user_id, profileId);
|
||||
|
||||
const duration = this.calculateDuration(startTime);
|
||||
|
||||
this.logSuccess('创建用户档案', {
|
||||
profileId: profileId.toString(),
|
||||
userId: userProfile.user_id.toString(),
|
||||
currentMap: userProfile.current_map
|
||||
}, duration);
|
||||
|
||||
return userProfile;
|
||||
} catch (error) {
|
||||
const duration = this.calculateDuration(startTime);
|
||||
|
||||
if (error instanceof BadRequestException || error instanceof ConflictException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logError('创建用户档案',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
{ userId: createUserProfileDto.user_id.toString() },
|
||||
duration,
|
||||
error instanceof Error ? error.stack : undefined
|
||||
);
|
||||
|
||||
throw new BadRequestException('用户档案创建失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询用户档案
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 从内存Map中根据ID快速查找档案
|
||||
* 2. 验证档案是否存在
|
||||
* 3. 记录查询操作和结果
|
||||
*
|
||||
* @param id 档案ID
|
||||
* @returns 用户档案实体
|
||||
* @throws NotFoundException 当档案不存在时
|
||||
*/
|
||||
async findOne(id: bigint): Promise<UserProfiles> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logStart('查询用户档案', { profileId: id.toString() });
|
||||
|
||||
try {
|
||||
const profile = this.profiles.get(id);
|
||||
|
||||
if (!profile) {
|
||||
this.logWarning('查询用户档案', '档案不存在', { profileId: id.toString() });
|
||||
throw new NotFoundException(`ID为 ${id} 的用户档案不存在`);
|
||||
}
|
||||
|
||||
const duration = this.calculateDuration(startTime);
|
||||
|
||||
this.logSuccess('查询用户档案', {
|
||||
profileId: id.toString(),
|
||||
userId: profile.user_id.toString()
|
||||
}, duration);
|
||||
|
||||
return profile;
|
||||
} catch (error) {
|
||||
const duration = this.calculateDuration(startTime);
|
||||
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logError('查询用户档案',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
{ profileId: id.toString() },
|
||||
duration,
|
||||
error instanceof Error ? error.stack : undefined
|
||||
);
|
||||
|
||||
throw new BadRequestException('用户档案查询失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID查询用户档案
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @returns 用户档案实体或null
|
||||
*/
|
||||
async findByUserId(userId: bigint): Promise<UserProfiles | null> {
|
||||
const profileId = this.userIdToProfileId.get(userId);
|
||||
if (!profileId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.profiles.get(profileId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据地图查询用户档案列表
|
||||
*
|
||||
* @param mapId 地图ID
|
||||
* @param status 用户状态过滤(可选)
|
||||
* @param limit 限制数量,默认50
|
||||
* @param offset 偏移量,默认0
|
||||
* @returns 用户档案列表
|
||||
*/
|
||||
async findByMap(mapId: string, status?: number, limit: number = 50, offset: number = 0): Promise<UserProfiles[]> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logStart('查询地图用户档案', { mapId, status, limit, offset });
|
||||
|
||||
try {
|
||||
// 过滤符合条件的档案
|
||||
const filteredProfiles = Array.from(this.profiles.values()).filter(profile => {
|
||||
if (profile.current_map !== mapId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (status !== undefined && profile.status !== status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// 按最后位置更新时间排序
|
||||
filteredProfiles.sort((a, b) => {
|
||||
const timeA = a.last_position_update?.getTime() || 0;
|
||||
const timeB = b.last_position_update?.getTime() || 0;
|
||||
return timeB - timeA; // 降序排列
|
||||
});
|
||||
|
||||
// 应用分页
|
||||
const result = filteredProfiles.slice(offset, offset + limit);
|
||||
|
||||
const duration = this.calculateDuration(startTime);
|
||||
|
||||
this.logSuccess('查询地图用户档案', {
|
||||
mapId,
|
||||
status,
|
||||
resultCount: result.length,
|
||||
totalCount: filteredProfiles.length
|
||||
}, duration);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = this.calculateDuration(startTime);
|
||||
|
||||
return this.handleSearchError(error, '查询地图用户档案', {
|
||||
mapId,
|
||||
status,
|
||||
duration
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户档案信息
|
||||
*
|
||||
* @param id 档案ID
|
||||
* @param updateData 更新的数据
|
||||
* @returns 更新后的用户档案实体
|
||||
* @throws NotFoundException 当档案不存在时
|
||||
*/
|
||||
async update(id: bigint, updateData: UpdateUserProfileDto): Promise<UserProfiles> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logStart('更新用户档案信息', {
|
||||
profileId: id.toString(),
|
||||
updateFields: Object.keys(updateData)
|
||||
});
|
||||
|
||||
try {
|
||||
// 检查档案是否存在
|
||||
const existingProfile = await this.findOne(id);
|
||||
|
||||
// 合并更新数据
|
||||
Object.assign(existingProfile, updateData);
|
||||
|
||||
// 更新内存中的数据
|
||||
this.profiles.set(id, existingProfile);
|
||||
|
||||
const duration = this.calculateDuration(startTime);
|
||||
|
||||
this.logSuccess('更新用户档案信息', {
|
||||
profileId: id.toString(),
|
||||
userId: existingProfile.user_id.toString(),
|
||||
updateFields: Object.keys(updateData)
|
||||
}, duration);
|
||||
|
||||
return existingProfile;
|
||||
} catch (error) {
|
||||
const duration = this.calculateDuration(startTime);
|
||||
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logError('更新用户档案信息',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
{ profileId: id.toString(), updateData },
|
||||
duration,
|
||||
error instanceof Error ? error.stack : undefined
|
||||
);
|
||||
|
||||
throw new BadRequestException('用户档案更新失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户位置信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param positionData 位置数据
|
||||
* @returns 更新后的用户档案实体
|
||||
* @throws NotFoundException 当用户档案不存在时
|
||||
*/
|
||||
async updatePosition(userId: bigint, positionData: UpdatePositionDto): Promise<UserProfiles> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logStart('更新用户位置', {
|
||||
userId: userId.toString(),
|
||||
currentMap: positionData.current_map,
|
||||
posX: positionData.pos_x,
|
||||
posY: positionData.pos_y
|
||||
});
|
||||
|
||||
try {
|
||||
// 查找用户档案
|
||||
const profileId = this.userIdToProfileId.get(userId);
|
||||
if (!profileId) {
|
||||
this.logWarning('更新用户位置', '档案不存在', { userId: userId.toString() });
|
||||
throw new NotFoundException(`用户ID ${userId} 的档案不存在`);
|
||||
}
|
||||
|
||||
const profile = this.profiles.get(profileId);
|
||||
if (!profile) {
|
||||
this.logWarning('更新用户位置', '档案数据不存在', {
|
||||
userId: userId.toString(),
|
||||
profileId: profileId.toString()
|
||||
});
|
||||
throw new NotFoundException(`用户ID ${userId} 的档案不存在`);
|
||||
}
|
||||
|
||||
// 更新位置信息
|
||||
profile.current_map = positionData.current_map;
|
||||
profile.pos_x = positionData.pos_x;
|
||||
profile.pos_y = positionData.pos_y;
|
||||
profile.last_position_update = new Date();
|
||||
|
||||
// 更新内存中的数据
|
||||
this.profiles.set(profileId, profile);
|
||||
|
||||
const duration = this.calculateDuration(startTime);
|
||||
|
||||
this.logSuccess('更新用户位置', {
|
||||
profileId: profileId.toString(),
|
||||
userId: userId.toString(),
|
||||
currentMap: profile.current_map,
|
||||
posX: profile.pos_x,
|
||||
posY: profile.pos_y
|
||||
}, duration);
|
||||
|
||||
return profile;
|
||||
} catch (error) {
|
||||
const duration = this.calculateDuration(startTime);
|
||||
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logError('更新用户位置',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
{ userId: userId.toString(), positionData },
|
||||
duration,
|
||||
error instanceof Error ? error.stack : undefined
|
||||
);
|
||||
|
||||
throw new BadRequestException('用户位置更新失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新用户状态
|
||||
*
|
||||
* @param userIds 用户ID列表
|
||||
* @param status 目标状态
|
||||
* @returns 更新的记录数量
|
||||
*/
|
||||
async batchUpdateStatus(userIds: bigint[], status: number): Promise<number> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logStart('批量更新用户状态', {
|
||||
userCount: userIds.length,
|
||||
targetStatus: status
|
||||
});
|
||||
|
||||
try {
|
||||
let updatedCount = 0;
|
||||
|
||||
for (const userId of userIds) {
|
||||
const profileId = this.userIdToProfileId.get(userId);
|
||||
if (profileId) {
|
||||
const profile = this.profiles.get(profileId);
|
||||
if (profile) {
|
||||
profile.status = status;
|
||||
this.profiles.set(profileId, profile);
|
||||
updatedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const duration = this.calculateDuration(startTime);
|
||||
|
||||
this.logSuccess('批量更新用户状态', {
|
||||
userCount: userIds.length,
|
||||
targetStatus: status,
|
||||
updatedCount
|
||||
}, duration);
|
||||
|
||||
return updatedCount;
|
||||
} catch (error) {
|
||||
const duration = this.calculateDuration(startTime);
|
||||
|
||||
this.logError('批量更新用户状态',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
{ userCount: userIds.length, targetStatus: status },
|
||||
duration,
|
||||
error instanceof Error ? error.stack : undefined
|
||||
);
|
||||
|
||||
throw new BadRequestException('批量更新用户状态失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户档案列表
|
||||
*
|
||||
* @param queryDto 查询条件
|
||||
* @returns 用户档案列表
|
||||
*/
|
||||
async findAll(queryDto: QueryUserProfileDto = {}): Promise<UserProfiles[]> {
|
||||
const { current_map, status, limit = 20, offset = 0 } = queryDto;
|
||||
|
||||
// 过滤符合条件的档案
|
||||
const filteredProfiles = Array.from(this.profiles.values()).filter(profile => {
|
||||
if (current_map && profile.current_map !== current_map) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (status !== undefined && profile.status !== status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// 按最后位置更新时间排序
|
||||
filteredProfiles.sort((a, b) => {
|
||||
const timeA = a.last_position_update?.getTime() || 0;
|
||||
const timeB = b.last_position_update?.getTime() || 0;
|
||||
return timeB - timeA;
|
||||
});
|
||||
|
||||
// 应用分页
|
||||
return filteredProfiles.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计用户档案数量
|
||||
*
|
||||
* @param conditions 查询条件
|
||||
* @returns 档案数量
|
||||
*/
|
||||
async count(conditions?: any): Promise<number> {
|
||||
if (!conditions) {
|
||||
return this.profiles.size;
|
||||
}
|
||||
|
||||
// 简单的条件过滤统计
|
||||
let count = 0;
|
||||
for (const profile of this.profiles.values()) {
|
||||
let match = true;
|
||||
|
||||
for (const [key, value] of Object.entries(conditions)) {
|
||||
if ((profile as any)[key] !== value) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户档案
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证目标档案是否存在
|
||||
* 2. 从内存Map中删除档案记录
|
||||
* 3. 删除用户ID到档案ID的映射
|
||||
* 4. 记录删除操作和结果
|
||||
* 5. 返回删除操作的统计信息
|
||||
*
|
||||
* @param id 档案ID
|
||||
* @returns 删除操作结果
|
||||
* @throws NotFoundException 当档案不存在时
|
||||
*/
|
||||
async remove(id: bigint): Promise<{ affected: number; message: string }> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logStart('删除用户档案', { profileId: id.toString() });
|
||||
|
||||
try {
|
||||
// 检查档案是否存在
|
||||
const profile = await this.findOne(id);
|
||||
|
||||
// 删除档案记录
|
||||
this.profiles.delete(id);
|
||||
this.userIdToProfileId.delete(profile.user_id);
|
||||
|
||||
const deleteResult = {
|
||||
affected: 1,
|
||||
message: `成功删除ID为 ${id} 的用户档案`
|
||||
};
|
||||
|
||||
const duration = this.calculateDuration(startTime);
|
||||
|
||||
this.logSuccess('删除用户档案', {
|
||||
profileId: id.toString(),
|
||||
userId: profile.user_id.toString(),
|
||||
affected: deleteResult.affected
|
||||
}, duration);
|
||||
|
||||
return deleteResult;
|
||||
} catch (error) {
|
||||
const duration = this.calculateDuration(startTime);
|
||||
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logError('删除用户档案',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
{ profileId: id.toString() },
|
||||
duration,
|
||||
error instanceof Error ? error.stack : undefined
|
||||
);
|
||||
|
||||
throw new BadRequestException('用户档案删除失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户档案是否存在
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @returns 是否存在
|
||||
*/
|
||||
async existsByUserId(userId: bigint): Promise<boolean> {
|
||||
return this.userIdToProfileId.has(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一ID
|
||||
*
|
||||
* 功能描述:
|
||||
* 生成唯一的档案ID,确保线程安全和ID唯一性
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 使用简单的锁机制防止并发冲突
|
||||
* 2. 自增ID生成,确保唯一性
|
||||
* 3. 释放锁,允许其他操作继续
|
||||
*
|
||||
* @returns 唯一的档案ID
|
||||
*/
|
||||
private generateUniqueId(): bigint {
|
||||
const lockKey = 'id_generation';
|
||||
|
||||
// 简单的锁机制
|
||||
while (this.ID_LOCK.has(lockKey)) {
|
||||
// 等待锁释放(简单的自旋锁)
|
||||
}
|
||||
|
||||
this.ID_LOCK.add(lockKey);
|
||||
|
||||
try {
|
||||
const id = this.CURRENT_ID;
|
||||
this.CURRENT_ID = this.CURRENT_ID + BigInt(1);
|
||||
return id;
|
||||
} finally {
|
||||
this.ID_LOCK.delete(lockKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有数据
|
||||
*
|
||||
* 功能描述:
|
||||
* 清空内存中的所有档案数据,用于测试环境的数据重置
|
||||
*
|
||||
* 注意:此方法仅用于测试环境,生产环境请勿使用
|
||||
*/
|
||||
async clearAll(): Promise<void> {
|
||||
this.profiles.clear();
|
||||
this.userIdToProfileId.clear();
|
||||
this.CURRENT_ID = BigInt(1);
|
||||
|
||||
this.logger.warn('清空所有用户档案数据', {
|
||||
operation: 'clearAll',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内存使用统计
|
||||
*
|
||||
* 功能描述:
|
||||
* 获取当前内存存储的统计信息,用于监控和调试
|
||||
*
|
||||
* @returns 内存使用统计
|
||||
*/
|
||||
getMemoryStats(): {
|
||||
profileCount: number;
|
||||
userIdMappingCount: number;
|
||||
currentId: string;
|
||||
} {
|
||||
return {
|
||||
profileCount: this.profiles.size,
|
||||
userIdMappingCount: this.userIdToProfileId.size,
|
||||
currentId: this.CURRENT_ID.toString()
|
||||
};
|
||||
}
|
||||
}
|
||||
25
src/core/db/user_wallets/create-user-wallets-tables.sql
Normal file
25
src/core/db/user_wallets/create-user-wallets-tables.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
-- 创建用户钱包和鲸币流水表
|
||||
CREATE TABLE IF NOT EXISTS `user_wallets` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`user_id` bigint NOT NULL COMMENT '关联users.id',
|
||||
`balance` int NOT NULL DEFAULT 0 COMMENT '鲸币余额',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_user_wallets_user_id_unique` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户钱包表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `wallet_transactions` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`user_id` bigint NOT NULL COMMENT '关联users.id',
|
||||
`type` varchar(24) NOT NULL COMMENT '流水类型:grant/spend/earn/refund',
|
||||
`amount` int NOT NULL COMMENT '变动数量,收入为正,支出为负',
|
||||
`balance_after` int NOT NULL COMMENT '变动后余额',
|
||||
`reference_type` varchar(50) NOT NULL COMMENT '业务引用类型',
|
||||
`reference_id` varchar(100) NOT NULL COMMENT '业务引用ID',
|
||||
`note` varchar(255) DEFAULT NULL COMMENT '流水备注',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_wallet_transactions_user_id` (`user_id`),
|
||||
KEY `idx_wallet_transactions_reference` (`reference_type`, `reference_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包鲸币流水表';
|
||||
41
src/core/db/user_wallets/user_wallets.entity.ts
Normal file
41
src/core/db/user_wallets/user_wallets.entity.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity('user_wallets')
|
||||
export class UserWallets {
|
||||
@PrimaryGeneratedColumn({
|
||||
type: 'bigint',
|
||||
comment: '主键ID',
|
||||
})
|
||||
id: bigint;
|
||||
|
||||
@Column({
|
||||
type: 'bigint',
|
||||
nullable: false,
|
||||
unique: true,
|
||||
comment: '关联users.id',
|
||||
})
|
||||
user_id: bigint;
|
||||
|
||||
@Column({
|
||||
type: 'int',
|
||||
nullable: false,
|
||||
default: 0,
|
||||
comment: '鲸币余额',
|
||||
})
|
||||
balance: number;
|
||||
|
||||
@Column({
|
||||
type: 'timestamp',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
comment: '创建时间',
|
||||
})
|
||||
created_at: Date;
|
||||
|
||||
@Column({
|
||||
type: 'timestamp',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
onUpdate: 'CURRENT_TIMESTAMP',
|
||||
comment: '更新时间',
|
||||
})
|
||||
updated_at: Date;
|
||||
}
|
||||
49
src/core/db/user_wallets/user_wallets.module.ts
Normal file
49
src/core/db/user_wallets/user_wallets.module.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { DynamicModule, Global, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UserWallets } from './user_wallets.entity';
|
||||
import { WalletTransactions } from './wallet_transactions.entity';
|
||||
import { UserWalletsMemoryService } from './user_wallets_memory.service';
|
||||
import { UserWalletsService } from './user_wallets.service';
|
||||
|
||||
@Global()
|
||||
@Module({})
|
||||
export class UserWalletsModule {
|
||||
static forDatabase(): DynamicModule {
|
||||
return {
|
||||
module: UserWalletsModule,
|
||||
imports: [TypeOrmModule.forFeature([UserWallets, WalletTransactions])],
|
||||
providers: [
|
||||
UserWalletsService,
|
||||
{
|
||||
provide: 'IUserWalletsService',
|
||||
useClass: UserWalletsService,
|
||||
},
|
||||
],
|
||||
exports: [UserWalletsService, 'IUserWalletsService'],
|
||||
};
|
||||
}
|
||||
|
||||
static forMemory(): DynamicModule {
|
||||
return {
|
||||
module: UserWalletsModule,
|
||||
providers: [
|
||||
UserWalletsMemoryService,
|
||||
{
|
||||
provide: 'IUserWalletsService',
|
||||
useClass: UserWalletsMemoryService,
|
||||
},
|
||||
],
|
||||
exports: [UserWalletsMemoryService, 'IUserWalletsService'],
|
||||
};
|
||||
}
|
||||
|
||||
static forRoot(useMemory?: boolean): DynamicModule {
|
||||
const shouldUseMemory = useMemory ?? (
|
||||
process.env.NODE_ENV === 'test' ||
|
||||
process.env.USE_MEMORY_STORAGE === 'true' ||
|
||||
!process.env.DB_HOST
|
||||
);
|
||||
|
||||
return shouldUseMemory ? this.forMemory() : this.forDatabase();
|
||||
}
|
||||
}
|
||||
119
src/core/db/user_wallets/user_wallets.service.ts
Normal file
119
src/core/db/user_wallets/user_wallets.service.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { UserWallets } from './user_wallets.entity';
|
||||
import { WalletTransactions } from './wallet_transactions.entity';
|
||||
|
||||
export const DEFAULT_INITIAL_WHALE_COINS = 1200;
|
||||
|
||||
export interface WalletBalancePayload {
|
||||
user_id: string;
|
||||
balance: number;
|
||||
currency: 'whale_coin';
|
||||
}
|
||||
|
||||
export interface SpendWalletResult {
|
||||
wallet: UserWallets;
|
||||
transaction: WalletTransactions;
|
||||
}
|
||||
|
||||
export interface EarnWalletResult {
|
||||
wallet: UserWallets;
|
||||
transaction: WalletTransactions;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class UserWalletsService {
|
||||
constructor(
|
||||
@InjectRepository(UserWallets)
|
||||
private readonly userWalletsRepository: Repository<UserWallets>,
|
||||
@InjectRepository(WalletTransactions)
|
||||
private readonly walletTransactionsRepository: Repository<WalletTransactions>,
|
||||
) {}
|
||||
|
||||
async ensureWallet(userId: bigint): Promise<UserWallets> {
|
||||
const existing = await this.userWalletsRepository.findOne({
|
||||
where: { user_id: userId },
|
||||
});
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const wallet = new UserWallets();
|
||||
wallet.user_id = userId;
|
||||
wallet.balance = DEFAULT_INITIAL_WHALE_COINS;
|
||||
wallet.created_at = new Date();
|
||||
wallet.updated_at = new Date();
|
||||
const savedWallet = await this.userWalletsRepository.save(wallet);
|
||||
await this.createTransaction(userId, 'grant', DEFAULT_INITIAL_WHALE_COINS, savedWallet.balance, 'registration', 'initial_wallet', '新用户初始鲸币');
|
||||
return savedWallet;
|
||||
}
|
||||
|
||||
async getBalance(userId: bigint): Promise<WalletBalancePayload> {
|
||||
const wallet = await this.ensureWallet(userId);
|
||||
return {
|
||||
user_id: userId.toString(),
|
||||
balance: wallet.balance,
|
||||
currency: 'whale_coin',
|
||||
};
|
||||
}
|
||||
|
||||
async spend(userId: bigint, amount: number, referenceType: string, referenceId: string, note?: string): Promise<SpendWalletResult> {
|
||||
if (!Number.isInteger(amount) || amount < 0) {
|
||||
throw new BadRequestException('鲸币消费数量不正确');
|
||||
}
|
||||
|
||||
const wallet = await this.ensureWallet(userId);
|
||||
if (wallet.balance < amount) {
|
||||
throw new BadRequestException('鲸币余额不足');
|
||||
}
|
||||
|
||||
wallet.balance -= amount;
|
||||
wallet.updated_at = new Date();
|
||||
const savedWallet = await this.userWalletsRepository.save(wallet);
|
||||
const transaction = await this.createTransaction(userId, 'spend', -amount, savedWallet.balance, referenceType, referenceId, note || '');
|
||||
|
||||
return {
|
||||
wallet: savedWallet,
|
||||
transaction,
|
||||
};
|
||||
}
|
||||
|
||||
async earn(userId: bigint, amount: number, referenceType: string, referenceId: string, note?: string): Promise<EarnWalletResult> {
|
||||
if (!Number.isInteger(amount) || amount < 0) {
|
||||
throw new BadRequestException('鲸币收入数量不正确');
|
||||
}
|
||||
|
||||
const wallet = await this.ensureWallet(userId);
|
||||
wallet.balance += amount;
|
||||
wallet.updated_at = new Date();
|
||||
const savedWallet = await this.userWalletsRepository.save(wallet);
|
||||
const transaction = await this.createTransaction(userId, 'earn', amount, savedWallet.balance, referenceType, referenceId, note || '');
|
||||
|
||||
return {
|
||||
wallet: savedWallet,
|
||||
transaction,
|
||||
};
|
||||
}
|
||||
|
||||
private async createTransaction(
|
||||
userId: bigint,
|
||||
type: string,
|
||||
amount: number,
|
||||
balanceAfter: number,
|
||||
referenceType: string,
|
||||
referenceId: string,
|
||||
note: string,
|
||||
): Promise<WalletTransactions> {
|
||||
const transaction = new WalletTransactions();
|
||||
transaction.user_id = userId;
|
||||
transaction.type = type;
|
||||
transaction.amount = amount;
|
||||
transaction.balance_after = balanceAfter;
|
||||
transaction.reference_type = referenceType;
|
||||
transaction.reference_id = referenceId;
|
||||
transaction.note = note || null;
|
||||
transaction.created_at = new Date();
|
||||
return await this.walletTransactionsRepository.save(transaction);
|
||||
}
|
||||
}
|
||||
95
src/core/db/user_wallets/user_wallets_memory.service.ts
Normal file
95
src/core/db/user_wallets/user_wallets_memory.service.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { DEFAULT_INITIAL_WHALE_COINS, EarnWalletResult, SpendWalletResult, WalletBalancePayload } from './user_wallets.service';
|
||||
import { UserWallets } from './user_wallets.entity';
|
||||
import { WalletTransactions } from './wallet_transactions.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UserWalletsMemoryService {
|
||||
private wallets: Map<bigint, UserWallets> = new Map();
|
||||
private transactions: WalletTransactions[] = [];
|
||||
private currentWalletId: bigint = BigInt(1);
|
||||
private currentTransactionId: bigint = BigInt(1);
|
||||
|
||||
async ensureWallet(userId: bigint): Promise<UserWallets> {
|
||||
const existing = this.wallets.get(userId);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const wallet = new UserWallets();
|
||||
wallet.id = this.currentWalletId++;
|
||||
wallet.user_id = userId;
|
||||
wallet.balance = DEFAULT_INITIAL_WHALE_COINS;
|
||||
wallet.created_at = new Date();
|
||||
wallet.updated_at = new Date();
|
||||
this.wallets.set(userId, wallet);
|
||||
await this.createTransaction(userId, 'grant', DEFAULT_INITIAL_WHALE_COINS, wallet.balance, 'registration', 'initial_wallet', '新用户初始鲸币');
|
||||
return wallet;
|
||||
}
|
||||
|
||||
async getBalance(userId: bigint): Promise<WalletBalancePayload> {
|
||||
const wallet = await this.ensureWallet(userId);
|
||||
return {
|
||||
user_id: userId.toString(),
|
||||
balance: wallet.balance,
|
||||
currency: 'whale_coin',
|
||||
};
|
||||
}
|
||||
|
||||
async spend(userId: bigint, amount: number, referenceType: string, referenceId: string, note?: string): Promise<SpendWalletResult> {
|
||||
if (!Number.isInteger(amount) || amount < 0) {
|
||||
throw new BadRequestException('鲸币消费数量不正确');
|
||||
}
|
||||
|
||||
const wallet = await this.ensureWallet(userId);
|
||||
if (wallet.balance < amount) {
|
||||
throw new BadRequestException('鲸币余额不足');
|
||||
}
|
||||
|
||||
wallet.balance -= amount;
|
||||
wallet.updated_at = new Date();
|
||||
const transaction = await this.createTransaction(userId, 'spend', -amount, wallet.balance, referenceType, referenceId, note || '');
|
||||
return {
|
||||
wallet,
|
||||
transaction,
|
||||
};
|
||||
}
|
||||
|
||||
async earn(userId: bigint, amount: number, referenceType: string, referenceId: string, note?: string): Promise<EarnWalletResult> {
|
||||
if (!Number.isInteger(amount) || amount < 0) {
|
||||
throw new BadRequestException('鲸币收入数量不正确');
|
||||
}
|
||||
|
||||
const wallet = await this.ensureWallet(userId);
|
||||
wallet.balance += amount;
|
||||
wallet.updated_at = new Date();
|
||||
const transaction = await this.createTransaction(userId, 'earn', amount, wallet.balance, referenceType, referenceId, note || '');
|
||||
return {
|
||||
wallet,
|
||||
transaction,
|
||||
};
|
||||
}
|
||||
|
||||
private async createTransaction(
|
||||
userId: bigint,
|
||||
type: string,
|
||||
amount: number,
|
||||
balanceAfter: number,
|
||||
referenceType: string,
|
||||
referenceId: string,
|
||||
note: string,
|
||||
): Promise<WalletTransactions> {
|
||||
const transaction = new WalletTransactions();
|
||||
transaction.id = this.currentTransactionId++;
|
||||
transaction.user_id = userId;
|
||||
transaction.type = type;
|
||||
transaction.amount = amount;
|
||||
transaction.balance_after = balanceAfter;
|
||||
transaction.reference_type = referenceType;
|
||||
transaction.reference_id = referenceId;
|
||||
transaction.note = note || null;
|
||||
transaction.created_at = new Date();
|
||||
this.transactions.push(transaction);
|
||||
return transaction;
|
||||
}
|
||||
}
|
||||
72
src/core/db/user_wallets/wallet_transactions.entity.ts
Normal file
72
src/core/db/user_wallets/wallet_transactions.entity.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity('wallet_transactions')
|
||||
@Index('idx_wallet_transactions_user_id', ['user_id'])
|
||||
@Index('idx_wallet_transactions_reference', ['reference_type', 'reference_id'])
|
||||
export class WalletTransactions {
|
||||
@PrimaryGeneratedColumn({
|
||||
type: 'bigint',
|
||||
comment: '主键ID',
|
||||
})
|
||||
id: bigint;
|
||||
|
||||
@Column({
|
||||
type: 'bigint',
|
||||
nullable: false,
|
||||
comment: '关联users.id',
|
||||
})
|
||||
user_id: bigint;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 24,
|
||||
nullable: false,
|
||||
comment: '流水类型:grant/spend/earn/refund',
|
||||
})
|
||||
type: string;
|
||||
|
||||
@Column({
|
||||
type: 'int',
|
||||
nullable: false,
|
||||
comment: '变动数量,收入为正,支出为负',
|
||||
})
|
||||
amount: number;
|
||||
|
||||
@Column({
|
||||
type: 'int',
|
||||
nullable: false,
|
||||
comment: '变动后余额',
|
||||
})
|
||||
balance_after: number;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 50,
|
||||
nullable: false,
|
||||
comment: '业务引用类型',
|
||||
})
|
||||
reference_type: string;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 100,
|
||||
nullable: false,
|
||||
comment: '业务引用ID',
|
||||
})
|
||||
reference_id: string;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
comment: '流水备注',
|
||||
})
|
||||
note?: string | null;
|
||||
|
||||
@Column({
|
||||
type: 'timestamp',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
comment: '创建时间',
|
||||
})
|
||||
created_at: Date;
|
||||
}
|
||||
203
src/core/db/users/base_users.service.ts
Normal file
203
src/core/db/users/base_users.service.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* 用户服务基类
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供统一的异常处理机制
|
||||
* - 定义通用的错误处理方法
|
||||
* - 统一日志记录格式
|
||||
* - 敏感信息脱敏处理
|
||||
*
|
||||
* 职责分离:
|
||||
* - 异常处理:统一的错误格式化和异常转换
|
||||
* - 日志管理:结构化日志记录和敏感信息脱敏
|
||||
* - 性能监控:操作成功和失败的统计记录
|
||||
* - 搜索优化:搜索异常的特殊处理机制
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-15: 代码规范优化 - 为保护方法补充@example示例 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 完善注释规范,添加完整的文件头和方法注释
|
||||
* - 2026-01-07: 功能新增 - 添加敏感信息脱敏处理和结构化日志记录
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @since 2025-01-07
|
||||
* @lastModified 2026-01-15
|
||||
*/
|
||||
|
||||
import { Logger, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
|
||||
export abstract class BaseUsersService {
|
||||
protected readonly logger = new Logger(this.constructor.name);
|
||||
|
||||
/**
|
||||
* 统一的错误格式化方法
|
||||
*
|
||||
* @param error 原始错误对象
|
||||
* @returns 格式化后的错误信息字符串
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const errorMsg = this.formatError(new Error('数据库连接失败'));
|
||||
* // 返回: "数据库连接失败"
|
||||
* ```
|
||||
*/
|
||||
protected formatError(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的异常处理方法
|
||||
*
|
||||
* @param error 原始错误
|
||||
* @param operation 操作名称
|
||||
* @param context 上下文信息
|
||||
* @throws 处理后的标准异常
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* try {
|
||||
* // 业务操作
|
||||
* } catch (error) {
|
||||
* this.handleServiceError(error, '创建用户', { username: 'test' });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected handleServiceError(error: unknown, operation: string, context?: Record<string, any>): never {
|
||||
const errorMessage = this.formatError(error);
|
||||
|
||||
// 记录错误日志
|
||||
this.logger.error(`${operation}失败`, {
|
||||
operation,
|
||||
error: errorMessage,
|
||||
context: context ? this.sanitizeLogData(context) : undefined,
|
||||
timestamp: new Date().toISOString()
|
||||
}, error instanceof Error ? error.stack : undefined);
|
||||
|
||||
// 如果是已知的业务异常,直接重新抛出
|
||||
if (error instanceof ConflictException ||
|
||||
error instanceof NotFoundException ||
|
||||
error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 系统异常转换为BadRequestException
|
||||
throw new BadRequestException(`${operation}失败,请稍后重试`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索异常的特殊处理(返回空结果而不抛出异常)
|
||||
*
|
||||
* @param error 原始错误
|
||||
* @param operation 操作名称
|
||||
* @param context 上下文信息
|
||||
* @returns 空数组
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* try {
|
||||
* // 搜索操作
|
||||
* } catch (error) {
|
||||
* return this.handleSearchError(error, '搜索用户', { keyword: 'test' });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected handleSearchError(error: unknown, operation: string, context?: Record<string, any>): any[] {
|
||||
const errorMessage = this.formatError(error);
|
||||
|
||||
this.logger.warn(`${operation}失败,返回空结果`, {
|
||||
operation,
|
||||
error: errorMessage,
|
||||
context: context ? this.sanitizeLogData(context) : undefined,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录操作成功日志
|
||||
*
|
||||
* @param operation 操作名称
|
||||
* @param context 上下文信息
|
||||
* @param duration 操作耗时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* this.logSuccess('创建用户', { userId: '123', username: 'test' }, 50);
|
||||
* ```
|
||||
*/
|
||||
protected logSuccess(operation: string, context?: Record<string, any>, duration?: number): void {
|
||||
this.logger.log(`${operation}成功`, {
|
||||
operation,
|
||||
context: context ? this.sanitizeLogData(context) : undefined,
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录操作开始日志
|
||||
*
|
||||
* @param operation 操作名称
|
||||
* @param context 上下文信息
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* this.logStart('创建用户', { username: 'test' });
|
||||
* ```
|
||||
*/
|
||||
protected logStart(operation: string, context?: Record<string, any>): void {
|
||||
this.logger.log(`开始${operation}`, {
|
||||
operation,
|
||||
context: context ? this.sanitizeLogData(context) : undefined,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏处理敏感信息
|
||||
*
|
||||
* @param data 原始数据
|
||||
* @returns 脱敏后的数据
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sanitized = this.sanitizeLogData({
|
||||
* email: 'test@example.com',
|
||||
* phone: '13800138000',
|
||||
* password_hash: 'secret'
|
||||
* });
|
||||
* // 返回: { email: 'te***@example.com', phone: '138****00', password_hash: '[REDACTED]' }
|
||||
* ```
|
||||
*/
|
||||
protected sanitizeLogData(data: Record<string, any>): Record<string, any> {
|
||||
const sanitized = { ...data };
|
||||
|
||||
// 脱敏邮箱
|
||||
if (sanitized.email) {
|
||||
const email = sanitized.email;
|
||||
const [localPart, domain] = email.split('@');
|
||||
if (localPart && domain) {
|
||||
sanitized.email = `${localPart.substring(0, 2)}***@${domain}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 脱敏手机号
|
||||
if (sanitized.phone) {
|
||||
const phone = sanitized.phone;
|
||||
if (phone.length > 4) {
|
||||
sanitized.phone = `${phone.substring(0, 3)}****${phone.substring(phone.length - 2)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 移除密码哈希
|
||||
if (sanitized.password_hash) {
|
||||
sanitized.password_hash = '[REDACTED]';
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
}
|
||||
173
src/core/db/users/user_status.enum.ts
Normal file
173
src/core/db/users/user_status.enum.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* 用户状态枚举(Core层)
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义用户账户的各种状态
|
||||
* - 提供状态检查和描述功能
|
||||
* - 支持用户生命周期管理
|
||||
*
|
||||
* 职责分离:
|
||||
* - 用户状态枚举值定义和管理
|
||||
* - 状态描述和错误消息的国际化支持
|
||||
* - 状态验证和转换工具函数提供
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 架构优化 - 从Business层移动到Core层,符合架构分层原则 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @since 2025-12-24
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
/**
|
||||
* 用户状态枚举
|
||||
*
|
||||
* 状态说明:
|
||||
* - active: 正常状态,可以正常使用所有功能
|
||||
* - inactive: 未激活状态,通常是新注册用户需要邮箱验证
|
||||
* - locked: 临时锁定状态,可以解锁恢复
|
||||
* - banned: 永久禁用状态,需要管理员处理
|
||||
* - deleted: 软删除状态,数据保留但不可使用
|
||||
* - pending: 待审核状态,需要管理员审核后激活
|
||||
*/
|
||||
export enum UserStatus {
|
||||
ACTIVE = 'active', // 正常状态
|
||||
INACTIVE = 'inactive', // 未激活状态
|
||||
LOCKED = 'locked', // 锁定状态
|
||||
BANNED = 'banned', // 禁用状态
|
||||
DELETED = 'deleted', // 删除状态
|
||||
PENDING = 'pending' // 待审核状态
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户状态的中文描述
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 根据用户状态枚举值查找对应的中文描述
|
||||
* 2. 提供用户友好的状态显示文本
|
||||
* 3. 处理未知状态的默认描述
|
||||
*
|
||||
* @param status 用户状态
|
||||
* @returns 状态描述
|
||||
* @throws 无异常抛出,未知状态返回默认描述
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const description = getUserStatusDescription(UserStatus.ACTIVE);
|
||||
* // 返回: "正常"
|
||||
* ```
|
||||
*/
|
||||
export function getUserStatusDescription(status: UserStatus): string {
|
||||
const descriptions = {
|
||||
[UserStatus.ACTIVE]: '正常',
|
||||
[UserStatus.INACTIVE]: '未激活',
|
||||
[UserStatus.LOCKED]: '已锁定',
|
||||
[UserStatus.BANNED]: '已禁用',
|
||||
[UserStatus.DELETED]: '已删除',
|
||||
[UserStatus.PENDING]: '待审核'
|
||||
};
|
||||
|
||||
return descriptions[status] || '未知状态';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否可以登录
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 验证用户状态是否允许登录系统
|
||||
* 2. 只有正常状态的用户可以登录
|
||||
* 3. 其他状态均不允许登录
|
||||
*
|
||||
* @param status 用户状态
|
||||
* @returns 是否可以登录
|
||||
* @throws 无异常抛出
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const canLogin = canUserLogin(UserStatus.ACTIVE);
|
||||
* // 返回: true
|
||||
* const cannotLogin = canUserLogin(UserStatus.LOCKED);
|
||||
* // 返回: false
|
||||
* ```
|
||||
*/
|
||||
export function canUserLogin(status: UserStatus): boolean {
|
||||
// 只有正常状态的用户可以登录
|
||||
return status === UserStatus.ACTIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户状态对应的错误消息
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 根据用户状态返回相应的错误提示信息
|
||||
* 2. 为不同状态提供用户友好的错误说明
|
||||
* 3. 指导用户如何解决状态问题
|
||||
*
|
||||
* @param status 用户状态
|
||||
* @returns 错误消息
|
||||
* @throws 无异常抛出,未知状态返回默认错误消息
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const errorMsg = getUserStatusErrorMessage(UserStatus.LOCKED);
|
||||
* // 返回: "账户已被锁定,请联系管理员"
|
||||
* ```
|
||||
*/
|
||||
export function getUserStatusErrorMessage(status: UserStatus): string {
|
||||
const errorMessages = {
|
||||
[UserStatus.ACTIVE]: '', // 正常状态无错误
|
||||
[UserStatus.INACTIVE]: '账户未激活,请先验证邮箱',
|
||||
[UserStatus.LOCKED]: '账户已被锁定,请联系管理员',
|
||||
[UserStatus.BANNED]: '账户已被禁用,请联系管理员',
|
||||
[UserStatus.DELETED]: '账户不存在',
|
||||
[UserStatus.PENDING]: '账户待审核,请等待管理员审核'
|
||||
};
|
||||
|
||||
return errorMessages[status] || '账户状态异常';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的用户状态
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 返回系统中定义的所有用户状态枚举值
|
||||
* 2. 用于状态选择器和验证逻辑
|
||||
* 3. 支持动态状态管理功能
|
||||
*
|
||||
* @returns 用户状态数组
|
||||
* @throws 无异常抛出
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const allStatuses = getAllUserStatuses();
|
||||
* // 返回: [UserStatus.ACTIVE, UserStatus.INACTIVE, ...]
|
||||
* ```
|
||||
*/
|
||||
export function getAllUserStatuses(): UserStatus[] {
|
||||
return Object.values(UserStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查状态值是否有效
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 验证输入的字符串是否为有效的用户状态枚举值
|
||||
* 2. 提供类型安全的状态验证功能
|
||||
* 3. 支持动态状态值验证和类型转换
|
||||
*
|
||||
* @param status 状态值
|
||||
* @returns 是否为有效状态
|
||||
* @throws 无异常抛出
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const isValid = isValidUserStatus('active');
|
||||
* // 返回: true
|
||||
* const isInvalid = isValidUserStatus('unknown');
|
||||
* // 返回: false
|
||||
* ```
|
||||
*/
|
||||
export function isValidUserStatus(status: string): status is UserStatus {
|
||||
return Object.values(UserStatus).includes(status as UserStatus);
|
||||
}
|
||||
188
src/core/db/users/users.constants.ts
Normal file
188
src/core/db/users/users.constants.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 用户模块常量定义
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义用户模块中使用的常量值
|
||||
* - 避免魔法数字,提高代码可维护性
|
||||
* - 集中管理配置参数
|
||||
*
|
||||
* 职责分离:
|
||||
* - 常量定义:用户角色、字段限制、查询限制等常量值
|
||||
* - 错误消息:统一的错误消息定义和管理
|
||||
* - 工具类:性能监控和验证工具的封装
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-15: 代码规范优化 - 补充职责分离描述 (修改者: moyin)
|
||||
* - 2026-01-09: 代码质量优化 - 提取魔法数字为常量定义 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2026-01-09
|
||||
* @lastModified 2026-01-15
|
||||
*/
|
||||
|
||||
import { ValidationError } from 'class-validator';
|
||||
|
||||
/**
|
||||
* 用户角色常量
|
||||
*/
|
||||
export const USER_ROLES = {
|
||||
/** 普通用户角色 */
|
||||
NORMAL_USER: 1,
|
||||
/** 管理员角色 */
|
||||
ADMIN: 9
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 字段长度限制常量
|
||||
*/
|
||||
export const FIELD_LIMITS = {
|
||||
/** 用户名最大长度 */
|
||||
USERNAME_MAX_LENGTH: 50,
|
||||
/** 昵称最大长度 */
|
||||
NICKNAME_MAX_LENGTH: 50,
|
||||
/** 邮箱最大长度 */
|
||||
EMAIL_MAX_LENGTH: 100,
|
||||
/** 手机号最大长度 */
|
||||
PHONE_MAX_LENGTH: 30,
|
||||
/** GitHub ID最大长度 */
|
||||
GITHUB_ID_MAX_LENGTH: 100,
|
||||
/** 头像URL最大长度 */
|
||||
AVATAR_URL_MAX_LENGTH: 255,
|
||||
/** 密码哈希最大长度 */
|
||||
PASSWORD_HASH_MAX_LENGTH: 255,
|
||||
/** 用户状态最大长度 */
|
||||
STATUS_MAX_LENGTH: 20
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 查询限制常量
|
||||
*/
|
||||
export const QUERY_LIMITS = {
|
||||
/** 默认查询限制 */
|
||||
DEFAULT_LIMIT: 100,
|
||||
/** 默认搜索限制 */
|
||||
DEFAULT_SEARCH_LIMIT: 20,
|
||||
/** 最大查询限制 */
|
||||
MAX_LIMIT: 1000
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 系统配置常量
|
||||
*/
|
||||
export const SYSTEM_CONFIG = {
|
||||
/** ID生成超时时间(毫秒) */
|
||||
ID_GENERATION_TIMEOUT: 5000,
|
||||
/** 锁等待间隔(毫秒) */
|
||||
LOCK_WAIT_INTERVAL: 1
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 数据库常量
|
||||
*/
|
||||
export const DATABASE_CONSTANTS = {
|
||||
/** 排序方向 */
|
||||
ORDER_DESC: 'DESC' as const,
|
||||
ORDER_ASC: 'ASC' as const,
|
||||
/** 数据库默认值 */
|
||||
CURRENT_TIMESTAMP: 'CURRENT_TIMESTAMP' as const,
|
||||
/** 锁键名 */
|
||||
ID_GENERATION_LOCK_KEY: 'id_generation' as const
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 测试常量
|
||||
*/
|
||||
export const TEST_CONSTANTS = {
|
||||
/** 测试用的不存在用户ID */
|
||||
NON_EXISTENT_USER_ID: 99999,
|
||||
/** 测试用的无效角色 */
|
||||
INVALID_ROLE: 999,
|
||||
/** 测试用的用户名长度限制 */
|
||||
USERNAME_LENGTH_LIMIT: 51,
|
||||
/** 测试用的批量操作数量 */
|
||||
BATCH_TEST_SIZE: 50,
|
||||
/** 测试用的性能测试数量 */
|
||||
PERFORMANCE_TEST_SIZE: 50,
|
||||
/** 测试用的分页大小 */
|
||||
TEST_PAGE_SIZE: 20,
|
||||
/** 测试用的查询偏移量 */
|
||||
TEST_OFFSET: 10
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 错误消息常量
|
||||
*/
|
||||
export const ERROR_MESSAGES = {
|
||||
/** 用户创建失败 */
|
||||
USER_CREATE_FAILED: '用户创建失败,请稍后重试',
|
||||
/** 用户更新失败 */
|
||||
USER_UPDATE_FAILED: '用户更新失败,请稍后重试',
|
||||
/** 用户删除失败 */
|
||||
USER_DELETE_FAILED: '用户删除失败,请稍后重试',
|
||||
/** 用户不存在 */
|
||||
USER_NOT_FOUND: '用户不存在',
|
||||
/** 数据验证失败 */
|
||||
VALIDATION_FAILED: '数据验证失败',
|
||||
/** ID生成超时 */
|
||||
ID_GENERATION_TIMEOUT: 'ID生成超时,可能存在死锁',
|
||||
/** 用户名已存在 */
|
||||
USERNAME_EXISTS: '用户名已存在',
|
||||
/** 邮箱已存在 */
|
||||
EMAIL_EXISTS: '邮箱已存在',
|
||||
/** 手机号已存在 */
|
||||
PHONE_EXISTS: '手机号已存在',
|
||||
/** GitHub ID已存在 */
|
||||
GITHUB_ID_EXISTS: 'GitHub ID已存在'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 性能监控工具类
|
||||
*/
|
||||
export class PerformanceMonitor {
|
||||
private startTime: number;
|
||||
|
||||
constructor() {
|
||||
this.startTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取执行时长
|
||||
* @returns 执行时长(毫秒)
|
||||
*/
|
||||
getDuration(): number {
|
||||
return Date.now() - this.startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置计时器
|
||||
*/
|
||||
reset(): void {
|
||||
this.startTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新的性能监控实例
|
||||
* @returns 性能监控实例
|
||||
*/
|
||||
static create(): PerformanceMonitor {
|
||||
return new PerformanceMonitor();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证工具类
|
||||
*/
|
||||
export class ValidationUtils {
|
||||
/**
|
||||
* 格式化验证错误消息
|
||||
*
|
||||
* @param validationErrors 验证错误数组
|
||||
* @returns 格式化后的错误消息字符串
|
||||
*/
|
||||
static formatValidationErrors(validationErrors: ValidationError[]): string {
|
||||
return validationErrors.map(error =>
|
||||
Object.values(error.constraints || {}).join(', ')
|
||||
).join('; ');
|
||||
}
|
||||
}
|
||||
275
src/core/db/users/users.dto.ts
Normal file
275
src/core/db/users/users.dto.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* 用户数据传输对象模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义用户创建和更新的数据传输对象
|
||||
* - 提供完整的数据验证规则和错误提示
|
||||
* - 支持多种登录方式的数据格式验证
|
||||
* - 确保数据传输的安全性和完整性
|
||||
*
|
||||
* 职责分离:
|
||||
* - 数据验证:使用class-validator进行输入数据验证
|
||||
* - 类型定义:定义清晰的数据结构和类型约束
|
||||
* - 错误处理:提供友好的验证错误提示信息
|
||||
* - 业务规则:实现用户数据的业务验证逻辑
|
||||
*
|
||||
* 依赖模块:
|
||||
* - class-validator: 数据验证装饰器
|
||||
* - class-transformer: 数据转换工具
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 完善注释规范,添加完整的文件头和字段注释
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-17
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import {
|
||||
IsString,
|
||||
IsEmail,
|
||||
IsPhoneNumber,
|
||||
IsInt,
|
||||
Min,
|
||||
Max,
|
||||
IsOptional,
|
||||
Length,
|
||||
IsNotEmpty,
|
||||
IsEnum
|
||||
} from 'class-validator';
|
||||
import { UserStatus } from './user_status.enum';
|
||||
import { USER_ROLES, FIELD_LIMITS } from './users.constants';
|
||||
|
||||
/**
|
||||
* 创建用户数据传输对象
|
||||
*
|
||||
* 职责:
|
||||
* - 定义用户创建时的数据结构和验证规则
|
||||
* - 确保输入数据的格式正确性和业务规则符合性
|
||||
* - 提供友好的错误提示信息
|
||||
*
|
||||
* 主要字段:
|
||||
* - username: 唯一用户名,用于登录识别
|
||||
* - email: 邮箱地址,用于通知和账户找回
|
||||
* - phone: 手机号码,支持全球格式
|
||||
* - password_hash: 密码哈希值,OAuth登录时可为空
|
||||
* - nickname: 显示昵称,在游戏中展示
|
||||
* - github_id: GitHub第三方登录标识
|
||||
* - avatar_url: 用户头像链接
|
||||
* - role: 用户角色,控制权限级别
|
||||
*
|
||||
* 使用场景:
|
||||
* - 用户注册接口的请求体验证
|
||||
* - 管理员创建用户的数据验证
|
||||
* - 第三方登录用户信息同步
|
||||
*
|
||||
* 验证规则:
|
||||
* - 必填字段:username, nickname
|
||||
* - 唯一性字段:username, email, phone, github_id
|
||||
* - 长度限制:username(1-50), nickname(1-50), github_id(1-100)
|
||||
* - 格式验证:email格式, phone国际格式
|
||||
* - 数值范围:role(1-9)
|
||||
*/
|
||||
export class CreateUserDto {
|
||||
/**
|
||||
* 用户名
|
||||
*
|
||||
* 业务规则:
|
||||
* - 必填字段,用于用户登录和唯一标识
|
||||
* - 长度限制:1-50个字符
|
||||
* - 全局唯一性:不允许重复
|
||||
* - 建议使用字母、数字、下划线组合
|
||||
*
|
||||
* 验证规则:
|
||||
* - 非空验证:确保用户名不为空
|
||||
* - 字符串类型验证
|
||||
* - 长度范围验证:1-50字符
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '用户名不能为空' })
|
||||
@Length(1, FIELD_LIMITS.USERNAME_MAX_LENGTH, { message: `用户名长度需在1-${FIELD_LIMITS.USERNAME_MAX_LENGTH}字符之间` })
|
||||
username: string;
|
||||
|
||||
/**
|
||||
* 邮箱地址
|
||||
*
|
||||
* 业务规则:
|
||||
* - 可选字段,用于账户找回和通知
|
||||
* - 全局唯一性:不允许重复
|
||||
* - 支持标准邮箱格式验证
|
||||
* - OAuth登录时可能为空
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段验证
|
||||
* - 邮箱格式验证:符合RFC标准
|
||||
* - 长度限制:最大100字符(数据库约束)
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsEmail({}, { message: '邮箱格式不正确' })
|
||||
email?: string;
|
||||
|
||||
/**
|
||||
* 手机号码
|
||||
*
|
||||
* 业务规则:
|
||||
* - 可选字段,用于账户找回和通知
|
||||
* - 全局唯一性:不允许重复
|
||||
* - 支持国际手机号格式
|
||||
* - 用于短信验证和双因子认证
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段验证
|
||||
* - 国际手机号格式验证
|
||||
* - 长度限制:最大30字符(数据库约束)
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsPhoneNumber(null, { message: '手机号格式不正确' })
|
||||
phone?: string;
|
||||
|
||||
/**
|
||||
* 密码哈希值
|
||||
*
|
||||
* 业务规则:
|
||||
* - 可选字段,OAuth登录时为空
|
||||
* - 存储加密后的密码,不存储明文
|
||||
* - 用于传统用户名密码登录方式
|
||||
* - 应使用bcrypt等安全哈希算法
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段验证
|
||||
* - 字符串类型验证
|
||||
* - 长度限制:最大255字符(数据库约束)
|
||||
*
|
||||
* 安全注意:
|
||||
* - 传输过程中应使用HTTPS
|
||||
* - 日志记录时会自动脱敏处理
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString({ message: '密码哈希必须是字符串' })
|
||||
password_hash?: string;
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*
|
||||
* 业务规则:
|
||||
* - 必填字段,用于游戏内显示
|
||||
* - 长度限制:1-50个字符
|
||||
* - 支持中文、英文、数字等字符
|
||||
* - 可以与用户名不同,更友好的显示名称
|
||||
*
|
||||
* 验证规则:
|
||||
* - 非空验证:确保昵称不为空
|
||||
* - 字符串类型验证
|
||||
* - 长度范围验证:1-50字符
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '昵称不能为空' })
|
||||
@Length(1, FIELD_LIMITS.NICKNAME_MAX_LENGTH, { message: `昵称长度需在1-${FIELD_LIMITS.NICKNAME_MAX_LENGTH}字符之间` })
|
||||
nickname: string;
|
||||
|
||||
/**
|
||||
* GitHub用户标识
|
||||
*
|
||||
* 业务规则:
|
||||
* - 可选字段,用于GitHub OAuth登录
|
||||
* - 全局唯一性:不允许重复
|
||||
* - 存储GitHub用户的唯一标识符
|
||||
* - 用于关联GitHub账户信息
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段验证
|
||||
* - 字符串类型验证
|
||||
* - 长度范围验证:1-100字符
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString({ message: 'GitHub ID必须是字符串' })
|
||||
@Length(1, FIELD_LIMITS.GITHUB_ID_MAX_LENGTH, { message: `GitHub ID长度需在1-${FIELD_LIMITS.GITHUB_ID_MAX_LENGTH}字符之间` })
|
||||
github_id?: string;
|
||||
|
||||
/**
|
||||
* 用户头像链接
|
||||
*
|
||||
* 业务规则:
|
||||
* - 可选字段,用于显示用户头像
|
||||
* - 支持GitHub头像或自定义头像
|
||||
* - 应为有效的HTTP/HTTPS链接
|
||||
* - 建议使用CDN加速访问
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段验证
|
||||
* - 字符串类型验证
|
||||
* - 长度限制:最大255字符(数据库约束)
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString({ message: '头像URL必须是字符串' })
|
||||
avatar_url?: string;
|
||||
|
||||
/**
|
||||
* 用户角色
|
||||
*
|
||||
* 业务规则:
|
||||
* - 可选字段,默认为普通用户(1)
|
||||
* - 角色级别:1-普通用户,9-管理员
|
||||
* - 控制用户在系统中的权限范围
|
||||
* - 管理员具有系统管理权限
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段验证
|
||||
* - 整数类型验证
|
||||
* - 数值范围验证:1-9之间
|
||||
* - 默认值:1(普通用户)
|
||||
*
|
||||
* 权限说明:
|
||||
* - 1: 普通用户 - 基础游戏功能
|
||||
* - 9: 管理员 - 系统管理权限
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsInt({ message: '角色必须是数字' })
|
||||
@Min(USER_ROLES.NORMAL_USER, { message: `角色值最小为${USER_ROLES.NORMAL_USER}` })
|
||||
@Max(USER_ROLES.ADMIN, { message: `角色值最大为${USER_ROLES.ADMIN}` })
|
||||
role?: number = USER_ROLES.NORMAL_USER;
|
||||
|
||||
/**
|
||||
* 邮箱验证状态
|
||||
*
|
||||
* 业务规则:
|
||||
* - 可选字段,默认为false(未验证)
|
||||
* - 控制邮箱相关功能的可用性
|
||||
* - OAuth登录时可直接设为true
|
||||
* - 影响密码重置等安全功能
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段验证
|
||||
* - 布尔类型验证
|
||||
* - 默认值:false(未验证)
|
||||
*/
|
||||
@IsOptional()
|
||||
email_verified?: boolean = false;
|
||||
|
||||
/**
|
||||
* 用户状态
|
||||
*
|
||||
* 业务规则:
|
||||
* - 可选字段,默认为active(正常状态)
|
||||
* - 控制用户账户的可用性和权限
|
||||
* - 支持多种状态:正常、未激活、锁定、禁用等
|
||||
* - 影响用户登录和API访问权限
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段验证
|
||||
* - 枚举类型验证
|
||||
* - 默认值:active(正常状态)
|
||||
*
|
||||
* 状态说明:
|
||||
* - active: 正常状态,可以正常使用
|
||||
* - inactive: 未激活,需要邮箱验证
|
||||
* - locked: 已锁定,临时禁用
|
||||
* - banned: 已禁用,管理员操作
|
||||
* - deleted: 已删除,软删除状态
|
||||
* - pending: 待审核,需要管理员审核
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsEnum(UserStatus, { message: '用户状态必须是有效的枚举值' })
|
||||
status?: UserStatus = UserStatus.ACTIVE;
|
||||
}
|
||||
497
src/core/db/users/users.entity.ts
Normal file
497
src/core/db/users/users.entity.ts
Normal file
@@ -0,0 +1,497 @@
|
||||
/**
|
||||
* 用户数据实体模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义用户数据表的实体映射和字段约束
|
||||
* - 提供用户数据的持久化存储结构
|
||||
* - 支持多种登录方式的用户信息存储
|
||||
* - 实现完整的用户数据模型和关系映射
|
||||
*
|
||||
* 职责分离:
|
||||
* - 数据映射:TypeORM实体与数据库表的映射关系
|
||||
* - 约束定义:字段类型、长度、唯一性等约束规则
|
||||
* - 关系管理:与其他实体的关联关系定义
|
||||
* - 索引优化:数据库查询性能优化策略
|
||||
*
|
||||
* 依赖模块:
|
||||
* - TypeORM: ORM框架,提供数据库映射功能
|
||||
* - MySQL: 底层数据库存储
|
||||
*
|
||||
* 数据库表:users
|
||||
* 存储引擎:InnoDB
|
||||
* 字符集:utf8mb4
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 完善注释规范,添加完整的文件头和字段注释
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-17
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToOne } from 'typeorm';
|
||||
import { UserStatus } from './user_status.enum';
|
||||
import { ZulipAccounts } from '../zulip_accounts/zulip_accounts.entity';
|
||||
import { FIELD_LIMITS } from './users.constants';
|
||||
|
||||
/**
|
||||
* 用户实体类
|
||||
*
|
||||
* 职责:
|
||||
* - 映射数据库users表的结构和约束
|
||||
* - 定义用户数据的字段类型和验证规则
|
||||
* - 提供用户信息的完整数据模型
|
||||
*
|
||||
* 主要功能:
|
||||
* - 用户身份标识和认证信息存储
|
||||
* - 支持传统登录和OAuth第三方登录
|
||||
* - 用户基础信息和角色权限管理
|
||||
* - 自动时间戳记录和更新
|
||||
*
|
||||
* 数据完整性:
|
||||
* - 主键约束:id字段自增主键
|
||||
* - 唯一约束:username, email, phone, github_id
|
||||
* - 非空约束:username, nickname, role
|
||||
* - 外键关联:可扩展关联用户详情、权限等表
|
||||
*
|
||||
* 使用场景:
|
||||
* - 用户注册和登录验证
|
||||
* - 用户信息查询和更新
|
||||
* - 权限验证和角色管理
|
||||
* - 用户数据统计和分析
|
||||
*
|
||||
* 索引策略:
|
||||
* - 主键索引:id (自动创建)
|
||||
* - 唯一索引:username, email, phone, github_id
|
||||
* - 普通索引:role (用于角色查询)
|
||||
* - 复合索引:created_at + role (用于分页查询)
|
||||
*/
|
||||
@Entity('users')
|
||||
export class Users {
|
||||
/**
|
||||
* 用户主键ID
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:BIGINT,支持大量用户数据
|
||||
* - 约束:主键、非空、自增
|
||||
* - 范围:1 ~ 9,223,372,036,854,775,807
|
||||
*
|
||||
* 业务规则:
|
||||
* - 系统自动生成,不可手动指定
|
||||
* - 全局唯一标识符,用于用户关联
|
||||
* - 作为其他表的外键引用
|
||||
*
|
||||
* 性能考虑:
|
||||
* - 自增主键,插入性能优异
|
||||
* - 聚簇索引,范围查询效率高
|
||||
* - BIGINT类型,避免ID耗尽问题
|
||||
*/
|
||||
@PrimaryGeneratedColumn({
|
||||
type: 'bigint',
|
||||
comment: '主键ID'
|
||||
})
|
||||
id: bigint;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:VARCHAR(50),支持多语言字符
|
||||
* - 约束:非空、唯一索引
|
||||
* - 字符集:utf8mb4,支持emoji等特殊字符
|
||||
*
|
||||
* 业务规则:
|
||||
* - 用户登录的唯一标识符
|
||||
* - 全系统唯一,不允许重复
|
||||
* - 长度限制:1-50个字符
|
||||
* - 建议格式:字母、数字、下划线组合
|
||||
*
|
||||
* 安全考虑:
|
||||
* - 不应包含敏感信息
|
||||
* - 避免使用易猜测的用户名
|
||||
* - 支持用户名修改(需要额外验证)
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: FIELD_LIMITS.USERNAME_MAX_LENGTH,
|
||||
nullable: false,
|
||||
unique: true,
|
||||
comment: '唯一用户名/登录名'
|
||||
})
|
||||
username: string;
|
||||
|
||||
/**
|
||||
* 邮箱地址
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:VARCHAR(100),支持长邮箱地址
|
||||
* - 约束:允许空、唯一索引
|
||||
* - 索引:用于快速邮箱查找
|
||||
*
|
||||
* 业务规则:
|
||||
* - 用于账户找回和重要通知
|
||||
* - 全系统唯一,不允许重复
|
||||
* - OAuth登录时可能为空
|
||||
* - 支持邮箱验证和双因子认证
|
||||
*
|
||||
* 隐私保护:
|
||||
* - 敏感信息,日志记录时脱敏
|
||||
* - 仅用于系统通知,不对外展示
|
||||
* - 支持用户自主修改和验证
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: FIELD_LIMITS.EMAIL_MAX_LENGTH,
|
||||
nullable: true,
|
||||
unique: true,
|
||||
comment: '邮箱(用于找回/通知)'
|
||||
})
|
||||
email: string;
|
||||
|
||||
/**
|
||||
* 邮箱验证状态
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:BOOLEAN,布尔值
|
||||
* - 约束:非空、默认值false
|
||||
* - 索引:用于查询已验证用户
|
||||
*
|
||||
* 业务规则:
|
||||
* - false:邮箱未验证
|
||||
* - true:邮箱已验证
|
||||
* - 影响密码重置等安全功能
|
||||
* - OAuth登录时可直接设为true
|
||||
*
|
||||
* 安全考虑:
|
||||
* - 未验证邮箱限制部分功能
|
||||
* - 验证后才能用于密码重置
|
||||
* - 支持重新发送验证邮件
|
||||
*/
|
||||
@Column({
|
||||
type: 'boolean',
|
||||
nullable: false,
|
||||
default: false,
|
||||
comment: '邮箱是否已验证'
|
||||
})
|
||||
email_verified: boolean;
|
||||
|
||||
/**
|
||||
* 手机号码
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:VARCHAR(30),支持国际号码格式
|
||||
* - 约束:允许空、唯一索引
|
||||
* - 格式:包含国家代码的完整号码
|
||||
*
|
||||
* 业务规则:
|
||||
* - 用于账户找回和短信通知
|
||||
* - 全系统唯一,不允许重复
|
||||
* - 支持国际手机号格式(+86、+1等)
|
||||
* - 用于短信验证码和双因子认证
|
||||
*
|
||||
* 隐私保护:
|
||||
* - 敏感信息,日志记录时脱敏
|
||||
* - 仅用于安全验证,不对外展示
|
||||
* - 支持用户自主修改和验证
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: FIELD_LIMITS.PHONE_MAX_LENGTH,
|
||||
nullable: true,
|
||||
unique: true,
|
||||
comment: '全球电话号码(用于找回/通知)'
|
||||
})
|
||||
phone: string;
|
||||
|
||||
/**
|
||||
* 密码哈希值
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:VARCHAR(255),支持各种哈希算法
|
||||
* - 约束:允许空(OAuth登录时)
|
||||
* - 存储:加密后的哈希值,不存储明文
|
||||
*
|
||||
* 业务规则:
|
||||
* - 传统用户名密码登录方式使用
|
||||
* - OAuth第三方登录时此字段为空
|
||||
* - 使用bcrypt等安全哈希算法
|
||||
* - 支持密码强度验证和定期更新
|
||||
*
|
||||
* 安全措施:
|
||||
* - 绝不存储明文密码
|
||||
* - 使用盐值防止彩虹表攻击
|
||||
* - 日志系统自动脱敏处理
|
||||
* - 传输过程使用HTTPS加密
|
||||
* - 支持密码重置和修改功能
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: FIELD_LIMITS.PASSWORD_HASH_MAX_LENGTH,
|
||||
nullable: true,
|
||||
comment: '密码哈希(OAuth登录为空)'
|
||||
})
|
||||
password_hash: string;
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:VARCHAR(50),支持多语言字符
|
||||
* - 约束:非空,无唯一性要求
|
||||
* - 字符集:utf8mb4,支持emoji表情
|
||||
*
|
||||
* 业务规则:
|
||||
* - 游戏内显示的友好名称
|
||||
* - 允许重复,提高用户体验
|
||||
* - 长度限制:1-50个字符
|
||||
* - 支持中文、英文、数字、表情符号
|
||||
*
|
||||
* 显示规则:
|
||||
* - 游戏内头顶显示名称
|
||||
* - 聊天消息发送者标识
|
||||
* - 排行榜和用户列表显示
|
||||
* - 支持用户随时修改
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: FIELD_LIMITS.NICKNAME_MAX_LENGTH,
|
||||
nullable: false,
|
||||
comment: '显示昵称(头顶显示)'
|
||||
})
|
||||
nickname: string;
|
||||
|
||||
/**
|
||||
* GitHub用户标识
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:VARCHAR(100),存储GitHub用户ID
|
||||
* - 约束:允许空、唯一索引
|
||||
* - 用途:GitHub OAuth登录关联
|
||||
*
|
||||
* 业务规则:
|
||||
* - GitHub第三方登录的唯一标识
|
||||
* - 全系统唯一,不允许重复
|
||||
* - 用于关联GitHub账户信息
|
||||
* - 支持GitHub头像和基础信息同步
|
||||
*
|
||||
* OAuth集成:
|
||||
* - 存储GitHub返回的用户ID
|
||||
* - 用于后续API调用身份验证
|
||||
* - 支持账户绑定和解绑操作
|
||||
* - 可扩展支持其他OAuth提供商
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: FIELD_LIMITS.GITHUB_ID_MAX_LENGTH,
|
||||
nullable: true,
|
||||
unique: true,
|
||||
comment: 'GitHub OpenID(第三方登录用)'
|
||||
})
|
||||
github_id: string;
|
||||
|
||||
/**
|
||||
* 用户头像链接
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:VARCHAR(255),支持长URL
|
||||
* - 约束:允许空,无唯一性要求
|
||||
* - 存储:完整的HTTP/HTTPS链接
|
||||
*
|
||||
* 业务规则:
|
||||
* - 用户头像图片的访问链接
|
||||
* - 支持GitHub头像或自定义上传
|
||||
* - 建议使用CDN加速访问
|
||||
* - 支持多种图片格式(jpg、png、gif等)
|
||||
*
|
||||
* 性能优化:
|
||||
* - 建议使用图片CDN服务
|
||||
* - 支持多尺寸头像适配
|
||||
* - 缓存策略优化加载速度
|
||||
* - 默认头像兜底机制
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: FIELD_LIMITS.AVATAR_URL_MAX_LENGTH,
|
||||
nullable: true,
|
||||
comment: 'GitHub头像或自定义头像URL'
|
||||
})
|
||||
avatar_url: string;
|
||||
|
||||
/**
|
||||
* 用户角色
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:TINYINT,节省存储空间
|
||||
* - 约束:非空、默认值1
|
||||
* - 范围:1-9,支持角色扩展
|
||||
*
|
||||
* 业务规则:
|
||||
* - 控制用户在系统中的权限级别
|
||||
* - 1:普通用户,基础游戏功能
|
||||
* - 9:管理员,系统管理权限
|
||||
* - 支持角色升级和降级操作
|
||||
*
|
||||
* 权限设计:
|
||||
* - 基于角色的访问控制(RBAC)
|
||||
* - 支持细粒度权限配置
|
||||
* - 可扩展更多角色类型
|
||||
* - 权限验证中间件集成
|
||||
*
|
||||
* 扩展性:
|
||||
* - 预留2-8角色级别供未来使用
|
||||
* - 支持角色权限动态配置
|
||||
* - 可关联角色权限表进行扩展
|
||||
*/
|
||||
@Column({
|
||||
type: 'tinyint',
|
||||
nullable: false,
|
||||
default: 1,
|
||||
comment: '角色:1-普通,9-管理员'
|
||||
})
|
||||
role: number;
|
||||
|
||||
/**
|
||||
* 用户状态
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:VARCHAR(20),存储状态枚举值
|
||||
* - 约束:非空、默认值'active'
|
||||
* - 索引:用于状态查询和统计
|
||||
*
|
||||
* 业务规则:
|
||||
* - 控制用户账户的可用性和权限
|
||||
* - active:正常状态,可以正常使用
|
||||
* - inactive:未激活,需要邮箱验证
|
||||
* - locked:已锁定,临时禁用
|
||||
* - banned:已禁用,管理员操作
|
||||
* - deleted:已删除,软删除状态
|
||||
* - pending:待审核,需要管理员审核
|
||||
*
|
||||
* 安全控制:
|
||||
* - 登录时检查状态权限
|
||||
* - API访问时验证状态
|
||||
* - 状态变更记录审计日志
|
||||
* - 支持批量状态管理
|
||||
*
|
||||
* 应用场景:
|
||||
* - 账户安全管理
|
||||
* - 用户生命周期控制
|
||||
* - 违规用户处理
|
||||
* - 系统维护和升级
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: FIELD_LIMITS.STATUS_MAX_LENGTH,
|
||||
nullable: true,
|
||||
default: UserStatus.ACTIVE,
|
||||
comment: '用户状态:active-正常,inactive-未激活,locked-锁定,banned-禁用,deleted-删除,pending-待审核'
|
||||
})
|
||||
status?: UserStatus;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:DATETIME,精确到秒
|
||||
* - 约束:非空、默认当前时间
|
||||
* - 时区:使用系统时区,建议UTC
|
||||
*
|
||||
* 业务规则:
|
||||
* - 记录用户注册的准确时间
|
||||
* - 用于用户数据统计和分析
|
||||
* - 支持按时间范围查询用户
|
||||
* - 不可修改,保证数据完整性
|
||||
*
|
||||
* 应用场景:
|
||||
* - 用户注册趋势分析
|
||||
* - 新用户欢迎流程触发
|
||||
* - 数据审计和合规要求
|
||||
* - 用户生命周期管理
|
||||
*/
|
||||
@CreateDateColumn({
|
||||
type: 'datetime',
|
||||
nullable: false,
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
comment: '注册时间'
|
||||
})
|
||||
created_at: Date;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:DATETIME,精确到秒
|
||||
* - 约束:非空、自动更新
|
||||
* - 触发:任何字段更新时自动刷新
|
||||
*
|
||||
* 业务规则:
|
||||
* - 记录用户信息最后修改时间
|
||||
* - 数据库级别自动维护
|
||||
* - 用于数据同步和缓存失效
|
||||
* - 支持增量数据同步
|
||||
*
|
||||
* 应用场景:
|
||||
* - 数据变更审计
|
||||
* - 缓存更新策略
|
||||
* - 数据同步时间戳
|
||||
* - 用户活跃度分析
|
||||
*/
|
||||
@UpdateDateColumn({
|
||||
type: 'datetime',
|
||||
nullable: false,
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
onUpdate: 'CURRENT_TIMESTAMP',
|
||||
comment: '更新时间'
|
||||
})
|
||||
updated_at: Date;
|
||||
|
||||
/**
|
||||
* 删除时间
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:DATETIME,精确到秒
|
||||
* - 约束:允许空,软删除时手动设置
|
||||
* - 索引:用于过滤已删除记录
|
||||
*
|
||||
* 业务规则:
|
||||
* - null:正常状态,未删除
|
||||
* - 有值:已软删除,记录删除时间
|
||||
* - 软删除的记录在查询时需要手动过滤
|
||||
* - 支持数据恢复和审计追踪
|
||||
*
|
||||
* 应用场景:
|
||||
* - 数据安全删除,避免误删
|
||||
* - 数据审计和合规要求
|
||||
* - 支持数据恢复功能
|
||||
* - 删除操作的时间追踪
|
||||
*/
|
||||
// @Column({
|
||||
// type: 'datetime',
|
||||
// nullable: true,
|
||||
// default: null,
|
||||
// comment: '软删除时间,null表示未删除'
|
||||
// })
|
||||
// deleted_at?: Date;
|
||||
|
||||
/**
|
||||
* 关联的Zulip账号
|
||||
*
|
||||
* 关系设计:
|
||||
* - 类型:一对一关系(OneToOne)
|
||||
* - 外键:在ZulipAccounts表中
|
||||
* - 级联:不设置级联删除,保证数据安全
|
||||
*
|
||||
* 业务规则:
|
||||
* - 每个游戏用户最多关联一个Zulip账号
|
||||
* - 支持延迟加载,提高查询性能
|
||||
* - 可选关联,不是所有用户都有Zulip账号
|
||||
*
|
||||
* 使用场景:
|
||||
* - 游戏内聊天功能集成
|
||||
* - 跨平台消息同步
|
||||
* - 用户身份验证和权限管理
|
||||
*/
|
||||
@OneToOne(() => ZulipAccounts, zulipAccount => zulipAccount.gameUser)
|
||||
zulipAccount?: ZulipAccounts;
|
||||
}
|
||||
75
src/core/db/users/users.module.ts
Normal file
75
src/core/db/users/users.module.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 用户模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 整合用户相关的实体、服务和控制器
|
||||
* - 配置TypeORM实体和Repository
|
||||
* - 支持数据库和内存存储的动态切换
|
||||
* - 导出用户服务供其他模块使用
|
||||
*
|
||||
* 职责分离:
|
||||
* - 模块配置:动态模块的创建和依赖注入配置
|
||||
* - 存储切换:数据库模式和内存模式的灵活切换
|
||||
* - 服务导出:统一的服务接口导出和类型安全
|
||||
* - 依赖管理:模块间依赖关系的清晰定义
|
||||
*
|
||||
* 存储模式:
|
||||
* - 数据库模式:使用TypeORM连接MySQL数据库
|
||||
* - 内存模式:使用Map存储,适用于开发和测试
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 完善注释规范,添加完整的文件头和方法注释
|
||||
* - 2025-12-17: 功能新增 - 添加双存储模式支持,by angjustinl
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-17
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { Module, DynamicModule, Global } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Users } from './users.entity';
|
||||
import { UsersService } from './users.service';
|
||||
import { UsersMemoryService } from './users_memory.service';
|
||||
|
||||
@Global()
|
||||
@Module({})
|
||||
export class UsersModule {
|
||||
/**
|
||||
* 创建数据库模式的用户模块
|
||||
*
|
||||
* @returns 配置了TypeORM的动态模块
|
||||
*/
|
||||
static forDatabase(): DynamicModule {
|
||||
return {
|
||||
module: UsersModule,
|
||||
imports: [TypeOrmModule.forFeature([Users])],
|
||||
providers: [
|
||||
{
|
||||
provide: 'UsersService',
|
||||
useClass: UsersService,
|
||||
},
|
||||
],
|
||||
exports: ['UsersService', TypeOrmModule],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建内存模式的用户模块
|
||||
*
|
||||
* @returns 配置了内存存储的动态模块
|
||||
*/
|
||||
static forMemory(): DynamicModule {
|
||||
return {
|
||||
module: UsersModule,
|
||||
providers: [
|
||||
{
|
||||
provide: 'UsersService',
|
||||
useClass: UsersMemoryService,
|
||||
},
|
||||
],
|
||||
exports: ['UsersService'],
|
||||
};
|
||||
}
|
||||
}
|
||||
714
src/core/db/users/users.service.ts
Normal file
714
src/core/db/users/users.service.ts
Normal file
@@ -0,0 +1,714 @@
|
||||
/**
|
||||
* 用户服务类
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供用户数据的增删改查技术实现
|
||||
* - 处理数据持久化和存储操作
|
||||
* - 数据格式验证和约束检查
|
||||
* - 支持完整的数据生命周期管理
|
||||
*
|
||||
* 职责分离:
|
||||
* - 数据持久化:通过TypeORM操作MySQL数据库
|
||||
* - 数据验证:数据格式和约束完整性检查
|
||||
* - 异常处理:统一的错误处理和日志记录
|
||||
* - 性能监控:操作耗时统计和性能优化
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 完善注释规范,添加完整的文件头和方法注释
|
||||
* - 2026-01-07: 功能优化 - 添加完整的日志记录系统和详细的技术实现注释
|
||||
* - 2026-01-07: 性能优化 - 优化异常处理和性能监控机制
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-17
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, FindOptionsWhere } from 'typeorm';
|
||||
import { Users } from './users.entity';
|
||||
import { CreateUserDto } from './users.dto';
|
||||
import { UserStatus } from './user_status.enum';
|
||||
import { validate } from 'class-validator';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { BaseUsersService } from './base_users.service';
|
||||
import { USER_ROLES, QUERY_LIMITS, ERROR_MESSAGES, DATABASE_CONSTANTS, ValidationUtils, PerformanceMonitor } from './users.constants';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService extends BaseUsersService {
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Users)
|
||||
private readonly usersRepository: Repository<Users>,
|
||||
) {
|
||||
super(); // 调用基类构造函数
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新用户
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 验证输入数据的格式和完整性
|
||||
* 2. 创建用户实体并设置默认值
|
||||
* 3. 保存用户数据到数据库
|
||||
* 4. 记录操作日志和性能指标
|
||||
*
|
||||
* @param createUserDto 创建用户的数据传输对象,包含用户基本信息
|
||||
* @returns 创建成功的用户实体,包含自动生成的ID和时间戳
|
||||
* @throws BadRequestException 当数据验证失败或输入格式错误时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const newUser = await usersService.create({
|
||||
* username: 'testuser',
|
||||
* email: 'test@example.com',
|
||||
* nickname: '测试用户',
|
||||
* password_hash: 'hashed_password'
|
||||
* });
|
||||
* console.log(`用户创建成功,ID: ${newUser.id}`);
|
||||
* ```
|
||||
*/
|
||||
async create(createUserDto: CreateUserDto): Promise<Users> {
|
||||
const monitor = PerformanceMonitor.create();
|
||||
|
||||
this.logger.log('开始创建用户', {
|
||||
operation: 'create',
|
||||
username: createUserDto.username,
|
||||
email: createUserDto.email,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
// 验证DTO
|
||||
await this.validateCreateUserDto(createUserDto);
|
||||
|
||||
// 创建用户实体
|
||||
const user = this.buildUserEntity(createUserDto);
|
||||
|
||||
// 保存到数据库
|
||||
const savedUser = await this.usersRepository.save(user);
|
||||
|
||||
this.logger.log('用户创建成功', {
|
||||
operation: 'create',
|
||||
userId: savedUser.id.toString(),
|
||||
username: savedUser.username,
|
||||
email: savedUser.email,
|
||||
duration: monitor.getDuration(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return savedUser;
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.error('用户创建系统异常', {
|
||||
operation: 'create',
|
||||
username: createUserDto.username,
|
||||
email: createUserDto.email,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration: monitor.getDuration(),
|
||||
timestamp: new Date().toISOString()
|
||||
}, error instanceof Error ? error.stack : undefined);
|
||||
|
||||
throw new BadRequestException(ERROR_MESSAGES.USER_CREATE_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证创建用户DTO
|
||||
*
|
||||
* @param createUserDto 用户数据
|
||||
* @throws BadRequestException 当数据验证失败时
|
||||
*/
|
||||
private async validateCreateUserDto(createUserDto: CreateUserDto): Promise<void> {
|
||||
const dto = plainToClass(CreateUserDto, createUserDto);
|
||||
const validationErrors = await validate(dto);
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
const errorMessages = ValidationUtils.formatValidationErrors(validationErrors);
|
||||
|
||||
this.logger.warn('用户创建失败:数据验证失败', {
|
||||
operation: 'create',
|
||||
username: createUserDto.username,
|
||||
email: createUserDto.email,
|
||||
validationErrors: errorMessages
|
||||
});
|
||||
|
||||
throw new BadRequestException(`${ERROR_MESSAGES.VALIDATION_FAILED}: ${errorMessages}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建用户实体
|
||||
*
|
||||
* @param createUserDto 用户数据
|
||||
* @returns 用户实体
|
||||
*/
|
||||
private buildUserEntity(createUserDto: CreateUserDto): Users {
|
||||
const user = new Users();
|
||||
user.username = createUserDto.username;
|
||||
user.email = createUserDto.email || null;
|
||||
user.phone = createUserDto.phone || null;
|
||||
user.password_hash = createUserDto.password_hash || null;
|
||||
user.nickname = createUserDto.nickname;
|
||||
user.github_id = createUserDto.github_id || null;
|
||||
user.avatar_url = createUserDto.avatar_url || null;
|
||||
user.role = createUserDto.role || USER_ROLES.NORMAL_USER;
|
||||
user.email_verified = createUserDto.email_verified || false;
|
||||
user.status = createUserDto.status || UserStatus.ACTIVE;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新用户(带重复检查)
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 检查用户名、邮箱、手机号、GitHub ID的唯一性约束
|
||||
* 2. 如果所有检查都通过,调用create方法创建用户
|
||||
* 3. 记录操作日志和性能指标
|
||||
*
|
||||
* @param createUserDto 创建用户的数据传输对象
|
||||
* @returns 创建的用户实体
|
||||
* @throws ConflictException 当用户名、邮箱、手机号或GitHub ID已存在时
|
||||
* @throws BadRequestException 当数据验证失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const newUser = await usersService.createWithDuplicateCheck({
|
||||
* username: 'testuser',
|
||||
* email: 'test@example.com',
|
||||
* nickname: '测试用户'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
async createWithDuplicateCheck(createUserDto: CreateUserDto): Promise<Users> {
|
||||
const monitor = PerformanceMonitor.create();
|
||||
|
||||
this.logStart('创建用户(带重复检查)', {
|
||||
username: createUserDto.username,
|
||||
email: createUserDto.email,
|
||||
phone: createUserDto.phone,
|
||||
github_id: createUserDto.github_id
|
||||
});
|
||||
|
||||
try {
|
||||
// 执行所有唯一性检查
|
||||
await this.validateUniqueness(createUserDto);
|
||||
|
||||
// 调用普通的创建方法
|
||||
const user = await this.create(createUserDto);
|
||||
|
||||
this.logSuccess('创建用户(带重复检查)', {
|
||||
userId: user.id.toString(),
|
||||
username: user.username
|
||||
}, monitor.getDuration());
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
this.handleServiceError(error, '创建用户(带重复检查)', {
|
||||
username: createUserDto.username,
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证用户数据的唯一性
|
||||
*
|
||||
* @param createUserDto 用户数据
|
||||
* @throws ConflictException 当发现重复数据时
|
||||
*/
|
||||
private async validateUniqueness(createUserDto: CreateUserDto): Promise<void> {
|
||||
await this.checkUsernameUniqueness(createUserDto.username);
|
||||
await this.checkEmailUniqueness(createUserDto.email);
|
||||
await this.checkPhoneUniqueness(createUserDto.phone);
|
||||
await this.checkGithubIdUniqueness(createUserDto.github_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户名唯一性
|
||||
*/
|
||||
private async checkUsernameUniqueness(username?: string): Promise<void> {
|
||||
if (username) {
|
||||
const existingUser = await this.usersRepository.findOne({
|
||||
where: { username }
|
||||
});
|
||||
if (existingUser) {
|
||||
this.logger.warn('用户创建失败:用户名已存在', {
|
||||
operation: 'uniqueness_check',
|
||||
username,
|
||||
existingUserId: existingUser.id.toString()
|
||||
});
|
||||
throw new ConflictException(ERROR_MESSAGES.USERNAME_EXISTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查邮箱唯一性
|
||||
*/
|
||||
private async checkEmailUniqueness(email?: string): Promise<void> {
|
||||
if (email) {
|
||||
const existingEmail = await this.usersRepository.findOne({
|
||||
where: { email }
|
||||
});
|
||||
if (existingEmail) {
|
||||
this.logger.warn('用户创建失败:邮箱已存在', {
|
||||
operation: 'uniqueness_check',
|
||||
email,
|
||||
existingUserId: existingEmail.id.toString()
|
||||
});
|
||||
throw new ConflictException(ERROR_MESSAGES.EMAIL_EXISTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查手机号唯一性
|
||||
*/
|
||||
private async checkPhoneUniqueness(phone?: string): Promise<void> {
|
||||
if (phone) {
|
||||
const existingPhone = await this.usersRepository.findOne({
|
||||
where: { phone }
|
||||
});
|
||||
if (existingPhone) {
|
||||
this.logger.warn('用户创建失败:手机号已存在', {
|
||||
operation: 'uniqueness_check',
|
||||
phone,
|
||||
existingUserId: existingPhone.id.toString()
|
||||
});
|
||||
throw new ConflictException(ERROR_MESSAGES.PHONE_EXISTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查GitHub ID唯一性
|
||||
*/
|
||||
private async checkGithubIdUniqueness(githubId?: string): Promise<void> {
|
||||
if (githubId) {
|
||||
const existingGithub = await this.usersRepository.findOne({
|
||||
where: { github_id: githubId }
|
||||
});
|
||||
if (existingGithub) {
|
||||
this.logger.warn('用户创建失败:GitHub ID已存在', {
|
||||
operation: 'uniqueness_check',
|
||||
github_id: githubId,
|
||||
existingUserId: existingGithub.id.toString()
|
||||
});
|
||||
throw new ConflictException(ERROR_MESSAGES.GITHUB_ID_EXISTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有用户
|
||||
*
|
||||
* @param limit 限制返回数量,默认100
|
||||
* @param offset 偏移量,默认0
|
||||
* @param includeDeleted 是否包含已删除用户,默认false
|
||||
* @returns 用户列表
|
||||
*/
|
||||
async findAll(limit: number = QUERY_LIMITS.DEFAULT_LIMIT, offset: number = 0, includeDeleted: boolean = false): Promise<Users[]> {
|
||||
// 注意:软删除功能暂未实现,includeDeleted参数预留用于未来扩展
|
||||
const whereCondition = {};
|
||||
|
||||
return await this.usersRepository.find({
|
||||
where: whereCondition,
|
||||
take: limit,
|
||||
skip: offset,
|
||||
order: { created_at: DATABASE_CONSTANTS.ORDER_DESC }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询用户
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @param includeDeleted 是否包含已删除用户,默认false
|
||||
* @returns 用户实体
|
||||
* @throws NotFoundException 当用户不存在时
|
||||
*/
|
||||
async findOne(id: bigint, includeDeleted: boolean = false): Promise<Users> {
|
||||
// 注意:软删除功能暂未实现,includeDeleted参数预留用于未来扩展
|
||||
const whereCondition = { id };
|
||||
|
||||
const user = await this.usersRepository.findOne({
|
||||
where: whereCondition
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException(`ID为 ${id} 的用户不存在`);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param includeDeleted 是否包含已删除用户,默认false
|
||||
* @returns 用户实体或null
|
||||
*/
|
||||
async findByUsername(username: string, includeDeleted: boolean = false): Promise<Users | null> {
|
||||
// 注意:软删除功能暂未实现,includeDeleted参数预留用于未来扩展
|
||||
const whereCondition = { username };
|
||||
|
||||
return await this.usersRepository.findOne({
|
||||
where: whereCondition
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据邮箱查询用户
|
||||
*
|
||||
* @param email 邮箱
|
||||
* @param includeDeleted 是否包含已删除用户,默认false
|
||||
* @returns 用户实体或null
|
||||
*/
|
||||
async findByEmail(email: string, includeDeleted: boolean = false): Promise<Users | null> {
|
||||
// 注意:软删除功能暂未实现,includeDeleted参数预留用于未来扩展
|
||||
const whereCondition = { email };
|
||||
|
||||
return await this.usersRepository.findOne({
|
||||
where: whereCondition
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据GitHub ID查询用户
|
||||
*
|
||||
* @param githubId GitHub ID
|
||||
* @param includeDeleted 是否包含已删除用户,默认false
|
||||
* @returns 用户实体或null
|
||||
*/
|
||||
async findByGithubId(githubId: string, includeDeleted: boolean = false): Promise<Users | null> {
|
||||
// 注意:软删除功能暂未实现,includeDeleted参数预留用于未来扩展
|
||||
const whereCondition = { github_id: githubId };
|
||||
|
||||
return await this.usersRepository.findOne({
|
||||
where: whereCondition
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*
|
||||
* 功能描述:
|
||||
* 更新指定用户的信息,包含完整的数据验证和唯一性检查
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证用户是否存在
|
||||
* 2. 检查更新字段的唯一性约束(用户名、邮箱、手机号、GitHub ID)
|
||||
* 3. 合并更新数据到现有用户实体
|
||||
* 4. 保存更新后的用户信息
|
||||
* 5. 记录操作日志
|
||||
*
|
||||
* @param id 用户ID,必须是有效的已存在用户
|
||||
* @param updateData 更新的数据,支持部分字段更新
|
||||
* @returns 更新后的用户实体
|
||||
* @throws NotFoundException 当用户不存在时
|
||||
* @throws ConflictException 当更新的数据与其他用户冲突时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const updatedUser = await usersService.update(BigInt(1), {
|
||||
* nickname: '新昵称',
|
||||
* email: 'new@example.com'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
async update(id: bigint, updateData: Partial<CreateUserDto>): Promise<Users> {
|
||||
const monitor = PerformanceMonitor.create();
|
||||
|
||||
this.logger.log('开始更新用户信息', {
|
||||
operation: 'update',
|
||||
userId: id.toString(),
|
||||
updateFields: Object.keys(updateData),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
// 1. 检查用户是否存在 - 确保要更新的用户确实存在
|
||||
const existingUser = await this.findOne(id);
|
||||
|
||||
// 2. 检查更新数据的唯一性约束 - 防止违反数据库唯一约束
|
||||
await this.checkUpdateUniqueness(id, updateData);
|
||||
|
||||
// 3. 合并更新数据 - 使用Object.assign将新数据合并到现有实体
|
||||
Object.assign(existingUser, updateData);
|
||||
|
||||
// 4. 保存更新后的用户信息 - TypeORM会自动更新updated_at字段
|
||||
const updatedUser = await this.usersRepository.save(existingUser);
|
||||
|
||||
this.logger.log('用户信息更新成功', {
|
||||
operation: 'update',
|
||||
userId: id.toString(),
|
||||
updateFields: Object.keys(updateData),
|
||||
duration: monitor.getDuration(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return updatedUser;
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundException || error instanceof ConflictException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.error('用户更新系统异常', {
|
||||
operation: 'update',
|
||||
userId: id.toString(),
|
||||
updateData,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration: monitor.getDuration(),
|
||||
timestamp: new Date().toISOString()
|
||||
}, error instanceof Error ? error.stack : undefined);
|
||||
|
||||
throw new BadRequestException(ERROR_MESSAGES.USER_UPDATE_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*
|
||||
* 功能描述:
|
||||
* 物理删除指定的用户记录,数据将从数据库中永久移除
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证用户是否存在
|
||||
* 2. 执行物理删除操作
|
||||
* 3. 返回删除结果统计
|
||||
* 4. 记录删除操作日志
|
||||
*
|
||||
* 注意事项:
|
||||
* - 这是物理删除,数据无法恢复
|
||||
* - 如需保留数据,请使用 softRemove 方法
|
||||
* - 删除前请确认用户没有关联的重要数据
|
||||
*
|
||||
* @param id 用户ID,必须是有效的已存在用户
|
||||
* @returns 删除操作结果,包含影响行数和操作消息
|
||||
* @throws NotFoundException 当用户不存在时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await usersService.remove(BigInt(1));
|
||||
* console.log(`删除了 ${result.affected} 个用户`);
|
||||
* ```
|
||||
*/
|
||||
async remove(id: bigint): Promise<{ affected: number; message: string }> {
|
||||
const monitor = PerformanceMonitor.create();
|
||||
|
||||
this.logger.log('开始删除用户', {
|
||||
operation: 'remove',
|
||||
userId: id.toString(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
// 1. 检查用户是否存在 - 确保要删除的用户确实存在
|
||||
await this.findOne(id);
|
||||
|
||||
// 2. 执行删除操作 - 使用where条件来处理bigint类型
|
||||
const result = await this.usersRepository.delete({ id });
|
||||
|
||||
const deleteResult = {
|
||||
affected: result.affected || 0,
|
||||
message: `成功删除ID为 ${id} 的用户`
|
||||
};
|
||||
|
||||
this.logger.log('用户删除成功', {
|
||||
operation: 'remove',
|
||||
userId: id.toString(),
|
||||
affected: deleteResult.affected,
|
||||
duration: monitor.getDuration(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return deleteResult;
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.error('用户删除系统异常', {
|
||||
operation: 'remove',
|
||||
userId: id.toString(),
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration: monitor.getDuration(),
|
||||
timestamp: new Date().toISOString()
|
||||
}, error instanceof Error ? error.stack : undefined);
|
||||
|
||||
throw new BadRequestException(ERROR_MESSAGES.USER_DELETE_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新数据的唯一性约束
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @param updateData 更新数据
|
||||
* @throws ConflictException 当发现冲突时
|
||||
*/
|
||||
private async checkUpdateUniqueness(id: bigint, updateData: Partial<CreateUserDto>): Promise<void> {
|
||||
const existingUser = await this.findOne(id);
|
||||
|
||||
if (updateData.username && updateData.username !== existingUser.username) {
|
||||
await this.checkUsernameUniqueness(updateData.username);
|
||||
}
|
||||
|
||||
if (updateData.email && updateData.email !== existingUser.email) {
|
||||
await this.checkEmailUniqueness(updateData.email);
|
||||
}
|
||||
|
||||
if (updateData.phone && updateData.phone !== existingUser.phone) {
|
||||
await this.checkPhoneUniqueness(updateData.phone);
|
||||
}
|
||||
|
||||
if (updateData.github_id && updateData.github_id !== existingUser.github_id) {
|
||||
await this.checkGithubIdUniqueness(updateData.github_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除用户
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @returns 软删除操作结果
|
||||
*/
|
||||
async softRemove(id: bigint): Promise<Users> {
|
||||
const user = await this.findOne(id);
|
||||
// 注意:软删除功能暂未实现,当前仅返回用户实体
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计用户数量
|
||||
*
|
||||
* @param conditions 查询条件
|
||||
* @returns 用户数量
|
||||
*/
|
||||
async count(conditions?: FindOptionsWhere<Users>): Promise<number> {
|
||||
return await this.usersRepository.count({ where: conditions });
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否存在
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @returns 是否存在
|
||||
*/
|
||||
async exists(id: bigint): Promise<boolean> {
|
||||
const count = await this.usersRepository.count({ where: { id } });
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建用户
|
||||
*
|
||||
* @param createUserDtos 用户数据数组
|
||||
* @returns 创建的用户列表
|
||||
*/
|
||||
async createBatch(createUserDtos: CreateUserDto[]): Promise<Users[]> {
|
||||
const users: Users[] = [];
|
||||
|
||||
for (const dto of createUserDtos) {
|
||||
const user = await this.create(dto);
|
||||
users.push(user);
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据角色查询用户
|
||||
*
|
||||
* @param role 角色值
|
||||
* @param includeDeleted 是否包含已删除用户,默认false
|
||||
* @returns 用户列表
|
||||
*/
|
||||
async findByRole(role: number, includeDeleted: boolean = false): Promise<Users[]> {
|
||||
// 注意:软删除功能暂未实现,includeDeleted参数预留用于未来扩展
|
||||
const whereCondition = { role };
|
||||
|
||||
return await this.usersRepository.find({
|
||||
where: whereCondition,
|
||||
order: { created_at: DATABASE_CONSTANTS.ORDER_DESC }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索用户(根据用户名或昵称)
|
||||
*
|
||||
* 功能描述:
|
||||
* 根据关键词在用户名和昵称字段中进行模糊搜索,支持部分匹配
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 使用QueryBuilder构建复杂查询
|
||||
* 2. 对用户名和昵称字段进行LIKE模糊匹配
|
||||
* 3. 按创建时间倒序排列结果
|
||||
* 4. 限制返回数量防止性能问题
|
||||
*
|
||||
* 性能考虑:
|
||||
* - 使用数据库索引优化查询性能
|
||||
* - 限制返回数量避免大数据量问题
|
||||
* - 建议在用户名和昵称字段上建立索引
|
||||
*
|
||||
* @param keyword 搜索关键词,支持中文、英文、数字等字符
|
||||
* @param limit 限制数量,默认20条,建议不超过100
|
||||
* @returns 匹配的用户列表,按创建时间倒序排列
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 搜索包含"张三"的用户
|
||||
* const users = await usersService.search('张三', 10);
|
||||
*
|
||||
* // 搜索包含"admin"的用户
|
||||
* const adminUsers = await usersService.search('admin');
|
||||
* ```
|
||||
*/
|
||||
async search(keyword: string, limit: number = QUERY_LIMITS.DEFAULT_SEARCH_LIMIT, includeDeleted: boolean = false): Promise<Users[]> {
|
||||
const monitor = PerformanceMonitor.create();
|
||||
|
||||
this.logStart('搜索用户', { keyword, limit, includeDeleted });
|
||||
|
||||
try {
|
||||
// 1. 构建查询 - 使用QueryBuilder支持复杂的WHERE条件
|
||||
const queryBuilder = this.usersRepository.createQueryBuilder('user');
|
||||
|
||||
// 添加搜索条件 - 在用户名和昵称中进行模糊匹配
|
||||
let whereClause = 'user.username LIKE :keyword OR user.nickname LIKE :keyword';
|
||||
|
||||
// 注意:软删除功能暂未实现,includeDeleted参数预留用于未来扩展
|
||||
|
||||
const result = await queryBuilder
|
||||
.where(whereClause, {
|
||||
keyword: `%${keyword}%` // 前后加%实现模糊匹配
|
||||
})
|
||||
.orderBy('user.created_at', DATABASE_CONSTANTS.ORDER_DESC) // 按创建时间倒序
|
||||
.limit(limit) // 限制返回数量
|
||||
.getMany();
|
||||
|
||||
this.logSuccess('搜索用户', {
|
||||
keyword,
|
||||
limit,
|
||||
includeDeleted,
|
||||
resultCount: result.length
|
||||
}, monitor.getDuration());
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
// 搜索异常使用特殊处理,返回空数组而不抛出异常
|
||||
return this.handleSearchError(error, '搜索用户', {
|
||||
keyword,
|
||||
limit,
|
||||
includeDeleted,
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
766
src/core/db/users/users_memory.service.ts
Normal file
766
src/core/db/users/users_memory.service.ts
Normal file
@@ -0,0 +1,766 @@
|
||||
/**
|
||||
* 用户内存存储服务类
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供基于内存的用户数据存储技术实现
|
||||
* - 作为数据库连接失败时的回退方案
|
||||
* - 实现与UsersService相同的接口
|
||||
* - 支持完整的CRUD操作和数据管理
|
||||
*
|
||||
* 职责分离:
|
||||
* - 数据存储:使用Map进行内存数据管理
|
||||
* - ID生成:线程安全的自增ID生成机制
|
||||
* - 数据验证:数据完整性和唯一性约束检查
|
||||
* - 异常处理:统一的错误处理和日志记录
|
||||
*
|
||||
* 使用场景:
|
||||
* - 开发环境无数据库时的快速启动
|
||||
* - 测试环境的轻量级存储
|
||||
* - 数据库故障时的临时降级
|
||||
*
|
||||
* 注意事项:
|
||||
* - 数据仅存储在内存中,重启后丢失
|
||||
* - 不适用于生产环境
|
||||
* - 性能优异但无持久化保证
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 架构分层优化 - 修正导入路径,确保Core层不依赖Business层 (修改者: moyin)
|
||||
* - 2026-01-08: 代码质量优化 - 重构create方法,提取私有方法减少代码重复 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 完善注释规范,添加完整的文件头和方法注释
|
||||
* - 2026-01-07: 功能新增 - 添加createWithDuplicateCheck方法,保持与数据库服务一致
|
||||
* - 2026-01-07: 功能优化 - 添加日志记录系统,统一异常处理和性能监控
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.3
|
||||
* @since 2025-12-17
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { Users } from './users.entity';
|
||||
import { CreateUserDto } from './users.dto';
|
||||
import { UserStatus } from './user_status.enum';
|
||||
import { validate } from 'class-validator';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { BaseUsersService } from './base_users.service';
|
||||
import { USER_ROLES, QUERY_LIMITS, SYSTEM_CONFIG, ERROR_MESSAGES, DATABASE_CONSTANTS, ValidationUtils, PerformanceMonitor } from './users.constants';
|
||||
|
||||
@Injectable()
|
||||
export class UsersMemoryService extends BaseUsersService {
|
||||
private users: Map<bigint, Users> = new Map();
|
||||
private CURRENT_ID: bigint = BigInt(USER_ROLES.NORMAL_USER);
|
||||
private readonly ID_LOCK = new Set<string>(); // 简单的ID生成锁
|
||||
|
||||
constructor() {
|
||||
super(); // 调用基类构造函数
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查找用户
|
||||
*
|
||||
* @param predicate 查找条件
|
||||
* @returns 匹配的用户或null
|
||||
*/
|
||||
private findUserByCondition(predicate: (user: Users) => boolean): Users | null {
|
||||
const user = Array.from(this.users.values()).find(predicate);
|
||||
return user || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @returns 用户实体或undefined
|
||||
*/
|
||||
private getUser(id: bigint): Users | undefined {
|
||||
return this.users.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户
|
||||
*
|
||||
* @param user 用户实体
|
||||
*/
|
||||
private saveUser(user: Users): void {
|
||||
this.users.set(user.id, user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 线程安全的ID生成方法
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 检查ID生成锁的状态,避免并发冲突
|
||||
* 2. 使用超时机制防止死锁情况
|
||||
* 3. 获取锁后安全地递增ID计数器
|
||||
* 4. 确保锁在任何情况下都会被正确释放
|
||||
* 5. 返回新生成的唯一ID
|
||||
*
|
||||
* @returns 新的唯一ID,保证全局唯一性
|
||||
* @throws Error 当ID生成超时或发生死锁时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const newId = await this.generateId();
|
||||
* console.log(`生成新ID: ${newId}`);
|
||||
* ```
|
||||
*/
|
||||
private async generateId(): Promise<bigint> {
|
||||
const lockKey = DATABASE_CONSTANTS.ID_GENERATION_LOCK_KEY;
|
||||
const maxWaitTime = SYSTEM_CONFIG.ID_GENERATION_TIMEOUT;
|
||||
const startTime = Date.now();
|
||||
|
||||
// 改进的锁机制,添加超时保护
|
||||
while (this.ID_LOCK.has(lockKey)) {
|
||||
if (Date.now() - startTime > maxWaitTime) {
|
||||
throw new Error(ERROR_MESSAGES.ID_GENERATION_TIMEOUT);
|
||||
}
|
||||
// 使用 Promise 避免忙等待
|
||||
await new Promise(resolve => setTimeout(resolve, SYSTEM_CONFIG.LOCK_WAIT_INTERVAL));
|
||||
}
|
||||
|
||||
this.ID_LOCK.add(lockKey);
|
||||
|
||||
try {
|
||||
const newId = this.CURRENT_ID++;
|
||||
return newId;
|
||||
} finally {
|
||||
// 确保锁一定会被释放
|
||||
this.ID_LOCK.delete(lockKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新用户
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 验证输入数据的格式和完整性
|
||||
* 2. 检查用户名、邮箱、手机号、GitHub ID的唯一性
|
||||
* 3. 创建用户实体并分配唯一ID
|
||||
* 4. 设置默认值和时间戳
|
||||
* 5. 保存到内存存储并记录操作日志
|
||||
*
|
||||
* @param createUserDto 创建用户的数据传输对象,包含用户基本信息
|
||||
* @returns 创建成功的用户实体,不包含敏感信息
|
||||
* @throws ConflictException 当用户名、邮箱、手机号或GitHub ID已存在时
|
||||
* @throws BadRequestException 当数据验证失败时
|
||||
*
|
||||
* @example
|
||||
* const newUser = await userService.create({
|
||||
* username: 'testuser',
|
||||
* email: 'test@example.com',
|
||||
* nickname: '测试用户'
|
||||
* });
|
||||
*/
|
||||
async create(createUserDto: CreateUserDto): Promise<Users> {
|
||||
const monitor = PerformanceMonitor.create();
|
||||
this.logStart('创建用户', { username: createUserDto.username });
|
||||
|
||||
try {
|
||||
// 验证DTO
|
||||
await this.validateUserDto(createUserDto);
|
||||
|
||||
// 检查唯一性约束
|
||||
await this.checkUniquenessConstraints(createUserDto);
|
||||
|
||||
// 创建用户实体
|
||||
const user = await this.createUserEntity(createUserDto);
|
||||
|
||||
// 保存到内存
|
||||
this.saveUser(user);
|
||||
|
||||
this.logSuccess('创建用户', {
|
||||
userId: user.id.toString(),
|
||||
username: user.username
|
||||
}, monitor.getDuration());
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
this.handleServiceError(error, '创建用户', {
|
||||
username: createUserDto.username,
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证用户DTO数据
|
||||
*
|
||||
* @param createUserDto 用户数据
|
||||
* @throws BadRequestException 当数据验证失败时
|
||||
*/
|
||||
private async validateUserDto(createUserDto: CreateUserDto): Promise<void> {
|
||||
const dto = plainToClass(CreateUserDto, createUserDto);
|
||||
const validationErrors = await validate(dto);
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
const errorMessages = ValidationUtils.formatValidationErrors(validationErrors);
|
||||
throw new BadRequestException(`数据验证失败: ${errorMessages}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查唯一性约束
|
||||
*
|
||||
* @param createUserDto 用户数据
|
||||
* @throws ConflictException 当发现重复数据时
|
||||
*/
|
||||
private async checkUniquenessConstraints(createUserDto: CreateUserDto): Promise<void> {
|
||||
// 检查用户名是否已存在
|
||||
if (createUserDto.username) {
|
||||
const existingUser = await this.findByUsername(createUserDto.username);
|
||||
if (existingUser) {
|
||||
throw new ConflictException(ERROR_MESSAGES.USERNAME_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
if (createUserDto.email) {
|
||||
const existingEmail = await this.findByEmail(createUserDto.email);
|
||||
if (existingEmail) {
|
||||
throw new ConflictException(ERROR_MESSAGES.EMAIL_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
if (createUserDto.phone) {
|
||||
const existingPhone = this.findUserByCondition(
|
||||
u => u.phone === createUserDto.phone
|
||||
);
|
||||
if (existingPhone) {
|
||||
throw new ConflictException(ERROR_MESSAGES.PHONE_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查GitHub ID是否已存在
|
||||
if (createUserDto.github_id) {
|
||||
const existingGithub = await this.findByGithubId(createUserDto.github_id);
|
||||
if (existingGithub) {
|
||||
throw new ConflictException(ERROR_MESSAGES.GITHUB_ID_EXISTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户实体
|
||||
*
|
||||
* @param createUserDto 用户数据
|
||||
* @returns 创建的用户实体
|
||||
*/
|
||||
private async createUserEntity(createUserDto: CreateUserDto): Promise<Users> {
|
||||
const user = new Users();
|
||||
user.id = await this.generateId();
|
||||
user.username = createUserDto.username;
|
||||
user.email = createUserDto.email || null;
|
||||
user.phone = createUserDto.phone || null;
|
||||
user.password_hash = createUserDto.password_hash || null;
|
||||
user.nickname = createUserDto.nickname;
|
||||
user.github_id = createUserDto.github_id || null;
|
||||
user.avatar_url = createUserDto.avatar_url || null;
|
||||
user.role = createUserDto.role || USER_ROLES.NORMAL_USER;
|
||||
user.email_verified = createUserDto.email_verified || false;
|
||||
user.status = createUserDto.status || UserStatus.ACTIVE;
|
||||
user.created_at = new Date();
|
||||
user.updated_at = new Date();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有用户
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 获取内存中的所有用户数据
|
||||
* 2. 按创建时间倒序排列(最新的在前)
|
||||
* 3. 应用分页参数进行数据切片
|
||||
* 4. 记录查询操作和性能指标
|
||||
*
|
||||
* @param limit 限制返回数量,默认100,用于分页控制
|
||||
* @param offset 偏移量,默认0,用于分页控制
|
||||
* @returns 用户列表,按创建时间倒序排列
|
||||
*
|
||||
* @example
|
||||
* // 获取前10个用户
|
||||
* const users = await userService.findAll(10, 0);
|
||||
*
|
||||
* // 获取第二页用户(每页20个)
|
||||
* const secondPageUsers = await userService.findAll(20, 20);
|
||||
*/
|
||||
async findAll(limit: number = QUERY_LIMITS.DEFAULT_LIMIT, offset: number = 0, includeDeleted: boolean = false): Promise<Users[]> {
|
||||
const monitor = PerformanceMonitor.create();
|
||||
this.logStart('查询所有用户', { limit, offset, includeDeleted });
|
||||
|
||||
try {
|
||||
let allUsers = Array.from(this.users.values());
|
||||
|
||||
// 注意:软删除功能暂未实现,includeDeleted参数预留用于未来扩展
|
||||
|
||||
// 按创建时间倒序排列
|
||||
allUsers.sort((a, b) => b.created_at.getTime() - a.created_at.getTime());
|
||||
|
||||
const result = allUsers.slice(offset, offset + limit);
|
||||
|
||||
this.logSuccess('查询所有用户', {
|
||||
resultCount: result.length,
|
||||
totalCount: allUsers.length,
|
||||
includeDeleted
|
||||
}, monitor.getDuration());
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.handleServiceError(error, '查询所有用户', {
|
||||
limit,
|
||||
offset,
|
||||
includeDeleted,
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询用户
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 从内存Map中根据ID快速查找用户
|
||||
* 2. 验证用户是否存在
|
||||
* 3. 记录查询操作和结果
|
||||
* 4. 如果用户不存在则抛出404异常
|
||||
*
|
||||
* @param id 用户ID,必须是有效的bigint类型
|
||||
* @returns 用户实体,包含完整的用户信息
|
||||
* @throws NotFoundException 当指定ID的用户不存在时
|
||||
*
|
||||
* @example
|
||||
* try {
|
||||
* const user = await userService.findOne(BigInt(123));
|
||||
* console.log(user.username);
|
||||
* } catch (error) {
|
||||
* // 处理用户不存在的情况
|
||||
* }
|
||||
*/
|
||||
async findOne(id: bigint, includeDeleted: boolean = false): Promise<Users> {
|
||||
const monitor = PerformanceMonitor.create();
|
||||
this.logStart('查询用户', { userId: id.toString(), includeDeleted });
|
||||
|
||||
try {
|
||||
const user = this.getUser(id);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException(`ID为 ${id} 的用户不存在`);
|
||||
}
|
||||
|
||||
this.logSuccess('查询用户', {
|
||||
userId: id.toString(),
|
||||
username: user.username,
|
||||
includeDeleted
|
||||
}, monitor.getDuration());
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
this.handleServiceError(error, '查询用户', {
|
||||
userId: id.toString(),
|
||||
includeDeleted,
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param includeDeleted 是否包含已删除用户,默认false
|
||||
* @returns 用户实体或null
|
||||
*/
|
||||
async findByUsername(username: string, includeDeleted: boolean = false): Promise<Users | null> {
|
||||
return this.findUserByCondition(u => u.username === username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据邮箱查询用户
|
||||
*
|
||||
* @param email 邮箱
|
||||
* @param includeDeleted 是否包含已删除用户,默认false
|
||||
* @returns 用户实体或null
|
||||
*/
|
||||
async findByEmail(email: string, includeDeleted: boolean = false): Promise<Users | null> {
|
||||
return this.findUserByCondition(u => u.email === email);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据GitHub ID查询用户
|
||||
*
|
||||
* @param githubId GitHub ID
|
||||
* @param includeDeleted 是否包含已删除用户,默认false
|
||||
* @returns 用户实体或null
|
||||
*/
|
||||
async findByGithubId(githubId: string, includeDeleted: boolean = false): Promise<Users | null> {
|
||||
return this.findUserByCondition(u => u.github_id === githubId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新数据的唯一性约束
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @param updateData 更新数据
|
||||
* @param existingUser 现有用户
|
||||
* @throws ConflictException 当发现冲突时
|
||||
*/
|
||||
private async checkUpdateUniquenessConstraints(
|
||||
id: bigint,
|
||||
updateData: Partial<CreateUserDto>,
|
||||
existingUser: Users
|
||||
): Promise<void> {
|
||||
if (updateData.username && updateData.username !== existingUser.username) {
|
||||
const usernameExists = await this.findByUsername(updateData.username);
|
||||
if (usernameExists) {
|
||||
throw new ConflictException(ERROR_MESSAGES.USERNAME_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateData.email && updateData.email !== existingUser.email) {
|
||||
const emailExists = await this.findByEmail(updateData.email);
|
||||
if (emailExists) {
|
||||
throw new ConflictException(ERROR_MESSAGES.EMAIL_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateData.phone && updateData.phone !== existingUser.phone) {
|
||||
const phoneExists = this.findUserByCondition(
|
||||
u => u.phone === updateData.phone && u.id !== id
|
||||
);
|
||||
if (phoneExists) {
|
||||
throw new ConflictException(ERROR_MESSAGES.PHONE_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateData.github_id && updateData.github_id !== existingUser.github_id) {
|
||||
const githubExists = await this.findByGithubId(updateData.github_id);
|
||||
if (githubExists && githubExists.id !== id) {
|
||||
throw new ConflictException(ERROR_MESSAGES.GITHUB_ID_EXISTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证目标用户是否存在
|
||||
* 2. 检查更新数据的唯一性约束(用户名、邮箱、手机号、GitHub ID)
|
||||
* 3. 应用更新数据到现有用户实体
|
||||
* 4. 更新时间戳并保存到内存
|
||||
* 5. 记录更新操作和性能指标
|
||||
*
|
||||
* @param id 用户ID,必须是有效的bigint类型
|
||||
* @param updateData 更新的数据,可以是部分用户信息
|
||||
* @returns 更新后的用户实体,包含最新的信息和时间戳
|
||||
* @throws NotFoundException 当指定ID的用户不存在时
|
||||
* @throws ConflictException 当更新的数据与其他用户产生唯一性冲突时
|
||||
*
|
||||
* @example
|
||||
* const updatedUser = await userService.update(BigInt(123), {
|
||||
* nickname: '新昵称',
|
||||
* email: 'newemail@example.com'
|
||||
* });
|
||||
*/
|
||||
async update(id: bigint, updateData: Partial<CreateUserDto>): Promise<Users> {
|
||||
const monitor = PerformanceMonitor.create();
|
||||
this.logStart('更新用户', {
|
||||
userId: id.toString(),
|
||||
updateFields: Object.keys(updateData)
|
||||
});
|
||||
|
||||
try {
|
||||
// 检查用户是否存在
|
||||
const existingUser = await this.findOne(id);
|
||||
|
||||
// 检查更新数据的唯一性约束
|
||||
await this.checkUpdateUniquenessConstraints(id, updateData, existingUser);
|
||||
|
||||
// 更新用户数据
|
||||
Object.assign(existingUser, updateData);
|
||||
existingUser.updated_at = new Date();
|
||||
|
||||
this.saveUser(existingUser);
|
||||
|
||||
this.logSuccess('更新用户', {
|
||||
userId: id.toString(),
|
||||
username: existingUser.username
|
||||
}, monitor.getDuration());
|
||||
|
||||
return existingUser;
|
||||
} catch (error) {
|
||||
this.handleServiceError(error, '更新用户', {
|
||||
userId: id.toString(),
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证目标用户是否存在
|
||||
* 2. 从内存Map中删除用户记录
|
||||
* 3. 记录删除操作和结果
|
||||
* 4. 返回删除操作的统计信息
|
||||
*
|
||||
* @param id 用户ID,必须是有效的bigint类型
|
||||
* @returns 删除操作结果,包含影响的记录数和操作消息
|
||||
* @throws NotFoundException 当指定ID的用户不存在时
|
||||
*
|
||||
* @example
|
||||
* const result = await userService.remove(BigInt(123));
|
||||
* console.log(result.message); // "成功删除ID为 123 的用户"
|
||||
*/
|
||||
async remove(id: bigint): Promise<{ affected: number; message: string }> {
|
||||
const monitor = PerformanceMonitor.create();
|
||||
this.logStart('删除用户', { userId: id.toString() });
|
||||
|
||||
try {
|
||||
// 检查用户是否存在
|
||||
const user = await this.findOne(id);
|
||||
|
||||
// 执行删除
|
||||
const deleted = this.users.delete(id);
|
||||
|
||||
const result = {
|
||||
affected: deleted ? 1 : 0,
|
||||
message: `成功删除ID为 ${id} 的用户`
|
||||
};
|
||||
|
||||
this.logSuccess('删除用户', {
|
||||
userId: id.toString(),
|
||||
username: user.username
|
||||
}, monitor.getDuration());
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.handleServiceError(error, '删除用户', {
|
||||
userId: id.toString(),
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除用户(内存模式下设置删除时间)
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @returns 被软删除的用户实体
|
||||
*/
|
||||
async softRemove(id: bigint): Promise<Users> {
|
||||
const user = await this.findOne(id);
|
||||
// 注意:软删除功能暂未实现,当前仅返回用户实体
|
||||
this.saveUser(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计用户数量
|
||||
*
|
||||
* @param conditions 查询条件(内存模式下简化处理)
|
||||
* @returns 用户数量
|
||||
*/
|
||||
async count(conditions?: Record<string, any>): Promise<number> {
|
||||
if (!conditions) {
|
||||
return this.users.size;
|
||||
}
|
||||
|
||||
// 简化的条件过滤
|
||||
let count = 0;
|
||||
for (const user of this.users.values()) {
|
||||
let match = true;
|
||||
for (const [key, value] of Object.entries(conditions)) {
|
||||
if ((user as any)[key] !== value) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否存在
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @returns 是否存在
|
||||
*/
|
||||
async exists(id: bigint): Promise<boolean> {
|
||||
return this.users.has(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新用户(带重复检查)
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 检查用户名、邮箱、手机号、GitHub ID的唯一性
|
||||
* 2. 如果所有检查都通过,调用create方法创建用户
|
||||
* 3. 记录操作日志和性能指标
|
||||
*
|
||||
* @param createUserDto 创建用户的数据传输对象
|
||||
* @returns 创建的用户实体
|
||||
* @throws ConflictException 当用户名、邮箱、手机号或GitHub ID已存在时
|
||||
* @throws BadRequestException 当数据验证失败时
|
||||
*/
|
||||
async createWithDuplicateCheck(createUserDto: CreateUserDto): Promise<Users> {
|
||||
const monitor = PerformanceMonitor.create();
|
||||
|
||||
this.logStart('创建用户(带重复检查)', {
|
||||
username: createUserDto.username,
|
||||
email: createUserDto.email,
|
||||
phone: createUserDto.phone,
|
||||
github_id: createUserDto.github_id
|
||||
});
|
||||
|
||||
try {
|
||||
// 执行所有唯一性检查
|
||||
await this.checkUniquenessConstraints(createUserDto);
|
||||
|
||||
// 调用普通的创建方法
|
||||
const user = await this.create(createUserDto);
|
||||
|
||||
this.logSuccess('创建用户(带重复检查)', {
|
||||
userId: user.id.toString(),
|
||||
username: user.username
|
||||
}, monitor.getDuration());
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
this.handleServiceError(error, '创建用户(带重复检查)', {
|
||||
username: createUserDto.username,
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建用户
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 遍历用户数据数组
|
||||
* 2. 对每个用户数据调用create方法
|
||||
* 3. 收集所有创建成功的用户
|
||||
* 4. 记录批量操作的统计信息和性能指标
|
||||
* 5. 如果某个用户创建失败,整个操作会中断并抛出异常
|
||||
*
|
||||
* @param createUserDtos 用户数据数组,每个元素都是CreateUserDto类型
|
||||
* @returns 创建成功的用户列表,顺序与输入数组一致
|
||||
* @throws ConflictException 当任何用户的唯一性约束冲突时
|
||||
* @throws BadRequestException 当任何用户的数据验证失败时
|
||||
*
|
||||
* @example
|
||||
* const users = await userService.createBatch([
|
||||
* { username: 'user1', email: 'user1@example.com', nickname: '用户1' },
|
||||
* { username: 'user2', email: 'user2@example.com', nickname: '用户2' }
|
||||
* ]);
|
||||
*/
|
||||
async createBatch(createUserDtos: CreateUserDto[]): Promise<Users[]> {
|
||||
const monitor = PerformanceMonitor.create();
|
||||
this.logStart('批量创建用户', { count: createUserDtos.length });
|
||||
|
||||
try {
|
||||
const users: Users[] = [];
|
||||
const createdUsers: Users[] = []; // 用于回滚的记录
|
||||
|
||||
try {
|
||||
for (const dto of createUserDtos) {
|
||||
const user = await this.create(dto);
|
||||
users.push(user);
|
||||
createdUsers.push(user);
|
||||
}
|
||||
|
||||
this.logSuccess('批量创建用户', {
|
||||
createdCount: users.length
|
||||
}, monitor.getDuration());
|
||||
|
||||
return users;
|
||||
} catch (error) {
|
||||
// 回滚已创建的用户
|
||||
for (const user of createdUsers) {
|
||||
this.users.delete(user.id);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleServiceError(error, '批量创建用户', {
|
||||
count: createUserDtos.length,
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据角色查询用户
|
||||
*
|
||||
* @param role 角色值
|
||||
* @param includeDeleted 是否包含已删除用户,默认false
|
||||
* @returns 用户列表
|
||||
*/
|
||||
async findByRole(role: number, includeDeleted: boolean = false): Promise<Users[]> {
|
||||
return Array.from(this.users.values())
|
||||
.filter(u => u.role === role)
|
||||
.sort((a, b) => b.created_at.getTime() - a.created_at.getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索用户(根据用户名或昵称)
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 将搜索关键词转换为小写以实现大小写不敏感搜索
|
||||
* 2. 遍历所有用户,匹配用户名或昵称中包含关键词的用户
|
||||
* 3. 按创建时间倒序排列搜索结果
|
||||
* 4. 限制返回结果数量以提高性能
|
||||
* 5. 记录搜索操作和性能指标
|
||||
*
|
||||
* @param keyword 搜索关键词,支持部分匹配,大小写不敏感
|
||||
* @param limit 限制返回数量,默认20,防止结果过多影响性能
|
||||
* @returns 匹配的用户列表,按创建时间倒序排列
|
||||
*
|
||||
* @example
|
||||
* // 搜索用户名或昵称包含"admin"的用户
|
||||
* const users = await userService.search('admin', 10);
|
||||
*
|
||||
* // 搜索所有包含"测试"的用户
|
||||
* const testUsers = await userService.search('测试');
|
||||
*/
|
||||
async search(keyword: string, limit: number = QUERY_LIMITS.DEFAULT_SEARCH_LIMIT, includeDeleted: boolean = false): Promise<Users[]> {
|
||||
const monitor = PerformanceMonitor.create();
|
||||
this.logStart('搜索用户', { keyword, limit, includeDeleted });
|
||||
|
||||
try {
|
||||
const lowerKeyword = keyword.toLowerCase();
|
||||
|
||||
const results = Array.from(this.users.values())
|
||||
.filter(u => {
|
||||
// 注意:软删除功能暂未实现,includeDeleted参数预留用于未来扩展
|
||||
|
||||
// 检查关键词匹配
|
||||
return u.username.toLowerCase().includes(lowerKeyword) ||
|
||||
u.nickname.toLowerCase().includes(lowerKeyword);
|
||||
})
|
||||
.sort((a, b) => b.created_at.getTime() - a.created_at.getTime())
|
||||
.slice(0, limit);
|
||||
|
||||
this.logSuccess('搜索用户', {
|
||||
keyword,
|
||||
resultCount: results.length,
|
||||
includeDeleted
|
||||
}, monitor.getDuration());
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
// 搜索异常使用特殊处理,返回空数组而不抛出异常
|
||||
return this.handleSearchError(error, '搜索用户', {
|
||||
keyword,
|
||||
limit,
|
||||
includeDeleted,
|
||||
duration: monitor.getDuration()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
391
src/core/db/zulip_accounts/base_zulip_accounts.service.ts
Normal file
391
src/core/db/zulip_accounts/base_zulip_accounts.service.ts
Normal file
@@ -0,0 +1,391 @@
|
||||
/**
|
||||
* Zulip账号关联数据访问服务基类
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供统一的数据访问操作基础功能
|
||||
* - 集成高性能日志系统,支持结构化日志记录
|
||||
* - 定义通用的数据转换方法和性能监控
|
||||
* - 为所有Zulip账号数据访问服务提供基础功能支持
|
||||
*
|
||||
* 职责分离:
|
||||
* - 数据访问:统一处理数据访问相关的基础操作
|
||||
* - 日志管理:集成AppLoggerService提供高性能日志记录
|
||||
* - 性能监控:提供操作耗时统计和性能指标收集
|
||||
* - 数据转换:统一数据格式化和转换逻辑
|
||||
* - 基础服务:为子类提供通用的数据访问方法
|
||||
*
|
||||
* 注意:业务异常处理已转移到 src/core/zulip_core/services/zulip_accounts_business.service.ts
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 架构优化 - 移除业务异常处理,专注数据访问功能 (修改者: moyin)
|
||||
* - 2026-01-12: 代码质量优化 - 添加列表响应构建工具方法,彻底消除所有重复代码 (修改者: moyin)
|
||||
* - 2026-01-12: 代码质量优化 - 添加数组映射工具方法,进一步减少重复代码 (修改者: moyin)
|
||||
* - 2026-01-12: 代码质量优化 - 添加BigInt转换和DTO转换的抽象方法,减少重复代码 (修改者: moyin)
|
||||
* - 2026-01-12: 性能优化 - 集成AppLoggerService,添加性能监控和结构化日志
|
||||
* - 2026-01-07: 代码规范优化 - 修复文件命名规范,将短横线改为下划线分隔
|
||||
* - 2026-01-07: 代码规范优化 - 完善文件头注释和方法三级注释
|
||||
* - 2026-01-07: 功能完善 - 增加搜索异常的特殊处理逻辑
|
||||
* - 2026-01-07: 架构优化 - 统一异常处理机制和日志记录格式
|
||||
* - 2025-01-07: 初始创建 - 创建基础服务类和异常处理框架
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 2.0.0
|
||||
* @since 2025-01-07
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { AppLoggerService, LogContext } from '../../utils/logger/logger.service';
|
||||
|
||||
export abstract class BaseZulipAccountsService {
|
||||
protected readonly logger: AppLoggerService;
|
||||
protected readonly moduleName: string;
|
||||
|
||||
constructor(
|
||||
@Inject(AppLoggerService) logger: AppLoggerService,
|
||||
moduleName: string = 'ZulipAccountsService'
|
||||
) {
|
||||
this.logger = logger;
|
||||
this.moduleName = moduleName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的错误格式化方法
|
||||
*
|
||||
* 数据访问逻辑:
|
||||
* 1. 检查错误对象类型,判断是否为Error实例
|
||||
* 2. 如果是Error实例,提取message属性作为错误信息
|
||||
* 3. 如果不是Error实例,将错误对象转换为字符串
|
||||
* 4. 返回格式化后的错误信息字符串
|
||||
*
|
||||
* @param error 原始错误对象,可能是Error实例或其他类型
|
||||
* @returns 格式化后的错误信息字符串,用于日志记录
|
||||
* @throws 无异常抛出,该方法保证返回字符串
|
||||
*/
|
||||
protected formatError(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的数据访问错误处理方法
|
||||
*
|
||||
* 数据访问逻辑:
|
||||
* 1. 格式化原始错误信息,提取可读的错误描述
|
||||
* 2. 使用AppLoggerService记录结构化错误日志
|
||||
* 3. 重新抛出原始错误,不进行业务异常转换
|
||||
* 4. 确保错误信息被正确记录用于调试
|
||||
*
|
||||
* @param error 原始错误对象,数据访问过程中发生的异常
|
||||
* @param operation 操作名称,用于日志记录和错误追踪
|
||||
* @param context 上下文信息,包含相关的数据访问参数
|
||||
* @returns 永不返回,该方法总是抛出异常
|
||||
* @throws 重新抛出原始错误
|
||||
*/
|
||||
protected handleDataAccessError(error: unknown, operation: string, context?: Record<string, any>): never {
|
||||
const errorMessage = this.formatError(error);
|
||||
|
||||
// 使用AppLoggerService记录结构化错误日志
|
||||
const logContext: LogContext = {
|
||||
module: this.moduleName,
|
||||
operation,
|
||||
error: errorMessage,
|
||||
context,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.logger.error(`${operation}失败`, logContext, error instanceof Error ? error.stack : undefined);
|
||||
|
||||
// 重新抛出原始错误,不进行业务异常转换
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索异常的特殊处理(返回空结果而不抛出异常)
|
||||
*
|
||||
* 数据访问逻辑:
|
||||
* 1. 格式化错误信息,提取可读的错误描述
|
||||
* 2. 使用AppLoggerService记录警告级别的结构化日志
|
||||
* 3. 返回空数组而不是抛出异常,保证搜索接口的可用性
|
||||
* 4. 记录完整的上下文信息,便于问题排查和监控
|
||||
* 5. 使用warn级别日志,区别于error级别的严重异常
|
||||
*
|
||||
* @param error 原始错误对象,搜索过程中发生的异常
|
||||
* @param operation 操作名称,用于日志记录和问题定位
|
||||
* @param context 上下文信息,包含搜索条件和相关参数
|
||||
* @returns 空数组,确保搜索接口始终返回有效的数组结果
|
||||
*/
|
||||
protected handleSearchError(error: unknown, operation: string, context?: Record<string, any>): any[] {
|
||||
const errorMessage = this.formatError(error);
|
||||
|
||||
// 使用AppLoggerService记录结构化警告日志
|
||||
const logContext: LogContext = {
|
||||
module: this.moduleName,
|
||||
operation,
|
||||
error: errorMessage,
|
||||
context,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.logger.warn(`${operation}失败,返回空结果`, logContext);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录操作成功日志
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 构建标准化的成功日志信息,包含操作名称和结果
|
||||
* 2. 使用AppLoggerService记录结构化日志信息
|
||||
* 3. 记录上下文信息,便于业务流程追踪和性能分析
|
||||
* 4. 可选记录操作耗时,用于性能监控和优化
|
||||
* 5. 添加时间戳,确保日志的时序性和可追溯性
|
||||
* 6. 使用info级别日志,标识正常的业务操作完成
|
||||
*
|
||||
* @param operation 操作名称,描述具体的业务操作类型
|
||||
* @param context 上下文信息,包含操作相关的业务数据
|
||||
* @param duration 操作耗时(毫秒),用于性能监控,可选参数
|
||||
* @returns 无返回值,仅记录日志
|
||||
*
|
||||
* @example
|
||||
* // 记录简单操作成功
|
||||
* this.logSuccess('创建用户', { userId: '12345', username: 'test' });
|
||||
*
|
||||
* @example
|
||||
* // 记录带耗时的操作成功
|
||||
* const startTime = Date.now();
|
||||
* // ... 执行业务逻辑
|
||||
* const duration = Date.now() - startTime;
|
||||
* this.logSuccess('复杂查询', { criteria, resultCount: 100 }, duration);
|
||||
*/
|
||||
protected logSuccess(operation: string, context?: Record<string, any>, duration?: number): void {
|
||||
const logContext: LogContext = {
|
||||
module: this.moduleName,
|
||||
operation,
|
||||
context,
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.logger.info(`${operation}成功`, logContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录操作开始日志
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 构建标准化的操作开始日志信息,标记业务流程起点
|
||||
* 2. 使用AppLoggerService记录结构化日志信息
|
||||
* 3. 记录上下文信息,包含操作的输入参数和相关数据
|
||||
* 4. 添加时间戳,便于与成功/失败日志进行时序关联
|
||||
* 5. 使用info级别日志,标识正常的业务操作开始
|
||||
* 6. 为后续的性能分析和问题排查提供起始点标记
|
||||
*
|
||||
* @param operation 操作名称,描述即将执行的业务操作类型
|
||||
* @param context 上下文信息,包含操作的输入参数和相关数据
|
||||
* @returns 无返回值,仅记录日志
|
||||
*
|
||||
* @example
|
||||
* // 记录数据库操作开始
|
||||
* this.logStart('创建用户', {
|
||||
* gameUserId: '12345',
|
||||
* email: 'user@example.com'
|
||||
* });
|
||||
*
|
||||
* @example
|
||||
* // 记录复杂业务流程开始
|
||||
* this.logStart('用户认证流程', {
|
||||
* userId: user.id,
|
||||
* authMethod: 'oauth',
|
||||
* clientIp: request.ip
|
||||
* });
|
||||
*/
|
||||
protected logStart(operation: string, context?: Record<string, any>): void {
|
||||
const logContext: LogContext = {
|
||||
module: this.moduleName,
|
||||
operation,
|
||||
context,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.logger.info(`开始${operation}`, logContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建性能监控器
|
||||
*
|
||||
* 功能描述:
|
||||
* 创建一个性能监控器对象,用于测量操作耗时和记录性能指标
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 记录操作开始时间戳
|
||||
* 2. 返回包含结束方法的监控器对象
|
||||
* 3. 结束方法自动计算耗时并记录日志
|
||||
* 4. 支持成功和失败两种结束状态
|
||||
*
|
||||
* @param operation 操作名称
|
||||
* @param context 操作上下文
|
||||
* @returns 性能监控器对象
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const monitor = this.createPerformanceMonitor('创建用户', { userId: '123' });
|
||||
* try {
|
||||
* const result = await this.repository.create(data);
|
||||
* monitor.success({ result: 'created' });
|
||||
* return result;
|
||||
* } catch (error) {
|
||||
* monitor.error(error);
|
||||
* throw error;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected createPerformanceMonitor(operation: string, context?: Record<string, any>) {
|
||||
const startTime = Date.now();
|
||||
this.logStart(operation, context);
|
||||
|
||||
return {
|
||||
success: (additionalContext?: Record<string, any>) => {
|
||||
const duration = Date.now() - startTime;
|
||||
this.logSuccess(operation, { ...context, ...additionalContext }, duration);
|
||||
},
|
||||
error: (error: unknown, additionalContext?: Record<string, any>) => {
|
||||
const duration = Date.now() - startTime;
|
||||
this.handleDataAccessError(error, operation, {
|
||||
...context,
|
||||
...additionalContext,
|
||||
duration
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析游戏用户ID为BigInt类型
|
||||
*
|
||||
* 数据转换逻辑:
|
||||
* 1. 将字符串类型的游戏用户ID转换为BigInt类型
|
||||
* 2. 统一处理ID转换逻辑,避免重复代码
|
||||
* 3. 提供类型安全的转换方法
|
||||
*
|
||||
* @param gameUserId 游戏用户ID字符串
|
||||
* @returns BigInt类型的游戏用户ID
|
||||
* @throws Error 当ID格式无效时
|
||||
*/
|
||||
protected parseGameUserId(gameUserId: string): bigint {
|
||||
try {
|
||||
return BigInt(gameUserId);
|
||||
} catch (error) {
|
||||
throw new Error(`无效的游戏用户ID格式: ${gameUserId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量解析ID数组为BigInt类型
|
||||
*
|
||||
* 数据转换逻辑:
|
||||
* 1. 将字符串ID数组转换为BigInt数组
|
||||
* 2. 统一处理批量ID转换逻辑
|
||||
* 3. 提供类型安全的批量转换方法
|
||||
*
|
||||
* @param ids 字符串ID数组
|
||||
* @returns BigInt类型的ID数组
|
||||
* @throws Error 当任何ID格式无效时
|
||||
*/
|
||||
protected parseIds(ids: string[]): bigint[] {
|
||||
try {
|
||||
return ids.map(id => BigInt(id));
|
||||
} catch (error) {
|
||||
throw new Error(`无效的ID格式: ${ids.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个ID为BigInt类型
|
||||
*
|
||||
* 数据转换逻辑:
|
||||
* 1. 将字符串类型的ID转换为BigInt类型
|
||||
* 2. 统一处理单个ID转换逻辑
|
||||
* 3. 提供类型安全的转换方法
|
||||
*
|
||||
* @param id 字符串ID
|
||||
* @returns BigInt类型的ID
|
||||
* @throws Error 当ID格式无效时
|
||||
*/
|
||||
protected parseId(id: string): bigint {
|
||||
try {
|
||||
return BigInt(id);
|
||||
} catch (error) {
|
||||
throw new Error(`无效的ID格式: ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 抽象方法:将实体转换为响应DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* 子类必须实现此方法,将数据库实体转换为API响应DTO
|
||||
*
|
||||
* @param entity 数据库实体对象
|
||||
* @returns 响应DTO对象
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 在子类中实现
|
||||
* protected toResponseDto(account: ZulipAccounts): ZulipAccountResponseDto {
|
||||
* return {
|
||||
* id: account.id.toString(),
|
||||
* gameUserId: account.gameUserId.toString(),
|
||||
* // ... 其他字段
|
||||
* };
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected abstract toResponseDto(entity: any): any;
|
||||
|
||||
/**
|
||||
* 将实体数组转换为响应DTO数组
|
||||
*
|
||||
* 功能描述:
|
||||
* 统一处理实体数组到DTO数组的转换,减少重复代码
|
||||
*
|
||||
* @param entities 实体数组
|
||||
* @returns 响应DTO数组
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const accounts = await this.repository.findMany();
|
||||
* const responseAccounts = this.toResponseDtoArray(accounts);
|
||||
* ```
|
||||
*/
|
||||
protected toResponseDtoArray(entities: any[]): any[] {
|
||||
return entities.map(entity => this.toResponseDto(entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建列表响应对象
|
||||
*
|
||||
* 功能描述:
|
||||
* 统一构建列表响应对象,减少重复的对象构建代码
|
||||
*
|
||||
* @param entities 实体数组
|
||||
* @returns 标准的列表响应对象
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const accounts = await this.repository.findMany();
|
||||
* return this.buildListResponse(accounts);
|
||||
* ```
|
||||
*/
|
||||
protected buildListResponse(entities: any[]): any {
|
||||
const responseAccounts = this.toResponseDtoArray(entities);
|
||||
return {
|
||||
accounts: responseAccounts,
|
||||
total: responseAccounts.length,
|
||||
count: responseAccounts.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
260
src/core/db/zulip_accounts/zulip_accounts.cache.config.ts
Normal file
260
src/core/db/zulip_accounts/zulip_accounts.cache.config.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* Zulip账号关联缓存配置
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义Zulip账号关联模块的缓存策略和配置
|
||||
* - 提供不同类型数据的缓存TTL设置
|
||||
* - 支持环境相关的缓存配置调整
|
||||
* - 提供缓存键命名规范和管理工具
|
||||
*
|
||||
* 职责分离:
|
||||
* - 缓存策略:定义不同数据类型的缓存时间和策略
|
||||
* - 键管理:提供统一的缓存键命名规范
|
||||
* - 环境适配:根据环境调整缓存配置
|
||||
* - 性能优化:平衡缓存效果和内存使用
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 初始创建 - 定义缓存配置和策略
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-12
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { CacheModuleOptions } from '@nestjs/cache-manager';
|
||||
|
||||
/**
|
||||
* 缓存配置常量
|
||||
*/
|
||||
export const CACHE_CONFIG = {
|
||||
// 缓存键前缀
|
||||
PREFIX: 'zulip_accounts',
|
||||
|
||||
// TTL配置(秒)
|
||||
TTL: {
|
||||
// 账号基础信息缓存 - 5分钟
|
||||
ACCOUNT_INFO: 300,
|
||||
|
||||
// 统计数据缓存 - 1分钟(变化频繁)
|
||||
STATISTICS: 60,
|
||||
|
||||
// 验证状态缓存 - 10分钟
|
||||
VERIFICATION_STATUS: 600,
|
||||
|
||||
// 错误账号列表缓存 - 2分钟(需要及时更新)
|
||||
ERROR_ACCOUNTS: 120,
|
||||
|
||||
// 批量查询结果缓存 - 3分钟
|
||||
BATCH_QUERY: 180,
|
||||
},
|
||||
|
||||
// 缓存大小限制
|
||||
MAX_ITEMS: {
|
||||
// 生产环境
|
||||
PRODUCTION: 5000,
|
||||
|
||||
// 开发环境
|
||||
DEVELOPMENT: 1000,
|
||||
|
||||
// 测试环境
|
||||
TEST: 500,
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 缓存键类型枚举
|
||||
*/
|
||||
export enum CacheKeyType {
|
||||
GAME_USER = 'game_user',
|
||||
ZULIP_USER = 'zulip_user',
|
||||
ZULIP_EMAIL = 'zulip_email',
|
||||
ACCOUNT_ID = 'account_id',
|
||||
STATISTICS = 'stats',
|
||||
VERIFICATION_LIST = 'verification_list',
|
||||
ERROR_LIST = 'error_list',
|
||||
BATCH_QUERY = 'batch_query',
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存配置工厂
|
||||
*/
|
||||
export class ZulipAccountsCacheConfigFactory {
|
||||
/**
|
||||
* 创建缓存模块配置
|
||||
*
|
||||
* @param environment 环境名称
|
||||
* @returns 缓存模块配置
|
||||
*/
|
||||
static createCacheConfig(environment: string = 'development'): CacheModuleOptions {
|
||||
const maxItems = this.getMaxItemsByEnvironment(environment);
|
||||
|
||||
return {
|
||||
ttl: CACHE_CONFIG.TTL.ACCOUNT_INFO, // 默认TTL
|
||||
max: maxItems,
|
||||
// 可以添加更多配置,如存储引擎等
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据环境获取最大缓存项数
|
||||
*
|
||||
* @param environment 环境名称
|
||||
* @returns 最大缓存项数
|
||||
* @private
|
||||
*/
|
||||
private static getMaxItemsByEnvironment(environment: string): number {
|
||||
switch (environment) {
|
||||
case 'production':
|
||||
return CACHE_CONFIG.MAX_ITEMS.PRODUCTION;
|
||||
case 'test':
|
||||
return CACHE_CONFIG.MAX_ITEMS.TEST;
|
||||
default:
|
||||
return CACHE_CONFIG.MAX_ITEMS.DEVELOPMENT;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建缓存键
|
||||
*
|
||||
* @param type 缓存键类型
|
||||
* @param identifier 标识符
|
||||
* @param suffix 后缀(可选)
|
||||
* @returns 完整的缓存键
|
||||
*/
|
||||
static buildCacheKey(
|
||||
type: CacheKeyType,
|
||||
identifier?: string | number,
|
||||
suffix?: string
|
||||
): string {
|
||||
const parts = [CACHE_CONFIG.PREFIX, type.toString()];
|
||||
|
||||
if (identifier !== undefined) {
|
||||
parts.push(String(identifier));
|
||||
}
|
||||
|
||||
if (suffix) {
|
||||
parts.push(suffix);
|
||||
}
|
||||
|
||||
return parts.join(':');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定类型的TTL
|
||||
*
|
||||
* @param type 缓存键类型
|
||||
* @returns TTL(秒)
|
||||
*/
|
||||
static getTTLByType(type: CacheKeyType): number {
|
||||
switch (type) {
|
||||
case CacheKeyType.STATISTICS:
|
||||
return CACHE_CONFIG.TTL.STATISTICS;
|
||||
case CacheKeyType.VERIFICATION_LIST:
|
||||
return CACHE_CONFIG.TTL.VERIFICATION_STATUS;
|
||||
case CacheKeyType.ERROR_LIST:
|
||||
return CACHE_CONFIG.TTL.ERROR_ACCOUNTS;
|
||||
case CacheKeyType.BATCH_QUERY:
|
||||
return CACHE_CONFIG.TTL.BATCH_QUERY;
|
||||
default:
|
||||
return CACHE_CONFIG.TTL.ACCOUNT_INFO;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成缓存键模式(用于批量删除)
|
||||
*
|
||||
* @param type 缓存键类型
|
||||
* @returns 缓存键模式
|
||||
*/
|
||||
static getCacheKeyPattern(type: CacheKeyType): string {
|
||||
return `${CACHE_CONFIG.PREFIX}:${type}:*`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存管理工具类
|
||||
*/
|
||||
export class ZulipAccountsCacheManager {
|
||||
/**
|
||||
* 获取所有相关的缓存键(用于清除)
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @param zulipUserId Zulip用户ID
|
||||
* @param zulipEmail Zulip邮箱
|
||||
* @returns 相关的缓存键列表
|
||||
*/
|
||||
static getRelatedCacheKeys(
|
||||
gameUserId?: string,
|
||||
zulipUserId?: number,
|
||||
zulipEmail?: string
|
||||
): string[] {
|
||||
const keys: string[] = [];
|
||||
|
||||
// 统计数据缓存(总是需要清除)
|
||||
keys.push(ZulipAccountsCacheConfigFactory.buildCacheKey(CacheKeyType.STATISTICS));
|
||||
|
||||
// 验证和错误列表缓存(可能受影响)
|
||||
keys.push(ZulipAccountsCacheConfigFactory.buildCacheKey(CacheKeyType.VERIFICATION_LIST));
|
||||
keys.push(ZulipAccountsCacheConfigFactory.buildCacheKey(CacheKeyType.ERROR_LIST));
|
||||
|
||||
// 具体记录的缓存
|
||||
if (gameUserId) {
|
||||
keys.push(
|
||||
ZulipAccountsCacheConfigFactory.buildCacheKey(CacheKeyType.GAME_USER, gameUserId),
|
||||
ZulipAccountsCacheConfigFactory.buildCacheKey(CacheKeyType.GAME_USER, gameUserId, 'with_user')
|
||||
);
|
||||
}
|
||||
|
||||
if (zulipUserId) {
|
||||
keys.push(
|
||||
ZulipAccountsCacheConfigFactory.buildCacheKey(CacheKeyType.ZULIP_USER, zulipUserId),
|
||||
ZulipAccountsCacheConfigFactory.buildCacheKey(CacheKeyType.ZULIP_USER, zulipUserId, 'with_user')
|
||||
);
|
||||
}
|
||||
|
||||
if (zulipEmail) {
|
||||
keys.push(
|
||||
ZulipAccountsCacheConfigFactory.buildCacheKey(CacheKeyType.ZULIP_EMAIL, zulipEmail),
|
||||
ZulipAccountsCacheConfigFactory.buildCacheKey(CacheKeyType.ZULIP_EMAIL, zulipEmail, 'with_user')
|
||||
);
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存键是否有效
|
||||
*
|
||||
* @param key 缓存键
|
||||
* @returns 是否有效
|
||||
*/
|
||||
static isValidCacheKey(key: string): boolean {
|
||||
return key.startsWith(CACHE_CONFIG.PREFIX + ':');
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析缓存键
|
||||
*
|
||||
* @param key 缓存键
|
||||
* @returns 解析结果
|
||||
*/
|
||||
static parseCacheKey(key: string): {
|
||||
prefix: string;
|
||||
type: string;
|
||||
identifier?: string;
|
||||
suffix?: string;
|
||||
} | null {
|
||||
if (!this.isValidCacheKey(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts = key.split(':');
|
||||
return {
|
||||
prefix: parts[0],
|
||||
type: parts[1],
|
||||
identifier: parts[2],
|
||||
suffix: parts[3],
|
||||
};
|
||||
}
|
||||
}
|
||||
65
src/core/db/zulip_accounts/zulip_accounts.constants.ts
Normal file
65
src/core/db/zulip_accounts/zulip_accounts.constants.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Zulip账号关联模块常量定义
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义模块中使用的所有常量和配置值
|
||||
* - 提供统一的常量管理和维护
|
||||
* - 避免魔法数字和硬编码值
|
||||
* - 便于配置调整和环境适配
|
||||
*
|
||||
* 职责分离:
|
||||
* - 常量定义:集中管理所有模块常量
|
||||
* - 配置管理:提供可配置的默认值
|
||||
* - 类型安全:确保常量的类型正确性
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 提取魔法数字为常量,提高代码质量 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 注释规范检查和修正 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 使用统一的常量文件,提高代码质量
|
||||
* - 2026-01-07: 功能新增 - 添加状态枚举和类型定义
|
||||
* - 2026-01-07: 初始创建 - 提取模块中的常量定义,统一管理
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.1
|
||||
* @since 2026-01-07
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
// 时间相关常量
|
||||
export const MILLISECONDS_PER_HOUR = 60 * 60 * 1000;
|
||||
export const MILLISECONDS_PER_DAY = 24 * MILLISECONDS_PER_HOUR;
|
||||
|
||||
// 验证相关常量
|
||||
export const DEFAULT_VERIFICATION_MAX_AGE = 24 * MILLISECONDS_PER_HOUR; // 24小时验证间隔
|
||||
export const DEFAULT_VERIFICATION_HOURS = 24;
|
||||
export const DEFAULT_VERIFICATION_INTERVAL = DEFAULT_VERIFICATION_MAX_AGE;
|
||||
|
||||
// 重试相关常量
|
||||
export const DEFAULT_MAX_RETRY_COUNT = 3; // 默认最大重试次数
|
||||
export const HIGH_RETRY_THRESHOLD = 5; // 高重试次数阈值
|
||||
|
||||
// 查询限制常量
|
||||
export const VERIFICATION_QUERY_LIMIT = 100; // 验证查询限制
|
||||
export const ERROR_ACCOUNTS_QUERY_LIMIT = 50; // 错误账号查询限制
|
||||
export const DEFAULT_ERROR_ACCOUNTS_LIMIT = 50; // 默认错误账号限制
|
||||
|
||||
// 业务规则常量
|
||||
export const DEFAULT_MAX_AGE_DAYS = 7; // 默认最大年龄天数
|
||||
|
||||
// 长度限制常量
|
||||
export const MAX_FULL_NAME_LENGTH = 100; // 用户全名最大长度
|
||||
export const MAX_SHORT_NAME_LENGTH = 50; // 用户短名称最大长度
|
||||
export const MIN_FULL_NAME_LENGTH = 2; // 用户全名最小长度
|
||||
|
||||
// 数据库配置常量
|
||||
export const REQUIRED_DB_ENV_VARS = ['DB_HOST', 'DB_PORT', 'DB_USERNAME', 'DB_PASSWORD', 'DB_NAME'];
|
||||
|
||||
// 状态枚举
|
||||
export const ACCOUNT_STATUS = {
|
||||
ACTIVE: 'active' as const,
|
||||
INACTIVE: 'inactive' as const,
|
||||
SUSPENDED: 'suspended' as const,
|
||||
ERROR: 'error' as const,
|
||||
} as const;
|
||||
|
||||
export type AccountStatus = typeof ACCOUNT_STATUS[keyof typeof ACCOUNT_STATUS];
|
||||
275
src/core/db/zulip_accounts/zulip_accounts.dto.ts
Normal file
275
src/core/db/zulip_accounts/zulip_accounts.dto.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* Zulip账号关联数据传输对象
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义API请求和响应的数据结构和验证规则
|
||||
* - 提供统一的数据传输格式和类型约束
|
||||
* - 支持Swagger文档自动生成和API接口描述
|
||||
* - 实现数据验证、转换和序列化功能
|
||||
*
|
||||
* 职责分离:
|
||||
* - 数据结构定义:定义所有API相关的数据传输对象
|
||||
* - 验证规则:通过装饰器定义字段验证和约束规则
|
||||
* - 文档生成:提供Swagger API文档的元数据信息
|
||||
* - 类型安全:确保前后端数据交互的类型一致性
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 完善文件头注释和移除未使用的导入
|
||||
* - 2026-01-07: 功能完善 - 优化DTO字段验证规则和文档描述
|
||||
* - 2025-01-07: 架构优化 - 统一数据传输对象的设计模式
|
||||
* - 2025-01-07: 初始创建 - 创建基础的DTO类和验证规则
|
||||
* - 2025-01-07: 功能实现 - 实现完整的请求响应DTO定义
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.1.0
|
||||
* @since 2025-01-07
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { IsString, IsNumber, IsEmail, IsEnum, IsOptional, IsBoolean } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* 创建Zulip账号关联请求DTO
|
||||
*/
|
||||
export class CreateZulipAccountDto {
|
||||
@ApiProperty({ description: '游戏用户ID', example: '12345' })
|
||||
@IsString()
|
||||
gameUserId: string;
|
||||
|
||||
@ApiProperty({ description: 'Zulip用户ID', example: 67890 })
|
||||
@IsNumber()
|
||||
zulipUserId: number;
|
||||
|
||||
@ApiProperty({ description: 'Zulip邮箱地址', example: 'user@example.com' })
|
||||
@IsEmail()
|
||||
zulipEmail: string;
|
||||
|
||||
@ApiProperty({ description: 'Zulip用户全名', example: '张三' })
|
||||
@IsString()
|
||||
zulipFullName: string;
|
||||
|
||||
@ApiProperty({ description: '加密的Zulip API Key' })
|
||||
@IsString()
|
||||
zulipApiKeyEncrypted: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: '账号状态',
|
||||
enum: ['active', 'inactive', 'suspended', 'error'],
|
||||
default: 'active'
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(['active', 'inactive', 'suspended', 'error'])
|
||||
status?: 'active' | 'inactive' | 'suspended' | 'error';
|
||||
|
||||
@ApiPropertyOptional({ description: '最后验证时间' })
|
||||
@IsOptional()
|
||||
lastVerifiedAt?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Zulip账号关联请求DTO
|
||||
*/
|
||||
export class UpdateZulipAccountDto {
|
||||
@ApiPropertyOptional({ description: 'Zulip用户全名', example: '李四' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
zulipFullName?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '加密的Zulip API Key' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
zulipApiKeyEncrypted?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: '账号状态',
|
||||
enum: ['active', 'inactive', 'suspended', 'error']
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(['active', 'inactive', 'suspended', 'error'])
|
||||
status?: 'active' | 'inactive' | 'suspended' | 'error';
|
||||
|
||||
@ApiPropertyOptional({ description: '错误信息' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
errorMessage?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '重试次数', example: 0 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
retryCount?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '最后验证时间' })
|
||||
@IsOptional()
|
||||
lastVerifiedAt?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zulip账号关联查询DTO
|
||||
*/
|
||||
export class QueryZulipAccountDto {
|
||||
@ApiPropertyOptional({ description: '游戏用户ID', example: '12345' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
gameUserId?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Zulip用户ID', example: 67890 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
zulipUserId?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Zulip邮箱地址', example: 'user@example.com' })
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
zulipEmail?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: '账号状态',
|
||||
enum: ['active', 'inactive', 'suspended', 'error']
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(['active', 'inactive', 'suspended', 'error'])
|
||||
status?: 'active' | 'inactive' | 'suspended' | 'error';
|
||||
|
||||
@ApiPropertyOptional({ description: '是否包含游戏用户信息', default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
includeGameUser?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zulip账号关联响应DTO
|
||||
*/
|
||||
export class ZulipAccountResponseDto {
|
||||
@ApiProperty({ description: '关联记录ID', example: '1' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: '游戏用户ID', example: '12345' })
|
||||
gameUserId: string;
|
||||
|
||||
@ApiProperty({ description: 'Zulip用户ID', example: 67890 })
|
||||
zulipUserId: number;
|
||||
|
||||
@ApiProperty({ description: 'Zulip邮箱地址', example: 'user@example.com' })
|
||||
zulipEmail: string;
|
||||
|
||||
@ApiProperty({ description: 'Zulip用户全名', example: '张三' })
|
||||
zulipFullName: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '账号状态',
|
||||
enum: ['active', 'inactive', 'suspended', 'error']
|
||||
})
|
||||
status: 'active' | 'inactive' | 'suspended' | 'error';
|
||||
|
||||
@ApiPropertyOptional({ description: '最后验证时间' })
|
||||
lastVerifiedAt?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '最后同步时间' })
|
||||
lastSyncedAt?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '错误信息' })
|
||||
errorMessage?: string;
|
||||
|
||||
@ApiProperty({ description: '重试次数', example: 0 })
|
||||
retryCount: number;
|
||||
|
||||
@ApiProperty({ description: '创建时间' })
|
||||
createdAt: string;
|
||||
|
||||
@ApiProperty({ description: '更新时间' })
|
||||
updatedAt: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '关联的游戏用户信息' })
|
||||
gameUser?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zulip账号关联列表响应DTO
|
||||
*/
|
||||
export class ZulipAccountListResponseDto {
|
||||
@ApiProperty({ description: '账号关联列表', type: [ZulipAccountResponseDto] })
|
||||
accounts: ZulipAccountResponseDto[];
|
||||
|
||||
@ApiProperty({ description: '总数', example: 100 })
|
||||
total: number;
|
||||
|
||||
@ApiProperty({ description: '当前页数量', example: 10 })
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号状态统计响应DTO
|
||||
*/
|
||||
export class ZulipAccountStatsResponseDto {
|
||||
@ApiProperty({ description: '正常状态账号数', example: 85 })
|
||||
active: number;
|
||||
|
||||
@ApiProperty({ description: '未激活账号数', example: 10 })
|
||||
inactive: number;
|
||||
|
||||
@ApiProperty({ description: '暂停状态账号数', example: 3 })
|
||||
suspended: number;
|
||||
|
||||
@ApiProperty({ description: '错误状态账号数', example: 2 })
|
||||
error: number;
|
||||
|
||||
@ApiProperty({ description: '总账号数', example: 100 })
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作请求DTO
|
||||
*/
|
||||
export class BatchUpdateStatusDto {
|
||||
@ApiProperty({ description: '账号ID列表', example: ['1', '2', '3'] })
|
||||
@IsString({ each: true })
|
||||
ids: string[];
|
||||
|
||||
@ApiProperty({
|
||||
description: '新状态',
|
||||
enum: ['active', 'inactive', 'suspended', 'error']
|
||||
})
|
||||
@IsEnum(['active', 'inactive', 'suspended', 'error'])
|
||||
status: 'active' | 'inactive' | 'suspended' | 'error';
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作响应DTO
|
||||
*/
|
||||
export class BatchUpdateResponseDto {
|
||||
@ApiProperty({ description: '操作是否成功' })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ description: '更新的记录数', example: 3 })
|
||||
updatedCount: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '错误信息' })
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号验证请求DTO
|
||||
*/
|
||||
export class VerifyAccountDto {
|
||||
@ApiProperty({ description: '游戏用户ID', example: '12345' })
|
||||
@IsString()
|
||||
gameUserId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号验证响应DTO
|
||||
*/
|
||||
export class VerifyAccountResponseDto {
|
||||
@ApiProperty({ description: '验证是否成功' })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ description: '账号是否有效' })
|
||||
isValid: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: '验证时间' })
|
||||
verifiedAt?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '错误信息' })
|
||||
error?: string;
|
||||
}
|
||||
476
src/core/db/zulip_accounts/zulip_accounts.entity.ts
Normal file
476
src/core/db/zulip_accounts/zulip_accounts.entity.ts
Normal file
@@ -0,0 +1,476 @@
|
||||
/**
|
||||
* Zulip账号关联实体
|
||||
*
|
||||
* 功能描述:
|
||||
* - 存储游戏用户与Zulip账号的关联关系
|
||||
* - 管理Zulip账号的基本信息和状态
|
||||
* - 提供账号验证和同步功能
|
||||
* - 支持多种状态管理和业务判断方法
|
||||
*
|
||||
* 职责分离:
|
||||
* - 数据模型定义:定义数据库表结构和字段约束
|
||||
* - 业务方法:提供账号状态判断和操作方法
|
||||
* - 关联关系:管理与Users表的一对一关系
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 使用统一的常量文件,提高代码质量
|
||||
* - 2026-01-07: 代码规范优化 - 完善文件头注释和方法注释规范
|
||||
* - 2026-01-07: 功能新增 - 添加数据库唯一约束和复合索引
|
||||
* - 2026-01-07: 功能新增 - 新增多个业务判断方法(isHealthy, canBeDeleted等)
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.1.1
|
||||
* @since 2025-01-05
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToOne, JoinColumn, Index } from 'typeorm';
|
||||
import { Users } from '../users/users.entity';
|
||||
import {
|
||||
DEFAULT_MAX_AGE_DAYS,
|
||||
DEFAULT_VERIFICATION_HOURS,
|
||||
DEFAULT_MAX_RETRY_COUNT,
|
||||
HIGH_RETRY_THRESHOLD,
|
||||
MILLISECONDS_PER_HOUR,
|
||||
MILLISECONDS_PER_DAY,
|
||||
} from './zulip_accounts.constants';
|
||||
|
||||
@Entity('zulip_accounts')
|
||||
@Index(['gameUserId']) // 普通索引,不是唯一索引
|
||||
@Index(['zulipUserId'], { unique: true })
|
||||
@Index(['zulipEmail'], { unique: true })
|
||||
@Index(['status']) // 单独的status索引
|
||||
@Index(['createdAt']) // 单独的created_at索引
|
||||
@Index(['status', 'lastVerifiedAt']) // 复合索引用于查询优化
|
||||
@Index(['status', 'updatedAt']) // 复合索引用于查询优化
|
||||
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;
|
||||
|
||||
/**
|
||||
* 检查账号是否处于正常状态
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 检查账号状态是否为'active'
|
||||
* 2. 返回布尔值表示是否正常
|
||||
*
|
||||
* @returns boolean 是否为正常状态
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = new ZulipAccounts();
|
||||
* account.status = 'active';
|
||||
* console.log(account.isActive()); // true
|
||||
* ```
|
||||
*/
|
||||
isActive(): boolean {
|
||||
return this.status === 'active';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查账号是否健康(正常且重试次数不多)
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 检查账号状态是否为'active'
|
||||
* 2. 检查重试次数是否小于默认阈值
|
||||
* 3. 两个条件都满足才认为健康
|
||||
*
|
||||
* @returns boolean 是否健康
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = new ZulipAccounts();
|
||||
* account.status = 'active';
|
||||
* account.retryCount = 1;
|
||||
* console.log(account.isHealthy()); // true
|
||||
* ```
|
||||
*/
|
||||
isHealthy(): boolean {
|
||||
return this.status === 'active' && this.retryCount < DEFAULT_MAX_RETRY_COUNT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查账号是否可以被删除
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 如果账号状态不是'active',可以删除
|
||||
* 2. 如果重试次数超过高阈值,可以删除
|
||||
* 3. 满足任一条件即可删除
|
||||
*
|
||||
* @returns boolean 是否可以删除
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = new ZulipAccounts();
|
||||
* account.status = 'error';
|
||||
* account.retryCount = 6;
|
||||
* console.log(account.canBeDeleted()); // true
|
||||
* ```
|
||||
*/
|
||||
canBeDeleted(): boolean {
|
||||
return this.status !== 'active' || this.retryCount > HIGH_RETRY_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查账号数据是否过期
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 获取当前时间
|
||||
* 2. 计算与最后更新时间的差值
|
||||
* 3. 比较差值是否超过最大年龄限制
|
||||
*
|
||||
* @param maxAge 最大年龄(毫秒),默认7天
|
||||
* @returns boolean 是否过期
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = new ZulipAccounts();
|
||||
* account.updatedAt = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000);
|
||||
* console.log(account.isStale()); // true (超过7天)
|
||||
* ```
|
||||
*/
|
||||
isStale(maxAge: number = DEFAULT_MAX_AGE_DAYS * MILLISECONDS_PER_DAY): boolean {
|
||||
const now = new Date();
|
||||
const timeDiff = now.getTime() - this.updatedAt.getTime();
|
||||
return timeDiff > maxAge;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查账号是否需要重新验证
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 如果从未验证过,需要验证
|
||||
* 2. 计算距离上次验证的时间差
|
||||
* 3. 比较时间差是否超过最大验证间隔
|
||||
*
|
||||
* @param maxAge 最大验证间隔(毫秒),默认24小时
|
||||
* @returns boolean 是否需要重新验证
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = new ZulipAccounts();
|
||||
* account.lastVerifiedAt = null;
|
||||
* console.log(account.needsVerification()); // true
|
||||
* ```
|
||||
*/
|
||||
needsVerification(maxAge: number = DEFAULT_VERIFICATION_HOURS * MILLISECONDS_PER_HOUR): boolean {
|
||||
if (!this.lastVerifiedAt) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const timeDiff = now.getTime() - this.lastVerifiedAt.getTime();
|
||||
return timeDiff > maxAge;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该重试操作
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 检查账号状态是否为'error'
|
||||
* 2. 检查重试次数是否小于最大重试次数
|
||||
* 3. 两个条件都满足才应该重试
|
||||
*
|
||||
* @param maxRetryCount 最大重试次数,默认3次
|
||||
* @returns boolean 是否应该重试
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = new ZulipAccounts();
|
||||
* account.status = 'error';
|
||||
* account.retryCount = 2;
|
||||
* console.log(account.shouldRetry()); // true
|
||||
* ```
|
||||
*/
|
||||
shouldRetry(maxRetryCount: number = DEFAULT_MAX_RETRY_COUNT): boolean {
|
||||
return this.status === 'error' && this.retryCount < maxRetryCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新验证时间
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 设置最后验证时间为当前时间
|
||||
* 2. 更新记录的最后修改时间
|
||||
* 3. 用于标记账号验证操作的完成
|
||||
*
|
||||
* @returns void 无返回值,直接修改实体属性
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = new ZulipAccounts();
|
||||
* account.updateVerificationTime();
|
||||
* console.log(account.lastVerifiedAt); // 当前时间
|
||||
* ```
|
||||
*/
|
||||
updateVerificationTime(): void {
|
||||
this.lastVerifiedAt = new Date();
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新同步时间
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 设置最后同步时间为当前时间
|
||||
* 2. 更新记录的最后修改时间
|
||||
* 3. 用于标记数据同步操作的完成
|
||||
*
|
||||
* @returns void 无返回值,直接修改实体属性
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = new ZulipAccounts();
|
||||
* account.updateSyncTime();
|
||||
* console.log(account.lastSyncedAt); // 当前时间
|
||||
* ```
|
||||
*/
|
||||
updateSyncTime(): void {
|
||||
this.lastSyncedAt = new Date();
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置错误状态
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 将账号状态设置为'error'
|
||||
* 2. 记录具体的错误信息
|
||||
* 3. 增加重试计数器
|
||||
* 4. 更新最后修改时间
|
||||
*
|
||||
* @param errorMessage 错误信息,描述具体的错误原因
|
||||
* @returns void 无返回值,直接修改实体属性
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = new ZulipAccounts();
|
||||
* account.setError('API连接超时');
|
||||
* console.log(account.status); // 'error'
|
||||
* console.log(account.retryCount); // 增加1
|
||||
* ```
|
||||
*/
|
||||
setError(errorMessage: string): void {
|
||||
this.status = 'error';
|
||||
this.errorMessage = errorMessage;
|
||||
this.retryCount += 1;
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除错误状态
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 检查当前状态是否为'error'
|
||||
* 2. 如果是错误状态,恢复为'active'状态
|
||||
* 3. 清空错误信息
|
||||
* 4. 更新最后修改时间
|
||||
*
|
||||
* @returns void 无返回值,直接修改实体属性
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = new ZulipAccounts();
|
||||
* account.status = 'error';
|
||||
* account.clearError();
|
||||
* console.log(account.status); // 'active'
|
||||
* console.log(account.errorMessage); // null
|
||||
* ```
|
||||
*/
|
||||
clearError(): void {
|
||||
if (this.status === 'error') {
|
||||
this.status = 'active';
|
||||
this.errorMessage = null;
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置重试计数
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 将重试次数重置为0
|
||||
* 2. 更新最后修改时间
|
||||
* 3. 用于成功操作后清除重试记录
|
||||
*
|
||||
* @returns void 无返回值,直接修改实体属性
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = new ZulipAccounts();
|
||||
* account.retryCount = 3;
|
||||
* account.resetRetryCount();
|
||||
* console.log(account.retryCount); // 0
|
||||
* ```
|
||||
*/
|
||||
resetRetryCount(): void {
|
||||
this.retryCount = 0;
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* 激活账号
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 将账号状态设置为'active'
|
||||
* 2. 清空错误信息
|
||||
* 3. 重置重试计数为0
|
||||
* 4. 更新最后修改时间
|
||||
*
|
||||
* @returns void 无返回值,直接修改实体属性
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = new ZulipAccounts();
|
||||
* account.status = 'suspended';
|
||||
* account.activate();
|
||||
* console.log(account.status); // 'active'
|
||||
* ```
|
||||
*/
|
||||
activate(): void {
|
||||
this.status = 'active';
|
||||
this.errorMessage = null;
|
||||
this.retryCount = 0;
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停账号
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 将账号状态设置为'suspended'
|
||||
* 2. 如果提供了原因,记录到错误信息中
|
||||
* 3. 更新最后修改时间
|
||||
*
|
||||
* @param reason 暂停原因,可选参数,用于记录暂停的具体原因
|
||||
* @returns void 无返回值,直接修改实体属性
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = new ZulipAccounts();
|
||||
* account.suspend('违反使用规则');
|
||||
* console.log(account.status); // 'suspended'
|
||||
* console.log(account.errorMessage); // '违反使用规则'
|
||||
* ```
|
||||
*/
|
||||
suspend(reason?: string): void {
|
||||
this.status = 'suspended';
|
||||
if (reason) {
|
||||
this.errorMessage = reason;
|
||||
}
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停用账号
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 将账号状态设置为'inactive'
|
||||
* 2. 更新最后修改时间
|
||||
* 3. 用于临时停用账号但保留数据
|
||||
*
|
||||
* @returns void 无返回值,直接修改实体属性
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = new ZulipAccounts();
|
||||
* account.deactivate();
|
||||
* console.log(account.status); // 'inactive'
|
||||
* ```
|
||||
*/
|
||||
deactivate(): void {
|
||||
this.status = 'inactive';
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
}
|
||||
190
src/core/db/zulip_accounts/zulip_accounts.module.ts
Normal file
190
src/core/db/zulip_accounts/zulip_accounts.module.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* Zulip账号关联数据模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供Zulip账号关联数据的访问接口和服务注册
|
||||
* - 封装TypeORM实体和Repository的依赖注入配置
|
||||
* - 为业务层提供统一的数据访问服务接口
|
||||
* - 支持数据库和内存模式的动态切换和环境适配
|
||||
* - 集成缓存和日志系统,提升性能和可观测性
|
||||
*
|
||||
* 职责分离:
|
||||
* - 模块配置:管理依赖注入和服务提供者的注册
|
||||
* - 环境适配:根据配置自动选择数据库或内存存储模式
|
||||
* - 服务导出:为其他模块提供数据访问服务的统一接口
|
||||
* - 全局注册:通过@Global装饰器实现全局模块共享
|
||||
* - 依赖管理:集成缓存、日志等基础设施服务
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 性能优化 - 集成缓存模块和AppLoggerService,提升性能和可观测性
|
||||
* - 2026-01-07: 代码规范优化 - 使用统一的常量文件,提高代码质量
|
||||
* - 2026-01-07: 代码规范优化 - 完善文件头注释和方法三级注释
|
||||
* - 2026-01-07: 功能完善 - 优化环境检测逻辑和模块配置
|
||||
* - 2025-01-07: 架构优化 - 实现动态模块配置和环境自适应
|
||||
* - 2025-01-05: 功能扩展 - 添加内存模式支持和自动切换机制
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.2.0
|
||||
* @since 2025-01-05
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Module, DynamicModule, Global } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
import { ZulipAccounts } from './zulip_accounts.entity';
|
||||
import { ZulipAccountsRepository } from './zulip_accounts.repository';
|
||||
import { ZulipAccountsMemoryRepository } from './zulip_accounts_memory.repository';
|
||||
import { ZulipAccountsService } from './zulip_accounts.service';
|
||||
import { ZulipAccountsMemoryService } from './zulip_accounts_memory.service';
|
||||
import { AppLoggerService } from '../../utils/logger/logger.service';
|
||||
import { REQUIRED_DB_ENV_VARS } from './zulip_accounts.constants';
|
||||
|
||||
/**
|
||||
* 检查数据库配置是否完整
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 遍历所有必需的数据库环境变量名称
|
||||
* 2. 检查每个环境变量是否在process.env中存在且有值
|
||||
* 3. 只有当所有必需变量都存在时才返回true
|
||||
* 4. 用于决定使用数据库模式还是内存模式
|
||||
*
|
||||
* @returns 是否配置了完整的数据库连接信息
|
||||
*
|
||||
* @example
|
||||
* // 检查数据库配置
|
||||
* if (isDatabaseConfigured()) {
|
||||
* console.log('使用数据库模式');
|
||||
* } else {
|
||||
* console.log('使用内存模式');
|
||||
* }
|
||||
*/
|
||||
function isDatabaseConfigured(): boolean {
|
||||
return REQUIRED_DB_ENV_VARS.every(varName => process.env[varName]);
|
||||
}
|
||||
|
||||
@Global()
|
||||
@Module({})
|
||||
export class ZulipAccountsModule {
|
||||
/**
|
||||
* 创建数据库模式的Zulip账号模块
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 导入TypeORM模块并注册ZulipAccounts实体
|
||||
* 2. 集成缓存模块提供数据缓存能力
|
||||
* 3. 注册数据库版本的Repository和Service实现
|
||||
* 4. 配置依赖注入的提供者和别名映射
|
||||
* 5. 导出服务接口供其他模块使用
|
||||
* 6. 确保TypeORM功能的完整集成和事务支持
|
||||
* 7. 集成AppLoggerService提供结构化日志
|
||||
*
|
||||
* @returns 配置了TypeORM和缓存的动态模块,包含数据库访问功能
|
||||
*
|
||||
* @example
|
||||
* // 在应用模块中使用数据库模式
|
||||
* @Module({
|
||||
* imports: [ZulipAccountsModule.forDatabase()],
|
||||
* })
|
||||
* export class AppModule {}
|
||||
*/
|
||||
static forDatabase(): DynamicModule {
|
||||
return {
|
||||
module: ZulipAccountsModule,
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ZulipAccounts]),
|
||||
CacheModule.register({
|
||||
ttl: 300, // 5分钟默认TTL
|
||||
max: 1000, // 最大缓存项数
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
ZulipAccountsRepository,
|
||||
AppLoggerService,
|
||||
{
|
||||
provide: 'ZulipAccountsService',
|
||||
useClass: ZulipAccountsService,
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
ZulipAccountsRepository,
|
||||
'ZulipAccountsService',
|
||||
TypeOrmModule,
|
||||
AppLoggerService,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建内存模式的Zulip账号模块
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 注册内存版本的Repository和Service实现
|
||||
* 2. 集成基础缓存模块(内存模式也可以使用缓存)
|
||||
* 3. 配置依赖注入的提供者,使用内存存储类
|
||||
* 4. 不依赖TypeORM和数据库连接
|
||||
* 5. 适用于开发、测试和演示环境
|
||||
* 6. 提供与数据库模式相同的接口和功能
|
||||
* 7. 集成AppLoggerService提供结构化日志
|
||||
*
|
||||
* @returns 配置了内存存储和缓存的动态模块,无需数据库连接
|
||||
*
|
||||
* @example
|
||||
* // 在测试环境中使用内存模式
|
||||
* @Module({
|
||||
* imports: [ZulipAccountsModule.forMemory()],
|
||||
* })
|
||||
* export class TestModule {}
|
||||
*/
|
||||
static forMemory(): DynamicModule {
|
||||
return {
|
||||
module: ZulipAccountsModule,
|
||||
imports: [
|
||||
CacheModule.register({
|
||||
ttl: 300, // 5分钟默认TTL
|
||||
max: 500, // 内存模式使用较小的缓存
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
AppLoggerService,
|
||||
{
|
||||
provide: 'ZulipAccountsRepository',
|
||||
useClass: ZulipAccountsMemoryRepository,
|
||||
},
|
||||
{
|
||||
provide: 'ZulipAccountsService',
|
||||
useClass: ZulipAccountsMemoryService,
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
'ZulipAccountsRepository',
|
||||
'ZulipAccountsService',
|
||||
AppLoggerService,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据环境自动选择模式
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 调用isDatabaseConfigured()检查数据库配置完整性
|
||||
* 2. 如果数据库配置完整,返回数据库模式的动态模块
|
||||
* 3. 如果数据库配置不完整,返回内存模式的动态模块
|
||||
* 4. 实现环境自适应,简化模块配置和部署流程
|
||||
* 5. 确保应用在不同环境下都能正常启动和运行
|
||||
*
|
||||
* @returns 根据环境配置自动选择的动态模块
|
||||
*
|
||||
* @example
|
||||
* // 在主模块中使用自动模式选择
|
||||
* @Module({
|
||||
* imports: [ZulipAccountsModule.forRoot()],
|
||||
* })
|
||||
* export class AppModule {}
|
||||
*/
|
||||
static forRoot(): DynamicModule {
|
||||
return isDatabaseConfigured()
|
||||
? ZulipAccountsModule.forDatabase()
|
||||
: ZulipAccountsModule.forMemory();
|
||||
}
|
||||
}
|
||||
429
src/core/db/zulip_accounts/zulip_accounts.performance.ts
Normal file
429
src/core/db/zulip_accounts/zulip_accounts.performance.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
/**
|
||||
* Zulip账号关联性能监控工具
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供性能监控和指标收集功能
|
||||
* - 支持操作耗时统计和性能基准对比
|
||||
* - 集成告警机制和性能阈值监控
|
||||
* - 提供性能报告和分析工具
|
||||
*
|
||||
* 职责分离:
|
||||
* - 性能监控:记录和统计各种操作的性能指标
|
||||
* - 阈值管理:定义和管理性能阈值和告警规则
|
||||
* - 指标收集:收集和聚合性能数据
|
||||
* - 报告生成:生成性能报告和分析结果
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 初始创建 - 实现性能监控和指标收集功能
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-12
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { AppLoggerService } from '../../utils/logger/logger.service';
|
||||
|
||||
/**
|
||||
* 性能指标接口
|
||||
*/
|
||||
export interface PerformanceMetric {
|
||||
/** 操作名称 */
|
||||
operation: string;
|
||||
/** 执行时长(毫秒) */
|
||||
duration: number;
|
||||
/** 开始时间 */
|
||||
startTime: number;
|
||||
/** 结束时间 */
|
||||
endTime: number;
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
/** 上下文信息 */
|
||||
context?: Record<string, any>;
|
||||
/** 错误信息(如果失败) */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能统计信息
|
||||
*/
|
||||
export interface PerformanceStats {
|
||||
/** 操作名称 */
|
||||
operation: string;
|
||||
/** 总调用次数 */
|
||||
totalCalls: number;
|
||||
/** 成功次数 */
|
||||
successCalls: number;
|
||||
/** 失败次数 */
|
||||
failureCalls: number;
|
||||
/** 成功率 */
|
||||
successRate: number;
|
||||
/** 平均耗时 */
|
||||
avgDuration: number;
|
||||
/** 最小耗时 */
|
||||
minDuration: number;
|
||||
/** 最大耗时 */
|
||||
maxDuration: number;
|
||||
/** P95耗时 */
|
||||
p95Duration: number;
|
||||
/** P99耗时 */
|
||||
p99Duration: number;
|
||||
/** 最后更新时间 */
|
||||
lastUpdated: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能阈值配置
|
||||
*/
|
||||
export const PERFORMANCE_THRESHOLDS = {
|
||||
// 数据库操作阈值(毫秒)
|
||||
DATABASE: {
|
||||
QUERY_SINGLE: 50, // 单条查询
|
||||
QUERY_BATCH: 200, // 批量查询
|
||||
INSERT: 100, // 插入操作
|
||||
UPDATE: 80, // 更新操作
|
||||
DELETE: 60, // 删除操作
|
||||
TRANSACTION: 300, // 事务操作
|
||||
},
|
||||
|
||||
// 缓存操作阈值(毫秒)
|
||||
CACHE: {
|
||||
GET: 5, // 缓存读取
|
||||
SET: 10, // 缓存写入
|
||||
DELETE: 8, // 缓存删除
|
||||
},
|
||||
|
||||
// 业务操作阈值(毫秒)
|
||||
BUSINESS: {
|
||||
CREATE_ACCOUNT: 500, // 创建账号
|
||||
VERIFY_ACCOUNT: 200, // 验证账号
|
||||
BATCH_UPDATE: 1000, // 批量更新
|
||||
STATISTICS: 300, // 统计查询
|
||||
},
|
||||
|
||||
// API接口阈值(毫秒)
|
||||
API: {
|
||||
SIMPLE_QUERY: 100, // 简单查询接口
|
||||
COMPLEX_QUERY: 500, // 复杂查询接口
|
||||
CREATE_OPERATION: 800, // 创建操作接口
|
||||
UPDATE_OPERATION: 600, // 更新操作接口
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 性能监控器类
|
||||
*/
|
||||
export class ZulipAccountsPerformanceMonitor {
|
||||
private static instance: ZulipAccountsPerformanceMonitor;
|
||||
private metrics: Map<string, PerformanceMetric[]> = new Map();
|
||||
private stats: Map<string, PerformanceStats> = new Map();
|
||||
private logger: AppLoggerService;
|
||||
|
||||
private constructor(logger: AppLoggerService) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
static getInstance(logger: AppLoggerService): ZulipAccountsPerformanceMonitor {
|
||||
if (!ZulipAccountsPerformanceMonitor.instance) {
|
||||
ZulipAccountsPerformanceMonitor.instance = new ZulipAccountsPerformanceMonitor(logger);
|
||||
}
|
||||
return ZulipAccountsPerformanceMonitor.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建性能监控器
|
||||
*
|
||||
* @param operation 操作名称
|
||||
* @param context 上下文信息
|
||||
* @returns 性能监控器对象
|
||||
*/
|
||||
createMonitor(operation: string, context?: Record<string, any>) {
|
||||
const startTime = Date.now();
|
||||
|
||||
return {
|
||||
/**
|
||||
* 记录成功完成
|
||||
*/
|
||||
success: (additionalContext?: Record<string, any>) => {
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
const metric: PerformanceMetric = {
|
||||
operation,
|
||||
duration,
|
||||
startTime,
|
||||
endTime,
|
||||
success: true,
|
||||
context: { ...context, ...additionalContext },
|
||||
};
|
||||
|
||||
this.recordMetric(metric);
|
||||
this.checkThreshold(metric);
|
||||
},
|
||||
|
||||
/**
|
||||
* 记录失败完成
|
||||
*/
|
||||
error: (error: unknown, additionalContext?: Record<string, any>) => {
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
const metric: PerformanceMetric = {
|
||||
operation,
|
||||
duration,
|
||||
startTime,
|
||||
endTime,
|
||||
success: false,
|
||||
context: { ...context, ...additionalContext },
|
||||
error: errorMessage,
|
||||
};
|
||||
|
||||
this.recordMetric(metric);
|
||||
this.checkThreshold(metric);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录性能指标
|
||||
*
|
||||
* @param metric 性能指标
|
||||
* @private
|
||||
*/
|
||||
private recordMetric(metric: PerformanceMetric): void {
|
||||
// 存储原始指标
|
||||
if (!this.metrics.has(metric.operation)) {
|
||||
this.metrics.set(metric.operation, []);
|
||||
}
|
||||
|
||||
const operationMetrics = this.metrics.get(metric.operation)!;
|
||||
operationMetrics.push(metric);
|
||||
|
||||
// 保持最近1000条记录
|
||||
if (operationMetrics.length > 1000) {
|
||||
operationMetrics.shift();
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
this.updateStats(metric.operation);
|
||||
|
||||
// 记录日志
|
||||
this.logger.debug('性能指标记录', {
|
||||
module: 'ZulipAccountsPerformanceMonitor',
|
||||
operation: 'recordMetric',
|
||||
metric: {
|
||||
operation: metric.operation,
|
||||
duration: metric.duration,
|
||||
success: metric.success,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新统计信息
|
||||
*
|
||||
* @param operation 操作名称
|
||||
* @private
|
||||
*/
|
||||
private updateStats(operation: string): void {
|
||||
const metrics = this.metrics.get(operation) || [];
|
||||
if (metrics.length === 0) return;
|
||||
|
||||
const successMetrics = metrics.filter(m => m.success);
|
||||
const durations = metrics.map(m => m.duration).sort((a, b) => a - b);
|
||||
|
||||
const stats: PerformanceStats = {
|
||||
operation,
|
||||
totalCalls: metrics.length,
|
||||
successCalls: successMetrics.length,
|
||||
failureCalls: metrics.length - successMetrics.length,
|
||||
successRate: (successMetrics.length / metrics.length) * 100,
|
||||
avgDuration: durations.reduce((sum, d) => sum + d, 0) / durations.length,
|
||||
minDuration: durations[0],
|
||||
maxDuration: durations[durations.length - 1],
|
||||
p95Duration: durations[Math.floor(durations.length * 0.95)],
|
||||
p99Duration: durations[Math.floor(durations.length * 0.99)],
|
||||
lastUpdated: new Date(),
|
||||
};
|
||||
|
||||
this.stats.set(operation, stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查性能阈值
|
||||
*
|
||||
* @param metric 性能指标
|
||||
* @private
|
||||
*/
|
||||
private checkThreshold(metric: PerformanceMetric): void {
|
||||
const threshold = this.getThreshold(metric.operation);
|
||||
if (!threshold) return;
|
||||
|
||||
if (metric.duration > threshold) {
|
||||
this.logger.warn('性能阈值超标', {
|
||||
module: 'ZulipAccountsPerformanceMonitor',
|
||||
operation: 'checkThreshold',
|
||||
metric: {
|
||||
operation: metric.operation,
|
||||
duration: metric.duration,
|
||||
threshold,
|
||||
exceeded: metric.duration - threshold,
|
||||
},
|
||||
context: metric.context,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作的性能阈值
|
||||
*
|
||||
* @param operation 操作名称
|
||||
* @returns 阈值(毫秒)或null
|
||||
* @private
|
||||
*/
|
||||
private getThreshold(operation: string): number | null {
|
||||
// 根据操作名称匹配阈值
|
||||
if (operation.includes('query') || operation.includes('find')) {
|
||||
if (operation.includes('batch') || operation.includes('many')) {
|
||||
return PERFORMANCE_THRESHOLDS.DATABASE.QUERY_BATCH;
|
||||
}
|
||||
return PERFORMANCE_THRESHOLDS.DATABASE.QUERY_SINGLE;
|
||||
}
|
||||
|
||||
if (operation.includes('create')) {
|
||||
return PERFORMANCE_THRESHOLDS.DATABASE.INSERT;
|
||||
}
|
||||
|
||||
if (operation.includes('update')) {
|
||||
return PERFORMANCE_THRESHOLDS.DATABASE.UPDATE;
|
||||
}
|
||||
|
||||
if (operation.includes('delete')) {
|
||||
return PERFORMANCE_THRESHOLDS.DATABASE.DELETE;
|
||||
}
|
||||
|
||||
if (operation.includes('transaction')) {
|
||||
return PERFORMANCE_THRESHOLDS.DATABASE.TRANSACTION;
|
||||
}
|
||||
|
||||
if (operation.includes('cache')) {
|
||||
return PERFORMANCE_THRESHOLDS.CACHE.GET;
|
||||
}
|
||||
|
||||
if (operation.includes('statistics')) {
|
||||
return PERFORMANCE_THRESHOLDS.BUSINESS.STATISTICS;
|
||||
}
|
||||
|
||||
// 默认阈值
|
||||
return 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作的统计信息
|
||||
*
|
||||
* @param operation 操作名称
|
||||
* @returns 统计信息或null
|
||||
*/
|
||||
getStats(operation: string): PerformanceStats | null {
|
||||
return this.stats.get(operation) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有统计信息
|
||||
*
|
||||
* @returns 所有统计信息
|
||||
*/
|
||||
getAllStats(): PerformanceStats[] {
|
||||
return Array.from(this.stats.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能报告
|
||||
*
|
||||
* @returns 性能报告
|
||||
*/
|
||||
getPerformanceReport(): {
|
||||
summary: {
|
||||
totalOperations: number;
|
||||
avgSuccessRate: number;
|
||||
slowestOperations: Array<{ operation: string; avgDuration: number }>;
|
||||
};
|
||||
details: PerformanceStats[];
|
||||
} {
|
||||
const allStats = this.getAllStats();
|
||||
|
||||
const summary = {
|
||||
totalOperations: allStats.length,
|
||||
avgSuccessRate: allStats.reduce((sum, s) => sum + s.successRate, 0) / allStats.length || 0,
|
||||
slowestOperations: allStats
|
||||
.sort((a, b) => b.avgDuration - a.avgDuration)
|
||||
.slice(0, 5)
|
||||
.map(s => ({ operation: s.operation, avgDuration: s.avgDuration })),
|
||||
};
|
||||
|
||||
return {
|
||||
summary,
|
||||
details: allStats,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除历史数据
|
||||
*
|
||||
* @param operation 操作名称(可选,不提供则清除所有)
|
||||
*/
|
||||
clearHistory(operation?: string): void {
|
||||
if (operation) {
|
||||
this.metrics.delete(operation);
|
||||
this.stats.delete(operation);
|
||||
} else {
|
||||
this.metrics.clear();
|
||||
this.stats.clear();
|
||||
}
|
||||
|
||||
this.logger.info('性能监控历史数据已清除', {
|
||||
module: 'ZulipAccountsPerformanceMonitor',
|
||||
operation: 'clearHistory',
|
||||
clearedOperation: operation || 'all',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能监控装饰器
|
||||
*
|
||||
* @param operation 操作名称
|
||||
* @returns 方法装饰器
|
||||
*/
|
||||
export function PerformanceMonitor(operation: string) {
|
||||
return function (_target: any, propertyName: string, descriptor: PropertyDescriptor) {
|
||||
const method = descriptor.value;
|
||||
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const logger = (this as any).logger as AppLoggerService;
|
||||
if (!logger) {
|
||||
// 如果没有logger,直接执行原方法
|
||||
return method.apply(this, args);
|
||||
}
|
||||
|
||||
const monitor = ZulipAccountsPerformanceMonitor
|
||||
.getInstance(logger)
|
||||
.createMonitor(operation, { method: propertyName });
|
||||
|
||||
try {
|
||||
const result = await method.apply(this, args);
|
||||
monitor.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
monitor.error(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
623
src/core/db/zulip_accounts/zulip_accounts.repository.ts
Normal file
623
src/core/db/zulip_accounts/zulip_accounts.repository.ts
Normal file
@@ -0,0 +1,623 @@
|
||||
/**
|
||||
* Zulip账号关联数据访问层
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供Zulip账号关联数据的CRUD操作
|
||||
* - 封装复杂查询逻辑和数据库交互
|
||||
* - 实现数据访问层的业务逻辑抽象
|
||||
* - 支持事务操作确保数据一致性
|
||||
* - 优化查询性能和批量操作效率
|
||||
* - 集成AppLoggerService提供结构化日志
|
||||
*
|
||||
* 职责分离:
|
||||
* - 数据访问:负责所有数据库操作和查询
|
||||
* - 事务管理:处理需要原子性的复合操作
|
||||
* - 查询优化:提供高效的数据库查询方法
|
||||
* - 性能监控:记录查询耗时和性能指标
|
||||
* - 并发控制:使用悲观锁防止竞态条件
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-15: 代码规范优化 - 清理未使用的导入FindOptionsWhere (修改者: moyin)
|
||||
* - 2026-01-12: 性能优化 - 集成AppLoggerService,优化查询和批量操作
|
||||
* - 2026-01-07: 代码规范优化 - 使用统一的常量文件,提高代码质量
|
||||
* - 2026-01-07: 代码规范优化 - 完善文件头注释和方法三级注释
|
||||
* - 2026-01-07: 功能新增 - 添加事务支持防止并发竞态条件
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.2.1
|
||||
* @since 2025-01-05
|
||||
* @lastModified 2026-01-15
|
||||
*/
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource, SelectQueryBuilder } from 'typeorm';
|
||||
import { ZulipAccounts } from './zulip_accounts.entity';
|
||||
import { AppLoggerService } from '../../utils/logger/logger.service';
|
||||
import {
|
||||
DEFAULT_VERIFICATION_INTERVAL,
|
||||
DEFAULT_MAX_RETRY_COUNT,
|
||||
VERIFICATION_QUERY_LIMIT,
|
||||
ERROR_ACCOUNTS_QUERY_LIMIT,
|
||||
} from './zulip_accounts.constants';
|
||||
import {
|
||||
CreateZulipAccountData,
|
||||
UpdateZulipAccountData,
|
||||
ZulipAccountQueryOptions,
|
||||
StatusStatistics,
|
||||
IZulipAccountsRepository,
|
||||
} from './zulip_accounts.types';
|
||||
|
||||
// 保持向后兼容的类型别名
|
||||
export type CreateZulipAccountDto = CreateZulipAccountData;
|
||||
export type UpdateZulipAccountDto = UpdateZulipAccountData;
|
||||
export { ZulipAccountQueryOptions };
|
||||
|
||||
@Injectable()
|
||||
export class ZulipAccountsRepository implements IZulipAccountsRepository {
|
||||
private readonly logger: AppLoggerService;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ZulipAccounts)
|
||||
private readonly repository: Repository<ZulipAccounts>,
|
||||
private readonly dataSource: DataSource,
|
||||
@Inject(AppLoggerService) logger: AppLoggerService,
|
||||
) {
|
||||
this.logger = logger;
|
||||
this.logger.info('ZulipAccountsRepository初始化完成', {
|
||||
module: 'ZulipAccountsRepository',
|
||||
operation: 'constructor'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新的Zulip账号关联(带事务支持和性能监控)
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 开启数据库事务确保原子性
|
||||
* 2. 使用悲观锁检查游戏用户ID是否已存在关联
|
||||
* 3. 检查Zulip用户ID是否已被使用
|
||||
* 4. 检查Zulip邮箱是否已被使用
|
||||
* 5. 创建新的关联记录并保存
|
||||
* 6. 记录操作日志和性能指标
|
||||
* 7. 提交事务或回滚
|
||||
*
|
||||
* @param createDto 创建数据
|
||||
* @returns Promise<ZulipAccounts> 创建的关联记录
|
||||
* @throws Error 当唯一性约束冲突时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = await repository.create({
|
||||
* gameUserId: BigInt(12345),
|
||||
* zulipUserId: 67890,
|
||||
* zulipEmail: 'user@example.com',
|
||||
* zulipFullName: '用户名',
|
||||
* zulipApiKeyEncrypted: 'encrypted_key'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
async create(createDto: CreateZulipAccountDto): Promise<ZulipAccounts> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logger.info('开始创建Zulip账号关联', {
|
||||
module: 'ZulipAccountsRepository',
|
||||
operation: 'create',
|
||||
gameUserId: createDto.gameUserId.toString(),
|
||||
zulipUserId: createDto.zulipUserId,
|
||||
zulipEmail: createDto.zulipEmail
|
||||
});
|
||||
|
||||
return await this.dataSource.transaction(async manager => {
|
||||
try {
|
||||
// 使用悲观锁在事务中检查唯一性约束
|
||||
const existingByGameUser = await manager
|
||||
.createQueryBuilder(ZulipAccounts, 'za')
|
||||
.where('za.gameUserId = :gameUserId', { gameUserId: createDto.gameUserId })
|
||||
.setLock('pessimistic_write')
|
||||
.getOne();
|
||||
|
||||
if (existingByGameUser) {
|
||||
throw new Error(`Game user ${createDto.gameUserId} already has a Zulip account`);
|
||||
}
|
||||
|
||||
const existingByZulipUser = await manager
|
||||
.createQueryBuilder(ZulipAccounts, 'za')
|
||||
.where('za.zulipUserId = :zulipUserId', { zulipUserId: createDto.zulipUserId })
|
||||
.setLock('pessimistic_write')
|
||||
.getOne();
|
||||
|
||||
if (existingByZulipUser) {
|
||||
throw new Error(`Zulip user ${createDto.zulipUserId} is already linked`);
|
||||
}
|
||||
|
||||
const existingByEmail = await manager
|
||||
.createQueryBuilder(ZulipAccounts, 'za')
|
||||
.where('za.zulipEmail = :zulipEmail', { zulipEmail: createDto.zulipEmail })
|
||||
.setLock('pessimistic_write')
|
||||
.getOne();
|
||||
|
||||
if (existingByEmail) {
|
||||
throw new Error(`Zulip email ${createDto.zulipEmail} is already linked`);
|
||||
}
|
||||
|
||||
// 创建实体
|
||||
const zulipAccount = manager.create(ZulipAccounts, createDto);
|
||||
const result = await manager.save(zulipAccount);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
this.logger.info('创建Zulip账号关联成功', {
|
||||
module: 'ZulipAccountsRepository',
|
||||
operation: 'create',
|
||||
gameUserId: createDto.gameUserId.toString(),
|
||||
accountId: result.id.toString(),
|
||||
duration
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
this.logger.error('创建Zulip账号关联失败', {
|
||||
module: 'ZulipAccountsRepository',
|
||||
operation: 'create',
|
||||
gameUserId: createDto.gameUserId.toString(),
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration
|
||||
}, error instanceof Error ? error.stack : undefined);
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID查找Zulip账号关联
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 根据includeGameUser参数决定是否加载关联的游戏用户信息
|
||||
* 2. 构建查询条件,使用gameUserId作为查询键
|
||||
* 3. 执行数据库查询,返回匹配的记录或null
|
||||
* 4. 如果需要关联信息,通过relations参数加载
|
||||
*
|
||||
* @param gameUserId 游戏用户ID,BigInt类型
|
||||
* @param includeGameUser 是否包含游戏用户信息,默认false
|
||||
* @returns Promise<ZulipAccounts | null> 关联记录或null
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = await repository.findByGameUserId(BigInt(12345), true);
|
||||
* if (account) {
|
||||
* console.log('用户邮箱:', account.zulipEmail);
|
||||
* console.log('游戏用户:', account.gameUser?.username);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async findByGameUserId(gameUserId: bigint, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
|
||||
const relations = includeGameUser ? ['gameUser'] : [];
|
||||
|
||||
return await this.repository.findOne({
|
||||
where: { gameUserId },
|
||||
relations,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Zulip用户ID查找账号关联
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 根据includeGameUser参数决定是否加载关联的游戏用户信息
|
||||
* 2. 构建查询条件,使用zulipUserId作为查询键
|
||||
* 3. 执行数据库查询,返回匹配的记录或null
|
||||
* 4. 如果需要关联信息,通过relations参数加载
|
||||
*
|
||||
* @param zulipUserId Zulip用户ID,数字类型
|
||||
* @param includeGameUser 是否包含游戏用户信息,默认false
|
||||
* @returns Promise<ZulipAccounts | null> 关联记录或null
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = await repository.findByZulipUserId(67890, false);
|
||||
* if (account) {
|
||||
* console.log('关联的游戏用户ID:', account.gameUserId.toString());
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
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> {
|
||||
const result = await this.repository.update({ id }, updateDto);
|
||||
if (result.affected === 0) {
|
||||
return null;
|
||||
}
|
||||
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> {
|
||||
const result = await this.repository.update({ gameUserId }, updateDto);
|
||||
if (result.affected === 0) {
|
||||
return null;
|
||||
}
|
||||
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账号关联(优化版本)
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 构建基础查询构建器
|
||||
* 2. 根据查询选项动态添加WHERE条件
|
||||
* 3. 支持关联查询和分页
|
||||
* 4. 使用索引优化查询性能
|
||||
* 5. 记录查询日志和性能指标
|
||||
*
|
||||
* @param options 查询选项
|
||||
* @returns Promise<ZulipAccounts[]> 关联记录列表
|
||||
*/
|
||||
async findMany(options: ZulipAccountQueryOptions = {}): Promise<ZulipAccounts[]> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logger.debug('开始查询多个Zulip账号关联', {
|
||||
module: 'ZulipAccountsRepository',
|
||||
operation: 'findMany',
|
||||
options
|
||||
});
|
||||
|
||||
try {
|
||||
const queryBuilder = this.createBaseQueryBuilder('za');
|
||||
|
||||
// 动态添加WHERE条件
|
||||
this.applyQueryConditions(queryBuilder, options);
|
||||
|
||||
// 处理关联查询
|
||||
if (options.includeGameUser) {
|
||||
queryBuilder.leftJoinAndSelect('za.gameUser', 'user');
|
||||
}
|
||||
|
||||
// 添加排序和分页
|
||||
queryBuilder
|
||||
.orderBy('za.createdAt', 'DESC')
|
||||
.addOrderBy('za.id', 'DESC'); // 添加第二排序字段确保结果稳定
|
||||
|
||||
// 如果有分页需求,可以在这里添加
|
||||
// if (options.limit) queryBuilder.limit(options.limit);
|
||||
// if (options.offset) queryBuilder.offset(options.offset);
|
||||
|
||||
const results = await queryBuilder.getMany();
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
this.logger.debug('查询多个Zulip账号关联完成', {
|
||||
module: 'ZulipAccountsRepository',
|
||||
operation: 'findMany',
|
||||
resultCount: results.length,
|
||||
duration
|
||||
});
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
this.logger.error('查询多个Zulip账号关联失败', {
|
||||
module: 'ZulipAccountsRepository',
|
||||
operation: 'findMany',
|
||||
options,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration
|
||||
}, error instanceof Error ? error.stack : undefined);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取需要验证的账号列表(优化查询)
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 计算验证截止时间(当前时间减去最大验证间隔)
|
||||
* 2. 查询状态为active的账号
|
||||
* 3. 筛选从未验证或验证时间超期的账号
|
||||
* 4. 按验证时间升序排序,NULL值优先
|
||||
* 5. 限制查询数量避免性能问题
|
||||
*
|
||||
* @param maxAge 最大验证间隔(毫秒),默认24小时
|
||||
* @returns Promise<ZulipAccounts[]> 需要验证的账号列表
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const accounts = await repository.findAccountsNeedingVerification();
|
||||
* console.log(`需要验证的账号数量: ${accounts.length}`);
|
||||
* ```
|
||||
*/
|
||||
async findAccountsNeedingVerification(maxAge: number = DEFAULT_VERIFICATION_INTERVAL): Promise<ZulipAccounts[]> {
|
||||
const cutoffTime = new Date(Date.now() - maxAge);
|
||||
|
||||
return await this.repository
|
||||
.createQueryBuilder('za')
|
||||
.where('za.status = :status', { status: 'active' })
|
||||
.andWhere(
|
||||
'(za.last_verified_at IS NULL OR za.last_verified_at < :cutoffTime)',
|
||||
{ cutoffTime }
|
||||
)
|
||||
.orderBy('za.last_verified_at', 'ASC', 'NULLS FIRST')
|
||||
.limit(VERIFICATION_QUERY_LIMIT) // 限制查询数量,避免性能问题
|
||||
.getMany();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误状态的账号列表(可重试的)
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 查询状态为error的账号
|
||||
* 2. 筛选重试次数小于最大重试次数的账号
|
||||
* 3. 按更新时间升序排序,优先处理较早的错误
|
||||
* 4. 限制查询数量避免性能问题
|
||||
*
|
||||
* @param maxRetryCount 最大重试次数,默认3次
|
||||
* @returns Promise<ZulipAccounts[]> 错误状态的账号列表
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const errorAccounts = await repository.findErrorAccounts(5);
|
||||
* console.log(`可重试的错误账号: ${errorAccounts.length}`);
|
||||
* ```
|
||||
*/
|
||||
async findErrorAccounts(maxRetryCount: number = DEFAULT_MAX_RETRY_COUNT): Promise<ZulipAccounts[]> {
|
||||
return await this.repository
|
||||
.createQueryBuilder('za')
|
||||
.where('za.status = :status', { status: 'error' })
|
||||
.andWhere('za.retry_count < :maxRetryCount', { maxRetryCount })
|
||||
.orderBy('za.updated_at', 'ASC')
|
||||
.limit(ERROR_ACCOUNTS_QUERY_LIMIT) // 限制查询数量
|
||||
.getMany();
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新账号状态
|
||||
*
|
||||
* @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<StatusStatistics> 状态统计
|
||||
*/
|
||||
async getStatusStatistics(): Promise<StatusStatistics> {
|
||||
const result = await this.repository
|
||||
.createQueryBuilder('za')
|
||||
.select('za.status', 'status')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.groupBy('za.status')
|
||||
.getRawMany();
|
||||
|
||||
const statistics: StatusStatistics = {
|
||||
active: 0,
|
||||
inactive: 0,
|
||||
suspended: 0,
|
||||
error: 0,
|
||||
};
|
||||
|
||||
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('za')
|
||||
.where('za.zulip_email = :zulipEmail', { zulipEmail });
|
||||
|
||||
if (excludeId) {
|
||||
queryBuilder.andWhere('za.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('za')
|
||||
.where('za.zulip_user_id = :zulipUserId', { zulipUserId });
|
||||
|
||||
if (excludeId) {
|
||||
queryBuilder.andWhere('za.id != :excludeId', { excludeId });
|
||||
}
|
||||
|
||||
const count = await queryBuilder.getCount();
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查游戏用户ID是否已存在
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @param excludeId 排除的记录ID(用于更新时检查)
|
||||
* @returns Promise<boolean> 是否已存在
|
||||
*/
|
||||
async existsByGameUserId(gameUserId: bigint, excludeId?: bigint): Promise<boolean> {
|
||||
const queryBuilder = this.repository
|
||||
.createQueryBuilder('za')
|
||||
.where('za.game_user_id = :gameUserId', { gameUserId });
|
||||
|
||||
if (excludeId) {
|
||||
queryBuilder.andWhere('za.id != :excludeId', { excludeId });
|
||||
}
|
||||
|
||||
const count = await queryBuilder.getCount();
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
// ========== 辅助方法 ==========
|
||||
|
||||
/**
|
||||
* 创建基础查询构建器
|
||||
*
|
||||
* @param alias 表别名
|
||||
* @returns SelectQueryBuilder<ZulipAccounts>
|
||||
* @private
|
||||
*/
|
||||
private createBaseQueryBuilder(alias: string = 'za'): SelectQueryBuilder<ZulipAccounts> {
|
||||
return this.repository.createQueryBuilder(alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用查询条件
|
||||
*
|
||||
* @param queryBuilder 查询构建器
|
||||
* @param options 查询选项
|
||||
* @private
|
||||
*/
|
||||
private applyQueryConditions(
|
||||
queryBuilder: SelectQueryBuilder<ZulipAccounts>,
|
||||
options: ZulipAccountQueryOptions
|
||||
): void {
|
||||
if (options.gameUserId) {
|
||||
queryBuilder.andWhere('za.gameUserId = :gameUserId', { gameUserId: options.gameUserId });
|
||||
}
|
||||
|
||||
if (options.zulipUserId) {
|
||||
queryBuilder.andWhere('za.zulipUserId = :zulipUserId', { zulipUserId: options.zulipUserId });
|
||||
}
|
||||
|
||||
if (options.zulipEmail) {
|
||||
queryBuilder.andWhere('za.zulipEmail = :zulipEmail', { zulipEmail: options.zulipEmail });
|
||||
}
|
||||
|
||||
if (options.status) {
|
||||
queryBuilder.andWhere('za.status = :status', { status: options.status });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录查询性能指标
|
||||
*
|
||||
* @param operation 操作名称
|
||||
* @param startTime 开始时间
|
||||
* @param resultCount 结果数量
|
||||
* @private
|
||||
*/
|
||||
private logQueryPerformance(operation: string, startTime: number, resultCount?: number): void {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.debug('查询性能指标', {
|
||||
module: 'ZulipAccountsRepository',
|
||||
operation,
|
||||
duration,
|
||||
resultCount,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 如果查询时间超过阈值,记录警告
|
||||
if (duration > 1000) { // 1秒阈值
|
||||
this.logger.warn('查询耗时过长', {
|
||||
module: 'ZulipAccountsRepository',
|
||||
operation,
|
||||
duration,
|
||||
resultCount,
|
||||
threshold: 1000
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
862
src/core/db/zulip_accounts/zulip_accounts.service.ts
Normal file
862
src/core/db/zulip_accounts/zulip_accounts.service.ts
Normal file
@@ -0,0 +1,862 @@
|
||||
/**
|
||||
* Zulip账号关联服务(数据库版本)
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供Zulip账号关联的数据访问服务
|
||||
* - 封装Repository层的数据操作
|
||||
* - 提供基础的CRUD操作接口
|
||||
* - 支持缓存机制提升查询性能
|
||||
*
|
||||
* 职责分离:
|
||||
* - 数据访问:封装Repository层的数据操作
|
||||
* - 缓存管理:管理数据缓存策略
|
||||
* - DTO转换:实体对象与响应DTO之间的转换
|
||||
* - 日志记录:记录数据访问操作日志
|
||||
*
|
||||
* 注意:业务逻辑已转移到 src/core/zulip_core/services/zulip_accounts_business.service.ts
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-15: 代码规范优化 - 清理未使用的导入NotFoundException (修改者: moyin)
|
||||
* - 2026-01-12: 代码规范优化 - 修复依赖注入配置,添加@Inject装饰器确保正确的参数注入 (修改者: moyin)
|
||||
* - 2026-01-12: 功能修改 - 优化create方法错误处理,正确转换重复创建错误为ConflictException (修改者: moyin)
|
||||
* - 2026-01-12: 架构优化 - 移除业务逻辑,转移到zulip_core业务服务 (修改者: moyin)
|
||||
* - 2026-01-12: 代码质量优化 - 清理重复导入,统一使用@Inject装饰器 (修改者: moyin)
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 2.1.1
|
||||
* @since 2025-01-07
|
||||
* @lastModified 2026-01-15
|
||||
*/
|
||||
|
||||
import { Injectable, Inject, ConflictException } from '@nestjs/common';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { BaseZulipAccountsService } from './base_zulip_accounts.service';
|
||||
import { ZulipAccountsRepository } from './zulip_accounts.repository';
|
||||
import { ZulipAccounts } from './zulip_accounts.entity';
|
||||
import { AppLoggerService } from '../../utils/logger/logger.service';
|
||||
import {
|
||||
DEFAULT_VERIFICATION_MAX_AGE,
|
||||
DEFAULT_MAX_RETRY_COUNT,
|
||||
} from './zulip_accounts.constants';
|
||||
import {
|
||||
CreateZulipAccountDto,
|
||||
UpdateZulipAccountDto,
|
||||
QueryZulipAccountDto,
|
||||
ZulipAccountResponseDto,
|
||||
ZulipAccountListResponseDto,
|
||||
ZulipAccountStatsResponseDto,
|
||||
BatchUpdateResponseDto,
|
||||
VerifyAccountResponseDto,
|
||||
} from './zulip_accounts.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ZulipAccountsService extends BaseZulipAccountsService {
|
||||
// 缓存键前缀
|
||||
private static readonly CACHE_PREFIX = 'zulip_accounts';
|
||||
private static readonly CACHE_TTL = 300; // 5分钟缓存
|
||||
private static readonly STATS_CACHE_TTL = 60; // 统计数据1分钟缓存
|
||||
|
||||
constructor(
|
||||
@Inject(ZulipAccountsRepository) private readonly repository: ZulipAccountsRepository,
|
||||
@Inject(AppLoggerService) logger: AppLoggerService,
|
||||
@Inject(CACHE_MANAGER) private readonly cacheManager: any,
|
||||
) {
|
||||
super(logger, 'ZulipAccountsService');
|
||||
this.logger.info('ZulipAccountsService初始化完成', {
|
||||
module: 'ZulipAccountsService',
|
||||
operation: 'constructor',
|
||||
cacheEnabled: !!this.cacheManager
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Zulip账号关联
|
||||
*
|
||||
* 数据访问逻辑:
|
||||
* 1. 接收创建请求数据
|
||||
* 2. 将字符串类型的gameUserId转换为BigInt类型
|
||||
* 3. 调用Repository层创建账号关联记录
|
||||
* 4. 清除相关缓存确保数据一致性
|
||||
* 5. 将实体对象转换为响应DTO返回
|
||||
*
|
||||
* @param createDto 创建数据,包含游戏用户ID、Zulip用户信息等
|
||||
* @returns Promise<ZulipAccountResponseDto> 创建的关联记录DTO
|
||||
* @throws 数据访问异常
|
||||
*/
|
||||
async create(createDto: CreateZulipAccountDto): Promise<ZulipAccountResponseDto> {
|
||||
const monitor = this.createPerformanceMonitor('创建Zulip账号关联', {
|
||||
gameUserId: createDto.gameUserId
|
||||
});
|
||||
|
||||
try {
|
||||
const account = await this.repository.create({
|
||||
gameUserId: this.parseGameUserId(createDto.gameUserId),
|
||||
zulipUserId: createDto.zulipUserId,
|
||||
zulipEmail: createDto.zulipEmail,
|
||||
zulipFullName: createDto.zulipFullName,
|
||||
zulipApiKeyEncrypted: createDto.zulipApiKeyEncrypted,
|
||||
status: createDto.status || 'active',
|
||||
});
|
||||
|
||||
// 清除相关缓存
|
||||
await this.clearRelatedCache(createDto.gameUserId, createDto.zulipUserId, createDto.zulipEmail);
|
||||
|
||||
const result = this.toResponseDto(account);
|
||||
monitor.success({
|
||||
accountId: account.id.toString(),
|
||||
status: account.status
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
// 检查是否是重复创建错误,转换为ConflictException
|
||||
const errorMessage = this.formatError(error);
|
||||
if (errorMessage.includes('already has a Zulip account') ||
|
||||
errorMessage.includes('duplicate') ||
|
||||
errorMessage.includes('unique constraint')) {
|
||||
const conflictError = new ConflictException(`游戏用户 ${createDto.gameUserId} 已存在Zulip账号关联`);
|
||||
monitor.error(conflictError);
|
||||
throw conflictError;
|
||||
} else {
|
||||
monitor.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID查找关联(带缓存)
|
||||
*
|
||||
* 数据访问逻辑:
|
||||
* 1. 构建缓存键并尝试从缓存获取数据
|
||||
* 2. 如果缓存命中,记录日志并返回缓存数据
|
||||
* 3. 如果缓存未命中,从数据库查询数据
|
||||
* 4. 将查询结果存入缓存,设置合适的TTL
|
||||
* 5. 记录查询日志和性能指标
|
||||
* 6. 将实体对象转换为响应DTO返回
|
||||
*
|
||||
* @param gameUserId 游戏用户ID,字符串格式
|
||||
* @param includeGameUser 是否包含游戏用户信息,默认false
|
||||
* @returns Promise<ZulipAccountResponseDto | null> 关联记录DTO或null
|
||||
* @throws BadRequestException 当查询参数无效或系统异常时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = await service.findByGameUserId('12345', true);
|
||||
* if (account) {
|
||||
* console.log('找到关联:', account.zulipEmail);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async findByGameUserId(gameUserId: string, includeGameUser: boolean = false): Promise<ZulipAccountResponseDto | null> {
|
||||
const cacheKey = this.buildCacheKey('game_user', gameUserId, includeGameUser);
|
||||
|
||||
try {
|
||||
// 尝试从缓存获取
|
||||
const cached = await this.cacheManager.get(cacheKey) as ZulipAccountResponseDto;
|
||||
if (cached) {
|
||||
this.logger.debug('缓存命中', {
|
||||
module: this.moduleName,
|
||||
operation: 'findByGameUserId',
|
||||
gameUserId,
|
||||
cacheKey
|
||||
});
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 缓存未命中,从数据库查询
|
||||
const monitor = this.createPerformanceMonitor('根据游戏用户ID查找关联', { gameUserId });
|
||||
|
||||
const account = await this.repository.findByGameUserId(this.parseGameUserId(gameUserId), includeGameUser);
|
||||
|
||||
if (!account) {
|
||||
this.logger.debug('未找到Zulip账号关联', {
|
||||
module: this.moduleName,
|
||||
operation: 'findByGameUserId',
|
||||
gameUserId
|
||||
});
|
||||
monitor.success({ found: false });
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = this.toResponseDto(account);
|
||||
|
||||
// 存入缓存
|
||||
await this.cacheManager.set(cacheKey, result, ZulipAccountsService.CACHE_TTL);
|
||||
|
||||
monitor.success({ found: true, cached: true });
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
this.handleDataAccessError(error, '根据游戏用户ID查找关联', { gameUserId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Zulip用户ID查找关联
|
||||
*
|
||||
* 数据访问逻辑:
|
||||
* 1. 记录查询操作开始日志
|
||||
* 2. 调用Repository层根据Zulip用户ID查找记录
|
||||
* 3. 如果未找到记录,记录调试日志并返回null
|
||||
* 4. 如果找到记录,记录成功日志
|
||||
* 5. 将实体对象转换为响应DTO返回
|
||||
* 6. 捕获异常并进行统一的错误处理
|
||||
*
|
||||
* @param zulipUserId Zulip用户ID,数字类型
|
||||
* @param includeGameUser 是否包含游戏用户信息,默认false
|
||||
* @returns Promise<ZulipAccountResponseDto | null> 关联记录DTO或null
|
||||
* @throws BadRequestException 当查询参数无效或系统异常时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = await service.findByZulipUserId(67890);
|
||||
* if (account) {
|
||||
* console.log('关联的游戏用户:', account.gameUserId);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async findByZulipUserId(zulipUserId: number, includeGameUser: boolean = false): Promise<ZulipAccountResponseDto | null> {
|
||||
this.logStart('根据Zulip用户ID查找关联', { zulipUserId });
|
||||
|
||||
try {
|
||||
const account = await this.repository.findByZulipUserId(zulipUserId, includeGameUser);
|
||||
|
||||
if (!account) {
|
||||
this.logger.debug('未找到Zulip账号关联', { zulipUserId });
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logSuccess('根据Zulip用户ID查找关联', { zulipUserId, found: true });
|
||||
return this.toResponseDto(account);
|
||||
|
||||
} catch (error) {
|
||||
this.handleDataAccessError(error, '根据Zulip用户ID查找关联', { zulipUserId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Zulip邮箱查找关联
|
||||
*
|
||||
* 数据访问逻辑:
|
||||
* 1. 记录查询操作开始日志
|
||||
* 2. 调用Repository层根据Zulip邮箱查找记录
|
||||
* 3. 如果未找到记录,记录调试日志并返回null
|
||||
* 4. 如果找到记录,记录成功日志
|
||||
* 5. 将实体对象转换为响应DTO返回
|
||||
* 6. 捕获异常并进行统一的错误处理
|
||||
*
|
||||
* @param zulipEmail Zulip邮箱地址,字符串格式
|
||||
* @param includeGameUser 是否包含游戏用户信息,默认false
|
||||
* @returns Promise<ZulipAccountResponseDto | null> 关联记录DTO或null
|
||||
* @throws BadRequestException 当查询参数无效或系统异常时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = await service.findByZulipEmail('user@example.com');
|
||||
* if (account) {
|
||||
* console.log('邮箱对应的用户:', account.zulipFullName);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async findByZulipEmail(zulipEmail: string, includeGameUser: boolean = false): Promise<ZulipAccountResponseDto | null> {
|
||||
this.logStart('根据Zulip邮箱查找关联', { zulipEmail });
|
||||
|
||||
try {
|
||||
const account = await this.repository.findByZulipEmail(zulipEmail, includeGameUser);
|
||||
|
||||
if (!account) {
|
||||
this.logger.debug('未找到Zulip账号关联', { zulipEmail });
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logSuccess('根据Zulip邮箱查找关联', { zulipEmail, found: true });
|
||||
return this.toResponseDto(account);
|
||||
|
||||
} catch (error) {
|
||||
this.handleDataAccessError(error, '根据Zulip邮箱查找关联', { zulipEmail });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查找关联
|
||||
*
|
||||
* 数据访问逻辑:
|
||||
* 1. 记录查询操作开始日志
|
||||
* 2. 将字符串类型的ID转换为BigInt类型
|
||||
* 3. 调用Repository层根据ID查找记录
|
||||
* 4. 如果未找到记录,抛出NotFoundException异常
|
||||
* 5. 如果找到记录,记录成功日志
|
||||
* 6. 将实体对象转换为响应DTO返回
|
||||
* 7. 捕获异常并进行统一的错误处理
|
||||
*
|
||||
* @param id 关联记录ID,字符串格式
|
||||
* @param includeGameUser 是否包含游戏用户信息,默认false
|
||||
* @returns Promise<ZulipAccountResponseDto> 关联记录DTO
|
||||
* @throws NotFoundException 当记录不存在时
|
||||
* @throws BadRequestException 当查询参数无效或系统异常时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = await service.findById('123', true);
|
||||
* console.log('找到记录:', account.zulipEmail);
|
||||
* ```
|
||||
*/
|
||||
async findById(id: string, includeGameUser: boolean = false): Promise<ZulipAccountResponseDto> {
|
||||
const monitor = this.createPerformanceMonitor('根据ID查找关联', { id });
|
||||
|
||||
try {
|
||||
const account = await this.repository.findById(this.parseId(id), includeGameUser);
|
||||
|
||||
const result = account ? this.toResponseDto(account) : null;
|
||||
monitor.success({ found: !!account });
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
monitor.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Zulip账号关联
|
||||
*
|
||||
* 数据访问逻辑:
|
||||
* 1. 记录更新操作开始时间和日志
|
||||
* 2. 将字符串类型的ID转换为BigInt类型
|
||||
* 3. 调用Repository层执行更新操作
|
||||
* 4. 如果记录不存在,抛出NotFoundException异常
|
||||
* 5. 记录操作成功日志和耗时
|
||||
* 6. 将更新后的实体转换为响应DTO返回
|
||||
* 7. 捕获异常并进行统一的错误处理
|
||||
*
|
||||
* @param id 关联记录ID,字符串格式
|
||||
* @param updateDto 更新数据,包含需要修改的字段
|
||||
* @returns Promise<ZulipAccountResponseDto> 更新后的记录DTO
|
||||
* @throws NotFoundException 当记录不存在时
|
||||
* @throws BadRequestException 当更新数据无效或系统异常时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const updated = await service.update('123', {
|
||||
* zulipFullName: '新用户名',
|
||||
* status: 'active'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
async update(id: string, updateDto: UpdateZulipAccountDto): Promise<ZulipAccountResponseDto> {
|
||||
const monitor = this.createPerformanceMonitor('更新Zulip账号关联', { id });
|
||||
|
||||
try {
|
||||
const account = await this.repository.update(this.parseId(id), updateDto);
|
||||
|
||||
const result = account ? this.toResponseDto(account) : null;
|
||||
monitor.success({ updated: !!account });
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
monitor.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID更新关联
|
||||
*
|
||||
* 数据访问逻辑:
|
||||
* 1. 记录更新操作开始时间和日志
|
||||
* 2. 将字符串类型的gameUserId转换为BigInt类型
|
||||
* 3. 调用Repository层根据游戏用户ID执行更新
|
||||
* 4. 如果记录不存在,抛出NotFoundException异常
|
||||
* 5. 记录操作成功日志和耗时
|
||||
* 6. 将更新后的实体转换为响应DTO返回
|
||||
* 7. 捕获异常并进行统一的错误处理
|
||||
*
|
||||
* @param gameUserId 游戏用户ID,字符串格式
|
||||
* @param updateDto 更新数据,包含需要修改的字段
|
||||
* @returns Promise<ZulipAccountResponseDto> 更新后的记录DTO
|
||||
* @throws NotFoundException 当记录不存在时
|
||||
* @throws BadRequestException 当更新数据无效或系统异常时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const updated = await service.updateByGameUserId('12345', {
|
||||
* status: 'suspended',
|
||||
* errorMessage: '账号异常'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
async updateByGameUserId(gameUserId: string, updateDto: UpdateZulipAccountDto): Promise<ZulipAccountResponseDto> {
|
||||
const monitor = this.createPerformanceMonitor('根据游戏用户ID更新关联', { gameUserId });
|
||||
|
||||
try {
|
||||
const account = await this.repository.updateByGameUserId(this.parseGameUserId(gameUserId), updateDto);
|
||||
|
||||
const result = account ? this.toResponseDto(account) : null;
|
||||
monitor.success({ updated: !!account });
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
monitor.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Zulip账号关联
|
||||
*
|
||||
* @param id 关联记录ID
|
||||
* @returns Promise<boolean> 是否删除成功
|
||||
*/
|
||||
async delete(id: string): Promise<boolean> {
|
||||
const monitor = this.createPerformanceMonitor('删除Zulip账号关联', { id });
|
||||
|
||||
try {
|
||||
const result = await this.repository.delete(this.parseId(id));
|
||||
|
||||
monitor.success({ deleted: result });
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
monitor.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID删除关联
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @returns Promise<boolean> 是否删除成功
|
||||
*/
|
||||
async deleteByGameUserId(gameUserId: string): Promise<boolean> {
|
||||
const monitor = this.createPerformanceMonitor('根据游戏用户ID删除关联', { gameUserId });
|
||||
|
||||
try {
|
||||
const result = await this.repository.deleteByGameUserId(this.parseGameUserId(gameUserId));
|
||||
|
||||
monitor.success({ deleted: result });
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
monitor.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询多个Zulip账号关联
|
||||
*
|
||||
* @param queryDto 查询条件
|
||||
* @returns Promise<ZulipAccountListResponseDto> 关联记录列表
|
||||
*/
|
||||
async findMany(queryDto: QueryZulipAccountDto = {}): Promise<ZulipAccountListResponseDto> {
|
||||
this.logStart('查询多个Zulip账号关联', queryDto);
|
||||
|
||||
try {
|
||||
const options = {
|
||||
gameUserId: queryDto.gameUserId ? this.parseGameUserId(queryDto.gameUserId) : undefined,
|
||||
zulipUserId: queryDto.zulipUserId,
|
||||
zulipEmail: queryDto.zulipEmail,
|
||||
status: queryDto.status,
|
||||
includeGameUser: queryDto.includeGameUser || false,
|
||||
};
|
||||
|
||||
const accounts = await this.repository.findMany(options);
|
||||
|
||||
this.logSuccess('查询多个Zulip账号关联', {
|
||||
count: accounts.length,
|
||||
conditions: queryDto
|
||||
});
|
||||
|
||||
return this.buildListResponse(accounts);
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
accounts: this.handleSearchError(error, '查询多个Zulip账号关联', queryDto),
|
||||
total: 0,
|
||||
count: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取需要验证的账号列表
|
||||
*
|
||||
* @param maxAge 最大验证间隔(毫秒),默认24小时
|
||||
* @returns Promise<ZulipAccountListResponseDto> 需要验证的账号列表
|
||||
*/
|
||||
async findAccountsNeedingVerification(maxAge: number = DEFAULT_VERIFICATION_MAX_AGE): Promise<ZulipAccountListResponseDto> {
|
||||
this.logStart('获取需要验证的账号列表', { maxAge });
|
||||
|
||||
try {
|
||||
const accounts = await this.repository.findAccountsNeedingVerification(maxAge);
|
||||
|
||||
this.logSuccess('获取需要验证的账号列表', { count: accounts.length });
|
||||
|
||||
return this.buildListResponse(accounts);
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
accounts: this.handleSearchError(error, '获取需要验证的账号列表', { maxAge }),
|
||||
total: 0,
|
||||
count: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误状态的账号列表
|
||||
*
|
||||
* @param maxRetryCount 最大重试次数,默认3次
|
||||
* @returns Promise<ZulipAccountListResponseDto> 错误状态的账号列表
|
||||
*/
|
||||
async findErrorAccounts(maxRetryCount: number = DEFAULT_MAX_RETRY_COUNT): Promise<ZulipAccountListResponseDto> {
|
||||
this.logStart('获取错误状态的账号列表', { maxRetryCount });
|
||||
|
||||
try {
|
||||
const accounts = await this.repository.findErrorAccounts(maxRetryCount);
|
||||
|
||||
this.logSuccess('获取错误状态的账号列表', { count: accounts.length });
|
||||
|
||||
return this.buildListResponse(accounts);
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
accounts: this.handleSearchError(error, '获取错误状态的账号列表', { maxRetryCount }),
|
||||
total: 0,
|
||||
count: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新账号状态
|
||||
*
|
||||
* @param ids 账号ID列表
|
||||
* @param status 新状态
|
||||
* @returns Promise<BatchUpdateResponseDto> 批量更新结果
|
||||
*/
|
||||
async batchUpdateStatus(ids: string[], status: 'active' | 'inactive' | 'suspended' | 'error'): Promise<BatchUpdateResponseDto> {
|
||||
const monitor = this.createPerformanceMonitor('批量更新账号状态', { count: ids.length, status });
|
||||
|
||||
try {
|
||||
const bigintIds = this.parseIds(ids);
|
||||
const updatedCount = await this.repository.batchUpdateStatus(bigintIds, status);
|
||||
|
||||
monitor.success({
|
||||
requestCount: ids.length,
|
||||
updatedCount,
|
||||
status
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
updatedCount,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('批量更新账号状态失败', {
|
||||
operation: 'batchUpdateStatus',
|
||||
error: this.formatError(error),
|
||||
count: ids.length,
|
||||
status,
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
updatedCount: 0,
|
||||
error: this.formatError(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账号状态统计(带缓存)
|
||||
*
|
||||
* 数据访问逻辑:
|
||||
* 1. 构建统计数据的缓存键
|
||||
* 2. 尝试从缓存获取统计数据
|
||||
* 3. 如果缓存命中,直接返回缓存数据
|
||||
* 4. 如果缓存未命中,从数据库查询统计数据
|
||||
* 5. 计算总数并构建完整的统计响应
|
||||
* 6. 将统计结果存入缓存,使用较短的TTL
|
||||
* 7. 记录操作日志和性能指标
|
||||
*
|
||||
* @returns Promise<ZulipAccountStatsResponseDto> 状态统计
|
||||
*/
|
||||
async getStatusStatistics(): Promise<ZulipAccountStatsResponseDto> {
|
||||
const cacheKey = this.buildCacheKey('stats');
|
||||
|
||||
try {
|
||||
// 尝试从缓存获取
|
||||
const cached = await this.cacheManager.get(cacheKey) as ZulipAccountStatsResponseDto;
|
||||
if (cached) {
|
||||
this.logger.debug('统计数据缓存命中', {
|
||||
module: this.moduleName,
|
||||
operation: 'getStatusStatistics',
|
||||
cacheKey
|
||||
});
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 缓存未命中,从数据库查询
|
||||
const monitor = this.createPerformanceMonitor('获取账号状态统计');
|
||||
|
||||
const statistics = await this.repository.getStatusStatistics();
|
||||
|
||||
const result = {
|
||||
active: statistics.active || 0,
|
||||
inactive: statistics.inactive || 0,
|
||||
suspended: statistics.suspended || 0,
|
||||
error: statistics.error || 0,
|
||||
total: (statistics.active || 0) + (statistics.inactive || 0) +
|
||||
(statistics.suspended || 0) + (statistics.error || 0),
|
||||
};
|
||||
|
||||
// 存入缓存,使用较短的TTL
|
||||
await this.cacheManager.set(cacheKey, result, ZulipAccountsService.STATS_CACHE_TTL);
|
||||
|
||||
monitor.success({
|
||||
total: result.total,
|
||||
cached: true
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
this.handleDataAccessError(error, '获取账号状态统计');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证账号有效性
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @returns Promise<VerifyAccountResponseDto> 验证结果
|
||||
*/
|
||||
async verifyAccount(gameUserId: string): Promise<VerifyAccountResponseDto> {
|
||||
const monitor = this.createPerformanceMonitor('验证账号有效性', { gameUserId });
|
||||
|
||||
try {
|
||||
// 1. 查找账号关联
|
||||
const account = await this.repository.findByGameUserId(this.parseGameUserId(gameUserId));
|
||||
|
||||
if (!account) {
|
||||
monitor.success({ isValid: false, reason: '账号关联不存在' });
|
||||
return {
|
||||
success: false,
|
||||
isValid: false,
|
||||
error: '账号关联不存在',
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 检查账号状态
|
||||
if (account.status !== 'active') {
|
||||
monitor.success({ isValid: false, reason: `账号状态为 ${account.status}` });
|
||||
return {
|
||||
success: true,
|
||||
isValid: false,
|
||||
error: `账号状态为 ${account.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 更新验证时间
|
||||
await this.repository.updateByGameUserId(this.parseGameUserId(gameUserId), {
|
||||
lastVerifiedAt: new Date(),
|
||||
});
|
||||
|
||||
monitor.success({ isValid: true });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
isValid: true,
|
||||
verifiedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('验证账号有效性失败', {
|
||||
operation: 'verifyAccount',
|
||||
gameUserId,
|
||||
error: this.formatError(error),
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
isValid: false,
|
||||
error: this.formatError(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查邮箱是否已存在
|
||||
*
|
||||
* @param zulipEmail Zulip邮箱
|
||||
* @param excludeId 排除的记录ID
|
||||
* @returns Promise<boolean> 是否已存在
|
||||
*/
|
||||
async existsByEmail(zulipEmail: string, excludeId?: string): Promise<boolean> {
|
||||
try {
|
||||
const excludeBigintId = excludeId ? this.parseId(excludeId) : undefined;
|
||||
return await this.repository.existsByEmail(zulipEmail, excludeBigintId);
|
||||
} catch (error) {
|
||||
this.logger.warn('检查邮箱存在性失败', {
|
||||
operation: 'existsByEmail',
|
||||
zulipEmail,
|
||||
error: this.formatError(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Zulip用户ID是否已存在
|
||||
*
|
||||
* @param zulipUserId Zulip用户ID
|
||||
* @param excludeId 排除的记录ID
|
||||
* @returns Promise<boolean> 是否已存在
|
||||
*/
|
||||
async existsByZulipUserId(zulipUserId: number, excludeId?: string): Promise<boolean> {
|
||||
try {
|
||||
const excludeBigintId = excludeId ? this.parseId(excludeId) : undefined;
|
||||
return await this.repository.existsByZulipUserId(zulipUserId, excludeBigintId);
|
||||
} catch (error) {
|
||||
this.logger.warn('检查Zulip用户ID存在性失败', {
|
||||
operation: 'existsByZulipUserId',
|
||||
zulipUserId,
|
||||
error: this.formatError(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将实体转换为响应DTO
|
||||
*
|
||||
* @param account 账号关联实体
|
||||
* @returns ZulipAccountResponseDto 响应DTO
|
||||
*/
|
||||
protected toResponseDto(account: ZulipAccounts): ZulipAccountResponseDto {
|
||||
return {
|
||||
id: account.id.toString(),
|
||||
gameUserId: account.gameUserId.toString(),
|
||||
zulipUserId: account.zulipUserId,
|
||||
zulipEmail: account.zulipEmail,
|
||||
zulipFullName: account.zulipFullName,
|
||||
status: account.status,
|
||||
lastVerifiedAt: account.lastVerifiedAt?.toISOString(),
|
||||
lastSyncedAt: account.lastSyncedAt?.toISOString(),
|
||||
errorMessage: account.errorMessage,
|
||||
retryCount: account.retryCount,
|
||||
createdAt: account.createdAt.toISOString(),
|
||||
updatedAt: account.updatedAt.toISOString(),
|
||||
gameUser: account.gameUser,
|
||||
};
|
||||
}
|
||||
|
||||
// ========== 缓存管理方法 ==========
|
||||
|
||||
/**
|
||||
* 构建缓存键
|
||||
*
|
||||
* @param type 缓存类型
|
||||
* @param identifier 标识符
|
||||
* @param includeGameUser 是否包含游戏用户信息
|
||||
* @returns 缓存键字符串
|
||||
* @private
|
||||
*/
|
||||
private buildCacheKey(type: string, identifier?: string, includeGameUser?: boolean): string {
|
||||
const parts = [ZulipAccountsService.CACHE_PREFIX, type];
|
||||
if (identifier) parts.push(identifier);
|
||||
if (includeGameUser) parts.push('with_user');
|
||||
return parts.join(':');
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除相关缓存
|
||||
*
|
||||
* 功能描述:
|
||||
* 当数据发生变更时,清除相关的缓存项以确保数据一致性
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @param zulipUserId Zulip用户ID
|
||||
* @param zulipEmail Zulip邮箱
|
||||
* @private
|
||||
*/
|
||||
private async clearRelatedCache(gameUserId?: string, zulipUserId?: number, zulipEmail?: string): Promise<void> {
|
||||
const keysToDelete: string[] = [];
|
||||
|
||||
// 清除统计缓存
|
||||
keysToDelete.push(this.buildCacheKey('stats'));
|
||||
|
||||
// 清除具体记录的缓存
|
||||
if (gameUserId) {
|
||||
keysToDelete.push(this.buildCacheKey('game_user', gameUserId, false));
|
||||
keysToDelete.push(this.buildCacheKey('game_user', gameUserId, true));
|
||||
}
|
||||
|
||||
if (zulipUserId) {
|
||||
keysToDelete.push(this.buildCacheKey('zulip_user', zulipUserId.toString(), false));
|
||||
keysToDelete.push(this.buildCacheKey('zulip_user', zulipUserId.toString(), true));
|
||||
}
|
||||
|
||||
if (zulipEmail) {
|
||||
keysToDelete.push(this.buildCacheKey('zulip_email', zulipEmail, false));
|
||||
keysToDelete.push(this.buildCacheKey('zulip_email', zulipEmail, true));
|
||||
}
|
||||
|
||||
// 批量删除缓存
|
||||
try {
|
||||
await Promise.all(keysToDelete.map(key => this.cacheManager.del(key)));
|
||||
|
||||
this.logger.debug('清除相关缓存', {
|
||||
module: this.moduleName,
|
||||
operation: 'clearRelatedCache',
|
||||
keysCount: keysToDelete.length,
|
||||
keys: keysToDelete
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn('清除缓存失败', {
|
||||
module: this.moduleName,
|
||||
operation: 'clearRelatedCache',
|
||||
error: this.formatError(error),
|
||||
keys: keysToDelete
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有相关缓存
|
||||
*
|
||||
* 功能描述:
|
||||
* 清除所有与Zulip账号相关的缓存,通常在批量操作后调用
|
||||
*
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async clearAllCache(): Promise<void> {
|
||||
try {
|
||||
// 这里可以根据实际的缓存实现来清除所有相关缓存
|
||||
// 由于cache-manager没有直接的模式匹配删除,我们清除已知的缓存类型
|
||||
const commonKeys = [
|
||||
this.buildCacheKey('stats'),
|
||||
// 可以添加更多已知的缓存键模式
|
||||
];
|
||||
|
||||
await Promise.all(commonKeys.map(key => this.cacheManager.del(key)));
|
||||
|
||||
this.logger.info('清除所有缓存完成', {
|
||||
module: this.moduleName,
|
||||
operation: 'clearAllCache',
|
||||
keysCount: commonKeys.length
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn('清除所有缓存失败', {
|
||||
module: this.moduleName,
|
||||
operation: 'clearAllCache',
|
||||
error: this.formatError(error)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/core/db/zulip_accounts/zulip_accounts.types.ts
Normal file
98
src/core/db/zulip_accounts/zulip_accounts.types.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Zulip账号关联类型定义
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义模块中使用的所有类型和接口
|
||||
* - 提供统一的类型管理和约束
|
||||
* - 确保类型安全和一致性
|
||||
* - 便于类型复用和维护
|
||||
*
|
||||
* 职责分离:
|
||||
* - 类型定义:集中管理所有模块类型
|
||||
* - 接口约束:定义数据结构和方法签名
|
||||
* - 类型安全:确保编译时类型检查
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 完善类型定义和接口约束
|
||||
* - 2026-01-07: 架构优化 - 提取统一的类型定义,改善架构分层
|
||||
* - 2026-01-07: 初始创建 - 提取和统一类型定义,提高代码质量
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.1
|
||||
* @since 2026-01-07
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
/**
|
||||
* 账号状态枚举
|
||||
*/
|
||||
export type AccountStatus = 'active' | 'inactive' | 'suspended' | 'error';
|
||||
|
||||
/**
|
||||
* 创建Zulip账号关联的数据传输对象
|
||||
*/
|
||||
export interface CreateZulipAccountData {
|
||||
gameUserId: bigint;
|
||||
zulipUserId: number;
|
||||
zulipEmail: string;
|
||||
zulipFullName: string;
|
||||
zulipApiKeyEncrypted: string;
|
||||
status?: AccountStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Zulip账号关联的数据传输对象
|
||||
*/
|
||||
export interface UpdateZulipAccountData {
|
||||
zulipFullName?: string;
|
||||
zulipApiKeyEncrypted?: string;
|
||||
status?: AccountStatus;
|
||||
lastVerifiedAt?: Date;
|
||||
lastSyncedAt?: Date;
|
||||
errorMessage?: string;
|
||||
retryCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zulip账号查询选项
|
||||
*/
|
||||
export interface ZulipAccountQueryOptions {
|
||||
gameUserId?: bigint;
|
||||
zulipUserId?: number;
|
||||
zulipEmail?: string;
|
||||
status?: AccountStatus;
|
||||
includeGameUser?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态统计结果
|
||||
*/
|
||||
export interface StatusStatistics {
|
||||
active: number;
|
||||
inactive: number;
|
||||
suspended: number;
|
||||
error: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository接口定义
|
||||
*/
|
||||
export interface IZulipAccountsRepository {
|
||||
create(data: CreateZulipAccountData): Promise<any>;
|
||||
findByGameUserId(gameUserId: bigint, includeGameUser?: boolean): Promise<any | null>;
|
||||
findByZulipUserId(zulipUserId: number, includeGameUser?: boolean): Promise<any | null>;
|
||||
findByZulipEmail(zulipEmail: string, includeGameUser?: boolean): Promise<any | null>;
|
||||
findById(id: bigint, includeGameUser?: boolean): Promise<any | null>;
|
||||
update(id: bigint, data: UpdateZulipAccountData): Promise<any | null>;
|
||||
updateByGameUserId(gameUserId: bigint, data: UpdateZulipAccountData): Promise<any | null>;
|
||||
delete(id: bigint): Promise<boolean>;
|
||||
deleteByGameUserId(gameUserId: bigint): Promise<boolean>;
|
||||
findMany(options?: ZulipAccountQueryOptions): Promise<any[]>;
|
||||
findAccountsNeedingVerification(maxAge?: number): Promise<any[]>;
|
||||
findErrorAccounts(maxRetryCount?: number): Promise<any[]>;
|
||||
batchUpdateStatus(ids: bigint[], status: AccountStatus): Promise<number>;
|
||||
getStatusStatistics(): Promise<StatusStatistics>;
|
||||
existsByEmail(email: string, excludeId?: bigint): Promise<boolean>;
|
||||
existsByZulipUserId(zulipUserId: number, excludeId?: bigint): Promise<boolean>;
|
||||
existsByGameUserId(gameUserId: bigint, excludeId?: bigint): Promise<boolean>;
|
||||
}
|
||||
446
src/core/db/zulip_accounts/zulip_accounts_memory.repository.ts
Normal file
446
src/core/db/zulip_accounts/zulip_accounts_memory.repository.ts
Normal file
@@ -0,0 +1,446 @@
|
||||
/**
|
||||
* Zulip账号关联内存数据访问层
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供Zulip账号关联数据的内存存储实现和CRUD操作
|
||||
* - 用于开发和测试环境,无需数据库连接和配置
|
||||
* - 实现与数据库版本相同的接口和查询功能
|
||||
* - 支持数据导入导出、备份恢复和测试数据管理
|
||||
*
|
||||
* 职责分离:
|
||||
* - 数据存储:使用Map结构提供高效的内存数据存储
|
||||
* - 查询实现:实现各种查询条件和过滤逻辑
|
||||
* - 约束检查:确保数据唯一性和完整性约束
|
||||
* - 测试支持:提供数据导入导出和清理功能
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 修复findAccountsNeedingVerification方法的限制逻辑,与数据库版本保持一致 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 使用统一的常量文件,提高代码质量
|
||||
* - 2026-01-07: 代码规范优化 - 完善文件头注释和方法三级注释
|
||||
* - 2026-01-07: 功能完善 - 优化查询性能和数据管理功能
|
||||
* - 2025-01-07: 架构优化 - 统一Repository层的接口设计和实现
|
||||
* - 2025-01-05: 功能扩展 - 添加批量操作和统计查询功能
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.1.2
|
||||
* @since 2025-01-05
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ZulipAccounts } from './zulip_accounts.entity';
|
||||
import {
|
||||
DEFAULT_VERIFICATION_MAX_AGE,
|
||||
DEFAULT_MAX_RETRY_COUNT,
|
||||
DEFAULT_ERROR_ACCOUNTS_LIMIT,
|
||||
} from './zulip_accounts.constants';
|
||||
import {
|
||||
CreateZulipAccountData,
|
||||
UpdateZulipAccountData,
|
||||
ZulipAccountQueryOptions,
|
||||
StatusStatistics,
|
||||
IZulipAccountsRepository,
|
||||
} from './zulip_accounts.types';
|
||||
|
||||
@Injectable()
|
||||
export class ZulipAccountsMemoryRepository implements IZulipAccountsRepository {
|
||||
private accounts: Map<bigint, ZulipAccounts> = new Map();
|
||||
private currentId: bigint = BigInt(1);
|
||||
|
||||
/**
|
||||
* 创建新的Zulip账号关联(带唯一性检查)
|
||||
*
|
||||
* @param createData 创建数据
|
||||
* @returns Promise<ZulipAccounts> 创建的关联记录
|
||||
*/
|
||||
async create(createData: CreateZulipAccountData): Promise<ZulipAccounts> {
|
||||
// 检查唯一性约束
|
||||
const existingByGameUser = await this.findByGameUserId(createData.gameUserId);
|
||||
if (existingByGameUser) {
|
||||
throw new Error(`Game user ${createData.gameUserId} already has a Zulip account`);
|
||||
}
|
||||
|
||||
const existingByZulipUser = await this.findByZulipUserId(createData.zulipUserId);
|
||||
if (existingByZulipUser) {
|
||||
throw new Error(`Zulip user ${createData.zulipUserId} is already linked`);
|
||||
}
|
||||
|
||||
const existingByEmail = await this.findByZulipEmail(createData.zulipEmail);
|
||||
if (existingByEmail) {
|
||||
throw new Error(`Zulip email ${createData.zulipEmail} is already linked`);
|
||||
}
|
||||
|
||||
const account = new ZulipAccounts();
|
||||
account.id = this.currentId++;
|
||||
account.gameUserId = createData.gameUserId;
|
||||
account.zulipUserId = createData.zulipUserId;
|
||||
account.zulipEmail = createData.zulipEmail;
|
||||
account.zulipFullName = createData.zulipFullName;
|
||||
account.zulipApiKeyEncrypted = createData.zulipApiKeyEncrypted;
|
||||
account.status = createData.status || 'active';
|
||||
account.lastVerifiedAt = null;
|
||||
account.lastSyncedAt = null;
|
||||
account.errorMessage = null;
|
||||
account.retryCount = 0;
|
||||
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 updateData 更新数据
|
||||
* @returns Promise<ZulipAccounts | null> 更新后的记录或null
|
||||
*/
|
||||
async update(id: bigint, updateData: UpdateZulipAccountData): Promise<ZulipAccounts | null> {
|
||||
const account = this.accounts.get(id);
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object.assign(account, updateData);
|
||||
account.updatedAt = new Date();
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID更新Zulip账号关联
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @param updateData 更新数据
|
||||
* @returns Promise<ZulipAccounts | null> 更新后的记录或null
|
||||
*/
|
||||
async updateByGameUserId(gameUserId: bigint, updateData: UpdateZulipAccountData): Promise<ZulipAccounts | null> {
|
||||
const account = await this.findByGameUserId(gameUserId);
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object.assign(account, updateData);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取需要验证的账号列表
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 计算验证截止时间,基于当前时间减去最大验证间隔
|
||||
* 2. 筛选状态为active且需要验证的账号记录
|
||||
* 3. 包含从未验证过的账号(lastVerifiedAt为null)
|
||||
* 4. 包含验证时间超过最大间隔的账号
|
||||
* 5. 按验证时间升序排序,优先处理最久未验证的账号
|
||||
*
|
||||
* @param maxAge 最大验证间隔(毫秒),默认24小时
|
||||
* @returns Promise<ZulipAccounts[]> 需要验证的账号列表,按验证时间升序排序
|
||||
*
|
||||
* @example
|
||||
* // 获取需要验证的账号(默认24小时)
|
||||
* const accounts = await repository.findAccountsNeedingVerification();
|
||||
*
|
||||
* @example
|
||||
* // 获取需要验证的账号(自定义12小时)
|
||||
* const accounts = await repository.findAccountsNeedingVerification(12 * 60 * 60 * 1000);
|
||||
*/
|
||||
async findAccountsNeedingVerification(maxAge: number = DEFAULT_VERIFICATION_MAX_AGE): 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();
|
||||
})
|
||||
.slice(0, 100); // 应用默认限制,与数据库版本保持一致
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误状态的账号列表(可重试的)
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 筛选状态为error的账号记录
|
||||
* 2. 过滤重试次数小于最大重试次数的账号
|
||||
* 3. 按更新时间升序排序,优先处理最早出错的账号
|
||||
* 4. 限制返回数量,避免一次处理过多错误账号
|
||||
* 5. 为错误恢复和重试机制提供数据支持
|
||||
*
|
||||
* @param maxRetryCount 最大重试次数,默认3次
|
||||
* @returns Promise<ZulipAccounts[]> 错误状态的账号列表,限制50条记录
|
||||
*
|
||||
* @example
|
||||
* // 获取可重试的错误账号(默认3次重试限制)
|
||||
* const errorAccounts = await repository.findErrorAccounts();
|
||||
*
|
||||
* @example
|
||||
* // 获取可重试的错误账号(自定义5次重试限制)
|
||||
* const errorAccounts = await repository.findErrorAccounts(5);
|
||||
*/
|
||||
async findErrorAccounts(maxRetryCount: number = DEFAULT_MAX_RETRY_COUNT): Promise<ZulipAccounts[]> {
|
||||
return Array.from(this.accounts.values())
|
||||
.filter(account => account.status === 'error' && account.retryCount < maxRetryCount)
|
||||
.sort((a, b) => a.updatedAt.getTime() - b.updatedAt.getTime())
|
||||
.slice(0, DEFAULT_ERROR_ACCOUNTS_LIMIT); // 限制返回数量
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新账号状态
|
||||
*
|
||||
* @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<StatusStatistics> 状态统计
|
||||
*/
|
||||
async getStatusStatistics(): Promise<StatusStatistics> {
|
||||
const statistics: StatusStatistics = {
|
||||
active: 0,
|
||||
inactive: 0,
|
||||
suspended: 0,
|
||||
error: 0,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查游戏用户ID是否已存在
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @param excludeId 排除的记录ID(用于更新时检查)
|
||||
* @returns Promise<boolean> 是否已存在
|
||||
*/
|
||||
async existsByGameUserId(gameUserId: bigint, excludeId?: bigint): Promise<boolean> {
|
||||
for (const [id, account] of this.accounts.entries()) {
|
||||
if (account.gameUserId === gameUserId && (!excludeId || id !== excludeId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出所有数据(用于测试和备份)
|
||||
*
|
||||
* @returns Promise<ZulipAccounts[]> 所有账号数据
|
||||
*/
|
||||
async exportData(): Promise<ZulipAccounts[]> {
|
||||
return Array.from(this.accounts.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入数据(用于测试数据初始化)
|
||||
*
|
||||
* @param accounts 账号数据列表
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async importData(accounts: ZulipAccounts[]): Promise<void> {
|
||||
this.accounts.clear();
|
||||
let maxId = BigInt(0);
|
||||
|
||||
for (const account of accounts) {
|
||||
this.accounts.set(account.id, account);
|
||||
if (account.id > maxId) {
|
||||
maxId = account.id;
|
||||
}
|
||||
}
|
||||
|
||||
this.currentId = maxId + BigInt(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有数据(用于测试)
|
||||
*
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async clearAll(): Promise<void> {
|
||||
this.accounts.clear();
|
||||
this.currentId = BigInt(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据统计信息
|
||||
*
|
||||
* @returns Promise<{ total: number; nextId: string }> 统计信息
|
||||
*/
|
||||
async getDataInfo(): Promise<{ total: number; nextId: string }> {
|
||||
return {
|
||||
total: this.accounts.size,
|
||||
nextId: this.currentId.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
607
src/core/db/zulip_accounts/zulip_accounts_memory.service.ts
Normal file
607
src/core/db/zulip_accounts/zulip_accounts_memory.service.ts
Normal file
@@ -0,0 +1,607 @@
|
||||
/**
|
||||
* Zulip账号关联服务(内存版本)
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供Zulip账号关联的内存存储数据访问服务
|
||||
* - 用于开发和测试环境,无需数据库依赖
|
||||
* - 实现与数据库版本相同的数据访问接口
|
||||
* - 支持数据导入导出和测试数据管理
|
||||
*
|
||||
* 职责分离:
|
||||
* - 数据访问:通过内存Repository提供数据持久化
|
||||
* - 接口兼容:与数据库版本保持完全一致的API接口
|
||||
* - 测试支持:提供测试环境的数据管理功能
|
||||
*
|
||||
* 注意:业务逻辑已转移到 src/core/zulip_core/services/zulip_accounts_business.service.ts
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-15: 代码规范优化 - 清理未使用的导入ConflictException和NotFoundException (修改者: moyin)
|
||||
* - 2026-01-12: 架构优化 - 移除业务逻辑,转移到zulip_core业务服务 (修改者: moyin)
|
||||
* - 2026-01-12: 代码质量优化 - 修复导入语句,添加缺失的AppLoggerService导入 (修改者: moyin)
|
||||
* - 2026-01-12: 代码质量优化 - 修复logger初始化问题,统一使用AppLoggerService (修改者: moyin)
|
||||
* - 2026-01-12: 代码质量优化 - 完成所有性能监控代码优化,统一使用createPerformanceMonitor方法 (修改者: moyin)
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 2.0.1
|
||||
* @since 2025-01-07
|
||||
* @lastModified 2026-01-15
|
||||
*/
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { BaseZulipAccountsService } from './base_zulip_accounts.service';
|
||||
import { ZulipAccountsMemoryRepository } from './zulip_accounts_memory.repository';
|
||||
import { ZulipAccounts } from './zulip_accounts.entity';
|
||||
import { AppLoggerService } from '../../utils/logger/logger.service';
|
||||
import {
|
||||
DEFAULT_VERIFICATION_MAX_AGE,
|
||||
DEFAULT_MAX_RETRY_COUNT,
|
||||
} from './zulip_accounts.constants';
|
||||
import {
|
||||
CreateZulipAccountDto,
|
||||
UpdateZulipAccountDto,
|
||||
QueryZulipAccountDto,
|
||||
ZulipAccountResponseDto,
|
||||
ZulipAccountListResponseDto,
|
||||
ZulipAccountStatsResponseDto,
|
||||
BatchUpdateResponseDto,
|
||||
VerifyAccountResponseDto,
|
||||
} from './zulip_accounts.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ZulipAccountsMemoryService extends BaseZulipAccountsService {
|
||||
constructor(
|
||||
@Inject('ZulipAccountsRepository')
|
||||
private readonly repository: ZulipAccountsMemoryRepository,
|
||||
@Inject(AppLoggerService) logger: AppLoggerService,
|
||||
) {
|
||||
super(logger, 'ZulipAccountsMemoryService');
|
||||
this.logger.info('ZulipAccountsMemoryService初始化完成', {
|
||||
module: 'ZulipAccountsMemoryService',
|
||||
operation: 'constructor'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Zulip账号关联
|
||||
*
|
||||
* 数据访问逻辑:
|
||||
* 1. 接收创建请求数据
|
||||
* 2. 将字符串类型的gameUserId转换为BigInt类型
|
||||
* 3. 调用内存Repository层创建账号关联记录
|
||||
* 4. 记录操作日志和性能指标
|
||||
* 5. 将实体对象转换为响应DTO返回
|
||||
*
|
||||
* @param createDto 创建数据,包含游戏用户ID、Zulip用户信息等
|
||||
* @returns Promise<ZulipAccountResponseDto> 创建的关联记录DTO
|
||||
* @throws 数据访问异常
|
||||
*/
|
||||
async create(createDto: CreateZulipAccountDto): Promise<ZulipAccountResponseDto> {
|
||||
const monitor = this.createPerformanceMonitor('创建Zulip账号关联', { gameUserId: createDto.gameUserId });
|
||||
|
||||
try {
|
||||
const account = await this.repository.create({
|
||||
gameUserId: this.parseGameUserId(createDto.gameUserId),
|
||||
zulipUserId: createDto.zulipUserId,
|
||||
zulipEmail: createDto.zulipEmail,
|
||||
zulipFullName: createDto.zulipFullName,
|
||||
zulipApiKeyEncrypted: createDto.zulipApiKeyEncrypted,
|
||||
status: createDto.status || 'active',
|
||||
});
|
||||
|
||||
const result = this.toResponseDto(account);
|
||||
monitor.success({ accountId: account.id.toString() });
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
monitor.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID查找关联
|
||||
*
|
||||
* 数据访问逻辑:
|
||||
* 1. 记录查询操作开始日志
|
||||
* 2. 将字符串类型的gameUserId转换为BigInt类型
|
||||
* 3. 调用内存Repository层根据游戏用户ID查找记录
|
||||
* 4. 如果未找到记录,记录调试日志并返回null
|
||||
* 5. 如果找到记录,记录成功日志
|
||||
* 6. 将实体对象转换为响应DTO返回
|
||||
* 7. 捕获异常并进行统一的错误处理
|
||||
*
|
||||
* @param gameUserId 游戏用户ID,字符串格式
|
||||
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略),默认false
|
||||
* @returns Promise<ZulipAccountResponseDto | null> 关联记录DTO或null
|
||||
* @throws BadRequestException 当查询参数无效或系统异常时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = await memoryService.findByGameUserId('12345', true);
|
||||
* if (account) {
|
||||
* console.log('找到关联:', account.zulipEmail);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async findByGameUserId(gameUserId: string, includeGameUser: boolean = false): Promise<ZulipAccountResponseDto | null> {
|
||||
const monitor = this.createPerformanceMonitor('根据游戏用户ID查找关联', { gameUserId });
|
||||
|
||||
try {
|
||||
const account = await this.repository.findByGameUserId(this.parseGameUserId(gameUserId), includeGameUser);
|
||||
|
||||
if (!account) {
|
||||
this.logger.debug('未找到Zulip账号关联', { gameUserId });
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = this.toResponseDto(account);
|
||||
monitor.success({ found: true });
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
monitor.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Zulip用户ID查找关联
|
||||
*
|
||||
* 数据访问逻辑:
|
||||
* 1. 记录查询操作开始日志
|
||||
* 2. 调用内存Repository层根据Zulip用户ID查找记录
|
||||
* 3. 如果未找到记录,记录调试日志并返回null
|
||||
* 4. 如果找到记录,记录成功日志
|
||||
* 5. 将实体对象转换为响应DTO返回
|
||||
* 6. 捕获异常并进行统一的错误处理
|
||||
*
|
||||
* @param zulipUserId Zulip用户ID,数字类型
|
||||
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略),默认false
|
||||
* @returns Promise<ZulipAccountResponseDto | null> 关联记录DTO或null
|
||||
* @throws BadRequestException 当查询参数无效或系统异常时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const account = await memoryService.findByZulipUserId(67890);
|
||||
* if (account) {
|
||||
* console.log('关联的游戏用户:', account.gameUserId);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async findByZulipUserId(zulipUserId: number, includeGameUser: boolean = false): Promise<ZulipAccountResponseDto | null> {
|
||||
this.logStart('根据Zulip用户ID查找关联', { zulipUserId });
|
||||
|
||||
try {
|
||||
const account = await this.repository.findByZulipUserId(zulipUserId, includeGameUser);
|
||||
|
||||
if (!account) {
|
||||
this.logger.debug('未找到Zulip账号关联', { zulipUserId });
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logSuccess('根据Zulip用户ID查找关联', { zulipUserId, found: true });
|
||||
return this.toResponseDto(account);
|
||||
|
||||
} catch (error) {
|
||||
this.handleDataAccessError(error, '根据Zulip用户ID查找关联', { zulipUserId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Zulip邮箱查找关联
|
||||
*
|
||||
* @param zulipEmail Zulip邮箱
|
||||
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
|
||||
* @returns Promise<ZulipAccountResponseDto | null> 关联记录或null
|
||||
*/
|
||||
async findByZulipEmail(zulipEmail: string, includeGameUser: boolean = false): Promise<ZulipAccountResponseDto | null> {
|
||||
this.logStart('根据Zulip邮箱查找关联', { zulipEmail });
|
||||
|
||||
try {
|
||||
const account = await this.repository.findByZulipEmail(zulipEmail, includeGameUser);
|
||||
|
||||
if (!account) {
|
||||
this.logger.debug('未找到Zulip账号关联', { zulipEmail });
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logSuccess('根据Zulip邮箱查找关联', { zulipEmail, found: true });
|
||||
return this.toResponseDto(account);
|
||||
|
||||
} catch (error) {
|
||||
this.handleDataAccessError(error, '根据Zulip邮箱查找关联', { zulipEmail });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查找关联
|
||||
*
|
||||
* @param id 关联记录ID
|
||||
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
|
||||
* @returns Promise<ZulipAccountResponseDto> 关联记录
|
||||
*/
|
||||
async findById(id: string, includeGameUser: boolean = false): Promise<ZulipAccountResponseDto> {
|
||||
this.logStart('根据ID查找关联', { id });
|
||||
|
||||
try {
|
||||
const account = await this.repository.findById(this.parseId(id), includeGameUser);
|
||||
|
||||
const result = account ? this.toResponseDto(account) : null;
|
||||
this.logSuccess('根据ID查找关联', { id, found: !!account });
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
this.handleDataAccessError(error, '根据ID查找关联', { id });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Zulip账号关联
|
||||
*
|
||||
* @param id 关联记录ID
|
||||
* @param updateDto 更新数据
|
||||
* @returns Promise<ZulipAccountResponseDto> 更新后的记录
|
||||
*/
|
||||
async update(id: string, updateDto: UpdateZulipAccountDto): Promise<ZulipAccountResponseDto> {
|
||||
const monitor = this.createPerformanceMonitor('更新Zulip账号关联', { id });
|
||||
|
||||
try {
|
||||
const account = await this.repository.update(this.parseId(id), updateDto);
|
||||
|
||||
const result = account ? this.toResponseDto(account) : null;
|
||||
monitor.success({ updated: !!account });
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
monitor.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID更新关联
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @param updateDto 更新数据
|
||||
* @returns Promise<ZulipAccountResponseDto> 更新后的记录
|
||||
*/
|
||||
async updateByGameUserId(gameUserId: string, updateDto: UpdateZulipAccountDto): Promise<ZulipAccountResponseDto> {
|
||||
const monitor = this.createPerformanceMonitor('根据游戏用户ID更新关联', { gameUserId });
|
||||
|
||||
try {
|
||||
const account = await this.repository.updateByGameUserId(this.parseGameUserId(gameUserId), updateDto);
|
||||
|
||||
const result = account ? this.toResponseDto(account) : null;
|
||||
monitor.success({ updated: !!account });
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
monitor.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Zulip账号关联
|
||||
*
|
||||
* @param id 关联记录ID
|
||||
* @returns Promise<boolean> 是否删除成功
|
||||
*/
|
||||
async delete(id: string): Promise<boolean> {
|
||||
const monitor = this.createPerformanceMonitor('删除Zulip账号关联', { id });
|
||||
|
||||
try {
|
||||
const result = await this.repository.delete(this.parseId(id));
|
||||
|
||||
monitor.success({ deleted: result });
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
monitor.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID删除关联
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @returns Promise<boolean> 是否删除成功
|
||||
*/
|
||||
async deleteByGameUserId(gameUserId: string): Promise<boolean> {
|
||||
const monitor = this.createPerformanceMonitor('根据游戏用户ID删除关联', { gameUserId });
|
||||
|
||||
try {
|
||||
const result = await this.repository.deleteByGameUserId(this.parseGameUserId(gameUserId));
|
||||
|
||||
monitor.success({ deleted: result });
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
monitor.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询多个Zulip账号关联
|
||||
*
|
||||
* @param queryDto 查询条件
|
||||
* @returns Promise<ZulipAccountListResponseDto> 关联记录列表
|
||||
*/
|
||||
async findMany(queryDto: QueryZulipAccountDto = {}): Promise<ZulipAccountListResponseDto> {
|
||||
this.logStart('查询多个Zulip账号关联', queryDto);
|
||||
|
||||
try {
|
||||
const options = {
|
||||
gameUserId: queryDto.gameUserId ? this.parseGameUserId(queryDto.gameUserId) : undefined,
|
||||
zulipUserId: queryDto.zulipUserId,
|
||||
zulipEmail: queryDto.zulipEmail,
|
||||
status: queryDto.status,
|
||||
includeGameUser: queryDto.includeGameUser || false,
|
||||
};
|
||||
|
||||
const accounts = await this.repository.findMany(options);
|
||||
|
||||
this.logSuccess('查询多个Zulip账号关联', {
|
||||
count: accounts.length,
|
||||
conditions: queryDto
|
||||
});
|
||||
|
||||
return this.buildListResponse(accounts);
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
accounts: this.handleSearchError(error, '查询多个Zulip账号关联', queryDto),
|
||||
total: 0,
|
||||
count: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取需要验证的账号列表
|
||||
*
|
||||
* @param maxAge 最大验证间隔(毫秒),默认24小时
|
||||
* @returns Promise<ZulipAccountListResponseDto> 需要验证的账号列表
|
||||
*/
|
||||
async findAccountsNeedingVerification(maxAge: number = DEFAULT_VERIFICATION_MAX_AGE): Promise<ZulipAccountListResponseDto> {
|
||||
this.logStart('获取需要验证的账号列表', { maxAge });
|
||||
|
||||
try {
|
||||
const accounts = await this.repository.findAccountsNeedingVerification(maxAge);
|
||||
|
||||
this.logSuccess('获取需要验证的账号列表', { count: accounts.length });
|
||||
|
||||
return this.buildListResponse(accounts);
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
accounts: this.handleSearchError(error, '获取需要验证的账号列表', { maxAge }),
|
||||
total: 0,
|
||||
count: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误状态的账号列表
|
||||
*
|
||||
* @param maxRetryCount 最大重试次数,默认3次
|
||||
* @returns Promise<ZulipAccountListResponseDto> 错误状态的账号列表
|
||||
*/
|
||||
async findErrorAccounts(maxRetryCount: number = DEFAULT_MAX_RETRY_COUNT): Promise<ZulipAccountListResponseDto> {
|
||||
this.logStart('获取错误状态的账号列表', { maxRetryCount });
|
||||
|
||||
try {
|
||||
const accounts = await this.repository.findErrorAccounts(maxRetryCount);
|
||||
|
||||
this.logSuccess('获取错误状态的账号列表', { count: accounts.length });
|
||||
|
||||
return this.buildListResponse(accounts);
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
accounts: this.handleSearchError(error, '获取错误状态的账号列表', { maxRetryCount }),
|
||||
total: 0,
|
||||
count: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新账号状态
|
||||
*
|
||||
* @param ids 账号ID列表
|
||||
* @param status 新状态
|
||||
* @returns Promise<BatchUpdateResponseDto> 批量更新结果
|
||||
*/
|
||||
async batchUpdateStatus(ids: string[], status: 'active' | 'inactive' | 'suspended' | 'error'): Promise<BatchUpdateResponseDto> {
|
||||
const monitor = this.createPerformanceMonitor('批量更新账号状态', { count: ids.length, status });
|
||||
|
||||
try {
|
||||
const bigintIds = this.parseIds(ids);
|
||||
const updatedCount = await this.repository.batchUpdateStatus(bigintIds, status);
|
||||
|
||||
monitor.success({
|
||||
requestCount: ids.length,
|
||||
updatedCount,
|
||||
status
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
updatedCount,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('批量更新账号状态失败', {
|
||||
operation: 'batchUpdateStatus',
|
||||
error: this.formatError(error),
|
||||
count: ids.length,
|
||||
status,
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
updatedCount: 0,
|
||||
error: this.formatError(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账号状态统计
|
||||
*
|
||||
* @returns Promise<ZulipAccountStatsResponseDto> 状态统计
|
||||
*/
|
||||
async getStatusStatistics(): Promise<ZulipAccountStatsResponseDto> {
|
||||
this.logStart('获取账号状态统计');
|
||||
|
||||
try {
|
||||
const statistics = await this.repository.getStatusStatistics();
|
||||
|
||||
const result = {
|
||||
active: statistics.active || 0,
|
||||
inactive: statistics.inactive || 0,
|
||||
suspended: statistics.suspended || 0,
|
||||
error: statistics.error || 0,
|
||||
total: (statistics.active || 0) + (statistics.inactive || 0) +
|
||||
(statistics.suspended || 0) + (statistics.error || 0),
|
||||
};
|
||||
|
||||
this.logSuccess('获取账号状态统计', result);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
this.handleDataAccessError(error, '获取账号状态统计');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证账号有效性
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @returns Promise<VerifyAccountResponseDto> 验证结果
|
||||
*/
|
||||
async verifyAccount(gameUserId: string): Promise<VerifyAccountResponseDto> {
|
||||
const monitor = this.createPerformanceMonitor('验证账号有效性', { gameUserId });
|
||||
|
||||
try {
|
||||
// 1. 查找账号关联
|
||||
const account = await this.repository.findByGameUserId(this.parseGameUserId(gameUserId));
|
||||
|
||||
if (!account) {
|
||||
monitor.success({ isValid: false, reason: '账号关联不存在' });
|
||||
return {
|
||||
success: false,
|
||||
isValid: false,
|
||||
error: '账号关联不存在',
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 检查账号状态
|
||||
if (account.status !== 'active') {
|
||||
monitor.success({ isValid: false, reason: `账号状态为 ${account.status}` });
|
||||
return {
|
||||
success: true,
|
||||
isValid: false,
|
||||
error: `账号状态为 ${account.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 更新验证时间
|
||||
await this.repository.updateByGameUserId(this.parseGameUserId(gameUserId), {
|
||||
lastVerifiedAt: new Date(),
|
||||
});
|
||||
|
||||
monitor.success({ isValid: true });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
isValid: true,
|
||||
verifiedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('验证账号有效性失败', {
|
||||
operation: 'verifyAccount',
|
||||
gameUserId,
|
||||
error: this.formatError(error),
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
isValid: false,
|
||||
error: this.formatError(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查邮箱是否已存在
|
||||
*
|
||||
* @param zulipEmail Zulip邮箱
|
||||
* @param excludeId 排除的记录ID
|
||||
* @returns Promise<boolean> 是否已存在
|
||||
*/
|
||||
async existsByEmail(zulipEmail: string, excludeId?: string): Promise<boolean> {
|
||||
try {
|
||||
const excludeBigintId = excludeId ? this.parseId(excludeId) : undefined;
|
||||
return await this.repository.existsByEmail(zulipEmail, excludeBigintId);
|
||||
} catch (error) {
|
||||
this.logger.warn('检查邮箱存在性失败', {
|
||||
operation: 'existsByEmail',
|
||||
zulipEmail,
|
||||
error: this.formatError(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Zulip用户ID是否已存在
|
||||
*
|
||||
* @param zulipUserId Zulip用户ID
|
||||
* @param excludeId 排除的记录ID
|
||||
* @returns Promise<boolean> 是否已存在
|
||||
*/
|
||||
async existsByZulipUserId(zulipUserId: number, excludeId?: string): Promise<boolean> {
|
||||
try {
|
||||
const excludeBigintId = excludeId ? this.parseId(excludeId) : undefined;
|
||||
return await this.repository.existsByZulipUserId(zulipUserId, excludeBigintId);
|
||||
} catch (error) {
|
||||
this.logger.warn('检查Zulip用户ID存在性失败', {
|
||||
operation: 'existsByZulipUserId',
|
||||
zulipUserId,
|
||||
error: this.formatError(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将实体转换为响应DTO
|
||||
*
|
||||
* @param account 账号关联实体
|
||||
* @returns ZulipAccountResponseDto 响应DTO
|
||||
*/
|
||||
protected toResponseDto(account: ZulipAccounts): ZulipAccountResponseDto {
|
||||
return {
|
||||
id: account.id.toString(),
|
||||
gameUserId: account.gameUserId.toString(),
|
||||
zulipUserId: account.zulipUserId,
|
||||
zulipEmail: account.zulipEmail,
|
||||
zulipFullName: account.zulipFullName,
|
||||
status: account.status,
|
||||
lastVerifiedAt: account.lastVerifiedAt?.toISOString(),
|
||||
lastSyncedAt: account.lastSyncedAt?.toISOString(),
|
||||
errorMessage: account.errorMessage,
|
||||
retryCount: account.retryCount,
|
||||
createdAt: account.createdAt.toISOString(),
|
||||
updatedAt: account.updatedAt.toISOString(),
|
||||
gameUser: account.gameUser,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user