feat(reward): add internal APIs for reward distribution

- Add /referral/chain/{userId} API in referral-service for getting referral chain with hasPlanted status
- Add internal authorization APIs in authorization-service:
  - GET /authorization/nearest-community: find nearest community in referral chain
  - GET /authorization/nearest-province: find nearest province company in referral chain
  - GET /authorization/nearest-city: find nearest city company in referral chain
- Add repository methods for finding active authorizations by accountSequence
- Update reward-service client to use accountSequence parameter

These APIs enable reward-service to correctly distribute:
- 分享权益 (share benefit): to referrer with hasPlanted=true
- 社区权益 (community benefit): to nearest community leader
- 省团队权益 (province team benefit): to nearest province company
- 市团队权益 (city team benefit): to nearest city company

🤖 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-10 21:49:13 -08:00
parent 05040b3495
commit 6f46a8633f
8 changed files with 383 additions and 9 deletions

View File

@ -1,3 +1,4 @@
export * from './authorization.controller'
export * from './admin-authorization.controller'
export * from './health.controller'
export * from './internal-authorization.controller'

View File

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

View File

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

View File

@ -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<bigint | null> {
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<bigint | null> {
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<bigint | null> {
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
}
}

View File

@ -28,4 +28,18 @@ export interface IAuthorizationRoleRepository {
* accountSequence
*/
findActiveCommunityByAccountSequences(accountSequences: bigint[]): Promise<AuthorizationRole[]>
/**
* accountSequence
*/
findActiveProvinceByAccountSequencesAndRegion(
accountSequences: bigint[],
provinceCode: string,
): Promise<AuthorizationRole[]>
/**
* accountSequence
*/
findActiveCityByAccountSequencesAndRegion(
accountSequences: bigint[],
cityCode: string,
): Promise<AuthorizationRole[]>
}

View File

@ -195,6 +195,48 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
return records.map((record) => this.toDomain(record))
}
async findActiveProvinceByAccountSequencesAndRegion(
accountSequences: bigint[],
provinceCode: string,
): Promise<AuthorizationRole[]> {
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<AuthorizationRole[]> {
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),

View File

@ -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: '创建成功' })

View File

@ -14,7 +14,7 @@ export class AuthorizationServiceClient implements IAuthorizationServiceClient {
async findNearestAuthorizedProvince(userId: bigint, provinceCode: string): Promise<bigint | null> {
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<bigint | null> {
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<bigint | null> {
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;