异步优化,IUser接口定义,接入redis数据库

This commit is contained in:
Jerry 2025-04-10 12:34:18 +08:00
parent 66c82dcb01
commit e16378e75b
29 changed files with 464 additions and 104 deletions

4
.env
View File

@ -1,2 +1,4 @@
PORT=3000
DEBUG=true# 调试日志
DEBUG=true
RD_PORT=6379
RD_ADD=127.0.0.1

View File

@ -15,6 +15,7 @@
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_PUBLIC_MODIFIER" value="true" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="PREFER_EXPLICIT_TYPES_VARS_FIELDS" value="true" />

13
README.md Normal file
View File

@ -0,0 +1,13 @@
## 构建:
- pnpm i
- pnpm run build
- pnpm run start
## 使用:
- public/image/图片
- logs/日志
## 贡献
- fork到自己的储存库
- 在自己的储存库内推送更新
- 提交pr等待合并

View File

@ -10,6 +10,7 @@
"chalk": "4",
"dotenv": "^16.0.0",
"express": "^4.18.0",
"ioredis": "^5.6.0",
"mkdirp": "^3.0.1"
},
"devDependencies": {

80
pnpm-lock.yaml generated
View File

@ -17,6 +17,9 @@ importers:
express:
specifier: ^4.18.0
version: 4.21.2
ioredis:
specifier: ^5.6.0
version: 5.6.0
mkdirp:
specifier: ^3.0.1
version: 3.0.1
@ -46,6 +49,9 @@ packages:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@ioredis/commands@1.2.0':
resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
@ -179,6 +185,10 @@ packages:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
cluster-key-slot@1.1.2:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@ -215,6 +225,19 @@ packages:
supports-color:
optional: true
debug@4.4.0:
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@ -346,6 +369,10 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
ioredis@5.6.0:
resolution: {integrity: sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==}
engines: {node: '>=12.22.0'}
ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
@ -370,6 +397,12 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
make-error@1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
@ -485,6 +518,14 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
redis-errors@1.2.0:
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
engines: {node: '>=4'}
redis-parser@3.0.0:
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
engines: {node: '>=4'}
resolve@1.22.10:
resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
engines: {node: '>= 0.4'}
@ -535,6 +576,9 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
standard-as-callback@2.1.0:
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
@ -639,6 +683,8 @@ snapshots:
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@ioredis/commands@1.2.0': {}
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.0': {}
@ -795,6 +841,8 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
cluster-key-slot@1.1.2: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@ -819,6 +867,12 @@ snapshots:
dependencies:
ms: 2.0.0
debug@4.4.0:
dependencies:
ms: 2.1.3
denque@2.1.0: {}
depd@2.0.0: {}
destroy@1.2.0: {}
@ -978,6 +1032,20 @@ snapshots:
inherits@2.0.4: {}
ioredis@5.6.0:
dependencies:
'@ioredis/commands': 1.2.0
cluster-key-slot: 1.1.2
debug: 4.4.0
denque: 2.1.0
lodash.defaults: 4.2.0
lodash.isarguments: 3.1.0
redis-errors: 1.2.0
redis-parser: 3.0.0
standard-as-callback: 2.1.0
transitivePeerDependencies:
- supports-color
ipaddr.js@1.9.1: {}
is-binary-path@2.1.0:
@ -996,6 +1064,10 @@ snapshots:
is-number@7.0.0: {}
lodash.defaults@4.2.0: {}
lodash.isarguments@3.1.0: {}
make-error@1.3.6: {}
math-intrinsics@1.1.0: {}
@ -1076,6 +1148,12 @@ snapshots:
dependencies:
picomatch: 2.3.1
redis-errors@1.2.0: {}
redis-parser@3.0.0:
dependencies:
redis-errors: 1.2.0
resolve@1.22.10:
dependencies:
is-core-module: 2.16.1
@ -1154,6 +1232,8 @@ snapshots:
source-map@0.6.1: {}
standard-as-callback@2.1.0: {}
statuses@2.0.1: {}
strip-bom@3.0.0: {}

View File

@ -3,23 +3,19 @@ import logger from './utils/core/logger';
import paths from './utils/core/path';
import sampleController from './modules/sample/sample.controller';
import imageController from './modules/image/image.controller';
import Config from './utils/core/config';
import fc from './utils/core/file';
import config from './utils/core/config';
const apps = {
createApp() {
async createApp() {
const app = express();
logger.info('晶灵核心初始化..');
Config.check(['PORT', 'DEBUG']);
app.use(express.json());
logger.debug('成功加载express.json()中间件');
const publicPath = paths.get('public');
app.use('/public', express.static(publicPath));
logger.debug(`静态资源路由挂载:/public ${publicPath}`);
logger.debug(`静态资源路由挂载:/public => ${publicPath}`);
const modules = [
{ path: '/api/sample', name: '测试模块', controller: sampleController },
@ -28,9 +24,9 @@ const apps = {
modules.forEach((module) => {
app.use(module.path, module.controller.getRouter());
logger.debug(`模块路由挂载: ${module.path.padEnd(12)} ${module.name}`);
logger.debug(`模块路由挂载: ${module.path.padEnd(12)} => ${module.name}`);
if (Config.get('DEBUG', false)) {
if (config.get('DEBUG', false)) {
module.controller.getRouter().stack.forEach((layer) => {
if (layer.route) {
const methods = Object.keys(layer.route)
@ -41,9 +37,7 @@ const apps = {
});
}
});
const logPath = paths.get('log');
fc.createDir(logPath);
logger.debug(`日志目录初始化: ${logPath}`);
paths.init();
logger.info('晶灵核心初始化完毕!');
return app;
},

View File

@ -1,10 +1,24 @@
import apps from './app';
import logger from './utils/core/logger';
import config from './utils/core/config';
import redis from './services/redis/redis';
const PORT = process.env.PORT || 3000;
config.check(['PORT', 'DEBUG', 'RD_PORT', 'RD_ADD']);
const PORT = config.get('PORT') || 3000;
const app = apps.createApp();
apps
.createApp()
.then((app) => {
app.listen(PORT, () => {
logger.info(`Crystelf-core listening on ${PORT}`);
});
})
.catch((err) => {
logger.error('Crystelf-core启动失败:', err);
process.exit(1);
});
app.listen(PORT, () => {
logger.info(`Crystelf-core listening on ${PORT}`);
process.on('SIGTERM', async () => {
await redis.disconnect();
process.exit(0);
});

View File

@ -1 +0,0 @@
keep

View File

@ -1,6 +1,7 @@
import express from 'express';
import ImageService from './image.service';
import logger from '../../utils/core/logger';
import response from '../../utils/core/response';
class ImageController {
private readonly router: express.Router;
@ -17,35 +18,27 @@ class ImageController {
}
private initializeRoutes(): void {
this.router.get('/:filename', this.handleGetImage);
this.router.get('*', this.handleGetImage);
}
private handleGetImage = (req: express.Request, res: express.Response): void => {
private handleGetImage = async (req: express.Request, res: express.Response): Promise<void> => {
try {
const filename = req.params.filename;
logger.debug(`有个小可爱正在请求${filename}噢..`);
const filePath = this.imageService.getImage(filename);
const fullPath = req.params[0];
logger.debug(`有个小可爱正在请求${fullPath}噢..`);
const filePath = await this.imageService.getImage(fullPath);
if (!filePath) {
this.sendError(res, 404, '文件不存在啦!');
logger.warn(`${fullPath}:文件不存在..`);
await response.error(res, '文件不存在啦!', 404);
return;
}
res.sendFile(filePath);
logger.info(`成功投递文件: ${filePath}`);
} catch (error) {
this.sendError(res, 500, '晶灵服务处理图像请求时出错..');
await response.error(res, '晶灵服务处理图像请求时出错..', 500);
logger.error('晶灵图像请求处理失败:', error);
}
};
private sendError(res: express.Response, statusCode: number, message: string): void {
res.status(statusCode).json({
success: false,
error: message,
timestamp: new Date().toISOString(),
});
}
}
export default new ImageController();

View File

@ -4,26 +4,38 @@ import paths from '../../utils/core/path';
import logger from '../../utils/core/logger';
class ImageService {
private readonly imageDir: string;
private readonly imagePath: string;
constructor() {
this.imageDir = paths.get('images');
logger.info(`晶灵云图数据中心初始化..数据存储在: ${this.imageDir}`);
this.imagePath = paths.get('images');
logger.info(`晶灵云图数据中心初始化..数据存储在: ${this.imagePath}`);
}
public getImage(filename: string): string | null {
const filePath = path.join(this.imageDir, filename);
logger.debug(`尝试访问图像路径: ${filePath}`);
if (!this.isValidFilename(filename)) {
throw new Error('无效的文件名格式..');
public async getImage(relativePath: string): Promise<string | null> {
if (!this.isValidPath(relativePath) && !this.isValidFilename(path.basename(relativePath))) {
throw new Error('非法路径请求');
}
const filePath = path.join(this.imagePath, relativePath);
logger.debug(`尝试访问图像路径: ${filePath}`);
return fs.existsSync(filePath) ? filePath : null;
}
private isValidPath(relativePath: string): boolean {
try {
const normalized = path.normalize(relativePath);
let flag = true;
if (normalized.startsWith('../') && path.isAbsolute(normalized)) flag = false;
return flag;
} catch (err) {
logger.error(err);
return false;
}
}
private isValidFilename(filename: string): boolean {
return /^[a-zA-Z0-9_\-\.]+$/.test(filename);
return /^[a-zA-Z0-9_\-.]+$/.test(filename);
}
}

View File

@ -1 +0,0 @@
keep

View File

@ -19,25 +19,25 @@ class SampleController {
this.router.post('/greet', this.postGreet);
}
private getHello = (req: express.Request, res: express.Response): void => {
private getHello = async (req: express.Request, res: express.Response): Promise<void> => {
try {
const result = sampleService.getHello();
response.success(res, result);
const result = await sampleService.getHello();
await response.success(res, result);
} catch (error) {
response.error(res, '请求失败了..', 500, error);
await response.error(res, '请求失败了..', 500, error);
}
};
private postGreet = (req: express.Request, res: express.Response): void => {
private postGreet = async (req: express.Request, res: express.Response): Promise<void> => {
try {
const { name } = req.body;
if (!name) {
return response.error(res, '姓名不能为空!', 400);
}
const result = sampleService.generateGreeting(name);
response.success(res, result);
await response.success(res, result);
} catch (error) {
response.error(res, '请求失败了..', 500, error);
await response.error(res, '请求失败了..', 500, error);
}
};
}

View File

@ -1,12 +1,12 @@
import logger from '../../utils/core/logger';
class SampleService {
getHello() {
public async getHello() {
logger.debug(`有个小可爱正在请求GetHello方法..`);
return { message: 'Hello World!' };
}
generateGreeting(name: string): object {
public async generateGreeting(name: string): Promise<object> {
logger.debug(`有个小可爱正在请求generateGreeting方法..`);
if (!name) {
logger.warn('Name is required');

124
src/services/redis/redis.ts Normal file
View File

@ -0,0 +1,124 @@
import Redis from 'ioredis';
import logger from '../../utils/core/logger';
import tools from '../../utils/core/tool';
import config from '../../utils/core/config';
import redisTool from '../../utils/redis/redisTools';
import IUser from '../../types/user';
class RedisService {
private client!: Redis;
private isConnected = false;
constructor() {
this.initialize().then();
}
private async initialize() {
await this.connectWithRetry();
this.setupEventListeners();
}
private async connectWithRetry(): Promise<void> {
try {
await tools.retry(
async () => {
this.client = new Redis({
host: config.get('RD_ADD'),
port: Number(config.get('RD_PORT')),
retryStrategy: (times) => {
return Math.min(times * 1000, 5000);
},
});
await this.client.ping();
this.isConnected = true;
logger.info(`Redis连接成功!位于${config.get('RD_ADD')}:${config.get('RD_PORT')}`);
},
{
maxAttempts: 5,
initialDelay: 1000,
}
);
} catch (error) {
logger.error('Redis连接失败:', error);
throw error;
}
}
private setupEventListeners(): void {
this.client.on('error', (err) => {
if (!err.message.includes('ECONNREFUSED')) {
logger.error('Redis错误:', err);
}
this.isConnected = false;
});
this.client.on('ready', () => {
this.isConnected = true;
logger.debug('Redis连接就绪!');
});
this.client.on('reconnecting', () => {
logger.warn('Redis重新连接中...');
});
}
public async waitUntilReady(): Promise<void> {
if (this.isConnected) return;
return new Promise((resolve) => {
const check = () => {
if (this.isConnected) {
resolve();
} else {
setTimeout(check, 100);
}
};
check();
});
}
public getClient(): Redis {
if (!this.isConnected) {
logger.fatal(1, 'Redis未连接..');
}
return this.client;
}
public async disconnect(): Promise<void> {
await this.client.quit();
this.isConnected = false;
}
public async setObject<T>(key: string, value: T, ttl?: number): Promise<void> {
const serialized = redisTool.serialize(value);
await this.getClient().set(key, serialized);
if (ttl) {
await this.getClient().expire(key, ttl);
}
}
public async getObject<T>(key: string): Promise<T | undefined> {
const serialized = await this.getClient().get(key);
if (!serialized) return undefined;
const deserialized = redisTool.deserialize<T>(serialized);
return redisTool.reviveDates(deserialized);
}
public async test(): Promise<void> {
const testData: IUser = {
name: 'Jerry',
qq: '114514',
isAdmin: true,
password: '114514',
createdAt: new Date(),
};
let test = redisTool.reviveDates(testData);
logger.debug(test);
}
}
const redisService = new RedisService();
export default redisService;

View File

View File

@ -1 +0,0 @@
keep

6
src/types/retry.ts Normal file
View File

@ -0,0 +1,6 @@
interface RetryOptions {
maxAttempts: number;
initialDelay: number;
}
export default RetryOptions;

10
src/types/user.ts Normal file
View File

@ -0,0 +1,10 @@
interface IUser {
name: string;
qq: string;
password: string;
isAdmin: boolean;
lastLogin?: Date;
createdAt: Date;
}
export default IUser;

View File

@ -1 +0,0 @@
keep

View File

@ -29,7 +29,7 @@ class ConfigManger {
const value = this.env[key];
if (value === undefined) {
if (defaultValue !== undefined) return defaultValue;
logger.error(`环境变量${key}未定义!`);
logger.warn(`环境变量${key}未定义!`);
return undefined as T;
}
@ -59,7 +59,7 @@ class ConfigManger {
public check(keys: string[]): void {
keys.forEach((key) => {
if (!(key in this.env)) {
logger.error(`必须环境变量缺失:${key}`);
logger.fatal(1, `必须环境变量缺失:${key}`);
} else {
logger.debug(`检测到环境变量${key}!`);
}
@ -67,6 +67,6 @@ class ConfigManger {
}
}
const Config = ConfigManger.getInstance();
const config = ConfigManger.getInstance();
export default Config;
export default config;

View File

@ -1,48 +1,52 @@
import path from 'path';
import fs from 'fs';
import chalk from 'chalk';
import fs from 'fs/promises';
import paths from './path';
import date from './date';
import logger from './logger';
import chalk from 'chalk';
class fc {
public static createDir(targetPath: string = '', includeFile: boolean = false): void {
/**
*
* @param targetPath
* @param includeFile
*/
public static async createDir(
targetPath: string = '',
includeFile: boolean = false
): Promise<void> {
const root = paths.get('root');
if (path.isAbsolute(targetPath)) {
if (includeFile) {
const parentDir = path.dirname(targetPath);
if (!fs.existsSync(parentDir)) {
fs.mkdirSync(parentDir, { recursive: true });
logger.debug(`成功创建绝对目录: ${parentDir}`);
}
try {
if (path.isAbsolute(targetPath)) {
const dirToCreate = includeFile ? path.dirname(targetPath) : targetPath;
await fs.mkdir(dirToCreate, { recursive: true });
//logger.debug(`成功创建绝对目录: ${dirToCreate}`);
return;
}
if (!fs.existsSync(targetPath)) {
fs.mkdirSync(targetPath, { recursive: true });
logger.debug(`成功创建绝对目录: ${targetPath}`);
}
return;
}
const fullPath = includeFile
? path.join(root, path.dirname(targetPath))
: path.join(root, targetPath);
const fullPath = includeFile
? path.join(root, path.dirname(targetPath))
: path.join(root, targetPath);
if (!fs.existsSync(fullPath)) {
fs.mkdirSync(fullPath, { recursive: true });
logger.debug(`成功创建相对目录: ${fullPath}`);
await fs.mkdir(fullPath, { recursive: true });
//logger.debug(`成功创建相对目录: ${fullPath}`);
} catch (err) {
logger.error(`创建目录失败: ${err}`);
}
}
public static logToFile(level: string, message: string): void {
public static async logToFile(message: string): Promise<void> {
const logFile = path.join(paths.get('log'), `${date.getCurrentDate()}.log`);
const logMessage = `[${date.getCurrentTime()}] [${level}] ${message}\n`;
const logMessage = `${message}\n`;
fs.appendFile(logFile, logMessage, (err) => {
if (err) console.error(chalk.red('[LOGGER] 写入日志失败:'), err);
});
try {
//await this.createDir(paths.get('log'));
await fs.appendFile(logFile, logMessage);
} catch (err) {
console.error(chalk.red('[LOGGER] 写入日志失败:'), err);
}
}
}

View File

@ -1,13 +1,14 @@
import chalk from 'chalk';
import Config from './config';
import config from './config';
import fc from './file';
import date from './date';
class Logger {
private static instance: Logger;
private readonly isDebug: boolean;
private constructor() {
this.isDebug = Config.get('DEBUG', false);
this.isDebug = config.get('DEBUG', false);
}
public static getInstance(): Logger {
@ -27,37 +28,37 @@ class Logger {
public info(...args: any[]): void {
const message = this.formatMessage('INFO', args);
console.log(chalk.green(message));
this.logToFile('INFO', message);
this.logToFile(message).then();
}
public warn(...args: any[]): void {
const message = this.formatMessage('WARN', args);
console.log(chalk.yellow(message));
this.logToFile('WARN', message);
this.logToFile(message).then();
}
public error(...args: any[]): void {
const message = this.formatMessage('ERROR', args);
console.error(chalk.red(message));
this.logToFile('ERROR', message);
this.logToFile(message).then();
}
public fatal(args: any[], exitCode: number = 1): never {
public fatal(exitCode: number = 1, ...args: any[]): never {
const message = this.formatMessage('FATAL', args);
console.error(chalk.red.bold(message));
this.logToFile('FATAL', message);
this.logToFile(message).then();
process.exit(exitCode);
}
private formatMessage(level: string, args: any[]): string {
return `[${level}] ${args
return `[${date.getCurrentTime()}][${level}] ${args
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : arg))
.join(' ')}`;
}
private logToFile(level: string, message: string): void {
private async logToFile(message: string): Promise<void> {
try {
fc.logToFile(level, `${new Date().toISOString()} ${message}`);
await fc.logToFile(`${message}`);
} catch (err: any) {
console.error(chalk.red(`[LOGGER] 写入日志失败: ${err.message}`));
}

View File

@ -1,12 +1,14 @@
import path from 'path';
import fs from 'fs';
import fc from './file';
import logger from './logger';
class PathManager {
private static instance: PathManager;
private readonly baseDir: string;
private constructor() {
this.baseDir = path.join(__dirname, '../..');
this.baseDir = path.join(__dirname, '../../..');
}
/**
@ -36,6 +38,18 @@ class PathManager {
return type ? mappings[type] : this.baseDir;
}
/**
*
*/
public init(): void {
const logPath = this.get('log');
const imagePath = this.get('images');
fc.createDir(logPath, false);
fc.createDir(imagePath, false);
logger.debug(`日志目录初始化: ${logPath}`);
logger.debug(`图像目录初始化: ${imagePath}`);
}
/**
*
* @param segments

View File

@ -8,7 +8,7 @@ class response {
* @param data
* @param statusCode HTTP状态码200
*/
static success(res: Response, data: any, statusCode = 200) {
public static async success(res: Response, data: any, statusCode = 200) {
res.status(statusCode).json({
success: true,
data,
@ -23,7 +23,7 @@ class response {
* @param statusCode HTTP状态码500
* @param error
*/
static error(res: Response, message: string, statusCode = 500, error?: any) {
public static async error(res: Response, message: string, statusCode = 500, error?: any) {
const response: Record<string, any> = {
success: false,
message,
@ -43,7 +43,13 @@ class response {
* @param page
* @param pageSize
*/
static pagination(res: Response, data: any[], total: number, page: number, pageSize: number) {
public static async pagination(
res: Response,
data: any[],
total: number,
page: number,
pageSize: number
) {
res.status(200).json({
success: true,
data,

View File

@ -0,0 +1,26 @@
import RetryOptions from '../../types/retry';
import logger from './logger';
let tools = {
async retry(operation: () => Promise<any>, options: RetryOptions): Promise<any> {
let attempt = 0;
let lastError: any;
while (attempt < options.maxAttempts) {
try {
return await operation();
} catch (error) {
lastError = error;
attempt++;
if (attempt < options.maxAttempts) {
const delay = options.initialDelay * Math.pow(2, attempt - 1);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
logger.error(lastError);
},
};
export default tools;

View File

@ -1 +0,0 @@
keep

View File

@ -1 +0,0 @@
keep

View File

@ -0,0 +1,31 @@
import logger from '../core/logger';
class redisTools {
public static serialize<T>(data: T): string {
return JSON.stringify(data);
}
public static deserialize<T>(jsonString: string): T | undefined {
try {
return JSON.parse(jsonString);
} catch (err) {
logger.error(`redis反序列化失败${err}`);
return undefined;
}
}
public static reviveDates<T>(obj: T): T {
const dateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/;
const reviver = (_: string, value: any) => {
if (typeof value === 'string' && dateRegex.test(value)) {
return new Date(value);
}
return value;
};
return JSON.parse(JSON.stringify(obj), reviver);
}
}
const redisTool = redisTools;
export default redisTool;

View File

@ -0,0 +1,34 @@
import logger from '../core/logger';
class RedisSerializer {
public serialize<T>(data: T): string {
return JSON.stringify(data);
}
public deserialize<T>(jsonString: string): T | undefined {
try {
return JSON.parse(jsonString) as T;
} catch (err) {
logger.error(`Redis反序列化失败: ${err}`);
}
}
/**
* Date类型
*/
public reviveDates<T>(obj: T): T {
const dateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/;
const reviver = (_: string, value: any) => {
if (typeof value === 'string' && dateRegex.test(value)) {
return new Date(value);
}
return value;
};
return JSON.parse(JSON.stringify(obj), reviver);
}
}
const serializer = new RedisSerializer();
export default serializer;