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:
parent
05040b3495
commit
6f46a8633f
|
|
@ -1,3 +1,4 @@
|
|||
export * from './authorization.controller'
|
||||
export * from './admin-authorization.controller'
|
||||
export * from './health.controller'
|
||||
export * from './internal-authorization.controller'
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[]>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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: '创建成功' })
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue