277 lines
9.3 KiB
TypeScript
277 lines
9.3 KiB
TypeScript
import Decimal from 'decimal.js';
|
||
import { ContributionAmount } from '../value-objects/contribution-amount.vo';
|
||
import { DistributionRate } from '../value-objects/distribution-rate.vo';
|
||
import { ContributionAccountAggregate, ContributionSourceType } from '../aggregates/contribution-account.aggregate';
|
||
import { ContributionRecordAggregate } from '../aggregates/contribution-record.aggregate';
|
||
import { SyncedAdoption, SyncedReferral } from '../repositories/synced-data.repository.interface';
|
||
|
||
/**
|
||
* 系统账户贡献值分配
|
||
*/
|
||
export interface SystemContributionAllocation {
|
||
accountType: 'OPERATION' | 'PROVINCE' | 'CITY' | 'HEADQUARTERS';
|
||
regionCode: string | null; // 省市代码,如 440000、440100
|
||
rate: DistributionRate;
|
||
amount: ContributionAmount;
|
||
}
|
||
|
||
/**
|
||
* 算力分配结果
|
||
*/
|
||
export interface ContributionDistributionResult {
|
||
// 个人贡献值记录
|
||
personalRecord: ContributionRecordAggregate;
|
||
|
||
// 团队层级贡献值记录(给上线们的)
|
||
teamLevelRecords: ContributionRecordAggregate[];
|
||
|
||
// 团队额外奖励贡献值记录(给认种人自己的)
|
||
teamBonusRecords: ContributionRecordAggregate[];
|
||
|
||
// 未分配的贡献值(归总部)
|
||
unallocatedContributions: {
|
||
type: string;
|
||
wouldBeAccountSequence: string | null;
|
||
levelDepth: number | null;
|
||
amount: ContributionAmount;
|
||
reason: string;
|
||
}[];
|
||
|
||
// 系统账户贡献值(支持按省市细分)
|
||
systemContributions: SystemContributionAllocation[];
|
||
}
|
||
|
||
/**
|
||
* 贡献值计算领域服务
|
||
* 负责核心的算力计算逻辑
|
||
*/
|
||
export class ContributionCalculatorService {
|
||
// 贡献值有效期:2年
|
||
private static readonly CONTRIBUTION_VALIDITY_YEARS = 2;
|
||
|
||
/**
|
||
* 计算认种产生的贡献值分配
|
||
*
|
||
* @param adoption 认种记录
|
||
* @param adopterAccount 认种人的算力账户(用于判断 TEAM_BONUS 解锁状态)
|
||
* @param ancestorChain 上线链条(从直接上线开始,最多15级)
|
||
* @param ancestorAccounts 上线的算力账户(用于判断 TEAM_LEVEL 解锁状态)
|
||
* @returns 分配结果
|
||
*/
|
||
calculateAdoptionContribution(
|
||
adoption: SyncedAdoption,
|
||
adopterAccount: ContributionAccountAggregate | null,
|
||
ancestorChain: SyncedReferral[],
|
||
ancestorAccounts: Map<string, ContributionAccountAggregate>,
|
||
): ContributionDistributionResult {
|
||
const baseContribution = new ContributionAmount(adoption.contributionPerTree);
|
||
const treeCount = adoption.treeCount;
|
||
const totalContribution = baseContribution.multiply(treeCount);
|
||
|
||
// 计算生效日期(次日)和过期日期(2年后)
|
||
const effectiveDate = this.getNextDay(adoption.adoptionDate);
|
||
const expireDate = this.addYears(effectiveDate, ContributionCalculatorService.CONTRIBUTION_VALIDITY_YEARS);
|
||
|
||
const result: ContributionDistributionResult = {
|
||
personalRecord: null as any,
|
||
teamLevelRecords: [],
|
||
teamBonusRecords: [],
|
||
unallocatedContributions: [],
|
||
systemContributions: [],
|
||
};
|
||
|
||
// 1. 个人贡献值 (70%)
|
||
result.personalRecord = ContributionRecordAggregate.createPersonal({
|
||
accountSequence: adoption.accountSequence,
|
||
sourceAdoptionId: adoption.originalAdoptionId,
|
||
treeCount,
|
||
baseContribution,
|
||
effectiveDate,
|
||
expireDate,
|
||
});
|
||
|
||
// 2. 系统账户贡献值 (15%)
|
||
// 运营账户(全国)- 12%
|
||
result.systemContributions.push({
|
||
accountType: 'OPERATION',
|
||
regionCode: null,
|
||
rate: DistributionRate.OPERATION,
|
||
amount: totalContribution.multiply(DistributionRate.OPERATION.value),
|
||
});
|
||
|
||
// 省公司账户 - 1%(按认种选择的省份)
|
||
const provinceCode = adoption.selectedProvince;
|
||
result.systemContributions.push({
|
||
accountType: 'PROVINCE',
|
||
regionCode: provinceCode || null,
|
||
rate: DistributionRate.PROVINCE,
|
||
amount: totalContribution.multiply(DistributionRate.PROVINCE.value),
|
||
});
|
||
|
||
// 市公司账户 - 2%(按认种选择的城市)
|
||
const cityCode = adoption.selectedCity;
|
||
result.systemContributions.push({
|
||
accountType: 'CITY',
|
||
regionCode: cityCode || null,
|
||
rate: DistributionRate.CITY,
|
||
amount: totalContribution.multiply(DistributionRate.CITY.value),
|
||
});
|
||
|
||
// 3. 团队贡献值 (15%)
|
||
this.distributeTeamContribution(
|
||
adoption,
|
||
adopterAccount,
|
||
baseContribution,
|
||
treeCount,
|
||
ancestorChain,
|
||
ancestorAccounts,
|
||
effectiveDate,
|
||
expireDate,
|
||
result,
|
||
);
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 分配团队贡献值
|
||
*/
|
||
private distributeTeamContribution(
|
||
adoption: SyncedAdoption,
|
||
adopterAccount: ContributionAccountAggregate | null,
|
||
baseContribution: ContributionAmount,
|
||
treeCount: number,
|
||
ancestorChain: SyncedReferral[],
|
||
ancestorAccounts: Map<string, ContributionAccountAggregate>,
|
||
effectiveDate: Date,
|
||
expireDate: Date,
|
||
result: ContributionDistributionResult,
|
||
): void {
|
||
// 3.1 层级部分 (7.5% = 0.5% × 15级)
|
||
for (let level = 1; level <= 15; level++) {
|
||
const ancestor = ancestorChain[level - 1];
|
||
|
||
if (!ancestor) {
|
||
// 没有这一级的上线,归总部
|
||
const levelAmount = baseContribution.multiply(treeCount).multiply(DistributionRate.LEVEL_PER.value);
|
||
result.unallocatedContributions.push({
|
||
type: 'LEVEL_NO_ANCESTOR',
|
||
wouldBeAccountSequence: null,
|
||
levelDepth: level,
|
||
amount: levelAmount,
|
||
reason: `第${level}级无上线`,
|
||
});
|
||
continue;
|
||
}
|
||
|
||
const ancestorAccount = ancestorAccounts.get(ancestor.accountSequence);
|
||
const unlockedLevelDepth = ancestorAccount?.unlockedLevelDepth ?? 0;
|
||
|
||
if (unlockedLevelDepth >= level) {
|
||
// 上线已解锁该级别
|
||
result.teamLevelRecords.push(
|
||
ContributionRecordAggregate.createTeamLevel({
|
||
accountSequence: ancestor.accountSequence,
|
||
sourceAdoptionId: adoption.originalAdoptionId,
|
||
sourceAccountSequence: adoption.accountSequence,
|
||
treeCount,
|
||
baseContribution,
|
||
levelDepth: level,
|
||
effectiveDate,
|
||
expireDate,
|
||
}),
|
||
);
|
||
} else {
|
||
// 上线未解锁该级别,归总部
|
||
const levelAmount = baseContribution.multiply(treeCount).multiply(DistributionRate.LEVEL_PER.value);
|
||
result.unallocatedContributions.push({
|
||
type: 'LEVEL_OVERFLOW',
|
||
wouldBeAccountSequence: ancestor.accountSequence,
|
||
levelDepth: level,
|
||
amount: levelAmount,
|
||
reason: `上线${ancestor.accountSequence}未解锁第${level}级(已解锁${unlockedLevelDepth}级)`,
|
||
});
|
||
}
|
||
}
|
||
|
||
// 3.2 额外奖励部分 (7.5% = 2.5% × 3档) - 给认种人自己
|
||
// T1: 自己认种即解锁(无条件,当前正在认种所以一定解锁)
|
||
// T2: 直推≥2人认种解锁
|
||
// T3: 直推≥4人认种解锁
|
||
const directReferralAdoptedCount = adopterAccount?.directReferralAdoptedCount ?? 0;
|
||
|
||
// T1 始终解锁(因为当前正在认种)
|
||
// T2 需要直推≥2人认种
|
||
// T3 需要直推≥4人认种
|
||
const effectiveUnlockedBonusTiers = this.calculateUnlockedBonusTiers(true, directReferralAdoptedCount);
|
||
|
||
for (let tier = 1; tier <= 3; tier++) {
|
||
if (effectiveUnlockedBonusTiers >= tier) {
|
||
// 认种人已解锁该档位,给认种人自己
|
||
result.teamBonusRecords.push(
|
||
ContributionRecordAggregate.createTeamBonus({
|
||
accountSequence: adoption.accountSequence,
|
||
sourceAdoptionId: adoption.originalAdoptionId,
|
||
sourceAccountSequence: adoption.accountSequence,
|
||
treeCount,
|
||
baseContribution,
|
||
bonusTier: tier,
|
||
effectiveDate,
|
||
expireDate,
|
||
}),
|
||
);
|
||
} else {
|
||
// 认种人未解锁该档位,归总部(冻结/待领取)
|
||
const bonusAmount = baseContribution.multiply(treeCount).multiply(DistributionRate.BONUS_PER.value);
|
||
result.unallocatedContributions.push({
|
||
type: `BONUS_TIER_${tier}`,
|
||
wouldBeAccountSequence: adoption.accountSequence,
|
||
levelDepth: null,
|
||
amount: bonusAmount,
|
||
reason: `认种人${adoption.accountSequence}未解锁第${tier}档奖励(需直推${tier === 2 ? '2人' : '4人'}认种)`,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据直推认种用户数计算解锁层级
|
||
*/
|
||
calculateUnlockedLevelDepth(directReferralAdoptedCount: number): number {
|
||
if (directReferralAdoptedCount >= 5) return 15;
|
||
if (directReferralAdoptedCount >= 3) return 10;
|
||
if (directReferralAdoptedCount >= 1) return 5;
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* 根据条件计算解锁的额外奖励档位数
|
||
*/
|
||
calculateUnlockedBonusTiers(hasAdopted: boolean, directReferralAdoptedCount: number): number {
|
||
let tiers = 0;
|
||
if (hasAdopted) tiers++;
|
||
if (directReferralAdoptedCount >= 2) tiers++;
|
||
if (directReferralAdoptedCount >= 4) tiers++;
|
||
return tiers;
|
||
}
|
||
|
||
/**
|
||
* 获取次日日期
|
||
*/
|
||
private getNextDay(date: Date): Date {
|
||
const nextDay = new Date(date);
|
||
nextDay.setDate(nextDay.getDate() + 1);
|
||
nextDay.setHours(0, 0, 0, 0);
|
||
return nextDay;
|
||
}
|
||
|
||
/**
|
||
* 添加年数
|
||
*/
|
||
private addYears(date: Date, years: number): Date {
|
||
const result = new Date(date);
|
||
result.setFullYear(result.getFullYear() + years);
|
||
return result;
|
||
}
|
||
}
|