rwadurian/backend/services/referral-service/src/application/event-handlers/planting-transferred.handle...

317 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 转让事件处理器(纯新增)
* 消费 planting.ownership.removed + planting.ownership.added 事件
* 更新卖方/买方及其所有上级的团队统计
*
* 回滚方式:删除此文件并从 event-handlers/index.ts 和 application.module.ts 中移除引用
*/
import { Injectable, Inject, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
IReferralRelationshipRepository,
REFERRAL_RELATIONSHIP_REPOSITORY,
ITeamStatisticsRepository,
TEAM_STATISTICS_REPOSITORY,
} from '../../domain';
import { KafkaService } from '../../infrastructure/messaging/kafka.service';
import { EventAckPublisher } from '../../infrastructure/kafka/event-ack.publisher';
import { PrismaService } from '../../infrastructure/database/prisma.service';
interface PlantingOwnershipRemovedEvent {
transferOrderNo: string;
sourceOrderNo: string;
sellerUserId: string;
sellerAccountSequence: string;
treeCount: number;
contributionPerTree: string;
selectedProvince: string;
selectedCity: string;
originalAdoptionDate: string;
originalExpireDate: string;
_outbox?: {
id: string;
aggregateId: string;
eventType: string;
ackTopic: string;
};
}
interface PlantingOwnershipAddedEvent {
transferOrderNo: string;
sourceOrderNo: string;
buyerUserId: string;
buyerAccountSequence: string;
treeCount: number;
contributionPerTree: string;
selectedProvince: string;
selectedCity: string;
originalAdoptionDate: string;
originalExpireDate: string;
_outbox?: {
id: string;
aggregateId: string;
eventType: string;
ackTopic: string;
};
}
@Injectable()
export class PlantingTransferredHandler implements OnModuleInit {
private readonly logger = new Logger(PlantingTransferredHandler.name);
constructor(
private readonly kafkaService: KafkaService,
private readonly eventAckPublisher: EventAckPublisher,
private readonly prisma: PrismaService,
@Inject(REFERRAL_RELATIONSHIP_REPOSITORY)
private readonly referralRepo: IReferralRelationshipRepository,
@Inject(TEAM_STATISTICS_REPOSITORY)
private readonly teamStatsRepo: ITeamStatisticsRepository,
) {}
async onModuleInit() {
// 订阅卖方减持事件
await this.kafkaService.subscribe(
'referral-service-transfer-removed',
['planting.ownership.removed'],
this.handleOwnershipRemoved.bind(this),
);
// 订阅买方增持事件
await this.kafkaService.subscribe(
'referral-service-transfer-added',
['planting.ownership.added'],
this.handleOwnershipAdded.bind(this),
);
this.logger.log('Subscribed to planting.ownership.removed + planting.ownership.added');
}
/**
* 处理卖方减持事件
* 1. 更新卖方个人认种统计selfPlantingCount -= treeCount
* 2. 更新卖方所有上级的团队认种统计(负数 delta重新扫描大区
*/
private async handleOwnershipRemoved(
topic: string,
message: Record<string, unknown>,
): Promise<void> {
const event = message as unknown as PlantingOwnershipRemovedEvent;
const outboxInfo = event._outbox;
const eventId = outboxInfo?.aggregateId || event.transferOrderNo;
this.logger.log(
`[TRANSFER-REMOVED] Processing: transferOrderNo=${event.transferOrderNo}, ` +
`seller=${event.sellerAccountSequence}, trees=${event.treeCount}`,
);
try {
// 幂等性检查
const processedEventId = `removed:${eventId}`;
const existing = await this.prisma.processedEvent.findUnique({
where: { eventId: processedEventId },
});
if (existing) {
this.logger.log(`[TRANSFER-REMOVED] Already processed: ${processedEventId}`);
if (outboxInfo) {
await this.eventAckPublisher.sendSuccess(eventId, outboxInfo.eventType);
}
return;
}
// 1. 查找卖方推荐关系
const relationship = await this.referralRepo.findByAccountSequence(
event.sellerAccountSequence,
);
if (!relationship) {
this.logger.warn(
`[TRANSFER-REMOVED] No referral relationship for ${event.sellerAccountSequence}`,
);
await this.markProcessedAndAck(processedEventId, 'PlantingOwnershipRemoved', outboxInfo, eventId);
return;
}
const userId = relationship.userId;
// 2. 更新卖方个人认种统计
const sellerStats = await this.teamStatsRepo.findByUserId(userId);
if (sellerStats) {
sellerStats.removePersonalPlanting(
event.treeCount,
event.selectedProvince,
event.selectedCity,
);
await this.teamStatsRepo.save(sellerStats);
sellerStats.clearDomainEvents();
}
// 3. 更新卖方所有上级的团队统计(负数 delta
const ancestors = relationship.getAllAncestorIds();
if (ancestors.length > 0) {
const updates = ancestors.map((ancestorId, i) => ({
userId: ancestorId,
countDelta: -event.treeCount, // 关键:负数
provinceCode: event.selectedProvince,
cityCode: event.selectedCity,
fromDirectReferralId: i === 0 ? userId : ancestors[0],
}));
await this.teamStatsRepo.batchUpdateTeamCountsForTransfer(updates);
}
// 4. 记录已处理 + 发送 ACK
await this.markProcessedAndAck(processedEventId, 'PlantingOwnershipRemoved', outboxInfo, eventId);
this.logger.log(
`[TRANSFER-REMOVED] ✓ Processed: seller=${event.sellerAccountSequence}, ` +
`trees=${event.treeCount}, ancestors=${ancestors.length}`,
);
} catch (error) {
this.logger.error(
`[TRANSFER-REMOVED] ✗ Failed for ${event.transferOrderNo}:`,
error,
);
if (outboxInfo) {
const msg = error instanceof Error ? error.message : String(error);
await this.eventAckPublisher.sendFailure(eventId, outboxInfo.eventType, msg);
}
throw error;
}
}
/**
* 处理买方增持事件
* 1. 更新买方个人认种统计selfPlantingCount += treeCount
* 2. 更新买方所有上级的团队认种统计(正数 delta复用现有方法
* 3. 两方处理完后发布确认事件通知 Saga
*/
private async handleOwnershipAdded(
topic: string,
message: Record<string, unknown>,
): Promise<void> {
const event = message as unknown as PlantingOwnershipAddedEvent;
const outboxInfo = event._outbox;
const eventId = outboxInfo?.aggregateId || event.transferOrderNo;
this.logger.log(
`[TRANSFER-ADDED] Processing: transferOrderNo=${event.transferOrderNo}, ` +
`buyer=${event.buyerAccountSequence}, trees=${event.treeCount}`,
);
try {
// 幂等性检查
const processedEventId = `added:${eventId}`;
const existing = await this.prisma.processedEvent.findUnique({
where: { eventId: processedEventId },
});
if (existing) {
this.logger.log(`[TRANSFER-ADDED] Already processed: ${processedEventId}`);
if (outboxInfo) {
await this.eventAckPublisher.sendSuccess(eventId, outboxInfo.eventType);
}
return;
}
// 1. 查找买方推荐关系
const relationship = await this.referralRepo.findByAccountSequence(
event.buyerAccountSequence,
);
if (!relationship) {
this.logger.warn(
`[TRANSFER-ADDED] No referral relationship for ${event.buyerAccountSequence}`,
);
await this.markProcessedAndAck(processedEventId, 'PlantingOwnershipAdded', outboxInfo, eventId);
return;
}
const userId = relationship.userId;
// 2. 更新买方个人认种统计
const buyerStats = await this.teamStatsRepo.findByUserId(userId);
if (buyerStats) {
buyerStats.addPersonalPlanting(
event.treeCount,
event.selectedProvince,
event.selectedCity,
);
await this.teamStatsRepo.save(buyerStats);
buyerStats.clearDomainEvents();
}
// 3. 更新买方所有上级的团队统计(正数 delta复用现有方法
const ancestors = relationship.getAllAncestorIds();
if (ancestors.length > 0) {
const updates = ancestors.map((ancestorId, i) => ({
userId: ancestorId,
countDelta: event.treeCount, // 正数
provinceCode: event.selectedProvince,
cityCode: event.selectedCity,
fromDirectReferralId: i === 0 ? userId : ancestors[0],
}));
await this.teamStatsRepo.batchUpdateTeamCounts(updates);
}
// 4. 记录已处理 + 发送 ACK
await this.markProcessedAndAck(processedEventId, 'PlantingOwnershipAdded', outboxInfo, eventId);
// 5. 两方都处理完后,发布确认事件通知 transfer-service Saga 推进
await this.publishTransferStatsUpdated(event.transferOrderNo);
this.logger.log(
`[TRANSFER-ADDED] ✓ Processed: buyer=${event.buyerAccountSequence}, ` +
`trees=${event.treeCount}, ancestors=${ancestors.length}`,
);
} catch (error) {
this.logger.error(
`[TRANSFER-ADDED] ✗ Failed for ${event.transferOrderNo}:`,
error,
);
if (outboxInfo) {
const msg = error instanceof Error ? error.message : String(error);
await this.eventAckPublisher.sendFailure(eventId, outboxInfo.eventType, msg);
}
throw error;
}
}
/**
* 发布团队统计更新完成确认(通知 Saga
*/
private async publishTransferStatsUpdated(transferOrderNo: string): Promise<void> {
await this.kafkaService.publish({
topic: 'referral.transfer.stats-updated',
key: transferOrderNo,
value: {
transferOrderNo,
consumerService: 'referral-service',
success: true,
confirmedAt: new Date().toISOString(),
},
});
this.logger.log(
`[TRANSFER] Published referral.transfer.stats-updated for ${transferOrderNo}`,
);
}
/**
* 标记事件已处理并发送 ACK
*/
private async markProcessedAndAck(
processedEventId: string,
eventType: string,
outboxInfo: PlantingOwnershipRemovedEvent['_outbox'],
eventId: string,
): Promise<void> {
await this.prisma.processedEvent.create({
data: { eventId: processedEventId, eventType },
});
if (outboxInfo) {
await this.eventAckPublisher.sendSuccess(eventId, outboxInfo.eventType);
}
}
}