diff --git a/backend/services/authorization-service/src/api/controllers/index.ts b/backend/services/authorization-service/src/api/controllers/index.ts index c12cb097..11d61abb 100644 --- a/backend/services/authorization-service/src/api/controllers/index.ts +++ b/backend/services/authorization-service/src/api/controllers/index.ts @@ -1,3 +1,4 @@ export * from './authorization.controller' export * from './admin-authorization.controller' export * from './health.controller' +export * from './internal-authorization.controller' diff --git a/backend/services/authorization-service/src/api/controllers/internal-authorization.controller.ts b/backend/services/authorization-service/src/api/controllers/internal-authorization.controller.ts new file mode 100644 index 00000000..859ab295 --- /dev/null +++ b/backend/services/authorization-service/src/api/controllers/internal-authorization.controller.ts @@ -0,0 +1,118 @@ +import { Controller, Get, Query, Logger } from '@nestjs/common' +import { ApiTags, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger' +import { AuthorizationApplicationService } from '@/application/services' + +/** + * 内部授权 API - 供其他微服务调用 + * 无需 JWT 认证(服务间内部通信) + */ +@ApiTags('Internal Authorization API') +@Controller('authorization') +export class InternalAuthorizationController { + private readonly logger = new Logger(InternalAuthorizationController.name) + + constructor(private readonly applicationService: AuthorizationApplicationService) {} + + /** + * 查找用户推荐链中最近的社区授权用户 + * 用于 reward-service 分配社区权益 + */ + @Get('nearest-community') + @ApiOperation({ summary: '查找最近的社区授权用户(内部 API)' }) + @ApiQuery({ name: 'accountSequence', description: '用户的 accountSequence' }) + @ApiResponse({ + status: 200, + description: '返回最近社区授权用户的 accountSequence,如果没有则返回 null', + schema: { + type: 'object', + properties: { + accountSequence: { type: 'number', nullable: true }, + }, + }, + }) + async findNearestCommunity( + @Query('accountSequence') accountSequence: string, + ): Promise<{ accountSequence: number | null }> { + this.logger.debug(`[INTERNAL] findNearestCommunity: accountSequence=${accountSequence}`) + + const result = await this.applicationService.findNearestAuthorizedCommunity( + Number(accountSequence), + ) + + return { + accountSequence: result ? Number(result) : null, + } + } + + /** + * 查找用户推荐链中最近的省公司授权用户(匹配指定省份) + * 用于 reward-service 分配省团队权益 + */ + @Get('nearest-province') + @ApiOperation({ summary: '查找最近的省公司授权用户(内部 API)' }) + @ApiQuery({ name: 'accountSequence', description: '用户的 accountSequence' }) + @ApiQuery({ name: 'provinceCode', description: '省份代码' }) + @ApiResponse({ + status: 200, + description: '返回最近省公司授权用户的 accountSequence,如果没有则返回 null', + schema: { + type: 'object', + properties: { + accountSequence: { type: 'number', nullable: true }, + }, + }, + }) + async findNearestProvince( + @Query('accountSequence') accountSequence: string, + @Query('provinceCode') provinceCode: string, + ): Promise<{ accountSequence: number | null }> { + this.logger.debug( + `[INTERNAL] findNearestProvince: accountSequence=${accountSequence}, provinceCode=${provinceCode}`, + ) + + const result = await this.applicationService.findNearestAuthorizedProvince( + Number(accountSequence), + provinceCode, + ) + + return { + accountSequence: result ? Number(result) : null, + } + } + + /** + * 查找用户推荐链中最近的市公司授权用户(匹配指定城市) + * 用于 reward-service 分配市团队权益 + */ + @Get('nearest-city') + @ApiOperation({ summary: '查找最近的市公司授权用户(内部 API)' }) + @ApiQuery({ name: 'accountSequence', description: '用户的 accountSequence' }) + @ApiQuery({ name: 'cityCode', description: '城市代码' }) + @ApiResponse({ + status: 200, + description: '返回最近市公司授权用户的 accountSequence,如果没有则返回 null', + schema: { + type: 'object', + properties: { + accountSequence: { type: 'number', nullable: true }, + }, + }, + }) + async findNearestCity( + @Query('accountSequence') accountSequence: string, + @Query('cityCode') cityCode: string, + ): Promise<{ accountSequence: number | null }> { + this.logger.debug( + `[INTERNAL] findNearestCity: accountSequence=${accountSequence}, cityCode=${cityCode}`, + ) + + const result = await this.applicationService.findNearestAuthorizedCity( + Number(accountSequence), + cityCode, + ) + + return { + accountSequence: result ? Number(result) : null, + } + } +} diff --git a/backend/services/authorization-service/src/app.module.ts b/backend/services/authorization-service/src/app.module.ts index 7f5a277f..8dd3ec3e 100644 --- a/backend/services/authorization-service/src/app.module.ts +++ b/backend/services/authorization-service/src/app.module.ts @@ -27,7 +27,12 @@ import { AuthorizationApplicationService, REFERRAL_REPOSITORY, TEAM_STATISTICS_R import { MonthlyAssessmentScheduler } from '@/application/schedulers' // API -import { AuthorizationController, AdminAuthorizationController, HealthController } from '@/api/controllers' +import { + AuthorizationController, + AdminAuthorizationController, + HealthController, + InternalAuthorizationController, +} from '@/api/controllers' // Shared import { JwtStrategy } from '@/shared/strategies' @@ -59,7 +64,13 @@ const MockReferralRepository = { RedisModule, KafkaModule, ], - controllers: [AuthorizationController, AdminAuthorizationController, HealthController, EventConsumerController], + controllers: [ + AuthorizationController, + AdminAuthorizationController, + HealthController, + InternalAuthorizationController, + EventConsumerController, + ], providers: [ // Prisma PrismaService, 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 b3ffd349..a0360184 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 @@ -621,4 +621,127 @@ export class AuthorizationApplicationService { childCommunityCount: childCommunityAuths.length, } } + + /** + * 查找用户推荐链中最近的社区授权用户 + * 用于 reward-service 分配社区权益 + * @returns accountSequence of nearest community authorization holder, or null + */ + async findNearestAuthorizedCommunity(accountSequence: number): Promise { + this.logger.debug(`[findNearestAuthorizedCommunity] accountSequence=${accountSequence}`) + + // 获取用户的祖先链(推荐链) + const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(BigInt(accountSequence)) + + if (ancestorAccountSequences.length === 0) { + return null + } + + // 在祖先链中找最近的有社区授权的用户 + const ancestorCommunities = await this.authorizationRepository.findActiveCommunityByAccountSequences( + ancestorAccountSequences.map((seq) => BigInt(seq)), + ) + + if (ancestorCommunities.length === 0) { + return null + } + + // 按祖先链顺序找第一个匹配的 + for (const ancestorSeq of ancestorAccountSequences) { + const found = ancestorCommunities.find( + (auth) => Number(auth.userId.accountSequence) === ancestorSeq, + ) + if (found) { + return found.userId.accountSequence + } + } + + return null + } + + /** + * 查找用户推荐链中最近的省公司授权用户(匹配指定省份) + * 用于 reward-service 分配省团队权益 + * @returns accountSequence of nearest province authorization holder, or null + */ + async findNearestAuthorizedProvince( + accountSequence: number, + provinceCode: string, + ): Promise { + this.logger.debug( + `[findNearestAuthorizedProvince] accountSequence=${accountSequence}, provinceCode=${provinceCode}`, + ) + + // 获取用户的祖先链(推荐链) + const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(BigInt(accountSequence)) + + if (ancestorAccountSequences.length === 0) { + return null + } + + // 在祖先链中找最近的有省公司授权且匹配省份代码的用户 + const ancestorProvinces = await this.authorizationRepository.findActiveProvinceByAccountSequencesAndRegion( + ancestorAccountSequences.map((seq) => BigInt(seq)), + provinceCode, + ) + + if (ancestorProvinces.length === 0) { + return null + } + + // 按祖先链顺序找第一个匹配的 + for (const ancestorSeq of ancestorAccountSequences) { + const found = ancestorProvinces.find( + (auth) => Number(auth.userId.accountSequence) === ancestorSeq, + ) + if (found) { + return found.userId.accountSequence + } + } + + return null + } + + /** + * 查找用户推荐链中最近的市公司授权用户(匹配指定城市) + * 用于 reward-service 分配市团队权益 + * @returns accountSequence of nearest city authorization holder, or null + */ + async findNearestAuthorizedCity( + accountSequence: number, + cityCode: string, + ): Promise { + this.logger.debug( + `[findNearestAuthorizedCity] accountSequence=${accountSequence}, cityCode=${cityCode}`, + ) + + // 获取用户的祖先链(推荐链) + const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(BigInt(accountSequence)) + + if (ancestorAccountSequences.length === 0) { + return null + } + + // 在祖先链中找最近的有市公司授权且匹配城市代码的用户 + const ancestorCities = await this.authorizationRepository.findActiveCityByAccountSequencesAndRegion( + ancestorAccountSequences.map((seq) => BigInt(seq)), + cityCode, + ) + + if (ancestorCities.length === 0) { + return null + } + + // 按祖先链顺序找第一个匹配的 + for (const ancestorSeq of ancestorAccountSequences) { + const found = ancestorCities.find( + (auth) => Number(auth.userId.accountSequence) === ancestorSeq, + ) + if (found) { + return found.userId.accountSequence + } + } + + return null + } } diff --git a/backend/services/authorization-service/src/domain/repositories/authorization-role.repository.ts b/backend/services/authorization-service/src/domain/repositories/authorization-role.repository.ts index d18be749..582054f3 100644 --- a/backend/services/authorization-service/src/domain/repositories/authorization-role.repository.ts +++ b/backend/services/authorization-service/src/domain/repositories/authorization-role.repository.ts @@ -28,4 +28,18 @@ export interface IAuthorizationRoleRepository { * 批量查询指定 accountSequence 列表中具有活跃社区授权的用户 */ findActiveCommunityByAccountSequences(accountSequences: bigint[]): Promise + /** + * 批量查询指定 accountSequence 列表中具有活跃省公司授权(且匹配省份代码)的用户 + */ + findActiveProvinceByAccountSequencesAndRegion( + accountSequences: bigint[], + provinceCode: string, + ): Promise + /** + * 批量查询指定 accountSequence 列表中具有活跃市公司授权(且匹配城市代码)的用户 + */ + findActiveCityByAccountSequencesAndRegion( + accountSequences: bigint[], + cityCode: string, + ): Promise } diff --git a/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts b/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts index edbf1a9f..143927b2 100644 --- a/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts +++ b/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts @@ -195,6 +195,48 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi return records.map((record) => this.toDomain(record)) } + async findActiveProvinceByAccountSequencesAndRegion( + accountSequences: bigint[], + provinceCode: string, + ): Promise { + if (accountSequences.length === 0) { + return [] + } + + const records = await this.prisma.authorizationRole.findMany({ + where: { + accountSequence: { in: accountSequences }, + roleType: RoleType.PROVINCE_COMPANY, + regionCode: provinceCode, + status: AuthorizationStatus.AUTHORIZED, + benefitActive: true, + }, + orderBy: { accountSequence: 'asc' }, + }) + return records.map((record) => this.toDomain(record)) + } + + async findActiveCityByAccountSequencesAndRegion( + accountSequences: bigint[], + cityCode: string, + ): Promise { + if (accountSequences.length === 0) { + return [] + } + + const records = await this.prisma.authorizationRole.findMany({ + where: { + accountSequence: { in: accountSequences }, + roleType: RoleType.CITY_COMPANY, + regionCode: cityCode, + status: AuthorizationStatus.AUTHORIZED, + benefitActive: true, + }, + orderBy: { accountSequence: 'asc' }, + }) + return records.map((record) => this.toDomain(record)) + } + private toDomain(record: any): AuthorizationRole { const props: AuthorizationRoleProps = { authorizationId: AuthorizationId.create(record.id), diff --git a/backend/services/referral-service/src/api/controllers/referral.controller.ts b/backend/services/referral-service/src/api/controllers/referral.controller.ts index f6ec4d93..13a03a5e 100644 --- a/backend/services/referral-service/src/api/controllers/referral.controller.ts +++ b/backend/services/referral-service/src/api/controllers/referral.controller.ts @@ -8,6 +8,7 @@ import { UseGuards, HttpCode, HttpStatus, + Inject, } from '@nestjs/common'; import { ApiTags, @@ -29,11 +30,23 @@ import { } from '../dto'; import { CreateReferralRelationshipCommand } from '../../application/commands'; import { GetUserReferralInfoQuery, GetDirectReferralsQuery } from '../../application/queries'; +import { + REFERRAL_RELATIONSHIP_REPOSITORY, + IReferralRelationshipRepository, + TEAM_STATISTICS_REPOSITORY, + ITeamStatisticsRepository, +} from '../../domain'; @ApiTags('Referral') @Controller('referral') export class ReferralController { - constructor(private readonly referralService: ReferralService) {} + constructor( + private readonly referralService: ReferralService, + @Inject(REFERRAL_RELATIONSHIP_REPOSITORY) + private readonly referralRepo: IReferralRelationshipRepository, + @Inject(TEAM_STATISTICS_REPOSITORY) + private readonly teamStatsRepo: ITeamStatisticsRepository, + ) {} @Get('me') @UseGuards(JwtAuthGuard) @@ -82,6 +95,58 @@ export class ReferralController { }; } + /** + * 获取用户的推荐链(内部API,供 reward-service 调用) + * 返回直接推荐人及其认种状态 + */ + @Get('chain/:userId') + @ApiOperation({ summary: '获取用户推荐链(内部API)' }) + @ApiParam({ name: 'userId', description: '用户ID' }) + @ApiResponse({ + status: 200, + description: '推荐链数据', + schema: { + type: 'object', + properties: { + ancestors: { + type: 'array', + items: { + type: 'object', + properties: { + userId: { type: 'string' }, + hasPlanted: { type: 'boolean' }, + }, + }, + }, + }, + }, + }) + async getReferralChainForReward( + @Param('userId') userId: string, + ): Promise<{ ancestors: Array<{ userId: bigint; hasPlanted: boolean }> }> { + const userIdBigInt = BigInt(userId); + + // 获取用户的推荐关系 + const relationship = await this.referralRepo.findByUserId(userIdBigInt); + if (!relationship || !relationship.referrerId) { + return { ancestors: [] }; + } + + // 获取直接推荐人的认种状态 + const referrerId = relationship.referrerId; + const referrerStats = await this.teamStatsRepo.findByUserId(referrerId); + const hasPlanted = referrerStats ? referrerStats.personalPlantingCount > 0 : false; + + return { + ancestors: [ + { + userId: referrerId, + hasPlanted, + }, + ], + }; + } + @Post() @ApiOperation({ summary: '创建推荐关系 (内部接口)' }) @ApiResponse({ status: 201, description: '创建成功' }) diff --git a/backend/services/reward-service/src/infrastructure/external/authorization-service/authorization-service.client.ts b/backend/services/reward-service/src/infrastructure/external/authorization-service/authorization-service.client.ts index 27bd54d2..aaaa5ac3 100644 --- a/backend/services/reward-service/src/infrastructure/external/authorization-service/authorization-service.client.ts +++ b/backend/services/reward-service/src/infrastructure/external/authorization-service/authorization-service.client.ts @@ -14,7 +14,7 @@ export class AuthorizationServiceClient implements IAuthorizationServiceClient { async findNearestAuthorizedProvince(userId: bigint, provinceCode: string): Promise { try { const response = await fetch( - `${this.baseUrl}/authorization/nearest-province?userId=${userId}&provinceCode=${provinceCode}`, + `${this.baseUrl}/authorization/nearest-province?accountSequence=${userId}&provinceCode=${provinceCode}`, ); if (!response.ok) { @@ -23,7 +23,7 @@ export class AuthorizationServiceClient implements IAuthorizationServiceClient { } const data = await response.json(); - return data.userId ? BigInt(data.userId) : null; + return data.accountSequence ? BigInt(data.accountSequence) : null; } catch (error) { this.logger.error(`Error finding nearest authorized province:`, error); return null; @@ -33,7 +33,7 @@ export class AuthorizationServiceClient implements IAuthorizationServiceClient { async findNearestAuthorizedCity(userId: bigint, cityCode: string): Promise { try { const response = await fetch( - `${this.baseUrl}/authorization/nearest-city?userId=${userId}&cityCode=${cityCode}`, + `${this.baseUrl}/authorization/nearest-city?accountSequence=${userId}&cityCode=${cityCode}`, ); if (!response.ok) { @@ -42,7 +42,7 @@ export class AuthorizationServiceClient implements IAuthorizationServiceClient { } const data = await response.json(); - return data.userId ? BigInt(data.userId) : null; + return data.accountSequence ? BigInt(data.accountSequence) : null; } catch (error) { this.logger.error(`Error finding nearest authorized city:`, error); return null; @@ -52,7 +52,7 @@ export class AuthorizationServiceClient implements IAuthorizationServiceClient { async findNearestCommunity(userId: bigint): Promise { try { const response = await fetch( - `${this.baseUrl}/authorization/nearest-community?userId=${userId}`, + `${this.baseUrl}/authorization/nearest-community?accountSequence=${userId}`, ); if (!response.ok) { @@ -61,7 +61,7 @@ export class AuthorizationServiceClient implements IAuthorizationServiceClient { } const data = await response.json(); - return data.userId ? BigInt(data.userId) : null; + return data.accountSequence ? BigInt(data.accountSequence) : null; } catch (error) { this.logger.error(`Error finding nearest community:`, error); return null;