Bladeren bron

feat: 咨询分类接口

IlhamTahir 1 jaar geleden
bovenliggende
commit
3283d94ea4

+ 4 - 0
.env.example

@@ -8,3 +8,7 @@ DATACENTER_ID=1
 APP_TITLE='API Service'
 JWT_SECRET=secret
 JWT_EXPIRES_IN=1h
+WX_APPID=wx123456
+WX_SECRET=secret
+WX_TOKEN=token
+WX_AESKEY=aeskey

+ 2 - 1
src/app.module.ts

@@ -1,8 +1,9 @@
 import { Module } from '@nestjs/common';
 import { CoreModule } from './core/core.module';
+import { ArticleModule } from './article/article.module';
 
 @Module({
-  imports: [CoreModule],
+  imports: [CoreModule, ArticleModule],
   controllers: [],
   providers: [],
 })

+ 12 - 0
src/article/article.module.ts

@@ -0,0 +1,12 @@
+import { Module } from '@nestjs/common';
+import { CategoryController } from './controller/category.controller';
+import { CategoryService } from './service/category.service';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { Category } from './entity/category.entity';
+
+@Module({
+  controllers: [CategoryController],
+  imports: [TypeOrmModule.forFeature([Category])],
+  providers: [CategoryService],
+})
+export class ArticleModule {}

+ 97 - 0
src/article/controller/category.controller.ts

@@ -0,0 +1,97 @@
+import {
+  Body,
+  Controller,
+  Delete,
+  Get,
+  Param,
+  Post,
+  Put,
+  Query,
+} from '@nestjs/common';
+import { CategoryService } from '../service/category.service';
+import { CreateCategoryRequest } from '../dto/create-category.request';
+import { NoAuth } from '../../core/decorators/no-auth.decorator';
+import { ApiBearerAuth, ApiOkResponse, getSchemaPath } from '@nestjs/swagger';
+import { CategoryVo } from '../vo/category.vo';
+import { CategoryMapper } from '../mapper/category.mapper';
+import { SearchCategoryFilter } from '../dto/search-category.filter';
+import { PageResultMapper } from '../../core/mapper/page-result.mapper';
+import { PageResult } from '../../core/vo/page-result';
+import { UpdateCategoryRequest } from '../dto/update-category.request';
+
+@Controller('categories')
+export class CategoryController {
+  constructor(private readonly categoryService: CategoryService) {}
+
+  @Post()
+  @ApiOkResponse({
+    description: 'Category',
+    type: CategoryVo,
+  })
+  @ApiBearerAuth()
+  async create(
+    @Body() createCategoryRequest: CreateCategoryRequest,
+  ): Promise<CategoryVo> {
+    return CategoryMapper.toVo(
+      await this.categoryService.create(createCategoryRequest),
+    );
+  }
+
+  @Get()
+  @ApiOkResponse({
+    description: '分类分页列表',
+    schema: {
+      allOf: [
+        { $ref: getSchemaPath(PageResult) }, // 引用 PageResult 模型
+        {
+          properties: {
+            data: {
+              type: 'array',
+              items: { $ref: getSchemaPath(CategoryVo) }, // 泛型内容具体化
+            },
+          },
+        },
+      ],
+    },
+  })
+  @ApiBearerAuth()
+  async search(@Query() searchCategoryFilter: SearchCategoryFilter) {
+    const [data, total] =
+      await this.categoryService.search(searchCategoryFilter);
+    return PageResultMapper.toPageResult<CategoryVo>(
+      CategoryMapper.toVos(data),
+      searchCategoryFilter.getPage(),
+      searchCategoryFilter.getSize(),
+      total,
+    );
+  }
+
+  @Get(':id')
+  @ApiBearerAuth()
+  @ApiOkResponse({
+    type: CategoryVo,
+  })
+  async get(@Param('id') id: string) {
+    return CategoryMapper.toVo(await this.categoryService.get(id));
+  }
+
+  @Put(':id')
+  @ApiBearerAuth()
+  @ApiOkResponse({
+    type: CategoryVo,
+  })
+  async update(
+    @Param('id') id: string,
+    @Body() updateCategoryRequest: UpdateCategoryRequest,
+  ) {
+    return CategoryMapper.toVo(
+      await this.categoryService.update(id, updateCategoryRequest),
+    );
+  }
+
+  @Delete()
+  @ApiBearerAuth()
+  async delete(@Param('id') id: string) {
+    await this.categoryService.delete(id);
+  }
+}

+ 36 - 0
src/article/dto/create-category.request.ts

