From f8cfb5e59729ca3ea0ef1f5551f49ecf22c861f5 Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 10 Dec 2025 17:03:34 -0800 Subject: [PATCH] feat(authorization): add admin API for community authorization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add new endpoint POST /api/v1/admin/authorizations/community that allows administrators to directly authorize users as community managers without requiring the user to self-apply first. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../admin-authorization.controller.ts | 23 +++++++++- .../api/dto/request/grant-community.dto.ts | 20 +++++++++ .../src/api/dto/request/index.ts | 1 + .../commands/grant-community.command.ts | 9 ++++ .../src/application/commands/index.ts | 1 + .../authorization-application.service.ts | 19 ++++++++ .../authorization-role.aggregate.ts | 43 +++++++++++++++++++ .../src/domain/events/authorization-events.ts | 23 ++++++++++ 8 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 backend/services/authorization-service/src/api/dto/request/grant-community.dto.ts create mode 100644 backend/services/authorization-service/src/application/commands/grant-community.command.ts diff --git a/backend/services/authorization-service/src/api/controllers/admin-authorization.controller.ts b/backend/services/authorization-service/src/api/controllers/admin-authorization.controller.ts index d72b4a1e..bb57cdb0 100644 --- a/backend/services/authorization-service/src/api/controllers/admin-authorization.controller.ts +++ b/backend/services/authorization-service/src/api/controllers/admin-authorization.controller.ts @@ -1,8 +1,8 @@ import { Controller, Post, Body, UseGuards, HttpCode, HttpStatus } from '@nestjs/common' import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger' import { AuthorizationApplicationService } from '@/application/services' -import { GrantProvinceCompanyCommand, GrantCityCompanyCommand } from '@/application/commands' -import { GrantProvinceCompanyDto, GrantCityCompanyDto } from '@/api/dto/request' +import { GrantCommunityCommand, GrantProvinceCompanyCommand, GrantCityCompanyCommand } from '@/application/commands' +import { GrantCommunityDto, GrantProvinceCompanyDto, GrantCityCompanyDto } from '@/api/dto/request' import { CurrentUser } from '@/shared/decorators' import { JwtAuthGuard } from '@/shared/guards' @@ -13,6 +13,25 @@ import { JwtAuthGuard } from '@/shared/guards' export class AdminAuthorizationController { constructor(private readonly applicationService: AuthorizationApplicationService) {} + @Post('community') + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: '授权社区(管理员)' }) + @ApiResponse({ status: 201, description: '授权成功' }) + async grantCommunity( + @CurrentUser() user: { userId: string; accountSequence: number }, + @Body() dto: GrantCommunityDto, + ): Promise<{ message: string }> { + const command = new GrantCommunityCommand( + dto.userId, + dto.accountSequence, + dto.communityName, + user.userId, + user.accountSequence, + ) + await this.applicationService.grantCommunity(command) + return { message: '社区授权成功' } + } + @Post('province-company') @HttpCode(HttpStatus.CREATED) @ApiOperation({ summary: '授权正式省公司(管理员)' }) diff --git a/backend/services/authorization-service/src/api/dto/request/grant-community.dto.ts b/backend/services/authorization-service/src/api/dto/request/grant-community.dto.ts new file mode 100644 index 00000000..8259d038 --- /dev/null +++ b/backend/services/authorization-service/src/api/dto/request/grant-community.dto.ts @@ -0,0 +1,20 @@ +import { IsString, IsNotEmpty, MaxLength, IsNumber } from 'class-validator' +import { ApiProperty } from '@nestjs/swagger' + +export class GrantCommunityDto { + @ApiProperty({ description: '用户ID' }) + @IsString() + @IsNotEmpty({ message: '用户ID不能为空' }) + userId: string + + @ApiProperty({ description: '账户序列号' }) + @IsNumber() + @IsNotEmpty({ message: '账户序列号不能为空' }) + accountSequence: number + + @ApiProperty({ description: '社区名称', example: '深圳社区' }) + @IsString() + @IsNotEmpty({ message: '社区名称不能为空' }) + @MaxLength(100, { message: '社区名称最大100字符' }) + communityName: string +} diff --git a/backend/services/authorization-service/src/api/dto/request/index.ts b/backend/services/authorization-service/src/api/dto/request/index.ts index e3852cd6..448ef51c 100644 --- a/backend/services/authorization-service/src/api/dto/request/index.ts +++ b/backend/services/authorization-service/src/api/dto/request/index.ts @@ -1,6 +1,7 @@ export * from './apply-community-auth.dto' export * from './apply-auth-province.dto' export * from './apply-auth-city.dto' +export * from './grant-community.dto' export * from './grant-province-company.dto' export * from './grant-city-company.dto' export * from './revoke-authorization.dto' diff --git a/backend/services/authorization-service/src/application/commands/grant-community.command.ts b/backend/services/authorization-service/src/application/commands/grant-community.command.ts new file mode 100644 index 00000000..97e2645f --- /dev/null +++ b/backend/services/authorization-service/src/application/commands/grant-community.command.ts @@ -0,0 +1,9 @@ +export class GrantCommunityCommand { + constructor( + public readonly userId: string, + public readonly accountSequence: number, + public readonly communityName: string, + public readonly adminId: string, + public readonly adminAccountSequence: number, + ) {} +} diff --git a/backend/services/authorization-service/src/application/commands/index.ts b/backend/services/authorization-service/src/application/commands/index.ts index 41e88694..087a92e5 100644 --- a/backend/services/authorization-service/src/application/commands/index.ts +++ b/backend/services/authorization-service/src/application/commands/index.ts @@ -1,6 +1,7 @@ export * from './apply-community-auth.command' export * from './apply-auth-province-company.command' export * from './apply-auth-city-company.command' +export * from './grant-community.command' export * from './grant-province-company.command' export * from './grant-city-company.command' export * from './revoke-authorization.command' diff --git a/backend/services/authorization-service/src/application/services/authorization-application.service.ts b/backend/services/authorization-service/src/application/services/authorization-application.service.ts index 2c543a50..796aecf4 100644 --- a/backend/services/authorization-service/src/application/services/authorization-application.service.ts +++ b/backend/services/authorization-service/src/application/services/authorization-application.service.ts @@ -30,6 +30,7 @@ import { ApplyAuthProvinceCompanyResult, ApplyAuthCityCompanyCommand, ApplyAuthCityCompanyResult, + GrantCommunityCommand, GrantProvinceCompanyCommand, GrantCityCompanyCommand, RevokeAuthorizationCommand, @@ -218,6 +219,24 @@ export class AuthorizationApplicationService { } } + /** + * 管理员直接授权社区 + */ + async grantCommunity(command: GrantCommunityCommand): Promise { + const userId = UserId.create(command.userId, command.accountSequence) + const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) + + const authorization = AuthorizationRole.createCommunity({ + userId, + communityName: command.communityName, + adminId, + }) + + await this.authorizationRepository.save(authorization) + await this.eventPublisher.publishAll(authorization.domainEvents) + authorization.clearDomainEvents() + } + /** * 管理员授权正式省公司 */ diff --git a/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.ts b/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.ts index fb29f81c..06ff6e1f 100644 --- a/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.ts +++ b/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.ts @@ -9,6 +9,7 @@ import { import { RoleType, AuthorizationStatus, MonthlyTargetType } from '@/domain/enums' import { DomainError } from '@/shared/exceptions' import { + CommunityAuthorizedEvent, CommunityAuthRequestedEvent, AuthProvinceCompanyRequestedEvent, AuthCityCompanyRequestedEvent, @@ -215,6 +216,48 @@ export class AuthorizationRole extends AggregateRoot { return auth } + // 工厂方法 - 管理员直接授权社区 + static createCommunity(params: { + userId: UserId + communityName: string + adminId: AdminUserId + }): AuthorizationRole { + const auth = new AuthorizationRole({ + authorizationId: AuthorizationId.generate(), + userId: params.userId, + roleType: RoleType.COMMUNITY, + regionCode: RegionCode.create(params.communityName), + regionName: params.communityName, + status: AuthorizationStatus.AUTHORIZED, + displayTitle: params.communityName, + authorizedAt: new Date(), + authorizedBy: params.adminId, + revokedAt: null, + revokedBy: null, + revokeReason: null, + assessmentConfig: AssessmentConfig.forCommunity(), + requireLocalPercentage: 0, + exemptFromPercentageCheck: true, + benefitActive: true, + benefitActivatedAt: new Date(), + benefitDeactivatedAt: null, + currentMonthIndex: 1, + createdAt: new Date(), + updatedAt: new Date(), + }) + + auth.addDomainEvent( + new CommunityAuthorizedEvent({ + authorizationId: auth.authorizationId.value, + userId: params.userId.value, + communityName: params.communityName, + authorizedBy: params.adminId.value, + }), + ) + + return auth + } + // 工厂方法 - 创建授权省公司 static createAuthProvinceCompany(params: { userId: UserId diff --git a/backend/services/authorization-service/src/domain/events/authorization-events.ts b/backend/services/authorization-service/src/domain/events/authorization-events.ts index a1dbb59d..66f6635a 100644 --- a/backend/services/authorization-service/src/domain/events/authorization-events.ts +++ b/backend/services/authorization-service/src/domain/events/authorization-events.ts @@ -1,6 +1,29 @@ import { DomainEvent } from './domain-event.base' import { RoleType } from '@/domain/enums' +// 社区授权事件(管理员直接授权) +export class CommunityAuthorizedEvent extends DomainEvent { + readonly eventType = 'authorization.community.authorized' + readonly aggregateId: string + readonly payload: { + authorizationId: string + userId: string + communityName: string + authorizedBy: string + } + + constructor(data: { + authorizationId: string + userId: string + communityName: string + authorizedBy: string + }) { + super() + this.aggregateId = data.authorizationId + this.payload = data + } +} + // 社区授权申请事件 export class CommunityAuthRequestedEvent extends DomainEvent { readonly eventType = 'authorization.community.requested'