feat(authorization): add admin APIs for auth-province and auth-city company

- Add POST /admin/authorizations/auth-province-company for 省团队授权
- Add POST /admin/authorizations/auth-city-company for 市团队授权
- Add team uniqueness validation for all province/city authorization types
- Add domain events: AuthProvinceCompanyGrantedEvent, AuthCityCompanyGrantedEvent
- Add factory methods: createAuthProvinceCompanyByAdmin, createAuthCityCompanyByAdmin

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-11 21:46:44 -08:00
parent 582828b8be
commit 905725fc2d
10 changed files with 400 additions and 4 deletions

View File

@ -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: '市团队授权成功' }
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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'

View File

@ -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,
) {}
}

View File

@ -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,
) {}
}

View File

@ -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'

View File

@ -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<void> {
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<void> {
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<void> {
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<void> {
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()
}
/**
*
*/

View File

@ -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
}
// 核心领域行为
/**

View File

@ -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'