mirror of
https://github.com/crystelf/crystelf-core.git
synced 2025-07-04 06:39:18 +00:00
异步优化,IUser接口定义,接入redis数据库
This commit is contained in:
parent
66c82dcb01
commit
e16378e75b
1
.idea/codeStyles/Project.xml
generated
1
.idea/codeStyles/Project.xml
generated
@ -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
13
README.md
Normal file
@ -0,0 +1,13 @@
|
||||
## 构建:
|
||||
- pnpm i
|
||||
- pnpm run build
|
||||
- pnpm run start
|
||||
|
||||
## 使用:
|
||||
- public/image/图片
|
||||
- logs/日志
|
||||
|
||||
## 贡献
|
||||
- fork到自己的储存库
|
||||
- 在自己的储存库内推送更新
|
||||
- 提交pr,等待合并
|
@ -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
80
pnpm-lock.yaml
generated
@ -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: {}
|
||||
|
18
src/app.ts
18
src/app.ts
@ -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;
|
||||
},
|
||||
|
22
src/main.ts
22
src/main.ts
@ -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);
|
||||
});
|
||||
|
@ -1 +0,0 @@
|
||||
keep
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
keep
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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
124
src/services/redis/redis.ts
Normal 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;
|
0
src/services/redis/redisStorage.ts
Normal file
0
src/services/redis/redisStorage.ts
Normal file
@ -1 +0,0 @@
|
||||
keep
|
6
src/types/retry.ts
Normal file
6
src/types/retry.ts
Normal file
@ -0,0 +1,6 @@
|
||||
interface RetryOptions {
|
||||
maxAttempts: number;
|
||||
initialDelay: number;
|
||||
}
|
||||
|
||||
export default RetryOptions;
|
10
src/types/user.ts
Normal file
10
src/types/user.ts
Normal file
@ -0,0 +1,10 @@
|
||||
interface IUser {
|
||||
name: string;
|
||||
qq: string;
|
||||
password: string;
|
||||
isAdmin: boolean;
|
||||
lastLogin?: Date;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export default IUser;
|
@ -1 +0,0 @@
|
||||
keep
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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}`));
|
||||
}
|
||||
|
@ -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 路径片段
|
||||
|
@ -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,
|
||||
|
@ -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;
|
@ -1 +0,0 @@
|
||||
keep
|
@ -1 +0,0 @@
|
||||
keep
|
31
src/utils/redis/redisTools.ts
Normal file
31
src/utils/redis/redisTools.ts
Normal 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;
|
34
src/utils/redis/serializer.ts
Normal file
34
src/utils/redis/serializer.ts
Normal 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;
|
Loading…
x
Reference in New Issue
Block a user