Skip to content

Commit

Permalink
feat: v3.8.2
Browse files Browse the repository at this point in the history
  • Loading branch information
surmon-china committed Feb 15, 2022
1 parent fa71909 commit 7260652
Show file tree
Hide file tree
Showing 14 changed files with 133 additions and 49 deletions.
13 changes: 11 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to this project will be documented in this file.

### 3.8.2 (2022-02-15)

**Feature**

- New API `/article/calendar`
- Refactoring API `/article/related/:id` to `/article/:id/context`
- Rename API `/article/hot` to `/article/hottest`
- Improve `Article` module

### 3.8.1 (2022-02-15)

**BugFix**
Expand All @@ -25,7 +34,7 @@ All notable changes to this project will be documented in this file.
- improve `Expansion` statistic service
- Remove query `cache` field
- Rename `tag.count` `category.count` to `<target>.articles_count`
- Add API `/article/hot` `/article/releted` `/tag/all`
- Add API `/article/hot` `/article/related` `/tag/all`
- Add `PermissionPipe` `ExposePipe` pipes
- Rename `HttpProcessor` to `Responsor`
- Generate documentation by `compodoc`
Expand Down Expand Up @@ -55,7 +64,7 @@ All notable changes to this project will be documented in this file.

**Feature**

- **[Article]** random releted articles
- **[Article]** random related articles
- **[Comment]** improve email content
- **[Vote]** send email to admin when new vote

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nodepress",
"version": "3.8.1",
"version": "3.8.2",
"description": "RESTful API service for Surmon.me blog",
"author": {
"name": "Surmon",
Expand Down
2 changes: 1 addition & 1 deletion src/constants/cache.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
export const CACHE_PREFIX = '__nodepress_cache_'
export const OPTION = CACHE_PREFIX + 'option'
export const ALL_TAGS = CACHE_PREFIX + 'all-tags'
export const HOT_ARTICLES = CACHE_PREFIX + 'hot-articles'
export const HOTTEST_ARTICLES = CACHE_PREFIX + 'hottest-articles'
export const ARCHIVE = CACHE_PREFIX + 'archive'
export const TODAY_VIEWS = CACHE_PREFIX + 'today-views'

Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/biz.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
export enum SortType {
Asc = 1, // 升序
Desc = -1, // 降序
Hot = 2, // 热序
Hottest = 2, // 热序
}

// 发布状态
Expand Down
2 changes: 1 addition & 1 deletion src/models/paginate.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class PaginateOptionDTO extends PaginateBaseOptionDTO {
}

export class PaginateOptionWithHotSortDTO extends PaginateBaseOptionDTO {
@IsIn([SortType.Asc, SortType.Desc, SortType.Hot])
@IsIn([SortType.Asc, SortType.Desc, SortType.Hottest])
@IsInt()
@IsNotEmpty()
@IsOptional()
Expand Down
8 changes: 6 additions & 2 deletions src/modules/archive/archive.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import { CacheService, CacheIOResult } from '@app/processors/cache/cache.service
import { MongooseModel } from '@app/interfaces/mongoose.interface'
import { SortType } from '@app/interfaces/biz.interface'
import { Category } from '@app/modules/category/category.model'
import { Article, ARTICLE_GUEST_QUERY_FILTER } from '@app/modules/article/article.model'
import {
Article,
ARTICLE_LIST_QUERY_GUEST_FILTER,
ARTICLE_LIST_QUERY_PROJECTION,
} from '@app/modules/article/article.model'
import { Tag } from '@app/modules/tag/tag.model'
import * as CACHE_KEY from '@app/constants/cache.constant'
import logger from '@app/utils/logger'
Expand Down Expand Up @@ -51,7 +55,7 @@ export class ArchiveService {

private getAllArticles(): Promise<Article[]> {
return this.articleModel
.find(ARTICLE_GUEST_QUERY_FILTER, null, { select: '-content' })
.find(ARTICLE_LIST_QUERY_GUEST_FILTER, ARTICLE_LIST_QUERY_PROJECTION)
.sort({ _id: SortType.Desc })
.exec()
}
Expand Down
55 changes: 39 additions & 16 deletions src/modules/article/article.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ import { SortType } from '@app/interfaces/biz.interface'
import { TagService } from '@app/modules/tag/tag.service'
import { CategoryService } from '@app/modules/category/category.service'
import { PaginateResult, PaginateQuery, PaginateOptions } from '@app/utils/paginate'
import { ArticlePaginateQueryDTO, ArticleListQueryDTO, ArticleIDsDTO, ArticlesStateDTO } from './article.dto'
import { ARTICLE_HOT_SORT_PARAMS } from './article.model'
import {
ArticlePaginateQueryDTO,
ArticleListQueryDTO,
ArticleCalendarQueryDTO,
ArticleIDsDTO,
ArticlesStateDTO,
} from './article.dto'
import { ARTICLE_HOTTEST_SORT_PARAMS } from './article.model'
import { ArticleService } from './article.service'
import { Article } from './article.model'

Expand All @@ -43,8 +49,8 @@ export class ArticleController {

// sort
if (!lodash.isUndefined(sort)) {
if (sort === SortType.Hot) {
paginateOptions.sort = ARTICLE_HOT_SORT_PARAMS
if (sort === SortType.Hottest) {
paginateOptions.sort = ARTICLE_HOTTEST_SORT_PARAMS
} else {
paginateOptions.dateSort = sort
}
Expand Down Expand Up @@ -91,20 +97,37 @@ export class ArticleController {
return this.articleService.paginater(paginateQuery, paginateOptions)
}

@Get('hot')
@Responsor.handle('Get hot articles')
getHotArticles(@Query(ExposePipe) query: ArticleListQueryDTO): Promise<Array<Article>> {
return query.count ? this.articleService.getHotArticles(query.count) : this.articleService.getHotArticlesCache()
@Get('hottest')
@Responsor.handle('Get hottest articles')
getHottestArticles(@Query(ExposePipe) query: ArticleListQueryDTO): Promise<Array<Article>> {
return query.count
? this.articleService.getHottestArticles(query.count)
: this.articleService.getHottestArticlesCache()
}

@Get('related/:id')
@Responsor.handle('Get related articles')
async getRelatedArticles(
@QueryParams() { params }: QueryParamsResult,
@Query(ExposePipe) query: ArticleListQueryDTO
): Promise<Array<Article>> {
const article = await this.articleService.getDetailByNumberIDOrSlug({ idOrSlug: Number(params.id) })
return this.articleService.getRelatedArticles(article, query.count ?? 20)
@Get('calendar')
@UseGuards(AdminMaybeGuard)
@Responsor.handle('Get article calendar')
getArticleCalendar(
@Query(ExposePipe) query: ArticleCalendarQueryDTO,
@QueryParams() { isUnauthenticated }: QueryParamsResult
) {
return this.articleService.getCalendar(isUnauthenticated, query.timezone)
}

@Get(':id/context')
@Responsor.handle('Get context articles')
async getArticleContext(@QueryParams() { params }: QueryParamsResult) {
const articleID = Number(params.id)
const article = await this.articleService.getDetailByNumberIDOrSlug({ idOrSlug: articleID, publicOnly: true })
const [prev_article] = await this.articleService.getNearArticles(articleID, 'early', 1)
const [next_article] = await this.articleService.getNearArticles(articleID, 'later', 1)
const related_articles = await this.articleService.getRelatedArticles(article, 20)
return {
prev_article: prev_article || null,
next_article: next_article || null,
related_articles,
}
}

@Get(':id')
Expand Down
7 changes: 7 additions & 0 deletions src/modules/article/article.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ export class ArticleListQueryDTO {
count?: number
}

export class ArticleCalendarQueryDTO {
@IsString()
@IsNotEmpty()
@IsOptional()
timezone?: string
}

export class ArticleIDsDTO {
@ArrayNotEmpty()
@ArrayUnique()
Expand Down
6 changes: 4 additions & 2 deletions src/modules/article/article.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ export const ARTICLE_PUBLISH_STATES = [PublishState.Draft, PublishState.Publishe
export const ARTICLE_PUBLIC_STATES = [PublicState.Public, PublicState.Secret, PublicState.Reserve] as const
export const ARTICLE_ORIGIN_STATES = [OriginState.Original, OriginState.Reprint, OriginState.Hybrid] as const

export const ARTICLE_GUEST_QUERY_FILTER = Object.freeze({
export const ARTICLE_FULL_QUERY_REF_POPULATE = ['category', 'tag']
export const ARTICLE_LIST_QUERY_PROJECTION = { content: false }
export const ARTICLE_LIST_QUERY_GUEST_FILTER = Object.freeze({
state: PublishState.Published,
public: PublicState.Public,
})

export const ARTICLE_HOT_SORT_PARAMS = Object.freeze({
export const ARTICLE_HOTTEST_SORT_PARAMS = Object.freeze({
'meta.comments': SortType.Desc,
'meta.likes': SortType.Desc,
})
Expand Down
73 changes: 56 additions & 17 deletions src/modules/article/article.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,18 @@ import { TagService } from '@app/modules/tag/tag.service'
import { PublishState } from '@app/interfaces/biz.interface'
import { MongooseModel, MongooseDoc, MongooseID } from '@app/interfaces/mongoose.interface'
import { PaginateResult, PaginateQuery, PaginateOptions } from '@app/utils/paginate'
import { Article, ARTICLE_GUEST_QUERY_FILTER, ARTICLE_HOT_SORT_PARAMS } from './article.model'
import {
Article,
ARTICLE_LIST_QUERY_GUEST_FILTER,
ARTICLE_LIST_QUERY_PROJECTION,
ARTICLE_FULL_QUERY_REF_POPULATE,
ARTICLE_HOTTEST_SORT_PARAMS,
} from './article.model'
import * as CACHE_KEY from '@app/constants/cache.constant'

@Injectable()
export class ArticleService {
private hotArticlesCache: CacheIntervalResult<Array<Article>>
private hottestArticlesCache: CacheIntervalResult<Array<Article>>

constructor(
private readonly seoService: SeoService,
Expand All @@ -30,35 +36,56 @@ export class ArticleService {
private readonly archiveService: ArchiveService,
@InjectModel(Article) private readonly articleModel: MongooseModel<Article>
) {
this.hotArticlesCache = this.cacheService.interval({
key: CACHE_KEY.HOT_ARTICLES,
promise: () => this.getHotArticles(20),
this.hottestArticlesCache = this.cacheService.interval({
key: CACHE_KEY.HOTTEST_ARTICLES,
promise: () => this.getHottestArticles(20),
timeout: {
success: 1000 * 60 * 30, // 成功后 30 分钟更新一次数据
error: 1000 * 60 * 5, // 失败后 5 分钟更新一次数据
},
})
}

public getHotArticles(count: number): Promise<Array<Article>> {
return this.paginater(ARTICLE_GUEST_QUERY_FILTER, {
public getHottestArticles(count: number): Promise<Array<Article>> {
return this.paginater(ARTICLE_LIST_QUERY_GUEST_FILTER, {
perPage: count,
sort: ARTICLE_HOT_SORT_PARAMS,
sort: ARTICLE_HOTTEST_SORT_PARAMS,
}).then((result) => result.documents)
}

public getHotArticlesCache(): Promise<Array<Article>> {
return this.hotArticlesCache()
public getHottestArticlesCache(): Promise<Array<Article>> {
return this.hottestArticlesCache()
}

// get near articles
public async getNearArticles(articleID: number, type: 'later' | 'early', count: number): Promise<Article[]> {
const typeFieldMap = {
early: { field: '$lt', sort: -1 },
later: { field: '$gt', sort: 1 },
}
const trgetType = typeFieldMap[type]
return this.articleModel
.find(
{ ...ARTICLE_LIST_QUERY_GUEST_FILTER, id: { [trgetType.field]: articleID } },
ARTICLE_LIST_QUERY_PROJECTION
)
.populate(ARTICLE_FULL_QUERY_REF_POPULATE)
.sort({ id: trgetType.sort })
.limit(count)
.exec()
}

// get related articles
public async getRelatedArticles(article: Article, count: number): Promise<Article[]> {
const findParams: FilterQuery<Article> = {
...ARTICLE_GUEST_QUERY_FILTER,
...ARTICLE_LIST_QUERY_GUEST_FILTER,
tag: { $in: article.tag.map((t) => (t as any)._id) },
category: { $in: article.category.map((c) => (c as any)._id) },
}
const articles = await this.articleModel.find(findParams, '-content', { limit: count * 3 }).exec()
const articles = await this.articleModel
.find(findParams, ARTICLE_LIST_QUERY_PROJECTION, { limit: count * 3 })
.populate(ARTICLE_FULL_QUERY_REF_POPULATE)
.exec()
const filtered = articles.filter((a) => a.id !== article.id).map((a) => a.toObject())
return lodash.sampleSize(filtered, count)
}
Expand All @@ -67,8 +94,8 @@ export class ArticleService {
public paginater(query: PaginateQuery<Article>, options: PaginateOptions): Promise<PaginateResult<Article>> {
return this.articleModel.paginate(query, {
...options,
projection: '-content',
populate: ['category', 'tag'],
projection: ARTICLE_LIST_QUERY_PROJECTION,
populate: ARTICLE_FULL_QUERY_REF_POPULATE,
})
}

Expand Down Expand Up @@ -103,8 +130,8 @@ export class ArticleService {
}

return this.articleModel
.findOne(publicOnly ? { ...params, ...ARTICLE_GUEST_QUERY_FILTER } : params)
.populate(populate ? ['category', 'tag'] : [])
.findOne(publicOnly ? { ...params, ...ARTICLE_LIST_QUERY_GUEST_FILTER } : params)
.populate(populate ? ARTICLE_FULL_QUERY_REF_POPULATE : [])
.exec()
.then((result) => result || Promise.reject(`Article '${idOrSlug}' not found`))
}
Expand Down Expand Up @@ -207,7 +234,19 @@ export class ArticleService {
}

public async getTotalCount(publicOnly: boolean): Promise<number> {
return await this.articleModel.countDocuments(publicOnly ? ARTICLE_GUEST_QUERY_FILTER : {}).exec()
return await this.articleModel.countDocuments(publicOnly ? ARTICLE_LIST_QUERY_GUEST_FILTER : {}).exec()
}

public getCalendar(publicOnly: boolean, timezone = 'GMT') {
return this.articleModel
.aggregate<{ _id: string; count: number }>([
{ $match: publicOnly ? ARTICLE_LIST_QUERY_GUEST_FILTER : {} },
{ $project: { day: { $dateToString: { date: '$create_at', format: '%Y-%m-%d', timezone } } } },
{ $group: { _id: '$day', count: { $sum: 1 } } },
{ $sort: { _id: 1 } },
])
.then((calendar) => calendar.map(({ _id, ...r }) => ({ ...r, day: _id })))
.catch(() => Promise.reject(`Invalid timezone identifier: '${timezone}'`))
}

public async getMetaStatistic() {
Expand Down
4 changes: 2 additions & 2 deletions src/modules/category/category.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { MongooseModel, MongooseDoc, MongooseID } from '@app/interfaces/mongoose
import { PaginateResult, PaginateQuery, PaginateOptions } from '@app/utils/paginate'
import { ArchiveService } from '@app/modules/archive/archive.service'
import { SeoService } from '@app/processors/helper/helper.service.seo'
import { Article, ARTICLE_GUEST_QUERY_FILTER } from '@app/modules/article/article.model'
import { Article, ARTICLE_LIST_QUERY_GUEST_FILTER } from '@app/modules/article/article.model'
import { Category } from './category.model'

@Injectable()
Expand All @@ -30,7 +30,7 @@ export class CategoryService {
): Promise<PaginateResult<Category>> {
const categories = await this.categoryModel.paginate(query, { ...options, lean: true })
const counts = await this.articleModel.aggregate([
{ $match: publicOnly ? ARTICLE_GUEST_QUERY_FILTER : {} },
{ $match: publicOnly ? ARTICLE_LIST_QUERY_GUEST_FILTER : {} },
{ $unwind: '$category' },
{ $group: { _id: '$category', count: { $sum: 1 } } },
])
Expand Down
2 changes: 1 addition & 1 deletion src/modules/comment/comment.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class CommentController {

// sort
if (!lodash.isUndefined(sort)) {
if (sort === SortType.Hot) {
if (sort === SortType.Hottest) {
paginateOptions.sort = { likes: SortType.Desc }
} else {
paginateOptions.dateSort = sort
Expand Down
4 changes: 2 additions & 2 deletions src/modules/tag/tag.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { MongooseModel, MongooseDoc, MongooseID } from '@app/interfaces/mongoose
import { PaginateResult, PaginateQuery, PaginateOptions } from '@app/utils/paginate'
import { SortType } from '@app/interfaces/biz.interface'
import { ArchiveService } from '@app/modules/archive/archive.service'
import { Article, ARTICLE_GUEST_QUERY_FILTER } from '@app/modules/article/article.model'
import { Article, ARTICLE_LIST_QUERY_GUEST_FILTER } from '@app/modules/article/article.model'
import { Tag } from './tag.model'
import * as CACHE_KEY from '@app/constants/cache.constant'
import logger from '@app/utils/logger'
Expand Down Expand Up @@ -43,7 +43,7 @@ export class TagService {

private async aggregate(publicOnly: boolean, documents: Array<Tag>) {
const counts = await this.articleModel.aggregate<{ _id: Types.ObjectId; count: number }>([
{ $match: publicOnly ? ARTICLE_GUEST_QUERY_FILTER : {} },
{ $match: publicOnly ? ARTICLE_LIST_QUERY_GUEST_FILTER : {} },
{ $unwind: '$tag' },
{ $group: { _id: '$tag', count: { $sum: 1 } } },
])
Expand Down
2 changes: 1 addition & 1 deletion src/modules/vote/vote.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export class VoteController {
try {
const postID = await this.disqusPublicService.getDisqusPostIDByCommentID(voteBody.comment_id)
if (postID) {
const result = await this.disqusPublicService.votePost({
await this.disqusPublicService.votePost({
access_token: token.access_token,
post: postID,
vote: voteBody.vote,
Expand Down

0 comments on commit 7260652

Please sign in to comment.