|
@@ -1,5 +1,5 @@
|
|
|
import { AppDataSource, WorkDayStatistics, Work, PlatformAccount, UserDayStatistics } from '../models/index.js';
|
|
import { AppDataSource, WorkDayStatistics, Work, PlatformAccount, UserDayStatistics } from '../models/index.js';
|
|
|
-import { Between, In } from 'typeorm';
|
|
|
|
|
|
|
+import { In } from 'typeorm';
|
|
|
import { logger } from '../utils/logger.js';
|
|
import { logger } from '../utils/logger.js';
|
|
|
|
|
|
|
|
interface StatisticsItem {
|
|
interface StatisticsItem {
|
|
@@ -306,154 +306,87 @@ export class WorkDayStatisticsService {
|
|
|
|
|
|
|
|
const endDateStr = endDate ? endDate : this.formatDate(dateEnd);
|
|
const endDateStr = endDate ? endDate : this.formatDate(dateEnd);
|
|
|
const startDateStr = startDate ? startDate : this.formatDate(dateStart);
|
|
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 },
|
|
|
|
|
- });
|
|
|
|
|
|
|
|
|
|
- // 按平台聚合数据:Map<platform, { fansCount, fansIncrease, viewsCount, likesCount, commentsCount, collectsCount, latestUpdateTime }>
|
|
|
|
|
- const platformMap = new Map<string, {
|
|
|
|
|
- fansCount: number;
|
|
|
|
|
- fansIncrease: number;
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 口径变更:user_day_statistics 的 play/comment/like/collect/fans_increase 等字段为“每日单独值”
|
|
|
|
|
+ * 因此:
|
|
|
|
|
+ * - 区间统计:直接按日期范围 SUM
|
|
|
|
|
+ * - 单日统计:startDate=endDate 时,也同样按该日 SUM(无需再做“累计差”)
|
|
|
|
|
+ * 粉丝数:使用 platform_accounts.fans_count(当前值)
|
|
|
|
|
+ */
|
|
|
|
|
+ const [fansRows, udsRows] = await Promise.all([
|
|
|
|
|
+ this.accountRepository
|
|
|
|
|
+ .createQueryBuilder('pa')
|
|
|
|
|
+ .select('pa.platform', 'platform')
|
|
|
|
|
+ .addSelect('COALESCE(SUM(pa.fansCount), 0)', 'fansCount')
|
|
|
|
|
+ .where('pa.userId = :userId', { userId })
|
|
|
|
|
+ .groupBy('pa.platform')
|
|
|
|
|
+ .getRawMany(),
|
|
|
|
|
+ this.userDayStatisticsRepository
|
|
|
|
|
+ .createQueryBuilder('uds')
|
|
|
|
|
+ .innerJoin(PlatformAccount, 'pa', 'pa.id = uds.account_id')
|
|
|
|
|
+ .select('pa.platform', 'platform')
|
|
|
|
|
+ .addSelect('COALESCE(SUM(uds.play_count), 0)', 'viewsCount')
|
|
|
|
|
+ .addSelect('COALESCE(SUM(uds.comment_count), 0)', 'commentsCount')
|
|
|
|
|
+ .addSelect('COALESCE(SUM(uds.like_count), 0)', 'likesCount')
|
|
|
|
|
+ .addSelect('COALESCE(SUM(uds.collect_count), 0)', 'collectsCount')
|
|
|
|
|
+ .addSelect('COALESCE(SUM(uds.fans_increase), 0)', 'fansIncrease')
|
|
|
|
|
+ .addSelect('MAX(uds.updated_at)', 'latestUpdateTime')
|
|
|
|
|
+ .where('pa.user_id = :userId', { userId })
|
|
|
|
|
+ .andWhere('DATE(uds.record_date) >= :startDate', { startDate: startDateStr })
|
|
|
|
|
+ .andWhere('DATE(uds.record_date) <= :endDate', { endDate: endDateStr })
|
|
|
|
|
+ .groupBy('pa.platform')
|
|
|
|
|
+ .getRawMany(),
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ const fansMap = new Map<string, number>();
|
|
|
|
|
+ for (const row of fansRows || []) {
|
|
|
|
|
+ const platform = String(row.platform || '');
|
|
|
|
|
+ if (!platform) continue;
|
|
|
|
|
+ fansMap.set(platform, Number(row.fansCount) || 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const statMap = new Map<string, {
|
|
|
viewsCount: number;
|
|
viewsCount: number;
|
|
|
- likesCount: number;
|
|
|
|
|
commentsCount: number;
|
|
commentsCount: number;
|
|
|
|
|
+ likesCount: number;
|
|
|
collectsCount: number;
|
|
collectsCount: number;
|
|
|
- latestUpdateTime: Date | null;
|
|
|
|
|
|
|
+ fansIncrease: number;
|
|
|
|
|
+ latestUpdateTime?: string | Date | null;
|
|
|
}>();
|
|
}>();
|
|
|
-
|
|
|
|
|
- // 遍历每个账号,计算该账号的数据,然后累加到对应平台
|
|
|
|
|
- for (const account of accounts) {
|
|
|
|
|
- // 获取该账号的作品列表(用于按“所有作品累计值之和”计算)
|
|
|
|
|
- const works = await this.workRepository.find({
|
|
|
|
|
- where: { accountId: account.id },
|
|
|
|
|
- select: ['id'],
|
|
|
|
|
|
|
+ for (const row of udsRows || []) {
|
|
|
|
|
+ const platform = String(row.platform || '');
|
|
|
|
|
+ if (!platform) continue;
|
|
|
|
|
+ statMap.set(platform, {
|
|
|
|
|
+ viewsCount: Number(row.viewsCount) || 0,
|
|
|
|
|
+ commentsCount: Number(row.commentsCount) || 0,
|
|
|
|
|
+ likesCount: Number(row.likesCount) || 0,
|
|
|
|
|
+ collectsCount: Number(row.collectsCount) || 0,
|
|
|
|
|
+ fansIncrease: Number(row.fansIncrease) || 0,
|
|
|
|
|
+ latestUpdateTime: row.latestUpdateTime ?? null,
|
|
|
});
|
|
});
|
|
|
- const workIds = works.map(w => w.id);
|
|
|
|
|
-
|
|
|
|
|
- // ===== 粉丝口径修正 =====
|
|
|
|
|
- // 取 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 endFans = endUserStat?.fansCount ?? account.fansCount ?? 0;
|
|
|
|
|
-
|
|
|
|
|
- // 取 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 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,
|
|
|
|
|
- latestUpdateTime: null,
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const platformStat = platformMap.get(platformKey)!;
|
|
|
|
|
- platformStat.fansCount += endFans;
|
|
|
|
|
- platformStat.fansIncrease += accountFansIncrease;
|
|
|
|
|
- platformStat.viewsCount += accountViewsIncrease;
|
|
|
|
|
- platformStat.likesCount += accountLikesIncrease;
|
|
|
|
|
- platformStat.commentsCount += accountCommentsIncrease;
|
|
|
|
|
- platformStat.collectsCount += accountCollectsIncrease;
|
|
|
|
|
-
|
|
|
|
|
- // 查询该账号在当前时间段内的最晚 updated_at
|
|
|
|
|
- const latestUserStat = await this.userDayStatisticsRepository
|
|
|
|
|
- .createQueryBuilder('uds')
|
|
|
|
|
- .where('uds.account_id = :accountId', { accountId: account.id })
|
|
|
|
|
- .andWhere('DATE(uds.record_date) >= :startDate', { startDate: startDateStr })
|
|
|
|
|
- .andWhere('DATE(uds.record_date) <= :endDate', { endDate: endDateStr })
|
|
|
|
|
- .orderBy('uds.updated_at', 'DESC')
|
|
|
|
|
- .getOne();
|
|
|
|
|
-
|
|
|
|
|
- if (latestUserStat && latestUserStat.updatedAt) {
|
|
|
|
|
- // 更新平台的最晚更新时间
|
|
|
|
|
- if (!platformStat.latestUpdateTime || latestUserStat.updatedAt > platformStat.latestUpdateTime) {
|
|
|
|
|
- platformStat.latestUpdateTime = latestUserStat.updatedAt;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 转换为数组格式,按粉丝数降序排序
|
|
|
|
|
- const platformData: PlatformStatItem[] = Array.from(platformMap.entries()).map(([platform, stat]) => {
|
|
|
|
|
- // 格式化更新时间为 "MM-DD HH:mm" 格式
|
|
|
|
|
- let updateTime: string | undefined;
|
|
|
|
|
- if (stat.latestUpdateTime) {
|
|
|
|
|
- const date = new Date(stat.latestUpdateTime);
|
|
|
|
|
- const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
- const day = String(date.getDate()).padStart(2, '0');
|
|
|
|
|
- const hours = String(date.getHours()).padStart(2, '0');
|
|
|
|
|
- const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
|
|
|
- updateTime = `${month}-${day} ${hours}:${minutes}`;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const platforms = new Set<string>([...fansMap.keys(), ...statMap.keys()]);
|
|
|
|
|
+
|
|
|
|
|
+ const platformData: PlatformStatItem[] = Array.from(platforms).map((platform) => {
|
|
|
|
|
+ const stat = statMap.get(platform);
|
|
|
|
|
+ const fansCount = fansMap.get(platform) ?? 0;
|
|
|
|
|
+ const latestUpdate = stat?.latestUpdateTime ? new Date(stat.latestUpdateTime as any) : null;
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
platform,
|
|
platform,
|
|
|
- fansCount: stat.fansCount,
|
|
|
|
|
- fansIncrease: stat.fansIncrease,
|
|
|
|
|
- viewsCount: stat.viewsCount,
|
|
|
|
|
- likesCount: stat.likesCount,
|
|
|
|
|
- commentsCount: stat.commentsCount,
|
|
|
|
|
- collectsCount: stat.collectsCount,
|
|
|
|
|
- updateTime,
|
|
|
|
|
|
|
+ fansCount,
|
|
|
|
|
+ fansIncrease: stat?.fansIncrease ?? 0,
|
|
|
|
|
+ viewsCount: stat?.viewsCount ?? 0,
|
|
|
|
|
+ likesCount: stat?.likesCount ?? 0,
|
|
|
|
|
+ commentsCount: stat?.commentsCount ?? 0,
|
|
|
|
|
+ collectsCount: stat?.collectsCount ?? 0,
|
|
|
|
|
+ updateTime: latestUpdate ? latestUpdate.toISOString() : undefined,
|
|
|
};
|
|
};
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- platformData.sort((a, b) => b.fansCount - a.fansCount);
|
|
|
|
|
-
|
|
|
|
|
|
|
+ platformData.sort((a, b) => (b.fansCount || 0) - (a.fansCount || 0));
|
|
|
return platformData;
|
|
return platformData;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -518,6 +451,86 @@ export class WorkDayStatisticsService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
|
|
+ * 获取每个账号在指定日期(<= targetDate)时,user_day_statistics 的“最新一条”
|
|
|
|
|
+ * 主要用于:总粉丝、更新时间等需要“最新状态”的字段。
|
|
|
|
|
+ */
|
|
|
|
|
+ private async getLatestUserDayStatsAtDate(
|
|
|
|
|
+ accountIds: number[],
|
|
|
|
|
+ targetDate: string
|
|
|
|
|
+ ): Promise<Map<number, { fansCount: number; updatedAt: Date | null; recordDate: Date | null }>> {
|
|
|
|
|
+ const map = new Map<number, { fansCount: number; updatedAt: Date | null; recordDate: Date | null }>();
|
|
|
|
|
+ if (!accountIds.length) return map;
|
|
|
|
|
+
|
|
|
|
|
+ // MySQL: 派生表先取每个账号 <= targetDate 的最新日期,再回连取该日数据
|
|
|
|
|
+ const placeholders = accountIds.map(() => '?').join(',');
|
|
|
|
|
+ const sql = `
|
|
|
|
|
+ SELECT
|
|
|
|
|
+ uds.account_id AS accountId,
|
|
|
|
|
+ uds.fans_count AS fansCount,
|
|
|
|
|
+ uds.record_date AS recordDate,
|
|
|
|
|
+ uds.updated_at AS updatedAt
|
|
|
|
|
+ FROM user_day_statistics uds
|
|
|
|
|
+ INNER JOIN (
|
|
|
|
|
+ SELECT account_id, MAX(record_date) AS record_date
|
|
|
|
|
+ FROM user_day_statistics
|
|
|
|
|
+ WHERE account_id IN (${placeholders})
|
|
|
|
|
+ AND record_date <= ?
|
|
|
|
|
+ GROUP BY account_id
|
|
|
|
|
+ ) latest
|
|
|
|
|
+ ON latest.account_id = uds.account_id AND latest.record_date = uds.record_date
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ const rows: any[] = await AppDataSource.query(sql, [...accountIds, targetDate]);
|
|
|
|
|
+ for (const row of rows || []) {
|
|
|
|
|
+ const accountId = Number(row.accountId) || 0;
|
|
|
|
|
+ if (!accountId) continue;
|
|
|
|
|
+ const fansCount = Number(row.fansCount) || 0;
|
|
|
|
|
+ const recordDate = row.recordDate ? new Date(row.recordDate) : null;
|
|
|
|
|
+ const updatedAt = row.updatedAt ? new Date(row.updatedAt) : null;
|
|
|
|
|
+ map.set(accountId, { fansCount, updatedAt, recordDate });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return map;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取指定日期(= dateStr)每个账号在 user_day_statistics 的数据(用于“昨日”口径的一一对应)。
|
|
|
|
|
+ */
|
|
|
|
|
+ private async getUserDayStatsByExactDate(
|
|
|
|
|
+ accountIds: number[],
|
|
|
|
|
+ dateStr: string
|
|
|
|
|
+ ): Promise<Map<number, { playCount: number; commentCount: number; likeCount: number; fansIncrease: number; updatedAt: Date | null }>> {
|
|
|
|
|
+ const map = new Map<number, { playCount: number; commentCount: number; likeCount: number; fansIncrease: number; updatedAt: Date | null }>();
|
|
|
|
|
+ if (!accountIds.length) return map;
|
|
|
|
|
+
|
|
|
|
|
+ const rows = await this.userDayStatisticsRepository
|
|
|
|
|
+ .createQueryBuilder('uds')
|
|
|
|
|
+ .select('uds.account_id', 'accountId')
|
|
|
|
|
+ .addSelect('uds.play_count', 'playCount')
|
|
|
|
|
+ .addSelect('uds.comment_count', 'commentCount')
|
|
|
|
|
+ .addSelect('uds.like_count', 'likeCount')
|
|
|
|
|
+ .addSelect('uds.fans_increase', 'fansIncrease')
|
|
|
|
|
+ .addSelect('uds.updated_at', 'updatedAt')
|
|
|
|
|
+ .where('uds.account_id IN (:...accountIds)', { accountIds })
|
|
|
|
|
+ .andWhere('DATE(uds.record_date) = :d', { d: dateStr })
|
|
|
|
|
+ .getRawMany();
|
|
|
|
|
+
|
|
|
|
|
+ for (const row of rows || []) {
|
|
|
|
|
+ const accountId = Number(row.accountId) || 0;
|
|
|
|
|
+ if (!accountId) continue;
|
|
|
|
|
+ map.set(accountId, {
|
|
|
|
|
+ playCount: Number(row.playCount) || 0,
|
|
|
|
|
+ commentCount: Number(row.commentCount) || 0,
|
|
|
|
|
+ likeCount: Number(row.likeCount) || 0,
|
|
|
|
|
+ fansIncrease: Number(row.fansIncrease) || 0,
|
|
|
|
|
+ updatedAt: row.updatedAt ? new Date(row.updatedAt) : null,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return map;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
* 获取数据总览
|
|
* 获取数据总览
|
|
|
* 返回账号列表和汇总统计数据
|
|
* 返回账号列表和汇总统计数据
|
|
|
*/
|
|
*/
|
|
@@ -577,6 +590,32 @@ export class WorkDayStatisticsService {
|
|
|
|
|
|
|
|
logger.info(`[WorkDayStatistics] getOverview - userId: ${userId}, today: ${todayStr}, yesterday: ${yesterdayStr}`);
|
|
logger.info(`[WorkDayStatistics] getOverview - userId: ${userId}, today: ${todayStr}, yesterday: ${yesterdayStr}`);
|
|
|
|
|
|
|
|
|
|
+ const accountIds = accounts.map(a => a.id);
|
|
|
|
|
+
|
|
|
|
|
+ // 账号总数:platform_accounts 中 user_id 对应数量(等价于 accounts.length)
|
|
|
|
|
+ const totalAccounts = accounts.length;
|
|
|
|
|
+
|
|
|
|
|
+ // 列表“总播放/汇总总播放”:统一从 works.play_count 聚合(累计)
|
|
|
|
|
+ const worksPlayRows = accountIds.length
|
|
|
|
|
+ ? await this.workRepository
|
|
|
|
|
+ .createQueryBuilder('w')
|
|
|
|
|
+ .select('w.accountId', 'accountId')
|
|
|
|
|
+ .addSelect('COALESCE(SUM(w.playCount), 0)', 'playCount')
|
|
|
|
|
+ .where('w.userId = :userId', { userId })
|
|
|
|
|
+ .andWhere('w.accountId IN (:...accountIds)', { accountIds })
|
|
|
|
|
+ .groupBy('w.accountId')
|
|
|
|
|
+ .getRawMany()
|
|
|
|
|
+ : [];
|
|
|
|
|
+ const totalPlayMap = new Map<number, number>();
|
|
|
|
|
+ for (const row of worksPlayRows || []) {
|
|
|
|
|
+ totalPlayMap.set(Number(row.accountId) || 0, Number(row.playCount) || 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // “昨日”口径:只取 user_day_statistics 指定日期那一行,一一对应
|
|
|
|
|
+ const yesterdayUdsMap = await this.getUserDayStatsByExactDate(accountIds, yesterdayStr);
|
|
|
|
|
+
|
|
|
|
|
+ // 粉丝数口径:直接取 platform_accounts.fans_count(不跟随 user_day_statistics)
|
|
|
|
|
+
|
|
|
const accountList: Array<{
|
|
const accountList: Array<{
|
|
|
id: number;
|
|
id: number;
|
|
|
nickname: string;
|
|
nickname: string;
|
|
@@ -584,6 +623,7 @@ export class WorkDayStatisticsService {
|
|
|
avatarUrl: string | null;
|
|
avatarUrl: string | null;
|
|
|
platform: string;
|
|
platform: string;
|
|
|
groupId: number | null;
|
|
groupId: number | null;
|
|
|
|
|
+ groupName?: string | null;
|
|
|
fansCount: number;
|
|
fansCount: number;
|
|
|
totalIncome: number | null;
|
|
totalIncome: number | null;
|
|
|
yesterdayIncome: number | null;
|
|
yesterdayIncome: number | null;
|
|
@@ -597,7 +637,6 @@ export class WorkDayStatisticsService {
|
|
|
}> = [];
|
|
}> = [];
|
|
|
|
|
|
|
|
// 汇总统计数据
|
|
// 汇总统计数据
|
|
|
- let totalAccounts = 0;
|
|
|
|
|
let totalIncome = 0;
|
|
let totalIncome = 0;
|
|
|
let yesterdayIncome = 0;
|
|
let yesterdayIncome = 0;
|
|
|
let totalViews = 0;
|
|
let totalViews = 0;
|
|
@@ -608,250 +647,16 @@ export class WorkDayStatisticsService {
|
|
|
let yesterdayFansIncrease = 0;
|
|
let yesterdayFansIncrease = 0;
|
|
|
|
|
|
|
|
for (const account of accounts) {
|
|
for (const account of accounts) {
|
|
|
- // 获取该账号的所有作品ID
|
|
|
|
|
- const works = await this.workRepository.find({
|
|
|
|
|
- where: { accountId: account.id },
|
|
|
|
|
- select: ['id'],
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ const accountTotalViews = totalPlayMap.get(account.id) ?? 0;
|
|
|
|
|
+ const yesterdayUds = yesterdayUdsMap.get(account.id);
|
|
|
|
|
|
|
|
- if (works.length === 0) {
|
|
|
|
|
- // 如果没有作品,只返回账号基本信息
|
|
|
|
|
- // 从 user_day_statistics 表获取账号的最新粉丝数
|
|
|
|
|
- // 使用日期字符串直接查询(更可靠,避免时区问题)
|
|
|
|
|
- const todayUserStat = await this.userDayStatisticsRepository
|
|
|
|
|
- .createQueryBuilder('uds')
|
|
|
|
|
- .where('uds.account_id = :accountId', { accountId: account.id })
|
|
|
|
|
- .andWhere('DATE(uds.record_date) = :today', { today: todayStr })
|
|
|
|
|
- .getOne();
|
|
|
|
|
-
|
|
|
|
|
- const yesterdayUserStat = await this.userDayStatisticsRepository
|
|
|
|
|
- .createQueryBuilder('uds')
|
|
|
|
|
- .where('uds.account_id = :accountId', { accountId: account.id })
|
|
|
|
|
- .andWhere('DATE(uds.record_date) = :yesterday', { yesterday: yesterdayStr })
|
|
|
|
|
- .getOne();
|
|
|
|
|
-
|
|
|
|
|
- // 获取今天的粉丝数:优先使用统计数据,如果没有则使用账号表的当前值
|
|
|
|
|
- let accountFansCount: number;
|
|
|
|
|
- if (todayUserStat) {
|
|
|
|
|
- accountFansCount = todayUserStat.fansCount || 0;
|
|
|
|
|
- } else {
|
|
|
|
|
- accountFansCount = account.fansCount || 0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 计算昨日涨粉(今天粉丝数 - 昨天粉丝数)
|
|
|
|
|
- let accountYesterdayFansIncrease: number;
|
|
|
|
|
- if (yesterdayUserStat) {
|
|
|
|
|
- // 昨天有数据,直接使用
|
|
|
|
|
- const yesterdayFans = yesterdayUserStat.fansCount || 0;
|
|
|
|
|
- accountYesterdayFansIncrease = Math.max(0, accountFansCount - yesterdayFans);
|
|
|
|
|
- logger.debug(`[WorkDayStatistics] Account ${account.id} (no works) - yesterday: ${yesterdayFans}, today: ${accountFansCount}, increase: ${accountYesterdayFansIncrease}`);
|
|
|
|
|
- } else {
|
|
|
|
|
- // 昨天没有数据,查找最近一天(早于今天)的数据作为基准
|
|
|
|
|
- const recentUserStat = await this.userDayStatisticsRepository
|
|
|
|
|
- .createQueryBuilder('uds')
|
|
|
|
|
- .where('uds.account_id = :accountId', { accountId: account.id })
|
|
|
|
|
- .andWhere('DATE(uds.record_date) < :today', { today: todayStr })
|
|
|
|
|
- .orderBy('uds.record_date', 'DESC')
|
|
|
|
|
- .getOne();
|
|
|
|
|
-
|
|
|
|
|
- if (recentUserStat) {
|
|
|
|
|
- const recentFans = recentUserStat.fansCount || 0;
|
|
|
|
|
- accountYesterdayFansIncrease = Math.max(0, accountFansCount - recentFans);
|
|
|
|
|
- logger.debug(`[WorkDayStatistics] Account ${account.id} (no works) - using recent: ${recentFans}, today: ${accountFansCount}, increase: ${accountYesterdayFansIncrease}`);
|
|
|
|
|
- } else {
|
|
|
|
|
- // 完全没有历史数据,涨粉为 0
|
|
|
|
|
- accountYesterdayFansIncrease = 0;
|
|
|
|
|
- logger.debug(`[WorkDayStatistics] Account ${account.id} (no works) - no history data, increase: 0`);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- accountList.push({
|
|
|
|
|
- id: account.id,
|
|
|
|
|
- nickname: account.accountName || '',
|
|
|
|
|
- username: account.accountId || '',
|
|
|
|
|
- avatarUrl: account.avatarUrl,
|
|
|
|
|
- platform: account.platform,
|
|
|
|
|
- groupId: account.groupId,
|
|
|
|
|
- groupName: account.group?.name ?? null,
|
|
|
|
|
- fansCount: accountFansCount,
|
|
|
|
|
- totalIncome: null,
|
|
|
|
|
- yesterdayIncome: null,
|
|
|
|
|
- totalViews: null,
|
|
|
|
|
- yesterdayViews: null,
|
|
|
|
|
- yesterdayComments: 0,
|
|
|
|
|
- yesterdayLikes: 0,
|
|
|
|
|
- yesterdayFansIncrease: accountYesterdayFansIncrease,
|
|
|
|
|
- updateTime: account.updatedAt.toISOString(),
|
|
|
|
|
- status: account.status,
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // 即使没有作品,也要累加账号的粉丝数到总粉丝数
|
|
|
|
|
- totalAccounts++;
|
|
|
|
|
- totalFans += accountFansCount;
|
|
|
|
|
- yesterdayFansIncrease += accountYesterdayFansIncrease;
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const workIds = works.map(w => w.id);
|
|
|
|
|
-
|
|
|
|
|
- // 获取每个作品的最新日期统计数据(总播放量等,不再包含粉丝数)
|
|
|
|
|
- const latestStatsQuery = this.statisticsRepository
|
|
|
|
|
- .createQueryBuilder('wds')
|
|
|
|
|
- .select('wds.work_id', 'workId')
|
|
|
|
|
- .addSelect('MAX(wds.record_date)', 'latestDate')
|
|
|
|
|
- .addSelect('MAX(wds.play_count)', 'playCount')
|
|
|
|
|
- .addSelect('MAX(wds.like_count)', 'likeCount')
|
|
|
|
|
- .addSelect('MAX(wds.comment_count)', 'commentCount')
|
|
|
|
|
- .where('wds.work_id IN (:...workIds)', { workIds })
|
|
|
|
|
- .groupBy('wds.work_id');
|
|
|
|
|
-
|
|
|
|
|
- const latestStats = await latestStatsQuery.getRawMany();
|
|
|
|
|
-
|
|
|
|
|
- // 计算总播放量(所有作品最新日期的play_count总和)
|
|
|
|
|
- let accountTotalViews = 0;
|
|
|
|
|
- const latestDateMap = new Map<number, string>();
|
|
|
|
|
- for (const stat of latestStats) {
|
|
|
|
|
- accountTotalViews += parseInt(stat.playCount) || 0;
|
|
|
|
|
- latestDateMap.set(stat.workId, stat.latestDate);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 获取昨天和今天的数据来计算增量(不再包含粉丝数,粉丝数从 user_day_statistics 表获取)
|
|
|
|
|
- const yesterdayStatsQuery = this.statisticsRepository
|
|
|
|
|
- .createQueryBuilder('wds')
|
|
|
|
|
- .select('wds.work_id', 'workId')
|
|
|
|
|
- .addSelect('SUM(wds.play_count)', 'playCount')
|
|
|
|
|
- .addSelect('SUM(wds.like_count)', 'likeCount')
|
|
|
|
|
- .addSelect('SUM(wds.comment_count)', 'commentCount')
|
|
|
|
|
- .where('wds.work_id IN (:...workIds)', { workIds })
|
|
|
|
|
- .andWhere('wds.record_date = :yesterday', { yesterday: yesterdayStr })
|
|
|
|
|
- .groupBy('wds.work_id');
|
|
|
|
|
-
|
|
|
|
|
- const todayStatsQuery = this.statisticsRepository
|
|
|
|
|
- .createQueryBuilder('wds')
|
|
|
|
|
- .select('wds.work_id', 'workId')
|
|
|
|
|
- .addSelect('SUM(wds.play_count)', 'playCount')
|
|
|
|
|
- .addSelect('SUM(wds.like_count)', 'likeCount')
|
|
|
|
|
- .addSelect('SUM(wds.comment_count)', 'commentCount')
|
|
|
|
|
- .where('wds.work_id IN (:...workIds)', { workIds })
|
|
|
|
|
- .andWhere('wds.record_date = :today', { today: todayStr })
|
|
|
|
|
- .groupBy('wds.work_id');
|
|
|
|
|
-
|
|
|
|
|
- const [yesterdayStats, todayStats] = await Promise.all([
|
|
|
|
|
- yesterdayStatsQuery.getRawMany(),
|
|
|
|
|
- todayStatsQuery.getRawMany(),
|
|
|
|
|
- ]);
|
|
|
|
|
-
|
|
|
|
|
- logger.info(`[WorkDayStatistics] Account ${account.id} (${account.accountName}) - workIds: ${workIds.length}, yesterdayStats: ${yesterdayStats.length}, todayStats: ${todayStats.length}`);
|
|
|
|
|
-
|
|
|
|
|
- if (yesterdayStats.length > 0 || todayStats.length > 0) {
|
|
|
|
|
- logger.debug(`[WorkDayStatistics] yesterdayStats:`, JSON.stringify(yesterdayStats.slice(0, 3)));
|
|
|
|
|
- logger.debug(`[WorkDayStatistics] todayStats:`, JSON.stringify(todayStats.slice(0, 3)));
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const accountFansCount = account.fansCount || 0;
|
|
|
|
|
+ const accountYesterdayViews = yesterdayUds?.playCount ?? 0;
|
|
|
|
|
+ const accountYesterdayComments = yesterdayUds?.commentCount ?? 0;
|
|
|
|
|
+ const accountYesterdayLikes = yesterdayUds?.likeCount ?? 0;
|
|
|
|
|
+ const accountYesterdayFansIncrease = yesterdayUds?.fansIncrease ?? 0;
|
|
|
|
|
|
|
|
- // 计算昨日增量
|
|
|
|
|
- let accountYesterdayViews = 0;
|
|
|
|
|
- let accountYesterdayComments = 0;
|
|
|
|
|
- let accountYesterdayLikes = 0;
|
|
|
|
|
- let accountYesterdayFansIncrease = 0;
|
|
|
|
|
-
|
|
|
|
|
- // 按作品ID汇总(不再包含粉丝数)
|
|
|
|
|
- const yesterdayMap = new Map<number, { play: number; like: number; comment: number }>();
|
|
|
|
|
- const todayMap = new Map<number, { play: number; like: number; comment: number }>();
|
|
|
|
|
-
|
|
|
|
|
- for (const stat of yesterdayStats) {
|
|
|
|
|
- const workId = parseInt(String(stat.workId)) || 0;
|
|
|
|
|
- yesterdayMap.set(workId, {
|
|
|
|
|
- play: Number(stat.playCount) || 0,
|
|
|
|
|
- like: Number(stat.likeCount) || 0,
|
|
|
|
|
- comment: Number(stat.commentCount) || 0,
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- for (const stat of todayStats) {
|
|
|
|
|
- const workId = parseInt(String(stat.workId)) || 0;
|
|
|
|
|
- todayMap.set(workId, {
|
|
|
|
|
- play: Number(stat.playCount) || 0,
|
|
|
|
|
- like: Number(stat.likeCount) || 0,
|
|
|
|
|
- comment: Number(stat.commentCount) || 0,
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- logger.debug(`[WorkDayStatistics] Account ${account.id} - yesterdayMap size: ${yesterdayMap.size}, todayMap size: ${todayMap.size}`);
|
|
|
|
|
-
|
|
|
|
|
- // 计算增量(今天 - 昨天,不再包含粉丝数)
|
|
|
|
|
- for (const workId of workIds) {
|
|
|
|
|
- const todayData = todayMap.get(workId) || { play: 0, like: 0, comment: 0 };
|
|
|
|
|
- const yesterdayData = yesterdayMap.get(workId) || { play: 0, like: 0, comment: 0 };
|
|
|
|
|
-
|
|
|
|
|
- const viewsDiff = todayData.play - yesterdayData.play;
|
|
|
|
|
- const commentsDiff = todayData.comment - yesterdayData.comment;
|
|
|
|
|
- const likesDiff = todayData.like - yesterdayData.like;
|
|
|
|
|
-
|
|
|
|
|
- accountYesterdayViews += Math.max(0, viewsDiff);
|
|
|
|
|
- accountYesterdayComments += Math.max(0, commentsDiff);
|
|
|
|
|
- accountYesterdayLikes += Math.max(0, likesDiff);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- logger.info(`[WorkDayStatistics] Account ${account.id} - Calculated: views=${accountYesterdayViews}, comments=${accountYesterdayComments}, likes=${accountYesterdayLikes}`);
|
|
|
|
|
-
|
|
|
|
|
- // 从 user_day_statistics 表获取账号的最新粉丝数和作品数
|
|
|
|
|
- // 优先使用统计数据,如果没有则使用账号表的当前值
|
|
|
|
|
-
|
|
|
|
|
- // 使用日期字符串直接查询(更可靠,避免时区问题)
|
|
|
|
|
- // todayStr 和 yesterdayStr 是中国时区的日期字符串(如 "2026-01-26")
|
|
|
|
|
- // 直接使用字符串查询,TypeORM 会自动转换为 DATE 类型进行比较
|
|
|
|
|
- const todayUserStat = await this.userDayStatisticsRepository
|
|
|
|
|
- .createQueryBuilder('uds')
|
|
|
|
|
- .where('uds.account_id = :accountId', { accountId: account.id })
|
|
|
|
|
- .andWhere('DATE(uds.record_date) = :today', { today: todayStr })
|
|
|
|
|
- .getOne();
|
|
|
|
|
-
|
|
|
|
|
- const yesterdayUserStat = await this.userDayStatisticsRepository
|
|
|
|
|
- .createQueryBuilder('uds')
|
|
|
|
|
- .where('uds.account_id = :accountId', { accountId: account.id })
|
|
|
|
|
- .andWhere('DATE(uds.record_date) = :yesterday', { yesterday: yesterdayStr })
|
|
|
|
|
- .getOne();
|
|
|
|
|
-
|
|
|
|
|
- // 获取今天的粉丝数:优先使用统计数据,如果没有则使用账号表的当前值
|
|
|
|
|
- let accountFansCount: number;
|
|
|
|
|
- let accountWorksCount: number;
|
|
|
|
|
- if (todayUserStat) {
|
|
|
|
|
- accountFansCount = todayUserStat.fansCount || 0;
|
|
|
|
|
- accountWorksCount = todayUserStat.worksCount || 0;
|
|
|
|
|
- } else {
|
|
|
|
|
- // 今天没有统计数据,使用账号表的当前值
|
|
|
|
|
- accountFansCount = account.fansCount || 0;
|
|
|
|
|
- accountWorksCount = account.worksCount || 0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 计算昨日涨粉(今天粉丝数 - 昨天粉丝数)
|
|
|
|
|
- let yesterdayFans: number;
|
|
|
|
|
- if (yesterdayUserStat) {
|
|
|
|
|
- // 昨天有数据,直接使用
|
|
|
|
|
- yesterdayFans = yesterdayUserStat.fansCount || 0;
|
|
|
|
|
- logger.debug(`[WorkDayStatistics] Account ${account.id} - yesterday has data: ${yesterdayFans}, today: ${accountFansCount}`);
|
|
|
|
|
- } else {
|
|
|
|
|
- // 昨天没有数据,查找最近一天(早于今天)的数据作为基准
|
|
|
|
|
- const recentUserStat = await this.userDayStatisticsRepository
|
|
|
|
|
- .createQueryBuilder('uds')
|
|
|
|
|
- .where('uds.account_id = :accountId', { accountId: account.id })
|
|
|
|
|
- .andWhere('DATE(uds.record_date) < :today', { today: todayStr })
|
|
|
|
|
- .orderBy('uds.record_date', 'DESC')
|
|
|
|
|
- .getOne();
|
|
|
|
|
-
|
|
|
|
|
- if (recentUserStat) {
|
|
|
|
|
- yesterdayFans = recentUserStat.fansCount || 0;
|
|
|
|
|
- logger.debug(`[WorkDayStatistics] Account ${account.id} - using recent data: ${yesterdayFans}, today: ${accountFansCount}`);
|
|
|
|
|
- } else {
|
|
|
|
|
- // 完全没有历史数据,用账号表的当前粉丝数作为基准(涨粉为 0)
|
|
|
|
|
- yesterdayFans = accountFansCount;
|
|
|
|
|
- logger.debug(`[WorkDayStatistics] Account ${account.id} - no history data, using current: ${accountFansCount}`);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 计算涨粉数(今天 - 昨天),确保不为负数
|
|
|
|
|
- accountYesterdayFansIncrease = Math.max(0, accountFansCount - yesterdayFans);
|
|
|
|
|
- logger.info(`[WorkDayStatistics] Account ${account.id} - fans increase: ${accountFansCount} - ${yesterdayFans} = ${accountYesterdayFansIncrease}`);
|
|
|
|
|
|
|
+ const updateTime = (yesterdayUds?.updatedAt ?? account.updatedAt).toISOString();
|
|
|
|
|
|
|
|
accountList.push({
|
|
accountList.push({
|
|
|
id: account.id,
|
|
id: account.id,
|
|
@@ -862,22 +667,20 @@ export class WorkDayStatisticsService {
|
|
|
groupId: account.groupId,
|
|
groupId: account.groupId,
|
|
|
groupName: account.group?.name ?? null,
|
|
groupName: account.group?.name ?? null,
|
|
|
fansCount: accountFansCount,
|
|
fansCount: accountFansCount,
|
|
|
- totalIncome: null, // 收益数据需要从其他表获取,暂时为null
|
|
|
|
|
|
|
+ totalIncome: null,
|
|
|
yesterdayIncome: null,
|
|
yesterdayIncome: null,
|
|
|
- totalViews: accountTotalViews > 0 ? accountTotalViews : null,
|
|
|
|
|
- yesterdayViews: accountYesterdayViews > 0 ? accountYesterdayViews : null,
|
|
|
|
|
|
|
+ totalViews: accountTotalViews,
|
|
|
|
|
+ yesterdayViews: accountYesterdayViews,
|
|
|
yesterdayComments: accountYesterdayComments,
|
|
yesterdayComments: accountYesterdayComments,
|
|
|
yesterdayLikes: accountYesterdayLikes,
|
|
yesterdayLikes: accountYesterdayLikes,
|
|
|
yesterdayFansIncrease: accountYesterdayFansIncrease,
|
|
yesterdayFansIncrease: accountYesterdayFansIncrease,
|
|
|
- updateTime: account.updatedAt.toISOString(),
|
|
|
|
|
|
|
+ updateTime,
|
|
|
status: account.status,
|
|
status: account.status,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 累加汇总数据
|
|
|
|
|
- totalAccounts++;
|
|
|
|
|
totalViews += accountTotalViews;
|
|
totalViews += accountTotalViews;
|
|
|
- yesterdayViews += accountYesterdayViews;
|
|
|
|
|
totalFans += accountFansCount;
|
|
totalFans += accountFansCount;
|
|
|
|
|
+ yesterdayViews += accountYesterdayViews;
|
|
|
yesterdayComments += accountYesterdayComments;
|
|
yesterdayComments += accountYesterdayComments;
|
|
|
yesterdayLikes += accountYesterdayLikes;
|
|
yesterdayLikes += accountYesterdayLikes;
|
|
|
yesterdayFansIncrease += accountYesterdayFansIncrease;
|
|
yesterdayFansIncrease += accountYesterdayFansIncrease;
|
|
@@ -973,22 +776,162 @@ export class WorkDayStatisticsService {
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 计算汇总统计
|
|
|
|
|
- let totalAccounts = 0;
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 口径变更:user_day_statistics 的各项数据为“每日单独值”,不再是累计值
|
|
|
|
|
+ * 因此平台详情:
|
|
|
|
|
+ * - 区间汇总:直接 SUM(user_day_statistics.*)(按账号、按天)
|
|
|
|
|
+ * - 每日汇总:按 record_date 分组 SUM
|
|
|
|
|
+ */
|
|
|
|
|
+ const accountIds = accounts.map(a => a.id);
|
|
|
|
|
+ const totalAccounts = accounts.length;
|
|
|
|
|
+
|
|
|
|
|
+ const [dailyRows, perAccountRows] = await Promise.all([
|
|
|
|
|
+ // 按日期维度汇总(每天所有账号数据之和)
|
|
|
|
|
+ // 这里直接使用原生 SQL,以保证和文档/手动验证时看到的 SQL 完全一致:
|
|
|
|
|
+ //
|
|
|
|
|
+ // SELECT
|
|
|
|
|
+ // uds.record_date AS recordDate,
|
|
|
|
|
+ // COALESCE(SUM(uds.play_count), 0) AS viewsCount,
|
|
|
|
|
+ // COALESCE(SUM(uds.comment_count), 0) AS commentsCount,
|
|
|
|
|
+ // COALESCE(SUM(uds.like_count), 0) AS likesCount,
|
|
|
|
|
+ // COALESCE(SUM(uds.fans_increase), 0) AS fansIncrease
|
|
|
|
|
+ // FROM user_day_statistics uds
|
|
|
|
|
+ // WHERE uds.account_id IN (...)
|
|
|
|
|
+ // AND uds.record_date >= ?
|
|
|
|
|
+ // AND uds.record_date <= ?
|
|
|
|
|
+ // GROUP BY uds.record_date
|
|
|
|
|
+ // ORDER BY uds.record_date ASC;
|
|
|
|
|
+ (async () => {
|
|
|
|
|
+ if (!accountIds.length) return [];
|
|
|
|
|
+
|
|
|
|
|
+ const inPlaceholders = accountIds.map(() => '?').join(',');
|
|
|
|
|
+ const sql = `
|
|
|
|
|
+ SELECT
|
|
|
|
|
+ uds.record_date AS recordDate,
|
|
|
|
|
+ COALESCE(SUM(uds.play_count), 0) AS viewsCount,
|
|
|
|
|
+ COALESCE(SUM(uds.comment_count), 0) AS commentsCount,
|
|
|
|
|
+ COALESCE(SUM(uds.like_count), 0) AS likesCount,
|
|
|
|
|
+ COALESCE(SUM(uds.fans_increase), 0) AS fansIncrease
|
|
|
|
|
+ FROM user_day_statistics uds
|
|
|
|
|
+ WHERE uds.account_id IN (${inPlaceholders})
|
|
|
|
|
+ AND uds.record_date >= ?
|
|
|
|
|
+ AND uds.record_date <= ?
|
|
|
|
|
+ GROUP BY uds.record_date
|
|
|
|
|
+ ORDER BY uds.record_date ASC
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ const params = [...accountIds, startDateStr, endDateStr];
|
|
|
|
|
+ return await AppDataSource.query(sql, params);
|
|
|
|
|
+ })(),
|
|
|
|
|
+ // 按账号维度汇总(区间内所有天的和)
|
|
|
|
|
+ this.userDayStatisticsRepository
|
|
|
|
|
+ .createQueryBuilder('uds')
|
|
|
|
|
+ .select('uds.account_id', 'accountId')
|
|
|
|
|
+ .addSelect('COUNT(1)', 'rowCount')
|
|
|
|
|
+ .addSelect('COALESCE(SUM(uds.play_count), 0)', 'viewsCount')
|
|
|
|
|
+ .addSelect('COALESCE(SUM(uds.comment_count), 0)', 'commentsCount')
|
|
|
|
|
+ .addSelect('COALESCE(SUM(uds.like_count), 0)', 'likesCount')
|
|
|
|
|
+ .addSelect('COALESCE(SUM(uds.fans_increase), 0)', 'fansIncrease')
|
|
|
|
|
+ .addSelect('MAX(uds.updated_at)', 'latestUpdateTime')
|
|
|
|
|
+ .where('uds.account_id IN (:...accountIds)', { accountIds })
|
|
|
|
|
+ .andWhere('DATE(uds.record_date) >= :startDate', { startDate: startDateStr })
|
|
|
|
|
+ .andWhere('DATE(uds.record_date) <= :endDate', { endDate: endDateStr })
|
|
|
|
|
+ .groupBy('uds.account_id')
|
|
|
|
|
+ .getRawMany(),
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 按日期汇总:每日汇总数据 =====
|
|
|
|
|
+ const dailyMap = new Map<string, { views: number; comments: number; likes: number; fansIncrease: number }>();
|
|
|
|
|
+ for (const row of dailyRows || []) {
|
|
|
|
|
+ if (!row.recordDate) continue;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 注意:record_date 在实体里是 DATE 类型,TypeORM 读出来通常是 Date 对象。
|
|
|
|
|
+ * 之前用 String(row.recordDate).slice(0, 10) 会得到类似 "Wed Jan 28" 这样的字符串前 10 位,
|
|
|
|
|
+ * 导致 key 和下面 this.formatDate(cursor) 生成的 "YYYY-MM-DD" 不一致,从而 dailyMap 命中失败,全部变成 0。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 这里改成显式按本地时间拼出 "YYYY-MM-DD",确保与 startDate/endDate 的格式一致。
|
|
|
|
|
+ */
|
|
|
|
|
+ let dateKey: string;
|
|
|
|
|
+ if (row.recordDate instanceof Date) {
|
|
|
|
|
+ const y = row.recordDate.getFullYear();
|
|
|
|
|
+ const m = String(row.recordDate.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
+ const d = String(row.recordDate.getDate()).padStart(2, '0');
|
|
|
|
|
+ dateKey = `${y}-${m}-${d}`;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 数据库如果已经返回字符串,例如 "2026-01-28",直接截前 10 位即可
|
|
|
|
|
+ dateKey = String(row.recordDate).slice(0, 10);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const prev = dailyMap.get(dateKey) ?? { views: 0, comments: 0, likes: 0, fansIncrease: 0 };
|
|
|
|
|
+
|
|
|
|
|
+ dailyMap.set(dateKey, {
|
|
|
|
|
+ views: prev.views + (Number(row.viewsCount) || 0),
|
|
|
|
|
+ comments: prev.comments + (Number(row.commentsCount) || 0),
|
|
|
|
|
+ likes: prev.likes + (Number(row.likesCount) || 0),
|
|
|
|
|
+ fansIncrease: prev.fansIncrease + (Number(row.fansIncrease) || 0),
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 补齐日期区间(没有数据也返回 0)
|
|
|
|
|
+ const dailyData: Array<{
|
|
|
|
|
+ date: string;
|
|
|
|
|
+ income: number;
|
|
|
|
|
+ recommendationCount: number | null;
|
|
|
|
|
+ viewsCount: number;
|
|
|
|
|
+ commentsCount: number;
|
|
|
|
|
+ likesCount: number;
|
|
|
|
|
+ fansIncrease: number;
|
|
|
|
|
+ }> = [];
|
|
|
|
|
+
|
|
|
|
|
+ const dStart = new Date(startDateStr);
|
|
|
|
|
+ const dEnd = new Date(endDateStr);
|
|
|
|
|
+ const cursor = new Date(dStart);
|
|
|
|
|
+ while (cursor <= dEnd) {
|
|
|
|
|
+ const dateKey = this.formatDate(cursor);
|
|
|
|
|
+ const v = dailyMap.get(dateKey) ?? { views: 0, comments: 0, likes: 0, fansIncrease: 0 };
|
|
|
|
|
+ dailyData.push({
|
|
|
|
|
+ date: dateKey,
|
|
|
|
|
+ income: 0,
|
|
|
|
|
+ recommendationCount: null,
|
|
|
|
|
+ viewsCount: v.views,
|
|
|
|
|
+ commentsCount: v.comments,
|
|
|
|
|
+ likesCount: v.likes,
|
|
|
|
|
+ fansIncrease: v.fansIncrease,
|
|
|
|
|
+ });
|
|
|
|
|
+ cursor.setDate(cursor.getDate() + 1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 按账号汇总:账号列表 & 顶部汇总 =====
|
|
|
|
|
+ const perAccountMap = new Map<
|
|
|
|
|
+ number,
|
|
|
|
|
+ { rowCount: number; views: number; comments: number; likes: number; fansIncrease: number; latestUpdateTime: Date | null }
|
|
|
|
|
+ >();
|
|
|
|
|
+ for (const row of perAccountRows || []) {
|
|
|
|
|
+ const accountId = Number(row.accountId) || 0;
|
|
|
|
|
+ if (!accountId) continue;
|
|
|
|
|
+ perAccountMap.set(accountId, {
|
|
|
|
|
+ rowCount: Number(row.rowCount) || 0,
|
|
|
|
|
+ views: Number(row.viewsCount) || 0,
|
|
|
|
|
+ comments: Number(row.commentsCount) || 0,
|
|
|
|
|
+ likes: Number(row.likesCount) || 0,
|
|
|
|
|
+ fansIncrease: Number(row.fansIncrease) || 0,
|
|
|
|
|
+ latestUpdateTime: row.latestUpdateTime ? new Date(row.latestUpdateTime) : null,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 顶部汇总:直接用账号维度汇总,确保和“账号详细数据”一致
|
|
|
let totalViews = 0;
|
|
let totalViews = 0;
|
|
|
let totalComments = 0;
|
|
let totalComments = 0;
|
|
|
let totalLikes = 0;
|
|
let totalLikes = 0;
|
|
|
let totalFansIncrease = 0;
|
|
let totalFansIncrease = 0;
|
|
|
|
|
+ for (const agg of perAccountMap.values()) {
|
|
|
|
|
+ totalViews += agg.views;
|
|
|
|
|
+ totalComments += agg.comments;
|
|
|
|
|
+ totalLikes += agg.likes;
|
|
|
|
|
+ totalFansIncrease += agg.fansIncrease;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 按日期汇总数据
|
|
|
|
|
- const dailyMap = new Map<string, {
|
|
|
|
|
- views: number;
|
|
|
|
|
- comments: number;
|
|
|
|
|
- likes: number;
|
|
|
|
|
- fansIncrease: number;
|
|
|
|
|
- }>();
|
|
|
|
|
-
|
|
|
|
|
- // 账号详细列表
|
|
|
|
|
const accountList: Array<{
|
|
const accountList: Array<{
|
|
|
id: number;
|
|
id: number;
|
|
|
nickname: string;
|
|
nickname: string;
|
|
@@ -1002,140 +945,26 @@ export class WorkDayStatisticsService {
|
|
|
likesCount: number;
|
|
likesCount: number;
|
|
|
fansIncrease: number;
|
|
fansIncrease: number;
|
|
|
updateTime: string;
|
|
updateTime: string;
|
|
|
- }> = [];
|
|
|
|
|
-
|
|
|
|
|
- for (const account of accounts) {
|
|
|
|
|
- const works = await this.workRepository.find({
|
|
|
|
|
- where: { accountId: account.id },
|
|
|
|
|
- select: ['id'],
|
|
|
|
|
- });
|
|
|
|
|
- const workIds = works.map(w => w.id);
|
|
|
|
|
-
|
|
|
|
|
- // 获取该账号在日期范围内的每日数据
|
|
|
|
|
- const dateStart = new Date(startDateStr);
|
|
|
|
|
- const dateEnd = new Date(endDateStr);
|
|
|
|
|
- const currentDate = new Date(dateStart);
|
|
|
|
|
-
|
|
|
|
|
- while (currentDate <= dateEnd) {
|
|
|
|
|
- const dateStr = this.formatDate(currentDate);
|
|
|
|
|
-
|
|
|
|
|
- // 获取该日期的数据
|
|
|
|
|
- const [daySums, prevDaySums] = await Promise.all([
|
|
|
|
|
- this.getWorkSumsAtDate(workIds, dateStr),
|
|
|
|
|
- this.getWorkSumsAtDate(workIds, this.formatDate(new Date(currentDate.getTime() - 24 * 60 * 60 * 1000))),
|
|
|
|
|
- ]);
|
|
|
|
|
-
|
|
|
|
|
- const dayViews = daySums.views - prevDaySums.views;
|
|
|
|
|
- const dayComments = daySums.comments - prevDaySums.comments;
|
|
|
|
|
- const dayLikes = daySums.likes - prevDaySums.likes;
|
|
|
|
|
-
|
|
|
|
|
- // 获取粉丝数据
|
|
|
|
|
- const dayUserStat = await this.userDayStatisticsRepository
|
|
|
|
|
- .createQueryBuilder('uds')
|
|
|
|
|
- .where('uds.account_id = :accountId', { accountId: account.id })
|
|
|
|
|
- .andWhere('DATE(uds.record_date) = :d', { d: dateStr })
|
|
|
|
|
- .getOne();
|
|
|
|
|
-
|
|
|
|
|
- const prevDayUserStat = await this.userDayStatisticsRepository
|
|
|
|
|
- .createQueryBuilder('uds')
|
|
|
|
|
- .where('uds.account_id = :accountId', { accountId: account.id })
|
|
|
|
|
- .andWhere('DATE(uds.record_date) = :d', { d: this.formatDate(new Date(currentDate.getTime() - 24 * 60 * 60 * 1000)) })
|
|
|
|
|
- .getOne();
|
|
|
|
|
-
|
|
|
|
|
- const dayFans = (dayUserStat?.fansCount || 0) - (prevDayUserStat?.fansCount || 0);
|
|
|
|
|
-
|
|
|
|
|
- if (!dailyMap.has(dateStr)) {
|
|
|
|
|
- dailyMap.set(dateStr, {
|
|
|
|
|
- views: 0,
|
|
|
|
|
- comments: 0,
|
|
|
|
|
- likes: 0,
|
|
|
|
|
- fansIncrease: 0,
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const daily = dailyMap.get(dateStr)!;
|
|
|
|
|
- daily.views += Math.max(0, dayViews);
|
|
|
|
|
- daily.comments += Math.max(0, dayComments);
|
|
|
|
|
- daily.likes += Math.max(0, dayLikes);
|
|
|
|
|
- daily.fansIncrease += Math.max(0, dayFans);
|
|
|
|
|
-
|
|
|
|
|
- currentDate.setDate(currentDate.getDate() + 1);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 计算账号的总数据(使用 endDate 的数据)
|
|
|
|
|
- const [endSums, startSums] = await Promise.all([
|
|
|
|
|
- this.getWorkSumsAtDate(workIds, endDateStr),
|
|
|
|
|
- this.getWorkSumsAtDate(workIds, startDateStr),
|
|
|
|
|
- ]);
|
|
|
|
|
-
|
|
|
|
|
- const accountViews = endSums.views - startSums.views;
|
|
|
|
|
- const accountComments = endSums.comments - startSums.comments;
|
|
|
|
|
- const accountLikes = endSums.likes - startSums.likes;
|
|
|
|
|
-
|
|
|
|
|
- // 获取粉丝数据
|
|
|
|
|
- const endUserStat = 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 startUserStat = await this.userDayStatisticsRepository
|
|
|
|
|
- .createQueryBuilder('uds')
|
|
|
|
|
- .where('uds.account_id = :accountId', { accountId: account.id })
|
|
|
|
|
- .andWhere('DATE(uds.record_date) <= :d', { d: startDateStr })
|
|
|
|
|
- .orderBy('uds.record_date', 'DESC')
|
|
|
|
|
- .getOne();
|
|
|
|
|
-
|
|
|
|
|
- const accountFansIncrease = (endUserStat?.fansCount || 0) - (startUserStat?.fansCount || 0);
|
|
|
|
|
-
|
|
|
|
|
- // 获取更新时间
|
|
|
|
|
- const latestUserStat = await this.userDayStatisticsRepository
|
|
|
|
|
- .createQueryBuilder('uds')
|
|
|
|
|
- .where('uds.account_id = :accountId', { accountId: account.id })
|
|
|
|
|
- .andWhere('DATE(uds.record_date) >= :startDate', { startDate: startDateStr })
|
|
|
|
|
- .andWhere('DATE(uds.record_date) <= :endDate', { endDate: endDateStr })
|
|
|
|
|
- .orderBy('uds.updated_at', 'DESC')
|
|
|
|
|
- .getOne();
|
|
|
|
|
-
|
|
|
|
|
- const updateTime = latestUserStat?.updatedAt
|
|
|
|
|
- ? this.formatUpdateTime(latestUserStat.updatedAt)
|
|
|
|
|
- : '';
|
|
|
|
|
-
|
|
|
|
|
- accountList.push({
|
|
|
|
|
|
|
+ }> = accounts.map((account) => {
|
|
|
|
|
+ const agg =
|
|
|
|
|
+ perAccountMap.get(account.id) ?? { rowCount: 0, views: 0, comments: 0, likes: 0, fansIncrease: 0, latestUpdateTime: null };
|
|
|
|
|
+ const updateTime = agg.latestUpdateTime ? this.formatUpdateTime(agg.latestUpdateTime) : '';
|
|
|
|
|
+ return {
|
|
|
id: account.id,
|
|
id: account.id,
|
|
|
nickname: account.accountName || '',
|
|
nickname: account.accountName || '',
|
|
|
username: account.accountId || '',
|
|
username: account.accountId || '',
|
|
|
avatarUrl: account.avatarUrl,
|
|
avatarUrl: account.avatarUrl,
|
|
|
platform: account.platform,
|
|
platform: account.platform,
|
|
|
- income: null, // 收益数据需要从其他表获取
|
|
|
|
|
- recommendationCount: null, // 推荐量(部分平台支持)
|
|
|
|
|
- viewsCount: accountViews > 0 ? accountViews : null,
|
|
|
|
|
- commentsCount: Math.max(0, accountComments),
|
|
|
|
|
- likesCount: Math.max(0, accountLikes),
|
|
|
|
|
- fansIncrease: Math.max(0, accountFansIncrease),
|
|
|
|
|
|
|
+ income: null,
|
|
|
|
|
+ recommendationCount: null,
|
|
|
|
|
+ // 没有任何记录时,前端展示“获取失败”,避免把“无数据”误显示成 0
|
|
|
|
|
+ viewsCount: agg.rowCount > 0 ? agg.views : null,
|
|
|
|
|
+ commentsCount: agg.comments,
|
|
|
|
|
+ likesCount: agg.likes,
|
|
|
|
|
+ fansIncrease: agg.fansIncrease,
|
|
|
updateTime,
|
|
updateTime,
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- totalAccounts++;
|
|
|
|
|
- totalViews += Math.max(0, accountViews);
|
|
|
|
|
- totalComments += Math.max(0, accountComments);
|
|
|
|
|
- totalLikes += Math.max(0, accountLikes);
|
|
|
|
|
- totalFansIncrease += Math.max(0, accountFansIncrease);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 转换为每日数据数组
|
|
|
|
|
- const dailyData = Array.from(dailyMap.entries())
|
|
|
|
|
- .map(([date, data]) => ({
|
|
|
|
|
- date,
|
|
|
|
|
- income: 0, // 收益数据需要从其他表获取
|
|
|
|
|
- recommendationCount: null, // 推荐量(部分平台支持)
|
|
|
|
|
- viewsCount: data.views,
|
|
|
|
|
- commentsCount: data.comments,
|
|
|
|
|
- likesCount: data.likes,
|
|
|
|
|
- fansIncrease: data.fansIncrease,
|
|
|
|
|
- }))
|
|
|
|
|
- .sort((a, b) => a.date.localeCompare(b.date));
|
|
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
summary: {
|
|
summary: {
|