forked from datawhale/whale-town-end
- 统一文件命名为snake_case格式(kebab-case snake_case) - 重构zulip模块为zulip_core,明确Core层职责 - 重构user-mgmt模块为user_mgmt,统一命名规范 - 调整模块依赖关系,优化架构分层 - 删除过时的文件和目录结构 - 更新相关文档和配置文件 本次重构涉及大量文件重命名和模块重组, 旨在建立更清晰的项目架构和统一的命名规范。
689 lines
19 KiB
TypeScript
689 lines
19 KiB
TypeScript
/**
|
||
* 文件模拟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已清理');
|
||
}
|
||
} |