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:
parent
033268deb9
commit
16d95999de
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue