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:
parent
7d9837fc22
commit
1a5925494e
|
|
@ -275,6 +275,7 @@ services:
|
||||||
- KAFKA_BROKERS=kafka:29092
|
- KAFKA_BROKERS=kafka:29092
|
||||||
- KAFKA_CLIENT_ID=referral-service
|
- KAFKA_CLIENT_ID=referral-service
|
||||||
- KAFKA_GROUP_ID=referral-service-group
|
- KAFKA_GROUP_ID=referral-service-group
|
||||||
|
- AUTHORIZATION_SERVICE_URL=http://rwa-authorization-service:3009
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
@ -282,6 +283,8 @@ services:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
kafka:
|
kafka:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
|
authorization-service:
|
||||||
|
condition: service_healthy
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3004/api/v1/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:3004/api/v1/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Inject,
|
Inject,
|
||||||
|
Logger,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
ApiTags,
|
ApiTags,
|
||||||
|
|
@ -36,6 +37,7 @@ import {
|
||||||
TEAM_STATISTICS_REPOSITORY,
|
TEAM_STATISTICS_REPOSITORY,
|
||||||
ITeamStatisticsRepository,
|
ITeamStatisticsRepository,
|
||||||
} from '../../domain';
|
} from '../../domain';
|
||||||
|
import { AuthorizationServiceClient } from '../../infrastructure/external';
|
||||||
|
|
||||||
@ApiTags('Referral')
|
@ApiTags('Referral')
|
||||||
@Controller('referral')
|
@Controller('referral')
|
||||||
|
|
@ -181,7 +183,12 @@ export class ReferralController {
|
||||||
@ApiTags('Internal Referral API')
|
@ApiTags('Internal Referral API')
|
||||||
@Controller('referrals')
|
@Controller('referrals')
|
||||||
export class InternalReferralController {
|
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')
|
@Get(':accountSequence/context')
|
||||||
@ApiOperation({ summary: '获取用户推荐上下文信息(内部API)' })
|
@ApiOperation({ summary: '获取用户推荐上下文信息(内部API)' })
|
||||||
|
|
@ -192,19 +199,37 @@ export class InternalReferralController {
|
||||||
@Query('provinceCode') provinceCode: string,
|
@Query('provinceCode') provinceCode: string,
|
||||||
@Query('cityCode') cityCode: string,
|
@Query('cityCode') cityCode: string,
|
||||||
) {
|
) {
|
||||||
// 获取用户的推荐链
|
const accountSeqNum = Number(accountSequence);
|
||||||
const query = new GetUserReferralInfoQuery(Number(accountSequence));
|
|
||||||
|
// 1. 获取用户的推荐链
|
||||||
|
const query = new GetUserReferralInfoQuery(accountSeqNum);
|
||||||
const referralInfo = await this.referralService.getUserReferralInfo(query);
|
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 {
|
return {
|
||||||
accountSequence,
|
accountSequence,
|
||||||
referralChain: referralInfo.referrerId ? [referralInfo.referrerId] : [],
|
referralChain: referralInfo.referrerId ? [referralInfo.referrerId] : [],
|
||||||
referrerId: referralInfo.referrerId,
|
referrerId: referralInfo.referrerId,
|
||||||
nearestProvinceAuth: null, // 省代账户ID - 需要后续实现
|
nearestProvinceAuth: authorizations.nearestProvinceAuth?.toString() ?? null,
|
||||||
nearestCityAuth: null, // 市代账户ID - 需要后续实现
|
nearestCityAuth: authorizations.nearestCityAuth?.toString() ?? null,
|
||||||
nearestCommunity: null, // 社区账户ID - 需要后续实现
|
nearestCommunity: authorizations.nearestCommunity?.toString() ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
178
backend/services/referral-service/src/infrastructure/external/authorization-service.client.ts
vendored
Normal file
178
backend/services/referral-service/src/infrastructure/external/authorization-service.client.ts
vendored
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './authorization-service.client';
|
||||||
|
|
@ -3,3 +3,4 @@ export * from './repositories';
|
||||||
export * from './messaging';
|
export * from './messaging';
|
||||||
export * from './cache';
|
export * from './cache';
|
||||||
export * from './kafka';
|
export * from './kafka';
|
||||||
|
export * from './external';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Module, Global } from '@nestjs/common';
|
import { Module, Global } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import {
|
import {
|
||||||
PrismaService,
|
PrismaService,
|
||||||
ReferralRelationshipRepository,
|
ReferralRelationshipRepository,
|
||||||
|
|
@ -8,6 +9,7 @@ import {
|
||||||
EventPublisherService,
|
EventPublisherService,
|
||||||
RedisService,
|
RedisService,
|
||||||
EventAckPublisher,
|
EventAckPublisher,
|
||||||
|
AuthorizationServiceClient,
|
||||||
} from '../infrastructure';
|
} from '../infrastructure';
|
||||||
import {
|
import {
|
||||||
REFERRAL_RELATIONSHIP_REPOSITORY,
|
REFERRAL_RELATIONSHIP_REPOSITORY,
|
||||||
|
|
@ -16,13 +18,14 @@ import {
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule, HttpModule],
|
||||||
providers: [
|
providers: [
|
||||||
PrismaService,
|
PrismaService,
|
||||||
KafkaService,
|
KafkaService,
|
||||||
RedisService,
|
RedisService,
|
||||||
EventPublisherService,
|
EventPublisherService,
|
||||||
EventAckPublisher,
|
EventAckPublisher,
|
||||||
|
AuthorizationServiceClient,
|
||||||
{
|
{
|
||||||
provide: REFERRAL_RELATIONSHIP_REPOSITORY,
|
provide: REFERRAL_RELATIONSHIP_REPOSITORY,
|
||||||
useClass: ReferralRelationshipRepository,
|
useClass: ReferralRelationshipRepository,
|
||||||
|
|
@ -38,6 +41,7 @@ import {
|
||||||
RedisService,
|
RedisService,
|
||||||
EventPublisherService,
|
EventPublisherService,
|
||||||
EventAckPublisher,
|
EventAckPublisher,
|
||||||
|
AuthorizationServiceClient,
|
||||||
REFERRAL_RELATIONSHIP_REPOSITORY,
|
REFERRAL_RELATIONSHIP_REPOSITORY,
|
||||||
TEAM_STATISTICS_REPOSITORY,
|
TEAM_STATISTICS_REPOSITORY,
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue