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 67a592f1..22b418df 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,20 @@ import { Controller, Post, Body, UseGuards, HttpCode, HttpStatus } from '@nestjs/common' import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger' import { AuthorizationApplicationService } from '@/application/services' -import { GrantCommunityCommand, GrantProvinceCompanyCommand, GrantCityCompanyCommand } from '@/application/commands' -import { GrantCommunityDto, GrantProvinceCompanyDto, GrantCityCompanyDto } from '@/api/dto/request' +import { + GrantCommunityCommand, + GrantProvinceCompanyCommand, + GrantCityCompanyCommand, + GrantAuthProvinceCompanyCommand, + GrantAuthCityCompanyCommand, +} from '@/application/commands' +import { + GrantCommunityDto, + GrantProvinceCompanyDto, + GrantCityCompanyDto, + GrantAuthProvinceCompanyDto, + GrantAuthCityCompanyDto, +} from '@/api/dto/request' import { CurrentUser } from '@/shared/decorators' import { JwtAuthGuard } from '@/shared/guards' @@ -74,4 +86,48 @@ export class AdminAuthorizationController { await this.applicationService.grantCityCompany(command) return { message: '正式市公司授权成功' } } + + @Post('auth-province-company') + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: '授权省团队(管理员)' }) + @ApiResponse({ status: 201, description: '授权成功' }) + @ApiResponse({ status: 400, description: '验证失败(如团队内已存在相同省份授权)' }) + async grantAuthProvinceCompany( + @CurrentUser() user: { userId: string; accountSequence: number }, + @Body() dto: GrantAuthProvinceCompanyDto, + ): Promise<{ message: string }> { + const command = new GrantAuthProvinceCompanyCommand( + dto.userId, + dto.accountSequence, + dto.provinceCode, + dto.provinceName, + user.userId, + user.accountSequence, + dto.skipAssessment ?? false, + ) + await this.applicationService.grantAuthProvinceCompany(command) + return { message: '省团队授权成功' } + } + + @Post('auth-city-company') + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: '授权市团队(管理员)' }) + @ApiResponse({ status: 201, description: '授权成功' }) + @ApiResponse({ status: 400, description: '验证失败(如团队内已存在相同城市授权)' }) + async grantAuthCityCompany( + @CurrentUser() user: { userId: string; accountSequence: number }, + @Body() dto: GrantAuthCityCompanyDto, + ): Promise<{ message: string }> { + const command = new GrantAuthCityCompanyCommand( + dto.userId, + dto.accountSequence, + dto.cityCode, + dto.cityName, + user.userId, + user.accountSequence, + dto.skipAssessment ?? false, + ) + await this.applicationService.grantAuthCityCompany(command) + return { message: '市团队授权成功' } + } } diff --git a/backend/services/authorization-service/src/api/dto/request/grant-auth-city-company.dto.ts b/backend/services/authorization-service/src/api/dto/request/grant-auth-city-company.dto.ts new file mode 100644 index 00000000..55edd97a --- /dev/null +++ b/backend/services/authorization-service/src/api/dto/request/grant-auth-city-company.dto.ts @@ -0,0 +1,31 @@ +import { IsString, IsNotEmpty, MaxLength, IsNumber, IsBoolean, IsOptional } from 'class-validator' +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' + +export class GrantAuthCityCompanyDto { + @ApiProperty({ description: '用户ID' }) + @IsString() + @IsNotEmpty({ message: '用户ID不能为空' }) + userId: string + + @ApiProperty({ description: '账户序列号' }) + @IsNumber() + @IsNotEmpty({ message: '账户序列号不能为空' }) + accountSequence: number + + @ApiProperty({ description: '城市代码', example: '430100' }) + @IsString() + @IsNotEmpty({ message: '城市代码不能为空' }) + @MaxLength(20, { message: '城市代码最大20字符' }) + cityCode: string + + @ApiProperty({ description: '城市名称', example: '长沙市' }) + @IsString() + @IsNotEmpty({ message: '城市名称不能为空' }) + @MaxLength(50, { message: '城市名称最大50字符' }) + cityName: string + + @ApiPropertyOptional({ description: '是否跳过考核直接激活权益', default: false }) + @IsBoolean() + @IsOptional() + skipAssessment?: boolean +} diff --git a/backend/services/authorization-service/src/api/dto/request/grant-auth-province-company.dto.ts b/backend/services/authorization-service/src/api/dto/request/grant-auth-province-company.dto.ts new file mode 100644 index 00000000..1081ab26 --- /dev/null +++ b/backend/services/authorization-service/src/api/dto/request/grant-auth-province-company.dto.ts @@ -0,0 +1,31 @@ +import { IsString, IsNotEmpty, MaxLength, IsNumber, IsBoolean, IsOptional } from 'class-validator' +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' + +export class GrantAuthProvinceCompanyDto { + @ApiProperty({ description: '用户ID' }) + @IsString() + @IsNotEmpty({ message: '用户ID不能为空' }) + userId: string + + @ApiProperty({ description: '账户序列号' }) + @IsNumber() + @IsNotEmpty({ message: '账户序列号不能为空' }) + accountSequence: number + + @ApiProperty({ description: '省份代码', example: '430000' }) + @IsString() + @IsNotEmpty({ message: '省份代码不能为空' }) + @MaxLength(20, { message: '省份代码最大20字符' }) + provinceCode: string + + @ApiProperty({ description: '省份名称', example: '湖南省' }) + @IsString() + @IsNotEmpty({ message: '省份名称不能为空' }) + @MaxLength(50, { message: '省份名称最大50字符' }) + provinceName: string + + @ApiPropertyOptional({ description: '是否跳过考核直接激活权益', default: false }) + @IsBoolean() + @IsOptional() + skipAssessment?: boolean +} 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 448ef51c..e9d2b81f 100644 --- a/backend/services/authorization-service/src/api/dto/request/index.ts +++ b/backend/services/authorization-service/src/api/dto/request/index.ts @@ -4,5 +4,7 @@ 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 './grant-auth-province-company.dto' +export * from './grant-auth-city-company.dto' export * from './revoke-authorization.dto' export * from './grant-monthly-bypass.dto' diff --git a/backend/services/authorization-service/src/application/commands/grant-auth-city-company.command.ts b/backend/services/authorization-service/src/application/commands/grant-auth-city-company.command.ts new file mode 100644 index 00000000..145363ce --- /dev/null +++ b/backend/services/authorization-service/src/application/commands/grant-auth-city-company.command.ts @@ -0,0 +1,11 @@ +export class GrantAuthCityCompanyCommand { + constructor( + public readonly userId: string, + public readonly accountSequence: number, + public readonly cityCode: string, + public readonly cityName: string, + public readonly adminId: string, + public readonly adminAccountSequence: number, + public readonly skipAssessment: boolean = false, + ) {} +} diff --git a/backend/services/authorization-service/src/application/commands/grant-auth-province-company.command.ts b/backend/services/authorization-service/src/application/commands/grant-auth-province-company.command.ts new file mode 100644 index 00000000..f706f050 --- /dev/null +++ b/backend/services/authorization-service/src/application/commands/grant-auth-province-company.command.ts @@ -0,0 +1,11 @@ +export class GrantAuthProvinceCompanyCommand { + constructor( + public readonly userId: string, + public readonly accountSequence: number, + public readonly provinceCode: string, + public readonly provinceName: string, + public readonly adminId: string, + public readonly adminAccountSequence: number, + public readonly skipAssessment: boolean = false, + ) {} +} diff --git a/backend/services/authorization-service/src/application/commands/index.ts b/backend/services/authorization-service/src/application/commands/index.ts index 087a92e5..8caf7139 100644 --- a/backend/services/authorization-service/src/application/commands/index.ts +++ b/backend/services/authorization-service/src/application/commands/index.ts @@ -4,6 +4,8 @@ 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 './grant-auth-province-company.command' +export * from './grant-auth-city-company.command' export * from './revoke-authorization.command' export * from './grant-monthly-bypass.command' export * from './exempt-percentage-check.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 c488833e..15a756ac 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 @@ -34,6 +34,8 @@ import { GrantCommunityCommand, GrantProvinceCompanyCommand, GrantCityCompanyCommand, + GrantAuthProvinceCompanyCommand, + GrantAuthCityCompanyCommand, RevokeAuthorizationCommand, GrantMonthlyBypassCommand, ExemptLocalPercentageCheckCommand, @@ -241,12 +243,28 @@ export class AuthorizationApplicationService { } /** - * 管理员授权正式省公司 + * 管理员授权正式省公司(省区域) + * 需要验证团队内唯一性:同一推荐链上不能有重复的相同省份授权 */ async grantProvinceCompany(command: GrantProvinceCompanyCommand): Promise { const userId = UserId.create(command.userId, command.accountSequence) const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) + const regionCode = RegionCode.create(command.provinceCode) + // 1. 验证团队内唯一性(同一推荐链上不能有重复的相同省份授权) + const validation = await this.validatorService.validateAuthorizationRequest( + userId, + RoleType.PROVINCE_COMPANY, + regionCode, + this.referralRepository, + this.authorizationRepository, + ) + + if (!validation.isValid) { + throw new ApplicationError(validation.errorMessage!) + } + + // 2. 创建授权 const authorization = AuthorizationRole.createProvinceCompany({ userId, provinceCode: command.provinceCode, @@ -261,12 +279,28 @@ export class AuthorizationApplicationService { } /** - * 管理员授权正式市公司 + * 管理员授权正式市公司(市区域) + * 需要验证团队内唯一性:同一推荐链上不能有重复的相同城市授权 */ async grantCityCompany(command: GrantCityCompanyCommand): Promise { const userId = UserId.create(command.userId, command.accountSequence) const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) + const regionCode = RegionCode.create(command.cityCode) + // 1. 验证团队内唯一性(同一推荐链上不能有重复的相同城市授权) + const validation = await this.validatorService.validateAuthorizationRequest( + userId, + RoleType.CITY_COMPANY, + regionCode, + this.referralRepository, + this.authorizationRepository, + ) + + if (!validation.isValid) { + throw new ApplicationError(validation.errorMessage!) + } + + // 2. 创建授权 const authorization = AuthorizationRole.createCityCompany({ userId, cityCode: command.cityCode, @@ -280,6 +314,80 @@ export class AuthorizationApplicationService { authorization.clearDomainEvents() } + /** + * 管理员授权授权省公司(省团队) + * Admin直接授权,跳过用户申请流程 + * 需要验证团队内唯一性:同一推荐链上不能有重复的相同省份授权 + */ + async grantAuthProvinceCompany(command: GrantAuthProvinceCompanyCommand): Promise { + const userId = UserId.create(command.userId, command.accountSequence) + const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) + const regionCode = RegionCode.create(command.provinceCode) + + // 1. 验证团队内唯一性(同一推荐链上不能有重复的相同省份授权) + const validation = await this.validatorService.validateAuthorizationRequest( + userId, + RoleType.AUTH_PROVINCE_COMPANY, + regionCode, + this.referralRepository, + this.authorizationRepository, + ) + + if (!validation.isValid) { + throw new ApplicationError(validation.errorMessage!) + } + + // 2. 创建授权 + const authorization = AuthorizationRole.createAuthProvinceCompanyByAdmin({ + userId, + provinceCode: command.provinceCode, + provinceName: command.provinceName, + adminId, + skipAssessment: command.skipAssessment, + }) + + await this.authorizationRepository.save(authorization) + await this.eventPublisher.publishAll(authorization.domainEvents) + authorization.clearDomainEvents() + } + + /** + * 管理员授权授权市公司(市团队) + * Admin直接授权,跳过用户申请流程 + * 需要验证团队内唯一性:同一推荐链上不能有重复的相同城市授权 + */ + async grantAuthCityCompany(command: GrantAuthCityCompanyCommand): Promise { + const userId = UserId.create(command.userId, command.accountSequence) + const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) + const regionCode = RegionCode.create(command.cityCode) + + // 1. 验证团队内唯一性(同一推荐链上不能有重复的相同城市授权) + const validation = await this.validatorService.validateAuthorizationRequest( + userId, + RoleType.AUTH_CITY_COMPANY, + regionCode, + this.referralRepository, + this.authorizationRepository, + ) + + if (!validation.isValid) { + throw new ApplicationError(validation.errorMessage!) + } + + // 2. 创建授权 + const authorization = AuthorizationRole.createAuthCityCompanyByAdmin({ + userId, + cityCode: command.cityCode, + cityName: command.cityName, + adminId, + skipAssessment: command.skipAssessment, + }) + + 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 1c24a513..73976fd4 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 @@ -13,6 +13,8 @@ import { CommunityAuthRequestedEvent, AuthProvinceCompanyRequestedEvent, AuthCityCompanyRequestedEvent, + AuthProvinceCompanyGrantedEvent, + AuthCityCompanyGrantedEvent, ProvinceCompanyAuthorizedEvent, CityCompanyAuthorizedEvent, BenefitActivatedEvent, @@ -436,6 +438,98 @@ export class AuthorizationRole extends AggregateRoot { return auth } + // 工厂方法 - Admin授权省团队 + static createAuthProvinceCompanyByAdmin(params: { + userId: UserId + provinceCode: string + provinceName: string + adminId: AdminUserId + skipAssessment?: boolean + }): AuthorizationRole { + const skipAssessment = params.skipAssessment ?? false + const auth = new AuthorizationRole({ + authorizationId: AuthorizationId.generate(), + userId: params.userId, + roleType: RoleType.AUTH_PROVINCE_COMPANY, + regionCode: RegionCode.create(params.provinceCode), + regionName: params.provinceName, + status: AuthorizationStatus.AUTHORIZED, + displayTitle: `授权${params.provinceName}`, + authorizedAt: new Date(), + authorizedBy: params.adminId, + revokedAt: null, + revokedBy: null, + revokeReason: null, + assessmentConfig: AssessmentConfig.forAuthProvince(), + requireLocalPercentage: 5.0, + exemptFromPercentageCheck: false, + benefitActive: skipAssessment, + benefitActivatedAt: skipAssessment ? new Date() : null, + benefitDeactivatedAt: null, + currentMonthIndex: skipAssessment ? 1 : 0, + createdAt: new Date(), + updatedAt: new Date(), + }) + + auth.addDomainEvent( + new AuthProvinceCompanyGrantedEvent({ + authorizationId: auth.authorizationId.value, + userId: params.userId.value, + provinceCode: params.provinceCode, + provinceName: params.provinceName, + authorizedBy: params.adminId.value, + }), + ) + + return auth + } + + // 工厂方法 - Admin授权市团队 + static createAuthCityCompanyByAdmin(params: { + userId: UserId + cityCode: string + cityName: string + adminId: AdminUserId + skipAssessment?: boolean + }): AuthorizationRole { + const skipAssessment = params.skipAssessment ?? false + const auth = new AuthorizationRole({ + authorizationId: AuthorizationId.generate(), + userId: params.userId, + roleType: RoleType.AUTH_CITY_COMPANY, + regionCode: RegionCode.create(params.cityCode), + regionName: params.cityName, + status: AuthorizationStatus.AUTHORIZED, + displayTitle: `授权${params.cityName}`, + authorizedAt: new Date(), + authorizedBy: params.adminId, + revokedAt: null, + revokedBy: null, + revokeReason: null, + assessmentConfig: AssessmentConfig.forAuthCity(), + requireLocalPercentage: 5.0, + exemptFromPercentageCheck: false, + benefitActive: skipAssessment, + benefitActivatedAt: skipAssessment ? new Date() : null, + benefitDeactivatedAt: null, + currentMonthIndex: skipAssessment ? 1 : 0, + createdAt: new Date(), + updatedAt: new Date(), + }) + + auth.addDomainEvent( + new AuthCityCompanyGrantedEvent({ + authorizationId: auth.authorizationId.value, + userId: params.userId.value, + cityCode: params.cityCode, + cityName: params.cityName, + authorizedBy: params.adminId.value, + }), + ) + + return auth + } + // 核心领域行为 /** 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 66f6635a..f2576dcd 100644 --- a/backend/services/authorization-service/src/domain/events/authorization-events.ts +++ b/backend/services/authorization-service/src/domain/events/authorization-events.ts @@ -137,6 +137,56 @@ export class CityCompanyAuthorizedEvent extends DomainEvent { } } +// Admin授权省团队事件 +export class AuthProvinceCompanyGrantedEvent extends DomainEvent { + readonly eventType = 'authorization.auth_province.granted' + readonly aggregateId: string + readonly payload: { + authorizationId: string + userId: string + provinceCode: string + provinceName: string + authorizedBy: string + } + + constructor(data: { + authorizationId: string + userId: string + provinceCode: string + provinceName: string + authorizedBy: string + }) { + super() + this.aggregateId = data.authorizationId + this.payload = data + } +} + +// Admin授权市团队事件 +export class AuthCityCompanyGrantedEvent extends DomainEvent { + readonly eventType = 'authorization.auth_city.granted' + readonly aggregateId: string + readonly payload: { + authorizationId: string + userId: string + cityCode: string + cityName: string + authorizedBy: string + } + + constructor(data: { + authorizationId: string + userId: string + cityCode: string + cityName: string + authorizedBy: string + }) { + super() + this.aggregateId = data.authorizationId + this.payload = data + } +} + // 权益激活事件 export class BenefitActivatedEvent extends DomainEvent { readonly eventType = 'authorization.benefit.activated'