mirror of
https://github.com/crystelf/crystelf-core.git
synced 2025-10-14 05:19:19 +00:00
feat:系统模块
This commit is contained in:
parent
d91f900f49
commit
7c4b933e9d
@ -22,6 +22,7 @@
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/swagger": "^11.2.0",
|
||||
"axios": "^1.10.0",
|
||||
"ioredis": "^5.6.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"simple-git": "^3.28.0",
|
||||
|
58
pnpm-lock.yaml
generated
58
pnpm-lock.yaml
generated
@ -26,6 +26,9 @@ dependencies:
|
||||
axios:
|
||||
specifier: ^1.10.0
|
||||
version: 1.10.0
|
||||
ioredis:
|
||||
specifier: ^5.6.1
|
||||
version: 5.6.1
|
||||
reflect-metadata:
|
||||
specifier: ^0.2.2
|
||||
version: 0.2.2
|
||||
@ -871,6 +874,10 @@ packages:
|
||||
'@types/node': 22.16.5
|
||||
dev: true
|
||||
|
||||
/@ioredis/commands@1.2.0:
|
||||
resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
|
||||
dev: false
|
||||
|
||||
/@isaacs/balanced-match@4.0.1:
|
||||
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
@ -2906,6 +2913,11 @@ packages:
|
||||
engines: {node: '>=0.8'}
|
||||
dev: true
|
||||
|
||||
/cluster-key-slot@1.1.2:
|
||||
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/co@4.6.0:
|
||||
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
|
||||
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
|
||||
@ -3146,6 +3158,11 @@ packages:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
/denque@2.1.0:
|
||||
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
|
||||
engines: {node: '>=0.10'}
|
||||
dev: false
|
||||
|
||||
/depd@2.0.0:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -4074,6 +4091,23 @@ packages:
|
||||
kind-of: 6.0.3
|
||||
dev: true
|
||||
|
||||
/ioredis@5.6.1:
|
||||
resolution: {integrity: sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
dependencies:
|
||||
'@ioredis/commands': 1.2.0
|
||||
cluster-key-slot: 1.1.2
|
||||
debug: 4.4.1
|
||||
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
|
||||
dev: false
|
||||
|
||||
/ipaddr.js@1.9.1:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@ -4772,6 +4806,14 @@ packages:
|
||||
p-locate: 5.0.0
|
||||
dev: true
|
||||
|
||||
/lodash.defaults@4.2.0:
|
||||
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
|
||||
dev: false
|
||||
|
||||
/lodash.isarguments@3.1.0:
|
||||
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
|
||||
dev: false
|
||||
|
||||
/lodash.memoize@4.1.2:
|
||||
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
|
||||
dev: true
|
||||
@ -5363,6 +5405,18 @@ packages:
|
||||
engines: {node: '>= 14.18.0'}
|
||||
dev: true
|
||||
|
||||
/redis-errors@1.2.0:
|
||||
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/redis-parser@3.0.0:
|
||||
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
redis-errors: 1.2.0
|
||||
dev: false
|
||||
|
||||
/reflect-metadata@0.2.2:
|
||||
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
||||
|
||||
@ -5696,6 +5750,10 @@ packages:
|
||||
escape-string-regexp: 2.0.0
|
||||
dev: true
|
||||
|
||||
/standard-as-callback@2.1.0:
|
||||
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
|
||||
dev: false
|
||||
|
||||
/statuses@2.0.1:
|
||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
@ -4,8 +4,20 @@ import { AppConfigModule } from './config/config.module';
|
||||
import { PathModule } from './core/path/path.module';
|
||||
import { SystemModule } from './core/system/system.module';
|
||||
import { ToolsModule } from './core/tools/tools.module';
|
||||
import { AutoUpdateModule } from './core/auto-update/auto-update.module';
|
||||
import { PersistenceModule } from './core/persistence/persistence.module';
|
||||
import { RedisModule } from './core/redis/redis.module';
|
||||
|
||||
@Module({
|
||||
imports: [RootModule, AppConfigModule, PathModule, SystemModule, ToolsModule],
|
||||
imports: [
|
||||
RootModule,
|
||||
AppConfigModule,
|
||||
PathModule,
|
||||
SystemModule,
|
||||
ToolsModule,
|
||||
PersistenceModule,
|
||||
AutoUpdateModule,
|
||||
RedisModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AutoUpdateService } from './auto-update.service';
|
||||
import { PathModule } from '../path/path.module';
|
||||
|
||||
@Module({
|
||||
imports: [PathModule],
|
||||
providers: [AutoUpdateService],
|
||||
exports: [AutoUpdateService],
|
||||
})
|
||||
export class AutoUpdateModule {}
|
10
src/core/files/files.module.ts
Normal file
10
src/core/files/files.module.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PathModule } from '../path/path.module';
|
||||
import { FilesService } from './files.service';
|
||||
|
||||
@Module({
|
||||
imports: [PathModule],
|
||||
providers: [FilesService],
|
||||
exports: [FilesService],
|
||||
})
|
||||
export class FilesModule {}
|
33
src/core/files/files.service.ts
Normal file
33
src/core/files/files.service.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import * as path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import { PathService } from '../path/path.service';
|
||||
@Injectable()
|
||||
export class FilesService {
|
||||
private readonly logger = new Logger(FilesService.name);
|
||||
|
||||
constructor(
|
||||
@Inject(PathService)
|
||||
private readonly paths: PathService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 创建目录
|
||||
* @param targetPath 目标路径
|
||||
* @param includeFile 是否包含文件路径
|
||||
*/
|
||||
async createDir(targetPath = '', includeFile = false): Promise<void> {
|
||||
const root = this.paths.get('root');
|
||||
try {
|
||||
const dirToCreate = path.isAbsolute(targetPath)
|
||||
? includeFile
|
||||
? path.dirname(targetPath)
|
||||
: targetPath
|
||||
: path.join(root, includeFile ? path.dirname(targetPath) : targetPath);
|
||||
|
||||
await fs.mkdir(dirToCreate, { recursive: true });
|
||||
} catch (err) {
|
||||
this.logger.error(`创建目录失败: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
11
src/core/persistence/persistence.module.ts
Normal file
11
src/core/persistence/persistence.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PersistenceService } from './persistence.service';
|
||||
import { PathModule } from '../path/path.module';
|
||||
import { FilesModule } from '../files/files.module';
|
||||
|
||||
@Module({
|
||||
imports: [PathModule, FilesModule],
|
||||
providers: [PersistenceService],
|
||||
exports: [PersistenceService],
|
||||
})
|
||||
export class PersistenceModule {}
|
61
src/core/persistence/persistence.service.ts
Normal file
61
src/core/persistence/persistence.service.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs/promises';
|
||||
import { PathService } from '../path/path.service';
|
||||
import { FilesService } from '../files/files.service';
|
||||
|
||||
@Injectable()
|
||||
export class PersistenceService {
|
||||
private readonly logger = new Logger(PersistenceService.name);
|
||||
|
||||
private getDataPath(dataName: string, fileName: string): string {
|
||||
return path.join(this.paths.get('userData'), dataName, `${fileName}.json`);
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(PathService)
|
||||
private readonly paths: PathService,
|
||||
@Inject(FilesService)
|
||||
private readonly fileService: FilesService,
|
||||
) {}
|
||||
|
||||
private async ensureDataPath(dataName: string): Promise<void> {
|
||||
const dataPath = path.join(this.paths.get('userData'), dataName);
|
||||
try {
|
||||
await this.fileService.createDir(dataPath, false);
|
||||
} catch (err) {
|
||||
this.logger.error('目录创建失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
public async writeDataLocal<T>(
|
||||
dataName: string,
|
||||
data: T,
|
||||
fileName: string,
|
||||
): Promise<void> {
|
||||
await this.ensureDataPath(dataName);
|
||||
const filePath = this.getDataPath(dataName, fileName);
|
||||
|
||||
try {
|
||||
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||
this.logger.debug(`用户数据已持久化到本地: ${filePath}`);
|
||||
} catch (err) {
|
||||
this.logger.error('写入失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
public async readDataLocal<T>(
|
||||
dataName: string,
|
||||
fileName: string,
|
||||
): Promise<T | undefined> {
|
||||
const filePath = this.getDataPath(dataName, fileName);
|
||||
|
||||
try {
|
||||
const data = await fs.readFile(filePath, 'utf-8');
|
||||
return JSON.parse(data) as T;
|
||||
} catch (err) {
|
||||
this.logger.error('读取失败:', err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
12
src/core/redis/redis.module.ts
Normal file
12
src/core/redis/redis.module.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RedisService } from './redis.service';
|
||||
import { AppConfigModule } from '../../config/config.module';
|
||||
import { ToolsModule } from '../tools/tools.module';
|
||||
import { PersistenceModule } from '../persistence/persistence.module';
|
||||
|
||||
@Module({
|
||||
imports: [AppConfigModule, ToolsModule, PersistenceModule],
|
||||
providers: [RedisService],
|
||||
exports: [RedisService],
|
||||
})
|
||||
export class RedisModule {}
|
150
src/core/redis/redis.service.ts
Normal file
150
src/core/redis/redis.service.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import { RedisUtils } from './redis.utils';
|
||||
import { AppConfigService } from '../../config/config.service';
|
||||
import { ToolsService } from '../tools/tools.service';
|
||||
import IUser from '../../types/user';
|
||||
import { PersistenceService } from '../persistence/persistence.service';
|
||||
|
||||
@Injectable()
|
||||
export class RedisService implements OnModuleInit {
|
||||
private readonly logger = new Logger(RedisService.name);
|
||||
private client!: Redis;
|
||||
private isConnected = false;
|
||||
|
||||
constructor(
|
||||
@Inject(AppConfigService)
|
||||
private readonly config: AppConfigService,
|
||||
@Inject(ToolsService)
|
||||
private readonly tools: ToolsService,
|
||||
@Inject(PersistenceService)
|
||||
private readonly Persistence: PersistenceService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.connectWithRetry();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
private async connectWithRetry(): Promise<void> {
|
||||
try {
|
||||
await this.tools.retry(
|
||||
async () => {
|
||||
this.client = new Redis({
|
||||
host: this.config.get('RD_ADD'),
|
||||
port: Number(this.config.get('RD_PORT')),
|
||||
retryStrategy: (times: number) => Math.min(times * 1000, 5000),
|
||||
});
|
||||
|
||||
await this.client.ping();
|
||||
this.isConnected = true;
|
||||
this.logger.log(
|
||||
`Redis连接成功! 位于 ${this.config.get('RD_ADD')}:${this.config.get('RD_PORT')}`,
|
||||
);
|
||||
},
|
||||
{
|
||||
maxAttempts: 5,
|
||||
initialDelay: 1000,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error('Redis连接失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private setupEventListeners(): void {
|
||||
this.client.on('error', (err) => {
|
||||
if (!err.message.includes('ECONNREFUSED')) {
|
||||
this.logger.error('Redis错误:', err);
|
||||
}
|
||||
this.isConnected = false;
|
||||
});
|
||||
|
||||
this.client.on('ready', () => {
|
||||
this.isConnected = true;
|
||||
this.logger.debug('Redis连接就绪!');
|
||||
});
|
||||
|
||||
this.client.on('reconnecting', () => {
|
||||
this.logger.warn('Redis重新连接中...');
|
||||
});
|
||||
}
|
||||
|
||||
public async waitUntilReady(): Promise<void> {
|
||||
if (this.isConnected) return;
|
||||
return new Promise((resolve) => {
|
||||
const check = () =>
|
||||
this.isConnected ? resolve() : setTimeout(check, 100);
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
public getClient(): Redis {
|
||||
if (!this.isConnected) {
|
||||
this.logger.error('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 = RedisUtils.serialize(value);
|
||||
await this.client.set(key, serialized);
|
||||
if (ttl) {
|
||||
await this.client.expire(key, ttl);
|
||||
}
|
||||
}
|
||||
|
||||
public async getObject<T>(key: string): Promise<T | undefined> {
|
||||
const serialized = await this.client.get(key);
|
||||
if (!serialized) return undefined;
|
||||
const deserialized = RedisUtils.deserialize<T>(serialized);
|
||||
return RedisUtils.reviveDates(deserialized);
|
||||
}
|
||||
|
||||
public async update<T>(key: string, updates: T): Promise<T> {
|
||||
const existing = await this.getObject<T>(key);
|
||||
if (!existing) {
|
||||
this.logger.error(`数据${key}不存在`);
|
||||
}
|
||||
const updated = { ...existing, ...updates };
|
||||
await this.setObject(key, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
public async fetch<T>(key: string, fileName: string): Promise<T | undefined> {
|
||||
const data = await this.getObject<T>(key);
|
||||
if (data) return data;
|
||||
|
||||
const fromLocal = await this.Persistence.readDataLocal<T>(key, fileName);
|
||||
if (fromLocal) {
|
||||
await this.setObject(key, fromLocal);
|
||||
return fromLocal;
|
||||
}
|
||||
|
||||
this.logger.error(`数据${key}不存在`);
|
||||
}
|
||||
|
||||
public async persistData<T>(
|
||||
key: string,
|
||||
data: T,
|
||||
fileName: string,
|
||||
): Promise<void> {
|
||||
await this.setObject(key, data);
|
||||
await this.Persistence.writeDataLocal(key, data, fileName);
|
||||
}
|
||||
|
||||
public async test(): Promise<void> {
|
||||
const user = await this.fetch<IUser>('Jerry', 'IUser');
|
||||
this.logger.debug('User:', user);
|
||||
}
|
||||
}
|
30
src/core/redis/redis.utils.ts
Normal file
30
src/core/redis/redis.utils.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
const logger = new Logger('RedisUtils');
|
||||
|
||||
export class RedisUtils {
|
||||
static serialize<T>(data: T): string {
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
|
||||
static deserialize<T>(jsonString: string): T | undefined {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (err) {
|
||||
logger.error(`redis反序列化失败:${err}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SystemService } from './system.service';
|
||||
import { PathModule } from '../path/path.module';
|
||||
import { AutoUpdateModule } from '../auto-update/auto-update.module';
|
||||
|
||||
@Module({
|
||||
imports: [PathModule],
|
||||
imports: [PathModule, AutoUpdateModule],
|
||||
providers: [SystemService],
|
||||
exports: [SystemService],
|
||||
})
|
||||
|
@ -2,6 +2,9 @@ import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { PathService } from '../path/path.service';
|
||||
import { AutoUpdateModule } from '../auto-update/auto-update.module';
|
||||
import { AutoUpdateService } from '../auto-update/auto-update.service';
|
||||
import * as process from 'node:process';
|
||||
|
||||
@Injectable()
|
||||
export class SystemService {
|
||||
@ -11,6 +14,8 @@ export class SystemService {
|
||||
constructor(
|
||||
@Inject(PathService)
|
||||
private readonly pathService: PathService,
|
||||
@Inject(AutoUpdateService)
|
||||
private readonly autoUpdateService: AutoUpdateService,
|
||||
) {
|
||||
this.restartFile = path.join(
|
||||
this.pathService.get('temp'),
|
||||
@ -50,4 +55,12 @@ export class SystemService {
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async checkUpdate(): Promise<void> {
|
||||
const updated = await this.autoUpdateService.checkForUpdates();
|
||||
if (updated) {
|
||||
this.logger.warn('系统代码已更新,正在重启..');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,9 @@ async function bootstrap() {
|
||||
const document = () => SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('', app, document);
|
||||
await app.listen(7000);
|
||||
await systemService.checkUpdate().catch((err) => {
|
||||
Logger.error(`自动更新失败: ${err?.message}`, '', 'System');
|
||||
});
|
||||
}
|
||||
bootstrap().then(() => {
|
||||
Logger.log(`API服务已启动:http://localhost:7000`);
|
||||
|
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;
|
Loading…
x
Reference in New Issue
Block a user