feat:增加批量删除、软删除和软删除恢复
- 完成基础的软删除 - 完成树形数据的软删除(children未处理)
This commit is contained in:
		
							parent
							
								
									c151657116
								
							
						
					
					
						commit
						5bcb4853e5
					
				
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							@ -17,7 +17,7 @@ export const database = (): TypeOrmModuleOptions => ({
 | 
			
		||||
    // database: 'ink_apps',
 | 
			
		||||
    // 以下为sqlite配置
 | 
			
		||||
    type: 'better-sqlite3',
 | 
			
		||||
    database: resolve(__dirname, '../../back/database6.db'),
 | 
			
		||||
    database: resolve(__dirname, '../../back/database9.db'),
 | 
			
		||||
    synchronize: true,
 | 
			
		||||
    autoLoadEntities: true,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -11,8 +11,14 @@ import {
 | 
			
		||||
    SerializeOptions,
 | 
			
		||||
} from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
import { CreateCategoryDto, QueryCategoryDto, UpdateCategoryDto } from '@/modules/content/dtos';
 | 
			
		||||
import {
 | 
			
		||||
    CreateCategoryDto,
 | 
			
		||||
    QueryCategoryDto,
 | 
			
		||||
    QueryCategoryTreeDto,
 | 
			
		||||
    UpdateCategoryDto,
 | 
			
		||||
} from '@/modules/content/dtos';
 | 
			
		||||
import { CategoryService } from '@/modules/content/services';
 | 
			
		||||
import { DeleteWithTrashDto, RestoreDto } from '@/modules/restful/dtos';
 | 
			
		||||
 | 
			
		||||
@Controller('categories')
 | 
			
		||||
export class CategoryController {
 | 
			
		||||
@ -20,8 +26,8 @@ export class CategoryController {
 | 
			
		||||
 | 
			
		||||
    @Get('tree')
 | 
			
		||||
    @SerializeOptions({ groups: ['category-tree'] })
 | 
			
		||||
    async tree() {
 | 
			
		||||
        return this.service.findTress();
 | 
			
		||||
    async tree(@Query() options: QueryCategoryTreeDto) {
 | 
			
		||||
        return this.service.findTrees(options);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Get()
 | 
			
		||||
@ -57,9 +63,19 @@ export class CategoryController {
 | 
			
		||||
        return this.service.update(data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Delete(':id')
 | 
			
		||||
    @Delete()
 | 
			
		||||
    @SerializeOptions({ groups: ['category-detail'] })
 | 
			
		||||
    async delete(@Param('id', new ParseUUIDPipe()) id: string) {
 | 
			
		||||
        return this.service.delete(id);
 | 
			
		||||
    async delete(@Body() data: DeleteWithTrashDto) {
 | 
			
		||||
        const { ids, trash } = data;
 | 
			
		||||
 | 
			
		||||
        return this.service.delete(ids, trash);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Patch('restore')
 | 
			
		||||
    @SerializeOptions({ groups: ['category-detail'] })
 | 
			
		||||
    async restore(@Body() data: RestoreDto) {
 | 
			
		||||
        const { ids } = data;
 | 
			
		||||
 | 
			
		||||
        return this.service.restore(ids);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,17 +1,8 @@
 | 
			
		||||
import {
 | 
			
		||||
    Body,
 | 
			
		||||
    Controller,
 | 
			
		||||
    Delete,
 | 
			
		||||
    Get,
 | 
			
		||||
    Param,
 | 
			
		||||
    ParseUUIDPipe,
 | 
			
		||||
    Post,
 | 
			
		||||
    Query,
 | 
			
		||||
    SerializeOptions,
 | 
			
		||||
} from '@nestjs/common';
 | 
			
		||||
import { Body, Controller, Delete, Get, Post, Query, SerializeOptions } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
import { CreateCommentDto, QueryCommentDto, QueryCommentTreeDto } from '@/modules/content/dtos';
 | 
			
		||||
import { CommentService } from '@/modules/content/services';
 | 
			
		||||
import { DeleteDto } from '@/modules/restful/dtos';
 | 
			
		||||
 | 
			
		||||
@Controller('comments')
 | 
			
		||||
export class CommentController {
 | 
			
		||||
@ -44,9 +35,10 @@ export class CommentController {
 | 
			
		||||
        return this.service.create(data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Delete(':id')
 | 
			
		||||
    @Delete()
 | 
			
		||||
    @SerializeOptions({ groups: ['comment-detail'] })
 | 
			
		||||
    async delete(@Param('id', new ParseUUIDPipe()) id: string) {
 | 
			
		||||
        return this.service.delete(id);
 | 
			
		||||
    async delete(@Body() data: DeleteDto) {
 | 
			
		||||
        const { ids } = data;
 | 
			
		||||
        return this.service.delete(ids);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@ import {
 | 
			
		||||
 | 
			
		||||
import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dtos';
 | 
			
		||||
import { PostService } from '@/modules/content/services';
 | 
			
		||||
import { DeleteDto, DeleteWithTrashDto } from '@/modules/restful/dtos';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文章控制器
 | 
			
		||||
@ -56,9 +57,19 @@ export class PostController {
 | 
			
		||||
        return this.postService.update(data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Delete(':id')
 | 
			
		||||
    @Delete()
 | 
			
		||||
    @SerializeOptions({ groups: ['post-detail'] })
 | 
			
		||||
    async delete(@Param('id', new ParseUUIDPipe()) id: string) {
 | 
			
		||||
        return this.postService.delete(id);
 | 
			
		||||
    async delete(@Body() data: DeleteWithTrashDto) {
 | 
			
		||||
        const { ids, trash } = data;
 | 
			
		||||
 | 
			
		||||
        return this.postService.delete(ids, trash);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Patch('restore')
 | 
			
		||||
    @SerializeOptions({ groups: ['post-detail'] })
 | 
			
		||||
    async restore(@Body() data: DeleteDto) {
 | 
			
		||||
        const { ids } = data;
 | 
			
		||||
 | 
			
		||||
        return this.postService.restore(ids);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -11,8 +11,9 @@ import {
 | 
			
		||||
    SerializeOptions,
 | 
			
		||||
} from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
import { CreateTagDto, QueryCategoryDto, UpdateTagDto } from '@/modules/content/dtos';
 | 
			
		||||
import { CreateTagDto, QueryTagsDto, UpdateTagDto } from '@/modules/content/dtos';
 | 
			
		||||
import { TagService } from '@/modules/content/services';
 | 
			
		||||
import { DeleteDto, DeleteWithTrashDto } from '@/modules/restful/dtos';
 | 
			
		||||
 | 
			
		||||
@Controller('tags')
 | 
			
		||||
export class TagController {
 | 
			
		||||
@ -22,7 +23,7 @@ export class TagController {
 | 
			
		||||
    @SerializeOptions({})
 | 
			
		||||
    async list(
 | 
			
		||||
        @Query()
 | 
			
		||||
        options: QueryCategoryDto,
 | 
			
		||||
        options: QueryTagsDto,
 | 
			
		||||
    ) {
 | 
			
		||||
        return this.service.paginate(options);
 | 
			
		||||
    }
 | 
			
		||||
@ -54,9 +55,19 @@ export class TagController {
 | 
			
		||||
        return this.service.update(data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Delete(':id')
 | 
			
		||||
    @SerializeOptions({})
 | 
			
		||||
    async delete(@Param('id', new ParseUUIDPipe()) id: string) {
 | 
			
		||||
        return this.service.delete(id);
 | 
			
		||||
    @Delete()
 | 
			
		||||
    @SerializeOptions({ groups: ['post-list'] })
 | 
			
		||||
    async delete(@Body() data: DeleteWithTrashDto) {
 | 
			
		||||
        const { ids, trash } = data;
 | 
			
		||||
 | 
			
		||||
        return this.service.delete(ids, trash);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Patch('restore')
 | 
			
		||||
    @SerializeOptions({ groups: ['post-list'] })
 | 
			
		||||
    async restore(@Body() data: DeleteDto) {
 | 
			
		||||
        const { ids } = data;
 | 
			
		||||
 | 
			
		||||
        return this.service.restore(ids);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ import { PartialType } from '@nestjs/swagger';
 | 
			
		||||
import { Transform } from 'class-transformer';
 | 
			
		||||
import {
 | 
			
		||||
    IsDefined,
 | 
			
		||||
    IsEnum,
 | 
			
		||||
    IsNotEmpty,
 | 
			
		||||
    IsNumber,
 | 
			
		||||
    IsOptional,
 | 
			
		||||
@ -14,12 +15,23 @@ import { toNumber } from 'lodash';
 | 
			
		||||
 | 
			
		||||
import { CategoryEntity } from '@/modules/content/entities';
 | 
			
		||||
import { DtoValidation } from '@/modules/core/decorators';
 | 
			
		||||
import { SelectTrashMode } from '@/modules/database/constants';
 | 
			
		||||
import { IsDataExist, IsTreeUnique, IsTreeUniqueExist } from '@/modules/database/constraints';
 | 
			
		||||
 | 
			
		||||
import { PaginateOptions } from '@/modules/database/types';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 树形分类查询验证
 | 
			
		||||
 */
 | 
			
		||||
@DtoValidation({ type: 'query' })
 | 
			
		||||
export class QueryCategoryDto implements PaginateOptions {
 | 
			
		||||
export class QueryCategoryTreeDto {
 | 
			
		||||
    @IsEnum(SelectTrashMode)
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    trashed?: SelectTrashMode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@DtoValidation({ type: 'query' })
 | 
			
		||||
export class QueryCategoryDto extends QueryCategoryTreeDto implements PaginateOptions {
 | 
			
		||||
    @Transform(({ value }) => toNumber(value))
 | 
			
		||||
    @Min(1, { message: '当前页数必须大于1' })
 | 
			
		||||
    @IsNumber()
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ import { PostOrderType } from '@/modules/content/constants';
 | 
			
		||||
import { CategoryEntity, TagEntity } from '@/modules/content/entities';
 | 
			
		||||
import { DtoValidation } from '@/modules/core/decorators';
 | 
			
		||||
import { toBoolean } from '@/modules/core/helpers';
 | 
			
		||||
import { SelectTrashMode } from '@/modules/database/constants';
 | 
			
		||||
import { IsDataExist } from '@/modules/database/constraints';
 | 
			
		||||
import { PaginateOptions } from '@/modules/database/types';
 | 
			
		||||
 | 
			
		||||
@ -66,6 +67,10 @@ export class QueryPostDto implements PaginateOptions {
 | 
			
		||||
    @IsUUID(undefined, { message: '标签ID必须是UUID' })
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    tag?: string;
 | 
			
		||||
 | 
			
		||||
    @IsEnum(SelectTrashMode)
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    trashed?: SelectTrashMode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ import { PartialType } from '@nestjs/swagger';
 | 
			
		||||
import { Transform } from 'class-transformer';
 | 
			
		||||
import {
 | 
			
		||||
    IsDefined,
 | 
			
		||||
    IsEnum,
 | 
			
		||||
    IsNotEmpty,
 | 
			
		||||
    IsNumber,
 | 
			
		||||
    IsOptional,
 | 
			
		||||
@ -12,6 +13,7 @@ import {
 | 
			
		||||
import { toNumber } from 'lodash';
 | 
			
		||||
 | 
			
		||||
import { DtoValidation } from '@/modules/core/decorators';
 | 
			
		||||
import { SelectTrashMode } from '@/modules/database/constants';
 | 
			
		||||
import { PaginateOptions } from '@/modules/database/types';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -19,6 +21,10 @@ import { PaginateOptions } from '@/modules/database/types';
 | 
			
		||||
 */
 | 
			
		||||
@DtoValidation({ type: 'query' })
 | 
			
		||||
export class QueryTagsDto implements PaginateOptions {
 | 
			
		||||
    @IsEnum(SelectTrashMode)
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    trashed?: SelectTrashMode;
 | 
			
		||||
 | 
			
		||||
    @Transform(({ value }) => toNumber(value))
 | 
			
		||||
    @Min(1, { message: '当前页数必须大于1' })
 | 
			
		||||
    @IsNumber()
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { Exclude, Expose } from 'class-transformer';
 | 
			
		||||
import { Column, Entity, ManyToMany, PrimaryColumn, Relation } from 'typeorm';
 | 
			
		||||
import { Exclude, Expose, Type } from 'class-transformer';
 | 
			
		||||
import { Column, DeleteDateColumn, Entity, ManyToMany, PrimaryColumn, Relation } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import { PostEntity } from '@/modules/content/entities/post.entity';
 | 
			
		||||
 | 
			
		||||
@ -26,4 +26,11 @@ export class TagEntity {
 | 
			
		||||
     */
 | 
			
		||||
    @Expose()
 | 
			
		||||
    postCount: number;
 | 
			
		||||
 | 
			
		||||
    @Expose()
 | 
			
		||||
    @Type(() => Date)
 | 
			
		||||
    @DeleteDateColumn({
 | 
			
		||||
        comment: '删除时间',
 | 
			
		||||
    })
 | 
			
		||||
    deletedAt: Date;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { unset } from 'lodash';
 | 
			
		||||
import { pick, unset } from 'lodash';
 | 
			
		||||
import { FindOptionsUtils, FindTreeOptions, TreeRepository } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import { CategoryEntity } from '@/modules/content/entities';
 | 
			
		||||
@ -17,7 +17,12 @@ export class CategoryRepository extends TreeRepository<CategoryEntity> {
 | 
			
		||||
     * 树形结构查询
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    async findTrees(options?: FindTreeOptions) {
 | 
			
		||||
    async findTrees(
 | 
			
		||||
        options?: FindTreeOptions & {
 | 
			
		||||
            onlyTrashed?: boolean;
 | 
			
		||||
            withTrashed?: boolean;
 | 
			
		||||
        },
 | 
			
		||||
    ) {
 | 
			
		||||
        const roots = await this.findRoots(options);
 | 
			
		||||
        await Promise.all(roots.map((root) => this.findDescendantsTree(root, options)));
 | 
			
		||||
        return roots;
 | 
			
		||||
@ -27,19 +32,28 @@ export class CategoryRepository extends TreeRepository<CategoryEntity> {
 | 
			
		||||
     * 查询顶级分类
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    findRoots(options?: FindTreeOptions) {
 | 
			
		||||
    findRoots(
 | 
			
		||||
        options?: FindTreeOptions & {
 | 
			
		||||
            onlyTrashed?: boolean;
 | 
			
		||||
            withTrashed?: boolean;
 | 
			
		||||
        },
 | 
			
		||||
    ) {
 | 
			
		||||
        const escapeAlias = (alias: string) => this.manager.connection.driver.escape(alias);
 | 
			
		||||
        const escapeColumn = (column: string) => this.manager.connection.driver.escape(column);
 | 
			
		||||
 | 
			
		||||
        const joinColumn = this.metadata.treeParentRelation!.joinColumns[0];
 | 
			
		||||
        const parentPropertyName = joinColumn.givenDatabaseName || joinColumn.databaseName;
 | 
			
		||||
 | 
			
		||||
        const qb = this.buildBaseQB().orderBy('category.customOrder', 'ASC');
 | 
			
		||||
        FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
 | 
			
		||||
        qb.where(`${escapeAlias('category')}.${escapeColumn(parentPropertyName)} IS NULL`);
 | 
			
		||||
 | 
			
		||||
        return qb
 | 
			
		||||
            .where(`${escapeAlias('category')}.${escapeColumn(parentPropertyName)} IS NULL`)
 | 
			
		||||
            .getMany();
 | 
			
		||||
        FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, pick(options, ['relations', 'depth']));
 | 
			
		||||
 | 
			
		||||
        if (options?.withTrashed) {
 | 
			
		||||
            qb.withDeleted();
 | 
			
		||||
            if (options?.onlyTrashed) qb.where(`category.deletedAt IS NOT NULL`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return qb.getMany();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -47,11 +61,22 @@ export class CategoryRepository extends TreeRepository<CategoryEntity> {
 | 
			
		||||
     * @param entity
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    findDescendants(entity: CategoryEntity, options?: FindTreeOptions) {
 | 
			
		||||
    findDescendants(
 | 
			
		||||
        entity: CategoryEntity,
 | 
			
		||||
        options?: FindTreeOptions & {
 | 
			
		||||
            onlyTrashed?: boolean;
 | 
			
		||||
            withTrashed?: boolean;
 | 
			
		||||
        },
 | 
			
		||||
    ) {
 | 
			
		||||
        const qb = this.createDescendantsQueryBuilder('category', 'treeClosure', entity);
 | 
			
		||||
        FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
 | 
			
		||||
        qb.orderBy('category.customOrder', 'ASC');
 | 
			
		||||
 | 
			
		||||
        if (options?.withTrashed) {
 | 
			
		||||
            qb.withDeleted();
 | 
			
		||||
            if (options?.onlyTrashed) qb.where(`category.deletedAt IS NOT NULL`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return qb.getMany();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -60,11 +85,22 @@ export class CategoryRepository extends TreeRepository<CategoryEntity> {
 | 
			
		||||
     * @param entity
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    findAncestors(entity: CategoryEntity, options?: FindTreeOptions) {
 | 
			
		||||
    findAncestors(
 | 
			
		||||
        entity: CategoryEntity,
 | 
			
		||||
        options?: FindTreeOptions & {
 | 
			
		||||
            onlyTrashed?: boolean;
 | 
			
		||||
            withTrashed?: boolean;
 | 
			
		||||
        },
 | 
			
		||||
    ) {
 | 
			
		||||
        const qb = this.createAncestorsQueryBuilder('category', 'treeClosure', entity);
 | 
			
		||||
        FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
 | 
			
		||||
        qb.orderBy('category.customOrder', 'ASC');
 | 
			
		||||
 | 
			
		||||
        if (options?.withTrashed) {
 | 
			
		||||
            qb.withDeleted();
 | 
			
		||||
            if (options?.onlyTrashed) qb.where(`category.deletedAt IS NOT NULL`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return qb.getMany();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -88,4 +124,42 @@ export class CategoryRepository extends TreeRepository<CategoryEntity> {
 | 
			
		||||
 | 
			
		||||
        return data as CategoryEntity[];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 统计后代元素数量
 | 
			
		||||
     * @param entity
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    async countDescendants(
 | 
			
		||||
        entity: CategoryEntity,
 | 
			
		||||
        options?: { withTrashed?: boolean; onlyTrashed?: boolean },
 | 
			
		||||
    ) {
 | 
			
		||||
        const qb = this.createDescendantsQueryBuilder('category', 'treeClosure', entity);
 | 
			
		||||
 | 
			
		||||
        if (options?.withTrashed) {
 | 
			
		||||
            qb.withDeleted();
 | 
			
		||||
            if (options?.onlyTrashed) qb.where(`category.deleteAt IS NOT NULL`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return qb.getCount();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 统计后代元素数量
 | 
			
		||||
     * @param entity
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    async countAncestors(
 | 
			
		||||
        entity: CategoryEntity,
 | 
			
		||||
        options?: { withTrashed?: boolean; onlyTrashed?: boolean },
 | 
			
		||||
    ) {
 | 
			
		||||
        const qb = this.createAncestorsQueryBuilder('category', 'treeClosure', entity);
 | 
			
		||||
 | 
			
		||||
        if (options?.withTrashed) {
 | 
			
		||||
            qb.withDeleted();
 | 
			
		||||
            if (options?.onlyTrashed) qb.where(`category.deleteAt IS NOT NULL`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return qb.getCount();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,17 @@
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
import { isNil, omit } from 'lodash';
 | 
			
		||||
import { EntityNotFoundError } from 'typeorm';
 | 
			
		||||
import { EntityNotFoundError, In } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import { CreateCategoryDto, QueryCategoryDto, UpdateCategoryDto } from '@/modules/content/dtos';
 | 
			
		||||
import {
 | 
			
		||||
    CreateCategoryDto,
 | 
			
		||||
    QueryCategoryDto,
 | 
			
		||||
    QueryCategoryTreeDto,
 | 
			
		||||
    UpdateCategoryDto,
 | 
			
		||||
} from '@/modules/content/dtos';
 | 
			
		||||
import { CategoryEntity } from '@/modules/content/entities';
 | 
			
		||||
import { CategoryRepository } from '@/modules/content/repositories';
 | 
			
		||||
import { SelectTrashMode } from '@/modules/database/constants';
 | 
			
		||||
import { treePaginate } from '@/modules/database/helpers';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -17,9 +23,15 @@ export class CategoryService {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询分类树
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    async findTress() {
 | 
			
		||||
        return this.repository.findTrees();
 | 
			
		||||
    async findTrees(options: QueryCategoryTreeDto) {
 | 
			
		||||
        const { trashed = SelectTrashMode.NONE } = options;
 | 
			
		||||
 | 
			
		||||
        return this.repository.findTrees({
 | 
			
		||||
            withTrashed: trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY,
 | 
			
		||||
            onlyTrashed: trashed === SelectTrashMode.ONLY,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -27,7 +39,12 @@ export class CategoryService {
 | 
			
		||||
     * @param options 分页选项
 | 
			
		||||
     */
 | 
			
		||||
    async paginate(options: QueryCategoryDto) {
 | 
			
		||||
        const tree = await this.repository.findTrees();
 | 
			
		||||
        const { trashed = SelectTrashMode.NONE } = options;
 | 
			
		||||
 | 
			
		||||
        const tree = await this.repository.findTrees({
 | 
			
		||||
            withTrashed: trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY,
 | 
			
		||||
            onlyTrashed: trashed === SelectTrashMode.ONLY,
 | 
			
		||||
        });
 | 
			
		||||
        const data = await this.repository.toFlatTrees(tree);
 | 
			
		||||
 | 
			
		||||
        return treePaginate(options, data);
 | 
			
		||||
@ -83,23 +100,36 @@ export class CategoryService {
 | 
			
		||||
     * 删除分类
 | 
			
		||||
     * @param id
 | 
			
		||||
     */
 | 
			
		||||
    async delete(id: string) {
 | 
			
		||||
        const item = await this.repository.findOneOrFail({
 | 
			
		||||
            where: { id },
 | 
			
		||||
    async delete(ids: string[], trash?: boolean) {
 | 
			
		||||
        const items = await this.repository.find({
 | 
			
		||||
            where: { id: In(ids) },
 | 
			
		||||
            withDeleted: true,
 | 
			
		||||
            relations: ['parent', 'children'],
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 把子分类提升一级
 | 
			
		||||
        if (!isNil(item.children) && item.children.length > 0) {
 | 
			
		||||
            const nchildren = [...item.children].map((c) => {
 | 
			
		||||
                c.parent = item.parent;
 | 
			
		||||
                return item;
 | 
			
		||||
            });
 | 
			
		||||
        for (const item of items) {
 | 
			
		||||
            if (!isNil(item.children) && item.children.length > 0) {
 | 
			
		||||
                const nchildren = [...item.children].map((c) => {
 | 
			
		||||
                    c.parent = item.parent;
 | 
			
		||||
                    return c;
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            await this.repository.save(nchildren, { reload: true });
 | 
			
		||||
                await this.repository.save(nchildren);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.repository.remove(item);
 | 
			
		||||
        if (trash) {
 | 
			
		||||
            const directs = items.filter((item) => !isNil(item.deletedAt));
 | 
			
		||||
            const sorts = items.filter((item) => isNil(item.deletedAt));
 | 
			
		||||
 | 
			
		||||
            return [
 | 
			
		||||
                ...(await this.repository.remove(directs)),
 | 
			
		||||
                ...(await this.repository.softRemove(sorts)),
 | 
			
		||||
            ];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.repository.remove(items);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -121,4 +151,22 @@ export class CategoryService {
 | 
			
		||||
        }
 | 
			
		||||
        return parent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 恢复分类
 | 
			
		||||
     * @param ids
 | 
			
		||||
     */
 | 
			
		||||
    async restore(ids: string[]) {
 | 
			
		||||
        const items = await this.repository.find({
 | 
			
		||||
            where: { id: In(ids) } as any,
 | 
			
		||||
            withDeleted: true,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const trasheds = items.filter((item) => !isNil(item)).map((item) => item.id);
 | 
			
		||||
        if (trasheds.length < 1) return [];
 | 
			
		||||
        await this.repository.restore(trasheds);
 | 
			
		||||
        const qb = this.repository.buildBaseQB();
 | 
			
		||||
        qb.andWhereInIds(trasheds);
 | 
			
		||||
        return qb.getMany();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import { ForbiddenException, Injectable } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
import { isNil } from 'lodash';
 | 
			
		||||
 | 
			
		||||
import { EntityNotFoundError, SelectQueryBuilder } from 'typeorm';
 | 
			
		||||
import { EntityNotFoundError, In, SelectQueryBuilder } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import { CreateCommentDto, QueryCommentDto, QueryCommentTreeDto } from '@/modules/content/dtos';
 | 
			
		||||
import { CommentEntity } from '@/modules/content/entities';
 | 
			
		||||
@ -82,11 +82,11 @@ export class CommentService {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 删除评论
 | 
			
		||||
     * @param id
 | 
			
		||||
     * @param ids
 | 
			
		||||
     */
 | 
			
		||||
    async delete(id: string) {
 | 
			
		||||
        const comment = await this.repository.findOneOrFail({ where: { id: id ?? null } });
 | 
			
		||||
        return this.repository.remove(comment);
 | 
			
		||||
    async delete(ids: string[]) {
 | 
			
		||||
        const comments = await this.repository.find({ where: { id: In(ids) } });
 | 
			
		||||
        return this.repository.remove(comments);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ import { PostEntity } from '@/modules/content/entities';
 | 
			
		||||
import { CategoryRepository, PostRepository, TagRepository } from '@/modules/content/repositories';
 | 
			
		||||
 | 
			
		||||
import { CategoryService } from '@/modules/content/services/category.service';
 | 
			
		||||
import { SelectTrashMode } from '@/modules/database/constants';
 | 
			
		||||
import { paginate } from '@/modules/database/helpers';
 | 
			
		||||
import { QueryHook } from '@/modules/database/types';
 | 
			
		||||
 | 
			
		||||
@ -108,9 +109,45 @@ export class PostService {
 | 
			
		||||
     * 删除文章
 | 
			
		||||
     * @param id
 | 
			
		||||
     */
 | 
			
		||||
    async delete(id: string) {
 | 
			
		||||
        const item = await this.repository.findOneByOrFail({ id });
 | 
			
		||||
        return this.repository.remove(item);
 | 
			
		||||
    async delete(ids: string[], trash?: boolean) {
 | 
			
		||||
        const items = await this.repository.find({
 | 
			
		||||
            where: { id: In(ids) } as any,
 | 
			
		||||
            withDeleted: true,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (trash) {
 | 
			
		||||
            // 对已软删除的数据再次删除时直接通过remove方法从数据库中清除
 | 
			
		||||
            const directs = items.filter((item) => !isNil(item.deletedAt));
 | 
			
		||||
            const softs = items.filter((item) => isNil(item.deletedAt));
 | 
			
		||||
 | 
			
		||||
            return [
 | 
			
		||||
                ...(await this.repository.remove(directs)),
 | 
			
		||||
                ...(await this.repository.softRemove(softs)),
 | 
			
		||||
            ];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.repository.remove(items);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 恢复文章
 | 
			
		||||
     * @param ids
 | 
			
		||||
     */
 | 
			
		||||
    async restore(ids: string[]) {
 | 
			
		||||
        const items = await this.repository.find({
 | 
			
		||||
            where: { id: In(ids) } as any,
 | 
			
		||||
            withDeleted: true,
 | 
			
		||||
        });
 | 
			
		||||
        // 过滤掉不在回收站中的数据
 | 
			
		||||
        const trasheds = items.filter((item) => !isNil(item)).map((item) => item.id);
 | 
			
		||||
 | 
			
		||||
        if (trasheds.length < 1) return [];
 | 
			
		||||
 | 
			
		||||
        await this.repository.restore(trasheds);
 | 
			
		||||
        const qb = await this.buildListQuery(this.repository.buildBaseQB(), {}, async (qbuilder) =>
 | 
			
		||||
            qbuilder.andWhereInIds(trasheds),
 | 
			
		||||
        );
 | 
			
		||||
        return qb.getMany();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -124,7 +161,12 @@ export class PostService {
 | 
			
		||||
        options: FindParams,
 | 
			
		||||
        callback?: QueryHook<PostEntity>,
 | 
			
		||||
    ) {
 | 
			
		||||
        const { category, tag, orderBy, isPublished } = options;
 | 
			
		||||
        const { category, tag, orderBy, isPublished, trashed = SelectTrashMode.NONE } = options;
 | 
			
		||||
 | 
			
		||||
        if (trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY) {
 | 
			
		||||
            qb.withDeleted();
 | 
			
		||||
            if (trashed === SelectTrashMode.ONLY) qb.where(`post.deletedAt is not null`);
 | 
			
		||||
        }
 | 
			
		||||
        if (typeof isPublished === 'boolean') {
 | 
			
		||||
            isPublished
 | 
			
		||||
                ? qb.where({
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,19 @@
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
import { omit } from 'lodash';
 | 
			
		||||
import { isNil, omit } from 'lodash';
 | 
			
		||||
 | 
			
		||||
import { In, SelectQueryBuilder } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import { CreateTagDto, QueryTagsDto, UpdateTagDto } from '@/modules/content/dtos';
 | 
			
		||||
import { TagEntity } from '@/modules/content/entities';
 | 
			
		||||
import { TagRepository } from '@/modules/content/repositories';
 | 
			
		||||
import { SelectTrashMode } from '@/modules/database/constants';
 | 
			
		||||
import { paginate } from '@/modules/database/helpers';
 | 
			
		||||
import { QueryHook } from '@/modules/database/types';
 | 
			
		||||
 | 
			
		||||
type FindParams = {
 | 
			
		||||
    [key in keyof Omit<QueryTagsDto, 'limit' | 'page'>]: QueryTagsDto[key];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 标签数据操作
 | 
			
		||||
@ -19,7 +28,7 @@ export class TagService {
 | 
			
		||||
     * @param callback 添加额外的查询
 | 
			
		||||
     */
 | 
			
		||||
    async paginate(options: QueryTagsDto) {
 | 
			
		||||
        const qb = this.repository.buildBaseQB();
 | 
			
		||||
        const qb = await this.buildListQuery(this.repository.buildBaseQB(), options);
 | 
			
		||||
        return paginate(qb, options);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -54,10 +63,64 @@ export class TagService {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 删除标签
 | 
			
		||||
     * @param id
 | 
			
		||||
     * @param ids
 | 
			
		||||
     */
 | 
			
		||||
    async delete(id: string) {
 | 
			
		||||
        const item = await this.repository.findOneByOrFail({ id });
 | 
			
		||||
        return this.repository.remove(item);
 | 
			
		||||
    async delete(ids: string[], trash: boolean) {
 | 
			
		||||
        const items = await this.repository.find({
 | 
			
		||||
            where: { id: In(ids) } as any,
 | 
			
		||||
            withDeleted: true,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (trash) {
 | 
			
		||||
            const directs = items.filter((item) => !isNil(item.deletedAt));
 | 
			
		||||
            const sorts = items.filter((item) => isNil(item.deletedAt));
 | 
			
		||||
 | 
			
		||||
            return [
 | 
			
		||||
                ...(await this.repository.remove(directs)),
 | 
			
		||||
                ...(await this.repository.softRemove(sorts)),
 | 
			
		||||
            ];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.repository.remove(items);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 软删除标签
 | 
			
		||||
     * @param ids
 | 
			
		||||
     */
 | 
			
		||||
    async restore(ids: string[]) {
 | 
			
		||||
        const items = await this.repository.find({
 | 
			
		||||
            where: { id: In(ids) } as any,
 | 
			
		||||
            withDeleted: true,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 过滤掉不在回收站的标签
 | 
			
		||||
        const trashed = items.filter((item) => !isNil(item.deletedAt)).map((item) => item.id);
 | 
			
		||||
        if (trashed.length < 1) return trashed;
 | 
			
		||||
        await this.repository.restore(trashed);
 | 
			
		||||
        const qb = this.repository.buildBaseQB().where({ id: In(trashed) });
 | 
			
		||||
        return qb.getMany();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 构建标签列表查询器(需要查到软删除)
 | 
			
		||||
     * @param qb
 | 
			
		||||
     * @param options
 | 
			
		||||
     * @param callback
 | 
			
		||||
     */
 | 
			
		||||
    protected async buildListQuery(
 | 
			
		||||
        qb: SelectQueryBuilder<TagEntity>,
 | 
			
		||||
        options: FindParams,
 | 
			
		||||
        callback?: QueryHook<TagEntity>,
 | 
			
		||||
    ) {
 | 
			
		||||
        const { trashed } = options;
 | 
			
		||||
 | 
			
		||||
        if (trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY) {
 | 
			
		||||
            qb.withDeleted();
 | 
			
		||||
            if (trashed === SelectTrashMode.ONLY) qb.where(`tag.deletedAt IS NOT NULL`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (callback) return callback(qb);
 | 
			
		||||
        return qb;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,3 +2,12 @@
 | 
			
		||||
 * 自定义 Repository 元数据
 | 
			
		||||
 */
 | 
			
		||||
export const CUSTOM_REPOSITORY_METADATA = 'CUSTOM_REPOSITORY_METADATA';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 软删除数据查询类型
 | 
			
		||||
 */
 | 
			
		||||
export enum SelectTrashMode {
 | 
			
		||||
    ALL = 'all',
 | 
			
		||||
    ONLY = 'only',
 | 
			
		||||
    NONE = 'none',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										18
									
								
								src/modules/restful/dtos/delete-with-trash.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/modules/restful/dtos/delete-with-trash.dto.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
import { Transform } from 'class-transformer';
 | 
			
		||||
 | 
			
		||||
import { IsBoolean, IsOptional } from 'class-validator';
 | 
			
		||||
 | 
			
		||||
import { DtoValidation } from '@/modules/core/decorators';
 | 
			
		||||
import { toBoolean } from '@/modules/core/helpers';
 | 
			
		||||
import { DeleteDto } from '@/modules/restful/dtos/delete.dto';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 带软删除的批量删除验证
 | 
			
		||||
 */
 | 
			
		||||
@DtoValidation()
 | 
			
		||||
export class DeleteWithTrashDto extends DeleteDto {
 | 
			
		||||
    @Transform(({ value }) => toBoolean(value))
 | 
			
		||||
    @IsBoolean()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    trash?: boolean;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								src/modules/restful/dtos/delete.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/modules/restful/dtos/delete.dto.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
import { IsDefined, IsUUID } from 'class-validator';
 | 
			
		||||
 | 
			
		||||
import { DtoValidation } from '@/modules/core/decorators';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 批量删除验证
 | 
			
		||||
 */
 | 
			
		||||
@DtoValidation()
 | 
			
		||||
export class DeleteDto {
 | 
			
		||||
    @IsUUID(undefined, {
 | 
			
		||||
        each: true,
 | 
			
		||||
        message: 'ID格式错误',
 | 
			
		||||
    })
 | 
			
		||||
    @IsDefined({
 | 
			
		||||
        each: true,
 | 
			
		||||
        message: 'ID必须指定',
 | 
			
		||||
    })
 | 
			
		||||
    ids: string[] = [];
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								src/modules/restful/dtos/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/modules/restful/dtos/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
export * from './delete-with-trash.dto';
 | 
			
		||||
export * from './delete.dto';
 | 
			
		||||
export * from './restore.dto';
 | 
			
		||||
							
								
								
									
										19
									
								
								src/modules/restful/dtos/restore.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/modules/restful/dtos/restore.dto.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
import { IsDefined, IsUUID } from 'class-validator';
 | 
			
		||||
 | 
			
		||||
import { DtoValidation } from '@/modules/core/decorators';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 批量恢复验证
 | 
			
		||||
 */
 | 
			
		||||
@DtoValidation()
 | 
			
		||||
export class RestoreDto {
 | 
			
		||||
    @IsUUID(undefined, {
 | 
			
		||||
        each: true,
 | 
			
		||||
        message: 'ID格式错误',
 | 
			
		||||
    })
 | 
			
		||||
    @IsDefined({
 | 
			
		||||
        each: true,
 | 
			
		||||
        message: 'ID必须指定',
 | 
			
		||||
    })
 | 
			
		||||
    ids: string[] = [];
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user