@@ -0,0 +1,36 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsAlpha, IsNotEmpty, Length, Min, MinLength } from 'class-validator';
+
+export class CreateCategoryRequest {
+  @ApiProperty({
+    example: '科学',
+  })
+  @IsNotEmpty({
+    message: '分类名称不能为空',
+  })
+  @Length(2, 5, {
+    message: '分类名称长度必须在2到5之间',
+  })
+  name: string;
+
+  @ApiProperty({
+    example: 'science',
+  })
+  @IsNotEmpty({
+    message: '分类编码不能为空',
+  })
+  @IsAlpha('en-US', {
+    message: '分类编码只能是英文字母',
+  })
+  @MinLength(4, {
+    message: '分类编码长度需要大于4个字符',
+  })
+  code: string;
+  @ApiProperty({
+    example: 1,
+  })
+  @Min(0, {
+    message: '排序值不能小于0',
+  })
+  order: number = 0;
+}

+ 7 - 0
src/article/dto/search-category.filter.ts

@@ -0,0 +1,7 @@
+import { BaseFilter } from '../../core/dto/base.filter';
+import { ApiProperty } from '@nestjs/swagger';
+
+export class SearchCategoryFilter extends BaseFilter {
+  @ApiProperty()
+  order = ['+order', '-createdTime'];
+}

+ 3 - 0
src/article/dto/update-category.request.ts

@@ -0,0 +1,3 @@
+import { CreateCategoryRequest } from './create-category.request';
+
+export class UpdateCategoryRequest extends CreateCategoryRequest {}

+ 16 - 0
src/article/entity/category.entity.ts

@@ -0,0 +1,16 @@
+import { TraceableEntity } from '../../core/entity/traceable.entity';
+import { Column, Entity } from 'typeorm';
+
+@Entity()
+export class Category extends TraceableEntity {
+  @Column()
+  name: string;
+
+  @Column({
+    unique: true,
+  })
+  code: string;
+
+  @Column()
+  order: number = 0;
+}

+ 8 - 0
src/article/error/category.error.ts

@@ -0,0 +1,8 @@
+import { ErrorResponse } from '../../core/errors/error.response';
+
+export const CategoryError: Record<string, ErrorResponse> = {
+  NOT_FOUND: {
+    code: 3001,
+    message: 'Category not found',
+  },
+};

+ 22 - 0
src/article/mapper/category.mapper.ts

@@ -0,0 +1,22 @@
+import { Category } from '../entity/category.entity';
+import { CategoryVo } from '../vo/category.vo';
+import { DateUtil } from '../../core/util/date.util';
+import { UserMapper } from '../../core/mapper/user.mapper';
+
+export class CategoryMapper {
+  static toVo(entity: Category): CategoryVo {
+    return {
+      createBy: UserMapper.toVo(entity.createBy),
+      updateBy: UserMapper.toVo(entity.updateBy),
+      id: entity.id,
+      name: entity.name,
+      code: entity.code,
+      order: entity.order,
+      createdTime: DateUtil.format(entity.createdTime),
+      updatedTime: DateUtil.format(entity.updatedTime),
+    };
+  }
+  static toVos(entities: Category[]): CategoryVo[] {
+    return entities.map((entity) => this.toVo(entity));
+  }
+}

+ 67 - 0
src/article/service/category.service.ts

@@ -0,0 +1,67 @@
+import { Injectable } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Category } from '../entity/category.entity';
+import { Repository } from 'typeorm';
+import { CreateCategoryRequest } from '../dto/create-category.request';
+import { SearchCategoryFilter } from '../dto/search-category.filter';
+import { BizException } from '../../core/exceptions/biz.exception';
+import { CategoryError } from '../error/category.error';
+import { UpdateCategoryRequest } from '../dto/update-category.request';
+import { UserService } from '../../core/service/user.service';
+
+@Injectable()
+export class CategoryService {
+  constructor(
+    @InjectRepository(Category)
+    private readonly categoryRepository: Repository<Category>,
+    private readonly userService: UserService,
+  ) {}
+
+  async create(createCategoryRequest: CreateCategoryRequest) {
+    const category = new Category();
+    category.name = createCategoryRequest.name;
+    category.code = createCategoryRequest.code;
+    category.order = createCategoryRequest.order;
+    category.createBy = this.userService.getCurrentUser();
+    category.updateBy = this.userService.getCurrentUser();
+
+    return this.categoryRepository.save(category);
+  }
+
+  async search(searchCategoryFilter: SearchCategoryFilter) {
+    return this.categoryRepository.findAndCount({
+      where: searchCategoryFilter.getConditions(),
+      skip: searchCategoryFilter.getSkip(),
+      take: searchCategoryFilter.getSize(),
+      order: searchCategoryFilter.getOrderBy(),
+    });
+  }
+
+  async get(id: string) {
+    const category = await this.categoryRepository.findOneBy({ id });
+    if (!category) {
+      throw new BizException(CategoryError.NOT_FOUND);
+    }
+    return category;
+  }
+
+  async update(id: string, updateCategoryRequest: UpdateCategoryRequest) {
+    const category = await this.categoryRepository.findOneBy({ id });
+    if (!category) {
+      throw new BizException(CategoryError.NOT_FOUND);
+    }
+    category.name = updateCategoryRequest.name;
+    category.code = updateCategoryRequest.code;
+    category.order = updateCategoryRequest.order;
+    category.updateBy = this.userService.getCurrentUser();
+    return this.categoryRepository.save(category);
+  }
+
+  async delete(id: string) {
+    const category = await this.categoryRepository.findOneBy({ id });
+    if (!category) {
+      throw new BizException(CategoryError.NOT_FOUND);
+    }
+    await this.categoryRepository.remove(category);
+  }
+}

