diff --git a/backend/services/authorization-service/src/api/dto/request/self-apply-authorization.dto.ts b/backend/services/authorization-service/src/api/dto/request/self-apply-authorization.dto.ts index 9ebba9e9..0adacbab 100644 --- a/backend/services/authorization-service/src/api/dto/request/self-apply-authorization.dto.ts +++ b/backend/services/authorization-service/src/api/dto/request/self-apply-authorization.dto.ts @@ -95,14 +95,21 @@ export class SelfApplyAuthorizationResponseDto { } /** - * 团队链中已有授权的持有者信息 + * 团队链中已占用的区域信息 + * 包含持有者信息和区域信息 */ -export class TeamChainAuthorizationHolderDto { +export class TeamChainOccupiedRegionDto { @ApiProperty({ description: '持有者账号序号' }) accountSequence: string @ApiProperty({ description: '持有者昵称' }) nickname: string + + @ApiProperty({ description: '区域代码(城市代码或省份代码)' }) + regionCode: string + + @ApiProperty({ description: '区域名称(城市名称或省份名称)' }) + regionName: string } /** @@ -118,9 +125,9 @@ export class UserAuthorizationStatusResponseDto { @ApiProperty({ description: '已拥有的授权类型列表' }) existingAuthorizations: string[] - @ApiPropertyOptional({ description: '团队链中市团队授权持有者(如有)' }) - teamChainCityTeamHolder?: TeamChainAuthorizationHolderDto + @ApiProperty({ description: '团队链中已被占用的市团队区域列表' }) + teamChainOccupiedCities: TeamChainOccupiedRegionDto[] - @ApiPropertyOptional({ description: '团队链中省团队授权持有者(如有)' }) - teamChainProvinceTeamHolder?: TeamChainAuthorizationHolderDto + @ApiProperty({ description: '团队链中已被占用的省团队区域列表' }) + teamChainOccupiedProvinces: TeamChainOccupiedRegionDto[] } 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 18cd0867..9ed4ddf6 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 @@ -45,7 +45,7 @@ import { SelfApplyAuthorizationResponseDto, UserAuthorizationStatusResponseDto, SelfApplyAuthorizationType, - TeamChainAuthorizationHolderDto, + TeamChainOccupiedRegionDto, } from '@/api/dto/request/self-apply-authorization.dto' import { AuthorizationDTO, StickmanRankingDTO, CommunityHierarchyDTO } from '@/application/dto' @@ -2786,12 +2786,12 @@ export class AuthorizationApplicationService { .filter(auth => auth.status !== AuthorizationStatus.REVOKED) .map(auth => this.mapRoleTypeToDisplayName(auth.roleType)) - // 3. 检查团队链中是否有市团队/省团队授权持有者 - const teamChainCityTeamHolder = await this.findTeamChainAuthorizationHolder( + // 3. 查找团队链中已被占用的城市和省份 + const teamChainOccupiedCities = await this.findTeamChainOccupiedRegions( accountSequence, RoleType.AUTH_CITY_COMPANY, ) - const teamChainProvinceTeamHolder = await this.findTeamChainAuthorizationHolder( + const teamChainOccupiedProvinces = await this.findTeamChainOccupiedRegions( accountSequence, RoleType.AUTH_PROVINCE_COMPANY, ) @@ -2800,8 +2800,8 @@ export class AuthorizationApplicationService { hasPlanted, plantedCount, existingAuthorizations, - teamChainCityTeamHolder, - teamChainProvinceTeamHolder, + teamChainOccupiedCities, + teamChainOccupiedProvinces, } } @@ -2910,7 +2910,16 @@ export class AuthorizationApplicationService { private async processCityTeamApplication( command: SelfApplyAuthorizationCommand, ): Promise { - // 检查是否已有该市的授权市公司 + // 检查用户是否已拥有省团队授权(市团队和省团队互斥,只能二选一) + const userAuthorizations = await this.authorizationRepository.findByAccountSequence(command.accountSequence) + const hasProvinceTeam = userAuthorizations.some( + auth => auth.roleType === RoleType.AUTH_PROVINCE_COMPANY && auth.status !== AuthorizationStatus.REVOKED, + ) + if (hasProvinceTeam) { + throw new ApplicationError('您已拥有省团队授权,市团队和省团队只能二选一') + } + + // 检查是否已有该市的授权市公司(全局唯一性) const existingList = await this.authorizationRepository.findActiveByRoleTypeAndRegion( RoleType.AUTH_CITY_COMPANY, RegionCode.create(command.cityCode!), @@ -2919,6 +2928,16 @@ export class AuthorizationApplicationService { throw new ApplicationError('该市已有授权市公司') } + // 检查团队链中是否已有人申请该城市(团队链唯一性) + const occupiedCities = await this.findTeamChainOccupiedRegions( + command.accountSequence, + RoleType.AUTH_CITY_COMPANY, + ) + const isCityOccupiedInChain = occupiedCities.some(r => r.regionCode === command.cityCode) + if (isCityOccupiedInChain) { + throw new ApplicationError(`您的团队链中已有人申请了「${command.cityName}」的市团队授权`) + } + // 创建授权市公司授权(自助申请直接生效,状态为 AUTHORIZED) const userId = UserId.create(command.userId, command.accountSequence) const authorization = AuthorizationRole.createSelfAppliedAuthCityCompany({ @@ -2952,7 +2971,16 @@ export class AuthorizationApplicationService { private async processProvinceTeamApplication( command: SelfApplyAuthorizationCommand, ): Promise { - // 检查是否已有该省的授权省公司 + // 检查用户是否已拥有市团队授权(市团队和省团队互斥,只能二选一) + const userAuthorizations = await this.authorizationRepository.findByAccountSequence(command.accountSequence) + const hasCityTeam = userAuthorizations.some( + auth => auth.roleType === RoleType.AUTH_CITY_COMPANY && auth.status !== AuthorizationStatus.REVOKED, + ) + if (hasCityTeam) { + throw new ApplicationError('您已拥有市团队授权,市团队和省团队只能二选一') + } + + // 检查是否已有该省的授权省公司(全局唯一性) const existingList = await this.authorizationRepository.findActiveByRoleTypeAndRegion( RoleType.AUTH_PROVINCE_COMPANY, RegionCode.create(command.provinceCode!), @@ -2961,6 +2989,16 @@ export class AuthorizationApplicationService { throw new ApplicationError('该省已有授权省公司') } + // 检查团队链中是否已有人申请该省份(团队链唯一性) + const occupiedProvinces = await this.findTeamChainOccupiedRegions( + command.accountSequence, + RoleType.AUTH_PROVINCE_COMPANY, + ) + const isProvinceOccupiedInChain = occupiedProvinces.some(r => r.regionCode === command.provinceCode) + if (isProvinceOccupiedInChain) { + throw new ApplicationError(`您的团队链中已有人申请了「${command.provinceName}」的省团队授权`) + } + // 创建授权省公司授权(自助申请直接生效,状态为 AUTHORIZED) const userId = UserId.create(command.userId, command.accountSequence) const authorization = AuthorizationRole.createSelfAppliedAuthProvinceCompany({ @@ -2989,41 +3027,47 @@ export class AuthorizationApplicationService { } /** - * 查找团队链中指定类型的授权持有者 - * 遍历用户的祖先链(推荐链),查找是否有人持有指定类型的授权 + * 查找团队链中指定类型的所有已占用区域 + * 遍历用户的祖先链(推荐链),收集所有已被占用的城市/省份 + * + * @returns 已占用区域列表(包含持有者信息和区域信息) */ - private async findTeamChainAuthorizationHolder( + private async findTeamChainOccupiedRegions( accountSequence: string, roleType: RoleType.AUTH_CITY_COMPANY | RoleType.AUTH_PROVINCE_COMPANY, - ): Promise { + ): Promise { + const occupiedRegions: TeamChainOccupiedRegionDto[] = [] + try { // 获取用户的祖先链(推荐链) const ancestorChain = await this.referralServiceClient.getReferralChain(accountSequence) if (!ancestorChain || ancestorChain.length === 0) { - return undefined + return occupiedRegions } - // 遍历祖先链,查找持有该授权的用户 + // 遍历祖先链,收集所有持有该类型授权的区域 for (const ancestorAccountSeq of ancestorChain) { const authorizations = await this.authorizationRepository.findByAccountSequence(ancestorAccountSeq) - const matchingAuth = authorizations.find( + const matchingAuths = authorizations.filter( auth => auth.roleType === roleType && auth.status !== AuthorizationStatus.REVOKED, ) - if (matchingAuth) { + for (const auth of matchingAuths) { // 获取用户昵称 - const userInfo = await this.identityServiceClient.getUserInfo(matchingAuth.userId.value) - return { + const userInfo = await this.identityServiceClient.getUserInfo(auth.userId.value) + occupiedRegions.push({ accountSequence: ancestorAccountSeq, nickname: userInfo?.nickname || `用户${ancestorAccountSeq}`, - } + regionCode: auth.regionCode?.value || '', + regionName: auth.regionName || '', + }) } } - return undefined + return occupiedRegions } catch (error) { - this.logger.error(`[findTeamChainAuthorizationHolder] Error: ${error}`) - return undefined + this.logger.error(`[findTeamChainOccupiedRegions] Error: ${error}`) + return occupiedRegions } } diff --git a/frontend/mobile-app/lib/core/services/authorization_service.dart b/frontend/mobile-app/lib/core/services/authorization_service.dart index ecf9bd7a..073e71db 100644 --- a/frontend/mobile-app/lib/core/services/authorization_service.dart +++ b/frontend/mobile-app/lib/core/services/authorization_service.dart @@ -343,20 +343,26 @@ extension SelfApplyAuthorizationTypeExtension on SelfApplyAuthorizationType { } } -/// 团队链中授权持有者信息 -class TeamChainAuthorizationHolder { +/// 团队链中已被占用的区域信息 +class TeamChainOccupiedRegion { final String accountSequence; final String nickname; + final String regionCode; + final String regionName; - TeamChainAuthorizationHolder({ + TeamChainOccupiedRegion({ required this.accountSequence, required this.nickname, + required this.regionCode, + required this.regionName, }); - factory TeamChainAuthorizationHolder.fromJson(Map json) { - return TeamChainAuthorizationHolder( + factory TeamChainOccupiedRegion.fromJson(Map json) { + return TeamChainOccupiedRegion( accountSequence: json['accountSequence'] ?? '', nickname: json['nickname'] ?? '', + regionCode: json['regionCode'] ?? '', + regionName: json['regionName'] ?? '', ); } } @@ -366,17 +372,17 @@ class UserAuthorizationStatusResponse { final bool hasPlanted; final int plantedCount; final List existingAuthorizations; - /// 团队链中市团队授权持有者(如有) - final TeamChainAuthorizationHolder? teamChainCityTeamHolder; - /// 团队链中省团队授权持有者(如有) - final TeamChainAuthorizationHolder? teamChainProvinceTeamHolder; + /// 团队链中已被占用的市团队区域列表 + final List teamChainOccupiedCities; + /// 团队链中已被占用的省团队区域列表 + final List teamChainOccupiedProvinces; UserAuthorizationStatusResponse({ required this.hasPlanted, required this.plantedCount, required this.existingAuthorizations, - this.teamChainCityTeamHolder, - this.teamChainProvinceTeamHolder, + this.teamChainOccupiedCities = const [], + this.teamChainOccupiedProvinces = const [], }); factory UserAuthorizationStatusResponse.fromJson(Map json) { @@ -386,14 +392,42 @@ class UserAuthorizationStatusResponse { existingAuthorizations: (json['existingAuthorizations'] as List?) ?.map((e) => e.toString()) .toList() ?? [], - teamChainCityTeamHolder: json['teamChainCityTeamHolder'] != null - ? TeamChainAuthorizationHolder.fromJson(json['teamChainCityTeamHolder']) - : null, - teamChainProvinceTeamHolder: json['teamChainProvinceTeamHolder'] != null - ? TeamChainAuthorizationHolder.fromJson(json['teamChainProvinceTeamHolder']) - : null, + teamChainOccupiedCities: (json['teamChainOccupiedCities'] as List?) + ?.map((e) => TeamChainOccupiedRegion.fromJson(e as Map)) + .toList() ?? [], + teamChainOccupiedProvinces: (json['teamChainOccupiedProvinces'] as List?) + ?.map((e) => TeamChainOccupiedRegion.fromJson(e as Map)) + .toList() ?? [], ); } + + /// 检查指定城市是否在团队链中已被占用 + bool isCityOccupied(String cityCode) { + return teamChainOccupiedCities.any((r) => r.regionCode == cityCode); + } + + /// 检查指定省份是否在团队链中已被占用 + bool isProvinceOccupied(String provinceCode) { + return teamChainOccupiedProvinces.any((r) => r.regionCode == provinceCode); + } + + /// 获取占用指定城市的持有者信息 + TeamChainOccupiedRegion? getCityOccupier(String cityCode) { + try { + return teamChainOccupiedCities.firstWhere((r) => r.regionCode == cityCode); + } catch (_) { + return null; + } + } + + /// 获取占用指定省份的持有者信息 + TeamChainOccupiedRegion? getProvinceOccupier(String provinceCode) { + try { + return teamChainOccupiedProvinces.firstWhere((r) => r.regionCode == provinceCode); + } catch (_) { + return null; + } + } } /// 自助申请授权响应 diff --git a/frontend/mobile-app/lib/features/authorization/presentation/pages/authorization_apply_page.dart b/frontend/mobile-app/lib/features/authorization/presentation/pages/authorization_apply_page.dart index 82b25b8c..56a2d6af 100644 --- a/frontend/mobile-app/lib/features/authorization/presentation/pages/authorization_apply_page.dart +++ b/frontend/mobile-app/lib/features/authorization/presentation/pages/authorization_apply_page.dart @@ -80,11 +80,8 @@ class _AuthorizationApplyPageState /// 用户已有的授权 List _existingAuthorizations = []; - /// 团队链中市团队授权持有者 - TeamChainAuthorizationHolder? _teamChainCityTeamHolder; - - /// 团队链中省团队授权持有者 - TeamChainAuthorizationHolder? _teamChainProvinceTeamHolder; + /// 用户授权状态完整响应(包含团队链已占用区域信息) + UserAuthorizationStatusResponse? _authorizationStatus; /// 保存的省市信息(来自认种时选择) String? _savedProvinceName; @@ -138,8 +135,7 @@ class _AuthorizationApplyPageState _hasPlanted = status.hasPlanted; _plantedCount = status.plantedCount; _existingAuthorizations = status.existingAuthorizations; - _teamChainCityTeamHolder = status.teamChainCityTeamHolder; - _teamChainProvinceTeamHolder = status.teamChainProvinceTeamHolder; + _authorizationStatus = status; _isLoading = false; }); } @@ -436,6 +432,17 @@ class _AuthorizationApplyPageState ); return; } + // 检查该城市是否已被团队链占用 + if (_authorizationStatus != null && _authorizationStatus!.isCityOccupied(tempCityCode!)) { + final occupier = _authorizationStatus!.getCityOccupier(tempCityCode!); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('该城市已被团队中的 ${occupier?.nickname ?? "其他用户"} 申请,请选择其他城市'), + backgroundColor: Colors.red, + ), + ); + return; + } } else if (type == AuthorizationType.provinceTeam) { if (tempProvinceCode == null || tempProvinceName == null) { ScaffoldMessenger.of(context).showSnackBar( @@ -446,6 +453,17 @@ class _AuthorizationApplyPageState ); return; } + // 检查该省份是否已被团队链占用 + if (_authorizationStatus != null && _authorizationStatus!.isProvinceOccupied(tempProvinceCode!)) { + final occupier = _authorizationStatus!.getProvinceOccupier(tempProvinceCode!); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('该省份已被团队中的 ${occupier?.nickname ?? "其他用户"} 申请,请选择其他省份'), + backgroundColor: Colors.red, + ), + ); + return; + } } Navigator.of(dialogContext).pop({ @@ -855,17 +873,27 @@ class _AuthorizationApplyPageState final isSelected = _selectedType == type; final isAlreadyHas = _existingAuthorizations.contains(type.displayName); - // 检查团队链中是否有该类型授权持有者 - TeamChainAuthorizationHolder? teamChainHolder; - if (type == AuthorizationType.cityTeam) { - teamChainHolder = _teamChainCityTeamHolder; - } else if (type == AuthorizationType.provinceTeam) { - teamChainHolder = _teamChainProvinceTeamHolder; + // 获取团队链中已占用的区域数量(用于显示提示) + int occupiedCount = 0; + if (type == AuthorizationType.cityTeam && _authorizationStatus != null) { + occupiedCount = _authorizationStatus!.teamChainOccupiedCities.length; + } else if (type == AuthorizationType.provinceTeam && _authorizationStatus != null) { + occupiedCount = _authorizationStatus!.teamChainOccupiedProvinces.length; } - final isTeamChainBlocked = teamChainHolder != null; - // 综合判断是否禁用 - final isDisabled = isAlreadyHas || isTeamChainBlocked; + // 检查市团队和省团队互斥:已有其中一个时不能再申请另一个 + bool isMutuallyExcluded = false; + String mutuallyExcludedReason = ''; + if (type == AuthorizationType.cityTeam && _existingAuthorizations.contains('省团队')) { + isMutuallyExcluded = true; + mutuallyExcludedReason = '已拥有省团队,市团队和省团队只能二选一'; + } else if (type == AuthorizationType.provinceTeam && _existingAuthorizations.contains('市团队')) { + isMutuallyExcluded = true; + mutuallyExcludedReason = '已拥有市团队,市团队和省团队只能二选一'; + } + + // 综合判断禁用条件:已拥有该授权 或 互斥限制 + final isDisabled = isAlreadyHas || isMutuallyExcluded; return GestureDetector( onTap: isDisabled @@ -947,11 +975,23 @@ class _AuthorizationApplyPageState : const Color(0xFF745D43), ), ), - // 显示团队链授权持有者信息 - if (isTeamChainBlocked) ...[ + // 显示团队链中已占用区域提示 + if (occupiedCount > 0 && !isDisabled) ...[ const SizedBox(height: 8), Text( - '团队中已有人拥有此权益:${teamChainHolder!.nickname}(${teamChainHolder.accountSequence})', + '团队链中已有 $occupiedCount 个${type == AuthorizationType.cityTeam ? '城市' : '省份'}被申请,请选择其他区域', + style: const TextStyle( + fontSize: 12, + fontFamily: 'Inter', + color: Color(0xFFFF9800), + ), + ), + ], + // 显示互斥限制提示 + if (isMutuallyExcluded) ...[ + const SizedBox(height: 8), + Text( + mutuallyExcludedReason, style: const TextStyle( fontSize: 12, fontFamily: 'Inter', @@ -979,7 +1019,8 @@ class _AuthorizationApplyPageState ), ), ) - else if (isTeamChainBlocked) + // 互斥不可申请标签 + else if (isMutuallyExcluded) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration(