|
@@ -1,9 +1,9 @@
|
|
|
-import { AppDataSource, WorkDayStatistics, Work, PlatformAccount } from '../models/index.js';
|
|
|
|
|
|
|
+import { AppDataSource, WorkDayStatistics, Work, PlatformAccount, UserDayStatistics } from '../models/index.js';
|
|
|
import { Between, In } from 'typeorm';
|
|
import { Between, In } from 'typeorm';
|
|
|
|
|
+import { logger } from '../utils/logger.js';
|
|
|
|
|
|
|
|
interface StatisticsItem {
|
|
interface StatisticsItem {
|
|
|
workId: number;
|
|
workId: number;
|
|
|
- fansCount?: number;
|
|
|
|
|
playCount?: number;
|
|
playCount?: number;
|
|
|
likeCount?: number;
|
|
likeCount?: number;
|
|
|
commentCount?: number;
|
|
commentCount?: number;
|
|
@@ -38,7 +38,6 @@ interface PlatformStatItem {
|
|
|
|
|
|
|
|
interface WorkStatisticsItem {
|
|
interface WorkStatisticsItem {
|
|
|
recordDate: string;
|
|
recordDate: string;
|
|
|
- fansCount: number;
|
|
|
|
|
playCount: number;
|
|
playCount: number;
|
|
|
likeCount: number;
|
|
likeCount: number;
|
|
|
commentCount: number;
|
|
commentCount: number;
|
|
@@ -50,6 +49,7 @@ export class WorkDayStatisticsService {
|
|
|
private statisticsRepository = AppDataSource.getRepository(WorkDayStatistics);
|
|
private statisticsRepository = AppDataSource.getRepository(WorkDayStatistics);
|
|
|
private workRepository = AppDataSource.getRepository(Work);
|
|
private workRepository = AppDataSource.getRepository(Work);
|
|
|
private accountRepository = AppDataSource.getRepository(PlatformAccount);
|
|
private accountRepository = AppDataSource.getRepository(PlatformAccount);
|
|
|
|
|
+ private userDayStatisticsRepository = AppDataSource.getRepository(UserDayStatistics);
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 保存作品日统计数据
|
|
* 保存作品日统计数据
|
|
@@ -74,9 +74,8 @@ export class WorkDayStatisticsService {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
if (existing) {
|
|
if (existing) {
|
|
|
- // 更新已有记录
|
|
|
|
|
|
|
+ // 更新已有记录(不再包含粉丝数,粉丝数从 user_day_statistics 表获取)
|
|
|
await this.statisticsRepository.update(existing.id, {
|
|
await this.statisticsRepository.update(existing.id, {
|
|
|
- fansCount: stat.fansCount ?? existing.fansCount,
|
|
|
|
|
playCount: stat.playCount ?? existing.playCount,
|
|
playCount: stat.playCount ?? existing.playCount,
|
|
|
likeCount: stat.likeCount ?? existing.likeCount,
|
|
likeCount: stat.likeCount ?? existing.likeCount,
|
|
|
commentCount: stat.commentCount ?? existing.commentCount,
|
|
commentCount: stat.commentCount ?? existing.commentCount,
|
|
@@ -85,11 +84,10 @@ export class WorkDayStatisticsService {
|
|
|
});
|
|
});
|
|
|
updatedCount++;
|
|
updatedCount++;
|
|
|
} else {
|
|
} else {
|
|
|
- // 插入新记录
|
|
|
|
|
|
|
+ // 插入新记录(不再包含粉丝数,粉丝数从 user_day_statistics 表获取)
|
|
|
const newStat = this.statisticsRepository.create({
|
|
const newStat = this.statisticsRepository.create({
|
|
|
workId: stat.workId,
|
|
workId: stat.workId,
|
|
|
recordDate: today,
|
|
recordDate: today,
|
|
|
- fansCount: stat.fansCount ?? 0,
|
|
|
|
|
playCount: stat.playCount ?? 0,
|
|
playCount: stat.playCount ?? 0,
|
|
|
likeCount: stat.likeCount ?? 0,
|
|
likeCount: stat.likeCount ?? 0,
|
|
|
commentCount: stat.commentCount ?? 0,
|
|
commentCount: stat.commentCount ?? 0,
|
|
@@ -131,13 +129,12 @@ export class WorkDayStatisticsService {
|
|
|
dateStart.setDate(dateStart.getDate() - Math.min(days, 30) + 1);
|
|
dateStart.setDate(dateStart.getDate() - Math.min(days, 30) + 1);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 构建查询
|
|
|
|
|
|
|
+ // 构建查询(不再从 work_day_statistics 读取粉丝数,粉丝数从 user_day_statistics 表获取)
|
|
|
const queryBuilder = this.statisticsRepository
|
|
const queryBuilder = this.statisticsRepository
|
|
|
.createQueryBuilder('wds')
|
|
.createQueryBuilder('wds')
|
|
|
.innerJoin(Work, 'w', 'wds.work_id = w.id')
|
|
.innerJoin(Work, 'w', 'wds.work_id = w.id')
|
|
|
.select('wds.record_date', 'recordDate')
|
|
.select('wds.record_date', 'recordDate')
|
|
|
.addSelect('w.accountId', 'accountId')
|
|
.addSelect('w.accountId', 'accountId')
|
|
|
- .addSelect('MAX(wds.fans_count)', 'accountFans')
|
|
|
|
|
.addSelect('SUM(wds.play_count)', 'accountViews')
|
|
.addSelect('SUM(wds.play_count)', 'accountViews')
|
|
|
.addSelect('SUM(wds.like_count)', 'accountLikes')
|
|
.addSelect('SUM(wds.like_count)', 'accountLikes')
|
|
|
.addSelect('SUM(wds.comment_count)', 'accountComments')
|
|
.addSelect('SUM(wds.comment_count)', 'accountComments')
|
|
@@ -269,8 +266,7 @@ export class WorkDayStatisticsService {
|
|
|
const firstDayQuery = this.statisticsRepository
|
|
const firstDayQuery = this.statisticsRepository
|
|
|
.createQueryBuilder('wds')
|
|
.createQueryBuilder('wds')
|
|
|
.innerJoin(Work, 'w', 'wds.work_id = w.id')
|
|
.innerJoin(Work, 'w', 'wds.work_id = w.id')
|
|
|
- .select('MAX(wds.fans_count)', 'fans')
|
|
|
|
|
- .addSelect('SUM(wds.play_count)', 'views')
|
|
|
|
|
|
|
+ .select('SUM(wds.play_count)', 'views')
|
|
|
.addSelect('SUM(wds.like_count)', 'likes')
|
|
.addSelect('SUM(wds.like_count)', 'likes')
|
|
|
.addSelect('SUM(wds.comment_count)', 'comments')
|
|
.addSelect('SUM(wds.comment_count)', 'comments')
|
|
|
.addSelect('SUM(wds.collect_count)', 'collects')
|
|
.addSelect('SUM(wds.collect_count)', 'collects')
|
|
@@ -284,8 +280,7 @@ export class WorkDayStatisticsService {
|
|
|
const lastDayQuery = this.statisticsRepository
|
|
const lastDayQuery = this.statisticsRepository
|
|
|
.createQueryBuilder('wds')
|
|
.createQueryBuilder('wds')
|
|
|
.innerJoin(Work, 'w', 'wds.work_id = w.id')
|
|
.innerJoin(Work, 'w', 'wds.work_id = w.id')
|
|
|
- .select('MAX(wds.fans_count)', 'fans')
|
|
|
|
|
- .addSelect('SUM(wds.play_count)', 'views')
|
|
|
|
|
|
|
+ .select('SUM(wds.play_count)', 'views')
|
|
|
.addSelect('SUM(wds.like_count)', 'likes')
|
|
.addSelect('SUM(wds.like_count)', 'likes')
|
|
|
.addSelect('SUM(wds.comment_count)', 'comments')
|
|
.addSelect('SUM(wds.comment_count)', 'comments')
|
|
|
.addSelect('SUM(wds.collect_count)', 'collects')
|
|
.addSelect('SUM(wds.collect_count)', 'collects')
|
|
@@ -301,8 +296,26 @@ export class WorkDayStatisticsService {
|
|
|
lastDayQuery.getRawOne(),
|
|
lastDayQuery.getRawOne(),
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
- const currentFans = account.fansCount ?? 0;
|
|
|
|
|
- const earliestFans = parseInt(firstDay?.fans) || currentFans;
|
|
|
|
|
|
|
+ // 从 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;
|
|
const fansIncrease = currentFans - earliestFans;
|
|
|
|
|
|
|
|
const viewsIncrease = (parseInt(lastDay?.views) || 0) - (parseInt(firstDay?.views) || 0);
|
|
const viewsIncrease = (parseInt(lastDay?.views) || 0) - (parseInt(firstDay?.views) || 0);
|
|
@@ -343,7 +356,6 @@ export class WorkDayStatisticsService {
|
|
|
.createQueryBuilder('wds')
|
|
.createQueryBuilder('wds')
|
|
|
.select('wds.work_id', 'workId')
|
|
.select('wds.work_id', 'workId')
|
|
|
.addSelect('wds.record_date', 'recordDate')
|
|
.addSelect('wds.record_date', 'recordDate')
|
|
|
- .addSelect('wds.fans_count', 'fansCount')
|
|
|
|
|
.addSelect('wds.play_count', 'playCount')
|
|
.addSelect('wds.play_count', 'playCount')
|
|
|
.addSelect('wds.like_count', 'likeCount')
|
|
.addSelect('wds.like_count', 'likeCount')
|
|
|
.addSelect('wds.comment_count', 'commentCount')
|
|
.addSelect('wds.comment_count', 'commentCount')
|
|
@@ -377,7 +389,6 @@ export class WorkDayStatisticsService {
|
|
|
|
|
|
|
|
groupedData[workId].push({
|
|
groupedData[workId].push({
|
|
|
recordDate,
|
|
recordDate,
|
|
|
- fansCount: parseInt(row.fansCount) || 0,
|
|
|
|
|
playCount: parseInt(row.playCount) || 0,
|
|
playCount: parseInt(row.playCount) || 0,
|
|
|
likeCount: parseInt(row.likeCount) || 0,
|
|
likeCount: parseInt(row.likeCount) || 0,
|
|
|
commentCount: parseInt(row.commentCount) || 0,
|
|
commentCount: parseInt(row.commentCount) || 0,
|
|
@@ -388,4 +399,382 @@ export class WorkDayStatisticsService {
|
|
|
|
|
|
|
|
return groupedData;
|
|
return groupedData;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取数据总览
|
|
|
|
|
+ * 返回账号列表和汇总统计数据
|
|
|
|
|
+ */
|
|
|
|
|
+ async getOverview(userId: number): Promise<{
|
|
|
|
|
+ accounts: Array<{
|
|
|
|
|
+ id: number;
|
|
|
|
|
+ nickname: string;
|
|
|
|
|
+ username: string;
|
|
|
|
|
+ avatarUrl: string | null;
|
|
|
|
|
+ platform: string;
|
|
|
|
|
+ groupId: number | null;
|
|
|
|
|
+ fansCount: number;
|
|
|
|
|
+ totalIncome: number | null;
|
|
|
|
|
+ yesterdayIncome: number | null;
|
|
|
|
|
+ totalViews: number | null;
|
|
|
|
|
+ yesterdayViews: number | null;
|
|
|
|
|
+ yesterdayComments: number;
|
|
|
|
|
+ yesterdayLikes: number;
|
|
|
|
|
+ yesterdayFansIncrease: number;
|
|
|
|
|
+ updateTime: string;
|
|
|
|
|
+ status: string;
|
|
|
|
|
+ }>;
|
|
|
|
|
+ summary: {
|
|
|
|
|
+ totalAccounts: number;
|
|
|
|
|
+ totalIncome: number;
|
|
|
|
|
+ yesterdayIncome: number;
|
|
|
|
|
+ totalViews: number;
|
|
|
|
|
+ yesterdayViews: number;
|
|
|
|
|
+ totalFans: number;
|
|
|
|
|
+ yesterdayComments: number;
|
|
|
|
|
+ yesterdayLikes: number;
|
|
|
|
|
+ yesterdayFansIncrease: number;
|
|
|
|
|
+ };
|
|
|
|
|
+ }> {
|
|
|
|
|
+ // 只查询支持的平台:抖音、百家号、视频号、小红书
|
|
|
|
|
+ const allowedPlatforms = ['douyin', 'baijiahao', 'weixin_video', 'xiaohongshu'];
|
|
|
|
|
+
|
|
|
|
|
+ // 获取用户的所有账号(只包含支持的平台)
|
|
|
|
|
+ const accounts = await this.accountRepository.find({
|
|
|
|
|
+ where: {
|
|
|
|
|
+ userId,
|
|
|
|
|
+ platform: In(allowedPlatforms),
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 使用中国时区(UTC+8)计算“今天/昨天”的业务日期
|
|
|
|
|
+ // 思路:在当前 UTC 时间基础上 +8 小时,再取 ISO 日期部分,即为中国日历日期
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+ const chinaNow = new Date(now.getTime() + 8 * 60 * 60 * 1000);
|
|
|
|
|
+ const chinaYesterday = new Date(chinaNow.getTime() - 24 * 60 * 60 * 1000);
|
|
|
|
|
+
|
|
|
|
|
+ // 格式化为 YYYY-MM-DD,与 MySQL DATE 字段匹配
|
|
|
|
|
+ const todayStr = chinaNow.toISOString().split('T')[0];
|
|
|
|
|
+ const yesterdayStr = chinaYesterday.toISOString().split('T')[0];
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(`[WorkDayStatistics] getOverview - userId: ${userId}, today: ${todayStr}, yesterday: ${yesterdayStr}`);
|
|
|
|
|
+
|
|
|
|
|
+ const accountList: Array<{
|
|
|
|
|
+ id: number;
|
|
|
|
|
+ nickname: string;
|
|
|
|
|
+ username: string;
|
|
|
|
|
+ avatarUrl: string | null;
|
|
|
|
|
+ platform: string;
|
|
|
|
|
+ groupId: number | null;
|
|
|
|
|
+ fansCount: number;
|
|
|
|
|
+ totalIncome: number | null;
|
|
|
|
|
+ yesterdayIncome: number | null;
|
|
|
|
|
+ totalViews: number | null;
|
|
|
|
|
+ yesterdayViews: number | null;
|
|
|
|
|
+ yesterdayComments: number;
|
|
|
|
|
+ yesterdayLikes: number;
|
|
|
|
|
+ yesterdayFansIncrease: number;
|
|
|
|
|
+ updateTime: string;
|
|
|
|
|
+ status: string;
|
|
|
|
|
+ }> = [];
|
|
|
|
|
+
|
|
|
|
|
+ // 汇总统计数据
|
|
|
|
|
+ let totalAccounts = 0;
|
|
|
|
|
+ let totalIncome = 0;
|
|
|
|
|
+ let yesterdayIncome = 0;
|
|
|
|
|
+ let totalViews = 0;
|
|
|
|
|
+ let yesterdayViews = 0;
|
|
|
|
|
+ let totalFans = 0;
|
|
|
|
|
+ let yesterdayComments = 0;
|
|
|
|
|
+ let yesterdayLikes = 0;
|
|
|
|
|
+ let yesterdayFansIncrease = 0;
|
|
|
|
|
+
|
|
|
|
|
+ for (const account of accounts) {
|
|
|
|
|
+ // 获取该账号的所有作品ID
|
|
|
|
|
+ const works = await this.workRepository.find({
|
|
|
|
|
+ where: { accountId: account.id },
|
|
|
|
|
+ select: ['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,
|
|
|
|
|
+ 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)));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算昨日增量
|
|
|
|
|
+ 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}`);
|
|
|
|
|
+
|
|
|
|
|
+ accountList.push({
|
|
|
|
|
+ id: account.id,
|
|
|
|
|
+ nickname: account.accountName || '',
|
|
|
|
|
+ username: account.accountId || '',
|
|
|
|
|
+ avatarUrl: account.avatarUrl,
|
|
|
|
|
+ platform: account.platform,
|
|
|
|
|
+ groupId: account.groupId,
|
|
|
|
|
+ fansCount: accountFansCount,
|
|
|
|
|
+ totalIncome: null, // 收益数据需要从其他表获取,暂时为null
|
|
|
|
|
+ yesterdayIncome: null,
|
|
|
|
|
+ totalViews: accountTotalViews > 0 ? accountTotalViews : null,
|
|
|
|
|
+ yesterdayViews: accountYesterdayViews > 0 ? accountYesterdayViews : null,
|
|
|
|
|
+ yesterdayComments: accountYesterdayComments,
|
|
|
|
|
+ yesterdayLikes: accountYesterdayLikes,
|
|
|
|
|
+ yesterdayFansIncrease: accountYesterdayFansIncrease,
|
|
|
|
|
+ updateTime: account.updatedAt.toISOString(),
|
|
|
|
|
+ status: account.status,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 累加汇总数据
|
|
|
|
|
+ totalAccounts++;
|
|
|
|
|
+ totalViews += accountTotalViews;
|
|
|
|
|
+ yesterdayViews += accountYesterdayViews;
|
|
|
|
|
+ totalFans += accountFansCount;
|
|
|
|
|
+ yesterdayComments += accountYesterdayComments;
|
|
|
|
|
+ yesterdayLikes += accountYesterdayLikes;
|
|
|
|
|
+ yesterdayFansIncrease += accountYesterdayFansIncrease;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ accounts: accountList,
|
|
|
|
|
+ summary: {
|
|
|
|
|
+ totalAccounts,
|
|
|
|
|
+ totalIncome,
|
|
|
|
|
+ yesterdayIncome,
|
|
|
|
|
+ totalViews,
|
|
|
|
|
+ yesterdayViews,
|
|
|
|
|
+ totalFans,
|
|
|
|
|
+ yesterdayComments,
|
|
|
|
|
+ yesterdayLikes,
|
|
|
|
|
+ yesterdayFansIncrease,
|
|
|
|
|
+ },
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|