317 lines
10 KiB
TypeScript
317 lines
10 KiB
TypeScript
/**
|
||
* 转让事件处理器(纯新增)
|
||
* 消费 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);
|
||
}
|
||
}
|
||
}
|