283 lines
8.3 KiB
TypeScript
283 lines
8.3 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository } from 'typeorm';
|
|
import { ShareEvent, SharePlatform } from './analytics.entity';
|
|
import { Article } from '../entities';
|
|
import {
|
|
TrackShareDto,
|
|
GetShareStatsDto,
|
|
ShareStatsResponse,
|
|
} from './analytics.dto';
|
|
|
|
@Injectable()
|
|
export class AnalyticsService {
|
|
private readonly logger = new Logger(AnalyticsService.name);
|
|
|
|
constructor(
|
|
@InjectRepository(ShareEvent)
|
|
private readonly shareEventRepository: Repository<ShareEvent>,
|
|
@InjectRepository(Article)
|
|
private readonly articleRepository: Repository<Article>,
|
|
) {}
|
|
|
|
async trackShare(trackShareDto: TrackShareDto): Promise<ShareEvent> {
|
|
const shareEvent = this.shareEventRepository.create(trackShareDto);
|
|
|
|
// Also update the article's share counters
|
|
await this.incrementArticleShareCounter(
|
|
trackShareDto.articleId,
|
|
trackShareDto.platform,
|
|
);
|
|
|
|
return await this.shareEventRepository.save(shareEvent);
|
|
}
|
|
|
|
private async incrementArticleShareCounter(
|
|
articleId: string,
|
|
platform: SharePlatform,
|
|
): Promise<void> {
|
|
const updateField = this.getShareCounterField(platform);
|
|
if (!updateField) return;
|
|
|
|
await this.articleRepository
|
|
.createQueryBuilder()
|
|
.update(Article)
|
|
.set({ [updateField]: () => `${updateField} + 1` })
|
|
.where('id = :id', { id: articleId })
|
|
.execute();
|
|
}
|
|
|
|
private getShareCounterField(platform: SharePlatform): string | null {
|
|
switch (platform) {
|
|
case 'facebook':
|
|
return 'facebookShares';
|
|
case 'twitter':
|
|
return 'twitterShares';
|
|
case 'instagram':
|
|
return 'instagramShares';
|
|
case 'tiktok':
|
|
return 'tiktokShares';
|
|
case 'telegram':
|
|
return 'telegramShares';
|
|
default:
|
|
return null; // 'link' shares don't increment counters
|
|
}
|
|
}
|
|
|
|
async getShareStats(
|
|
getShareStatsDto: GetShareStatsDto,
|
|
): Promise<ShareStatsResponse[]> {
|
|
const query = this.articleRepository
|
|
.createQueryBuilder('article')
|
|
.select([
|
|
'article.id as "articleId"',
|
|
'article.title as "articleTitle"',
|
|
'article.facebookShares as "facebookShares"',
|
|
'article.twitterShares as "twitterShares"',
|
|
'article.instagramShares as "instagramShares"',
|
|
'article.tiktokShares as "tiktokShares"',
|
|
'article.telegramShares as "telegramShares"',
|
|
'article.views as "views"',
|
|
'article.createdAt as "createdAt"',
|
|
'article.updatedAt as "updatedAt"',
|
|
])
|
|
.addSelect(
|
|
`(article.facebookShares + article.twitterShares + article.instagramShares + article.tiktokShares + article.telegramShares) as "totalShares"`,
|
|
)
|
|
.addSelect(
|
|
`CASE
|
|
WHEN article.views > 0
|
|
THEN ROUND(
|
|
(article.facebookShares + article.twitterShares + article.instagramShares + article.tiktokShares + article.telegramShares)::decimal / article.views * 100,
|
|
2
|
|
)
|
|
ELSE 0
|
|
END as "shareRate"`,
|
|
);
|
|
|
|
if (getShareStatsDto.articleId) {
|
|
query.where('article.id = :articleId', {
|
|
articleId: getShareStatsDto.articleId,
|
|
});
|
|
}
|
|
|
|
if (getShareStatsDto.startDate) {
|
|
query.andWhere('article.createdAt >= :startDate', {
|
|
startDate: getShareStatsDto.startDate,
|
|
});
|
|
}
|
|
|
|
if (getShareStatsDto.endDate) {
|
|
query.andWhere('article.createdAt <= :endDate', {
|
|
endDate: getShareStatsDto.endDate,
|
|
});
|
|
}
|
|
|
|
query.orderBy('"totalShares"', 'DESC');
|
|
|
|
const rawResults = await query.getRawMany<{
|
|
articleId: string;
|
|
articleTitle: string;
|
|
facebookShares: string;
|
|
twitterShares: string;
|
|
instagramShares: string;
|
|
tiktokShares: string;
|
|
telegramShares: string;
|
|
views: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
totalShares: string;
|
|
shareRate: string;
|
|
}>();
|
|
|
|
// Get link shares from share_events table
|
|
const results: ShareStatsResponse[] = [];
|
|
for (const rawResult of rawResults) {
|
|
const linkShares = await this.shareEventRepository.count({
|
|
where: {
|
|
articleId: rawResult.articleId,
|
|
platform: 'link',
|
|
},
|
|
});
|
|
|
|
const facebookShares = parseInt(rawResult.facebookShares) || 0;
|
|
const twitterShares = parseInt(rawResult.twitterShares) || 0;
|
|
const instagramShares = parseInt(rawResult.instagramShares) || 0;
|
|
const tiktokShares = parseInt(rawResult.tiktokShares) || 0;
|
|
const telegramShares = parseInt(rawResult.telegramShares) || 0;
|
|
const views = parseInt(rawResult.views) || 0;
|
|
const baseTotalShares =
|
|
facebookShares +
|
|
twitterShares +
|
|
instagramShares +
|
|
tiktokShares +
|
|
telegramShares;
|
|
const totalShares = baseTotalShares + linkShares;
|
|
const shareRate =
|
|
views > 0 ? parseFloat(((totalShares / views) * 100).toFixed(2)) : 0;
|
|
|
|
results.push({
|
|
articleId: rawResult.articleId,
|
|
articleTitle: rawResult.articleTitle,
|
|
facebookShares,
|
|
twitterShares,
|
|
instagramShares,
|
|
tiktokShares,
|
|
telegramShares,
|
|
linkShares,
|
|
totalShares,
|
|
views,
|
|
shareRate,
|
|
createdAt: new Date(rawResult.createdAt),
|
|
updatedAt: new Date(rawResult.updatedAt),
|
|
});
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
async getTopSharedArticles(
|
|
limit: number = 10,
|
|
): Promise<ShareStatsResponse[]> {
|
|
const stats = await this.getShareStats({});
|
|
return stats.slice(0, limit);
|
|
}
|
|
|
|
async getShareTrends(
|
|
startDate: Date,
|
|
endDate: Date,
|
|
interval: 'day' | 'week' | 'month' = 'day',
|
|
): Promise<
|
|
Array<{ period: string; platform: SharePlatform; count: number }>
|
|
> {
|
|
const dateFormat =
|
|
interval === 'day'
|
|
? 'YYYY-MM-DD'
|
|
: interval === 'week'
|
|
? 'YYYY-WW'
|
|
: 'YYYY-MM';
|
|
|
|
const query = this.shareEventRepository
|
|
.createQueryBuilder('share_event')
|
|
.select([
|
|
`TO_CHAR(share_event.createdAt, '${dateFormat}') as period`,
|
|
'share_event.platform as platform',
|
|
'COUNT(*) as count',
|
|
])
|
|
.where('share_event.createdAt >= :startDate', { startDate })
|
|
.andWhere('share_event.createdAt <= :endDate', { endDate })
|
|
.groupBy('period, platform')
|
|
.orderBy('period', 'ASC');
|
|
|
|
const rawResults = await query.getRawMany<{
|
|
period: string;
|
|
platform: string;
|
|
count: string;
|
|
}>();
|
|
|
|
// Convert platform strings to SharePlatform type
|
|
return rawResults.map((result) => ({
|
|
period: result.period,
|
|
platform: result.platform as SharePlatform,
|
|
count: parseInt(result.count) || 0,
|
|
}));
|
|
}
|
|
|
|
async getTotalShareStats(): Promise<{
|
|
totalShares: number;
|
|
facebookShares: number;
|
|
twitterShares: number;
|
|
instagramShares: number;
|
|
tiktokShares: number;
|
|
telegramShares: number;
|
|
linkShares: number;
|
|
}> {
|
|
interface ArticleStatsRaw {
|
|
facebookShares: string;
|
|
twitterShares: string;
|
|
instagramShares: string;
|
|
tiktokShares: string;
|
|
telegramShares: string;
|
|
}
|
|
|
|
const articleStats = (await this.articleRepository
|
|
.createQueryBuilder('article')
|
|
.select([
|
|
'SUM(article.facebookShares) as facebookShares',
|
|
'SUM(article.twitterShares) as twitterShares',
|
|
'SUM(article.instagramShares) as instagramShares',
|
|
'SUM(article.tiktokShares) as tiktokShares',
|
|
'SUM(article.telegramShares) as telegramShares',
|
|
])
|
|
.getRawOne()) as ArticleStatsRaw;
|
|
|
|
const linkShares = await this.shareEventRepository.count({
|
|
where: { platform: 'link' },
|
|
});
|
|
|
|
const facebookShares = parseInt(articleStats?.facebookShares || '0') || 0;
|
|
const twitterShares = parseInt(articleStats?.twitterShares || '0') || 0;
|
|
const instagramShares = parseInt(articleStats?.instagramShares || '0') || 0;
|
|
const tiktokShares = parseInt(articleStats?.tiktokShares || '0') || 0;
|
|
const telegramShares = parseInt(articleStats?.telegramShares || '0') || 0;
|
|
|
|
const totalShares =
|
|
facebookShares +
|
|
twitterShares +
|
|
instagramShares +
|
|
tiktokShares +
|
|
telegramShares +
|
|
linkShares;
|
|
|
|
return {
|
|
totalShares,
|
|
facebookShares,
|
|
twitterShares,
|
|
instagramShares,
|
|
tiktokShares,
|
|
telegramShares,
|
|
linkShares,
|
|
};
|
|
}
|
|
}
|