Skip to content

Commit

Permalink
feat(aggregate): aggregate data module (#396)
Browse files Browse the repository at this point in the history
  • Loading branch information
wibus-wee authored Oct 1, 2022
1 parent 3607a0c commit 1d266d6
Show file tree
Hide file tree
Showing 7 changed files with 365 additions and 2 deletions.
2 changes: 2 additions & 0 deletions apps/core/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { JSONSerializeInterceptor } from '~/shared/common/interceptors/json-seri
import { ResponseInterceptor } from '~/shared/common/interceptors/response.interceptor';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AggregateModule } from './modules/aggregate/aggregate.module';
import { CategoryModule } from './modules/category/category.module';
import { PageModule } from './modules/page/page.module';
import { PostModule } from './modules/post/post.module';
Expand All @@ -26,6 +27,7 @@ import { UserModule } from './modules/user/user.module';
PostModule,
PageModule,
CategoryModule,
AggregateModule,
],
controllers: [AppController],
providers: [
Expand Down
92 changes: 92 additions & 0 deletions apps/core/src/modules/aggregate/aggregate.controller.ts
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();
}
}
10 changes: 10 additions & 0 deletions apps/core/src/modules/aggregate/aggregate.dto.ts
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;
}
13 changes: 13 additions & 0 deletions apps/core/src/modules/aggregate/aggregate.interface.ts
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;
}[];
}
21 changes: 21 additions & 0 deletions apps/core/src/modules/aggregate/aggregate.module.ts
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 {}
218 changes: 218 additions & 0 deletions apps/core/src/modules/aggregate/aggregate.service.ts
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),
]);
}
}
Loading

0 comments on commit 1d266d6

Please sign in to comment.