+ 14 - 0
src/article/vo/category.vo.ts

@@ -0,0 +1,14 @@
+import { TraceableVo } from '../../core/vo/traceable.vo';
+import { ApiProperty, ApiSchema } from '@nestjs/swagger';
+
+@ApiSchema({
+  name: 'Category',
+})
+export class CategoryVo extends TraceableVo {
+  @ApiProperty()
+  name: string;
+  @ApiProperty()
+  code: string;
+  @ApiProperty()
+  order: number;
+}

+ 2 - 1
src/core/controller/role.controller.ts

@@ -1,7 +1,7 @@
 import { Body, Controller, HttpStatus, Post } from '@nestjs/common';
 import { RoleService } from '../service/role.service';
 import { CreateRoleRequest } from '../dto/create-role.request';
-import { ApiResponse } from '@nestjs/swagger';
+import { ApiBearerAuth, ApiResponse } from '@nestjs/swagger';
 import { RoleVo } from '../vo/role.vo';
 import { RoleMapper } from '../mapper/role.mapper';
 
@@ -14,6 +14,7 @@ export class RoleController {
     description: 'Role',
     type: RoleVo,
   })
+  @ApiBearerAuth()
   @Post()
   async create(@Body() createRoleRequest: CreateRoleRequest) {
     return RoleMapper.toVo(await this.roleService.create(createRoleRequest));

+ 4 - 3
src/core/controller/user.controller.ts

@@ -1,25 +1,26 @@
 import { Controller, Get, HttpStatus, Query } from '@nestjs/common';
 import { UserService } from '../service/user.service';
-import { SearchUserFilter } from '../dto/serach-user.filter';
+import { SearchUserFilter } from '../dto/search-user.filter';
 import { NoAuth } from '../decorators/no-auth.decorator';
 import { PageResultMapper } from '../mapper/page-result.mapper';
 import { UserVo } from '../vo/user.vo';
 import { UserMapper } from '../mapper/user.mapper';
 import {
+  ApiBearerAuth,
   ApiExtraModels,
   ApiOkResponse,
-  ApiResponse,
   getSchemaPath,
 } from '@nestjs/swagger';
 import { PageResult } from '../vo/page-result';
 
-@Controller('user')
+@Controller('users')
 export class UserController {
   constructor(private readonly userService: UserService) {}
 
   @Get()
   @NoAuth()
   @ApiExtraModels(PageResult, UserVo) // 注册额外模型
+  @ApiBearerAuth()
   @ApiOkResponse({
     description: '分页用户列表',
     schema: {

+ 1 - 0
src/core/core.module.ts

@@ -36,6 +36,7 @@ import { UserController } from './controller/user.controller';
         database: configService.get<string>('DB_NAME'),
         autoLoadEntities: true,
         entities: [__dirname + '/**/*.entity{.ts,.js}'],
+        subscribers: [__dirname + '/**/*.subscriber{.ts,.js}'],
         synchronize: ['development', 'test'].includes(process.env.NODE_ENV),
         logging: true,
       }),

+ 16 - 1
src/core/dto/base.filter.ts

@@ -40,5 +40,20 @@ export abstract class BaseFilter {
   }
 
   // 获取查询条件(由子类实现)
-  abstract getConditions(): Record<string, any>;
+  getConditions(): Record<string, any> {
+    return {};
+  }
+
+  // 解析排序参数
+  getOrderBy(): Record<string, 'ASC' | 'DESC'> {
+    const orderConditions: Record<string, 'ASC' | 'DESC'> = {};
+    if (this.order) {
+      this.order.forEach((condition) => {
+        const direction = condition[0] === '+' ? 'ASC' : 'DESC';
+        const field = condition.substring(1); // 移除第一个字符 (+ 或 -)
+        orderConditions[field] = direction;
+      });
+    }
+    return orderConditions;
+  }
 }

+ 0 - 0
src/core/dto/serach-user.filter.ts → src/core/dto/search-user.filter.ts


+ 2 - 2
src/core/entity/traceable.entity.ts

@@ -3,9 +3,9 @@ import { ManyToOne } from 'typeorm';
 import { User } from './user.entity';
 
 export abstract class TraceableEntity extends BaseEntity {
-  @ManyToOne(() => User)
+  @ManyToOne(() => User, { eager: true })
   createBy: User;
 
-  @ManyToOne(() => User)
+  @ManyToOne(() => User, { eager: true })
   updateBy: User;
 }

+ 2 - 1
src/core/exceptions/global-exception.filter.ts

@@ -44,7 +44,8 @@ export class GlobalExceptionFilter implements ExceptionFilter {
     const message =
       typeof exceptionResponse === 'string'
         ? exceptionResponse
-        : (exceptionResponse as any).message || 'An unexpected error occurred.';
+        : (exceptionResponse as ErrorResponse).message ||
+          'An unexpected error occurred.';
 
     return {
       status,

+ 2 - 1
src/core/guards/auth.guard.ts

@@ -45,6 +45,7 @@ export class AuthGuard implements CanActivate {
       }
 
       // 将用户信息附加到请求对象
+      this.userService.setCurrentUser(user);
       request.user = user;
       return true;
     } catch (error) {
@@ -52,7 +53,7 @@ export class AuthGuard implements CanActivate {
     }
   }
   private extractTokenFromHeader(request: any): string | null {
-    const authHeader = request.headers['Authorization'];
+    const authHeader = request.headers['authorization'];
     if (!authHeader || !authHeader.startsWith('Bearer ')) {
       return null;
     }

+ 2 - 2
src/core/mapper/page-result.mapper.ts

@@ -10,8 +10,8 @@ export class PageResultMapper {
     return {
       data: data,
       pagination: {
-        page,
-        size,
+        page: Number(page),
+        size: Number(size),
         total,
       },
     };

+ 2 - 1
src/core/mapper/user.mapper.ts

@@ -3,7 +3,8 @@ import { UserVo } from '../vo/user.vo';
 import { DateUtil } from '../util/date.util';
 
 export class UserMapper {
-  static toVo(entity: User): UserVo {
+  static toVo(entity?: User): UserVo {
+    if (!entity) return null;
     return {
       id: entity.id,
       username: entity.username,

+ 17 - 1
src/core/service/user.service.ts

@@ -3,7 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
 import { User } from '../entity/user.entity';
 import { Repository } from 'typeorm';
 import * as bcrypt from 'bcrypt';
-import { SearchUserFilter } from '../dto/serach-user.filter';
+import { SearchUserFilter } from '../dto/search-user.filter';
+import { JwtPayload } from 'jsonwebtoken';
 
 @Injectable()
 export class UserService {
@@ -11,6 +12,21 @@ export class UserService {
     @InjectRepository(User) private readonly userRepository: Repository<User>,
   ) {}
 
+  private currentUser: User;
+
+  setCurrentUser(user: User) {
+    this.currentUser = user;
+  }
+
+  getCurrentUser() {
+    return this.currentUser;
+  }
+
+  async setCurrentUserByJwtPayload(payload: JwtPayload) {
+    const user = await this.userRepository.findOneBy({ id: payload.sub });
+    this.setCurrentUser(user);
+  }
+
   async findById(id: string) {
     return this.userRepository.findOne({
       where: {

+ 4 - 1
src/core/vo/role.vo.ts

@@ -1,6 +1,9 @@
 import { BaseVo } from './base.vo';
-import { ApiProperty } from '@nestjs/swagger';
+import { ApiProperty, ApiSchema } from '@nestjs/swagger';
 
+@ApiSchema({
+  name: 'Role',
+})
 export class RoleVo extends BaseVo {
   @ApiProperty()
   name: string;

+ 11 - 0
src/core/vo/traceable.vo.ts

@@ -0,0 +1,11 @@
+import { BaseVo } from './base.vo';
+import { UserVo } from './user.vo';
+import { ApiProperty } from '@nestjs/swagger';
+
+export class TraceableVo extends BaseVo {
+  @ApiProperty()
+  createBy: UserVo | null;
+
+  @ApiProperty()
+  updateBy: UserVo | null;
+}

+ 1 - 2
src/main.ts

@@ -2,7 +2,6 @@ import { NestFactory } from '@nestjs/core';
 import { AppModule } from './app.module';
 import { SnowflakeUtil } from './core/util/snowflake.util';
 import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
-import { ValidationPipe } from '@nestjs/common';
 import { GlobalExceptionFilter } from './core/exceptions/global-exception.filter';
 import { GlobalValidationPipe } from './core/pipe/global-validation.pipe';
 
@@ -17,7 +16,7 @@ async function bootstrap() {
       .setTitle(process.env.APP_TITLE)
       .setDescription('The api description')
       .setVersion('1.0')
-
+      .addBearerAuth()
       .build();
     const document = SwaggerModule.createDocument(app, config);
     SwaggerModule.setup('docs', app, document, {