platform-detail-data-calculation-logic.md 12 KB

平台数据详情页数据计算逻辑详解

概述

本文档详细说明平台数据详情页(/api/work-day-statistics/platform-detail)中各项数据的计算逻辑,帮助理解为什么某些数据可能显示为 0 或 null。

数据计算流程

1. 接口入口

文件: server/src/routes/workDayStatistics.ts

路由: GET /api/work-day-statistics/platform-detail

参数:

  • platform: 平台类型(必填)
  • startDate: 开始日期(必填,格式:YYYY-MM-DD)
  • endDate: 结束日期(必填,格式:YYYY-MM-DD)

2. 服务方法

文件: server/src/services/WorkDayStatisticsService.ts

方法: getPlatformDetail(userId, platform, { startDate, endDate })

详细计算逻辑

2.1 获取账号列表

// 获取该平台的所有账号
const accounts = await this.accountRepository.find({
  where: {
    userId,
    platform: platform as any,
  },
  relations: ['group'],
});

2.2 遍历每个账号计算数据

对每个账号执行以下步骤:

步骤 1: 获取账号的所有作品

const works = await this.workRepository.find({
  where: { accountId: account.id },
  select: ['id'],
});
const workIds = works.map(w => w.id);

注意: 如果账号没有作品,workIds 为空数组,后续所有统计数据都会是 0。

步骤 2: 计算账号在时间范围内的增量数据

核心方法: getWorkSumsAtDate(workIds, targetDate)

作用: 获取指定日期(<= targetDate)时,该账号所有作品的累计数据总和

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 (...)
    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 [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;

关键点:

  • 如果开始日期和结束日期相同,增量 = 当日累计值 - 前一日累计值
  • 如果账号没有作品,workIds 为空,getWorkSumsAtDate 返回 { views: 0, likes: 0, comments: 0, collects: 0 }
  • 如果账号没有统计数据,getWorkSumsAtDate 也会返回 0

步骤 3: 计算粉丝增量

// 获取结束日期的粉丝数(<= endDate 的最近一条记录)
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();

// 获取开始日期的粉丝数(<= startDate 的最近一条记录)
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);

关键点:

  • 如果开始日期和结束日期相同,增量 = 当日粉丝数 - 前一日粉丝数
  • 如果没有统计数据,endUserStatstartUserStatnull,使用 || 0 默认值
  • 如果开始日期和结束日期都没有数据,增量 = 0 - 0 = 0

步骤 4: 获取更新时间

// 获取时间范围内的最新更新时间
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)
  : '';

关键点:

  • 只有在时间范围内有 user_day_statistics 记录时才有值
  • 如果没有记录,返回空字符串 ""

步骤 5: 组装账号数据

accountList.push({
  id: account.id,
  nickname: account.accountName || '',
  username: account.accountId || '',
  avatarUrl: account.avatarUrl,
  platform: account.platform,
  income: null, // 收益数据需要从其他表获取
  recommendationCount: null, // 推荐量(部分平台支持)
  viewsCount: accountViews > 0 ? accountViews : null,  // ⚠️ 关键:只有 > 0 才返回,否则为 null
  commentsCount: Math.max(0, accountComments),        // ⚠️ 关键:负数会被截断为 0
  likesCount: Math.max(0, accountLikes),              // ⚠️ 关键:负数会被截断为 0
  fansIncrease: Math.max(0, accountFansIncrease),     // ⚠️ 关键:负数会被截断为 0
  updateTime,
});

关键逻辑:

  1. viewsCount:

    • 如果 accountViews > 0,返回实际值
    • 如果 accountViews <= 0,返回 null
    • 这意味着如果增量为 0 或负数,前端会显示为 null
  2. commentsCount / likesCount / fansIncrease:

    • 使用 Math.max(0, value) 确保不为负数
    • 如果计算结果为负数,会被截断为 0
    • 这意味着如果数据减少(比如掉粉),会显示为 0 而不是负数

2.3 计算每日汇总数据

// 遍历日期范围内的每一天
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);
}

关键点:

  • 每日增量 = 当日累计值 - 前一日累计值
  • 如果前一日没有数据,前一日累计值 = 0
  • 负数会被 Math.max(0, ...) 截断为 0

2.4 计算汇总统计

// 累加所有账号的数据
totalAccounts++;
totalViews += Math.max(0, accountViews);
totalComments += Math.max(0, accountComments);
totalLikes += Math.max(0, accountLikes);
totalFansIncrease += Math.max(0, accountFansIncrease);

数据为 0 或 null 的原因分析

情况 1: viewsCount 为 null

