diff --git a/backend/services/docker-compose.yml b/backend/services/docker-compose.yml index 15ae7079..c51bc5cc 100644 --- a/backend/services/docker-compose.yml +++ b/backend/services/docker-compose.yml @@ -275,6 +275,7 @@ services: - KAFKA_BROKERS=kafka:29092 - KAFKA_CLIENT_ID=referral-service - KAFKA_GROUP_ID=referral-service-group + - AUTHORIZATION_SERVICE_URL=http://rwa-authorization-service:3009 depends_on: postgres: condition: service_healthy @@ -282,6 +283,8 @@ services: condition: service_healthy kafka: condition: service_started + authorization-service: + condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3004/api/v1/health"] interval: 30s 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 3949fd9d..4789da30 100644 --- a/backend/services/referral-service/src/api/controllers/referral.controller.ts +++ b/backend/services/referral-service/src/api/controllers/referral.controller.ts @@ -9,6 +9,7 @@ import { HttpCode, HttpStatus, Inject, + Logger, } from '@nestjs/common'; import { ApiTags, @@ -36,6 +37,7 @@ import { TEAM_STATISTICS_REPOSITORY, ITeamStatisticsRepository, } from '../../domain'; +import { AuthorizationServiceClient } from '../../infrastructure/external'; @ApiTags('Referral') @Controller('referral') @@ -181,7 +183,12 @@ export class ReferralController { @ApiTags('Internal Referral API') @Controller('referrals') export class InternalReferralController { - constructor(private readonly referralService: ReferralService) {} + private readonly logger = new Logger(InternalReferralController.name); + + constructor( + private readonly referralService: ReferralService, + private readonly authorizationClient: AuthorizationServiceClient, + ) {} @Get(':accountSequence/context') @ApiOperation({ summary: '获取用户推荐上下文信息(内部API)' }) @@ -192,19 +199,37 @@ export class InternalReferralController { @Query('provinceCode') provinceCode: string, @Query('cityCode') cityCode: string, ) { - // 获取用户的推荐链 - const query = new GetUserReferralInfoQuery(Number(accountSequence)); + const accountSeqNum = Number(accountSequence); + + // 1. 获取用户的推荐链 + const query = new GetUserReferralInfoQuery(accountSeqNum); const referralInfo = await this.referralService.getUserReferralInfo(query); - // 返回推荐上下文信息 - // 目前返回基础信息,后续可以扩展省市授权等信息 + // 2. 并行查询授权信息(省/市/社区) + // 使用 fallback 机制:如果 authorization-service 不可用,返回 null + const authorizations = await this.authorizationClient.findAllNearestAuthorizations( + accountSeqNum, + provinceCode, + cityCode, + ); + + this.logger.debug( + `[getReferralContext] accountSequence=${accountSequence}, ` + + `provinceCode=${provinceCode}, cityCode=${cityCode}, ` + + `referrerId=${referralInfo.referrerId}, ` + + `nearestCommunity=${authorizations.nearestCommunity}, ` + + `nearestProvinceAuth=${authorizations.nearestProvinceAuth}, ` + + `nearestCityAuth=${authorizations.nearestCityAuth}`, + ); + + // 3. 返回完整的推荐上下文信息 return { accountSequence, referralChain: referralInfo.referrerId ? [referralInfo.referrerId] : [], referrerId: referralInfo.referrerId, - nearestProvinceAuth: null, // 省代账户ID - 需要后续实现 - nearestCityAuth: null, // 市代账户ID - 需要后续实现 - nearestCommunity: null, // 社区账户ID - 需要后续实现 + nearestProvinceAuth: authorizations.nearestProvinceAuth?.toString() ?? null, + nearestCityAuth: authorizations.nearestCityAuth?.toString() ?? null, + nearestCommunity: authorizations.nearestCommunity?.toString() ?? null, }; } } diff --git a/backend/services/referral-service/src/infrastructure/external/authorization-service.client.ts b/backend/services/referral-service/src/infrastructure/external/authorization-service.client.ts new file mode 100644 index 00000000..f25bde87 --- /dev/null +++ b/backend/services/referral-service/src/infrastructure/external/authorization-service.client.ts @@ -0,0 +1,178 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { HttpService } from '@nestjs/axios'; +import { firstValueFrom, timeout, catchError } from 'rxjs'; +import { of } from 'rxjs'; + +export interface NearestAuthorizationResult { + accountSequence: number | null; +} + +/** + * Authorization Service 客户端 + * 用于查询用户推荐链中最近的省/市/社区授权用户 + */ +@Injectable() +export class AuthorizationServiceClient { + private readonly logger = new Logger(AuthorizationServiceClient.name); + private readonly baseUrl: string; + private readonly timeoutMs: number; + + constructor( + private readonly configService: ConfigService, + private readonly httpService: HttpService, + ) { + this.baseUrl = + this.configService.get('AUTHORIZATION_SERVICE_URL') || + 'http://localhost:3009'; + this.timeoutMs = this.configService.get('AUTHORIZATION_SERVICE_TIMEOUT_MS') || 5000; + } + + /** + * 查找用户推荐链中最近的社区授权用户 + * @param accountSequence 用户的 accountSequence + * @returns 最近社区授权用户的 accountSequence,如果没有则返回 null + */ + async findNearestCommunity(accountSequence: number): Promise { + try { + const response = await firstValueFrom( + this.httpService + .get( + `${this.baseUrl}/api/v1/authorization/nearest-community`, + { + params: { accountSequence }, + }, + ) + .pipe( + timeout(this.timeoutMs), + catchError((error) => { + this.logger.warn( + `Failed to find nearest community for accountSequence=${accountSequence}: ${error.message}`, + ); + return of({ data: { accountSequence: null } }); + }), + ), + ); + + return response.data.accountSequence; + } catch (error) { + this.logger.error( + `Error finding nearest community for accountSequence=${accountSequence}`, + error, + ); + return null; + } + } + + /** + * 查找用户推荐链中最近的省公司授权用户(匹配指定省份) + * @param accountSequence 用户的 accountSequence + * @param provinceCode 省份代码 + * @returns 最近省公司授权用户的 accountSequence,如果没有则返回 null + */ + async findNearestProvince( + accountSequence: number, + provinceCode: string, + ): Promise { + try { + const response = await firstValueFrom( + this.httpService + .get( + `${this.baseUrl}/api/v1/authorization/nearest-province`, + { + params: { accountSequence, provinceCode }, + }, + ) + .pipe( + timeout(this.timeoutMs), + catchError((error) => { + this.logger.warn( + `Failed to find nearest province for accountSequence=${accountSequence}, provinceCode=${provinceCode}: ${error.message}`, + ); + return of({ data: { accountSequence: null } }); + }), + ), + ); + + return response.data.accountSequence; + } catch (error) { + this.logger.error( + `Error finding nearest province for accountSequence=${accountSequence}, provinceCode=${provinceCode}`, + error, + ); + return null; + } + } + + /** + * 查找用户推荐链中最近的市公司授权用户(匹配指定城市) + * @param accountSequence 用户的 accountSequence + * @param cityCode 城市代码 + * @returns 最近市公司授权用户的 accountSequence,如果没有则返回 null + */ + async findNearestCity( + accountSequence: number, + cityCode: string, + ): Promise { + try { + const response = await firstValueFrom( + this.httpService + .get( + `${this.baseUrl}/api/v1/authorization/nearest-city`, + { + params: { accountSequence, cityCode }, + }, + ) + .pipe( + timeout(this.timeoutMs), + catchError((error) => { + this.logger.warn( + `Failed to find nearest city for accountSequence=${accountSequence}, cityCode=${cityCode}: ${error.message}`, + ); + return of({ data: { accountSequence: null } }); + }), + ), + ); + + return response.data.accountSequence; + } catch (error) { + this.logger.error( + `Error finding nearest city for accountSequence=${accountSequence}, cityCode=${cityCode}`, + error, + ); + return null; + } + } + + /** + * 并行查询所有授权信息 + * 优化性能:同时发起三个请求 + */ + async findAllNearestAuthorizations( + accountSequence: number, + provinceCode: string, + cityCode: string, + ): Promise<{ + nearestCommunity: number | null; + nearestProvinceAuth: number | null; + nearestCityAuth: number | null; + }> { + const [nearestCommunity, nearestProvinceAuth, nearestCityAuth] = + await Promise.all([ + this.findNearestCommunity(accountSequence), + this.findNearestProvince(accountSequence, provinceCode), + this.findNearestCity(accountSequence, cityCode), + ]); + + this.logger.debug( + `Authorization lookup for accountSequence=${accountSequence}: ` + + `community=${nearestCommunity}, province=${nearestProvinceAuth}, city=${nearestCityAuth}`, + ); + + return { + nearestCommunity, + nearestProvinceAuth, + nearestCityAuth, + }; + } +} diff --git a/backend/services/referral-service/src/infrastructure/external/index.ts b/backend/services/referral-service/src/infrastructure/external/index.ts new file mode 100644 index 00000000..b9c9d806 --- /dev/null +++ b/backend/services/referral-service/src/infrastructure/external/index.ts @@ -0,0 +1 @@ +export * from './authorization-service.client'; diff --git a/backend/services/referral-service/src/infrastructure/index.ts b/backend/services/referral-service/src/infrastructure/index.ts index d1ee8cd6..21bdbba7 100644 --- a/backend/services/referral-service/src/infrastructure/index.ts +++ b/backend/services/referral-service/src/infrastructure/index.ts @@ -3,3 +3,4 @@ export * from './repositories'; export * from './messaging'; export * from './cache'; export * from './kafka'; +export * from './external'; diff --git a/backend/services/referral-service/src/modules/infrastructure.module.ts b/backend/services/referral-service/src/modules/infrastructure.module.ts index bfcb48bc..b991b3de 100644 --- a/backend/services/referral-service/src/modules/infrastructure.module.ts +++ b/backend/services/referral-service/src/modules/infrastructure.module.ts @@ -1,5 +1,6 @@ import { Module, Global } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { HttpModule } from '@nestjs/axios'; import { PrismaService, ReferralRelationshipRepository, @@ -8,6 +9,7 @@ import { EventPublisherService, RedisService, EventAckPublisher, + AuthorizationServiceClient, } from '../infrastructure'; import { REFERRAL_RELATIONSHIP_REPOSITORY, @@ -16,13 +18,14 @@ import { @Global() @Module({ - imports: [ConfigModule], + imports: [ConfigModule, HttpModule], providers: [ PrismaService, KafkaService, RedisService, EventPublisherService, EventAckPublisher, + AuthorizationServiceClient, { provide: REFERRAL_RELATIONSHIP_REPOSITORY, useClass: ReferralRelationshipRepository, @@ -38,6 +41,7 @@ import { RedisService, EventPublisherService, EventAckPublisher, + AuthorizationServiceClient, REFERRAL_RELATIONSHIP_REPOSITORY, TEAM_STATISTICS_REPOSITORY, ],