feat: 技术支持工单/企微权限/开发版本管理/消息推送等迭代

This commit is contained in:
2026-08-04 21:32:13 +08:00
parent c8ea5a3119
commit 9d96c73246
1341 changed files with 0 additions and 195605 deletions
@@ -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;
}