/** * 转让事件处理器(纯新增) * 消费 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, ): Promise { 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, ): Promise { 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 { 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 { await this.prisma.processedEvent.create({ data: { eventId: processedEventId, eventType }, }); if (outboxInfo) { await this.eventAckPublisher.sendSuccess(eventId, outboxInfo.eventType); } } }