feat(authorization): 优化市/省团队自助申请逻辑
- 团队链区域唯一性:同一直推链上只阻止申请已被占用的城市/省份,非全部阻止 - 市/省团队互斥:同一用户只能拥有市团队或省团队之一 - 前端优化:显示已占用区域数量提示,选择时验证区域可用性 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
30f3487bd3
commit
428ac91737
|
|
@ -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[]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SelfApplyAuthorizationResponseDto> {
|
||||
// 检查是否已有该市的授权市公司
|
||||
// 检查用户是否已拥有省团队授权(市团队和省团队互斥,只能二选一)
|
||||
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<SelfApplyAuthorizationResponseDto> {
|
||||
// 检查是否已有该省的授权省公司
|
||||
// 检查用户是否已拥有市团队授权(市团队和省团队互斥,只能二选一)
|
||||
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<TeamChainAuthorizationHolderDto | undefined> {
|
||||
): Promise<TeamChainOccupiedRegionDto[]> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String, dynamic> json) {
|
||||
return TeamChainAuthorizationHolder(
|
||||
factory TeamChainOccupiedRegion.fromJson(Map<String, dynamic> 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<String> existingAuthorizations;
|
||||
/// 团队链中市团队授权持有者(如有)
|
||||
final TeamChainAuthorizationHolder? teamChainCityTeamHolder;
|
||||
/// 团队链中省团队授权持有者(如有)
|
||||
final TeamChainAuthorizationHolder? teamChainProvinceTeamHolder;
|
||||
/// 团队链中已被占用的市团队区域列表
|
||||
final List<TeamChainOccupiedRegion> teamChainOccupiedCities;
|
||||
/// 团队链中已被占用的省团队区域列表
|
||||
final List<TeamChainOccupiedRegion> 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<String, dynamic> json) {
|
||||
|
|
@ -386,14 +392,42 @@ class UserAuthorizationStatusResponse {
|
|||
existingAuthorizations: (json['existingAuthorizations'] as List<dynamic>?)
|
||||
?.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<dynamic>?)
|
||||
?.map((e) => TeamChainOccupiedRegion.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ?? [],
|
||||
teamChainOccupiedProvinces: (json['teamChainOccupiedProvinces'] as List<dynamic>?)
|
||||
?.map((e) => TeamChainOccupiedRegion.fromJson(e as Map<String, dynamic>))
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 自助申请授权响应
|
||||
|
|
|
|||
|
|
@ -80,11 +80,8 @@ class _AuthorizationApplyPageState
|
|||
/// 用户已有的授权
|
||||
List<String> _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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue