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链接!' diff --git a/openapi.json b/openapi.json index 51f3a03..4af77ff 100644 --- a/openapi.json +++ b/openapi.json @@ -1,5 +1,5 @@ { - "hash": "af79bc2a57d52a28a84989f154b3acd055e336d3bde702136b50bb0e89d68944", + "hash": "a9a86c65b13852f5820f588fd81c1e7d442f1c008afac8a379e75b2d2038257d", "openapi": "3.0.0", "paths": { "/hello": { @@ -2763,6 +2763,15 @@ } } }, + { + "name": "inviter", + "required": false, + "in": "query", + "description": "邀请人", + "schema": { + "type": "string" + } + }, { "name": "labels", "required": false, @@ -2880,6 +2889,262 @@ ] } }, + "/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": "inviter", + "required": false, + "in": "query", + "description": "邀请人", + "schema": { + "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": { + "$ref": "#/components/schemas/CountResult" + } + } + } + }, + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CountResult" + } + } + } + } + }, + "tags": [ + "user" + ] + } + }, "/users/{userId}": { "get": { "operationId": "getUser", @@ -3143,7 +3408,340 @@ "required": true, "in": "path", "schema": { - "type": "string" + "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": "inviter", + "required": false, + "in": "query", + "description": "邀请人", + "schema": { + "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" } } ], @@ -3152,18 +3750,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 +3774,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 +3788,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", @@ -3815,6 +4352,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": [ @@ -3867,6 +4427,10 @@ "type": "string", "description": "简介" }, + "inviter": { + "type": "string", + "description": "邀请人" + }, "labels": { "description": "标签", "type": "array", @@ -3987,6 +4551,9 @@ }, "required": [ "labels", + "roles", + "permissions", + "groups", "id" ] }, @@ -4008,6 +4575,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": [ @@ -4034,6 +4624,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": [ @@ -5254,6 +5867,13 @@ "description": "是否有密码", "readOnly": true }, + "groups": { + "description": "团队", + "type": "array", + "items": { + "type": "string" + } + }, "labels": { "description": "标签", "type": "array", @@ -5261,6 +5881,20 @@ "type": "string" } }, + "permissions": { + "description": "权限", + "type": "array", + "items": { + "type": "string" + } + }, + "roles": { + "description": "角色", + "type": "array", + "items": { + "type": "string" + } + }, "avatar": { "type": "string", "description": "头像" @@ -5293,6 +5927,10 @@ "type": "string", "description": "简介" }, + "inviter": { + "type": "string", + "description": "邀请人" + }, "language": { "type": "string", "description": "使用语言" @@ -5313,17 +5951,14 @@ "type": "string", "description": "手机号" }, + "registerIp": { + "type": "string", + "description": "注册 IP" + }, "registerRegion": { "type": "string", "description": "注册地区,存地区编号" }, - "roles": { - "description": "角色", - "type": "array", - "items": { - "type": "string" - } - }, "username": { "type": "string", "description": "用户名" @@ -5332,20 +5967,6 @@ "type": "string", "description": "员工编号" }, - "permissions": { - "description": "权限", - "type": "array", - "items": { - "type": "string" - } - }, - "groups": { - "description": "团队", - "type": "array", - "items": { - "type": "string" - } - }, "active": { "type": "boolean", "description": "是否启用" @@ -5365,6 +5986,17 @@ } } }, + "CountResult": { + "type": "object", + "properties": { + "count": { + "type": "number" + } + }, + "required": [ + "count" + ] + }, "UpdateUserDto": { "type": "object", "properties": { @@ -5405,6 +6037,10 @@ "type": "string", "description": "简介" }, + "inviter": { + "type": "string", + "description": "邀请人" + }, "labels": { "description": "标签", "type": "array", @@ -5518,6 +6154,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/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/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/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/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', 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/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[]; /** * 最后登录时间 diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 1dc1329..665e567 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); + } }