WorkDayStatisticsService.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709
  1. import { AppDataSource, WorkDayStatistics, Work, PlatformAccount } from '../models/index.js';
  2. import { Between, In } from 'typeorm';
  3. import { logger } from '../utils/logger.js';
  4. interface StatisticsItem {
  5. workId: number;
  6. fansCount?: number;
  7. playCount?: number;
  8. likeCount?: number;
  9. commentCount?: number;
  10. shareCount?: number;
  11. collectCount?: number;
  12. }
  13. interface SaveResult {
  14. inserted: number;
  15. updated: number;
  16. }
  17. interface TrendData {
  18. dates: string[];
  19. fans: number[];
  20. views: number[];
  21. likes: number[];
  22. comments: number[];
  23. shares: number[];
  24. collects: number[];
  25. }
  26. interface PlatformStatItem {
  27. platform: string;
  28. fansCount: number;
  29. fansIncrease: number;
  30. viewsCount: number;
  31. likesCount: number;
  32. commentsCount: number;
  33. collectsCount: number;
  34. }
  35. interface WorkStatisticsItem {
  36. recordDate: string;
  37. fansCount: number;
  38. playCount: number;
  39. likeCount: number;
  40. commentCount: number;
  41. shareCount: number;
  42. collectCount: number;
  43. }
  44. export class WorkDayStatisticsService {
  45. private statisticsRepository = AppDataSource.getRepository(WorkDayStatistics);
  46. private workRepository = AppDataSource.getRepository(Work);
  47. private accountRepository = AppDataSource.getRepository(PlatformAccount);
  48. /**
  49. * 保存作品日统计数据
  50. * 当天的数据走更新流,日期变化走新增流
  51. */
  52. async saveStatistics(statistics: StatisticsItem[]): Promise<SaveResult> {
  53. const today = new Date();
  54. today.setHours(0, 0, 0, 0);
  55. let insertedCount = 0;
  56. let updatedCount = 0;
  57. for (const stat of statistics) {
  58. if (!stat.workId) continue;
  59. // 检查当天是否已有记录
  60. const existing = await this.statisticsRepository.findOne({
  61. where: {
  62. workId: stat.workId,
  63. recordDate: today,
  64. },
  65. });
  66. if (existing) {
  67. // 更新已有记录
  68. await this.statisticsRepository.update(existing.id, {
  69. fansCount: stat.fansCount ?? existing.fansCount,
  70. playCount: stat.playCount ?? existing.playCount,
  71. likeCount: stat.likeCount ?? existing.likeCount,
  72. commentCount: stat.commentCount ?? existing.commentCount,
  73. shareCount: stat.shareCount ?? existing.shareCount,
  74. collectCount: stat.collectCount ?? existing.collectCount,
  75. });
  76. updatedCount++;
  77. } else {
  78. // 插入新记录
  79. const newStat = this.statisticsRepository.create({
  80. workId: stat.workId,
  81. recordDate: today,
  82. fansCount: stat.fansCount ?? 0,
  83. playCount: stat.playCount ?? 0,
  84. likeCount: stat.likeCount ?? 0,
  85. commentCount: stat.commentCount ?? 0,
  86. shareCount: stat.shareCount ?? 0,
  87. collectCount: stat.collectCount ?? 0,
  88. });
  89. await this.statisticsRepository.save(newStat);
  90. insertedCount++;
  91. }
  92. }
  93. return { inserted: insertedCount, updated: updatedCount };
  94. }
  95. /**
  96. * 获取数据趋势
  97. */
  98. async getTrend(
  99. userId: number,
  100. options: {
  101. days?: number;
  102. startDate?: string;
  103. endDate?: string;
  104. accountId?: number;
  105. }
  106. ): Promise<TrendData> {
  107. const { days = 7, startDate, endDate, accountId } = options;
  108. // 计算日期范围
  109. let dateStart: Date;
  110. let dateEnd: Date;
  111. if (startDate && endDate) {
  112. dateStart = new Date(startDate);
  113. dateEnd = new Date(endDate);
  114. } else {
  115. dateEnd = new Date();
  116. dateStart = new Date();
  117. dateStart.setDate(dateStart.getDate() - Math.min(days, 30) + 1);
  118. }
  119. // 构建查询
  120. const queryBuilder = this.statisticsRepository
  121. .createQueryBuilder('wds')
  122. .innerJoin(Work, 'w', 'wds.work_id = w.id')
  123. .select('wds.record_date', 'recordDate')
  124. .addSelect('w.accountId', 'accountId')
  125. .addSelect('MAX(wds.fans_count)', 'accountFans')
  126. .addSelect('SUM(wds.play_count)', 'accountViews')
  127. .addSelect('SUM(wds.like_count)', 'accountLikes')
  128. .addSelect('SUM(wds.comment_count)', 'accountComments')
  129. .addSelect('SUM(wds.share_count)', 'accountShares')
  130. .addSelect('SUM(wds.collect_count)', 'accountCollects')
  131. .where('w.userId = :userId', { userId })
  132. .andWhere('wds.record_date >= :dateStart', { dateStart })
  133. .andWhere('wds.record_date <= :dateEnd', { dateEnd })
  134. .groupBy('wds.record_date')
  135. .addGroupBy('w.accountId')
  136. .orderBy('wds.record_date', 'ASC');
  137. if (accountId) {
  138. queryBuilder.andWhere('w.accountId = :accountId', { accountId });
  139. }
  140. const accountResults = await queryBuilder.getRawMany();
  141. // 按日期汇总所有账号的数据
  142. const dateMap = new Map<string, {
  143. fans: number;
  144. views: number;
  145. likes: number;
  146. comments: number;
  147. shares: number;
  148. collects: number;
  149. }>();
  150. for (const row of accountResults) {
  151. const dateKey = row.recordDate instanceof Date
  152. ? row.recordDate.toISOString().split('T')[0]
  153. : String(row.recordDate).split('T')[0];
  154. if (!dateMap.has(dateKey)) {
  155. dateMap.set(dateKey, {
  156. fans: 0,
  157. views: 0,
  158. likes: 0,
  159. comments: 0,
  160. shares: 0,
  161. collects: 0,
  162. });
  163. }
  164. const current = dateMap.get(dateKey)!;
  165. current.fans += parseInt(row.accountFans) || 0;
  166. current.views += parseInt(row.accountViews) || 0;
  167. current.likes += parseInt(row.accountLikes) || 0;
  168. current.comments += parseInt(row.accountComments) || 0;
  169. current.shares += parseInt(row.accountShares) || 0;
  170. current.collects += parseInt(row.accountCollects) || 0;
  171. }
  172. // 构建响应数据
  173. const dates: string[] = [];
  174. const fans: number[] = [];
  175. const views: number[] = [];
  176. const likes: number[] = [];
  177. const comments: number[] = [];
  178. const shares: number[] = [];
  179. const collects: number[] = [];
  180. // 按日期排序
  181. const sortedDates = Array.from(dateMap.keys()).sort();
  182. for (const dateKey of sortedDates) {
  183. dates.push(dateKey.slice(5)); // "YYYY-MM-DD" -> "MM-DD"
  184. const data = dateMap.get(dateKey)!;
  185. fans.push(data.fans);
  186. views.push(data.views);
  187. likes.push(data.likes);
  188. comments.push(data.comments);
  189. shares.push(data.shares);
  190. collects.push(data.collects);
  191. }
  192. // 如果没有数据,生成空的日期范围
  193. if (dates.length === 0) {
  194. const d = new Date(dateStart);
  195. while (d <= dateEnd) {
  196. dates.push(`${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`);
  197. fans.push(0);
  198. views.push(0);
  199. likes.push(0);
  200. comments.push(0);
  201. shares.push(0);
  202. collects.push(0);
  203. d.setDate(d.getDate() + 1);
  204. }
  205. }
  206. return { dates, fans, views, likes, comments, shares, collects };
  207. }
  208. /**
  209. * 按平台分组获取统计数据
  210. */
  211. async getStatisticsByPlatform(
  212. userId: number,
  213. options: {
  214. days?: number;
  215. startDate?: string;
  216. endDate?: string;
  217. }
  218. ): Promise<PlatformStatItem[]> {
  219. const { days = 30, startDate, endDate } = options;
  220. // 计算日期范围
  221. let dateStart: Date;
  222. let dateEnd: Date;
  223. if (startDate && endDate) {
  224. dateStart = new Date(startDate);
  225. dateEnd = new Date(endDate);
  226. } else {
  227. dateEnd = new Date();
  228. dateStart = new Date();
  229. dateStart.setDate(dateStart.getDate() - Math.min(days, 30) + 1);
  230. }
  231. // 获取用户的所有账号
  232. const accounts = await this.accountRepository.find({
  233. where: { userId },
  234. });
  235. const platformData: PlatformStatItem[] = [];
  236. for (const account of accounts) {
  237. // 获取该账号在区间内第一天和最后一天的数据
  238. const firstDayQuery = this.statisticsRepository
  239. .createQueryBuilder('wds')
  240. .innerJoin(Work, 'w', 'wds.work_id = w.id')
  241. .select('MAX(wds.fans_count)', 'fans')
  242. .addSelect('SUM(wds.play_count)', 'views')
  243. .addSelect('SUM(wds.like_count)', 'likes')
  244. .addSelect('SUM(wds.comment_count)', 'comments')
  245. .addSelect('SUM(wds.collect_count)', 'collects')
  246. .where('w.accountId = :accountId', { accountId: account.id })
  247. .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)', {
  248. accountId2: account.id,
  249. dateStart,
  250. dateEnd,
  251. });
  252. const lastDayQuery = this.statisticsRepository
  253. .createQueryBuilder('wds')
  254. .innerJoin(Work, 'w', 'wds.work_id = w.id')
  255. .select('MAX(wds.fans_count)', 'fans')
  256. .addSelect('SUM(wds.play_count)', 'views')
  257. .addSelect('SUM(wds.like_count)', 'likes')
  258. .addSelect('SUM(wds.comment_count)', 'comments')
  259. .addSelect('SUM(wds.collect_count)', 'collects')
  260. .where('w.accountId = :accountId', { accountId: account.id })
  261. .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)', {
  262. accountId2: account.id,
  263. dateStart,
  264. dateEnd,
  265. });
  266. const [firstDay, lastDay] = await Promise.all([
  267. firstDayQuery.getRawOne(),
  268. lastDayQuery.getRawOne(),
  269. ]);
  270. const currentFans = account.fansCount ?? 0;
  271. const earliestFans = parseInt(firstDay?.fans) || currentFans;
  272. const fansIncrease = currentFans - earliestFans;
  273. const viewsIncrease = (parseInt(lastDay?.views) || 0) - (parseInt(firstDay?.views) || 0);
  274. const likesIncrease = (parseInt(lastDay?.likes) || 0) - (parseInt(firstDay?.likes) || 0);
  275. const commentsIncrease = (parseInt(lastDay?.comments) || 0) - (parseInt(firstDay?.comments) || 0);
  276. const collectsIncrease = (parseInt(lastDay?.collects) || 0) - (parseInt(firstDay?.collects) || 0);
  277. platformData.push({
  278. platform: account.platform,
  279. fansCount: currentFans,
  280. fansIncrease,
  281. viewsCount: Math.max(0, viewsIncrease),
  282. likesCount: Math.max(0, likesIncrease),
  283. commentsCount: Math.max(0, commentsIncrease),
  284. collectsCount: Math.max(0, collectsIncrease),
  285. });
  286. }
  287. // 按粉丝数降序排序
  288. platformData.sort((a, b) => b.fansCount - a.fansCount);
  289. return platformData;
  290. }
  291. /**
  292. * 批量获取作品的历史统计数据
  293. */
  294. async getWorkStatisticsHistory(
  295. workIds: number[],
  296. options: {
  297. startDate?: string;
  298. endDate?: string;
  299. }
  300. ): Promise<Record<string, WorkStatisticsItem[]>> {
  301. const { startDate, endDate } = options;
  302. const queryBuilder = this.statisticsRepository
  303. .createQueryBuilder('wds')
  304. .select('wds.work_id', 'workId')
  305. .addSelect('wds.record_date', 'recordDate')
  306. .addSelect('wds.fans_count', 'fansCount')
  307. .addSelect('wds.play_count', 'playCount')
  308. .addSelect('wds.like_count', 'likeCount')
  309. .addSelect('wds.comment_count', 'commentCount')
  310. .addSelect('wds.share_count', 'shareCount')
  311. .addSelect('wds.collect_count', 'collectCount')
  312. .where('wds.work_id IN (:...workIds)', { workIds })
  313. .orderBy('wds.work_id', 'ASC')
  314. .addOrderBy('wds.record_date', 'ASC');
  315. if (startDate) {
  316. queryBuilder.andWhere('wds.record_date >= :startDate', { startDate });
  317. }
  318. if (endDate) {
  319. queryBuilder.andWhere('wds.record_date <= :endDate', { endDate });
  320. }
  321. const results = await queryBuilder.getRawMany();
  322. // 按 workId 分组
  323. const groupedData: Record<string, WorkStatisticsItem[]> = {};
  324. for (const row of results) {
  325. const workId = String(row.workId);
  326. if (!groupedData[workId]) {
  327. groupedData[workId] = [];
  328. }
  329. const recordDate = row.recordDate instanceof Date
  330. ? row.recordDate.toISOString().split('T')[0]
  331. : String(row.recordDate).split('T')[0];
  332. groupedData[workId].push({
  333. recordDate,
  334. fansCount: parseInt(row.fansCount) || 0,
  335. playCount: parseInt(row.playCount) || 0,
  336. likeCount: parseInt(row.likeCount) || 0,
  337. commentCount: parseInt(row.commentCount) || 0,
  338. shareCount: parseInt(row.shareCount) || 0,
  339. collectCount: parseInt(row.collectCount) || 0,
  340. });
  341. }
  342. return groupedData;
  343. }
  344. /**
  345. * 获取数据总览
  346. * 返回账号列表和汇总统计数据
  347. */
  348. async getOverview(userId: number): Promise<{
  349. accounts: Array<{
  350. id: number;
  351. nickname: string;
  352. username: string;
  353. avatarUrl: string | null;
  354. platform: string;
  355. groupId: number | null;
  356. fansCount: number;
  357. totalIncome: number | null;
  358. yesterdayIncome: number | null;
  359. totalViews: number | null;
  360. yesterdayViews: number | null;
  361. yesterdayComments: number;
  362. yesterdayLikes: number;
  363. yesterdayFansIncrease: number;
  364. updateTime: string;
  365. status: string;
  366. }>;
  367. summary: {
  368. totalAccounts: number;
  369. totalIncome: number;
  370. yesterdayIncome: number;
  371. totalViews: number;
  372. yesterdayViews: number;
  373. totalFans: number;
  374. yesterdayComments: number;
  375. yesterdayLikes: number;
  376. yesterdayFansIncrease: number;
  377. };
  378. }> {
  379. // 只查询支持的平台:抖音、百家号、视频号、小红书
  380. const allowedPlatforms = ['douyin', 'baijiahao', 'weixin_video', 'xiaohongshu'];
  381. // 获取用户的所有账号(只包含支持的平台)
  382. const accounts = await this.accountRepository.find({
  383. where: {
  384. userId,
  385. platform: In(allowedPlatforms),
  386. },
  387. });
  388. // 使用中国时区(UTC+8)计算“今天/昨天”的业务日期
  389. // 思路:在当前 UTC 时间基础上 +8 小时,再取 ISO 日期部分,即为中国日历日期
  390. const now = new Date();
  391. const chinaNow = new Date(now.getTime() + 8 * 60 * 60 * 1000);
  392. const chinaYesterday = new Date(chinaNow.getTime() - 24 * 60 * 60 * 1000);
  393. // 格式化为 YYYY-MM-DD,与 MySQL DATE 字段匹配
  394. const todayStr = chinaNow.toISOString().split('T')[0];
  395. const yesterdayStr = chinaYesterday.toISOString().split('T')[0];
  396. logger.info(`[WorkDayStatistics] getOverview - userId: ${userId}, today: ${todayStr}, yesterday: ${yesterdayStr}`);
  397. const accountList: Array<{
  398. id: number;
  399. nickname: string;
  400. username: string;
  401. avatarUrl: string | null;
  402. platform: string;
  403. groupId: number | null;
  404. fansCount: number;
  405. totalIncome: number | null;
  406. yesterdayIncome: number | null;
  407. totalViews: number | null;
  408. yesterdayViews: number | null;
  409. yesterdayComments: number;
  410. yesterdayLikes: number;
  411. yesterdayFansIncrease: number;
  412. updateTime: string;
  413. status: string;
  414. }> = [];
  415. // 汇总统计数据
  416. let totalAccounts = 0;
  417. let totalIncome = 0;
  418. let yesterdayIncome = 0;
  419. let totalViews = 0;
  420. let yesterdayViews = 0;
  421. let totalFans = 0;
  422. let yesterdayComments = 0;
  423. let yesterdayLikes = 0;
  424. let yesterdayFansIncrease = 0;
  425. for (const account of accounts) {
  426. // 获取该账号的所有作品ID
  427. const works = await this.workRepository.find({
  428. where: { accountId: account.id },
  429. select: ['id'],
  430. });
  431. if (works.length === 0) {
  432. // 如果没有作品,只返回账号基本信息
  433. const accountFansCount = account.fansCount || 0;
  434. accountList.push({
  435. id: account.id,
  436. nickname: account.accountName || '',
  437. username: account.accountId || '',
  438. avatarUrl: account.avatarUrl,
  439. platform: account.platform,
  440. groupId: account.groupId,
  441. fansCount: accountFansCount,
  442. totalIncome: null,
  443. yesterdayIncome: null,
  444. totalViews: null,
  445. yesterdayViews: null,
  446. yesterdayComments: 0,
  447. yesterdayLikes: 0,
  448. yesterdayFansIncrease: 0,
  449. updateTime: account.updatedAt.toISOString(),
  450. status: account.status,
  451. });
  452. // 即使没有作品,也要累加账号的粉丝数到总粉丝数
  453. totalAccounts++;
  454. totalFans += accountFansCount;
  455. continue;
  456. }
  457. const workIds = works.map(w => w.id);
  458. // 获取每个作品的最新日期统计数据(总播放量等)
  459. const latestStatsQuery = this.statisticsRepository
  460. .createQueryBuilder('wds')
  461. .select('wds.work_id', 'workId')
  462. .addSelect('MAX(wds.record_date)', 'latestDate')
  463. .addSelect('MAX(wds.play_count)', 'playCount')
  464. .addSelect('MAX(wds.like_count)', 'likeCount')
  465. .addSelect('MAX(wds.comment_count)', 'commentCount')
  466. .addSelect('MAX(wds.fans_count)', 'fansCount')
  467. .where('wds.work_id IN (:...workIds)', { workIds })
  468. .groupBy('wds.work_id');
  469. const latestStats = await latestStatsQuery.getRawMany();
  470. // 计算总播放量(所有作品最新日期的play_count总和)
  471. let accountTotalViews = 0;
  472. const latestDateMap = new Map<number, string>();
  473. for (const stat of latestStats) {
  474. accountTotalViews += parseInt(stat.playCount) || 0;
  475. latestDateMap.set(stat.workId, stat.latestDate);
  476. }
  477. // 获取昨天和今天的数据来计算增量
  478. // 使用日期字符串直接比较(DATE 类型会自动转换)
  479. const yesterdayStatsQuery = this.statisticsRepository
  480. .createQueryBuilder('wds')
  481. .select('wds.work_id', 'workId')
  482. .addSelect('SUM(wds.play_count)', 'playCount')
  483. .addSelect('SUM(wds.like_count)', 'likeCount')
  484. .addSelect('SUM(wds.comment_count)', 'commentCount')
  485. .addSelect('MAX(wds.fans_count)', 'fansCount')
  486. .where('wds.work_id IN (:...workIds)', { workIds })
  487. .andWhere('wds.record_date = :yesterday', { yesterday: yesterdayStr })
  488. .groupBy('wds.work_id');
  489. const todayStatsQuery = this.statisticsRepository
  490. .createQueryBuilder('wds')
  491. .select('wds.work_id', 'workId')
  492. .addSelect('SUM(wds.play_count)', 'playCount')
  493. .addSelect('SUM(wds.like_count)', 'likeCount')
  494. .addSelect('SUM(wds.comment_count)', 'commentCount')
  495. .addSelect('MAX(wds.fans_count)', 'fansCount')
  496. .where('wds.work_id IN (:...workIds)', { workIds })
  497. .andWhere('wds.record_date = :today', { today: todayStr })
  498. .groupBy('wds.work_id');
  499. const [yesterdayStats, todayStats] = await Promise.all([
  500. yesterdayStatsQuery.getRawMany(),
  501. todayStatsQuery.getRawMany(),
  502. ]);
  503. logger.info(`[WorkDayStatistics] Account ${account.id} (${account.accountName}) - workIds: ${workIds.length}, yesterdayStats: ${yesterdayStats.length}, todayStats: ${todayStats.length}`);
  504. if (yesterdayStats.length > 0 || todayStats.length > 0) {
  505. logger.debug(`[WorkDayStatistics] yesterdayStats:`, JSON.stringify(yesterdayStats.slice(0, 3)));
  506. logger.debug(`[WorkDayStatistics] todayStats:`, JSON.stringify(todayStats.slice(0, 3)));
  507. }
  508. // 计算昨日增量
  509. let accountYesterdayViews = 0;
  510. let accountYesterdayComments = 0;
  511. let accountYesterdayLikes = 0;
  512. let accountYesterdayFansIncrease = 0;
  513. // 按作品ID汇总
  514. const yesterdayMap = new Map<number, { play: number; like: number; comment: number; fans: number }>();
  515. const todayMap = new Map<number, { play: number; like: number; comment: number; fans: number }>();
  516. for (const stat of yesterdayStats) {
  517. const workId = parseInt(String(stat.workId)) || 0;
  518. yesterdayMap.set(workId, {
  519. play: Number(stat.playCount) || 0,
  520. like: Number(stat.likeCount) || 0,
  521. comment: Number(stat.commentCount) || 0,
  522. fans: Number(stat.fansCount) || 0,
  523. });
  524. }
  525. for (const stat of todayStats) {
  526. const workId = parseInt(String(stat.workId)) || 0;
  527. todayMap.set(workId, {
  528. play: Number(stat.playCount) || 0,
  529. like: Number(stat.likeCount) || 0,
  530. comment: Number(stat.commentCount) || 0,
  531. fans: Number(stat.fansCount) || 0,
  532. });
  533. }
  534. logger.debug(`[WorkDayStatistics] Account ${account.id} - yesterdayMap size: ${yesterdayMap.size}, todayMap size: ${todayMap.size}`);
  535. // 计算增量(今天 - 昨天)
  536. for (const workId of workIds) {
  537. const todayData = todayMap.get(workId) || { play: 0, like: 0, comment: 0, fans: 0 };
  538. const yesterdayData = yesterdayMap.get(workId) || { play: 0, like: 0, comment: 0, fans: 0 };
  539. const viewsDiff = todayData.play - yesterdayData.play;
  540. const commentsDiff = todayData.comment - yesterdayData.comment;
  541. const likesDiff = todayData.like - yesterdayData.like;
  542. accountYesterdayViews += Math.max(0, viewsDiff);
  543. accountYesterdayComments += Math.max(0, commentsDiff);
  544. accountYesterdayLikes += Math.max(0, likesDiff);
  545. }
  546. logger.info(`[WorkDayStatistics] Account ${account.id} - Calculated: views=${accountYesterdayViews}, comments=${accountYesterdayComments}, likes=${accountYesterdayLikes}`);
  547. // 获取账号的最新粉丝数(从最新日期的统计数据中取最大值)
  548. let accountFansCount = account.fansCount || 0;
  549. if (latestStats.length > 0) {
  550. const maxFans = Math.max(...latestStats.map(s => parseInt(s.fansCount) || 0));
  551. if (maxFans > 0) {
  552. accountFansCount = maxFans;
  553. }
  554. }
  555. // 计算昨日涨粉(今天最新粉丝数 - 昨天最新粉丝数)
  556. // 如果今天有统计数据,用今天数据中的最大粉丝数;否则用账号表的当前粉丝数
  557. const todayMaxFans = todayStats.length > 0
  558. ? Math.max(...todayStats.map(s => parseInt(s.fansCount) || 0))
  559. : accountFansCount;
  560. // 如果昨天有统计数据,用昨天数据中的最大粉丝数;否则需要找最近一天的数据作为基准
  561. let yesterdayMaxFans: number;
  562. if (yesterdayStats.length > 0) {
  563. // 昨天有数据,直接用昨天的最大粉丝数
  564. yesterdayMaxFans = Math.max(...yesterdayStats.map(s => parseInt(s.fansCount) || 0));
  565. } else {
  566. // 昨天没有数据,需要找最近一天的数据作为基准
  567. // 查询该账号最近一天(早于今天)的统计数据
  568. const recentStatsQuery = this.statisticsRepository
  569. .createQueryBuilder('wds')
  570. .select('MAX(wds.fans_count)', 'fansCount')
  571. .where('wds.work_id IN (:...workIds)', { workIds })
  572. .andWhere('wds.record_date < :today', { today: todayStr });
  573. const recentStat = await recentStatsQuery.getRawOne();
  574. if (recentStat && recentStat.fansCount) {
  575. // 找到了最近一天的数据,用它作为基准
  576. yesterdayMaxFans = parseInt(recentStat.fansCount) || accountFansCount;
  577. } else {
  578. // 完全没有历史数据,用账号表的当前粉丝数作为基准(但这样计算出来的增量可能不准确)
  579. yesterdayMaxFans = accountFansCount;
  580. }
  581. }
  582. accountYesterdayFansIncrease = todayMaxFans - yesterdayMaxFans;
  583. accountList.push({
  584. id: account.id,
  585. nickname: account.accountName || '',
  586. username: account.accountId || '',
  587. avatarUrl: account.avatarUrl,
  588. platform: account.platform,
  589. groupId: account.groupId,
  590. fansCount: accountFansCount,
  591. totalIncome: null, // 收益数据需要从其他表获取,暂时为null
  592. yesterdayIncome: null,
  593. totalViews: accountTotalViews > 0 ? accountTotalViews : null,
  594. yesterdayViews: accountYesterdayViews > 0 ? accountYesterdayViews : null,
  595. yesterdayComments: accountYesterdayComments,
  596. yesterdayLikes: accountYesterdayLikes,
  597. yesterdayFansIncrease: accountYesterdayFansIncrease,
  598. updateTime: account.updatedAt.toISOString(),
  599. status: account.status,
  600. });
  601. // 累加汇总数据
  602. totalAccounts++;
  603. totalViews += accountTotalViews;
  604. yesterdayViews += accountYesterdayViews;
  605. totalFans += accountFansCount;
  606. yesterdayComments += accountYesterdayComments;
  607. yesterdayLikes += accountYesterdayLikes;
  608. yesterdayFansIncrease += accountYesterdayFansIncrease;
  609. }
  610. return {
  611. accounts: accountList,
  612. summary: {
  613. totalAccounts,
  614. totalIncome,
  615. yesterdayIncome,
  616. totalViews,
  617. yesterdayViews,
  618. totalFans,
  619. yesterdayComments,
  620. yesterdayLikes,
  621. yesterdayFansIncrease,
  622. },
  623. };
  624. }
  625. }