|
@@ -0,0 +1,218 @@
|
|
|
|
|
+import { Injectable } from '@nestjs/common';
|
|
|
|
|
+import { Prisma } from '@prisma/mongo-stats/client';
|
|
|
|
|
+import type {
|
|
|
|
|
+ AdsDailyStats,
|
|
|
|
|
+ AdsHourlyStats,
|
|
|
|
|
+ ChannelDailyUserStats,
|
|
|
|
|
+ ChannelHourlyUserStats,
|
|
|
|
|
+} from '@prisma/mongo-stats/client';
|
|
|
|
|
+import { PrismaMongoService } from '../../prisma/prisma-mongo.service';
|
|
|
|
|
+import {
|
|
|
|
|
+ ChannelStatsQueryDto,
|
|
|
|
|
+ AdsStatsQueryDto,
|
|
|
|
|
+} from './dto/stats-reporting.dto';
|
|
|
|
|
+import {
|
|
|
|
|
+ assertRange,
|
|
|
|
|
+ alignDayStart,
|
|
|
|
|
+ alignHourStart,
|
|
|
|
|
+} from './stats-reporting.time';
|
|
|
|
|
+
|
|
|
|
|
+interface PaginatedResult<T> {
|
|
|
|
|
+ page: number;
|
|
|
|
|
+ size: number;
|
|
|
|
|
+ total: number;
|
|
|
|
|
+ items: T[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface PaginationParams {
|
|
|
|
|
+ page?: number;
|
|
|
|
|
+ size?: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface NormalizedPagination {
|
|
|
|
|
+ page: number;
|
|
|
|
|
+ size: number;
|
|
|
|
|
+ skip: number;
|
|
|
|
|
+ take: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const DEFAULT_PAGE = 1;
|
|
|
|
|
+const DEFAULT_SIZE = 10;
|
|
|
|
|
+const MAX_SIZE = 200;
|
|
|
|
|
+
|
|
|
|
|
+@Injectable()
|
|
|
|
|
+export class StatsReportingService {
|
|
|
|
|
+ constructor(private readonly prisma: PrismaMongoService) {}
|
|
|
|
|
+
|
|
|
|
|
+ async getAdsHourly(query: AdsStatsQueryDto): Promise<any> {
|
|
|
|
|
+ assertRange(query.fromSec, query.toSec);
|
|
|
|
|
+ const { page, size, skip, take } = this.normalizePagination({
|
|
|
|
|
+ page: query.page,
|
|
|
|
|
+ size: query.size,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const fromAligned = alignHourStart(query.fromSec);
|
|
|
|
|
+ const toAligned = alignHourStart(query.toSec);
|
|
|
|
|
+
|
|
|
|
|
+ const where: Prisma.AdsHourlyStatsWhereInput = {
|
|
|
|
|
+ hourStartAt: {
|
|
|
|
|
+ gte: BigInt(fromAligned),
|
|
|
|
|
+ lt: BigInt(toAligned),
|
|
|
|
|
+ },
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (query.adsId) {
|
|
|
|
|
+ where.adsId = query.adsId;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const [items, total] = await Promise.all([
|
|
|
|
|
+ this.prisma.adsHourlyStats.findMany({
|
|
|
|
|
+ where,
|
|
|
|
|
+ orderBy: [{ hourStartAt: 'desc' }, { id: 'desc' }],
|
|
|
|
|
+ skip,
|
|
|
|
|
+ take,
|
|
|
|
|
+ }),
|
|
|
|
|
+ this.prisma.adsHourlyStats.count({ where }),
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return { page, size, total, items };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async getAdsDaily(query: AdsStatsQueryDto): Promise<any> {
|
|
|
|
|
+ assertRange(query.fromSec, query.toSec);
|
|
|
|
|
+ const { page, size, skip, take } = this.normalizePagination({
|
|
|
|
|
+ page: query.page,
|
|
|
|
|
+ size: query.size,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const fromAligned = alignDayStart(query.fromSec);
|
|
|
|
|
+ const toAligned = alignDayStart(query.toSec);
|
|
|
|
|
+
|
|
|
|
|
+ const where: Prisma.AdsDailyStatsWhereInput = {
|
|
|
|
|
+ dayStartAt: {
|
|
|
|
|
+ gte: BigInt(fromAligned),
|
|
|
|
|
+ lt: BigInt(toAligned),
|
|
|
|
|
+ },
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (query.adsId) {
|
|
|
|
|
+ where.adsId = query.adsId;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const [items, total] = await Promise.all([
|
|
|
|
|
+ this.prisma.adsDailyStats.findMany({
|
|
|
|
|
+ where,
|
|
|
|
|
+ orderBy: [{ dayStartAt: 'desc' }, { id: 'desc' }],
|
|
|
|
|
+ skip,
|
|
|
|
|
+ take,
|
|
|
|
|
+ }),
|
|
|
|
|
+ this.prisma.adsDailyStats.count({ where }),
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return { page, size, total, items };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async getChannelHourlyUsers(query: ChannelStatsQueryDto): Promise<any> {
|
|
|
|
|
+ assertRange(query.fromSec, query.toSec);
|
|
|
|
|
+ const { page, size, skip, take } = this.normalizePagination({
|
|
|
|
|
+ page: query.page,
|
|
|
|
|
+ size: query.size,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const fromAligned = alignHourStart(query.fromSec);
|
|
|
|
|
+ const toAligned = alignHourStart(query.toSec);
|
|
|
|
|
+
|
|
|
|
|
+ const where: Prisma.ChannelHourlyUserStatsWhereInput = {
|
|
|
|
|
+ hourStartAt: {
|
|
|
|
|
+ gte: BigInt(fromAligned),
|
|
|
|
|
+ lt: BigInt(toAligned),
|
|
|
|
|
+ },
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (query.channelId) {
|
|
|
|
|
+ where.channelId = query.channelId;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const [items, total] = await Promise.all([
|
|
|
|
|
+ this.prisma.channelHourlyUserStats.findMany({
|
|
|
|
|
+ where,
|
|
|
|
|
+ orderBy: [{ hourStartAt: 'desc' }, { id: 'desc' }],
|
|
|
|
|
+ skip,
|
|
|
|
|
+ take,
|
|
|
|
|
+ }),
|
|
|
|
|
+ this.prisma.channelHourlyUserStats.count({ where }),
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return { page, size, total, items };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async getChannelDailyUsers(query: ChannelStatsQueryDto): Promise<any> {
|
|
|
|
|
+ assertRange(query.fromSec, query.toSec);
|
|
|
|
|
+ const { page, size, skip, take } = this.normalizePagination({
|
|
|
|
|
+ page: query.page,
|
|
|
|
|
+ size: query.size,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const fromAligned = alignDayStart(query.fromSec);
|
|
|
|
|
+ const toAligned = alignDayStart(query.toSec);
|
|
|
|
|
+
|
|
|
|
|
+ const where: Prisma.ChannelDailyUserStatsWhereInput = {
|
|
|
|
|
+ dayStartAt: {
|
|
|
|
|
+ gte: BigInt(fromAligned),
|
|
|
|
|
+ lt: BigInt(toAligned),
|
|
|
|
|
+ },
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (query.channelId) {
|
|
|
|
|
+ where.channelId = query.channelId;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const [items, total] = await Promise.all([
|
|
|
|
|
+ this.prisma.channelDailyUserStats.findMany({
|
|
|
|
|
+ where,
|
|
|
|
|
+ orderBy: [{ dayStartAt: 'desc' }, { id: 'desc' }],
|
|
|
|
|
+ skip,
|
|
|
|
|
+ take,
|
|
|
|
|
+ }),
|
|
|
|
|
+ this.prisma.channelDailyUserStats.count({ where }),
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return { page, size, total, items };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private normalizePagination({
|
|
|
|
|
+ page,
|
|
|
|
|
+ size,
|
|
|
|
|
+ }: PaginationParams = {}): NormalizedPagination {
|
|
|
|
|
+ const normalizedPage = this.normalizePage(page);
|
|
|
|
|
+ const normalizedSize = this.normalizeSize(size);
|
|
|
|
|
+ const skip = Math.max(0, (normalizedPage - 1) * normalizedSize);
|
|
|
|
|
+ return {
|
|
|
|
|
+ page: normalizedPage,
|
|
|
|
|
+ size: normalizedSize,
|
|
|
|
|
+ skip,
|
|
|
|
|
+ take: normalizedSize,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private normalizePage(value?: number) {
|
|
|
|
|
+ if (!Number.isFinite(value ?? NaN)) {
|
|
|
|
|
+ return DEFAULT_PAGE;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return Math.max(DEFAULT_PAGE, Math.floor(value));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private normalizeSize(value?: number) {
|
|
|
|
|
+ if (!Number.isFinite(value ?? NaN)) {
|
|
|
|
|
+ return DEFAULT_SIZE;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const normalized = Math.floor(value);
|
|
|
|
|
+ if (normalized < 1) {
|
|
|
|
|
+ return DEFAULT_SIZE;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return Math.min(MAX_SIZE, normalized);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|