From e6686c99958c3ceeb1b3678ac23707bcc707f1e5 Mon Sep 17 00:00:00 2001 From: zzswang Date: Mon, 12 May 2025 19:26:25 +0800 Subject: [PATCH 1/6] fix: user aggregate api --- openapi.json | 890 +++++++++++++++++---- src/common/entities/count.entity.ts | 3 + src/common/index.ts | 1 + src/user/dto/aggregate.dto.ts | 23 + src/user/entities/user.aggregate.entity.ts | 54 ++ src/user/user.controller.ts | 31 +- src/user/user.service.ts | 48 +- 7 files changed, 908 insertions(+), 142 deletions(-) create mode 100644 src/common/entities/count.entity.ts create mode 100644 src/user/dto/aggregate.dto.ts create mode 100644 src/user/entities/user.aggregate.entity.ts diff --git a/openapi.json b/openapi.json index 51f3a03..db4156a 100644 --- a/openapi.json +++ b/openapi.json @@ -1,5 +1,5 @@ { - "hash": "af79bc2a57d52a28a84989f154b3acd055e336d3bde702136b50bb0e89d68944", + "hash": "82fcbe018c9fd79aab12e521511bcb7a5b394a2dc70da6415a711d446fba8ea0", "openapi": "3.0.0", "paths": { "/hello": { @@ -2880,6 +2880,256 @@ ] } }, + "/users/@countUsers": { + "post": { + "operationId": "countUsers", + "summary": "", + "description": "Count users", + "parameters": [ + { + "name": "_sort", + "required": false, + "in": "query", + "description": "排序参数", + "schema": { + "enum": [ + "createdAt", + "-createdAt", + "updatedAt", + "-updatedAt", + "lastLoginAt", + "-lastLoginAt", + "expireAt", + "-expireAt" + ], + "type": "string" + } + }, + { + "name": "id", + "required": false, + "in": "query", + "description": "按 id 筛选", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "name_like", + "required": false, + "in": "query", + "description": "名称 模糊查询", + "schema": { + "type": "string" + } + }, + { + "name": "username_like", + "required": false, + "in": "query", + "description": "用户名 模糊查询", + "schema": { + "type": "string" + } + }, + { + "name": "nickname_like", + "required": false, + "in": "query", + "description": "昵称 模糊查询", + "schema": { + "type": "string" + } + }, + { + "name": "ns_tree", + "required": false, + "in": "query", + "description": "所属命名空间的 tree 查询", + "schema": { + "type": "string" + } + }, + { + "name": "expireAt_gte", + "required": false, + "in": "query", + "description": "过期时间大于该时间", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "expireAt_lte", + "required": false, + "in": "query", + "description": "过期时间小于该时间", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "active", + "required": false, + "in": "query", + "description": "是否启用", + "schema": { + "type": "boolean" + } + }, + { + "name": "email", + "required": false, + "in": "query", + "description": "邮箱", + "schema": { + "type": "string" + } + }, + { + "name": "groups", + "required": false, + "in": "query", + "description": "团队", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "labels", + "required": false, + "in": "query", + "description": "标签", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "name", + "required": false, + "in": "query", + "description": "姓名", + "schema": { + "type": "string" + } + }, + { + "name": "phone", + "required": false, + "in": "query", + "description": "手机号", + "schema": { + "type": "string" + } + }, + { + "name": "registerRegion", + "required": false, + "in": "query", + "description": "注册地区,存地区编号", + "schema": { + "type": "string" + } + }, + { + "name": "roles", + "required": false, + "in": "query", + "description": "角色", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "status", + "required": false, + "in": "query", + "description": "状态", + "schema": { + "type": "string" + } + }, + { + "name": "type", + "required": false, + "in": "query", + "description": "类型, 登录端", + "schema": { + "type": "string" + } + }, + { + "name": "username", + "required": false, + "in": "query", + "description": "用户名", + "schema": { + "type": "string" + } + }, + { + "name": "_limit", + "required": false, + "in": "query", + "description": "分页大小", + "schema": { + "type": "number" + } + }, + { + "name": "_offset", + "required": false, + "in": "query", + "description": "分页偏移", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "The result of count users.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CountResult" + } + } + } + } + }, + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CountResult" + } + } + } + } + }, + "tags": [ + "user" + ] + } + }, "/users/{userId}": { "get": { "operationId": "getUser", @@ -3064,87 +3314,411 @@ } } }, - "201": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - } - }, - "tags": [ - "user" - ] - } - }, - "/users/{email}/@upsertUserByEmail": { - "post": { - "operationId": "upsertUserByEmail", - "summary": "", - "description": "Upsert user by email", - "parameters": [ + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "user" + ] + } + }, + "/users/{email}/@upsertUserByEmail": { + "post": { + "operationId": "upsertUserByEmail", + "summary": "", + "description": "Upsert user by email", + "parameters": [ + { + "name": "email", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserDto" + } + } + } + }, + "responses": { + "200": { + "description": "The user upserted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "user" + ] + } + }, + "/users/{phone}/@upsertUserByPhone": { + "post": { + "operationId": "upsertUserByPhone", + "summary": "", + "description": "Upsert user by phone", + "parameters": [ + { + "name": "phone", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserDto" + } + } + } + }, + "responses": { + "200": { + "description": "The user upserted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "user" + ] + } + }, + "/users/{userId}/@verifyIdentity": { + "post": { + "operationId": "verifyIdentity", + "summary": "", + "description": "Verify identity", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The user has been verified.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + } + }, + "tags": [ + "user" + ] + } + }, + "/users/{userId}/@updatePassword": { + "post": { + "operationId": "updatePassword", + "summary": "", + "description": "Update password", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdatePasswordDto" + } + } + } + }, + "responses": { + "204": { + "description": "" + } + }, + "tags": [ + "user" + ] + } + }, + "/users/@aggregate": { + "post": { + "operationId": "aggregateUsers", + "summary": "", + "description": "Aggregate user", + "parameters": [ + { + "name": "_sort", + "required": false, + "in": "query", + "description": "排序参数", + "schema": { + "enum": [ + "createdAt", + "-createdAt", + "updatedAt", + "-updatedAt", + "lastLoginAt", + "-lastLoginAt", + "expireAt", + "-expireAt" + ], + "type": "string" + } + }, + { + "name": "id", + "required": false, + "in": "query", + "description": "按 id 筛选", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "name_like", + "required": false, + "in": "query", + "description": "名称 模糊查询", + "schema": { + "type": "string" + } + }, + { + "name": "username_like", + "required": false, + "in": "query", + "description": "用户名 模糊查询", + "schema": { + "type": "string" + } + }, + { + "name": "nickname_like", + "required": false, + "in": "query", + "description": "昵称 模糊查询", + "schema": { + "type": "string" + } + }, + { + "name": "ns_tree", + "required": false, + "in": "query", + "description": "所属命名空间的 tree 查询", + "schema": { + "type": "string" + } + }, + { + "name": "expireAt_gte", + "required": false, + "in": "query", + "description": "过期时间大于该时间", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "expireAt_lte", + "required": false, + "in": "query", + "description": "过期时间小于该时间", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "active", + "required": false, + "in": "query", + "description": "是否启用", + "schema": { + "type": "boolean" + } + }, + { + "name": "email", + "required": false, + "in": "query", + "description": "邮箱", + "schema": { + "type": "string" + } + }, + { + "name": "groups", + "required": false, + "in": "query", + "description": "团队", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "labels", + "required": false, + "in": "query", + "description": "标签", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "name", + "required": false, + "in": "query", + "description": "姓名", + "schema": { + "type": "string" + } + }, + { + "name": "phone", + "required": false, + "in": "query", + "description": "手机号", + "schema": { + "type": "string" + } + }, { - "name": "email", - "required": true, - "in": "path", + "name": "registerRegion", + "required": false, + "in": "query", + "description": "注册地区,存地区编号", "schema": { "type": "string" } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateUserDto" + }, + { + "name": "roles", + "required": false, + "in": "query", + "description": "角色", + "schema": { + "type": "array", + "items": { + "type": "string" } } - } - }, - "responses": { - "200": { - "description": "The user upserted.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - } + }, + { + "name": "status", + "required": false, + "in": "query", + "description": "状态", + "schema": { + "type": "string" } }, - "201": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } + { + "name": "type", + "required": false, + "in": "query", + "description": "类型, 登录端", + "schema": { + "type": "string" } - } - }, - "tags": [ - "user" - ] - } - }, - "/users/{phone}/@upsertUserByPhone": { - "post": { - "operationId": "upsertUserByPhone", - "summary": "", - "description": "Upsert user by phone", - "parameters": [ + }, { - "name": "phone", - "required": true, - "in": "path", + "name": "username", + "required": false, + "in": "query", + "description": "用户名", "schema": { "type": "string" } + }, + { + "name": "_limit", + "required": false, + "in": "query", + "description": "分页大小", + "schema": { + "type": "number" + } + }, + { + "name": "_offset", + "required": false, + "in": "query", + "description": "分页偏移", + "schema": { + "type": "number" + } } ], "requestBody": { @@ -3152,18 +3726,21 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateUserDto" + "$ref": "#/components/schemas/AggregateUserDto" } } } }, "responses": { "200": { - "description": "The user upserted.", + "description": "A paged array of user aggregate results.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/User" + "type": "array", + "items": { + "$ref": "#/components/schemas/UserAggregateResult" + } } } } @@ -3173,39 +3750,10 @@ "content": { "application/json": { "schema": { - "type": "object" - } - } - } - } - }, - "tags": [ - "user" - ] - } - }, - "/users/{userId}/@verifyIdentity": { - "post": { - "operationId": "verifyIdentity", - "summary": "", - "description": "Verify identity", - "parameters": [ - { - "name": "userId", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "The user has been verified.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" + "type": "array", + "items": { + "$ref": "#/components/schemas/UserAggregateResult" + } } } } @@ -3216,41 +3764,6 @@ ] } }, - "/users/{userId}/@updatePassword": { - "post": { - "operationId": "updatePassword", - "summary": "", - "description": "Update password", - "parameters": [ - { - "name": "userId", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdatePasswordDto" - } - } - } - }, - "responses": { - "204": { - "description": "" - } - }, - "tags": [ - "user" - ] - } - }, "/industries": { "get": { "operationId": "listIndustries", @@ -5365,6 +5878,17 @@ } } }, + "CountResult": { + "type": "object", + "properties": { + "count": { + "type": "number" + } + }, + "required": [ + "count" + ] + }, "UpdateUserDto": { "type": "object", "properties": { @@ -5518,6 +6042,92 @@ "newPassword" ] }, + "AggregateUserDto": { + "type": "object", + "properties": { + "group": { + "type": "array", + "description": "The group by clause", + "items": { + "type": "string", + "enum": [ + "level", + "labels", + "language", + "ns", + "registerRegion", + "roles", + "groups", + "active", + "status", + "createdAt" + ] + } + } + } + }, + "DateGroup": { + "type": "object", + "properties": { + "year": { + "type": "number" + }, + "month": { + "type": "number" + }, + "week": { + "type": "number" + }, + "day": { + "type": "number" + }, + "hour": { + "type": "number" + } + } + }, + "UserAggregateResult": { + "type": "object", + "properties": { + "level": { + "type": "number" + }, + "label": { + "type": "string" + }, + "language": { + "type": "string" + }, + "ns": { + "type": "string" + }, + "registerRegion": { + "type": "string" + }, + "role": { + "type": "string" + }, + "group": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "status": { + "type": "string" + }, + "createdAt": { + "$ref": "#/components/schemas/DateGroup" + }, + "count": { + "type": "number", + "description": "统计数量" + } + }, + "required": [ + "count" + ] + }, "Industry": { "type": "object", "properties": { diff --git a/src/common/entities/count.entity.ts b/src/common/entities/count.entity.ts new file mode 100644 index 0000000..c718384 --- /dev/null +++ b/src/common/entities/count.entity.ts @@ -0,0 +1,3 @@ +export class CountResult { + count: number; +} diff --git a/src/common/index.ts b/src/common/index.ts index 2a5a0db..4d48253 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -2,3 +2,4 @@ export * from './route-logger.middleware'; export * from './query.dto'; export * from './cache.interceptor'; export * from './exception-factory'; +export * from './entities/count.entity'; diff --git a/src/user/dto/aggregate.dto.ts b/src/user/dto/aggregate.dto.ts new file mode 100644 index 0000000..c40a9ab --- /dev/null +++ b/src/user/dto/aggregate.dto.ts @@ -0,0 +1,23 @@ +import { IsEnum, IsOptional } from 'class-validator'; + +export enum GroupField { + level = 'level', + label = 'labels', + language = 'language', + ns = 'ns', + registerRegion = 'registerRegion', + role = 'roles', + group = 'groups', + active = 'active', + status = 'status', + createdAt = 'createdAt', +} + +export class AggregateUserDto { + /** + * The group by clause + */ + @IsOptional() + @IsEnum(GroupField, { each: true }) + group?: GroupField[]; +} diff --git a/src/user/entities/user.aggregate.entity.ts b/src/user/entities/user.aggregate.entity.ts new file mode 100644 index 0000000..b718ad9 --- /dev/null +++ b/src/user/entities/user.aggregate.entity.ts @@ -0,0 +1,54 @@ +import { Type } from 'class-transformer'; +import { IsBoolean, IsInt, IsNotEmpty, IsNumber, IsOptional, IsString, Min } from 'class-validator'; + +import { DateGroup } from 'src/mongo'; + +export class UserAggregateResult { + @IsOptional() + @IsNumber() + level?: number; + + @IsOptional() + @IsString() + label?: string; + + @IsOptional() + @IsString() + language?: string; + + @IsOptional() + @IsString() + ns?: string; + + @IsOptional() + @IsString() + registerRegion?: string; + + @IsOptional() + @IsString() + role?: string; + + @IsOptional() + @IsString() + group?: string; + + @IsOptional() + @IsBoolean() + active?: boolean; + + @IsOptional() + @IsString() + status?: string; + + @IsOptional() + createdAt?: DateGroup; + + /** + * 统计数量 + */ + @IsNotEmpty() + @IsInt() + @Min(0) + @Type(() => Number) + count: number; +} diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 1dc1329..2765673 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -29,14 +29,16 @@ import { import { Response } from 'express'; import { RedisClientType } from 'redis'; -import { SetCacheInterceptor, UnsetCacheInterceptor } from 'src/common'; +import { CountResult, SetCacheInterceptor, UnsetCacheInterceptor } from 'src/common'; import { ErrorCodes } from 'src/constants'; import { NamespaceService } from 'src/namespace'; +import { AggregateUserDto } from './dto/aggregate.dto'; import { CreateUserDto } from './dto/create-user.dto'; import { ListUsersQuery } from './dto/list-users.dto'; import { UpdatePasswordDto } from './dto/update-password.dto'; import { UpdateUserDto } from './dto/update-user.dto'; +import { UserAggregateResult } from './entities/user.aggregate.entity'; import { User, UserDocument } from './entities/user.entity'; import { UserService } from './user.service'; import { verifyIdentity } from './verify-identity'; @@ -164,6 +166,20 @@ export class UserController { return data; } + /** + * Count users + */ + @ApiOperation({ operationId: 'countUsers' }) + @ApiOkResponse({ + description: 'The result of count users.', + type: [CountResult], + }) + @Post('@countUsers') + async count(@Query() query: ListUsersQuery): Promise { + const count = await this.userService.count(query); + return { count }; + } + /** * Find user */ @@ -518,4 +534,17 @@ export class UserController { await this.userService.updatePassword(userId, dto.newPassword); } + + /** + * Aggregate user + */ + @ApiOperation({ operationId: 'aggregateUsers' }) + @ApiOkResponse({ + description: 'A paged array of user aggregate results.', + type: [UserAggregateResult], + }) + @Post('@aggregate') + async aggregate(@Query() query: ListUsersQuery, @Body() body: AggregateUserDto) { + return this.userService.aggregate(query, body); + } } diff --git a/src/user/user.service.ts b/src/user/user.service.ts index f7ee4eb..3d133c0 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -6,11 +6,13 @@ import { FilterQuery, Model } from 'mongoose'; import { createHash, validateHash } from 'src/lib/crypt'; import { countTailZero, inferNumber } from 'src/lib/lang/number'; -import { buildMongooseQuery } from 'src/mongo'; +import { buildMongooseQuery, genAggGroupId, genSort, unWindGroupId } from 'src/mongo'; +import { AggregateUserDto } from './dto/aggregate.dto'; import { CreateUserDto } from './dto/create-user.dto'; import { ListUsersQuery } from './dto/list-users.dto'; import { UpdateUserDto } from './dto/update-user.dto'; +import { UserAggregateResult } from './entities/user.aggregate.entity'; import { User, UserDocument } from './entities/user.entity'; const debug = Debug('auth:user:service'); @@ -172,4 +174,48 @@ export class UserService { cleanupAllData(): Promise { return this.userModel.deleteMany({}).exec(); } + + /** + * 统计 + */ + aggregate(query: ListUsersQuery, dto: AggregateUserDto): Promise { + const { filter, offset, limit, sort } = buildMongooseQuery(wrapFilter(query)); + const { group = [] } = dto; + const groupId = genAggGroupId(group); + + // 特殊字段需要先进行 $unwind + const unwindFields = ['labels', 'groups', 'roles']; + const unwindStages = group + .filter((field) => unwindFields.includes(field)) + .map((field) => ({ $unwind: { path: `$${field}`, preserveNullAndEmptyArrays: true } })); + + const pipeline = [ + { $match: filter }, + ...unwindStages, // 添加 $unwind 阶段 + { + $group: { + _id: groupId, + count: { $sum: 1 }, + }, + }, + { + $project: { + ...unWindGroupId(groupId), + _id: 0, + count: 1, + }, + }, + sort && { + $sort: genSort(sort), + }, + offset && { + $skip: offset, + }, + limit && { + $limit: limit, + }, + ].filter(Boolean); + + return this.userModel.aggregate(pipeline); + } } From a4906c215059b5574eccb35890290b1c2b4ec763 Mon Sep 17 00:00:00 2001 From: zzswang Date: Mon, 12 May 2025 19:28:47 +0800 Subject: [PATCH 2/6] chore: remove issue link check --- .github/workflows/issue-link.yml | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 .github/workflows/issue-link.yml diff --git a/.github/workflows/issue-link.yml b/.github/workflows/issue-link.yml deleted file mode 100644 index c0d8537..0000000 --- a/.github/workflows/issue-link.yml +++ /dev/null @@ -1,25 +0,0 @@ -# This workflow will run tests using node and then publish a package to NPM Packages when a release is created -# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages - -# 用于验证pr是否包括issue链接 - -name: Issue link verify - -on: - pull_request: - types: [edited, synchronize, opened, reopened] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true -jobs: - pr-verify: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Verify Linked Issue - uses: hattan/verify-linked-issue-action@v1.1.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - message: '请添加issue链接!' From ee1e04e65500728273a82141fd127243682ef91d Mon Sep 17 00:00:00 2001 From: zzswang Date: Tue, 13 May 2025 11:38:07 +0800 Subject: [PATCH 3/6] fix: register user with more info --- src/auth/auth.controller.ts | 15 +++++ src/auth/dto/register.dto.ts | 107 ++++++++++++++++++++++++++++++- src/user/dto/create-user.dto.ts | 25 +++++++- src/user/entities/user.entity.ts | 15 ++++- 4 files changed, 157 insertions(+), 5 deletions(-) diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index a09e7db..3788e8c 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -284,6 +284,11 @@ export class AuthController { username: dto.username, password: dto.password, ns: dto.ns, + inviter: dto.inviter, + labels: dto.labels, + registerIp: dto.registerIp, + registerRegion: dto.registerRegion, + type: dto.type, }); } @@ -316,6 +321,11 @@ export class AuthController { return this.userService.create({ phone: dto.phone, ns: dto.ns, + inviter: dto.inviter, + labels: dto.labels, + registerIp: dto.registerIp, + registerRegion: dto.registerRegion, + type: dto.type, }); } @@ -348,6 +358,11 @@ export class AuthController { return this.userService.create({ email: dto.email, ns: dto.ns, + inviter: dto.inviter, + labels: dto.labels, + registerIp: dto.registerIp, + registerRegion: dto.registerRegion, + type: dto.type, }); } diff --git a/src/auth/dto/register.dto.ts b/src/auth/dto/register.dto.ts index b5b6e46..672fb4e 100644 --- a/src/auth/dto/register.dto.ts +++ b/src/auth/dto/register.dto.ts @@ -1,4 +1,4 @@ -import { IsEmail, IsMobilePhone, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { IsEmail, IsIP, IsMobilePhone, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsNs, IsPassword, IsUsername } from 'src/common/validate'; @@ -23,6 +23,41 @@ export class RegisterDto { @IsOptional() @IsNs() ns?: string; + + /** + * 邀请人 + */ + @IsOptional() + @IsString() + inviter?: string; + + /** + * 标签 + */ + @IsOptional() + @IsString({ each: true }) + labels?: string[]; + + /** + * 注册 IP + */ + @IsOptional() + @IsIP() + registerIp?: string; + + /** + * 注册地区,存地区编号 + */ + @IsOptional() + @IsString() + registerRegion?: string; + + /** + * 类型, 登录端 + */ + @IsOptional() + @IsString() + type?: string; } export class RegisterbyPhoneDto { @@ -53,6 +88,41 @@ export class RegisterbyPhoneDto { @IsOptional() @IsNs() ns?: string; + + /** + * 邀请人 + */ + @IsOptional() + @IsString() + inviter?: string; + + /** + * 标签 + */ + @IsOptional() + @IsString({ each: true }) + labels?: string[]; + + /** + * 注册 IP + */ + @IsOptional() + @IsIP() + registerIp?: string; + + /** + * 注册地区,存地区编号 + */ + @IsOptional() + @IsString() + registerRegion?: string; + + /** + * 类型, 登录端 + */ + @IsOptional() + @IsString() + type?: string; } export class RegisterByEmailDto { @@ -83,4 +153,39 @@ export class RegisterByEmailDto { @IsOptional() @IsNs() ns?: string; + + /** + * 邀请人 + */ + @IsOptional() + @IsString() + inviter?: string; + + /** + * 标签 + */ + @IsOptional() + @IsString({ each: true }) + labels?: string[]; + + /** + * 注册 IP + */ + @IsOptional() + @IsIP() + registerIp?: string; + + /** + * 注册地区,存地区编号 + */ + @IsOptional() + @IsString() + registerRegion?: string; + + /** + * 类型, 登录端 + */ + @IsOptional() + @IsString() + type?: string; } diff --git a/src/user/dto/create-user.dto.ts b/src/user/dto/create-user.dto.ts index 738a32b..22ece8b 100644 --- a/src/user/dto/create-user.dto.ts +++ b/src/user/dto/create-user.dto.ts @@ -4,16 +4,39 @@ import { IsOptional, IsString } from 'class-validator'; import { UserDoc } from '../entities/user.entity'; export class CreateUserDto extends OmitType(UserDoc, [ + 'groups', 'labels', 'lastSeenAt', 'lastLoginIp', 'lastLoginAt', - 'registerIp', + 'permissions', + 'roles', ] as const) { + /** + * 团队 + */ + @IsOptional() + @IsString({ each: true }) + groups?: string[]; + /** * 标签 */ @IsOptional() @IsString({ each: true }) labels?: string[]; + + /** + * 权限 + */ + @IsOptional() + @IsString({ each: true }) + permissions?: string[]; + + /** + * 角色 + */ + @IsOptional() + @IsString({ each: true }) + roles?: string[]; } diff --git a/src/user/entities/user.entity.ts b/src/user/entities/user.entity.ts index 8429722..fd05c2d 100644 --- a/src/user/entities/user.entity.ts +++ b/src/user/entities/user.entity.ts @@ -84,9 +84,18 @@ export class UserDoc { @Prop() intro?: string; + /** + * 邀请人 + */ + @IsOptional() + @IsString() + @Prop() + inviter?: string; + /** * 标签 */ + @IsOptional() @IsString({ each: true }) @Prop() labels: string[]; @@ -180,7 +189,7 @@ export class UserDoc { @IsOptional() @IsString({ each: true }) @Prop() - roles?: string[]; + roles: string[]; /** * 用户名 @@ -211,7 +220,7 @@ export class UserDoc { @IsOptional() @IsString({ each: true }) @Prop() - permissions?: string[]; + permissions: string[]; /** * 团队 @@ -219,7 +228,7 @@ export class UserDoc { @IsOptional() @IsString({ each: true }) @Prop() - groups?: string[]; + groups: string[]; /** * 最后登录时间 From 3b96528857acb7f156e2538e82a5ede980a1805c Mon Sep 17 00:00:00 2001 From: zzswang Date: Tue, 13 May 2025 11:44:21 +0800 Subject: [PATCH 4/6] fix: register dto --- openapi.json | 132 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 110 insertions(+), 22 deletions(-) diff --git a/openapi.json b/openapi.json index db4156a..bfb5ced 100644 --- a/openapi.json +++ b/openapi.json @@ -1,5 +1,5 @@ { - "hash": "82fcbe018c9fd79aab12e521511bcb7a5b394a2dc70da6415a711d446fba8ea0", + "hash": "bd04223d5edb14e8cdb8d168c52fe946d2b5421b9931c69f593d20e433e882f6", "openapi": "3.0.0", "paths": { "/hello": { @@ -4328,6 +4328,29 @@ "ns": { "type": "string", "description": "命名空间" + }, + "inviter": { + "type": "string", + "description": "邀请人" + }, + "labels": { + "description": "标签", + "type": "array", + "items": { + "type": "string" + } + }, + "registerIp": { + "type": "string", + "description": "注册 IP" + }, + "registerRegion": { + "type": "string", + "description": "注册地区,存地区编号" + }, + "type": { + "type": "string", + "description": "类型, 登录端" } }, "required": [ @@ -4380,6 +4403,10 @@ "type": "string", "description": "简介" }, + "inviter": { + "type": "string", + "description": "邀请人" + }, "labels": { "description": "标签", "type": "array", @@ -4500,6 +4527,9 @@ }, "required": [ "labels", + "roles", + "permissions", + "groups", "id" ] }, @@ -4521,6 +4551,29 @@ "ns": { "type": "string", "description": "命名空间" + }, + "inviter": { + "type": "string", + "description": "邀请人" + }, + "labels": { + "description": "标签", + "type": "array", + "items": { + "type": "string" + } + }, + "registerIp": { + "type": "string", + "description": "注册 IP" + }, + "registerRegion": { + "type": "string", + "description": "注册地区,存地区编号" + }, + "type": { + "type": "string", + "description": "类型, 登录端" } }, "required": [ @@ -4547,6 +4600,29 @@ "ns": { "type": "string", "description": "命名空间" + }, + "inviter": { + "type": "string", + "description": "邀请人" + }, + "labels": { + "description": "标签", + "type": "array", + "items": { + "type": "string" + } + }, + "registerIp": { + "type": "string", + "description": "注册 IP" + }, + "registerRegion": { + "type": "string", + "description": "注册地区,存地区编号" + }, + "type": { + "type": "string", + "description": "类型, 登录端" } }, "required": [ @@ -5767,6 +5843,13 @@ "description": "是否有密码", "readOnly": true }, + "groups": { + "description": "团队", + "type": "array", + "items": { + "type": "string" + } + }, "labels": { "description": "标签", "type": "array", @@ -5774,6 +5857,20 @@ "type": "string" } }, + "permissions": { + "description": "权限", + "type": "array", + "items": { + "type": "string" + } + }, + "roles": { + "description": "角色", + "type": "array", + "items": { + "type": "string" + } + }, "avatar": { "type": "string", "description": "头像" @@ -5806,6 +5903,10 @@ "type": "string", "description": "简介" }, + "inviter": { + "type": "string", + "description": "邀请人" + }, "language": { "type": "string", "description": "使用语言" @@ -5826,17 +5927,14 @@ "type": "string", "description": "手机号" }, + "registerIp": { + "type": "string", + "description": "注册 IP" + }, "registerRegion": { "type": "string", "description": "注册地区,存地区编号" }, - "roles": { - "description": "角色", - "type": "array", - "items": { - "type": "string" - } - }, "username": { "type": "string", "description": "用户名" @@ -5845,20 +5943,6 @@ "type": "string", "description": "员工编号" }, - "permissions": { - "description": "权限", - "type": "array", - "items": { - "type": "string" - } - }, - "groups": { - "description": "团队", - "type": "array", - "items": { - "type": "string" - } - }, "active": { "type": "boolean", "description": "是否启用" @@ -5929,6 +6013,10 @@ "type": "string", "description": "简介" }, + "inviter": { + "type": "string", + "description": "邀请人" + }, "labels": { "description": "标签", "type": "array", From a24d0bf3f609dd2ade72243bb6f239f9827bfadc Mon Sep 17 00:00:00 2001 From: zzswang Date: Tue, 13 May 2025 13:05:12 +0800 Subject: [PATCH 5/6] fix: list user by inviter --- openapi.json | 29 ++++++++++++++++++++++++++++- src/user/dto/list-users.dto.ts | 1 + 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/openapi.json b/openapi.json index bfb5ced..9b8cf8e 100644 --- a/openapi.json +++ b/openapi.json @@ -1,5 +1,5 @@ { - "hash": "bd04223d5edb14e8cdb8d168c52fe946d2b5421b9931c69f593d20e433e882f6", + "hash": "638ff830562977b943fea9b64b162bef085f27500e7f787feced48c9085a64f2", "openapi": "3.0.0", "paths": { "/hello": { @@ -2763,6 +2763,15 @@ } } }, + { + "name": "inviter", + "required": false, + "in": "query", + "description": "邀请人", + "schema": { + "type": "string" + } + }, { "name": "labels", "required": false, @@ -3003,6 +3012,15 @@ } } }, + { + "name": "inviter", + "required": false, + "in": "query", + "description": "邀请人", + "schema": { + "type": "string" + } + }, { "name": "labels", "required": false, @@ -3624,6 +3642,15 @@ } } }, + { + "name": "inviter", + "required": false, + "in": "query", + "description": "邀请人", + "schema": { + "type": "string" + } + }, { "name": "labels", "required": false, diff --git a/src/user/dto/list-users.dto.ts b/src/user/dto/list-users.dto.ts index 29a9c50..fc3da4d 100644 --- a/src/user/dto/list-users.dto.ts +++ b/src/user/dto/list-users.dto.ts @@ -16,6 +16,7 @@ export class ListUsersQuery extends IntersectionType( 'active', 'email', 'groups', + 'inviter', 'labels', 'name', 'phone', From ced04fa4915281919bcb358a1e700bff85fe096e Mon Sep 17 00:00:00 2001 From: zzswang Date: Tue, 13 May 2025 13:17:10 +0800 Subject: [PATCH 6/6] fix: count users response type --- openapi.json | 7 ++----- src/user/user.controller.ts | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/openapi.json b/openapi.json index 9b8cf8e..4af77ff 100644 --- a/openapi.json +++ b/openapi.json @@ -1,5 +1,5 @@ { - "hash": "638ff830562977b943fea9b64b162bef085f27500e7f787feced48c9085a64f2", + "hash": "a9a86c65b13852f5820f588fd81c1e7d442f1c008afac8a379e75b2d2038257d", "openapi": "3.0.0", "paths": { "/hello": { @@ -3124,10 +3124,7 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CountResult" - } + "$ref": "#/components/schemas/CountResult" } } } diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 2765673..665e567 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -172,7 +172,7 @@ export class UserController { @ApiOperation({ operationId: 'countUsers' }) @ApiOkResponse({ description: 'The result of count users.', - type: [CountResult], + type: CountResult, }) @Post('@countUsers') async count(@Query() query: ListUsersQuery): Promise {