feat: 技术支持工单/企微权限/开发版本管理/消息推送等迭代
This commit is contained in:
@@ -1,105 +0,0 @@
|
||||
import { BadRequestException, Body, Controller, Post, UseGuards } from '@nestjs/common';
|
||||
import { AnalyticsService } from './analytics.service';
|
||||
import { PromoCodeService } from '../promo/promo-code.service';
|
||||
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { OptionalJwtAuthGuard } from '../../common/guards/optional-jwt-auth.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { PromoTouchDto } from './dto/promo.dto';
|
||||
import {
|
||||
TrackPartnerEventsDto,
|
||||
TrackStoreEventsDto,
|
||||
TrackUserEventsDto,
|
||||
} from './dto/track-events.dto';
|
||||
import { ActorType, ClientApp } from '@dukang/shared-types';
|
||||
|
||||
@Controller('analytics')
|
||||
export class AnalyticsController {
|
||||
constructor(private readonly analyticsService: AnalyticsService) {}
|
||||
|
||||
@Post('events')
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
track(@CurrentUser() user: AuthUser | undefined, @Body() body: TrackUserEventsDto) {
|
||||
const userId = user?.actorType === ActorType.USER ? user.actorId : undefined;
|
||||
const clientApp = user?.clientApp ?? body.clientApp ?? ClientApp.USER_H5;
|
||||
return this.analyticsService.trackBatchOptional(userId, clientApp, body.events, body.sessionId);
|
||||
}
|
||||
|
||||
@Post('store-events')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
trackStore(@CurrentUser() user: AuthUser, @Body() body: TrackStoreEventsDto) {
|
||||
if (user.actorType !== ActorType.STORE) {
|
||||
throw new BadRequestException('仅门店端可上报');
|
||||
}
|
||||
const storeId = body.storeId ? BigInt(body.storeId) : user.storeId;
|
||||
if (storeId == null) throw new BadRequestException('缺少门店信息');
|
||||
return this.analyticsService.trackStoreBatch(
|
||||
user.actorId,
|
||||
storeId,
|
||||
user.clientApp,
|
||||
body.events,
|
||||
body.sessionId,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('partner-events')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
trackPartner(@CurrentUser() user: AuthUser, @Body() body: TrackPartnerEventsDto) {
|
||||
if (user.actorType !== ActorType.PARTNER) {
|
||||
throw new BadRequestException('仅合伙人端可上报');
|
||||
}
|
||||
return this.analyticsService.trackPartnerBatch(
|
||||
user.actorId,
|
||||
user.actorId,
|
||||
user.clientApp,
|
||||
body.events,
|
||||
body.sessionId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('promo')
|
||||
export class PromoController {
|
||||
constructor(
|
||||
private readonly promoCodeService: PromoCodeService,
|
||||
private readonly analyticsService: AnalyticsService,
|
||||
) {}
|
||||
|
||||
@Post('touch')
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
async touch(@CurrentUser() user: AuthUser | undefined, @Body() dto: PromoTouchDto) {
|
||||
const userId = user?.actorType === ActorType.USER ? user.actorId : undefined;
|
||||
const result = await this.promoCodeService.touch(
|
||||
{
|
||||
promoCode: dto.promoCode,
|
||||
qrcodeId: dto.qrcodeId,
|
||||
promoId: dto.promoId,
|
||||
countScan: dto.countScan,
|
||||
},
|
||||
userId,
|
||||
);
|
||||
|
||||
void this.analyticsService.trackBatchOptional(
|
||||
userId,
|
||||
user?.clientApp ?? ClientApp.USER_H5,
|
||||
[
|
||||
{
|
||||
eventName: 'promo_touch',
|
||||
params: {
|
||||
sessionId: dto.sessionId,
|
||||
promoCode: result.promoCode,
|
||||
promoCodeId: result.promoCodeId,
|
||||
channelName: result.channelName,
|
||||
attributed: result.attributed,
|
||||
sourceApplied: result.sourceApplied,
|
||||
scanCounted: result.scanCounted,
|
||||
sourceType: 'PROMO_CODE',
|
||||
sourceRefId: result.promoCodeId,
|
||||
},
|
||||
},
|
||||
],
|
||||
dto.sessionId,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { IamModule } from '../iam/iam.module';
|
||||
import { PromoModule } from '../promo/promo.module';
|
||||
import { AnalyticsController, PromoController } from './analytics.controller';
|
||||
import { AnalyticsService } from './analytics.service';
|
||||
import { OptionalJwtAuthGuard } from '../../common/guards/optional-jwt-auth.guard';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
|
||||
@Module({
|
||||
imports: [forwardRef(() => IamModule), PromoModule],
|
||||
controllers: [AnalyticsController, PromoController],
|
||||
providers: [AnalyticsService, OptionalJwtAuthGuard, JwtAuthGuard],
|
||||
exports: [AnalyticsService],
|
||||
})
|
||||
export class AnalyticsModule {}
|
||||
@@ -1,239 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { ClientApp } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
|
||||
export type TrackEventInput = {
|
||||
eventName: string;
|
||||
pagePath?: string;
|
||||
refType?: string;
|
||||
refId?: bigint;
|
||||
sessionId?: string;
|
||||
sourceType?: string;
|
||||
sourceRefId?: bigint;
|
||||
extraJson?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type TrackStoreEventInput = TrackEventInput & {
|
||||
storeAccountId?: bigint;
|
||||
storeId?: bigint;
|
||||
};
|
||||
|
||||
export type TrackPartnerEventInput = TrackEventInput & {
|
||||
partnerAccountId: bigint;
|
||||
};
|
||||
|
||||
type RawEvent = { eventName: string; params?: Record<string, unknown> };
|
||||
|
||||
@Injectable()
|
||||
export class AnalyticsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async trackBatchOptional(
|
||||
userId: bigint | null | undefined,
|
||||
clientApp: string,
|
||||
events: RawEvent[],
|
||||
sessionId?: string,
|
||||
) {
|
||||
if (!events?.length) return { count: 0 };
|
||||
await this.prisma.logUserAnalytics.createMany({
|
||||
data: events.map((e) =>
|
||||
this.toUserRow(userId ?? null, clientApp, {
|
||||
eventName: e.eventName,
|
||||
...this.parseParams(e.params, sessionId),
|
||||
}),
|
||||
),
|
||||
});
|
||||
return { count: events.length };
|
||||
}
|
||||
|
||||
async trackBatch(userId: bigint, clientApp: string, events: RawEvent[], sessionId?: string) {
|
||||
return this.trackBatchOptional(userId, clientApp, events, sessionId);
|
||||
}
|
||||
|
||||
async trackStoreBatch(
|
||||
storeAccountId: bigint | undefined,
|
||||
storeId: bigint,
|
||||
clientApp: ClientApp | string,
|
||||
events: RawEvent[],
|
||||
sessionId?: string,
|
||||
) {
|
||||
if (!events?.length) return { count: 0 };
|
||||
await this.prisma.logStoreAnalytics.createMany({
|
||||
data: events.map((e) =>
|
||||
this.toStoreRow(storeAccountId, clientApp, {
|
||||
storeId,
|
||||
eventName: e.eventName,
|
||||
...this.parseParams(e.params, sessionId),
|
||||
}),
|
||||
),
|
||||
});
|
||||
return { count: events.length };
|
||||
}
|
||||
|
||||
async trackPartnerBatch(
|
||||
actorAccountId: bigint | undefined,
|
||||
partnerAccountId: bigint,
|
||||
clientApp: ClientApp | string,
|
||||
events: RawEvent[],
|
||||
sessionId?: string,
|
||||
) {
|
||||
if (!events?.length) return { count: 0 };
|
||||
await this.prisma.logPartnerAnalytics.createMany({
|
||||
data: events.map((e) =>
|
||||
this.toPartnerRow(actorAccountId, clientApp, {
|
||||
partnerAccountId,
|
||||
eventName: e.eventName,
|
||||
...this.parseParams(e.params, sessionId),
|
||||
}),
|
||||
),
|
||||
});
|
||||
return { count: events.length };
|
||||
}
|
||||
|
||||
async trackOne(userId: bigint, clientApp: ClientApp | string, event: TrackEventInput) {
|
||||
await this.prisma.logUserAnalytics.create({
|
||||
data: this.toUserRow(userId, clientApp, event),
|
||||
});
|
||||
}
|
||||
|
||||
trackOneSafe(userId: bigint, clientApp: ClientApp | string, event: TrackEventInput) {
|
||||
void this.trackOne(userId, clientApp, event).catch(() => {});
|
||||
}
|
||||
|
||||
trackOneSafeOptional(
|
||||
userId: bigint | null | undefined,
|
||||
clientApp: ClientApp | string,
|
||||
event: TrackEventInput,
|
||||
) {
|
||||
void this.trackBatchOptional(
|
||||
userId,
|
||||
clientApp,
|
||||
[{ eventName: event.eventName, params: this.eventToParams(event) }],
|
||||
event.sessionId,
|
||||
).catch(() => {});
|
||||
}
|
||||
|
||||
async trackStoreOne(
|
||||
storeAccountId: bigint | undefined,
|
||||
clientApp: ClientApp | string,
|
||||
event: TrackStoreEventInput,
|
||||
) {
|
||||
if (event.storeId == null) return;
|
||||
await this.prisma.logStoreAnalytics.create({
|
||||
data: this.toStoreRow(storeAccountId, clientApp, event),
|
||||
});
|
||||
}
|
||||
|
||||
trackStoreOneSafe(storeAccountId: bigint | undefined, clientApp: ClientApp | string, event: TrackStoreEventInput) {
|
||||
if (event.storeId == null) return;
|
||||
void this.trackStoreOne(storeAccountId, clientApp, event).catch(() => {});
|
||||
}
|
||||
|
||||
async trackPartnerOne(
|
||||
actorAccountId: bigint | undefined,
|
||||
clientApp: ClientApp | string,
|
||||
event: TrackPartnerEventInput,
|
||||
) {
|
||||
await this.prisma.logPartnerAnalytics.create({
|
||||
data: this.toPartnerRow(actorAccountId, clientApp, event),
|
||||
});
|
||||
}
|
||||
|
||||
trackPartnerOneSafe(
|
||||
actorAccountId: bigint | undefined,
|
||||
clientApp: ClientApp | string,
|
||||
event: TrackPartnerEventInput,
|
||||
) {
|
||||
void this.trackPartnerOne(actorAccountId, clientApp, event).catch(() => {});
|
||||
}
|
||||
|
||||
private parseParams(
|
||||
params?: Record<string, unknown>,
|
||||
fallbackSessionId?: string,
|
||||
): Omit<TrackEventInput, 'eventName'> {
|
||||
const p = params ?? {};
|
||||
const sessionId =
|
||||
typeof p.sessionId === 'string' ? p.sessionId.slice(0, 64) : fallbackSessionId?.slice(0, 64);
|
||||
const pagePath = typeof p.pagePath === 'string' ? p.pagePath.slice(0, 128) : undefined;
|
||||
const refType = typeof p.refType === 'string' ? p.refType : undefined;
|
||||
const refId = p.refId != null ? BigInt(String(p.refId)) : undefined;
|
||||
const sourceType = typeof p.sourceType === 'string' ? p.sourceType.slice(0, 32) : undefined;
|
||||
const sourceRefId = p.sourceRefId != null ? BigInt(String(p.sourceRefId)) : undefined;
|
||||
const {
|
||||
sessionId: _s,
|
||||
pagePath: _p,
|
||||
refType: _rt,
|
||||
refId: _ri,
|
||||
sourceType: _st,
|
||||
sourceRefId: _sr,
|
||||
...rest
|
||||
} = p;
|
||||
return {
|
||||
sessionId,
|
||||
pagePath,
|
||||
refType,
|
||||
refId,
|
||||
sourceType,
|
||||
sourceRefId,
|
||||
extraJson: Object.keys(rest).length ? rest : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private eventToParams(event: TrackEventInput): Record<string, unknown> {
|
||||
return {
|
||||
...(event.pagePath ? { pagePath: event.pagePath } : {}),
|
||||
...(event.refType ? { refType: event.refType } : {}),
|
||||
...(event.refId != null ? { refId: event.refId.toString() } : {}),
|
||||
...(event.sessionId ? { sessionId: event.sessionId } : {}),
|
||||
...(event.sourceType ? { sourceType: event.sourceType } : {}),
|
||||
...(event.sourceRefId != null ? { sourceRefId: event.sourceRefId.toString() } : {}),
|
||||
...(event.extraJson ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
private toUserRow(userId: bigint | null, clientApp: ClientApp | string, event: TrackEventInput) {
|
||||
return {
|
||||
userId,
|
||||
sessionId: event.sessionId,
|
||||
eventName: event.eventName,
|
||||
clientApp: clientApp as ClientApp,
|
||||
pagePath: event.pagePath,
|
||||
refType: event.refType,
|
||||
refId: event.refId,
|
||||
sourceType: event.sourceType,
|
||||
sourceRefId: event.sourceRefId,
|
||||
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(
|
||||
actorAccountId: bigint | undefined,
|
||||
clientApp: ClientApp | string,
|
||||
event: TrackPartnerEventInput,
|
||||
) {
|
||||
return {
|
||||
partnerAccountId: event.partnerAccountId,
|
||||
eventName: event.eventName,
|
||||
clientApp: clientApp as ClientApp,
|
||||
refType: event.refType,
|
||||
refId: event.refId ?? actorAccountId,
|
||||
extraJson: event.extraJson as never,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export class PromoTouchDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
promoCode?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
qrcodeId?: string;
|
||||
|
||||
/** 小程序码 scene 中的推广活动 ID */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
promoId?: string;
|
||||
|
||||
/**
|
||||
* 是否累加扫码次数。扫码进入为 true;登录后归因可传 false,避免重复计数。
|
||||
* 默认 true。
|
||||
*/
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => {
|
||||
if (value === false || value === 'false' || value === 0 || value === '0') return false;
|
||||
if (value === true || value === 'true' || value === 1 || value === '1') return true;
|
||||
return undefined;
|
||||
})
|
||||
@IsBoolean()
|
||||
countScan?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
sessionId?: string;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { IsArray, IsOptional, IsString, MaxLength, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class AnalyticsEventDto {
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
eventName!: string;
|
||||
|
||||
@IsOptional()
|
||||
params?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class TrackUserEventsDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AnalyticsEventDto)
|
||||
events!: AnalyticsEventDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
sessionId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(32)
|
||||
clientApp?: string;
|
||||
}
|
||||
|
||||
export class TrackStoreEventsDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AnalyticsEventDto)
|
||||
events!: AnalyticsEventDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
sessionId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
storeId?: string;
|
||||
}
|
||||
|
||||
export class TrackPartnerEventsDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AnalyticsEventDto)
|
||||
events!: AnalyticsEventDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
sessionId?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user