Files
whale-town-end/src/business/admin/database_management.service.ts
moyin 5f662ef091 feat: 完善管理员系统和用户管理模块
- 更新管理员控制器和数据库管理功能
- 完善管理员操作日志系统
- 添加全面的属性测试覆盖
- 优化用户管理和用户档案服务
- 更新代码检查规范文档

功能改进:
- 增强管理员权限验证
- 完善操作日志记录
- 优化数据库管理接口
- 提升系统安全性和可维护性
2026-01-09 17:05:08 +08:00

702 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 数据库管理服务
*
* 功能描述:
* - 提供统一的数据库管理接口集成所有数据库服务的CRUD操作
* - 实现管理员专用的数据库操作功能
* - 提供统一的响应格式和错误处理
* - 支持操作日志记录和审计功能
*
* 职责分离:
* - 业务逻辑编排:协调各个数据库服务的操作
* - 数据转换DTO与实体之间的转换
* - 权限控制:确保只有管理员可以执行操作
* - 日志记录:记录所有数据库操作的详细日志
*
* 集成的服务:
* - UsersService: 用户数据管理
* - UserProfilesService: 用户档案管理
* - ZulipAccountsService: Zulip账号关联管理
*
* 最近修改:
* - 2026-01-09: Bug修复 - 修复类型错误正确处理skin_id类型转换和Zulip账号查询参数 (修改者: moyin)
* - 2026-01-09: 功能实现 - 实现所有TODO项完成UserProfiles和ZulipAccounts的CRUD操作 (修改者: moyin)
* - 2026-01-09: 代码质量优化 - 替换any类型为具体的DTO类型提高类型安全性 (修改者: moyin)
* - 2026-01-09: 代码质量优化 - 统一使用admin_utils中的响应创建函数消除重复代码 (修改者: moyin)
* - 2026-01-08: 注释规范优化 - 修正@author字段更新版本号和修改记录 (修改者: moyin)
* - 2026-01-08: 注释规范优化 - 完善方法注释,添加@param、@returns、@throws和@example (修改者: moyin)
* - 2026-01-08: 代码规范优化 - 将魔法数字20提取为常量DEFAULT_PAGE_SIZE (修改者: moyin)
* - 2026-01-08: 代码质量优化 - 提取用户格式化逻辑,补充缺失方法实现,使用操作监控工具 (修改者: moyin)
* - 2026-01-08: 功能新增 - 创建数据库管理服务,支持管理员数据库操作 (修改者: assistant)
*
* @author moyin
* @version 1.6.0
* @since 2026-01-08
* @lastModified 2026-01-09
*/
import { Injectable, Logger, NotFoundException, BadRequestException, ConflictException, Inject } from '@nestjs/common';
import { UsersService } from '../../core/db/users/users.service';
import { UserProfilesService } from '../../core/db/user_profiles/user_profiles.service';
import { UserProfiles } from '../../core/db/user_profiles/user_profiles.entity';
import { ZulipAccountsService } from '../../core/db/zulip_accounts/zulip_accounts.service';
import { ZulipAccountResponseDto } from '../../core/db/zulip_accounts/zulip_accounts.dto';
import { getCurrentTimestamp, UserFormatter, OperationMonitor, createSuccessResponse, createErrorResponse, createListResponse } from './admin_utils';
import {
AdminCreateUserDto,
AdminUpdateUserDto,
AdminCreateUserProfileDto,
AdminUpdateUserProfileDto,
AdminCreateZulipAccountDto,
AdminUpdateZulipAccountDto
} from './admin_database.dto';
/**
* 常量定义
*/
const DEFAULT_PAGE_SIZE = 20;
/**
* 管理员API统一响应格式
*/
export interface AdminApiResponse<T = any> {
success: boolean;
data?: T;
message: string;
error_code?: string;
timestamp?: string;
request_id?: string;
}
/**
* 管理员列表响应格式
*/
export interface AdminListResponse<T = any> {
success: boolean;
data: {
items: T[];
total: number;
limit: number;
offset: number;
has_more: boolean;
};
message: string;
error_code?: string;
timestamp?: string;
request_id?: string;
}
@Injectable()
export class DatabaseManagementService {
private readonly logger = new Logger(DatabaseManagementService.name);
constructor(
@Inject('UsersService') private readonly usersService: UsersService,
@Inject('IUserProfilesService') private readonly userProfilesService: UserProfilesService,
@Inject('ZulipAccountsService') private readonly zulipAccountsService: ZulipAccountsService,
) {
this.logger.log('DatabaseManagementService初始化完成');
}
/**
* 记录操作日志
*
* @param level 日志级别
* @param message 日志消息
* @param context 日志上下文
*/
private logOperation(level: 'log' | 'warn' | 'error', message: string, context: Record<string, any>): void {
this.logger[level](message, {
...context,
timestamp: getCurrentTimestamp()
});
}
/**
* 处理服务异常
*
* @param error 异常对象
* @param operation 操作名称
* @param context 操作上下文
* @returns 错误响应
*/
private handleServiceError(error: any, operation: string, context: Record<string, any>): AdminApiResponse {
this.logOperation('error', `${operation}失败`, {
operation,
error: error instanceof Error ? error.message : String(error),
context
});
if (error instanceof NotFoundException) {
return createErrorResponse(error.message, 'RESOURCE_NOT_FOUND');
}
if (error instanceof ConflictException) {
return createErrorResponse(error.message, 'RESOURCE_CONFLICT');
}
if (error instanceof BadRequestException) {
return createErrorResponse(error.message, 'INVALID_REQUEST');
}
return createErrorResponse(`${operation}失败,请稍后重试`, 'INTERNAL_ERROR');
}
/**
* 处理列表查询异常
*
* @param error 异常对象
* @param operation 操作名称
* @param context 操作上下文
* @returns 空列表响应
*/
private handleListError(error: any, operation: string, context: Record<string, any>): AdminListResponse {
this.logOperation('error', `${operation}失败`, {
operation,
error: error instanceof Error ? error.message : String(error),
context
});
return createListResponse([], 0, context.limit || DEFAULT_PAGE_SIZE, context.offset || 0, `${operation}失败,返回空列表`);
}
// ==================== 用户管理方法 ====================
/**
* 获取用户列表
*
* 功能描述:
* 分页获取系统中的用户列表,支持限制数量和偏移量参数
*
* 业务逻辑:
* 1. 记录操作开始时间和参数
* 2. 调用用户服务获取用户数据和总数
* 3. 格式化用户信息,隐藏敏感字段
* 4. 记录操作成功日志和性能数据
* 5. 返回标准化的列表响应
*
* @param limit 限制数量默认20最大100
* @param offset 偏移量默认0用于分页
* @returns 包含用户列表、总数和分页信息的响应对象
*
* @throws NotFoundException 当查询条件无效时
* @throws InternalServerErrorException 当数据库操作失败时
*
* @example
* ```typescript
* const result = await service.getUserList(20, 0);
* console.log(result.data.items.length); // 用户数量
* console.log(result.data.total); // 总用户数
* ```
*/
async getUserList(limit: number = DEFAULT_PAGE_SIZE, offset: number = 0): Promise<AdminListResponse> {
return await OperationMonitor.executeWithMonitoring(
'获取用户列表',
{ limit, offset },
async () => {
const users = await this.usersService.findAll(limit, offset);
const total = await this.usersService.count();
const formattedUsers = users.map(user => UserFormatter.formatBasicUser(user));
return createListResponse(formattedUsers, total, limit, offset, '用户列表获取成功');
},
this.logOperation.bind(this)
).catch(error => this.handleListError(error, '获取用户列表', { limit, offset }));
}
/**
* 根据ID获取用户详情
*
* 功能描述:
* 根据用户ID获取指定用户的详细信息
*
* 业务逻辑:
* 1. 记录操作开始时间和用户ID
* 2. 调用用户服务查询用户信息
* 3. 格式化用户详细信息
* 4. 记录操作成功日志和性能数据
* 5. 返回标准化的详情响应
*
* @param id 用户ID必须是有效的bigint类型
* @returns 包含用户详细信息的响应对象
*
* @throws NotFoundException 当用户不存在时
* @throws BadRequestException 当用户ID格式无效时
* @throws InternalServerErrorException 当数据库操作失败时
*
* @example
* ```typescript
* const result = await service.getUserById(BigInt(123));
* console.log(result.data.username); // 用户名
* console.log(result.data.email); // 邮箱
* ```
*/
async getUserById(id: bigint): Promise<AdminApiResponse> {
return await OperationMonitor.executeWithMonitoring(
'获取用户详情',
{ userId: id.toString() },
async () => {
const user = await this.usersService.findOne(id);
const formattedUser = UserFormatter.formatDetailedUser(user);
return createSuccessResponse(formattedUser, '用户详情获取成功');
},
this.logOperation.bind(this)
).catch(error => this.handleServiceError(error, '获取用户详情', { userId: id.toString() }));
}
/**
* 搜索用户
*
* 功能描述:
* 根据关键词搜索用户,支持用户名、邮箱、昵称等字段的模糊匹配
*
* 业务逻辑:
* 1. 记录搜索操作开始时间和关键词
* 2. 调用用户服务执行搜索查询
* 3. 格式化搜索结果
* 4. 记录搜索成功日志和性能数据
* 5. 返回标准化的搜索响应
*
* @param keyword 搜索关键词,支持用户名、邮箱、昵称的模糊匹配
* @param limit 返回结果数量限制默认20最大50
* @returns 包含搜索结果的响应对象
*
* @throws BadRequestException 当关键词为空或格式无效时
* @throws InternalServerErrorException 当搜索操作失败时
*
* @example
* ```typescript
* const result = await service.searchUsers('admin', 10);
* console.log(result.data.items); // 搜索结果列表
* ```
*/
async searchUsers(keyword: string, limit: number = DEFAULT_PAGE_SIZE): Promise<AdminListResponse> {
return await OperationMonitor.executeWithMonitoring(
'搜索用户',
{ keyword, limit },
async () => {
const users = await this.usersService.search(keyword, limit);
const formattedUsers = users.map(user => UserFormatter.formatBasicUser(user));
return createListResponse(formattedUsers, users.length, limit, 0, '用户搜索成功');
},
this.logOperation.bind(this)
).catch(error => this.handleListError(error, '搜索用户', { keyword, limit }));
}
/**
* 创建用户
*
* @param userData 用户数据
* @returns 创建结果响应
*/
async createUser(userData: AdminCreateUserDto): Promise<AdminApiResponse> {
return await OperationMonitor.executeWithMonitoring(
'创建用户',
{ username: userData.username },
async () => {
const newUser = await this.usersService.create(userData);
const formattedUser = UserFormatter.formatBasicUser(newUser);
return createSuccessResponse(formattedUser, '用户创建成功');
},
this.logOperation.bind(this)
).catch(error => this.handleServiceError(error, '创建用户', { username: userData.username }));
}
/**
* 更新用户
*
* @param id 用户ID
* @param updateData 更新数据
* @returns 更新结果响应
*/
async updateUser(id: bigint, updateData: AdminUpdateUserDto): Promise<AdminApiResponse> {
return await OperationMonitor.executeWithMonitoring(
'更新用户',
{ userId: id.toString(), updateFields: Object.keys(updateData) },
async () => {
const updatedUser = await this.usersService.update(id, updateData);
const formattedUser = UserFormatter.formatBasicUser(updatedUser);
return createSuccessResponse(formattedUser, '用户更新成功');
},
this.logOperation.bind(this)
).catch(error => this.handleServiceError(error, '更新用户', { userId: id.toString(), updateData }));
}
/**
* 删除用户
*
* @param id 用户ID
* @returns 删除结果响应
*/
async deleteUser(id: bigint): Promise<AdminApiResponse> {
return await OperationMonitor.executeWithMonitoring(
'删除用户',
{ userId: id.toString() },
async () => {
await this.usersService.remove(id);
return createSuccessResponse({ deleted: true, id: id.toString() }, '用户删除成功');
},
this.logOperation.bind(this)
).catch(error => this.handleServiceError(error, '删除用户', { userId: id.toString() }));
}
// ==================== 用户档案管理方法 ====================
/**
* 获取用户档案列表
*
* @param limit 限制数量
* @param offset 偏移量
* @returns 用户档案列表响应
*/
async getUserProfileList(limit: number = DEFAULT_PAGE_SIZE, offset: number = 0): Promise<AdminListResponse> {
return await OperationMonitor.executeWithMonitoring(
'获取用户档案列表',
{ limit, offset },
async () => {
const profiles = await this.userProfilesService.findAll({ limit, offset });
const total = await this.userProfilesService.count();
const formattedProfiles = profiles.map(profile => this.formatUserProfile(profile));
return createListResponse(formattedProfiles, total, limit, offset, '用户档案列表获取成功');
},
this.logOperation.bind(this)
).catch(error => this.handleListError(error, '获取用户档案列表', { limit, offset }));
}
/**
* 根据ID获取用户档案详情
*
* @param id 档案ID
* @returns 用户档案详情响应
*/
async getUserProfileById(id: bigint): Promise<AdminApiResponse> {
return await OperationMonitor.executeWithMonitoring(
'获取用户档案详情',
{ profileId: id.toString() },
async () => {
const profile = await this.userProfilesService.findOne(id);
const formattedProfile = this.formatUserProfile(profile);
return createSuccessResponse(formattedProfile, '用户档案详情获取成功');
},
this.logOperation.bind(this)
).catch(error => this.handleServiceError(error, '获取用户档案详情', { profileId: id.toString() }));
}
/**
* 根据地图获取用户档案
*
* @param mapId 地图ID
* @param limit 限制数量
* @param offset 偏移量
* @returns 用户档案列表响应
*/
async getUserProfilesByMap(mapId: string, limit: number = DEFAULT_PAGE_SIZE, offset: number = 0): Promise<AdminListResponse> {
return await OperationMonitor.executeWithMonitoring(
'根据地图获取用户档案',
{ mapId, limit, offset },
async () => {
const profiles = await this.userProfilesService.findByMap(mapId, undefined, limit, offset);
const total = await this.userProfilesService.count();
const formattedProfiles = profiles.map(profile => this.formatUserProfile(profile));
return createListResponse(formattedProfiles, total, limit, offset, `地图 ${mapId} 的用户档案列表获取成功`);
},
this.logOperation.bind(this)
).catch(error => this.handleListError(error, '根据地图获取用户档案', { mapId, limit, offset }));
}
/**
* 创建用户档案
*
* @param createProfileDto 创建数据
* @returns 创建结果响应
*/
async createUserProfile(createProfileDto: AdminCreateUserProfileDto): Promise<AdminApiResponse> {
return await OperationMonitor.executeWithMonitoring(
'创建用户档案',
{ userId: createProfileDto.user_id },
async () => {
const profileData = {
user_id: BigInt(createProfileDto.user_id),
bio: createProfileDto.bio,
resume_content: createProfileDto.resume_content,
tags: createProfileDto.tags ? JSON.parse(createProfileDto.tags) : undefined,
social_links: createProfileDto.social_links ? JSON.parse(createProfileDto.social_links) : undefined,
skin_id: createProfileDto.skin_id ? parseInt(createProfileDto.skin_id) : undefined,
current_map: createProfileDto.current_map,
pos_x: createProfileDto.pos_x,
pos_y: createProfileDto.pos_y,
status: createProfileDto.status
};
const newProfile = await this.userProfilesService.create(profileData);
const formattedProfile = this.formatUserProfile(newProfile);
return createSuccessResponse(formattedProfile, '用户档案创建成功');
},
this.logOperation.bind(this)
).catch(error => this.handleServiceError(error, '创建用户档案', { userId: createProfileDto.user_id }));
}
/**
* 更新用户档案
*
* @param id 档案ID
* @param updateProfileDto 更新数据
* @returns 更新结果响应
*/
async updateUserProfile(id: bigint, updateProfileDto: AdminUpdateUserProfileDto): Promise<AdminApiResponse> {
return await OperationMonitor.executeWithMonitoring(
'更新用户档案',
{ profileId: id.toString(), updateFields: Object.keys(updateProfileDto) },
async () => {
// 转换AdminUpdateUserProfileDto为UpdateUserProfileDto
const updateData: any = {};
if (updateProfileDto.bio !== undefined) {
updateData.bio = updateProfileDto.bio;
}
if (updateProfileDto.resume_content !== undefined) {
updateData.resume_content = updateProfileDto.resume_content;
}
if (updateProfileDto.tags !== undefined) {
updateData.tags = JSON.parse(updateProfileDto.tags);
}
if (updateProfileDto.social_links !== undefined) {
updateData.social_links = JSON.parse(updateProfileDto.social_links);
}
if (updateProfileDto.skin_id !== undefined) {
updateData.skin_id = parseInt(updateProfileDto.skin_id);
}
if (updateProfileDto.current_map !== undefined) {
updateData.current_map = updateProfileDto.current_map;
}
if (updateProfileDto.pos_x !== undefined) {
updateData.pos_x = updateProfileDto.pos_x;
}
if (updateProfileDto.pos_y !== undefined) {
updateData.pos_y = updateProfileDto.pos_y;
}
if (updateProfileDto.status !== undefined) {
updateData.status = updateProfileDto.status;
}
const updatedProfile = await this.userProfilesService.update(id, updateData);
const formattedProfile = this.formatUserProfile(updatedProfile);
return createSuccessResponse(formattedProfile, '用户档案更新成功');
},
this.logOperation.bind(this)
).catch(error => this.handleServiceError(error, '更新用户档案', { profileId: id.toString(), updateData: updateProfileDto }));
}
/**
* 删除用户档案
*
* @param id 档案ID
* @returns 删除结果响应
*/
async deleteUserProfile(id: bigint): Promise<AdminApiResponse> {
return await OperationMonitor.executeWithMonitoring(
'删除用户档案',
{ profileId: id.toString() },
async () => {
const result = await this.userProfilesService.remove(id);
return createSuccessResponse({ deleted: true, id: id.toString(), affected: result.affected }, '用户档案删除成功');
},
this.logOperation.bind(this)
).catch(error => this.handleServiceError(error, '删除用户档案', { profileId: id.toString() }));
}
// ==================== Zulip账号关联管理方法 ====================
/**
* 获取Zulip账号关联列表
*
* @param limit 限制数量
* @param offset 偏移量
* @returns Zulip账号关联列表响应
*/
async getZulipAccountList(limit: number = DEFAULT_PAGE_SIZE, offset: number = 0): Promise<AdminListResponse> {
return await OperationMonitor.executeWithMonitoring(
'获取Zulip账号关联列表',
{ limit, offset },
async () => {
// ZulipAccountsService的findMany方法目前不支持分页参数
// 先获取所有数据,然后手动分页
const result = await this.zulipAccountsService.findMany({});
// 手动实现分页
const startIndex = offset;
const endIndex = offset + limit;
const paginatedAccounts = result.accounts.slice(startIndex, endIndex);
const formattedAccounts = paginatedAccounts.map(account => this.formatZulipAccount(account));
return createListResponse(formattedAccounts, result.total, limit, offset, 'Zulip账号关联列表获取成功');
},
this.logOperation.bind(this)
).catch(error => this.handleListError(error, '获取Zulip账号关联列表', { limit, offset }));
}
/**
* 根据ID获取Zulip账号关联详情
*
* @param id 关联ID
* @returns Zulip账号关联详情响应
*/
async getZulipAccountById(id: string): Promise<AdminApiResponse> {
return await OperationMonitor.executeWithMonitoring(
'获取Zulip账号关联详情',
{ accountId: id },
async () => {
const account = await this.zulipAccountsService.findById(id, true);
const formattedAccount = this.formatZulipAccount(account);
return createSuccessResponse(formattedAccount, 'Zulip账号关联详情获取成功');
},
this.logOperation.bind(this)
).catch(error => this.handleServiceError(error, '获取Zulip账号关联详情', { accountId: id }));
}
/**
* 获取Zulip账号关联统计
*
* @returns 统计信息响应
*/
async getZulipAccountStatistics(): Promise<AdminApiResponse> {
return await OperationMonitor.executeWithMonitoring(
'获取Zulip账号关联统计',
{},
async () => {
const stats = await this.zulipAccountsService.getStatusStatistics();
return createSuccessResponse(stats, 'Zulip账号关联统计获取成功');
},
this.logOperation.bind(this)
).catch(error => this.handleServiceError(error, '获取Zulip账号关联统计', {}));
}
/**
* 创建Zulip账号关联
*
* @param createAccountDto 创建数据
* @returns 创建结果响应
*/
async createZulipAccount(createAccountDto: AdminCreateZulipAccountDto): Promise<AdminApiResponse> {
return await OperationMonitor.executeWithMonitoring(
'创建Zulip账号关联',
{ gameUserId: createAccountDto.gameUserId },
async () => {
const newAccount = await this.zulipAccountsService.create(createAccountDto);
const formattedAccount = this.formatZulipAccount(newAccount);
return createSuccessResponse(formattedAccount, 'Zulip账号关联创建成功');
},
this.logOperation.bind(this)
).catch(error => this.handleServiceError(error, '创建Zulip账号关联', { gameUserId: createAccountDto.gameUserId }));
}
/**
* 更新Zulip账号关联
*
* @param id 关联ID
* @param updateAccountDto 更新数据
* @returns 更新结果响应
*/
async updateZulipAccount(id: string, updateAccountDto: AdminUpdateZulipAccountDto): Promise<AdminApiResponse> {
return await OperationMonitor.executeWithMonitoring(
'更新Zulip账号关联',
{ accountId: id, updateFields: Object.keys(updateAccountDto) },
async () => {
const updatedAccount = await this.zulipAccountsService.update(id, updateAccountDto);
const formattedAccount = this.formatZulipAccount(updatedAccount);
return createSuccessResponse(formattedAccount, 'Zulip账号关联更新成功');
},
this.logOperation.bind(this)
).catch(error => this.handleServiceError(error, '更新Zulip账号关联', { accountId: id, updateData: updateAccountDto }));
}
/**
* 删除Zulip账号关联
*
* @param id 关联ID
* @returns 删除结果响应
*/
async deleteZulipAccount(id: string): Promise<AdminApiResponse> {
return await OperationMonitor.executeWithMonitoring(
'删除Zulip账号关联',
{ accountId: id },
async () => {
const result = await this.zulipAccountsService.delete(id);
return createSuccessResponse({ deleted: result, id }, 'Zulip账号关联删除成功');
},
this.logOperation.bind(this)
).catch(error => this.handleServiceError(error, '删除Zulip账号关联', { accountId: id }));
}
/**
* 批量更新Zulip账号状态
*
* @param ids ID列表
* @param status 新状态
* @param reason 操作原因
* @returns 批量更新结果响应
*/
async batchUpdateZulipAccountStatus(ids: string[], status: string, reason?: string): Promise<AdminApiResponse> {
return await OperationMonitor.executeWithMonitoring(
'批量更新Zulip账号状态',
{ count: ids.length, status, reason },
async () => {
const result = await this.zulipAccountsService.batchUpdateStatus(ids, status as any);
return createSuccessResponse({
success_count: result.updatedCount,
failed_count: ids.length - result.updatedCount,
total_count: ids.length,
reason
}, `Zulip账号关联批量状态更新完成成功${result.updatedCount},失败:${ids.length - result.updatedCount}`);
},
this.logOperation.bind(this)
).catch(error => this.handleServiceError(error, '批量更新Zulip账号状态', { count: ids.length, status, reason }));
}
/**
* 格式化用户档案信息
*
* @param profile 用户档案实体
* @returns 格式化的用户档案信息
*/
private formatUserProfile(profile: UserProfiles) {
return {
id: profile.id.toString(),
user_id: profile.user_id.toString(),
bio: profile.bio,
resume_content: profile.resume_content,
tags: profile.tags,
social_links: profile.social_links,
skin_id: profile.skin_id,
current_map: profile.current_map,
pos_x: profile.pos_x,
pos_y: profile.pos_y,
status: profile.status,
last_login_at: profile.last_login_at,
last_position_update: profile.last_position_update
};
}
/**
* 格式化Zulip账号关联信息
*
* @param account Zulip账号关联实体
* @returns 格式化的Zulip账号关联信息
*/
private formatZulipAccount(account: ZulipAccountResponseDto) {
return {
id: account.id,
gameUserId: account.gameUserId,
zulipUserId: account.zulipUserId,
zulipEmail: account.zulipEmail,
zulipFullName: account.zulipFullName,
status: account.status,
lastVerifiedAt: account.lastVerifiedAt,
lastSyncedAt: account.lastSyncedAt,
errorMessage: account.errorMessage,
retryCount: account.retryCount,
createdAt: account.createdAt,
updatedAt: account.updatedAt,
gameUser: account.gameUser
};
}
}