Quellcode durchsuchen

feat: user 分页接口

IlhamTahir vor 1 Jahr
Ursprung
Commit
11b41a1a3e

+ 1 - 0
package.json

@@ -30,6 +30,7 @@
     "bcrypt": "^5.1.1",
     "class-transformer": "^0.5.1",
     "class-validator": "^0.14.1",
+    "dayjs": "^1.11.13",
     "jsonwebtoken": "^9.0.2",
     "mysql2": "^3.11.4",
     "nest-wechat": "^0.2.50",

+ 3 - 0
pnpm-lock.yaml

@@ -38,6 +38,9 @@ importers:
       class-validator:
         specifier: ^0.14.1
         version: 0.14.1
+      dayjs:
+        specifier: ^1.11.13
+        version: 1.11.13
       jsonwebtoken:
         specifier: ^9.0.2
         version: 9.0.2

+ 46 - 2
src/core/controller/user.controller.ts

@@ -1,4 +1,48 @@
-import { Controller } from '@nestjs/common';
+import { Controller, Get, HttpStatus, Query } from '@nestjs/common';
+import { UserService } from '../service/user.service';
+import { SearchUserFilter } from '../dto/serach-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 {
+  ApiExtraModels,
+  ApiOkResponse,
+  ApiResponse,
+  getSchemaPath,
+} from '@nestjs/swagger';
+import { PageResult } from '../vo/page-result';
 
 @Controller('user')
-export class UserController {}
+export class UserController {
+  constructor(private readonly userService: UserService) {}
+
+  @Get()
+  @NoAuth()
+  @ApiExtraModels(PageResult, UserVo) // 注册额外模型
+  @ApiOkResponse({
+    description: '分页用户列表',
+    schema: {
+      allOf: [
+        { $ref: getSchemaPath(PageResult) }, // 引用 PageResult 模型
+        {
+          properties: {
+            data: {
+              type: 'array',
+              items: { $ref: getSchemaPath(UserVo) }, // 泛型内容具体化
+            },
+          },
+        },
+      ],
+    },
+  })
+  async search(@Query() searchUserFilter: SearchUserFilter) {
+    const [data, total] = await this.userService.search(searchUserFilter);
+    return PageResultMapper.toPageResult<UserVo>(
+      UserMapper.toVos(data),
+      searchUserFilter.getPage(),
+      searchUserFilter.getSize(),
+      total,
+    );
+  }
+}

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

@@ -13,9 +13,10 @@ import { RoleController } from './controller/role.controller';
 import { RoleService } from './service/role.service';
 import { Role } from './entity/role.entity';
 import { Permission } from './entity/permission.entity';
