feat(backend): implement assessment rules for reward distribution

Add proper assessment/考核 logic for all 5 benefit types:
- Community (80 USDT): 10-tree initial assessment, splits rewards between
  current community and parent/headquarters based on progress
- Province Team (20 USDT): 500-tree initial assessment for AUTH_PROVINCE_COMPANY
- Province Area (15 USDT + 1%): 50000-tree target for PROVINCE_COMPANY,
  routes to system account until target reached
- City Team (40 USDT): 100-tree initial assessment for AUTH_CITY_COMPANY
- City Area (35 USDT + 2%): 10000-tree target for CITY_COMPANY,
  routes to system account until target reached

Changes:
- authorization-service: Add 4 new distribution API endpoints and application
  service methods for province/city team/area reward distribution
- authorization-service: Add repository methods for querying authorizations
  including benefitActive=false records
- reward-service: Update client to call new distribution APIs
- reward-service: Modify calculation methods to return multiple reward entries
  based on assessment distribution plan

🤖 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 07:54:55 -08:00
parent 033268deb9
commit 16d95999de
6 changed files with 1295 additions and 136 deletions

View File

@ -115,4 +115,169 @@ export class InternalAuthorizationController {
accountSequence: result ? Number(result) : null,
}
}
/**
*
*
* reward-service
*/
@Get('community-reward-distribution')
@ApiOperation({ summary: '获取社区权益分配方案(内部 API' })
@ApiQuery({ name: 'accountSequence', description: '认种用户的 accountSequence' })
@ApiQuery({ name: 'treeCount', description: '认种棵数' })
@ApiResponse({
status: 200,
description: '返回社区权益分配方案',
schema: {
type: 'object',
properties: {
distributions: {
type: 'array',
items: {
type: 'object',
properties: {
accountSequence: { type: 'number', description: '接收者账号' },
treeCount: { type: 'number', description: '分配棵数' },
reason: { type: 'string', description: '分配原因' },
},
},
},
},
},
})
async getCommunityRewardDistribution(
@Query('accountSequence') accountSequence: string,
@Query('treeCount') treeCount: string,
): Promise<{
distributions: Array<{
accountSequence: number
treeCount: number
reason: string
}>
}> {
this.logger.debug(
`[INTERNAL] getCommunityRewardDistribution: accountSequence=${accountSequence}, treeCount=${treeCount}`,
)
return this.applicationService.getCommunityRewardDistribution(
Number(accountSequence),
Number(treeCount),
)
}
/**
* (20 USDT)
*/
@Get('province-team-reward-distribution')
@ApiOperation({ summary: '获取省团队权益分配方案(内部 API' })
@ApiQuery({ name: 'accountSequence', description: '认种用户的 accountSequence' })
@ApiQuery({ name: 'provinceCode', description: '省份代码' })
@ApiQuery({ name: 'treeCount', description: '认种棵数' })
async getProvinceTeamRewardDistribution(
@Query('accountSequence') accountSequence: string,
@Query('provinceCode') provinceCode: string,
@Query('treeCount') treeCount: string,
): Promise<{
distributions: Array<{
accountSequence: number
treeCount: number
reason: string
}>
}> {
this.logger.debug(
`[INTERNAL] getProvinceTeamRewardDistribution: accountSequence=${accountSequence}, provinceCode=${provinceCode}, treeCount=${treeCount}`,
)
return this.applicationService.getProvinceTeamRewardDistribution(
Number(accountSequence),
provinceCode,
Number(treeCount),
)
}
/**
* (15 USDT + 1%)
*/
@Get('province-area-reward-distribution')
@ApiOperation({ summary: '获取省区域权益分配方案(内部 API' })
@ApiQuery({ name: 'provinceCode', description: '省份代码' })
@ApiQuery({ name: 'treeCount', description: '认种棵数' })
async getProvinceAreaRewardDistribution(
@Query('provinceCode') provinceCode: string,
@Query('treeCount') treeCount: string,
): Promise<{
distributions: Array<{
accountSequence: number
treeCount: number
reason: string
isSystemAccount: boolean
}>
}> {
this.logger.debug(
`[INTERNAL] getProvinceAreaRewardDistribution: provinceCode=${provinceCode}, treeCount=${treeCount}`,
)
return this.applicationService.getProvinceAreaRewardDistribution(
provinceCode,
Number(treeCount),
)
}
/**
* (40 USDT)
*/
@Get('city-team-reward-distribution')
@ApiOperation({ summary: '获取市团队权益分配方案(内部 API' })
@ApiQuery({ name: 'accountSequence', description: '认种用户的 accountSequence' })
@ApiQuery({ name: 'cityCode', description: '城市代码' })
@ApiQuery({ name: 'treeCount', description: '认种棵数' })
async getCityTeamRewardDistribution(
@Query('accountSequence') accountSequence: string,
@Query('cityCode') cityCode: string,
@Query('treeCount') treeCount: string,
): Promise<{
distributions: Array<{
accountSequence: number
treeCount: number
reason: string
}>
}> {
this.logger.debug(
`[INTERNAL] getCityTeamRewardDistribution: accountSequence=${accountSequence}, cityCode=${cityCode}, treeCount=${treeCount}`,
)
return this.applicationService.getCityTeamRewardDistribution(
Number(accountSequence),
cityCode,
Number(treeCount),
)
}
/**
* (35 USDT + 2%)
*/
@Get('city-area-reward-distribution')
@ApiOperation({ summary: '获取市区域权益分配方案(内部 API' })
@ApiQuery({ name: 'cityCode', description: '城市代码' })
@ApiQuery({ name: 'treeCount', description: '认种棵数' })
async getCityAreaRewardDistribution(
@Query('cityCode') cityCode: string,
@Query('treeCount') treeCount: string,
): Promise<{
distributions: Array<{
accountSequence: number
treeCount: number
reason: string
isSystemAccount: boolean
}>
}> {
this.logger.debug(
`[INTERNAL] getCityAreaRewardDistribution: cityCode=${cityCode}, treeCount=${treeCount}`,
)
return this.applicationService.getCityAreaRewardDistribution(
cityCode,
Number(treeCount),
)
}
}

