|
|
@@ -51,6 +51,55 @@ export class WorkDayStatisticsService {
|
|
|
private accountRepository = AppDataSource.getRepository(PlatformAccount);
|
|
|
private userDayStatisticsRepository = AppDataSource.getRepository(UserDayStatistics);
|
|
|
|
|
|
+ private formatDate(d: Date) {
|
|
|
+ const yyyy = d.getFullYear();
|
|
|
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
|
+ const dd = String(d.getDate()).padStart(2, '0');
|
|
|
+ return `${yyyy}-${mm}-${dd}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取某个账号在指定日期(<= targetDate)时各作品的“最新一条”累计数据总和
|
|
|
+ * 口径:对该账号所有作品,每个作品取 record_date <= targetDate 的最大日期那条记录,然后把 play/like/comment/collect 求和
|
|
|
+ */
|
|
|
+ private async getWorkSumsAtDate(
|
|
|
+ workIds: number[],
|
|
|
+ targetDate: string
|
|
|
+ ): Promise<{ views: number; likes: number; comments: number; collects: number }> {
|
|
|
+ if (!workIds.length) {
|
|
|
+ return { views: 0, likes: 0, comments: 0, collects: 0 };
|
|
|
+ }
|
|
|
+
|
|
|
+ // MySQL: 派生表先取每个作品 <= targetDate 的最新日期,再回连取该日数据求和
|
|
|
+ // 注意:workIds 使用 IN (...),由 TypeORM 负责参数化,避免注入
|
|
|
+ const placeholders = workIds.map(() => '?').join(',');
|
|
|
+ const sql = `
|
|
|
+ SELECT
|
|
|
+ COALESCE(SUM(wds.play_count), 0) AS views,
|
|
|
+ COALESCE(SUM(wds.like_count), 0) AS likes,
|
|
|
+ COALESCE(SUM(wds.comment_count), 0) AS comments,
|
|
|
+ COALESCE(SUM(wds.collect_count), 0) AS collects
|
|
|
+ FROM work_day_statistics wds
|
|
|
+ INNER JOIN (
|
|
|
+ SELECT wds2.work_id, MAX(wds2.record_date) AS record_date
|
|
|
+ FROM work_day_statistics wds2
|
|
|
+ WHERE wds2.work_id IN (${placeholders})
|
|
|
+ AND wds2.record_date <= ?
|
|
|
+ GROUP BY wds2.work_id
|
|
|
+ ) latest
|
|
|
+ ON latest.work_id = wds.work_id AND latest.record_date = wds.record_date
|
|
|
+ `;
|
|
|
+
|
|
|
+ const rows = await AppDataSource.query(sql, [...workIds, targetDate]);
|
|
|
+ const row = rows?.[0] || {};
|
|
|
+ return {
|
|
|
+ views: Number(row.views) || 0,
|
|
|
+ likes: Number(row.likes) || 0,
|
|
|
+ comments: Number(row.comments) || 0,
|
|
|
+ collects: Number(row.collects) || 0,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 保存作品日统计数据
|
|
|
* 当天的数据走更新流,日期变化走新增流
|
|
|
@@ -254,87 +303,122 @@ export class WorkDayStatisticsService {
|
|
|
dateStart.setDate(dateStart.getDate() - Math.min(days, 30) + 1);
|
|
|
}
|
|
|
|
|
|
+ const endDateStr = endDate ? endDate : this.formatDate(dateEnd);
|
|
|
+ const startDateStr = startDate ? startDate : this.formatDate(dateStart);
|
|
|
+ const isSingleDay = startDateStr === endDateStr;
|
|
|
+ // 单日查询(如“昨天”)按“当日增量”口径:当日粉丝 - 前一日粉丝
|
|
|
+ const startBaselineStr = (() => {
|
|
|
+ if (!isSingleDay) return startDateStr;
|
|
|
+ const d = new Date(endDateStr);
|
|
|
+ d.setDate(d.getDate() - 1);
|
|
|
+ return this.formatDate(d);
|
|
|
+ })();
|
|
|
+
|
|
|
// 获取用户的所有账号
|
|
|
const accounts = await this.accountRepository.find({
|
|
|
where: { userId },
|
|
|
});
|
|
|
|
|
|
- const platformData: PlatformStatItem[] = [];
|
|
|
+ // 按平台聚合数据:Map<platform, { fansCount, fansIncrease, viewsCount, likesCount, commentsCount, collectsCount }>
|
|
|
+ const platformMap = new Map<string, {
|
|
|
+ fansCount: number;
|
|
|
+ fansIncrease: number;
|
|
|
+ viewsCount: number;
|
|
|
+ likesCount: number;
|
|
|
+ commentsCount: number;
|
|
|
+ collectsCount: number;
|
|
|
+ }>();
|
|
|
|
|
|
+ // 遍历每个账号,计算该账号的数据,然后累加到对应平台
|
|
|
for (const account of accounts) {
|
|
|
- // 获取该账号在区间内第一天和最后一天的数据
|
|
|
- const firstDayQuery = this.statisticsRepository
|
|
|
- .createQueryBuilder('wds')
|
|
|
- .innerJoin(Work, 'w', 'wds.work_id = w.id')
|
|
|
- .select('SUM(wds.play_count)', 'views')
|
|
|
- .addSelect('SUM(wds.like_count)', 'likes')
|
|
|
- .addSelect('SUM(wds.comment_count)', 'comments')
|
|
|
- .addSelect('SUM(wds.collect_count)', 'collects')
|
|
|
- .where('w.accountId = :accountId', { accountId: account.id })
|
|
|
- .andWhere('wds.record_date = (SELECT MIN(record_date) FROM work_day_statistics wds2 INNER JOIN works w2 ON wds2.work_id = w2.id WHERE w2.accountId = :accountId2 AND wds2.record_date >= :dateStart AND wds2.record_date <= :dateEnd)', {
|
|
|
- accountId2: account.id,
|
|
|
- dateStart,
|
|
|
- dateEnd,
|
|
|
- });
|
|
|
+ // 获取该账号的作品列表(用于按“所有作品累计值之和”计算)
|
|
|
+ const works = await this.workRepository.find({
|
|
|
+ where: { accountId: account.id },
|
|
|
+ select: ['id'],
|
|
|
+ });
|
|
|
+ const workIds = works.map(w => w.id);
|
|
|
|
|
|
- const lastDayQuery = this.statisticsRepository
|
|
|
- .createQueryBuilder('wds')
|
|
|
- .innerJoin(Work, 'w', 'wds.work_id = w.id')
|
|
|
- .select('SUM(wds.play_count)', 'views')
|
|
|
- .addSelect('SUM(wds.like_count)', 'likes')
|
|
|
- .addSelect('SUM(wds.comment_count)', 'comments')
|
|
|
- .addSelect('SUM(wds.collect_count)', 'collects')
|
|
|
- .where('w.accountId = :accountId', { accountId: account.id })
|
|
|
- .andWhere('wds.record_date = (SELECT MAX(record_date) FROM work_day_statistics wds2 INNER JOIN works w2 ON wds2.work_id = w2.id WHERE w2.accountId = :accountId2 AND wds2.record_date >= :dateStart AND wds2.record_date <= :dateEnd)', {
|
|
|
- accountId2: account.id,
|
|
|
- dateStart,
|
|
|
- dateEnd,
|
|
|
- });
|
|
|
+ // ===== 粉丝口径修正 =====
|
|
|
+ // 取 endDate 当天粉丝(若当天没有记录,则取 <= endDate 的最近一条)
|
|
|
+ const endUserStat =
|
|
|
+ (await this.userDayStatisticsRepository
|
|
|
+ .createQueryBuilder('uds')
|
|
|
+ .where('uds.account_id = :accountId', { accountId: account.id })
|
|
|
+ .andWhere('DATE(uds.record_date) = :d', { d: endDateStr })
|
|
|
+ .getOne()) ??
|
|
|
+ (await this.userDayStatisticsRepository
|
|
|
+ .createQueryBuilder('uds')
|
|
|
+ .where('uds.account_id = :accountId', { accountId: account.id })
|
|
|
+ .andWhere('DATE(uds.record_date) <= :d', { d: endDateStr })
|
|
|
+ .orderBy('uds.record_date', 'DESC')
|
|
|
+ .getOne());
|
|
|
|
|
|
- const [firstDay, lastDay] = await Promise.all([
|
|
|
- firstDayQuery.getRawOne(),
|
|
|
- lastDayQuery.getRawOne(),
|
|
|
- ]);
|
|
|
+ const endFans = endUserStat?.fansCount ?? account.fansCount ?? 0;
|
|
|
|
|
|
- // 从 user_day_statistics 表获取粉丝数
|
|
|
- const todayDate = new Date();
|
|
|
- todayDate.setHours(0, 0, 0, 0);
|
|
|
- const todayUserStat = await this.userDayStatisticsRepository.findOne({
|
|
|
- where: {
|
|
|
- accountId: account.id,
|
|
|
- recordDate: todayDate,
|
|
|
- },
|
|
|
- });
|
|
|
- const currentFans = todayUserStat?.fansCount ?? account.fansCount ?? 0;
|
|
|
-
|
|
|
- // 获取最早日期的粉丝数
|
|
|
- const earliestUserStat = await this.userDayStatisticsRepository
|
|
|
- .createQueryBuilder('uds')
|
|
|
- .where('uds.account_id = :accountId', { accountId: account.id })
|
|
|
- .andWhere('uds.record_date >= :dateStart', { dateStart })
|
|
|
- .andWhere('uds.record_date <= :dateEnd', { dateEnd })
|
|
|
- .orderBy('uds.record_date', 'ASC')
|
|
|
- .getOne();
|
|
|
- const earliestFans = earliestUserStat?.fansCount ?? currentFans;
|
|
|
- const fansIncrease = currentFans - earliestFans;
|
|
|
+ // 取 baseline 当天粉丝(若没有记录,则取 <= baseline 的最近一条)
|
|
|
+ const baselineUserStat =
|
|
|
+ (await this.userDayStatisticsRepository
|
|
|
+ .createQueryBuilder('uds')
|
|
|
+ .where('uds.account_id = :accountId', { accountId: account.id })
|
|
|
+ .andWhere('DATE(uds.record_date) = :d', { d: startBaselineStr })
|
|
|
+ .getOne()) ??
|
|
|
+ (await this.userDayStatisticsRepository
|
|
|
+ .createQueryBuilder('uds')
|
|
|
+ .where('uds.account_id = :accountId', { accountId: account.id })
|
|
|
+ .andWhere('DATE(uds.record_date) <= :d', { d: startBaselineStr })
|
|
|
+ .orderBy('uds.record_date', 'DESC')
|
|
|
+ .getOne());
|
|
|
+
|
|
|
+ const baselineFans = baselineUserStat?.fansCount ?? endFans;
|
|
|
+ // 涨粉量允许为负数(掉粉),不做截断
|
|
|
+ const accountFansIncrease = endFans - baselineFans;
|
|
|
+
|
|
|
+ // ===== 播放/点赞/评论/收藏口径修正 =====
|
|
|
+ // 单日:当日累计 - 前一日累计
|
|
|
+ // 区间:end 日累计 - start 日累计
|
|
|
+ const [endSums, baseSums] = await Promise.all([
|
|
|
+ this.getWorkSumsAtDate(workIds, endDateStr),
|
|
|
+ this.getWorkSumsAtDate(workIds, startBaselineStr),
|
|
|
+ ]);
|
|
|
|
|
|
- const viewsIncrease = (parseInt(lastDay?.views) || 0) - (parseInt(firstDay?.views) || 0);
|
|
|
- const likesIncrease = (parseInt(lastDay?.likes) || 0) - (parseInt(firstDay?.likes) || 0);
|
|
|
- const commentsIncrease = (parseInt(lastDay?.comments) || 0) - (parseInt(firstDay?.comments) || 0);
|
|
|
- const collectsIncrease = (parseInt(lastDay?.collects) || 0) - (parseInt(firstDay?.collects) || 0);
|
|
|
+ const accountViewsIncrease = endSums.views - baseSums.views;
|
|
|
+ const accountLikesIncrease = endSums.likes - baseSums.likes;
|
|
|
+ const accountCommentsIncrease = endSums.comments - baseSums.comments;
|
|
|
+ const accountCollectsIncrease = endSums.collects - baseSums.collects;
|
|
|
+
|
|
|
+ // 累加到平台聚合数据
|
|
|
+ const platformKey = account.platform;
|
|
|
+ if (!platformMap.has(platformKey)) {
|
|
|
+ platformMap.set(platformKey, {
|
|
|
+ fansCount: 0,
|
|
|
+ fansIncrease: 0,
|
|
|
+ viewsCount: 0,
|
|
|
+ likesCount: 0,
|
|
|
+ commentsCount: 0,
|
|
|
+ collectsCount: 0,
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
- platformData.push({
|
|
|
- platform: account.platform,
|
|
|
- fansCount: currentFans,
|
|
|
- fansIncrease,
|
|
|
- viewsCount: Math.max(0, viewsIncrease),
|
|
|
- likesCount: Math.max(0, likesIncrease),
|
|
|
- commentsCount: Math.max(0, commentsIncrease),
|
|
|
- collectsCount: Math.max(0, collectsIncrease),
|
|
|
- });
|
|
|
+ const platformStat = platformMap.get(platformKey)!;
|
|
|
+ platformStat.fansCount += endFans;
|
|
|
+ platformStat.fansIncrease += accountFansIncrease;
|
|
|
+ platformStat.viewsCount += accountViewsIncrease;
|
|
|
+ platformStat.likesCount += accountLikesIncrease;
|
|
|
+ platformStat.commentsCount += accountCommentsIncrease;
|
|
|
+ platformStat.collectsCount += accountCollectsIncrease;
|
|
|
}
|
|
|
|
|
|
- // 按粉丝数降序排序
|
|
|
+ // 转换为数组格式,按粉丝数降序排序
|
|
|
+ const platformData: PlatformStatItem[] = Array.from(platformMap.entries()).map(([platform, stat]) => ({
|
|
|
+ platform,
|
|
|
+ fansCount: stat.fansCount,
|
|
|
+ fansIncrease: stat.fansIncrease,
|
|
|
+ viewsCount: stat.viewsCount,
|
|
|
+ likesCount: stat.likesCount,
|
|
|
+ commentsCount: stat.commentsCount,
|
|
|
+ collectsCount: stat.collectsCount,
|
|
|
+ }));
|
|
|
+
|
|
|
platformData.sort((a, b) => b.fansCount - a.fansCount);
|
|
|
|
|
|
return platformData;
|