+import { UserController } from './controller/user.controller';
 
 @Module({
-  controllers: [TokenController, RoleController],
+  controllers: [TokenController, RoleController, UserController],
   imports: [
     ConfigModule.forRoot({
       isGlobal: true,

+ 44 - 0
src/core/dto/base.filter.ts

@@ -0,0 +1,44 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsOptional, IsArray, Matches } from 'class-validator';
+
+export abstract class BaseFilter {
+  @ApiProperty({ required: false, description: '分页页码', example: 1 })
+  @IsOptional()
+  page?: number = 1;
+
+  @ApiProperty({ required: false, description: '每页数量', example: 10 })
+  @IsOptional()
+  size?: number = 10;
+
+  @ApiProperty({
+    required: false,
+    description: '排序字段和顺序,支持多个条件,例如 order[0]=+createdTime',
+    isArray: true,
+    example: ['+createdTime', '-username'],
+  })
+  @IsOptional()
+  @IsArray()
+  @Matches(/^[+-][a-zA-Z0-9_]+$/, {
+    each: true,
+    message: '排序条件必须以 "+" 或 "-" 开头,后跟字段名',
+  })
+  order?: string[];
+
+  // 获取分页起始位置
+  getSkip(): number {
+    return (this.page - 1) * this.size;
+  }
+
+  // 获取分页大小
+  getSize(): number {
+    return this.size;
+  }
+
+  // 获取页码
+  getPage(): number {
+    return this.page;
+  }
+
+  // 获取查询条件(由子类实现)
+  abstract getConditions(): Record<string, any>;
+}

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

@@ -0,0 +1,18 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsOptional, IsString } from 'class-validator';
+import { BaseFilter } from './base.filter';
+
+export class SearchUserFilter extends BaseFilter {
+  @ApiProperty({ required: false, description: '用户名搜索关键字' })
+  @IsOptional()
+  @IsString()
+  username?: string;
+
+  getConditions(): Record<string, any> {
+    const conditions: Record<string, any> = {};
+    if (this.username) {
+      conditions.username = `%${this.username}%`; // 假设使用模糊查询
+    }
+    return conditions;
+  }
+}

+ 19 - 0
src/core/mapper/page-result.mapper.ts

@@ -0,0 +1,19 @@
+import { PageResult } from '../vo/page-result';
+
+export class PageResultMapper {
+  static toPageResult<T>(
+    data: T[],
+    page: number,
+    size: number,
+    total: number,
+  ): PageResult<T> {
+    return {
+      data: data,
+      pagination: {
+        page,
+        size,
+        total,
+      },
+    };
+  }
+}

+ 19 - 0
src/core/mapper/user.mapper.ts

@@ -0,0 +1,19 @@
+import { User } from '../entity/user.entity';
+import { UserVo } from '../vo/user.vo';
+import { DateUtil } from '../util/date.util';
+
+export class UserMapper {
+  static toVo(entity: User): UserVo {
+    return {
+      id: entity.id,
+      username: entity.username,
+      locked: entity.locked,
+      enabled: entity.enabled,
+      createdTime: DateUtil.format(entity.createdTime),
+      updatedTime: DateUtil.format(entity.updatedTime),
+    };
+  }
+  static toVos(entities: User[]): UserVo[] {
+    return entities.map((entity) => this.toVo(entity));
+  }
+}

+ 10 - 0
src/core/service/user.service.ts

@@ -3,12 +3,14 @@ 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';
 
 @Injectable()
 export class UserService {
   constructor(
     @InjectRepository(User) private readonly userRepository: Repository<User>,
   ) {}
+
   async findById(id: string) {
     return this.userRepository.findOne({
       where: {
@@ -50,4 +52,12 @@ export class UserService {
       console.log('Error creating initial user', error);
     }
   }
+
+  async search(searchUserFilter: SearchUserFilter) {
+    return this.userRepository.findAndCount({
+      where: searchUserFilter.getConditions(),
+      skip: searchUserFilter.getSkip(),
+      take: searchUserFilter.getSize(),
+    });
+  }
 }

+ 17 - 0
src/core/util/date.util.ts

@@ -0,0 +1,17 @@
+import * as dayjs from 'dayjs';
+
+export class DateUtil {
+  private static defaultFormat: string = 'YYYY-MM-DD HH:mm:ss';
+
+  static toISOString(date: Date): string {
+    return dayjs(date).toISOString();
+  }
+
+  static format(date: Date, format: string = this.defaultFormat): string {
+    return dayjs(date).format(format);
+  }
+
+  static setDefaultFormat(format: string): void {
+    this.defaultFormat = format;
+  }
+}

+ 5 - 2
src/core/vo/base.vo.ts

@@ -1,7 +1,10 @@
+import { ApiProperty } from '@nestjs/swagger';
+
 export abstract class BaseVo {
+  @ApiProperty()
   id: string;
-
+  @ApiProperty()
   createdTime: string;
-
+  @ApiProperty()
   updatedTime: string;
 }

+ 21 - 0
src/core/vo/page-result.ts

@@ -0,0 +1,21 @@
+import { ApiExtraModels, ApiProperty } from '@nestjs/swagger';
+@ApiExtraModels()
+export class PageResult<T> {
+  @ApiProperty({ isArray: true, description: '分页数据' })
+  data: T[];
+
+  @ApiProperty({
+    description: '分页信息',
+    type: 'object',
+    properties: {
+      page: { type: 'number', example: 1 },
+      size: { type: 'number', example: 10 },
+      total: { type: 'number', example: 100 },
+    },
+  })
+  pagination: {
+    page: number;
+    size: number;
+    total: number;
+  };
+}

+ 14 - 0
src/core/vo/user.vo.ts

@@ -0,0 +1,14 @@
+import { BaseVo } from './base.vo';
+import { ApiProperty, ApiSchema } from '@nestjs/swagger';
+
+@ApiSchema({
+  name: 'User',
+})
+export class UserVo extends BaseVo {
+  @ApiProperty()
+  username: string;
+  @ApiProperty()
+  locked: boolean;
+  @ApiProperty()
+  enabled: boolean;
+}