refactor:项目架构重构和命名规范化

- 统一文件命名为snake_case格式(kebab-case  snake_case)
- 重构zulip模块为zulip_core,明确Core层职责
- 重构user-mgmt模块为user_mgmt,统一命名规范
- 调整模块依赖关系,优化架构分层
- 删除过时的文件和目录结构
- 更新相关文档和配置文件

本次重构涉及大量文件重命名和模块重组,
旨在建立更清晰的项目架构和统一的命名规范。
This commit is contained in:
moyin
2026-01-08 00:14:14 +08:00
parent 4fa4bd1a70
commit bb796a2469
178 changed files with 24767 additions and 3484 deletions

View File

@@ -0,0 +1,689 @@
/**
* 文件模拟Redis服务实现
*
* 功能描述:
* - 在本地开发环境中使用文件系统模拟Redis功能
* - 支持完整的Redis基础操作和过期机制
* - 提供数据持久化和自动过期清理功能
* - 适用于开发测试环境的Redis功能模拟
*
* 职责分离:
* - 数据存储使用JSON文件持久化Redis数据
* - 过期管理实现TTL机制和自动过期清理
* - 接口实现完整实现IRedisService接口规范
* - 文件操作:管理数据文件的读写和目录创建
*
* 最近修改:
* - 2025-01-07: 代码规范优化 - 为所有公共方法添加完整的三级注释,包含业务逻辑和示例代码
* - 2025-01-07: 代码规范优化 - 修复常量命名规范,为主要方法添加完整的三级注释
* - 2025-01-07: 代码规范优化 - 完善文件头注释和方法注释,添加详细业务逻辑说明
*
* @author moyin
* @version 1.0.3
* @since 2025-01-07
* @lastModified 2025-01-07
*/
import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
import { promises as fs } from 'fs';
import * as path from 'path';
import { IRedisService } from './redis.interface';
/**
* 文件模拟Redis服务
*
* 职责:
* - 在本地开发环境中使用文件系统模拟Redis功能
* - 实现完整的Redis操作接口
* - 管理数据持久化和过期清理
*
* 主要方法:
* - initializeStorage() - 初始化文件存储
* - loadData/saveData() - 数据文件读写
* - cleanExpiredKeys() - 过期键清理
* - set/get/del() - 基础键值操作
*
* 使用场景:
* - 本地开发环境的Redis功能模拟
* - 单元测试和集成测试
* - 无需真实Redis服务器的开发场景
*/
@Injectable()
export class FileRedisService implements IRedisService, OnModuleDestroy {
private readonly logger = new Logger(FileRedisService.name);
private readonly DATA_DIR = path.join(process.cwd(), 'redis-data');
private readonly DATA_FILE = path.join(this.DATA_DIR, 'redis.json');
private readonly CLEANUP_INTERVAL = 60000; // 每分钟清理一次过期键
private data: Map<string, { value: string; expireAt?: number }> = new Map();
private cleanupTimer?: NodeJS.Timeout;
constructor() {
this.initializeStorage();
}
/**
* 初始化存储
*
* 业务逻辑:
* 1. 创建数据存储目录(如果不存在)
* 2. 尝试从文件加载现有数据
* 3. 启动定时过期清理任务
* 4. 记录初始化状态日志
*
* @throws Error 文件系统操作失败时
*
* @example
* ```typescript
* // 在构造函数中自动调用
* constructor() {
* this.initializeStorage();
* }
* ```
*/
async initializeStorage(): Promise<void> {
try {
// 确保数据目录存在
await fs.mkdir(this.DATA_DIR, { recursive: true });
// 尝试加载现有数据
await this.loadData();
// 启动过期清理任务
this.startExpirationCleanup();
this.logger.log('文件Redis服务初始化完成');
} catch (error) {
this.logger.error('初始化文件Redis服务失败', error);
}
}
/**
* 从文件加载数据
*
* 业务逻辑:
* 1. 读取JSON数据文件内容
* 2. 解析JSON数据并转换为Map结构
* 3. 检查并过滤已过期的数据项
* 4. 初始化内存数据存储
* 5. 记录加载的数据条数
*
* @throws Error 文件读取或JSON解析失败时
*
* @example
* ```typescript
* await this.loadData();
* console.log(`加载了 ${this.data.size} 条数据`);
* ```
*/
private async loadData(): Promise<void> {
try {
const fileContent = await fs.readFile(this.DATA_FILE, 'utf-8');
const jsonData = JSON.parse(fileContent);
this.data = new Map();
for (const [key, item] of Object.entries(jsonData)) {
const typedItem = item as { value: string; expireAt?: number };
// 检查是否已过期
if (!typedItem.expireAt || typedItem.expireAt > Date.now()) {
this.data.set(key, typedItem);
}
}
this.logger.log(`从文件加载了 ${this.data.size} 条Redis数据`);
} catch (error) {
// 文件不存在或格式错误,使用空数据
this.data = new Map();
this.logger.log('初始化空的Redis数据存储');
}
}
/**
* 保存数据到文件
*
* 业务逻辑:
* 1. 确保数据目录存在
* 2. 将内存中的Map数据转换为JSON对象
* 3. 格式化JSON字符串缩进2个空格
* 4. 异步写入到数据文件
* 5. 处理文件写入异常
*
* @throws Error 文件写入失败时
*
* @example
* ```typescript
* this.data.set('key', { value: 'data' });
* await this.saveData();
* ```
*/
private async saveData(): Promise<void> {
try {
// 确保数据目录存在
const dataDir = path.dirname(this.DATA_FILE);
await fs.mkdir(dataDir, { recursive: true });
const jsonData = Object.fromEntries(this.data);
await fs.writeFile(this.DATA_FILE, JSON.stringify(jsonData, null, 2));
} catch (error) {
this.logger.error('保存Redis数据到文件失败', error);
}
}
/**
* 启动过期清理任务
*
* 业务逻辑:
* 1. 清理现有定时器(如果存在)
* 2. 设置定时器每60秒执行一次清理
* 3. 调用cleanExpiredKeys方法清理过期数据
* 4. 确保应用运行期间持续清理过期键
* 5. 保存定时器引用以便后续清理
*
* @example
* ```typescript
* this.startExpirationCleanup();
* // 每分钟自动清理过期键
* ```
*/
private startExpirationCleanup(): void {
// 清理现有定时器
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
}
this.cleanupTimer = setInterval(async () => {
await this.cleanExpiredKeys();
}, this.CLEANUP_INTERVAL);
}
/**
* 清理过期的键
*
* 业务逻辑:
* 1. 获取当前时间戳
* 2. 遍历所有数据项检查过期时间
* 3. 删除已过期的键值对
* 4. 统计清理的键数量
* 5. 如有清理则保存数据并记录日志
*
* @example
* ```typescript
* await this.cleanExpiredKeys();
* // 清理了 3 个过期的Redis键
* ```
*/
private async cleanExpiredKeys(): Promise<void> {
const now = Date.now();
let cleanedCount = 0;
for (const [key, item] of this.data.entries()) {
if (item.expireAt && item.expireAt <= now) {
this.data.delete(key);
cleanedCount++;
}
}
if (cleanedCount > 0) {
this.logger.log(`清理了 ${cleanedCount} 个过期的Redis键`);
await this.saveData(); // 保存清理后的数据
}
}
/**
* 设置键值对
*
* 业务逻辑:
* 1. 创建数据项对象,包含值和可选的过期时间
* 2. 如果设置了TTL计算过期时间戳
* 3. 将数据存储到内存Map中
* 4. 异步保存数据到文件
* 5. 记录操作日志
*
* @param key 键名,不能为空
* @param value 值,支持字符串类型
* @param ttl 可选的过期时间(秒),不设置则永不过期
* @returns Promise<void> 操作完成的Promise
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* await redisService.set('user:123', 'userData', 3600);
* ```
*/
async set(key: string, value: string, ttl?: number): Promise<void> {
const item: { value: string; expireAt?: number } = { value };
if (ttl && ttl > 0) {
item.expireAt = Date.now() + ttl * 1000;
}
this.data.set(key, item);
await this.saveData();
this.logger.debug(`设置Redis键: ${key}, TTL: ${ttl || '永不过期'}`);
}
/**
* 获取键对应的值
*
* 业务逻辑:
* 1. 从内存Map中查找键对应的数据项
* 2. 检查数据项是否存在
* 3. 验证数据项是否已过期
* 4. 如果过期则删除并保存数据
* 5. 返回有效的值或null
*
* @param key 键名,不能为空
* @returns Promise<string | null> 键对应的值不存在或已过期返回null
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* const value = await redisService.get('user:123');
* ```
*/
async get(key: string): Promise<string | null> {
const item = this.data.get(key);
if (!item) {
return null;
}
// 检查是否过期
if (item.expireAt && item.expireAt <= Date.now()) {
this.data.delete(key);
await this.saveData();
return null;
}
return item.value;
}
/**
* 删除指定的键
*
* 业务逻辑:
* 1. 检查键是否存在于内存Map中
* 2. 从内存Map中删除键
* 3. 如果键存在则保存数据到文件
* 4. 记录删除操作日志
* 5. 返回删除是否成功
*
* @param key 键名,不能为空
* @returns Promise<boolean> 删除成功返回true键不存在返回false
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* const deleted = await redisService.del('user:123');
* console.log(deleted ? '删除成功' : '键不存在');
* ```
*/
async del(key: string): Promise<boolean> {
const existed = this.data.has(key);
this.data.delete(key);
if (existed) {
await this.saveData();
this.logger.debug(`删除Redis键: ${key}`);
}
return existed;
}
/**
* 检查键是否存在
*
* 业务逻辑:
* 1. 从内存Map中查找键对应的数据项
* 2. 检查数据项是否存在
* 3. 验证数据项是否已过期
* 4. 如果过期则删除并保存数据
* 5. 返回键的存在状态
*
* @param key 键名,不能为空
* @returns Promise<boolean> 键存在返回true不存在或已过期返回false
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* const exists = await redisService.exists('user:123');
* if (exists) {
* console.log('用户数据存在');
* }
* ```
*/
async exists(key: string): Promise<boolean> {
const item = this.data.get(key);
if (!item) {
return false;
}
// 检查是否过期
if (item.expireAt && item.expireAt <= Date.now()) {
this.data.delete(key);
await this.saveData();
return false;
}
return true;
}
/**
* 设置键的过期时间
*
* 业务逻辑:
* 1. 从内存Map中查找键对应的数据项
* 2. 检查数据项是否存在
* 3. 计算过期时间戳并设置到数据项
* 4. 保存更新后的数据到文件
* 5. 记录过期时间设置日志
*
* @param key 键名,不能为空
* @param ttl 过期时间必须大于0
* @returns Promise<void> 操作完成的Promise
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* await redisService.expire('user:123', 3600); // 1小时后过期
* ```
*/
async expire(key: string, ttl: number): Promise<void> {
const item = this.data.get(key);
if (item) {
item.expireAt = Date.now() + ttl * 1000;
await this.saveData();
this.logger.debug(`设置Redis键过期时间: ${key}, TTL: ${ttl}`);
}
}
/**
* 获取键的剩余过期时间
*
* 业务逻辑:
* 1. 从内存Map中查找键对应的数据项
* 2. 检查数据项是否存在
* 3. 检查是否设置了过期时间
* 4. 计算剩余过期时间
* 5. 如果已过期则删除键并保存数据
*
* @param key 键名,不能为空
* @returns Promise<number> 剩余时间(秒),-1表示永不过期-2表示键不存在
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* const ttl = await redisService.ttl('user:123');
* if (ttl > 0) {
* console.log(`还有${ttl}秒过期`);
* } else if (ttl === -1) {
* console.log('永不过期');
* } else {
* console.log('键不存在');
* }
* ```
*/
async ttl(key: string): Promise<number> {
const item = this.data.get(key);
if (!item) {
return -2; // 键不存在
}
if (!item.expireAt) {
return -1; // 永不过期
}
const remaining = Math.ceil((item.expireAt - Date.now()) / 1000);
if (remaining <= 0) {
// 已过期,删除键
this.data.delete(key);
await this.saveData();
return -2;
}
return remaining;
}
/**
* 清空所有数据
*
* 业务逻辑:
* 1. 清空内存Map中的所有数据
* 2. 保存空数据到文件
* 3. 记录清空操作日志
*
* @returns Promise<void> 操作完成的Promise
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* await redisService.flushall();
* console.log('所有数据已清空');
* ```
*/
async flushall(): Promise<void> {
this.data.clear();
await this.saveData();
this.logger.log('清空所有Redis数据');
}
/**
* 设置键值对并指定过期时间
*
* 业务逻辑:
* 1. 创建数据项对象,包含值和过期时间戳
* 2. 计算过期时间戳(当前时间 + TTL秒数
* 3. 将数据存储到内存Map中
* 4. 异步保存数据到文件
* 5. 记录操作日志
*
* @param key 键名,不能为空
* @param ttl 过期时间必须大于0
* @param value 值,支持字符串类型
* @returns Promise<void> 操作完成的Promise
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* await redisService.setex('session:abc', 1800, 'sessionData');
* ```
*/
async setex(key: string, ttl: number, value: string): Promise<void> {
const item: { value: string; expireAt?: number } = {
value,
expireAt: Date.now() + ttl * 1000,
};
this.data.set(key, item);
await this.saveData();
this.logger.debug(`设置Redis键(setex): ${key}, TTL: ${ttl}`);
}
/**
* 键值自增操作
*
* 业务逻辑:
* 1. 从内存Map中查找键对应的数据项
* 2. 如果键不存在则初始化为1
* 3. 如果键存在则将值转换为数字并加1
* 4. 更新数据项的值
* 5. 保存数据到文件并记录日志
*
* @param key 键名,不能为空
* @returns Promise<number> 自增后的新值
* @throws Error 当文件操作失败或值不是数字时
*
* @example
* ```typescript
* const newValue = await redisService.incr('counter');
* console.log(`计数器新值: ${newValue}`);
* ```
*/
async incr(key: string): Promise<number> {
const item = this.data.get(key);
let newValue: number;
if (!item) {
newValue = 1;
this.data.set(key, { value: '1' });
} else {
newValue = parseInt(item.value, 10) + 1;
item.value = newValue.toString();
}
await this.saveData();
this.logger.debug(`自增Redis键: ${key}, 新值: ${newValue}`);
return newValue;
}
/**
* 向集合添加成员
*
* 业务逻辑:
* 1. 从内存Map中查找键对应的数据项
* 2. 如果键不存在则创建新的Set集合
* 3. 如果键存在则解析JSON数据为Set集合
* 4. 向集合中添加新成员
* 5. 将更新后的集合保存到内存Map和文件
*
* @param key 集合键名,不能为空
* @param member 要添加的成员,不能为空
* @returns Promise<void> 操作完成的Promise
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* await redisService.sadd('users', 'user123');
* ```
*/
async sadd(key: string, member: string): Promise<void> {
const item = this.data.get(key);
let members: Set<string>;
if (!item) {
members = new Set([member]);
} else {
members = new Set(JSON.parse(item.value));
members.add(member);
}
this.data.set(key, { value: JSON.stringify([...members]), expireAt: item?.expireAt });
await this.saveData();
this.logger.debug(`添加集合成员: ${key} -> ${member}`);
}
/**
* 从集合移除成员
*
* 业务逻辑:
* 1. 从内存Map中查找键对应的数据项
* 2. 如果键不存在则直接返回
* 3. 解析JSON数据为Set集合
* 4. 从集合中移除指定成员
* 5. 如果集合为空则删除键,否则更新集合数据
*
* @param key 集合键名,不能为空
* @param member 要移除的成员,不能为空
* @returns Promise<void> 操作完成的Promise
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* await redisService.srem('users', 'user123');
* ```
*/
async srem(key: string, member: string): Promise<void> {
const item = this.data.get(key);
if (!item) {
return;
}
const members = new Set<string>(JSON.parse(item.value));
members.delete(member);
if (members.size === 0) {
this.data.delete(key);
} else {
item.value = JSON.stringify([...members]);
}
await this.saveData();
this.logger.debug(`移除集合成员: ${key} -> ${member}`);
}
/**
* 获取集合的所有成员
*
* 业务逻辑:
* 1. 从内存Map中查找键对应的数据项
* 2. 如果键不存在则返回空数组
* 3. 检查数据项是否已过期
* 4. 如果过期则删除键并保存数据,返回空数组
* 5. 解析JSON数据并返回成员列表
*
* @param key 集合键名,不能为空
* @returns Promise<string[]> 集合成员列表,集合不存在返回空数组
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* const members = await redisService.smembers('users');
* console.log('用户列表:', members);
* ```
*/
async smembers(key: string): Promise<string[]> {
const item = this.data.get(key);
if (!item) {
return [];
}
// 检查是否过期
if (item.expireAt && item.expireAt <= Date.now()) {
this.data.delete(key);
await this.saveData();
return [];
}
return JSON.parse(item.value);
}
/**
* 模块销毁时的清理操作
*
* 业务逻辑:
* 1. 清理定时器,防止内存泄漏
* 2. 保存当前数据到文件
* 3. 记录清理操作日志
* 4. 释放相关资源
*
* @returns void 无返回值
*
* @example
* ```typescript
* // NestJS框架会在模块销毁时自动调用
* onModuleDestroy() {
* // 自动清理定时器和保存数据
* }
* ```
*/
onModuleDestroy(): void {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = undefined;
this.logger.log('清理定时器已停止');
}
// 保存最后的数据
this.saveData().catch(error => {
this.logger.error('模块销毁时保存数据失败', error);
});
this.logger.log('FileRedisService已清理');
}
}