From 286a82b3d7cb93fa7c0f04d38d9d280e66bd2d8f Mon Sep 17 00:00:00 2001 From: Innei Date: Sat, 15 Jan 2022 18:51:50 +0800 Subject: [PATCH] refactor: config store stateless --- package.json | 2 +- pm2.dev.config.js | 16 ++++ src/bootstrap.ts | 2 + src/constants/cache.constant.ts | 2 + src/modules/aggregate/aggregate.service.ts | 4 +- src/modules/auth/auth.module.ts | 5 +- src/modules/comment/comment.service.ts | 4 +- src/modules/configs/configs.service.ts | 93 ++++++++++++++-------- src/modules/feed/feed.controller.ts | 2 +- src/modules/link/link.service.ts | 4 +- src/modules/option/option.controller.ts | 4 +- src/modules/option/option.service.ts | 37 ++++----- 12 files changed, 109 insertions(+), 66 deletions(-) create mode 100644 pm2.dev.config.js diff --git a/package.json b/package.json index 66370fd3..684152af 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "license": "MIT", "dashboard": { "repo": "mx-space/admin-next", - "version": "3.11.9" + "version": "3.11.10" }, "husky": { "hooks": { diff --git a/pm2.dev.config.js b/pm2.dev.config.js new file mode 100644 index 00000000..bf50c811 --- /dev/null +++ b/pm2.dev.config.js @@ -0,0 +1,16 @@ +module.exports = { + apps: [ + { + name: 'mx-server', + script: 'dist/src/main.js', + autorestart: true, + exec_mode: 'cluster', + watch: false, + instances: 2, + max_memory_restart: '230M', + env: { + NODE_ENV: 'development', + }, + }, + ], +} diff --git a/src/bootstrap.ts b/src/bootstrap.ts index 8e23dfae..787b4267 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -10,6 +10,8 @@ import { SpiderGuard } from './common/guard/spider.guard' import { LoggingInterceptor } from './common/interceptors/logging.interceptor' import { MyLogger } from './processors/logger/logger.service' +console.log('ENV:', process.env.NODE_ENV) + const Origin = CROSS_DOMAIN.allowedOrigins declare const module: any diff --git a/src/constants/cache.constant.ts b/src/constants/cache.constant.ts index 32eae441..fa908d7c 100644 --- a/src/constants/cache.constant.ts +++ b/src/constants/cache.constant.ts @@ -7,6 +7,8 @@ export enum RedisKeys { IpInfoMap = 'ip_info_map', LikeSite = 'like_site', AdminPage = 'admin_next_index_entry', + + ConfigCache = 'config_cache', } export enum RedisItems { diff --git a/src/modules/aggregate/aggregate.service.ts b/src/modules/aggregate/aggregate.service.ts index 13dbe929..d34bb473 100644 --- a/src/modules/aggregate/aggregate.service.ts +++ b/src/modules/aggregate/aggregate.service.ts @@ -238,9 +238,9 @@ export class AggregateService { async buildRssStructure(): Promise { const data = await this.getRSSFeedContent() - const title = this.configs.get('seo').title + const title = (await this.configs.get('seo')).title const author = (await this.configs.getMaster()).name - const url = this.configs.get('url').webUrl + const url = (await this.configs.get('url')).webUrl return { title, author, diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index f627ca47..f15d08e5 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -17,7 +17,10 @@ export const __secret: any = SECURITY.jwtSecret || Buffer.from(getMachineId()).toString('base64').slice(0, 15) || 'asjhczxiucipoiopiqm2376' -consola.log('JWT Secret start with :', __secret.slice(0, 5)) +consola.log( + 'JWT Secret start with :', + __secret.slice(0, 5) + '*'.repeat(__secret.length - 5), +) const jwtModule = JwtModule.registerAsync({ useFactory() { diff --git a/src/modules/comment/comment.service.ts b/src/modules/comment/comment.service.ts index 447b65bc..cf8f0e0d 100644 --- a/src/modules/comment/comment.service.ts +++ b/src/modules/comment/comment.service.ts @@ -46,7 +46,7 @@ export class CommentService { async checkSpam(doc: Partial) { const res = await (async () => { - const commentOptions = this.configs.get('commentOptions') + const commentOptions = await this.configs.get('commentOptions') if (!commentOptions.antiSpam) { return false } @@ -195,7 +195,7 @@ export class CommentService { } async sendEmail(model: DocumentType, type: ReplyMailType) { - const enable = this.configs.get('mailOptions').enable + const enable = (await this.configs.get('mailOptions')).enable if (!enable) { return } diff --git a/src/modules/configs/configs.service.ts b/src/modules/configs/configs.service.ts index 1bb94850..b7d879f4 100644 --- a/src/modules/configs/configs.service.ts +++ b/src/modules/configs/configs.service.ts @@ -1,15 +1,14 @@ -import { - Injectable, - InternalServerErrorException, - Logger, -} from '@nestjs/common' +import { Injectable, Logger } from '@nestjs/common' import { DocumentType, ReturnModelType } from '@typegoose/typegoose' import { BeAnObject } from '@typegoose/typegoose/lib/types' -import { cloneDeep, merge } from 'lodash' +import { cloneDeep, mergeWith } from 'lodash' import { LeanDocument } from 'mongoose' import { InjectModel } from 'nestjs-typegoose' import { API_VERSION } from '~/app.config' +import { RedisKeys } from '~/constants/cache.constant' +import { CacheService } from '~/processors/cache/cache.service' import { sleep } from '~/utils/index.util' +import { getRedisKey } from '~/utils/redis.util' import { UserModel } from '../user/user.model' import { UserService } from '../user/user.service' import { BackupOptionsDto, MailOptionsDto } from './configs.dto' @@ -45,12 +44,12 @@ const generateDefaultConfig: () => IConfig = () => ({ @Injectable() export class ConfigsService { - private config: IConfig = generateDefaultConfig() private logger: Logger constructor( @InjectModel(OptionModel) private readonly optionModel: ReturnModelType, private readonly userService: UserService, + private readonly redis: CacheService, ) { this.configInit().then(() => { this.logger.log('Config 已经加载完毕!') @@ -59,12 +58,17 @@ export class ConfigsService { } private configInitd = false + private async setConfig(config: IConfig) { + const redis = this.redis.getClient() + await redis.set(getRedisKey(RedisKeys.ConfigCache), JSON.stringify(config)) + } + public waitForConfigReady() { // eslint-disable-next-line no-async-promise-executor return new Promise>(async (r, j) => { // 开始等待, 后续调用直接返回 if (this.configInitd) { - r(this.getConfig()) + r(await this.getConfig()) return } @@ -72,7 +76,7 @@ export class ConfigsService { let curCount = 0 do { if (this.configInitd) { - r(this.getConfig()) + r(await this.getConfig()) return } await sleep(100) @@ -91,45 +95,64 @@ export class ConfigsService { protected async configInit() { const configs = await this.optionModel.find().lean() + const mergedConfig = generateDefaultConfig() configs.forEach((field) => { const name = field.name as keyof IConfig const value = field.value - this.config[name] = value + mergedConfig[name] = value }) + await this.setConfig(mergedConfig) this.configInitd = true } - // 10 分钟自动同步一次 - // @Interval(1000 * 60 * 10) - // private async syncConfig() { - // this.configInitd = false - // this.config = generateDefaultConfig() as any - // await this.configInit() - // this.logger.log('Config 已经同步完毕!') - // } - - public get(key: T): Readonly { - if (!this.configInitd) { - throw new InternalServerErrorException('Config 未初始化') - } - return cloneDeep(this.config[key]) as Readonly + public get(key: T): Promise> { + return new Promise((resolve, reject) => { + this.waitForConfigReady() + .then((config) => { + resolve(config[key]) + }) + .catch(reject) + }) } - public getConfig(): Readonly { - return cloneDeep(this.config) + public async getConfig(): Promise> { + const redis = this.redis.getClient() + const configCache = await redis.get(getRedisKey(RedisKeys.ConfigCache)) + + if (configCache) { + try { + return JSON.parse(configCache) + } catch { + await this.configInit() + return await this.getConfig() + } + } else { + await this.configInit() + return await this.getConfig() + } } public async patch(key: T, data: IConfig[T]) { - await this.optionModel.updateOne( - { name: key as string }, - { value: merge(this.config[key], data) }, - { upsert: true }, - ) - const newData = (await this.optionModel.findOne({ name: key as string })) - .value + const config = await this.getConfig() + const updatedConfigRow = await this.optionModel + .findOneAndUpdate( + { name: key as string }, + { + value: mergeWith(cloneDeep(config[key]), data, (old, newer) => { + // 数组不合并 + if (Array.isArray(old)) { + return newer + } + }), + }, + { upsert: true, new: true }, + ) + .lean() + const newData = updatedConfigRow.value + const mergedFullConfig = Object.assign({}, config, { [key]: newData }) - this.config[key] = newData + await this.setConfig(mergedFullConfig) - return cloneDeep(this.config[key]) + return newData } get getMaster() { diff --git a/src/modules/feed/feed.controller.ts b/src/modules/feed/feed.controller.ts index 30b5f9a1..8496d04c 100644 --- a/src/modules/feed/feed.controller.ts +++ b/src/modules/feed/feed.controller.ts @@ -25,7 +25,7 @@ export class FeedController { async rss() { const { author, data, url } = await this.aggregateService.buildRssStructure() - const { title } = this.configs.get('seo') + const { title } = await this.configs.get('seo') const { avatar } = await this.configs.getMaster() const now = new Date() const xml = ` diff --git a/src/modules/link/link.service.ts b/src/modules/link/link.service.ts index 6d389a5d..464dadf5 100644 --- a/src/modules/link/link.service.ts +++ b/src/modules/link/link.service.ts @@ -74,7 +74,7 @@ export class LinkService { if (!model.email) { return } - const enable = this.configs.get('mailOptions').enable + const enable = (await this.configs.get('mailOptions')).enable if (!enable || isDev) { console.log(` TO: ${model.email} @@ -92,7 +92,7 @@ export class LinkService { }) } async sendToMaster(authorName: string, model: LinkModel) { - const enable = this.configs.get('mailOptions').enable + const enable = (await this.configs.get('mailOptions')).enable if (!enable || isDev) { console.log(`来自 ${authorName} 的友链请求: 站点标题: ${model.name} diff --git a/src/modules/option/option.controller.ts b/src/modules/option/option.controller.ts index bf6bccd5..94fca571 100644 --- a/src/modules/option/option.controller.ts +++ b/src/modules/option/option.controller.ts @@ -39,13 +39,13 @@ export class OptionController { } @Get('/:key') - getOptionKey(@Param('key') key: keyof IConfig) { + async getOptionKey(@Param('key') key: keyof IConfig) { if (typeof key !== 'string' && !key) { throw new UnprocessableEntityException( 'key must be IConfigKeys, got ' + key, ) } - const value = this.configs.get(key) + const value = await this.configs.get(key) if (!value) { throw new BadRequestException('key is not exists.') } diff --git a/src/modules/option/option.service.ts b/src/modules/option/option.service.ts index 43349571..5ad66122 100644 --- a/src/modules/option/option.service.ts +++ b/src/modules/option/option.service.ts @@ -31,32 +31,29 @@ export class OptionService { forbidNonWhitelisted: true, } validate = new ValidationPipe(this.validOptions) - patchAndValid(key: keyof IConfig, value: any) { - value = camelcaseKeys(value, { deep: true }) + async patchAndValid(key: T, value: IConfig[T]) { + value = camelcaseKeys(value, { deep: true }) as any switch (key) { case 'mailOptions': { - this.validWithDto(MailOptionsDto, value) - const task = this.configs.patch('mailOptions', value) - task.then((dto) => { - // re-init after set email option - this.emailService.init() - }) - return task + const option = await this.configs.patch( + 'mailOptions', + this.validWithDto(MailOptionsDto, value), + ) + this.emailService.init() + + return option } case 'algoliaSearchOptions': { - return this.configs - .patch( - 'algoliaSearchOptions', - this.validWithDto(AlgoliaSearchOptionsDto, value), - ) - .then((r) => { - if (r.enable) { - this.cronService.pushToAlgoliaSearch() - } - return r - }) + const option = await this.configs.patch( + 'algoliaSearchOptions', + this.validWithDto(AlgoliaSearchOptionsDto, value), + ) + if (option.enable) { + this.cronService.pushToAlgoliaSearch() + } + return option } default: { const dto = map[key]