refactor: config store stateless

This commit is contained in:
Innei
2022-01-15 18:51:50 +08:00
parent 3e7de6845b
commit 286a82b3d7
12 changed files with 109 additions and 66 deletions

View File

@@ -7,7 +7,7 @@
"license": "MIT",
"dashboard": {
"repo": "mx-space/admin-next",
"version": "3.11.9"
"version": "3.11.10"
},
"husky": {
"hooks": {

16
pm2.dev.config.js Normal file
View File

@@ -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',
},
},
],
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -238,9 +238,9 @@ export class AggregateService {
async buildRssStructure(): Promise<RSSProps> {
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,

View File

@@ -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() {

View File

@@ -46,7 +46,7 @@ export class CommentService {
async checkSpam(doc: Partial<CommentModel>) {
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<CommentModel>, type: ReplyMailType) {
const enable = this.configs.get('mailOptions').enable
const enable = (await this.configs.get('mailOptions')).enable
if (!enable) {
return
}

View File

@@ -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<typeof OptionModel>,
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<Readonly<IConfig>>(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<T extends keyof IConfig>(key: T): Readonly<IConfig[T]> {
if (!this.configInitd) {
throw new InternalServerErrorException('Config 未初始化')
}
return cloneDeep(this.config[key]) as Readonly<IConfig[T]>
public get<T extends keyof IConfig>(key: T): Promise<Readonly<IConfig[T]>> {
return new Promise((resolve, reject) => {
this.waitForConfigReady()
.then((config) => {
resolve(config[key])
})
.catch(reject)
})
}
public getConfig(): Readonly<IConfig> {
return cloneDeep(this.config)
public async getConfig(): Promise<Readonly<IConfig>> {
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<T extends keyof IConfig>(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() {

View File

@@ -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 = `<?xml version="1.0" encoding="UTF-8"?>

View File

@@ -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}

View File

@@ -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.')
}

View File

@@ -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<T extends keyof IConfig>(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]