View File

@ -744,4 +744,598 @@ export class AuthorizationApplicationService {
return null
}
/**
*
*
*
*
* 1.
* 2. benefitActive=true
* 3. benefitActive=false
* - (10)
* -
* -
* 4.
*/
async getCommunityRewardDistribution(
accountSequence: number,
treeCount: number,
): Promise<{
distributions: Array<{
accountSequence: number
treeCount: number
reason: string
}>
}> {
this.logger.debug(
`[getCommunityRewardDistribution] accountSequence=${accountSequence}, treeCount=${treeCount}`,
)
const HEADQUARTERS_ACCOUNT_SEQUENCE = 1 // 总部社区账号
// 1. 获取用户的祖先链(推荐链)
const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(BigInt(accountSequence))
if (ancestorAccountSequences.length === 0) {
// 无推荐链,全部给总部
return {
distributions: [
{
accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE,
treeCount,
reason: '无推荐链,进总部社区',
},
],
}
}
// 2. 查找祖先链中所有社区授权(包括 benefitActive=false 的)
const ancestorCommunities = await this.authorizationRepository.findCommunityByAccountSequences(
ancestorAccountSequences.map((seq) => BigInt(seq)),
)
if (ancestorCommunities.length === 0) {
// 推荐链上没有社区,全部给总部
return {
distributions: [
{
accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE,
treeCount,
reason: '推荐链上无社区授权,进总部社区',
},
],
}
}
// 3. 按祖先链顺序找最近的社区
let nearestCommunity: typeof ancestorCommunities[0] | null = null
let nearestCommunityIndex = -1
for (let i = 0; i < ancestorAccountSequences.length; i++) {
const ancestorSeq = ancestorAccountSequences[i]
const found = ancestorCommunities.find(
(auth) => Number(auth.userId.accountSequence) === ancestorSeq,
)
if (found) {
nearestCommunity = found
nearestCommunityIndex = i
break
}
}
if (!nearestCommunity) {
// 这种情况理论上不应该发生,但作为兜底
return {
distributions: [
{
accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE,
treeCount,
reason: '未找到匹配的社区,进总部社区',
},
],
}
}
// 4. 检查最近社区的权益状态
if (nearestCommunity.benefitActive) {
// 权益已激活,全部给该社区
return {
distributions: [
{
accountSequence: Number(nearestCommunity.userId.accountSequence),
treeCount,
reason: '社区权益已激活',
},
],
}
}
// 5. 权益未激活,需要计算考核分配
// 获取该社区的团队统计数据(当前已完成的认种数量)
const communityStats = await this.statsRepository.findByAccountSequence(
nearestCommunity.userId.accountSequence,
)
const currentTeamCount = communityStats?.totalTeamPlantingCount ?? 0
const initialTarget = nearestCommunity.getInitialTarget() // 社区初始考核目标10棵
this.logger.debug(
`[getCommunityRewardDistribution] Community ${nearestCommunity.userId.accountSequence} ` +
`benefitActive=false, currentTeamCount=${currentTeamCount}, initialTarget=${initialTarget}`,
)
// 6. 查找上级社区(用于接收考核前的权益)
let parentCommunityAccountSequence: number = HEADQUARTERS_ACCOUNT_SEQUENCE
let parentCommunityReason = '上级为总部社区'
// 从最近社区之后继续查找上级社区
for (let i = nearestCommunityIndex + 1; i < ancestorAccountSequences.length; i++) {
const ancestorSeq = ancestorAccountSequences[i]
const found = ancestorCommunities.find(
(auth) => Number(auth.userId.accountSequence) === ancestorSeq && auth.benefitActive,
)
if (found) {
parentCommunityAccountSequence = Number(found.userId.accountSequence)
parentCommunityReason = '上级社区权益已激活'
break
}
}
// 7. 计算分配方案
const distributions: Array<{
accountSequence: number
treeCount: number
reason: string
}> = []
if (currentTeamCount >= initialTarget) {
// 已达标但权益未激活(可能是月度考核失败),全部给该社区
// 注:这种情况下应该由系统自动激活权益,但这里作为兜底处理
distributions.push({
accountSequence: Number(nearestCommunity.userId.accountSequence),
treeCount,
reason: '已达初始考核目标',
})
} else {
// 未达标,需要拆分
const remaining = initialTarget - currentTeamCount // 还差多少棵达标
if (treeCount <= remaining) {
// 本次认种全部用于考核,给上级/总部
distributions.push({
accountSequence: parentCommunityAccountSequence,
treeCount,
reason: `初始考核中(${currentTeamCount}/${initialTarget})${parentCommunityReason}`,
})
} else {
// 本次认种跨越考核达标点
// 考核前的部分给上级/总部
distributions.push({
accountSequence: parentCommunityAccountSequence,
treeCount: remaining,
reason: `初始考核(${currentTeamCount}+${remaining}=${initialTarget})${parentCommunityReason}`,
})
// 考核后的部分给该社区
distributions.push({
accountSequence: Number(nearestCommunity.userId.accountSequence),
treeCount: treeCount - remaining,
reason: `考核达标后权益生效`,
})
}
}
this.logger.debug(
`[getCommunityRewardDistribution] Result: ${JSON.stringify(distributions)}`,
)
return { distributions }
}
/**
* (20 USDT)
*
*
* 1. AUTH_PROVINCE_COMPANY
* 2. benefitActive=true
* 3. benefitActive=false
* - (500)
* - /
* -
* 4.
*/
async getProvinceTeamRewardDistribution(
accountSequence: number,
provinceCode: string,
treeCount: number,
): Promise<{
distributions: Array<{
accountSequence: number
treeCount: number
reason: string
}>
}> {
this.logger.debug(
`[getProvinceTeamRewardDistribution] accountSequence=${accountSequence}, provinceCode=${provinceCode}, treeCount=${treeCount}`,
)
const HEADQUARTERS_ACCOUNT_SEQUENCE = 1
// 1. 获取用户的祖先链
const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(BigInt(accountSequence))
if (ancestorAccountSequences.length === 0) {
return {
distributions: [
{ accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '无推荐链,进总部社区' },
],
}
}
// 2. 查找祖先链中所有授权省公司(包括 benefitActive=false
const ancestorAuthProvinces = await this.authorizationRepository.findAuthProvinceByAccountSequencesAndRegion(
ancestorAccountSequences.map((seq) => BigInt(seq)),
provinceCode,
)
if (ancestorAuthProvinces.length === 0) {
return {
distributions: [
{ accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '推荐链上无授权省公司,进总部社区' },
],
}
}
// 3. 按祖先链顺序找最近的授权省公司
let nearestAuthProvince: typeof ancestorAuthProvinces[0] | null = null
let nearestIndex = -1
for (let i = 0; i < ancestorAccountSequences.length; i++) {
const ancestorSeq = ancestorAccountSequences[i]
const found = ancestorAuthProvinces.find(
(auth) => Number(auth.userId.accountSequence) === ancestorSeq,
)
if (found) {
nearestAuthProvince = found
nearestIndex = i
break
}
}
if (!nearestAuthProvince) {
return {
distributions: [
{ accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '未找到匹配的授权省公司,进总部社区' },
],
}
}
// 4. 检查权益状态
if (nearestAuthProvince.benefitActive) {
return {
distributions: [
{ accountSequence: Number(nearestAuthProvince.userId.accountSequence), treeCount, reason: '省团队权益已激活' },
],
}
}
// 5. 权益未激活,计算考核分配
const stats = await this.statsRepository.findByAccountSequence(nearestAuthProvince.userId.accountSequence)
const currentTeamCount = stats?.totalTeamPlantingCount ?? 0
const initialTarget = nearestAuthProvince.getInitialTarget() // 500棵
// 6. 查找上级(用于接收考核前的权益)
let parentAccountSequence: number = HEADQUARTERS_ACCOUNT_SEQUENCE
let parentReason = '上级为总部社区'
for (let i = nearestIndex + 1; i < ancestorAccountSequences.length; i++) {
const ancestorSeq = ancestorAccountSequences[i]
const found = ancestorAuthProvinces.find(
(auth) => Number(auth.userId.accountSequence) === ancestorSeq && auth.benefitActive,
)
if (found) {
parentAccountSequence = Number(found.userId.accountSequence)
parentReason = '上级授权省公司权益已激活'
break
}
}
// 7. 计算分配
const distributions: Array<{ accountSequence: number; treeCount: number; reason: string }> = []
if (currentTeamCount >= initialTarget) {
distributions.push({
accountSequence: Number(nearestAuthProvince.userId.accountSequence),
treeCount,
reason: '已达初始考核目标',
})
} else {
const remaining = initialTarget - currentTeamCount
if (treeCount <= remaining) {
distributions.push({
accountSequence: parentAccountSequence,
treeCount,
reason: `初始考核中(${currentTeamCount}/${initialTarget})${parentReason}`,
})
} else {
distributions.push({
accountSequence: parentAccountSequence,
treeCount: remaining,
reason: `初始考核(${currentTeamCount}+${remaining}=${initialTarget})${parentReason}`,
})
distributions.push({
accountSequence: Number(nearestAuthProvince.userId.accountSequence),
treeCount: treeCount - remaining,
reason: '考核达标后权益生效',
})
}
}
this.logger.debug(`[getProvinceTeamRewardDistribution] Result: ${JSON.stringify(distributions)}`)
return { distributions }
}
/**
* (15 USDT + 1%)
*
*
* 1. PROVINCE_COMPANY
* 2. benefitActive=true
* 3. benefitActive=false
*/
async getProvinceAreaRewardDistribution(
provinceCode: string,
treeCount: number,
): Promise<{
distributions: Array<{
accountSequence: number
treeCount: number
reason: string
isSystemAccount: boolean
}>
}> {
this.logger.debug(
`[getProvinceAreaRewardDistribution] provinceCode=${provinceCode}, treeCount=${treeCount}`,
)
// 系统省账户ID格式: 9 + 省份代码
const systemProvinceAccountId = Number(`9${provinceCode.padStart(6, '0')}`)
// 查找该省份的正式省公司
const provinceCompany = await this.authorizationRepository.findProvinceCompanyByRegion(provinceCode)
if (provinceCompany && provinceCompany.benefitActive) {
// 正式省公司权益已激活,进该省公司账户
return {
distributions: [
{
accountSequence: Number(provinceCompany.userId.accountSequence),
treeCount,
reason: '省区域权益已激活',
isSystemAccount: false,
},
],
}
}
// 无正式省公司或权益未激活,进系统省账户
const reason = provinceCompany
? `省区域权益未激活(考核中),进系统省账户`
: '无正式省公司授权,进系统省账户'
return {
distributions: [
{
accountSequence: systemProvinceAccountId,
treeCount,
reason,
isSystemAccount: true,
},
],
}
}
/**
* (40 USDT)
*
*
* 1. AUTH_CITY_COMPANY
* 2. benefitActive=true
* 3. benefitActive=false
* - (100)
* - /
* -
* 4.
*/
async getCityTeamRewardDistribution(
accountSequence: number,
cityCode: string,
treeCount: number,
): Promise<{
distributions: Array<{
accountSequence: number
treeCount: number
reason: string
}>
}> {
this.logger.debug(
`[getCityTeamRewardDistribution] accountSequence=${accountSequence}, cityCode=${cityCode}, treeCount=${treeCount}`,
)
const HEADQUARTERS_ACCOUNT_SEQUENCE = 1
// 1. 获取用户的祖先链
const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(BigInt(accountSequence))
if (ancestorAccountSequences.length === 0) {
return {
distributions: [
{ accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '无推荐链,进总部社区' },
],
}
}
// 2. 查找祖先链中所有授权市公司(包括 benefitActive=false
const ancestorAuthCities = await this.authorizationRepository.findAuthCityByAccountSequencesAndRegion(
ancestorAccountSequences.map((seq) => BigInt(seq)),
cityCode,
)
if (ancestorAuthCities.length === 0) {
return {
distributions: [
{ accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '推荐链上无授权市公司,进总部社区' },
],
}
}
// 3. 按祖先链顺序找最近的授权市公司
let nearestAuthCity: typeof ancestorAuthCities[0] | null = null
let nearestIndex = -1
for (let i = 0; i < ancestorAccountSequences.length; i++) {
const ancestorSeq = ancestorAccountSequences[i]
const found = ancestorAuthCities.find(
(auth) => Number(auth.userId.accountSequence) === ancestorSeq,
)
if (found) {
nearestAuthCity = found
nearestIndex = i
break
}
}
if (!nearestAuthCity) {
return {
distributions: [
{ accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '未找到匹配的授权市公司,进总部社区' },
],
}
}
// 4. 检查权益状态
if (nearestAuthCity.benefitActive) {
return {
distributions: [
{ accountSequence: Number(nearestAuthCity.userId.accountSequence), treeCount, reason: '市团队权益已激活' },
],
}
}
// 5. 权益未激活,计算考核分配
const stats = await this.statsRepository.findByAccountSequence(nearestAuthCity.userId.accountSequence)
const currentTeamCount = stats?.totalTeamPlantingCount ?? 0
const initialTarget = nearestAuthCity.getInitialTarget() // 100棵
// 6. 查找上级
let parentAccountSequence: number = HEADQUARTERS_ACCOUNT_SEQUENCE
let parentReason = '上级为总部社区'
for (let i = nearestIndex + 1; i < ancestorAccountSequences.length; i++) {
const ancestorSeq = ancestorAccountSequences[i]
const found = ancestorAuthCities.find(
(auth) => Number(auth.userId.accountSequence) === ancestorSeq && auth.benefitActive,
)
if (found) {
parentAccountSequence = Number(found.userId.accountSequence)
parentReason = '上级授权市公司权益已激活'
break
}
}
// 7. 计算分配
const distributions: Array<{ accountSequence: number; treeCount: number; reason: string }> = []
if (currentTeamCount >= initialTarget) {
distributions.push({
accountSequence: Number(nearestAuthCity.userId.accountSequence),
treeCount,
reason: '已达初始考核目标',
})
} else {
const remaining = initialTarget - currentTeamCount
if (treeCount <= remaining) {
distributions.push({
accountSequence: parentAccountSequence,
treeCount,
reason: `初始考核中(${currentTeamCount}/${initialTarget})${parentReason}`,
})
} else {
distributions.push({
accountSequence: parentAccountSequence,
treeCount: remaining,
reason: `初始考核(${currentTeamCount}+${remaining}=${initialTarget})${parentReason}`,
})
distributions.push({
accountSequence: Number(nearestAuthCity.userId.accountSequence),
treeCount: treeCount - remaining,
reason: '考核达标后权益生效',
})
}
}
this.logger.debug(`[getCityTeamRewardDistribution] Result: ${JSON.stringify(distributions)}`)
return { distributions }
}
/**
* (35 USDT + 2%)
*
*
* 1. CITY_COMPANY
* 2. benefitActive=true
* 3. benefitActive=false
*/
async getCityAreaRewardDistribution(
cityCode: string,
treeCount: number,
): Promise<{
distributions: Array<{
accountSequence: number
treeCount: number
reason: string
isSystemAccount: boolean
}>
}> {
this.logger.debug(
`[getCityAreaRewardDistribution] cityCode=${cityCode}, treeCount=${treeCount}`,
)
// 系统市账户ID格式: 8 + 城市代码
const systemCityAccountId = Number(`8${cityCode.padStart(6, '0')}`)
// 查找该城市的正式市公司
const cityCompany = await this.authorizationRepository.findCityCompanyByRegion(cityCode)
if (cityCompany && cityCompany.benefitActive) {
// 正式市公司权益已激活,进该市公司账户
return {
distributions: [
{
accountSequence: Number(cityCompany.userId.accountSequence),
treeCount,
reason: '市区域权益已激活',
isSystemAccount: false,
},
],
}
}
// 无正式市公司或权益未激活,进系统市账户
const reason = cityCompany
? `市区域权益未激活(考核中),进系统市账户`
: '无正式市公司授权,进系统市账户'
return {
distributions: [
{
accountSequence: systemCityAccountId,
treeCount,
reason,
isSystemAccount: true,
},
],
}
}
}

