167 lines
4.9 KiB
TypeScript
167 lines
4.9 KiB
TypeScript
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
import type { ClientApp } from '@prisma/client';
|
|
import { PrismaService } from '../../common/prisma/prisma.module';
|
|
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
|
|
|
export type TrackEventInput = {
|
|
eventName: string;
|
|
pagePath?: string;
|
|
refType?: string;
|
|
refId?: bigint;
|
|
extraJson?: Record<string, unknown>;
|
|
};
|
|
|
|
export type TrackStoreEventInput = TrackEventInput & {
|
|
storeAccountId?: bigint;
|
|
storeId: bigint;
|
|
};
|
|
|
|
export type TrackPartnerEventInput = TrackEventInput & {
|
|
partnerAccountId?: bigint;
|
|
partnerId: bigint;
|
|
};
|
|
|
|
@Injectable()
|
|
export class AnalyticsService {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
async trackBatch(
|
|
userId: bigint,
|
|
clientApp: string,
|
|
events: Array<{ eventName: string; params?: Record<string, unknown> }>,
|
|
) {
|
|
if (!events?.length) return { count: 0 };
|
|
await this.prisma.logUserAnalytics.createMany({
|
|
data: events.map((e) => this.toRow(userId, clientApp, {
|
|
eventName: e.eventName,
|
|
extraJson: e.params,
|
|
refType: typeof e.params?.refType === 'string' ? e.params.refType : undefined,
|
|
refId: e.params?.refId != null ? BigInt(String(e.params.refId)) : undefined,
|
|
pagePath: typeof e.params?.pagePath === 'string' ? e.params.pagePath : undefined,
|
|
})),
|
|
});
|
|
return { count: events.length };
|
|
}
|
|
|
|
async trackOne(userId: bigint, clientApp: ClientApp | string, event: TrackEventInput) {
|
|
await this.prisma.logUserAnalytics.create({
|
|
data: this.toRow(userId, clientApp, event),
|
|
});
|
|
}
|
|
|
|
trackOneSafe(userId: bigint, clientApp: ClientApp | string, event: TrackEventInput) {
|
|
void this.trackOne(userId, clientApp, event).catch(() => {});
|
|
}
|
|
|
|
async trackStoreOne(storeAccountId: bigint | undefined, clientApp: ClientApp | string, event: TrackStoreEventInput) {
|
|
await this.prisma.logStoreAnalytics.create({
|
|
data: this.toStoreRow(storeAccountId, clientApp, event),
|
|
});
|
|
}
|
|
|
|
trackStoreOneSafe(storeAccountId: bigint | undefined, clientApp: ClientApp | string, event: TrackStoreEventInput) {
|
|
void this.trackStoreOne(storeAccountId, clientApp, event).catch(() => {});
|
|
}
|
|
|
|
async trackPartnerOne(
|
|
partnerAccountId: bigint | undefined,
|
|
clientApp: ClientApp | string,
|
|
event: TrackPartnerEventInput,
|
|
) {
|
|
await this.prisma.logPartnerAnalytics.create({
|
|
data: this.toPartnerRow(partnerAccountId, clientApp, event),
|
|
});
|
|
}
|
|
|
|
trackPartnerOneSafe(
|
|
partnerAccountId: bigint | undefined,
|
|
clientApp: ClientApp | string,
|
|
event: TrackPartnerEventInput,
|
|
) {
|
|
void this.trackPartnerOne(partnerAccountId, clientApp, event).catch(() => {});
|
|
}
|
|
|
|
private toRow(userId: bigint, clientApp: ClientApp | string, event: TrackEventInput) {
|
|
return {
|
|
userId,
|
|
eventName: event.eventName,
|
|
clientApp: clientApp as ClientApp,
|
|
pagePath: event.pagePath,
|
|
refType: event.refType,
|
|
refId: event.refId,
|
|
extraJson: event.extraJson as never,
|
|
};
|
|
}
|
|
|
|
private toStoreRow(
|
|
storeAccountId: bigint | undefined,
|
|
clientApp: ClientApp | string,
|
|
event: TrackStoreEventInput,
|
|
) {
|
|
return {
|
|
storeAccountId,
|
|
storeId: event.storeId,
|
|
eventName: event.eventName,
|
|
clientApp: clientApp as ClientApp,
|
|
refType: event.refType,
|
|
refId: event.refId,
|
|
extraJson: event.extraJson as never,
|
|
};
|
|
}
|
|
|
|
private toPartnerRow(
|
|
partnerAccountId: bigint | undefined,
|
|
clientApp: ClientApp | string,
|
|
event: TrackPartnerEventInput,
|
|
) {
|
|
return {
|
|
partnerAccountId,
|
|
partnerId: event.partnerId,
|
|
eventName: event.eventName,
|
|
clientApp: clientApp as ClientApp,
|
|
refType: event.refType,
|
|
refId: event.refId,
|
|
extraJson: event.extraJson as never,
|
|
};
|
|
}
|
|
|
|
/** 扫码归因:始终累加 scan_count;已登录用户首次写入 user_promo_attribution */
|
|
async touchPromo(promoCode: string, userId?: bigint) {
|
|
const code = promoCode.trim().toUpperCase();
|
|
const promo = await this.prisma.commonPromoCode.findUnique({ where: { code } });
|
|
if (!promo || promo.status !== 'ACTIVE') {
|
|
throw new NotFoundException('推广码无效或已停用');
|
|
}
|
|
|
|
await this.prisma.commonPromoCode.update({
|
|
where: { id: promo.id },
|
|
data: { scanCount: { increment: 1 } },
|
|
});
|
|
|
|
let attributed = false;
|
|
if (userId) {
|
|
const existing = await this.prisma.userPromoAttribution.findUnique({
|
|
where: { userId },
|
|
});
|
|
if (!existing) {
|
|
await this.prisma.userPromoAttribution.create({
|
|
data: {
|
|
userId,
|
|
promoCodeId: promo.id,
|
|
channelName: promo.name,
|
|
firstTouchAt: new Date(),
|
|
},
|
|
});
|
|
attributed = true;
|
|
}
|
|
}
|
|
|
|
return serializeBigInt({
|
|
promoCode: promo.code,
|
|
channelName: promo.name,
|
|
attributed,
|
|
});
|
|
}
|
|
}
|
|
|