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

## 概述

本文档详细说明平台数据详情页（`/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 获取账号列表

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

### 2.2 遍历每个账号计算数据

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

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

```typescript
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 逻辑**:
```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
```

**计算增量**:
```typescript
// 获取结束日期和开始日期的累计数据
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: 计算粉丝增量

```typescript
// 获取结束日期的粉丝数（<= 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);
```

**关键点**:
- 如果开始日期和结束日期相同，增量 = 当日粉丝数 - 前一日粉丝数
- 如果没有统计数据，`endUserStat` 或 `startUserStat` 为 `null`，使用 `|| 0` 默认值
- 如果开始日期和结束日期都没有数据，增量 = 0 - 0 = 0

#### 步骤 4: 获取更新时间

```typescript
// 获取时间范围内的最新更新时间
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: 组装账号数据

```typescript
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 计算每日汇总数据

```typescript
// 遍历日期范围内的每一天
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 计算汇总统计

```typescript
// 累加所有账号的数据
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. 检查账号是否有作品

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

### 2. 检查作品统计数据

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

### 3. 检查账号统计数据

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

### 4. 检查特定日期的数据

```sql
-- 检查作品统计数据
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 的逻辑

**当前逻辑**:
```typescript
viewsCount: accountViews > 0 ? accountViews : null,
```

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

### 2. 允许显示负数（如果需要）

**当前逻辑**:
```typescript
fansIncrease: Math.max(0, accountFansIncrease),
```

**建议**:
```typescript
// 如果需要显示掉粉（负数），可以改为：
fansIncrease: accountFansIncrease, // 允许负数
```

### 3. 改进 updateTime 的逻辑

**当前逻辑**:
```typescript
const updateTime = latestUserStat?.updatedAt 
  ? this.formatUpdateTime(latestUserStat.updatedAt)
  : '';
```

**建议**:
```typescript
// 如果没有统计数据，可以使用账号表的更新时间
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