View File

@ -42,4 +42,33 @@ export interface IAuthorizationRoleRepository {
accountSequences: bigint[],
cityCode: string,
): Promise<AuthorizationRole[]>
/**
* accountSequence benefitActive=false
*
*/
findCommunityByAccountSequences(accountSequences: bigint[]): Promise<AuthorizationRole[]>
/**
* accountSequence benefitActive=false
*
*/
findAuthProvinceByAccountSequencesAndRegion(
accountSequences: bigint[],
provinceCode: string,
): Promise<AuthorizationRole[]>
/**
* accountSequence benefitActive=false
*
*/
findAuthCityByAccountSequencesAndRegion(
accountSequences: bigint[],
cityCode: string,
): Promise<AuthorizationRole[]>
/**
*
*/
findProvinceCompanyByRegion(provinceCode: string): Promise<AuthorizationRole | null>
/**
*
*/
findCityCompanyByRegion(cityCode: string): Promise<AuthorizationRole | null>
}

View File

@ -237,6 +237,89 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
return records.map((record) => this.toDomain(record))
}
async findCommunityByAccountSequences(
accountSequences: bigint[],
): Promise<AuthorizationRole[]> {
if (accountSequences.length === 0) {
return []
}
const records = await this.prisma.authorizationRole.findMany({
where: {
accountSequence: { in: accountSequences },
roleType: RoleType.COMMUNITY,
status: AuthorizationStatus.AUTHORIZED,
// 注意:不过滤 benefitActive用于计算社区权益分配
},
orderBy: { accountSequence: 'asc' },
})
return records.map((record) => this.toDomain(record))
}
async findAuthProvinceByAccountSequencesAndRegion(
accountSequences: bigint[],
provinceCode: string,
): Promise<AuthorizationRole[]> {
if (accountSequences.length === 0) {
return []
}
const records = await this.prisma.authorizationRole.findMany({
where: {
accountSequence: { in: accountSequences },
roleType: RoleType.AUTH_PROVINCE_COMPANY,
regionCode: provinceCode,
status: AuthorizationStatus.AUTHORIZED,
// 注意:不过滤 benefitActive用于计算省团队权益分配
},
orderBy: { accountSequence: 'asc' },
})
return records.map((record) => this.toDomain(record))
}
async findAuthCityByAccountSequencesAndRegion(
accountSequences: bigint[],
cityCode: string,
): Promise<AuthorizationRole[]> {
if (accountSequences.length === 0) {
return []
}
const records = await this.prisma.authorizationRole.findMany({
where: {
accountSequence: { in: accountSequences },
roleType: RoleType.AUTH_CITY_COMPANY,
regionCode: cityCode,
status: AuthorizationStatus.AUTHORIZED,
// 注意:不过滤 benefitActive用于计算市团队权益分配
},
orderBy: { accountSequence: 'asc' },
})
return records.map((record) => this.toDomain(record))
}
async findProvinceCompanyByRegion(provinceCode: string): Promise<AuthorizationRole | null> {
const record = await this.prisma.authorizationRole.findFirst({
where: {
roleType: RoleType.PROVINCE_COMPANY,
regionCode: provinceCode,
status: AuthorizationStatus.AUTHORIZED,
},
})
return record ? this.toDomain(record) : null
}
async findCityCompanyByRegion(cityCode: string): Promise<AuthorizationRole | null> {
const record = await this.prisma.authorizationRole.findFirst({
where: {
roleType: RoleType.CITY_COMPANY,
regionCode: cityCode,
status: AuthorizationStatus.AUTHORIZED,
},
})
return record ? this.toDomain(record) : null
}
private toDomain(record: any): AuthorizationRole {
const props: AuthorizationRoleProps = {
authorizationId: AuthorizationId.create(record.id),

View File

@ -12,10 +12,32 @@ export interface IReferralServiceClient {
}>;
}
export interface RewardDistribution {
distributions: Array<{
accountSequence: number;
treeCount: number;
reason: string;
}>;
}
export interface AreaRewardDistribution {
distributions: Array<{
accountSequence: number;
treeCount: number;
reason: string;
isSystemAccount: boolean;
}>;
}
export interface IAuthorizationServiceClient {
findNearestAuthorizedProvince(userId: bigint, provinceCode: string): Promise<bigint | null>;
findNearestAuthorizedCity(userId: bigint, cityCode: string): Promise<bigint | null>;
findNearestCommunity(userId: bigint): Promise<bigint | null>;
getCommunityRewardDistribution(userId: bigint, treeCount: number): Promise<RewardDistribution>;
getProvinceTeamRewardDistribution(userId: bigint, provinceCode: string, treeCount: number): Promise<RewardDistribution>;
getProvinceAreaRewardDistribution(provinceCode: string, treeCount: number): Promise<AreaRewardDistribution>;
getCityTeamRewardDistribution(userId: bigint, cityCode: string, treeCount: number): Promise<RewardDistribution>;
getCityAreaRewardDistribution(cityCode: string, treeCount: number): Promise<AreaRewardDistribution>;
}
export const REFERRAL_SERVICE_CLIENT = Symbol('IReferralServiceClient');
@ -107,49 +129,49 @@ export class RewardCalculationService {
);
rewards.push(...shareRewards);
// 6. 省团队权益 (20 USDT)
const provinceTeamReward = await this.calculateProvinceTeamRight(
// 6. 省团队权益 (20 USDT) - 可能返回多条记录(考核分配)
const provinceTeamRewards = await this.calculateProvinceTeamRight(
params.sourceOrderNo,
params.sourceUserId,
params.provinceCode,
params.treeCount,
);
rewards.push(provinceTeamReward);
rewards.push(...provinceTeamRewards);
// 7. 省区域权益 (15 USDT + 1%算力)
const provinceAreaReward = this.calculateProvinceAreaRight(
// 7. 省区域权益 (15 USDT + 1%算力) - 可能返回多条记录(考核分配)
const provinceAreaRewards = await this.calculateProvinceAreaRight(
params.sourceOrderNo,
params.sourceUserId,
params.provinceCode,
params.treeCount,
);
rewards.push(provinceAreaReward);
rewards.push(...provinceAreaRewards);
// 8. 市团队权益 (40 USDT)
const cityTeamReward = await this.calculateCityTeamRight(
// 8. 市团队权益 (40 USDT) - 可能返回多条记录(考核分配)
const cityTeamRewards = await this.calculateCityTeamRight(
params.sourceOrderNo,
params.sourceUserId,
params.cityCode,
params.treeCount,
);
rewards.push(cityTeamReward);
rewards.push(...cityTeamRewards);
// 9. 市区域权益 (35 USDT + 2%算力)
const cityAreaReward = this.calculateCityAreaRight(
// 9. 市区域权益 (35 USDT + 2%算力) - 可能返回多条记录(考核分配)
const cityAreaRewards = await this.calculateCityAreaRight(
params.sourceOrderNo,
params.sourceUserId,
params.cityCode,
params.treeCount,
);
rewards.push(cityAreaReward);
rewards.push(...cityAreaRewards);
// 10. 社区权益 (80 USDT)
const communityReward = await this.calculateCommunityRight(
// 10. 社区权益 (80 USDT) - 可能返回多条记录(考核分配)
const communityRewards = await this.calculateCommunityRight(
params.sourceOrderNo,
params.sourceUserId,
params.treeCount,
);
rewards.push(communityReward);
rewards.push(...communityRewards);
return rewards;
}
@ -338,199 +360,238 @@ export class RewardCalculationService {
/**
* (20 USDT)
* 500
*/
private async calculateProvinceTeamRight(
sourceOrderNo: string,
sourceUserId: bigint,
provinceCode: string,
treeCount: number,
): Promise<RewardLedgerEntry> {
): Promise<RewardLedgerEntry[]> {
const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.PROVINCE_TEAM_RIGHT];
const usdtAmount = Money.USDT(usdt * treeCount);
const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent);
const rewardSource = RewardSource.create(
RightType.PROVINCE_TEAM_RIGHT,
sourceOrderNo,
sourceUserId,
);
// 查找最近的授权省公司
const nearestProvince = await this.authorizationService.findNearestAuthorizedProvince(
// 调用 authorization-service 获取省团队权益分配方案
const distribution = await this.authorizationService.getProvinceTeamRewardDistribution(
sourceUserId,
provinceCode,
treeCount,
);
if (nearestProvince) {
return RewardLedgerEntry.createSettleable({
userId: nearestProvince,
accountSequence: nearestProvince,
rewardSource,
usdtAmount,
hashpowerAmount: hashpower,
memo: `省团队权益:来自${provinceCode}省的认种`,
});
} else {
return RewardLedgerEntry.createSettleable({
userId: HEADQUARTERS_COMMUNITY_USER_ID,
accountSequence: HEADQUARTERS_COMMUNITY_USER_ID,
rewardSource,
usdtAmount,
hashpowerAmount: hashpower,
memo: '省团队权益:无达标授权省公司,进总部社区',
});
const rewards: RewardLedgerEntry[] = [];
// 根据分配方案创建奖励记录
for (const item of distribution.distributions) {
const itemUsdtAmount = Money.USDT(usdt * item.treeCount);
const itemHashpower = Hashpower.fromTreeCount(item.treeCount, hashpowerPercent);
const rewardSource = RewardSource.create(
RightType.PROVINCE_TEAM_RIGHT,
sourceOrderNo,
sourceUserId,
);
rewards.push(
RewardLedgerEntry.createSettleable({
userId: BigInt(item.accountSequence),
accountSequence: BigInt(item.accountSequence),
rewardSource,
usdtAmount: itemUsdtAmount,
hashpowerAmount: itemHashpower,
memo: `省团队权益(${provinceCode})${item.reason}`,
}),
);
}
return rewards;
}
/**
* (15 USDT + 1%)
* 50000
* -
* -
*/
private calculateProvinceAreaRight(
private async calculateProvinceAreaRight(
sourceOrderNo: string,
sourceUserId: bigint,
provinceCode: string,
treeCount: number,
): RewardLedgerEntry {
): Promise<RewardLedgerEntry[]> {
const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.PROVINCE_AREA_RIGHT];
const usdtAmount = Money.USDT(usdt * treeCount);
const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent);
const rewardSource = RewardSource.create(
RightType.PROVINCE_AREA_RIGHT,
sourceOrderNo,
sourceUserId,
// 调用 authorization-service 获取省区域权益分配方案
const distribution = await this.authorizationService.getProvinceAreaRewardDistribution(
provinceCode,
treeCount,
);
// 进系统省公司账户 (使用特殊账户ID格式)
const systemProvinceAccountId = BigInt(`9${provinceCode.padStart(6, '0')}`);
const rewards: RewardLedgerEntry[] = [];
return RewardLedgerEntry.createSettleable({
userId: systemProvinceAccountId,
accountSequence: systemProvinceAccountId,
rewardSource,
usdtAmount,
hashpowerAmount: hashpower,
memo: `省区域权益:${provinceCode}15U + 1%算力`,
});
// 根据分配方案创建奖励记录
for (const item of distribution.distributions) {
const itemUsdtAmount = Money.USDT(usdt * item.treeCount);
const itemHashpower = Hashpower.fromTreeCount(item.treeCount, hashpowerPercent);
const rewardSource = RewardSource.create(
RightType.PROVINCE_AREA_RIGHT,
sourceOrderNo,
sourceUserId,
);
rewards.push(
RewardLedgerEntry.createSettleable({
userId: BigInt(item.accountSequence),
accountSequence: BigInt(item.accountSequence),
rewardSource,
usdtAmount: itemUsdtAmount,
hashpowerAmount: itemHashpower,
memo: `省区域权益(${provinceCode})${item.reason}`,
}),
);
}
return rewards;
}
/**
* (40 USDT)
* 100
*/
private async calculateCityTeamRight(
sourceOrderNo: string,
sourceUserId: bigint,
cityCode: string,
treeCount: number,
): Promise<RewardLedgerEntry> {
): Promise<RewardLedgerEntry[]> {
const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.CITY_TEAM_RIGHT];
const usdtAmount = Money.USDT(usdt * treeCount);
const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent);
const rewardSource = RewardSource.create(
RightType.CITY_TEAM_RIGHT,
sourceOrderNo,
sourceUserId,
);
// 查找最近的授权市公司
const nearestCity = await this.authorizationService.findNearestAuthorizedCity(
// 调用 authorization-service 获取市团队权益分配方案
const distribution = await this.authorizationService.getCityTeamRewardDistribution(
sourceUserId,
cityCode,
treeCount,
);
if (nearestCity) {
return RewardLedgerEntry.createSettleable({
userId: nearestCity,
accountSequence: nearestCity,
rewardSource,
usdtAmount,
hashpowerAmount: hashpower,
memo: `市团队权益:来自${cityCode}市的认种`,
});
} else {
return RewardLedgerEntry.createSettleable({
userId: HEADQUARTERS_COMMUNITY_USER_ID,
accountSequence: HEADQUARTERS_COMMUNITY_USER_ID,
rewardSource,
usdtAmount,
hashpowerAmount: hashpower,
memo: '市团队权益:无达标授权市公司,进总部社区',
});
const rewards: RewardLedgerEntry[] = [];
// 根据分配方案创建奖励记录
for (const item of distribution.distributions) {
const itemUsdtAmount = Money.USDT(usdt * item.treeCount);
const itemHashpower = Hashpower.fromTreeCount(item.treeCount, hashpowerPercent);
const rewardSource = RewardSource.create(
RightType.CITY_TEAM_RIGHT,
sourceOrderNo,
sourceUserId,
);
rewards.push(
RewardLedgerEntry.createSettleable({
userId: BigInt(item.accountSequence),
accountSequence: BigInt(item.accountSequence),
rewardSource,
usdtAmount: itemUsdtAmount,
hashpowerAmount: itemHashpower,
memo: `市团队权益(${cityCode})${item.reason}`,
}),
);
}
return rewards;
}
/**
* (35 USDT + 2%)
* 10000
* -
* -
*/
private calculateCityAreaRight(
private async calculateCityAreaRight(
sourceOrderNo: string,
sourceUserId: bigint,
cityCode: string,
treeCount: number,
): RewardLedgerEntry {
): Promise<RewardLedgerEntry[]> {
const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.CITY_AREA_RIGHT];
const usdtAmount = Money.USDT(usdt * treeCount);
const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent);
const rewardSource = RewardSource.create(
RightType.CITY_AREA_RIGHT,
sourceOrderNo,
sourceUserId,
// 调用 authorization-service 获取市区域权益分配方案
const distribution = await this.authorizationService.getCityAreaRewardDistribution(
cityCode,
treeCount,
);
// 进系统市公司账户
const systemCityAccountId = BigInt(`8${cityCode.padStart(6, '0')}`);
const rewards: RewardLedgerEntry[] = [];
return RewardLedgerEntry.createSettleable({
userId: systemCityAccountId,
accountSequence: systemCityAccountId,
rewardSource,
usdtAmount,
hashpowerAmount: hashpower,
memo: `市区域权益:${cityCode}35U + 2%算力`,
});
// 根据分配方案创建奖励记录
for (const item of distribution.distributions) {
const itemUsdtAmount = Money.USDT(usdt * item.treeCount);
const itemHashpower = Hashpower.fromTreeCount(item.treeCount, hashpowerPercent);
const rewardSource = RewardSource.create(
RightType.CITY_AREA_RIGHT,
sourceOrderNo,
sourceUserId,
);
rewards.push(
RewardLedgerEntry.createSettleable({
userId: BigInt(item.accountSequence),
accountSequence: BigInt(item.accountSequence),
rewardSource,
usdtAmount: itemUsdtAmount,
hashpowerAmount: itemHashpower,
memo: `市区域权益(${cityCode})${item.reason}`,
}),
);
}
return rewards;
}
/**
* (80 USDT)
*
* -
* - /
*/
private async calculateCommunityRight(
sourceOrderNo: string,
sourceUserId: bigint,
treeCount: number,
): Promise<RewardLedgerEntry> {
): Promise<RewardLedgerEntry[]> {
const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.COMMUNITY_RIGHT];
const usdtAmount = Money.USDT(usdt * treeCount);
const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent);
const rewardSource = RewardSource.create(
RightType.COMMUNITY_RIGHT,
sourceOrderNo,
// 调用 authorization-service 获取社区权益分配方案
const distribution = await this.authorizationService.getCommunityRewardDistribution(
sourceUserId,
treeCount,
);
// 查找最近的社区
const nearestCommunity = await this.authorizationService.findNearestCommunity(sourceUserId);
const rewards: RewardLedgerEntry[] = [];
if (nearestCommunity) {
return RewardLedgerEntry.createSettleable({
userId: nearestCommunity,
accountSequence: nearestCommunity,
rewardSource,
usdtAmount,
hashpowerAmount: hashpower,
memo: '社区权益:来自社区成员的认种',
});
} else {
return RewardLedgerEntry.createSettleable({
userId: HEADQUARTERS_COMMUNITY_USER_ID,
accountSequence: HEADQUARTERS_COMMUNITY_USER_ID,
rewardSource,
usdtAmount,
hashpowerAmount: hashpower,
memo: '社区权益:无归属社区,进总部社区',
});
// 根据分配方案创建奖励记录
for (const item of distribution.distributions) {
const itemUsdtAmount = Money.USDT(usdt * item.treeCount);
const itemHashpower = Hashpower.fromTreeCount(item.treeCount, hashpowerPercent);
const rewardSource = RewardSource.create(
RightType.COMMUNITY_RIGHT,
sourceOrderNo,
sourceUserId,
);
rewards.push(
RewardLedgerEntry.createSettleable({
userId: BigInt(item.accountSequence),
accountSequence: BigInt(item.accountSequence),
rewardSource,
usdtAmount: itemUsdtAmount,
hashpowerAmount: itemHashpower,
memo: `社区权益:${item.reason}`,
}),
);
}
return rewards;
}
}

View File

@ -1,6 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { IAuthorizationServiceClient } from '../../../domain/services/reward-calculation.service';
import { IAuthorizationServiceClient, RewardDistribution, AreaRewardDistribution } from '../../../domain/services/reward-calculation.service';
// authorization-service 返回格式(经过 TransformInterceptor 包装)
interface AuthorizationServiceResponse<T> {
@ -13,6 +13,23 @@ interface NearestAuthorizationResult {
accountSequence: number | null;
}
interface RewardDistributionResult {
distributions: Array<{
accountSequence: number;
treeCount: number;
reason: string;
}>;
}
interface AreaRewardDistributionResult {
distributions: Array<{
accountSequence: number;
treeCount: number;
reason: string;
isSystemAccount: boolean;
}>;
}
@Injectable()
export class AuthorizationServiceClient implements IAuthorizationServiceClient {
private readonly logger = new Logger(AuthorizationServiceClient.name);
@ -85,4 +102,214 @@ export class AuthorizationServiceClient implements IAuthorizationServiceClient {
return null;
}
}
async getCommunityRewardDistribution(userId: bigint, treeCount: number): Promise<RewardDistribution> {
const HEADQUARTERS_ACCOUNT_SEQUENCE = 1;
const defaultDistribution: RewardDistribution = {
distributions: [
{
accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE,
treeCount,
reason: '服务调用失败,默认进总部社区',
},
],
};
try {
const response = await fetch(
`${this.baseUrl}/api/v1/authorization/community-reward-distribution?accountSequence=${userId}&treeCount=${treeCount}`,
);
if (!response.ok) {
this.logger.warn(`Failed to get community reward distribution for user ${userId}, treeCount ${treeCount}`);
return defaultDistribution;
}
// authorization-service 返回格式: { success, data: { distributions }, timestamp }
const result: AuthorizationServiceResponse<RewardDistributionResult> = await response.json();
if (!result.data?.distributions || result.data.distributions.length === 0) {
this.logger.warn(`Empty distributions returned for user ${userId}`);
return defaultDistribution;
}
this.logger.debug(
`getCommunityRewardDistribution for userId=${userId}, treeCount=${treeCount}: ` +
`distributions=${JSON.stringify(result.data.distributions)}`,
);
return result.data;
} catch (error) {
this.logger.error(`Error getting community reward distribution:`, error);
return defaultDistribution;
}
}
async getProvinceTeamRewardDistribution(userId: bigint, provinceCode: string, treeCount: number): Promise<RewardDistribution> {
const HEADQUARTERS_ACCOUNT_SEQUENCE = 1;
const defaultDistribution: RewardDistribution = {
distributions: [
{
accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE,
treeCount,
reason: '服务调用失败,默认进总部社区',
},
],
};
try {
const response = await fetch(
`${this.baseUrl}/api/v1/authorization/province-team-reward-distribution?accountSequence=${userId}&provinceCode=${provinceCode}&treeCount=${treeCount}`,
);
if (!response.ok) {
this.logger.warn(`Failed to get province team reward distribution for user ${userId}, province ${provinceCode}`);
return defaultDistribution;
}
const result: AuthorizationServiceResponse<RewardDistributionResult> = await response.json();
if (!result.data?.distributions || result.data.distributions.length === 0) {
this.logger.warn(`Empty province team distributions returned for user ${userId}`);
return defaultDistribution;
}
this.logger.debug(
`getProvinceTeamRewardDistribution for userId=${userId}, provinceCode=${provinceCode}, treeCount=${treeCount}: ` +
`distributions=${JSON.stringify(result.data.distributions)}`,
);
return result.data;
} catch (error) {
this.logger.error(`Error getting province team reward distribution:`, error);
return defaultDistribution;
}
}
async getProvinceAreaRewardDistribution(provinceCode: string, treeCount: number): Promise<AreaRewardDistribution> {
// 系统省账户ID: 9 + provinceCode (6位)
const systemProvinceAccountId = Number(`9${provinceCode.padStart(6, '0')}`);
const defaultDistribution: AreaRewardDistribution = {
distributions: [
{
accountSequence: systemProvinceAccountId,
treeCount,
reason: '服务调用失败,默认进系统省账户',
isSystemAccount: true,
},
],
};
try {
const response = await fetch(
`${this.baseUrl}/api/v1/authorization/province-area-reward-distribution?provinceCode=${provinceCode}&treeCount=${treeCount}`,
);
if (!response.ok) {
this.logger.warn(`Failed to get province area reward distribution for province ${provinceCode}`);
return defaultDistribution;
}
const result: AuthorizationServiceResponse<AreaRewardDistributionResult> = await response.json();
if (!result.data?.distributions || result.data.distributions.length === 0) {
this.logger.warn(`Empty province area distributions returned for province ${provinceCode}`);
return defaultDistribution;
}
this.logger.debug(
`getProvinceAreaRewardDistribution for provinceCode=${provinceCode}, treeCount=${treeCount}: ` +
`distributions=${JSON.stringify(result.data.distributions)}`,
);
return result.data;
} catch (error) {
this.logger.error(`Error getting province area reward distribution:`, error);
return defaultDistribution;
}
}
async getCityTeamRewardDistribution(userId: bigint, cityCode: string, treeCount: number): Promise<RewardDistribution> {
const HEADQUARTERS_ACCOUNT_SEQUENCE = 1;
const defaultDistribution: RewardDistribution = {
distributions: [
{
accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE,
treeCount,
reason: '服务调用失败,默认进总部社区',
},
],
};
try {
const response = await fetch(
`${this.baseUrl}/api/v1/authorization/city-team-reward-distribution?accountSequence=${userId}&cityCode=${cityCode}&treeCount=${treeCount}`,
);
if (!response.ok) {
this.logger.warn(`Failed to get city team reward distribution for user ${userId}, city ${cityCode}`);
return defaultDistribution;
}
const result: AuthorizationServiceResponse<RewardDistributionResult> = await response.json();
if (!result.data?.distributions || result.data.distributions.length === 0) {
this.logger.warn(`Empty city team distributions returned for user ${userId}`);
return defaultDistribution;
}
this.logger.debug(
`getCityTeamRewardDistribution for userId=${userId}, cityCode=${cityCode}, treeCount=${treeCount}: ` +
`distributions=${JSON.stringify(result.data.distributions)}`,
);
return result.data;
} catch (error) {
this.logger.error(`Error getting city team reward distribution:`, error);
return defaultDistribution;
}
}
async getCityAreaRewardDistribution(cityCode: string, treeCount: number): Promise<AreaRewardDistribution> {
// 系统市账户ID: 8 + cityCode (6位)
const systemCityAccountId = Number(`8${cityCode.padStart(6, '0')}`);
const defaultDistribution: AreaRewardDistribution = {
distributions: [
{
accountSequence: systemCityAccountId,
treeCount,
reason: '服务调用失败,默认进系统市账户',
isSystemAccount: true,
},
],
};
try {
const response = await fetch(
`${this.baseUrl}/api/v1/authorization/city-area-reward-distribution?cityCode=${cityCode}&treeCount=${treeCount}`,
);
if (!response.ok) {
this.logger.warn(`Failed to get city area reward distribution for city ${cityCode}`);
return defaultDistribution;
}
const result: AuthorizationServiceResponse<AreaRewardDistributionResult> = await response.json();
if (!result.data?.distributions || result.data.distributions.length === 0) {
this.logger.warn(`Empty city area distributions returned for city ${cityCode}`);
return defaultDistribution;
}
this.logger.debug(
`getCityAreaRewardDistribution for cityCode=${cityCode}, treeCount=${treeCount}: ` +
`distributions=${JSON.stringify(result.data.distributions)}`,
);
return result.data;
} catch (error) {
this.logger.error(`Error getting city area reward distribution:`, error);
return defaultDistribution;
}
}
}