|
|
@@ -54,6 +54,25 @@ interface AdTypeClickRecord {
|
|
|
clicks: number;
|
|
|
}
|
|
|
|
|
|
+interface IpClickStatsParams {
|
|
|
+ page: number;
|
|
|
+ size: number;
|
|
|
+ fromSec: number;
|
|
|
+ toSec: number;
|
|
|
+ channelId?: string;
|
|
|
+ deviceType?: string;
|
|
|
+ adType?: string;
|
|
|
+ ip?: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface IpClickRecord {
|
|
|
+ ip: string;
|
|
|
+ channelId: string;
|
|
|
+ deviceType: string | null;
|
|
|
+ adType: string | null;
|
|
|
+ clicks: number;
|
|
|
+}
|
|
|
+
|
|
|
const DEFAULT_PAGE = 1;
|
|
|
const DEFAULT_SIZE = 10;
|
|
|
const MAX_SIZE = 100;
|
|
|
@@ -198,6 +217,117 @@ export class StatsReportingService {
|
|
|
return { page, size, total, items: toJsonSafe(items) };
|
|
|
}
|
|
|
|
|
|
+ async getIpClicks(
|
|
|
+ params: IpClickStatsParams,
|
|
|
+ ): Promise<PaginatedResult<IpClickRecord>> {
|
|
|
+ const { page, size, skip, take } = this.normalizePagination({
|
|
|
+ page: params.page,
|
|
|
+ size: params.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;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (params.ip) {
|
|
|
+ match.ip = params.ip;
|
|
|
+ }
|
|
|
+
|
|
|
+ const buildGroupStage = () => ({
|
|
|
+ $group: {
|
|
|
+ _id: {
|
|
|
+ channelId: '$channelId',
|
|
|
+ adType: '$adType',
|
|
|
+ deviceType: '$machine',
|
|
|
+ ip: '$ip',
|
|
|
+ },
|
|
|
+ clicks: { $sum: 1 },
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ const pipeline = [
|
|
|
+ { $match: match },
|
|
|
+ buildGroupStage(),
|
|
|
+ {
|
|
|
+ $project: {
|
|
|
+ _id: 0,
|
|
|
+ channelId: '$_id.channelId',
|
|
|
+ adType: '$_id.adType',
|
|
|
+ deviceType: '$_id.deviceType',
|
|
|
+ ip: '$_id.ip',
|
|
|
+ clicks: 1,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ $sort: {
|
|
|
+ channelId: 1,
|
|
|
+ adType: 1,
|
|
|
+ deviceType: 1,
|
|
|
+ ip: 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<IpClickRecord>>,
|
|
|
+ (this.prisma.adClickEvents as any).aggregateRaw({
|
|
|
+ pipeline: countPipeline,
|
|
|
+ }) as Promise<Array<{ total: number | bigint }>>,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ const countValue =
|
|
|
+ Array.isArray(countRows) && countRows.length > 0
|
|
|
+ ? countRows[0].total
|
|
|
+ : 0;
|
|
|
+ const total =
|
|
|
+ typeof countValue === 'bigint'
|
|
|
+ ? Number(countValue)
|
|
|
+ : typeof countValue === 'number'
|
|
|
+ ? countValue
|
|
|
+ : 0;
|
|
|
+
|
|
|
+ const normalizedItems = (items ?? []).map((item) => ({
|
|
|
+ channelId: item.channelId ?? '',
|
|
|
+ deviceType: item.deviceType ?? null,
|
|
|
+ adType: item.adType ?? null,
|
|
|
+ ip: item.ip ?? '',
|
|
|
+ clicks:
|
|
|
+ typeof item.clicks === 'number' ? item.clicks : Number(item.clicks),
|
|
|
+ }));
|
|
|
+
|
|
|
+ return {
|
|
|
+ page,
|
|
|
+ size,
|
|
|
+ total,
|
|
|
+ items: toJsonSafe(normalizedItems),
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
async getAdTypeClicks(
|
|
|
params: AdTypeClicksParams,
|
|
|
): Promise<PaginatedResult<AdTypeClickRecord>> {
|