161 lines
5.6 KiB
TypeScript
161 lines
5.6 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { CDCEvent } from '../../infrastructure/kafka/cdc-consumer.service';
|
|
import { SyncedDataRepository } from '../../infrastructure/persistence/repositories/synced-data.repository';
|
|
import { UnitOfWork } from '../../infrastructure/persistence/unit-of-work/unit-of-work';
|
|
|
|
/**
|
|
* 引荐关系 CDC 事件处理器
|
|
* 处理从1.0 referral-service同步过来的referral_relationships数据
|
|
*
|
|
* 1.0 表结构 (referral_relationships):
|
|
* - user_id: BigInt (用户ID)
|
|
* - account_sequence: String (账户序列号)
|
|
* - referrer_id: BigInt (推荐人用户ID, 注意:不是 account_sequence)
|
|
* - ancestor_path: BigInt[] (祖先路径数组,存储 user_id)
|
|
* - depth: Int (层级深度)
|
|
*
|
|
* 2.0 存储策略:
|
|
* - 保存 original_user_id (1.0 的 user_id)
|
|
* - 保存 referrer_user_id (1.0 的 referrer_id)
|
|
* - 尝试查找 referrer 的 account_sequence 并保存
|
|
* - ancestor_path 转换为逗号分隔的字符串
|
|
*/
|
|
@Injectable()
|
|
export class ReferralSyncedHandler {
|
|
private readonly logger = new Logger(ReferralSyncedHandler.name);
|
|
|
|
constructor(
|
|
private readonly syncedDataRepository: SyncedDataRepository,
|
|
private readonly unitOfWork: UnitOfWork,
|
|
) {}
|
|
|
|
async handle(event: CDCEvent): Promise<void> {
|
|
const { op, before, after } = event.payload;
|
|
|
|
try {
|
|
switch (op) {
|
|
case 'c': // create
|
|
case 'r': // read (snapshot)
|
|
await this.handleCreate(after, event.sequenceNum);
|
|
break;
|
|
case 'u': // update
|
|
await this.handleUpdate(after, event.sequenceNum);
|
|
break;
|
|
case 'd': // delete
|
|
await this.handleDelete(before);
|
|
break;
|
|
default:
|
|
this.logger.warn(`Unknown CDC operation: ${op}`);
|
|
}
|
|
} catch (error) {
|
|
this.logger.error(`Failed to handle referral CDC event`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private async handleCreate(data: any, sequenceNum: bigint): Promise<void> {
|
|
if (!data) return;
|
|
|
|
// 1.0 字段映射
|
|
const accountSequence = data.account_sequence || data.accountSequence;
|
|
const originalUserId = data.user_id || data.userId;
|
|
const referrerUserId = data.referrer_id || data.referrerId;
|
|
const ancestorPathArray = data.ancestor_path || data.ancestorPath;
|
|
const depth = data.depth || 0;
|
|
|
|
// 将 BigInt[] 转换为逗号分隔的字符串
|
|
const ancestorPath = this.convertAncestorPath(ancestorPathArray);
|
|
|
|
// 尝试查找推荐人的 account_sequence
|
|
let referrerAccountSequence: string | null = null;
|
|
if (referrerUserId) {
|
|
const referrer = await this.syncedDataRepository.findSyncedReferralByOriginalUserId(BigInt(referrerUserId));
|
|
if (referrer) {
|
|
referrerAccountSequence = referrer.accountSequence;
|
|
} else {
|
|
this.logger.debug(
|
|
`Referrer user_id ${referrerUserId} not found yet for ${accountSequence}, will resolve later`,
|
|
);
|
|
}
|
|
}
|
|
|
|
await this.unitOfWork.executeInTransaction(async () => {
|
|
await this.syncedDataRepository.upsertSyncedReferral({
|
|
accountSequence,
|
|
referrerAccountSequence,
|
|
referrerUserId: referrerUserId ? BigInt(referrerUserId) : null,
|
|
originalUserId: originalUserId ? BigInt(originalUserId) : null,
|
|
ancestorPath,
|
|
depth,
|
|
sourceSequenceNum: sequenceNum,
|
|
});
|
|
});
|
|
|
|
this.logger.log(
|
|
`Referral synced: ${accountSequence} (user_id: ${originalUserId}) -> referrer_id: ${referrerUserId || 'none'}`,
|
|
);
|
|
}
|
|
|
|
private async handleUpdate(data: any, sequenceNum: bigint): Promise<void> {
|
|
if (!data) return;
|
|
|
|
const accountSequence = data.account_sequence || data.accountSequence;
|
|
const originalUserId = data.user_id || data.userId;
|
|
const referrerUserId = data.referrer_id || data.referrerId;
|
|
const ancestorPathArray = data.ancestor_path || data.ancestorPath;
|
|
const depth = data.depth || 0;
|
|
|
|
const ancestorPath = this.convertAncestorPath(ancestorPathArray);
|
|
|
|
// 尝试查找推荐人的 account_sequence
|
|
let referrerAccountSequence: string | null = null;
|
|
if (referrerUserId) {
|
|
const referrer = await this.syncedDataRepository.findSyncedReferralByOriginalUserId(BigInt(referrerUserId));
|
|
if (referrer) {
|
|
referrerAccountSequence = referrer.accountSequence;
|
|
}
|
|
}
|
|
|
|
await this.syncedDataRepository.upsertSyncedReferral({
|
|
accountSequence,
|
|
referrerAccountSequence,
|
|
referrerUserId: referrerUserId ? BigInt(referrerUserId) : null,
|
|
originalUserId: originalUserId ? BigInt(originalUserId) : null,
|
|
ancestorPath,
|
|
depth,
|
|
sourceSequenceNum: sequenceNum,
|
|
});
|
|
|
|
this.logger.debug(`Referral updated: ${accountSequence}`);
|
|
}
|
|
|
|
private async handleDelete(data: any): Promise<void> {
|
|
if (!data) return;
|
|
// 引荐关系删除需要特殊处理
|
|
this.logger.warn(`Referral delete event received: ${data.account_sequence || data.accountSequence}`);
|
|
}
|
|
|
|
/**
|
|
* 将 BigInt[] 数组转换为逗号分隔的字符串
|
|
* @param ancestorPath BigInt 数组或 null
|
|
* @returns 逗号分隔的字符串或 null
|
|
*/
|
|
private convertAncestorPath(ancestorPath: any): string | null {
|
|
if (!ancestorPath) return null;
|
|
|
|
// 处理可能的数组格式
|
|
if (Array.isArray(ancestorPath)) {
|
|
return ancestorPath.map((id) => String(id)).join(',');
|
|
}
|
|
|
|
// 如果已经是字符串 (可能是 PostgreSQL 数组的字符串表示)
|
|
if (typeof ancestorPath === 'string') {
|
|
// PostgreSQL 数组格式: {1,2,3} 或 [1,2,3]
|
|
const cleaned = ancestorPath.replace(/[{}\[\]]/g, '');
|
|
return cleaned || null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|