refactor: config store stateless
This commit is contained in:
@@ -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
16
pm2.dev.config.js
Normal 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',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"?>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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.')
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user