-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(aggregate): aggregate data module (#396)
- Loading branch information
Showing
7 changed files
with
365 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
/* | ||
* @FilePath: /mog-core/apps/core/src/modules/aggregate/aggregate.controller.ts | ||
* @author: Wibus | ||
* @Date: 2022-10-01 20:52:08 | ||
* @LastEditors: Wibus | ||
* @LastEditTime: 2022-10-01 20:54:01 | ||
* Coding With IU | ||
*/ | ||
|
||
import { CacheKey, CacheTTL, Controller, Get, Query } from '@nestjs/common'; | ||
import { ApiOperation } from '@nestjs/swagger'; | ||
import { ConfigService } from '~/libs/config/src'; | ||
import { Auth } from '~/shared/common/decorator/auth.decorator'; | ||
import { ApiName } from '~/shared/common/decorator/openapi.decorator'; | ||
import { IsMaster } from '~/shared/common/decorator/role.decorator'; | ||
import { CacheKeys } from '~/shared/constants/cache.constant'; | ||
import { TopQueryDto } from './aggregate.dto'; | ||
import { AggregateService } from './aggregate.service'; | ||
|
||
@Controller('aggregate') | ||
@ApiName | ||
export class AggregateController { | ||
constructor( | ||
private readonly aggregateService: AggregateService, | ||
private readonly configService: ConfigService, | ||
) {} | ||
|
||
@Get('/') | ||
@ApiOperation({ summary: '获取概要' }) | ||
@CacheKey(CacheKeys.AggregateCatch) | ||
@CacheTTL(300) | ||
async aggregate() { | ||
const tasks = await Promise.allSettled([ | ||
// this.configService.getMaster(), | ||
this.aggregateService.getAllCategory(), | ||
this.aggregateService.getAllPages(), | ||
// this.configService.get("urls"), | ||
// this.configService.get("site"), | ||
]); | ||
const [categories, pageMeta] = tasks.map((t) => { | ||
if (t.status === 'fulfilled') { | ||
return t.value; | ||
} else { | ||
return null; | ||
} | ||
}); | ||
return { | ||
categories, | ||
pageMeta, | ||
}; | ||
} | ||
|
||
@Get('/top') | ||
@ApiOperation({ summary: '获取网站统计信息' }) | ||
async top(@Query() query: TopQueryDto, @IsMaster() isMaster: boolean) { | ||
const { size } = query; | ||
return await this.aggregateService.topActivity(size, isMaster); | ||
} | ||
|
||
@Get('/sitemap') | ||
@ApiOperation({ summary: '获取网站sitemap' }) | ||
@CacheKey(CacheKeys.SiteMapCatch) | ||
@CacheTTL(3600) | ||
async getSiteMapContent() { | ||
return { data: await this.aggregateService.getSiteMapContent() }; | ||
} | ||
|
||
@Get('/feed') | ||
@ApiOperation({ summary: '获取网站RSS' }) | ||
@CacheKey(CacheKeys.RSS) | ||
@CacheTTL(3600) | ||
async getRSSFeed() { | ||
return await this.aggregateService.buildRssStructure(); | ||
} | ||
|
||
@Get('/stat') | ||
@ApiOperation({ summary: '获取网站统计信息' }) | ||
@Auth() | ||
async stat() { | ||
const [count] = await Promise.all([this.aggregateService.getCounts()]); | ||
return { | ||
...count, | ||
}; | ||
} | ||
|
||
@Get('/clear') | ||
@ApiOperation({ summary: '清除缓存' }) | ||
@Auth() | ||
async clearCache() { | ||
return await this.aggregateService.clearAggregateCache(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { Transform } from 'class-transformer'; | ||
import { Min, Max, IsOptional } from 'class-validator'; | ||
|
||
export class TopQueryDto { | ||
@Transform(({ value: val }) => parseInt(val)) | ||
@Min(1) | ||
@Max(10) | ||
@IsOptional() | ||
size?: number; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export interface RSSProps { | ||
title: string; | ||
// url: string; | ||
// author: string; | ||
data: { | ||
created: Date | null; | ||
modified: Date | null; | ||
// link: string; | ||
title: string; | ||
text: string; | ||
id: string; | ||
}[]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/* | ||
* @FilePath: /mog-core/apps/core/src/modules/aggregate/aggregate.module.ts | ||
* @author: Wibus | ||
* @Date: 2022-10-01 19:50:35 | ||
* @LastEditors: Wibus | ||
* @LastEditTime: 2022-10-01 20:54:25 | ||
* Coding With IU | ||
*/ | ||
|
||
import { Module } from '@nestjs/common'; | ||
import { PageServiceModule } from '~/apps/page-service/src/page-service.module'; | ||
import { AggregateController } from './aggregate.controller'; | ||
import { AggregateService } from './aggregate.service'; | ||
|
||
@Module({ | ||
imports: [PageServiceModule], | ||
controllers: [AggregateController], | ||
providers: [AggregateService], | ||
exports: [AggregateService], | ||
}) | ||
export class AggregateModule {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
/* | ||
* @FilePath: /mog-core/apps/core/src/modules/aggregate/aggregate.service.ts | ||
* @author: Wibus | ||
* @Date: 2022-10-01 19:52:38 | ||
* @LastEditors: Wibus | ||
* @LastEditTime: 2022-10-01 20:50:46 | ||
* Coding With IU | ||
*/ | ||
|
||
import { forwardRef, Inject, Injectable } from '@nestjs/common'; | ||
import { ReturnModelType } from '@typegoose/typegoose'; | ||
import { AnyParamConstructor } from '@typegoose/typegoose/lib/types'; | ||
import { pick } from 'lodash'; | ||
import { CategoryService } from '~/apps/page-service/src/category.service'; | ||
import { CategoryModel } from '~/apps/page-service/src/model/category.model'; | ||
import { PageService } from '~/apps/page-service/src/page-service.service'; | ||
import { PostService } from '~/apps/page-service/src/post-service.service'; | ||
import { CacheService } from '~/libs/cache/src'; | ||
import { ConfigService } from '~/libs/config/src'; | ||
import { CacheKeys } from '~/shared/constants/cache.constant'; | ||
import { RSSProps } from './aggregate.interface'; | ||
|
||
@Injectable() | ||
export class AggregateService { | ||
constructor( | ||
@Inject(forwardRef(() => PostService)) | ||
private readonly postService: PostService, | ||
@Inject(forwardRef(() => PageService)) | ||
private readonly pageService: PageService, | ||
@Inject(forwardRef(() => CategoryService)) | ||
private readonly categoryService: CategoryService, | ||
|
||
private readonly configService: ConfigService, | ||
private readonly redis: CacheService, | ||
) {} | ||
|
||
getAllCategory() { | ||
return this.categoryService.getAllCategories(); | ||
} | ||
|
||
getAllPages() { | ||
return this.pageService.model | ||
.find({}, 'title _id slug order') | ||
.sort({ | ||
order: -1, | ||
modified: -1, | ||
}) | ||
.lean(); | ||
} | ||
|
||
/** | ||
* findTop 查询最新文章 | ||
* @param model 模型 | ||
* @param condition 查询条件 | ||
* @param size 获取数量 | ||
*/ | ||
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 created text'); | ||
} | ||
|
||
/** | ||
* topActivity 查询最新文章 | ||
* @param size 获取数量 | ||
* @param isMaster 是否主人 | ||
*/ | ||
async topActivity(size = 6, isMaster = false) { | ||
const [posts] = await Promise.all([ | ||
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; | ||
}); | ||
}), | ||
]); | ||
|
||
return { posts }; | ||
} | ||
|
||
/** | ||
* getSiteMapContent 获取站点地图 | ||
*/ | ||
async getSiteMapContent() { | ||
// const { | ||
// urls: { webUrl: baseURL }, | ||
// } = await this.configService.waitForConfigReady(); | ||
const combineTasks = await Promise.all([ | ||
this.postService.model | ||
.find({ | ||
hide: false, // 只获取发布的文章 | ||
password: { $nq: null }, // 只获取没有密码的文章 | ||
rss: true, // 只获取公开RSS的文章 | ||
}) | ||
.populate('category') | ||
.then((list) => { | ||
// 如果文章存在密码,则不获取 | ||
return list.filter((document) => { | ||
return document.password === null; | ||
}); | ||
}) | ||
.then((list) => | ||
list.map((document) => { | ||
return { | ||
url: new URL( | ||
`/posts/${(document.category as CategoryModel).slug}/${ | ||
document.slug | ||
}`, | ||
// baseURL | ||
), | ||
published_at: document.modified | ||
? new Date(document.modified) | ||
: new Date(document.created!), | ||
}; | ||
}), | ||
), | ||
]); | ||
return combineTasks.flat(1).sort((a, b) => { | ||
return -a.published_at.getTime() - b.published_at.getTime(); | ||
}); | ||
} | ||
|
||
/** | ||
* getRSSFeedContent 获取RSS内容 | ||
*/ | ||
async getRSSFeedContent() { | ||
// const { | ||
// urls: { webUrl }, | ||
// } = await this.configService.waitForConfigReady(); | ||
|
||
// const baseURL = webUrl.replace(/\/$/, ""); | ||
|
||
const [posts] = await Promise.all([ | ||
await this.postService.model | ||
.find({ | ||
hide: false, | ||
rss: true, | ||
}) | ||
.limit(10) | ||
.sort({ created: -1 }) | ||
.populate('category') | ||
.then((list) => { | ||
// 如果文章存在密码,则不获取 | ||
return list.filter((document) => { | ||
return document.password === null; | ||
}); | ||
}), | ||
]); | ||
const postsRss: RSSProps['data'] = posts.map((post) => { | ||
return { | ||
id: post._id, | ||
title: post.title, | ||
text: post.text, | ||
created: post.created!, | ||
modified: post.modified || null, | ||
// link: baseURL + this.urlService.build(post), | ||
}; | ||
}); | ||
return postsRss | ||
.sort((a, b) => b.created!.getTime() - a.created!.getTime()) | ||
.slice(0, 10); | ||
} | ||
|
||
/** | ||
* buildRssStructure 构建RSS结构 | ||
* @returns {Promise<RSSProps>} | ||
*/ | ||
async buildRssStructure(): Promise<RSSProps> { | ||
const data = await this.getRSSFeedContent(); | ||
const title = (await this.configService.get('seo')).title || ''; | ||
return { | ||
title, | ||
data, | ||
}; | ||
} | ||
|
||
async getCounts() { | ||
const [posts, pages, categories] = await Promise.all([ | ||
this.postService.model.countDocuments({ | ||
hide: false, | ||
password: { $nq: null }, | ||
rss: true, | ||
}), | ||
this.pageService.model.countDocuments(), | ||
this.categoryService.model.countDocuments(), | ||
]); | ||
|
||
return { | ||
posts, | ||
pages, | ||
categories, | ||
}; | ||
} | ||
|
||
public clearAggregateCache() { | ||
return Promise.all([ | ||
this.redis.getClient().del(CacheKeys.RSS), | ||
this.redis.getClient().del(CacheKeys.RSSXmlCatch), | ||
this.redis.getClient().del(CacheKeys.AggregateCatch), | ||
this.redis.getClient().del(CacheKeys.SiteMapCatch), | ||
this.redis.getClient().del(CacheKeys.SiteMapXmlCatch), | ||
]); | ||
} | ||
} |
Oops, something went wrong.