Files
core/src/modules/aggregate/aggregate.service.ts
2022-03-27 18:25:23 +08:00

399 lines
11 KiB
TypeScript

import dayjs from 'dayjs'
import { pick } from 'lodash'
import { FilterQuery } from 'mongoose'
import { URL } from 'url'
import { Inject, Injectable, forwardRef } from '@nestjs/common'
import { OnEvent } from '@nestjs/event-emitter'
import { DocumentType, ReturnModelType } from '@typegoose/typegoose'
import { AnyParamConstructor } from '@typegoose/typegoose/lib/types'
import { CacheKeys, RedisKeys } from '~/constants/cache.constant'
import { EventBusEvents } from '~/constants/event.constant'
import { CacheService } from '~/processors/cache/cache.service'
import { WebEventsGateway } from '~/processors/gateway/web/events.gateway'
import { addYearCondition } from '~/transformers/db-query.transformer'
import { getRedisKey } from '~/utils/redis.util'
import { getShortDate } from '~/utils/time.util'
import { CategoryModel } from '../category/category.model'
import { CategoryService } from '../category/category.service'
import { CommentState } from '../comment/comment.model'
import { CommentService } from '../comment/comment.service'
import { ConfigsService } from '../configs/configs.service'
import { LinkState } from '../link/link.model'
import { LinkService } from '../link/link.service'
import { NoteModel } from '../note/note.model'
import { NoteService } from '../note/note.service'
import { PageService } from '../page/page.service'
import { PostService } from '../post/post.service'
import { RecentlyService } from '../recently/recently.service'
import { SayService } from '../say/say.service'
import { TimelineType } from './aggregate.dto'
import { RSSProps } from './aggregate.interface'
@Injectable()
export class AggregateService {
constructor(
@Inject(forwardRef(() => PostService))
private readonly postService: PostService,
@Inject(forwardRef(() => NoteService))
private readonly noteService: NoteService,
@Inject(forwardRef(() => CategoryService))
private readonly categoryService: CategoryService,
@Inject(forwardRef(() => PageService))
private readonly pageService: PageService,
@Inject(forwardRef(() => SayService))
private readonly sayService: SayService,
@Inject(forwardRef(() => CommentService))
private readonly commentService: CommentService,
@Inject(forwardRef(() => LinkService))
private readonly linkService: LinkService,
@Inject(forwardRef(() => RecentlyService))
private readonly recentlyService: RecentlyService,
private readonly configs: ConfigsService,
private readonly gateway: WebEventsGateway,
private readonly cacheService: CacheService,
) {}
getAllCategory() {
return this.categoryService.findAllCategory()
}
getAllPages() {
return this.pageService.model
.find({}, 'title _id slug order')
.sort({
order: -1,
modified: -1,
})
.lean()
}
async getLatestNote(cond: FilterQuery<DocumentType<NoteModel>> = {}) {
return (await this.noteService.getLatestOne(cond)).latest
}
private findTop<
U extends AnyParamConstructor<any>,
T extends ReturnModelType<U>,
>(model: T, condition = {}, size = 6) {
return model
.find(condition)
.sort({ created: -1 })
.limit(size)
.select('_id title name slug avatar nid created')
}
async topActivity(size = 6, isMaster = false) {
const [notes, posts, says] = await Promise.all([
this.findTop(
this.noteService.model,
!isMaster
? {
hide: false,
password: undefined,
}
: {},
size,
).lean(),
this.findTop(
this.postService.model,
!isMaster ? { hide: false } : {},
size,
)
.populate('categoryId')
.lean()
.then((res) => {
return res.map((post) => {
post.category = pick(post.categoryId, ['name', 'slug'])
delete post.categoryId
return post
})
}),
this.sayService.model.find({}).sort({ create: -1 }).limit(size),
])
return { notes, posts, says }
}
async getTimeline(
year: number | undefined,
type: TimelineType | undefined,
sortBy: 1 | -1 = 1,
) {
const data: any = {}
const getPosts = () =>
this.postService.model
.find({ ...addYearCondition(year) })
.sort({ created: sortBy })
.populate('category')
.lean()
.then((list) =>
list.map((item) => ({
...pick(item, ['_id', 'title', 'slug', 'created', 'modified']),
category: item.category,
// summary:
// item.summary ??
// (item.text.length > 150
// ? item.text.slice(0, 150) + '...'
// : item.text),
url: encodeURI(
`/posts/${(item.category as CategoryModel).slug}/${item.slug}`,
),
})),
)
const getNotes = () =>
this.noteService.model
.find(
{
hide: false,
...addYearCondition(year),
},
'_id nid title weather mood created modified hasMemory',
)
.sort({ created: sortBy })
.lean()
switch (type) {
case TimelineType.Post: {
data.posts = await getPosts()
break
}
case TimelineType.Note: {
data.notes = await getNotes()
break
}
default: {
const tasks = await Promise.all([getPosts(), getNotes()])
data.posts = tasks[0]
data.notes = tasks[1]
}
}
return data
}
async getSiteMapContent() {
const {
url: { webUrl: baseURL },
} = await this.configs.waitForConfigReady()
const combineTasks = await Promise.all([
this.pageService.model
.find()
.lean()
.then((list) =>
list.map((doc) => ({
url: new URL(`/${doc.slug}`, baseURL),
published_at: doc.modified
? new Date(doc.modified)
: new Date(doc.created!),
})),
),
this.noteService.model
.find({
hide: false,
$or: [
{ password: undefined },
{ password: { $exists: false } },
{ password: null },
],
secret: {
$lte: new Date(),
},
})
.lean()
.then((list) =>
list.map((doc) => {
return {
url: new URL(`/notes/${doc.nid}`, baseURL),
published_at: doc.modified
? new Date(doc.modified)
: new Date(doc.created!),
}
}),
),
this.postService.model
.find({
hide: false,
})
.populate('category')
.then((list) =>
list.map((doc) => {
return {
url: new URL(
`/posts/${(doc.category as CategoryModel).slug}/${doc.slug}`,
baseURL,
),
published_at: doc.modified
? new Date(doc.modified)
: new Date(doc.created!),
}
}),
),
])
return combineTasks
.flat(1)
.sort((a, b) => -(a.published_at.getTime() - b.published_at.getTime()))
}
async buildRssStructure(): Promise<RSSProps> {
const data = await this.getRSSFeedContent()
const title = (await this.configs.get('seo')).title
const author = (await this.configs.getMaster()).name
const url = (await this.configs.get('url')).webUrl
return {
title,
author,
url,
data,
}
}
async getRSSFeedContent() {
const {
url: { webUrl: baseURL },
} = await this.configs.waitForConfigReady()
const [posts, notes] = await Promise.all([
this.postService.model
.find({ hide: false })
.limit(10)
.sort({ created: -1 })
.populate('category'),
this.noteService.model
.find({
hide: false,
$or: [
{ password: undefined },
{ password: { $exists: false } },
{ password: null },
],
})
.limit(10)
.sort({ created: -1 }),
])
const postsRss: RSSProps['data'] = posts.map((post) => {
return {
title: post.title,
text: post.text,
created: post.created!,
modified: post.modified,
link: new URL(
'/posts' + `/${(post.category as CategoryModel).slug}/${post.slug}`,
baseURL,
).toString(),
}
})
const notesRss: RSSProps['data'] = notes.map((note) => {
const isSecret = note.secret
? dayjs(note.secret).isAfter(new Date())
: false
return {
title: note.title,
text: isSecret ? '这篇文章暂时没有公开呢' : note.text,
created: note.created!,
modified: note.modified,
link: new URL(`/notes/${note.nid}`, baseURL).toString(),
}
})
return postsRss
.concat(notesRss)
.sort((a, b) => b.created!.getTime() - a.created!.getTime())
.slice(0, 10)
}
async getCounts() {
const redisClient = this.cacheService.getClient()
const dateFormat = getShortDate(new Date())
const [
online,
posts,
notes,
pages,
says,
comments,
allComments,
unreadComments,
links,
linkApply,
categories,
recently,
] = await Promise.all([
this.gateway.getcurrentClientCount(),
this.postService.model.countDocuments(),
this.noteService.model.countDocuments(),
this.categoryService.model.countDocuments(),
this.sayService.model.countDocuments(),
this.commentService.model.countDocuments({
parent: null,
$or: [{ state: CommentState.Read }, { state: CommentState.Unread }],
}),
this.commentService.model.countDocuments({
$or: [{ state: CommentState.Read }, { state: CommentState.Unread }],
}),
this.commentService.model.countDocuments({
state: CommentState.Unread,
}),
this.linkService.model.countDocuments({
state: LinkState.Pass,
}),
this.linkService.model.countDocuments({
state: LinkState.Audit,
}),
this.categoryService.model.countDocuments({}),
this.recentlyService.model.countDocuments({}),
])
const [todayMaxOnline, todayOnlineTotal] = await Promise.all([
redisClient.hget(getRedisKey(RedisKeys.MaxOnlineCount), dateFormat),
redisClient.hget(
getRedisKey(RedisKeys.MaxOnlineCount, 'total'),
dateFormat,
),
])
return {
allComments,
categories,
comments,
linkApply,
links,
notes,
pages,
posts,
says,
recently,
unreadComments,
online,
todayMaxOnline: todayMaxOnline || 0,
todayOnlineTotal: todayOnlineTotal || 0,
}
}
@OnEvent(EventBusEvents.CleanAggregateCache, { async: true })
public clearAggregateCache() {
return Promise.all([
this.cacheService.getClient().del(CacheKeys.RSS),
this.cacheService.getClient().del(CacheKeys.RSSXmlCatch),
this.cacheService.getClient().del(CacheKeys.AggregateCatch),
this.cacheService.getClient().del(CacheKeys.SiteMapCatch),
this.cacheService.getClient().del(CacheKeys.SiteMapXmlCatch),
])
}
}