原因:

  • accountViews <= 0 时返回 null
  • 可能的情况:
    1. 账号没有作品(workIds 为空)
    2. 账号有作品但没有统计数据
    3. 开始日期和结束日期的累计播放量相同(增量为 0)
    4. 播放量减少(增量为负数)

解决方案:

  • 检查账号是否有作品
  • 检查 work_day_statistics 表中是否有该账号作品的数据
  • 检查时间范围是否合理

情况 2: commentsCount / likesCount / fansIncrease 为 0

原因:

  • 计算结果为 0 或负数时,使用 Math.max(0, value) 截断为 0
  • 可能的情况:
    1. 账号没有作品或统计数据
    2. 开始日期和结束日期的累计值相同(增量为 0)
    3. 数据减少(增量为负数,被截断为 0)

解决方案:

  • 检查账号是否有作品
  • 检查统计数据是否存在
  • 检查时间范围是否合理
  • 如果需要显示负数(如掉粉),需要修改逻辑

情况 3: updateTime 为空字符串

原因:

  • 时间范围内没有 user_day_statistics 记录

解决方案:

  • 检查账号是否有统计数据
  • 检查时间范围是否合理
  • 可能需要触发数据同步

情况 4: income / recommendationCount 为 null

原因:

  • 这些字段目前未实现,固定返回 null

解决方案:

  • 需要实现收益和推荐量的数据获取逻辑

数据依赖关系

数据来源表

  1. 作品统计数据: work_day_statistics

    • 字段: work_id, record_date, play_count, like_count, comment_count, collect_count
    • 用途: 计算播放量、点赞量、评论量增量
  2. 账号统计数据: user_day_statistics

    • 字段: account_id, record_date, fans_count, updated_at
    • 用途: 计算粉丝增量、获取更新时间
  3. 账号表: platform_accounts

    • 字段: id, account_name, account_id, avatar_url, platform
    • 用途: 获取账号基本信息
  4. 作品表: works

    • 字段: id, account_id
    • 用途: 获取账号下的所有作品ID

数据计算依赖链

账号 (platform_accounts)
  ↓
作品 (works) → 作品ID列表 (workIds)
  ↓
作品统计数据 (work_day_statistics) → 累计数据 (getWorkSumsAtDate)
  ↓
增量计算 (endSums - startSums) → 账号数据 (viewsCount, commentsCount, likesCount)
  ↓
账号统计数据 (user_day_statistics) → 粉丝增量 (fansIncrease)

调试建议

1. 检查账号是否有作品

SELECT COUNT(*) FROM works WHERE account_id = ?;

2. 检查作品统计数据

SELECT COUNT(*) FROM work_day_statistics wds
INNER JOIN works w ON wds.work_id = w.id
WHERE w.account_id = ?;

3. 检查账号统计数据

SELECT COUNT(*) FROM user_day_statistics 
WHERE account_id = ?;

4. 检查特定日期的数据

-- 检查作品统计数据
SELECT * FROM work_day_statistics wds
INNER JOIN works w ON wds.work_id = w.id
WHERE w.account_id = ? 
  AND DATE(wds.record_date) = '2026-01-27';

-- 检查账号统计数据
SELECT * FROM user_day_statistics 
WHERE account_id = ? 
  AND DATE(record_date) = '2026-01-27';

修复建议

1. 改进 viewsCount 的逻辑

当前逻辑:

viewsCount: accountViews > 0 ? accountViews : null,

建议:

// 如果账号有作品,即使增量为 0 也显示 0,而不是 null
viewsCount: workIds.length > 0 ? (accountViews > 0 ? accountViews : 0) : null,

2. 允许显示负数(如果需要)

当前逻辑:

fansIncrease: Math.max(0, accountFansIncrease),

建议:

// 如果需要显示掉粉(负数),可以改为:
fansIncrease: accountFansIncrease, // 允许负数

3. 改进 updateTime 的逻辑

当前逻辑:

const updateTime = latestUserStat?.updatedAt 
  ? this.formatUpdateTime(latestUserStat.updatedAt)
  : '';

建议:

// 如果没有统计数据,可以使用账号表的更新时间
const updateTime = latestUserStat?.updatedAt 
  ? this.formatUpdateTime(latestUserStat.updatedAt)
  : (account.updatedAt ? this.formatUpdateTime(account.updatedAt) : '');

相关文件

  • server/src/services/WorkDayStatisticsService.ts - 数据计算逻辑
  • server/src/routes/workDayStatistics.ts - API 路由
  • client/src/views/Analytics/PlatformDetail/index.vue - 前端展示

更新日期

2026-01-28