| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994 |
- import { AppDataSource, WorkDayStatistics, Work, PlatformAccount, UserDayStatistics } from '../models/index.js';
- import { In } from 'typeorm';
- import { logger } from '../utils/logger.js';
- interface StatisticsItem {
- workId: number;
- playCount?: number;
- likeCount?: number;
- commentCount?: number;
- shareCount?: number;
- collectCount?: number;
- }
- interface SaveResult {
- inserted: number;
- updated: number;
- }
- interface TrendData {
- dates: string[];
- fans: number[];
- views: number[];
- likes: number[];
- comments: number[];
- shares: number[];
- collects: number[];
- }
- interface PlatformStatItem {
- platform: string;
- fansCount: number;
- fansIncrease: number;
- viewsCount: number;
- likesCount: number;
- commentsCount: number;
- collectsCount: number;
- updateTime?: string;
- }
- interface WorkStatisticsItem {
- recordDate: string;
- playCount: number;
- likeCount: number;
- commentCount: number;
- shareCount: number;
- collectCount: number;
- }
- export class WorkDayStatisticsService {
- private statisticsRepository = AppDataSource.getRepository(WorkDayStatistics);
- private workRepository = AppDataSource.getRepository(Work);
- private accountRepository = AppDataSource.getRepository(PlatformAccount);
- private userDayStatisticsRepository = AppDataSource.getRepository(UserDayStatistics);
- private formatDate(d: Date) {
- const yyyy = d.getFullYear();
- const mm = String(d.getMonth() + 1).padStart(2, '0');
- const dd = String(d.getDate()).padStart(2, '0');
- return `${yyyy}-${mm}-${dd}`;
- }
- /**
- * 获取某个账号在指定日期(<= targetDate)时各作品的“最新一条”累计数据总和
- * 口径:对该账号所有作品,每个作品取 record_date <= targetDate 的最大日期那条记录,然后把 play/like/comment/collect 求和
- */
- private async getWorkSumsAtDate(
- workIds: number[],
- targetDate: string
- ): Promise<{ views: number; likes: number; comments: number; collects: number }> {
- if (!workIds.length) {
- return { views: 0, likes: 0, comments: 0, collects: 0 };
- }
- // MySQL: 派生表先取每个作品 <= targetDate 的最新日期,再回连取该日数据求和
- // 注意:workIds 使用 IN (...),由 TypeORM 负责参数化,避免注入
- const placeholders = workIds.map(() => '?').join(',');
- const sql = `
- SELECT
- COALESCE(SUM(wds.play_count), 0) AS views,
- COALESCE(SUM(wds.like_count), 0) AS likes,
- COALESCE(SUM(wds.comment_count), 0) AS comments,
- COALESCE(SUM(wds.collect_count), 0) AS collects
- FROM work_day_statistics wds
- INNER JOIN (
- SELECT wds2.work_id, MAX(wds2.record_date) AS record_date
- FROM work_day_statistics wds2
- WHERE wds2.work_id IN (${placeholders})
- AND wds2.record_date <= ?
- GROUP BY wds2.work_id
- ) latest
- ON latest.work_id = wds.work_id AND latest.record_date = wds.record_date
- `;
- const rows = await AppDataSource.query(sql, [...workIds, targetDate]);
- const row = rows?.[0] || {};
- return {
- views: Number(row.views) || 0,
- likes: Number(row.likes) || 0,
- comments: Number(row.comments) || 0,
- collects: Number(row.collects) || 0,
- };
- }
- /**
- * 保存作品日统计数据
- * 当天的数据走更新流,日期变化走新增流
- */
- async saveStatistics(statistics: StatisticsItem[]): Promise<SaveResult> {
- const today = new Date();
- today.setHours(0, 0, 0, 0);
- let insertedCount = 0;
- let updatedCount = 0;
- for (const stat of statistics) {
- if (!stat.workId) continue;
- // 检查当天是否已有记录
- const existing = await this.statisticsRepository.findOne({
- where: {
- workId: stat.workId,
- recordDate: today,
- },
- });
- if (existing) {
- // 更新已有记录(不再包含粉丝数,粉丝数从 user_day_statistics 表获取)
- await this.statisticsRepository.update(existing.id, {
- playCount: stat.playCount ?? existing.playCount,
- likeCount: stat.likeCount ?? existing.likeCount,
- commentCount: stat.commentCount ?? existing.commentCount,
- shareCount: stat.shareCount ?? existing.shareCount,
- collectCount: stat.collectCount ?? existing.collectCount,
- });
- updatedCount++;
- } else {
- // 插入新记录(不再包含粉丝数,粉丝数从 user_day_statistics 表获取)
- const newStat = this.statisticsRepository.create({
- workId: stat.workId,
- recordDate: today,
- playCount: stat.playCount ?? 0,
- likeCount: stat.likeCount ?? 0,
- commentCount: stat.commentCount ?? 0,
- shareCount: stat.shareCount ?? 0,
- collectCount: stat.collectCount ?? 0,
- });
- await this.statisticsRepository.save(newStat);
- insertedCount++;
- }
- }
- return { inserted: insertedCount, updated: updatedCount };
- }
- /**
- * 获取数据趋势
- */
- async getTrend(
- userId: number,
- options: {
- days?: number;
- startDate?: string;
- endDate?: string;
- accountId?: number;
- }
- ): Promise<TrendData> {
- const { days = 7, startDate, endDate, accountId } = options;
- // 计算日期范围
- let dateStart: Date;
- let dateEnd: Date;
- if (startDate && endDate) {
- dateStart = new Date(startDate);
- dateEnd = new Date(endDate);
- } else {
- dateEnd = new Date();
- dateStart = new Date();
- dateStart.setDate(dateStart.getDate() - Math.min(days, 30) + 1);
- }
- // 构建查询(不再从 work_day_statistics 读取粉丝数,粉丝数从 user_day_statistics 表获取)
- const queryBuilder = this.statisticsRepository
- .createQueryBuilder('wds')
- .innerJoin(Work, 'w', 'wds.work_id = w.id')
- .select('wds.record_date', 'recordDate')
- .addSelect('w.accountId', 'accountId')
- .addSelect('SUM(wds.play_count)', 'accountViews')
- .addSelect('SUM(wds.like_count)', 'accountLikes')
- .addSelect('SUM(wds.comment_count)', 'accountComments')
- .addSelect('SUM(wds.share_count)', 'accountShares')
- .addSelect('SUM(wds.collect_count)', 'accountCollects')
- .where('w.userId = :userId', { userId })
- .andWhere('wds.record_date >= :dateStart', { dateStart })
- .andWhere('wds.record_date <= :dateEnd', { dateEnd })
- .groupBy('wds.record_date')
- .addGroupBy('w.accountId')
- .orderBy('wds.record_date', 'ASC');
- if (accountId) {
- queryBuilder.andWhere('w.accountId = :accountId', { accountId });
- }
- const accountResults = await queryBuilder.getRawMany();
- // 按日期汇总所有账号的数据
- const dateMap = new Map<string, {
- fans: number;
- views: number;
- likes: number;
- comments: number;
- shares: number;
- collects: number;
- }>();
- for (const row of accountResults) {
- const dateKey = row.recordDate instanceof Date
- ? row.recordDate.toISOString().split('T')[0]
- : String(row.recordDate).split('T')[0];
-
- if (!dateMap.has(dateKey)) {
- dateMap.set(dateKey, {
- fans: 0,
- views: 0,
- likes: 0,
- comments: 0,
- shares: 0,
- collects: 0,
- });
- }
- const current = dateMap.get(dateKey)!;
- current.fans += parseInt(row.accountFans) || 0;
- current.views += parseInt(row.accountViews) || 0;
- current.likes += parseInt(row.accountLikes) || 0;
- current.comments += parseInt(row.accountComments) || 0;
- current.shares += parseInt(row.accountShares) || 0;
- current.collects += parseInt(row.accountCollects) || 0;
- }
- // 构建响应数据
- const dates: string[] = [];
- const fans: number[] = [];
- const views: number[] = [];
- const likes: number[] = [];
- const comments: number[] = [];
- const shares: number[] = [];
- const collects: number[] = [];
- // 按日期排序
- const sortedDates = Array.from(dateMap.keys()).sort();
- for (const dateKey of sortedDates) {
- dates.push(dateKey.slice(5)); // "YYYY-MM-DD" -> "MM-DD"
- const data = dateMap.get(dateKey)!;
- fans.push(data.fans);
- views.push(data.views);
- likes.push(data.likes);
- comments.push(data.comments);
- shares.push(data.shares);
- collects.push(data.collects);
- }
- // 如果没有数据,生成空的日期范围
- if (dates.length === 0) {
- const d = new Date(dateStart);
- while (d <= dateEnd) {
- dates.push(`${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`);
- fans.push(0);
- views.push(0);
- likes.push(0);
- comments.push(0);
- shares.push(0);
- collects.push(0);
- d.setDate(d.getDate() + 1);
- }
- }
- return { dates, fans, views, likes, comments, shares, collects };
- }
- /**
- * 按平台分组获取统计数据
- */
- async getStatisticsByPlatform(
- userId: number,
- options: {
- days?: number;
- startDate?: string;
- endDate?: string;
- }
- ): Promise<PlatformStatItem[]> {
- const { days = 30, startDate, endDate } = options;
- // 计算日期范围
- let dateStart: Date;
- let dateEnd: Date;
- if (startDate && endDate) {
- dateStart = new Date(startDate);
- dateEnd = new Date(endDate);
- } else {
- dateEnd = new Date();
- dateStart = new Date();
- dateStart.setDate(dateStart.getDate() - Math.min(days, 30) + 1);
- }
- const endDateStr = endDate ? endDate : this.formatDate(dateEnd);
- const startDateStr = startDate ? startDate : this.formatDate(dateStart);
- /**
- * 口径变更: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;
- commentsCount: number;
- likesCount: number;
- collectsCount: number;
- fansIncrease: number;
- latestUpdateTime?: string | Date | null;
- }>();
- 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 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 {
- platform,
- 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 || 0) - (a.fansCount || 0));
- return platformData;
- }
- /**
- * 批量获取作品的历史统计数据
- */
- async getWorkStatisticsHistory(
- workIds: number[],
- options: {
- startDate?: string;
- endDate?: string;
- }
- ): Promise<Record<string, WorkStatisticsItem[]>> {
- const { startDate, endDate } = options;
- const queryBuilder = this.statisticsRepository
- .createQueryBuilder('wds')
- .select('wds.work_id', 'workId')
- .addSelect('wds.record_date', 'recordDate')
- .addSelect('wds.play_count', 'playCount')
- .addSelect('wds.like_count', 'likeCount')
- .addSelect('wds.comment_count', 'commentCount')
- .addSelect('wds.share_count', 'shareCount')
- .addSelect('wds.collect_count', 'collectCount')
- .where('wds.work_id IN (:...workIds)', { workIds })
- .orderBy('wds.work_id', 'ASC')
- .addOrderBy('wds.record_date', 'ASC');
- if (startDate) {
- queryBuilder.andWhere('wds.record_date >= :startDate', { startDate });
- }
- if (endDate) {
- queryBuilder.andWhere('wds.record_date <= :endDate', { endDate });
- }
- const results = await queryBuilder.getRawMany();
- // 按 workId 分组
- const groupedData: Record<string, WorkStatisticsItem[]> = {};
- for (const row of results) {
- const workId = String(row.workId);
- if (!groupedData[workId]) {
- groupedData[workId] = [];
- }
- const recordDate = row.recordDate instanceof Date
- ? row.recordDate.toISOString().split('T')[0]
- : String(row.recordDate).split('T')[0];
- groupedData[workId].push({
- recordDate,
- playCount: parseInt(row.playCount) || 0,
- likeCount: parseInt(row.likeCount) || 0,
- commentCount: parseInt(row.commentCount) || 0,
- shareCount: parseInt(row.shareCount) || 0,
- collectCount: parseInt(row.collectCount) || 0,
- });
- }
- return groupedData;
- }
- /**
- * 获取每个账号在指定日期(<= 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;
- }
- /**
- * 获取数据总览
- * 返回账号列表和汇总统计数据
- */
- async getOverview(userId: number): Promise<{
- accounts: Array<{
- id: number;
- nickname: string;
- username: string;
- avatarUrl: string | null;
- platform: string;
- groupId: number | null;
- groupName?: string | 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),
- },
- relations: ['group'],
- });
- // 使用中国时区(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 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<{
- id: number;
- nickname: string;
- username: string;
- avatarUrl: string | null;
- platform: string;
- groupId: number | null;
- groupName?: string | 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 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) {
- const accountTotalViews = totalPlayMap.get(account.id) ?? 0;
- const yesterdayUds = yesterdayUdsMap.get(account.id);
- 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;
- const updateTime = (yesterdayUds?.updatedAt ?? account.updatedAt).toISOString();
- 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: accountTotalViews,
- yesterdayViews: accountYesterdayViews,
- yesterdayComments: accountYesterdayComments,
- yesterdayLikes: accountYesterdayLikes,
- yesterdayFansIncrease: accountYesterdayFansIncrease,
- updateTime,
- status: account.status,
- });
- totalViews += accountTotalViews;
- totalFans += accountFansCount;
- yesterdayViews += accountYesterdayViews;
- yesterdayComments += accountYesterdayComments;
- yesterdayLikes += accountYesterdayLikes;
- yesterdayFansIncrease += accountYesterdayFansIncrease;
- }
- return {
- accounts: accountList,
- summary: {
- totalAccounts,
- totalIncome,
- yesterdayIncome,
- totalViews,
- yesterdayViews,
- totalFans,
- yesterdayComments,
- yesterdayLikes,
- yesterdayFansIncrease,
- },
- };
- }
- /**
- * 获取平台详情数据
- * 包括汇总统计、每日汇总数据和账号列表
- */
- async getPlatformDetail(
- userId: number,
- platform: string,
- options: {
- startDate: string;
- endDate: string;
- }
- ): Promise<{
- summary: {
- totalAccounts: number;
- totalIncome: number;
- viewsCount: number;
- commentsCount: number;
- likesCount: number;
- fansIncrease: number;
- recommendationCount: number | null; // 推荐量(部分平台支持)
- };
- dailyData: Array<{
- date: string;
- income: number;
- recommendationCount: number | null;
- viewsCount: number;
- commentsCount: number;
- likesCount: number;
- fansIncrease: number;
- }>;
- accounts: Array<{
- id: number;
- nickname: string;
- username: string;
- avatarUrl: string | null;
- platform: string;
- income: number | null;
- recommendationCount: number | null;
- viewsCount: number | null;
- commentsCount: number;
- likesCount: number;
- fansIncrease: number;
- updateTime: string;
- }>;
- }> {
- const { startDate, endDate } = options;
- const startDateStr = startDate;
- const endDateStr = endDate;
- // 获取该平台的所有账号
- const accounts = await this.accountRepository.find({
- where: {
- userId,
- platform: platform as any,
- },
- relations: ['group'],
- });
- if (accounts.length === 0) {
- return {
- summary: {
- totalAccounts: 0,
- totalIncome: 0,
- viewsCount: 0,
- commentsCount: 0,
- likesCount: 0,
- fansIncrease: 0,
- recommendationCount: null,
- },
- dailyData: [],
- accounts: [],
- };
- }
- /**
- * 口径变更: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 totalComments = 0;
- let totalLikes = 0;
- let totalFansIncrease = 0;
- for (const agg of perAccountMap.values()) {
- totalViews += agg.views;
- totalComments += agg.comments;
- totalLikes += agg.likes;
- totalFansIncrease += agg.fansIncrease;
- }
- const accountList: Array<{
- id: number;
- nickname: string;
- username: string;
- avatarUrl: string | null;
- platform: string;
- income: number | null;
- recommendationCount: number | null;
- viewsCount: number | null;
- commentsCount: number;
- likesCount: number;
- fansIncrease: number;
- updateTime: string;
- }> = 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,
- nickname: account.accountName || '',
- username: account.accountId || '',
- avatarUrl: account.avatarUrl,
- platform: account.platform,
- income: null,
- recommendationCount: null,
- // 没有任何记录时,前端展示“获取失败”,避免把“无数据”误显示成 0
- viewsCount: agg.rowCount > 0 ? agg.views : null,
- commentsCount: agg.comments,
- likesCount: agg.likes,
- fansIncrease: agg.fansIncrease,
- updateTime,
- };
- });
- return {
- summary: {
- totalAccounts,
- totalIncome: 0, // 收益数据需要从其他表获取
- viewsCount: totalViews,
- commentsCount: totalComments,
- likesCount: totalLikes,
- fansIncrease: totalFansIncrease,
- recommendationCount: null, // 推荐量(部分平台支持)
- },
- dailyData,
- accounts: accountList,
- };
- }
- /**
- * 格式化更新时间为 "MM-DD HH:mm" 格式
- */
- private formatUpdateTime(date: Date): string {
- 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');
- return `${month}-${day} ${hours}:${minutes}`;
- }
- }
|