feat(referral): integrate authorization-service for community/province/city rights allocation

## Problem
社区/省/市权益分配一直返回 null,导致所有权益都分配给系统账户而非正确的授权用户。
原因:referral-service 的 getReferralContext 接口中 nearestCommunity、nearestProvinceAuth、
nearestCityAuth 三个字段硬编码为 null,注释说"需要后续实现"但一直未实现。

## Solution
1. 新建 AuthorizationServiceClient 调用 authorization-service 的内部 API
   - /api/v1/authorization/nearest-community
   - /api/v1/authorization/nearest-province
   - /api/v1/authorization/nearest-city
2. 修改 InternalReferralController 使用并行查询获取授权信息
3. 添加 fallback 机制:authorization-service 不可用时返回 null(保持现有行为)
4. docker-compose.yml 添加 AUTHORIZATION_SERVICE_URL 环境变量

## Files Changed
- backend/services/referral-service/src/infrastructure/external/authorization-service.client.ts (new)
- backend/services/referral-service/src/api/controllers/referral.controller.ts
- backend/services/referral-service/src/modules/infrastructure.module.ts
- backend/services/docker-compose.yml

🤖 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 02:10:33 -08:00
parent 7d9837fc22
commit 1a5925494e
6 changed files with 221 additions and 9 deletions

View File

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

View File

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

View File

@ -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<string>('AUTHORIZATION_SERVICE_URL') ||
'http://localhost:3009';
this.timeoutMs = this.configService.get<number>('AUTHORIZATION_SERVICE_TIMEOUT_MS') || 5000;
}
/**
*
* @param accountSequence accountSequence
* @returns accountSequence null
*/
async findNearestCommunity(accountSequence: number): Promise<number | null> {
try {
const response = await firstValueFrom(
this.httpService
.get<NearestAuthorizationResult>(
`${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<number | null> {
try {
const response = await firstValueFrom(
this.httpService
.get<NearestAuthorizationResult>(
`${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<number | null> {
try {
const response = await firstValueFrom(
this.httpService
.get<NearestAuthorizationResult>(
`${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,
};
}
}

View File

@ -0,0 +1 @@
export * from './authorization-service.client';

View File

@ -3,3 +3,4 @@ export * from './repositories';
export * from './messaging';
export * from './cache';
export * from './kafka';
export * from './external';

View File

@ -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,
],