feat(contribution): implement TEAM_BONUS backfill when unlock conditions met
When a user's direct referral count reaches 2 or 4, the system now automatically backfills previously pending TEAM_BONUS (T2/T3) contributions that were allocated to headquarters while waiting for unlock conditions. - Add BonusClaimService for handling bonus backfill logic - Add findPendingBonusByAccountSequence and claimBonusRecords to repository - Integrate bonus claim into updateReferrerUnlockStatus flow - Add BonusClaimed event consumer in mining-wallet-service - Generate ledger records for backfilled contributions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ef330a2687
commit
2a79c83715
|
|
@ -12,6 +12,7 @@ import { CDCEventDispatcher } from './event-handlers/cdc-event-dispatcher';
|
||||||
import { ContributionCalculationService } from './services/contribution-calculation.service';
|
import { ContributionCalculationService } from './services/contribution-calculation.service';
|
||||||
import { ContributionDistributionPublisherService } from './services/contribution-distribution-publisher.service';
|
import { ContributionDistributionPublisherService } from './services/contribution-distribution-publisher.service';
|
||||||
import { ContributionRateService } from './services/contribution-rate.service';
|
import { ContributionRateService } from './services/contribution-rate.service';
|
||||||
|
import { BonusClaimService } from './services/bonus-claim.service';
|
||||||
import { SnapshotService } from './services/snapshot.service';
|
import { SnapshotService } from './services/snapshot.service';
|
||||||
|
|
||||||
// Queries
|
// Queries
|
||||||
|
|
@ -38,6 +39,7 @@ import { ContributionScheduler } from './schedulers/contribution.scheduler';
|
||||||
ContributionCalculationService,
|
ContributionCalculationService,
|
||||||
ContributionDistributionPublisherService,
|
ContributionDistributionPublisherService,
|
||||||
ContributionRateService,
|
ContributionRateService,
|
||||||
|
BonusClaimService,
|
||||||
SnapshotService,
|
SnapshotService,
|
||||||
|
|
||||||
// Queries
|
// Queries
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { UnallocatedContributionRepository, UnallocatedContribution } from '../../infrastructure/persistence/repositories/unallocated-contribution.repository';
|
||||||
|
import { ContributionAccountRepository } from '../../infrastructure/persistence/repositories/contribution-account.repository';
|
||||||
|
import { ContributionRecordRepository } from '../../infrastructure/persistence/repositories/contribution-record.repository';
|
||||||
|
import { OutboxRepository } from '../../infrastructure/persistence/repositories/outbox.repository';
|
||||||
|
import { UnitOfWork } from '../../infrastructure/persistence/unit-of-work/unit-of-work';
|
||||||
|
import { ContributionRecordAggregate } from '../../domain/aggregates/contribution-record.aggregate';
|
||||||
|
import { ContributionSourceType } from '../../domain/aggregates/contribution-account.aggregate';
|
||||||
|
import { ContributionAmount } from '../../domain/value-objects/contribution-amount.vo';
|
||||||
|
import { DistributionRate } from '../../domain/value-objects/distribution-rate.vo';
|
||||||
|
import { ContributionRecordSyncedEvent } from '../../domain/events';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 奖励补发服务
|
||||||
|
* 当用户解锁新的奖励档位时,补发之前所有认种对应的奖励
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class BonusClaimService {
|
||||||
|
private readonly logger = new Logger(BonusClaimService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly unallocatedContributionRepository: UnallocatedContributionRepository,
|
||||||
|
private readonly contributionAccountRepository: ContributionAccountRepository,
|
||||||
|
private readonly contributionRecordRepository: ContributionRecordRepository,
|
||||||
|
private readonly outboxRepository: OutboxRepository,
|
||||||
|
private readonly unitOfWork: UnitOfWork,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查并处理奖励补发
|
||||||
|
* 当用户的直推认种人数变化时调用
|
||||||
|
* @param accountSequence 用户账号
|
||||||
|
* @param previousCount 之前的直推认种人数
|
||||||
|
* @param newCount 新的直推认种人数
|
||||||
|
*/
|
||||||
|
async checkAndClaimBonus(
|
||||||
|
accountSequence: string,
|
||||||
|
previousCount: number,
|
||||||
|
newCount: number,
|
||||||
|
): Promise<void> {
|
||||||
|
// 检查是否达到新的解锁条件
|
||||||
|
const tiersToClaimList: number[] = [];
|
||||||
|
|
||||||
|
// T2: 直推≥2人认种时解锁
|
||||||
|
if (previousCount < 2 && newCount >= 2) {
|
||||||
|
tiersToClaimList.push(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// T3: 直推≥4人认种时解锁
|
||||||
|
if (previousCount < 4 && newCount >= 4) {
|
||||||
|
tiersToClaimList.push(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tiersToClaimList.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`User ${accountSequence} unlocked bonus tiers: ${tiersToClaimList.join(', ')} ` +
|
||||||
|
`(directReferralAdoptedCount: ${previousCount} -> ${newCount})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 在事务中处理补发
|
||||||
|
await this.unitOfWork.executeInTransaction(async () => {
|
||||||
|
for (const tier of tiersToClaimList) {
|
||||||
|
await this.claimBonusTier(accountSequence, tier);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 补发指定档位的奖励
|
||||||
|
*/
|
||||||
|
private async claimBonusTier(accountSequence: string, bonusTier: number): Promise<void> {
|
||||||
|
// 1. 查询待领取的记录
|
||||||
|
const pendingRecords = await this.unallocatedContributionRepository.findPendingBonusByAccountSequence(
|
||||||
|
accountSequence,
|
||||||
|
bonusTier,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pendingRecords.length === 0) {
|
||||||
|
this.logger.debug(`No pending T${bonusTier} bonus records for ${accountSequence}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Claiming ${pendingRecords.length} T${bonusTier} bonus records for ${accountSequence}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. 创建贡献值记录
|
||||||
|
const contributionRecords: ContributionRecordAggregate[] = [];
|
||||||
|
for (const pending of pendingRecords) {
|
||||||
|
const record = new ContributionRecordAggregate({
|
||||||
|
accountSequence: accountSequence,
|
||||||
|
sourceType: ContributionSourceType.TEAM_BONUS,
|
||||||
|
sourceAdoptionId: pending.sourceAdoptionId,
|
||||||
|
sourceAccountSequence: pending.sourceAccountSequence,
|
||||||
|
treeCount: 0, // 补发记录不记录树数
|
||||||
|
baseContribution: new ContributionAmount(0),
|
||||||
|
distributionRate: DistributionRate.BONUS_PER,
|
||||||
|
bonusTier: bonusTier,
|
||||||
|
amount: pending.amount,
|
||||||
|
effectiveDate: pending.effectiveDate,
|
||||||
|
expireDate: pending.expireDate,
|
||||||
|
});
|
||||||
|
contributionRecords.push(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 保存贡献值记录
|
||||||
|
const savedRecords = await this.contributionRecordRepository.saveMany(contributionRecords);
|
||||||
|
|
||||||
|
// 4. 更新用户的贡献值账户
|
||||||
|
let totalAmount = new ContributionAmount(0);
|
||||||
|
for (const pending of pendingRecords) {
|
||||||
|
totalAmount = new ContributionAmount(totalAmount.value.plus(pending.amount.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.contributionAccountRepository.updateContribution(
|
||||||
|
accountSequence,
|
||||||
|
ContributionSourceType.TEAM_BONUS,
|
||||||
|
totalAmount,
|
||||||
|
null,
|
||||||
|
bonusTier,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. 标记待领取记录为已分配
|
||||||
|
const pendingIds = pendingRecords.map((r) => r.id);
|
||||||
|
await this.unallocatedContributionRepository.claimBonusRecords(pendingIds, accountSequence);
|
||||||
|
|
||||||
|
// 6. 发布事件到 Kafka(通过 Outbox)
|
||||||
|
await this.publishBonusClaimEvents(accountSequence, savedRecords, pendingRecords);
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Claimed T${bonusTier} bonus for ${accountSequence}: ` +
|
||||||
|
`${pendingRecords.length} records, total amount: ${totalAmount.value.toString()}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布补发事件
|
||||||
|
*/
|
||||||
|
private async publishBonusClaimEvents(
|
||||||
|
accountSequence: string,
|
||||||
|
savedRecords: ContributionRecordAggregate[],
|
||||||
|
pendingRecords: UnallocatedContribution[],
|
||||||
|
): Promise<void> {
|
||||||
|
// 1. 发布贡献值记录同步事件(用于 mining-admin-service CDC)
|
||||||
|
for (const record of savedRecords) {
|
||||||
|
const event = new ContributionRecordSyncedEvent(
|
||||||
|
record.id!,
|
||||||
|
record.accountSequence,
|
||||||
|
record.sourceType,
|
||||||
|
record.sourceAdoptionId,
|
||||||
|
record.sourceAccountSequence,
|
||||||
|
record.treeCount,
|
||||||
|
record.baseContribution.value.toString(),
|
||||||
|
record.distributionRate.value.toString(),
|
||||||
|
record.levelDepth,
|
||||||
|
record.bonusTier,
|
||||||
|
record.amount.value.toString(),
|
||||||
|
record.effectiveDate,
|
||||||
|
record.expireDate,
|
||||||
|
record.isExpired,
|
||||||
|
record.createdAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.outboxRepository.save({
|
||||||
|
aggregateType: ContributionRecordSyncedEvent.AGGREGATE_TYPE,
|
||||||
|
aggregateId: record.id!.toString(),
|
||||||
|
eventType: ContributionRecordSyncedEvent.EVENT_TYPE,
|
||||||
|
payload: event.toPayload(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 发布补发事件到 mining-wallet-service
|
||||||
|
const userContributions = savedRecords.map((record, index) => ({
|
||||||
|
accountSequence: record.accountSequence,
|
||||||
|
contributionType: 'TEAM_BONUS',
|
||||||
|
amount: record.amount.value.toString(),
|
||||||
|
bonusTier: record.bonusTier,
|
||||||
|
effectiveDate: record.effectiveDate.toISOString(),
|
||||||
|
expireDate: record.expireDate.toISOString(),
|
||||||
|
sourceAdoptionId: record.sourceAdoptionId.toString(),
|
||||||
|
sourceAccountSequence: record.sourceAccountSequence,
|
||||||
|
isBackfill: true, // 标记为补发
|
||||||
|
}));
|
||||||
|
|
||||||
|
const eventId = `bonus-claim-${accountSequence}-${Date.now()}`;
|
||||||
|
const payload = {
|
||||||
|
eventType: 'BonusClaimed',
|
||||||
|
eventId,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
payload: {
|
||||||
|
accountSequence,
|
||||||
|
bonusTier: savedRecords[0]?.bonusTier,
|
||||||
|
claimedCount: savedRecords.length,
|
||||||
|
userContributions,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.outboxRepository.save({
|
||||||
|
eventType: 'BonusClaimed',
|
||||||
|
topic: 'contribution.bonus.claimed',
|
||||||
|
key: accountSequence,
|
||||||
|
payload,
|
||||||
|
aggregateId: accountSequence,
|
||||||
|
aggregateType: 'ContributionAccount',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ import { ContributionRecordAggregate } from '../../domain/aggregates/contributio
|
||||||
import { SyncedReferral } from '../../domain/repositories/synced-data.repository.interface';
|
import { SyncedReferral } from '../../domain/repositories/synced-data.repository.interface';
|
||||||
import { ContributionDistributionPublisherService } from './contribution-distribution-publisher.service';
|
import { ContributionDistributionPublisherService } from './contribution-distribution-publisher.service';
|
||||||
import { ContributionRateService } from './contribution-rate.service';
|
import { ContributionRateService } from './contribution-rate.service';
|
||||||
|
import { BonusClaimService } from './bonus-claim.service';
|
||||||
import { ContributionRecordSyncedEvent, NetworkProgressUpdatedEvent, ContributionAccountUpdatedEvent } from '../../domain/events';
|
import { ContributionRecordSyncedEvent, NetworkProgressUpdatedEvent, ContributionAccountUpdatedEvent } from '../../domain/events';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -33,6 +34,7 @@ export class ContributionCalculationService {
|
||||||
private readonly unitOfWork: UnitOfWork,
|
private readonly unitOfWork: UnitOfWork,
|
||||||
private readonly distributionPublisher: ContributionDistributionPublisherService,
|
private readonly distributionPublisher: ContributionDistributionPublisherService,
|
||||||
private readonly contributionRateService: ContributionRateService,
|
private readonly contributionRateService: ContributionRateService,
|
||||||
|
private readonly bonusClaimService: BonusClaimService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -330,6 +332,7 @@ export class ContributionCalculationService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新上线的解锁状态(直推用户认种后)
|
* 更新上线的解锁状态(直推用户认种后)
|
||||||
|
* 如果解锁了新的奖励档位,会触发补发逻辑
|
||||||
*/
|
*/
|
||||||
private async updateReferrerUnlockStatus(referrerAccountSequence: string): Promise<void> {
|
private async updateReferrerUnlockStatus(referrerAccountSequence: string): Promise<void> {
|
||||||
const account = await this.contributionAccountRepository.findByAccountSequence(referrerAccountSequence);
|
const account = await this.contributionAccountRepository.findByAccountSequence(referrerAccountSequence);
|
||||||
|
|
@ -341,10 +344,10 @@ export class ContributionCalculationService {
|
||||||
);
|
);
|
||||||
|
|
||||||
// 更新解锁状态
|
// 更新解锁状态
|
||||||
const currentCount = account.directReferralAdoptedCount;
|
const previousCount = account.directReferralAdoptedCount;
|
||||||
if (directReferralAdoptedCount > currentCount) {
|
if (directReferralAdoptedCount > previousCount) {
|
||||||
// 需要增量更新
|
// 需要增量更新
|
||||||
for (let i = currentCount; i < directReferralAdoptedCount; i++) {
|
for (let i = previousCount; i < directReferralAdoptedCount; i++) {
|
||||||
account.incrementDirectReferralAdoptedCount();
|
account.incrementDirectReferralAdoptedCount();
|
||||||
}
|
}
|
||||||
await this.contributionAccountRepository.save(account);
|
await this.contributionAccountRepository.save(account);
|
||||||
|
|
@ -355,6 +358,13 @@ export class ContributionCalculationService {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Updated referrer ${referrerAccountSequence} unlock status: level=${account.unlockedLevelDepth}, bonus=${account.unlockedBonusTiers}`,
|
`Updated referrer ${referrerAccountSequence} unlock status: level=${account.unlockedLevelDepth}, bonus=${account.unlockedBonusTiers}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 检查并处理奖励补发(T2: 直推≥2人, T3: 直推≥4人)
|
||||||
|
await this.bonusClaimService.checkAndClaimBonus(
|
||||||
|
referrerAccountSequence,
|
||||||
|
previousCount,
|
||||||
|
directReferralAdoptedCount,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,16 @@ export interface UnallocatedContribution {
|
||||||
unallocType: string;
|
unallocType: string;
|
||||||
wouldBeAccountSequence: string | null;
|
wouldBeAccountSequence: string | null;
|
||||||
levelDepth: number | null;
|
levelDepth: number | null;
|
||||||
|
bonusTier: number | null;
|
||||||
amount: ContributionAmount;
|
amount: ContributionAmount;
|
||||||
reason: string | null;
|
reason: string | null;
|
||||||
sourceAdoptionId: bigint;
|
sourceAdoptionId: bigint;
|
||||||
sourceAccountSequence: string;
|
sourceAccountSequence: string;
|
||||||
effectiveDate: Date;
|
effectiveDate: Date;
|
||||||
expireDate: Date;
|
expireDate: Date;
|
||||||
allocatedToHeadquarters: boolean;
|
status: string;
|
||||||
allocatedAt: Date | null;
|
allocatedAt: Date | null;
|
||||||
|
allocatedToAccountSequence: string | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,20 +132,82 @@ export class UnallocatedContributionRepository {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户待领取的奖励档位贡献值
|
||||||
|
* @param accountSequence 用户账号
|
||||||
|
* @param bonusTier 奖励档位 (2 或 3)
|
||||||
|
*/
|
||||||
|
async findPendingBonusByAccountSequence(
|
||||||
|
accountSequence: string,
|
||||||
|
bonusTier: number,
|
||||||
|
): Promise<UnallocatedContribution[]> {
|
||||||
|
const records = await this.client.unallocatedContribution.findMany({
|
||||||
|
where: {
|
||||||
|
wouldBeAccountSequence: accountSequence,
|
||||||
|
unallocType: `BONUS_TIER_${bonusTier}`,
|
||||||
|
status: 'PENDING',
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return records.map((r) => this.toDomain(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 领取奖励档位 - 将待领取记录标记为已分配给用户
|
||||||
|
* @param ids 记录ID列表
|
||||||
|
* @param accountSequence 分配给的用户账号
|
||||||
|
*/
|
||||||
|
async claimBonusRecords(ids: bigint[], accountSequence: string): Promise<void> {
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
|
||||||
|
await this.client.unallocatedContribution.updateMany({
|
||||||
|
where: {
|
||||||
|
id: { in: ids },
|
||||||
|
status: 'PENDING',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: 'ALLOCATED_TO_USER',
|
||||||
|
allocatedAt: new Date(),
|
||||||
|
allocatedToAccountSequence: accountSequence,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户所有待领取的奖励(所有档位)
|
||||||
|
*/
|
||||||
|
async findAllPendingBonusByAccountSequence(
|
||||||
|
accountSequence: string,
|
||||||
|
): Promise<UnallocatedContribution[]> {
|
||||||
|
const records = await this.client.unallocatedContribution.findMany({
|
||||||
|
where: {
|
||||||
|
wouldBeAccountSequence: accountSequence,
|
||||||
|
unallocType: { startsWith: 'BONUS_TIER_' },
|
||||||
|
status: 'PENDING',
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return records.map((r) => this.toDomain(r));
|
||||||
|
}
|
||||||
|
|
||||||
private toDomain(record: any): UnallocatedContribution {
|
private toDomain(record: any): UnallocatedContribution {
|
||||||
return {
|
return {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
unallocType: record.unallocType,
|
unallocType: record.unallocType,
|
||||||
wouldBeAccountSequence: record.wouldBeAccountSequence,
|
wouldBeAccountSequence: record.wouldBeAccountSequence,
|
||||||
levelDepth: record.levelDepth,
|
levelDepth: record.levelDepth,
|
||||||
|
bonusTier: record.bonusTier,
|
||||||
amount: new ContributionAmount(record.amount),
|
amount: new ContributionAmount(record.amount),
|
||||||
reason: record.reason,
|
reason: record.reason,
|
||||||
sourceAdoptionId: record.sourceAdoptionId,
|
sourceAdoptionId: record.sourceAdoptionId,
|
||||||
sourceAccountSequence: record.sourceAccountSequence,
|
sourceAccountSequence: record.sourceAccountSequence,
|
||||||
effectiveDate: record.effectiveDate,
|
effectiveDate: record.effectiveDate,
|
||||||
expireDate: record.expireDate,
|
expireDate: record.expireDate,
|
||||||
allocatedToHeadquarters: record.allocatedToHeadquarters,
|
status: record.status,
|
||||||
allocatedAt: record.allocatedAt,
|
allocatedAt: record.allocatedAt,
|
||||||
|
allocatedToAccountSequence: record.allocatedToAccountSequence,
|
||||||
createdAt: record.createdAt,
|
createdAt: record.createdAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import { SystemAccountService } from '../../../application/services/system-accou
|
||||||
import {
|
import {
|
||||||
ContributionDistributionCompletedEvent,
|
ContributionDistributionCompletedEvent,
|
||||||
ContributionDistributionPayload,
|
ContributionDistributionPayload,
|
||||||
|
BonusClaimedEvent,
|
||||||
|
BonusClaimedPayload,
|
||||||
} from '../events/contribution-distribution.event';
|
} from '../events/contribution-distribution.event';
|
||||||
|
|
||||||
// 4小时 TTL(秒)
|
// 4小时 TTL(秒)
|
||||||
|
|
@ -114,6 +116,65 @@ export class ContributionDistributionConsumer implements OnModuleInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理奖励补发事件
|
||||||
|
* 当用户解锁新的奖励档位时,补发之前所有认种对应的奖励
|
||||||
|
*/
|
||||||
|
@EventPattern('contribution.bonus.claimed')
|
||||||
|
async handleBonusClaimed(@Payload() message: any): Promise<void> {
|
||||||
|
const event: BonusClaimedEvent = message.value || message;
|
||||||
|
const eventId = event.eventId || message.eventId;
|
||||||
|
|
||||||
|
if (!eventId) {
|
||||||
|
this.logger.warn('Received BonusClaimed event without eventId, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`Processing bonus claim event: ${eventId}`);
|
||||||
|
|
||||||
|
// 幂等性检查
|
||||||
|
if (await this.isEventProcessed(eventId)) {
|
||||||
|
this.logger.debug(`Event ${eventId} already processed, skipping`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.processBonusClaim(event.payload);
|
||||||
|
|
||||||
|
// 标记为已处理
|
||||||
|
await this.markEventProcessed(eventId, event.eventType);
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Bonus claim for ${event.payload.accountSequence} T${event.payload.bonusTier} processed: ` +
|
||||||
|
`${event.payload.claimedCount} records`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to process bonus claim for ${event.payload.accountSequence}`,
|
||||||
|
error instanceof Error ? error.stack : error,
|
||||||
|
);
|
||||||
|
throw error; // 让 Kafka 重试
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理奖励补发
|
||||||
|
*/
|
||||||
|
private async processBonusClaim(payload: BonusClaimedPayload): Promise<void> {
|
||||||
|
for (const contrib of payload.userContributions) {
|
||||||
|
await this.contributionWalletService.creditContribution({
|
||||||
|
accountSequence: contrib.accountSequence,
|
||||||
|
amount: new Decimal(contrib.amount),
|
||||||
|
contributionType: contrib.contributionType,
|
||||||
|
bonusTier: contrib.bonusTier,
|
||||||
|
effectiveDate: new Date(contrib.effectiveDate),
|
||||||
|
expireDate: new Date(contrib.expireDate),
|
||||||
|
sourceAdoptionId: contrib.sourceAdoptionId,
|
||||||
|
sourceAccountSequence: contrib.sourceAccountSequence,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 幂等性检查 - Redis + DB 双重检查,4小时去重窗口
|
* 幂等性检查 - Redis + DB 双重检查,4小时去重窗口
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,36 @@ export interface UnallocatedContributionItem {
|
||||||
bonusTier?: number;
|
bonusTier?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 奖励补发事件
|
||||||
|
* 来自 contribution-service,当用户解锁新的奖励档位时触发
|
||||||
|
*/
|
||||||
|
export interface BonusClaimedEvent {
|
||||||
|
eventType: 'BonusClaimed';
|
||||||
|
eventId: string;
|
||||||
|
timestamp: string;
|
||||||
|
payload: BonusClaimedPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BonusClaimedPayload {
|
||||||
|
accountSequence: string;
|
||||||
|
bonusTier: number;
|
||||||
|
claimedCount: number;
|
||||||
|
userContributions: BonusClaimedContributionItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BonusClaimedContributionItem {
|
||||||
|
accountSequence: string;
|
||||||
|
contributionType: 'TEAM_BONUS';
|
||||||
|
amount: string;
|
||||||
|
bonusTier: number;
|
||||||
|
effectiveDate: string;
|
||||||
|
expireDate: string;
|
||||||
|
sourceAdoptionId: string;
|
||||||
|
sourceAccountSequence: string;
|
||||||
|
isBackfill: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户注册事件
|
* 用户注册事件
|
||||||
* 来自 auth-service
|
* 来自 auth-service
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue