|
|
@@ -37,9 +37,26 @@ interface NormalizedPagination {
|
|
|
take: number;
|
|
|
}
|
|
|
|
|
|
+interface AdTypeClicksParams {
|
|
|
+ page: number;
|
|
|
+ size: number;
|
|
|
+ channelId?: string;
|
|
|
+ deviceType?: string;
|
|
|
+ adType?: string;
|
|
|
+ fromSec: number;
|
|
|
+ toSec: number;
|
|
|
+}
|
|
|
+
|
|
|
+interface AdTypeClickRecord {
|
|
|
+ channelId: string;
|
|
|
+ deviceType: string | null;
|
|
|
+ adType: string | null;
|
|
|
+ clicks: number;
|
|
|
+}
|
|
|
+
|
|
|
const DEFAULT_PAGE = 1;
|
|
|
const DEFAULT_SIZE = 10;
|
|
|
-const MAX_SIZE = 200;
|
|
|
+const MAX_SIZE = 100;
|
|
|
|
|
|
@Injectable()
|
|
|
export class StatsReportingService {
|
|
|
@@ -181,6 +198,100 @@ export class StatsReportingService {
|
|
|
return { page, size, total, items: toJsonSafe(items) };
|
|
|
}
|
|
|
|
|
|
+ async getAdTypeClicks(
|
|
|
+ params: AdTypeClicksParams,
|
|
|
+ ): Promise<PaginatedResult<AdTypeClickRecord>> {
|
|
|
+ const { page, size } = params;
|
|
|
+ const skip = Math.max(0, (page - 1) * size);
|
|
|
+ const take = size;
|
|
|
+
|
|
|
+ const match: Record<string, unknown> = {
|
|
|
+ clickedAt: {
|
|
|
+ $gte: BigInt(params.fromSec),
|
|
|
+ $lt: BigInt(params.toSec),
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ if (params.channelId) {
|
|
|
+ match.channelId = params.channelId;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (params.adType) {
|
|
|
+ match.adType = params.adType;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (params.deviceType) {
|
|
|
+ match.machine = params.deviceType;
|
|
|
+ }
|
|
|
+
|
|
|
+ const buildGroupStage = () => ({
|
|
|
+ $group: {
|
|
|
+ _id: {
|
|
|
+ channelId: '$channelId',
|
|
|
+ deviceType: '$machine',
|
|
|
+ adType: '$adType',
|
|
|
+ },
|
|
|
+ clicks: { $sum: 1 },
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ const pipeline = [
|
|
|
+ { $match: match },
|
|
|
+ buildGroupStage(),
|
|
|
+ {
|
|
|
+ $project: {
|
|
|
+ _id: 0,
|
|
|
+ channelId: '$_id.channelId',
|
|
|
+ deviceType: '$_id.deviceType',
|
|
|
+ adType: '$_id.adType',
|
|
|
+ clicks: 1,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ $sort: {
|
|
|
+ channelId: 1,
|
|
|
+ adType: 1,
|
|
|
+ deviceType: 1,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ { $skip: skip },
|
|
|
+ { $limit: take },
|
|
|
+ ] as const;
|
|
|
+
|
|
|
+ const countPipeline = [
|
|
|
+ { $match: match },
|
|
|
+ buildGroupStage(),
|
|
|
+ { $count: 'total' },
|
|
|
+ ];
|
|
|
+
|
|
|
+ const [items, countRows] = await Promise.all([
|
|
|
+ (this.prisma.adClickEvents as any).aggregateRaw({
|
|
|
+ pipeline,
|
|
|
+ }) as Promise<Array<AdTypeClickRecord>>,
|
|
|
+ (this.prisma.adClickEvents as any).aggregateRaw({
|
|
|
+ pipeline: countPipeline,
|
|
|
+ }) as Promise<Array<{ total: number }>>,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ const total =
|
|
|
+ Array.isArray(countRows) && countRows.length > 0 ? countRows[0].total : 0;
|
|
|
+
|
|
|
+ const normalizedItems = (items ?? []).map((item) => ({
|
|
|
+ channelId: item.channelId ?? '',
|
|
|
+ deviceType: item.deviceType ?? null,
|
|
|
+ adType: item.adType ?? null,
|
|
|
+ clicks:
|
|
|
+ typeof item.clicks === 'number' ? item.clicks : Number(item.clicks),
|
|
|
+ }));
|
|
|
+
|
|
|
+ return {
|
|
|
+ page,
|
|
|
+ size,
|
|
|
+ total,
|
|
|
+ items: toJsonSafe(normalizedItems),
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
private normalizePagination({
|
|
|
page,
|
|
|
size,
|
|
|
@@ -205,15 +316,10 @@ export class StatsReportingService {
|
|
|
}
|
|
|
|
|
|
private normalizeSize(value?: number) {
|
|
|
- if (!Number.isFinite(value ?? NaN)) {
|
|
|
- return DEFAULT_SIZE;
|
|
|
- }
|
|
|
-
|
|
|
- const normalized = Math.floor(value);
|
|
|
- if (normalized < 1) {
|
|
|
- return DEFAULT_SIZE;
|
|
|
- }
|
|
|
+ const requestedSize = Number.isFinite(value ?? NaN)
|
|
|
+ ? Math.floor(value as number)
|
|
|
+ : DEFAULT_SIZE;
|
|
|
|
|
|
- return Math.min(MAX_SIZE, normalized);
|
|
|
+ return Math.max(1, Math.min(MAX_SIZE, requestedSize));
|
|
|
}
|
|
|
}
|