Przeglądaj źródła

新增用户统计表以及数据统计页数据

Ethanfly 1 dzień temu
rodzic
commit
16ebf2c07d

+ 15 - 1
database/schema.sql

@@ -189,7 +189,6 @@ CREATE TABLE IF NOT EXISTS work_day_statistics (
     id INT PRIMARY KEY AUTO_INCREMENT,
     work_id INT NOT NULL,
     record_date DATE NOT NULL,
-    fans_count INT DEFAULT 0 COMMENT '粉丝数(来自账号)',
     play_count INT DEFAULT 0 COMMENT '播放数',
     like_count INT DEFAULT 0 COMMENT '点赞数',
     comment_count INT DEFAULT 0 COMMENT '评论数',
@@ -201,3 +200,18 @@ CREATE TABLE IF NOT EXISTS work_day_statistics (
     INDEX idx_work_id (work_id),
     INDEX idx_record_date (record_date)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='作品每日统计数据';
+
+-- 账号每日统计表(记录每个平台账号的粉丝数和作品数,不关联作品)
+CREATE TABLE IF NOT EXISTS user_day_statistics (
+    id INT PRIMARY KEY AUTO_INCREMENT,
+    account_id INT NOT NULL COMMENT '账号ID(关联platform_accounts.id)',
+    record_date DATE NOT NULL COMMENT '记录日期',
+    fans_count INT DEFAULT 0 COMMENT '粉丝数',
+    works_count INT DEFAULT 0 COMMENT '作品数',
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_account_date (account_id, record_date),
+    INDEX idx_account_id (account_id),
+    INDEX idx_record_date (record_date),
+    FOREIGN KEY (account_id) REFERENCES platform_accounts(id) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='账号每日统计数据(按平台账号统计)';

BIN
server/python/platforms/__pycache__/baijiahao.cpython-311.pyc


BIN
server/python/platforms/__pycache__/xiaohongshu.cpython-311.pyc


+ 26 - 0
server/src/models/entities/UserDayStatistics.ts

@@ -0,0 +1,26 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
+
+@Entity('user_day_statistics')
+@Index(['accountId', 'recordDate'], { unique: true })
+export class UserDayStatistics {
+  @PrimaryGeneratedColumn()
+  id!: number;
+
+  @Column({ name: 'account_id', type: 'int' })
+  accountId!: number;
+
+  @Column({ name: 'record_date', type: 'date' })
+  recordDate!: Date;
+
+  @Column({ name: 'fans_count', type: 'int', default: 0, comment: '粉丝数' })
+  fansCount!: number;
+
+  @Column({ name: 'works_count', type: 'int', default: 0, comment: '作品数' })
+  worksCount!: number;
+
+  @CreateDateColumn({ name: 'created_at' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at' })
+  updatedAt!: Date;
+}

+ 0 - 3
server/src/models/entities/WorkDayStatistics.ts

@@ -12,9 +12,6 @@ export class WorkDayStatistics {
   @Column({ name: 'record_date', type: 'date' })
   recordDate!: Date;
 
-  @Column({ name: 'fans_count', type: 'int', default: 0, comment: '粉丝数(来自账号)' })
-  fansCount!: number;
-
   @Column({ name: 'play_count', type: 'int', default: 0, comment: '播放数' })
   playCount!: number;
 

+ 3 - 0
server/src/models/index.ts

@@ -12,6 +12,7 @@ import { AnalyticsData } from './entities/AnalyticsData.js';
 import { OperationLog } from './entities/OperationLog.js';
 import { Work } from './entities/Work.js';
 import { WorkDayStatistics } from './entities/WorkDayStatistics.js';
+import { UserDayStatistics } from './entities/UserDayStatistics.js';
 
 export const AppDataSource = new DataSource({
   type: 'mysql',
@@ -35,6 +36,7 @@ export const AppDataSource = new DataSource({
     OperationLog,
     Work,
     WorkDayStatistics,
+    UserDayStatistics,
   ],
   charset: 'utf8mb4',
 });
@@ -59,4 +61,5 @@ export {
   OperationLog,
   Work,
   WorkDayStatistics,
+  UserDayStatistics,
 };

+ 18 - 0
server/src/services/AccountService.ts

@@ -17,6 +17,7 @@ import { CookieManager } from '../automation/cookie.js';
 import { logger } from '../utils/logger.js';
 import { headlessBrowserService } from './HeadlessBrowserService.js';
 import { aiService } from '../ai/index.js';
+import { UserDayStatisticsService } from './UserDayStatisticsService.js';
 
 interface GetAccountsParams {
   platform?: string;
@@ -488,6 +489,23 @@ export class AccountService {
 
     const updated = await this.accountRepository.findOne({ where: { id: accountId } });
 
+    // 保存账号每日统计数据(粉丝数、作品数)
+    // 无论是否更新了粉丝数/作品数,都要保存当前值到统计表,确保每天都有记录
+    if (updated) {
+      try {
+        const userDayStatisticsService = new UserDayStatisticsService();
+        await userDayStatisticsService.saveStatistics({
+          accountId,
+          fansCount: updated.fansCount || 0,
+          worksCount: updated.worksCount || 0,
+        });
+        logger.debug(`[AccountService] Saved account day statistics for account ${accountId} (fans: ${updated.fansCount || 0}, works: ${updated.worksCount || 0})`);
+      } catch (error) {
+        logger.error(`[AccountService] Failed to save account day statistics for account ${accountId}:`, error);
+        // 不抛出错误,不影响主流程
+      }
+    }
+
     // 通知其他客户端
     wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_UPDATED, { account: this.formatAccount(updated!) });
 

+ 107 - 0
server/src/services/UserDayStatisticsService.ts

@@ -0,0 +1,107 @@
+import { AppDataSource, UserDayStatistics } from '../models/index.js';
+import { logger } from '../utils/logger.js';
+
+export interface UserDayStatisticsItem {
+  accountId: number;
+  fansCount: number;
+  worksCount: number;
+}
+
+export interface SaveResult {
+  inserted: number;
+  updated: number;
+}
+
+export class UserDayStatisticsService {
+  private statisticsRepository = AppDataSource.getRepository(UserDayStatistics);
+
+  /**
+   * 保存用户每日统计数据
+   * 同日更新,隔日新增
+   */
+  async saveStatistics(item: UserDayStatisticsItem): Promise<SaveResult> {
+    // 使用中国时区(UTC+8)计算今天的业务日期
+    const now = new Date();
+    const chinaNow = new Date(now.getTime() + 8 * 60 * 60 * 1000);
+    const today = new Date(chinaNow);
+    today.setHours(0, 0, 0, 0);
+
+    // 检查今天是否已有记录
+    const existing = await this.statisticsRepository.findOne({
+      where: {
+        accountId: item.accountId,
+        recordDate: today,
+      },
+    });
+
+    if (existing) {
+      // 更新已有记录
+      await this.statisticsRepository.update(existing.id, {
+        fansCount: item.fansCount ?? existing.fansCount,
+        worksCount: item.worksCount ?? existing.worksCount,
+      });
+      logger.debug(`[UserDayStatistics] Updated record for account ${item.accountId}, date: ${today.toISOString().split('T')[0]}`);
+      return { inserted: 0, updated: 1 };
+    } else {
+      // 插入新记录
+      const newStat = this.statisticsRepository.create({
+        accountId: item.accountId,
+        recordDate: today,
+        fansCount: item.fansCount ?? 0,
+        worksCount: item.worksCount ?? 0,
+      });
+      await this.statisticsRepository.save(newStat);
+      logger.debug(`[UserDayStatistics] Inserted new record for account ${item.accountId}, date: ${today.toISOString().split('T')[0]}`);
+      return { inserted: 1, updated: 0 };
+    }
+  }
+
+  /**
+   * 批量保存用户每日统计数据
+   */
+  async saveStatisticsBatch(items: UserDayStatisticsItem[]): Promise<SaveResult> {
+    let insertedCount = 0;
+    let updatedCount = 0;
+
+    for (const item of items) {
+      const result = await this.saveStatistics(item);
+      insertedCount += result.inserted;
+      updatedCount += result.updated;
+    }
+
+    logger.info(`[UserDayStatistics] Batch save completed: inserted=${insertedCount}, updated=${updatedCount}`);
+    return { inserted: insertedCount, updated: updatedCount };
+  }
+
+  /**
+   * 获取账号指定日期的统计数据
+   */
+  async getStatisticsByDate(
+    accountId: number,
+    date: Date
+  ): Promise<UserDayStatistics | null> {
+    return await this.statisticsRepository.findOne({
+      where: {
+        accountId,
+        recordDate: date,
+      },
+    });
+  }
+
+  /**
+   * 获取账号指定日期范围的统计数据
+   */
+  async getStatisticsByDateRange(
+    accountId: number,
+    startDate: Date,
+    endDate: Date
+  ): Promise<UserDayStatistics[]> {
+    return await this.statisticsRepository
+      .createQueryBuilder('uds')
+      .where('uds.account_id = :accountId', { accountId })
+      .andWhere('uds.record_date >= :startDate', { startDate })
+      .andWhere('uds.record_date <= :endDate', { endDate })
+      .orderBy('uds.record_date', 'ASC')
+      .getMany();
+  }
+}

+ 136 - 65
server/src/services/WorkDayStatisticsService.ts

@@ -1,10 +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 { logger } from '../utils/logger.js';
 
 interface StatisticsItem {
   workId: number;
-  fansCount?: number;
   playCount?: number;
   likeCount?: number;
   commentCount?: number;
@@ -39,7 +38,6 @@ interface PlatformStatItem {
 
 interface WorkStatisticsItem {
   recordDate: string;
-  fansCount: number;
   playCount: number;
   likeCount: number;
   commentCount: number;
@@ -51,6 +49,7 @@ export class WorkDayStatisticsService {
   private statisticsRepository = AppDataSource.getRepository(WorkDayStatistics);
   private workRepository = AppDataSource.getRepository(Work);
   private accountRepository = AppDataSource.getRepository(PlatformAccount);
+  private userDayStatisticsRepository = AppDataSource.getRepository(UserDayStatistics);
 
   /**
    * 保存作品日统计数据
@@ -75,9 +74,8 @@ export class WorkDayStatisticsService {
       });
 
       if (existing) {
-        // 更新已有记录
+        // 更新已有记录(不再包含粉丝数,粉丝数从 user_day_statistics 表获取)
         await this.statisticsRepository.update(existing.id, {
-          fansCount: stat.fansCount ?? existing.fansCount,
           playCount: stat.playCount ?? existing.playCount,
           likeCount: stat.likeCount ?? existing.likeCount,
           commentCount: stat.commentCount ?? existing.commentCount,
@@ -86,11 +84,10 @@ export class WorkDayStatisticsService {
         });
         updatedCount++;
       } else {
-        // 插入新记录
+        // 插入新记录(不再包含粉丝数,粉丝数从 user_day_statistics 表获取)
         const newStat = this.statisticsRepository.create({
           workId: stat.workId,
           recordDate: today,
-          fansCount: stat.fansCount ?? 0,
           playCount: stat.playCount ?? 0,
           likeCount: stat.likeCount ?? 0,
           commentCount: stat.commentCount ?? 0,
@@ -132,13 +129,12 @@ export class WorkDayStatisticsService {
       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('MAX(wds.fans_count)', 'accountFans')
       .addSelect('SUM(wds.play_count)', 'accountViews')
       .addSelect('SUM(wds.like_count)', 'accountLikes')
       .addSelect('SUM(wds.comment_count)', 'accountComments')
@@ -270,8 +266,7 @@ export class WorkDayStatisticsService {
       const firstDayQuery = this.statisticsRepository
         .createQueryBuilder('wds')
         .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.comment_count)', 'comments')
         .addSelect('SUM(wds.collect_count)', 'collects')
@@ -285,8 +280,7 @@ export class WorkDayStatisticsService {
       const lastDayQuery = this.statisticsRepository
         .createQueryBuilder('wds')
         .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.comment_count)', 'comments')
         .addSelect('SUM(wds.collect_count)', 'collects')
@@ -302,8 +296,26 @@ export class WorkDayStatisticsService {
         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 viewsIncrease = (parseInt(lastDay?.views) || 0) - (parseInt(firstDay?.views) || 0);
@@ -344,7 +356,6 @@ export class WorkDayStatisticsService {
       .createQueryBuilder('wds')
       .select('wds.work_id', 'workId')
       .addSelect('wds.record_date', 'recordDate')
-      .addSelect('wds.fans_count', 'fansCount')
       .addSelect('wds.play_count', 'playCount')
       .addSelect('wds.like_count', 'likeCount')
       .addSelect('wds.comment_count', 'commentCount')
@@ -378,7 +389,6 @@ export class WorkDayStatisticsService {
 
       groupedData[workId].push({
         recordDate,
-        fansCount: parseInt(row.fansCount) || 0,
         playCount: parseInt(row.playCount) || 0,
         likeCount: parseInt(row.likeCount) || 0,
         commentCount: parseInt(row.commentCount) || 0,
@@ -487,7 +497,55 @@ export class WorkDayStatisticsService {
 
       if (works.length === 0) {
         // 如果没有作品,只返回账号基本信息
-        const accountFansCount = account.fansCount || 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 || '',
@@ -502,7 +560,7 @@ export class WorkDayStatisticsService {
           yesterdayViews: null,
           yesterdayComments: 0,
           yesterdayLikes: 0,
-          yesterdayFansIncrease: 0,
+          yesterdayFansIncrease: accountYesterdayFansIncrease,
           updateTime: account.updatedAt.toISOString(),
           status: account.status,
         });
@@ -510,12 +568,13 @@ export class WorkDayStatisticsService {
         // 即使没有作品,也要累加账号的粉丝数到总粉丝数
         totalAccounts++;
         totalFans += accountFansCount;
+        yesterdayFansIncrease += accountYesterdayFansIncrease;
         continue;
       }
 
       const workIds = works.map(w => w.id);
 
-      // 获取每个作品的最新日期统计数据(总播放量等)
+      // 获取每个作品的最新日期统计数据(总播放量等,不再包含粉丝数
       const latestStatsQuery = this.statisticsRepository
         .createQueryBuilder('wds')
         .select('wds.work_id', 'workId')
@@ -523,7 +582,6 @@ export class WorkDayStatisticsService {
         .addSelect('MAX(wds.play_count)', 'playCount')
         .addSelect('MAX(wds.like_count)', 'likeCount')
         .addSelect('MAX(wds.comment_count)', 'commentCount')
-        .addSelect('MAX(wds.fans_count)', 'fansCount')
         .where('wds.work_id IN (:...workIds)', { workIds })
         .groupBy('wds.work_id');
 
@@ -537,15 +595,13 @@ export class WorkDayStatisticsService {
         latestDateMap.set(stat.workId, stat.latestDate);
       }
 
-      // 获取昨天和今天的数据来计算增量
-      // 使用日期字符串直接比较(DATE 类型会自动转换)
+      // 获取昨天和今天的数据来计算增量(不再包含粉丝数,粉丝数从 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')
-        .addSelect('MAX(wds.fans_count)', 'fansCount')
         .where('wds.work_id IN (:...workIds)', { workIds })
         .andWhere('wds.record_date = :yesterday', { yesterday: yesterdayStr })
         .groupBy('wds.work_id');
@@ -556,7 +612,6 @@ export class WorkDayStatisticsService {
         .addSelect('SUM(wds.play_count)', 'playCount')
         .addSelect('SUM(wds.like_count)', 'likeCount')
         .addSelect('SUM(wds.comment_count)', 'commentCount')
-        .addSelect('MAX(wds.fans_count)', 'fansCount')
         .where('wds.work_id IN (:...workIds)', { workIds })
         .andWhere('wds.record_date = :today', { today: todayStr })
         .groupBy('wds.work_id');
@@ -579,9 +634,9 @@ export class WorkDayStatisticsService {
       let accountYesterdayLikes = 0;
       let accountYesterdayFansIncrease = 0;
 
-      // 按作品ID汇总
-      const yesterdayMap = new Map<number, { play: number; like: number; comment: number; fans: number }>();
-      const todayMap = new Map<number, { play: number; like: number; comment: number; fans: number }>();
+      // 按作品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;
@@ -589,7 +644,6 @@ export class WorkDayStatisticsService {
           play: Number(stat.playCount) || 0,
           like: Number(stat.likeCount) || 0,
           comment: Number(stat.commentCount) || 0,
-          fans: Number(stat.fansCount) || 0,
         });
       }
 
@@ -599,16 +653,15 @@ export class WorkDayStatisticsService {
           play: Number(stat.playCount) || 0,
           like: Number(stat.likeCount) || 0,
           comment: Number(stat.commentCount) || 0,
-          fans: Number(stat.fansCount) || 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, fans: 0 };
-        const yesterdayData = yesterdayMap.get(workId) || { play: 0, like: 0, comment: 0, fans: 0 };
+        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;
@@ -621,46 +674,64 @@ export class WorkDayStatisticsService {
       
       logger.info(`[WorkDayStatistics] Account ${account.id} - Calculated: views=${accountYesterdayViews}, comments=${accountYesterdayComments}, likes=${accountYesterdayLikes}`);
 
-      // 获取账号的最新粉丝数(从最新日期的统计数据中取最大值)
-      let accountFansCount = account.fansCount || 0;
-      if (latestStats.length > 0) {
-        const maxFans = Math.max(...latestStats.map(s => parseInt(s.fansCount) || 0));
-        if (maxFans > 0) {
-          accountFansCount = maxFans;
-        }
+      // 从 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;
       }
-
-      // 计算昨日涨粉(今天最新粉丝数 - 昨天最新粉丝数)
-      // 如果今天有统计数据,用今天数据中的最大粉丝数;否则用账号表的当前粉丝数
-      const todayMaxFans = todayStats.length > 0
-        ? Math.max(...todayStats.map(s => parseInt(s.fansCount) || 0))
-        : accountFansCount;
       
-      // 如果昨天有统计数据,用昨天数据中的最大粉丝数;否则需要找最近一天的数据作为基准
-      let yesterdayMaxFans: number;
-      if (yesterdayStats.length > 0) {
-        // 昨天有数据,直接用昨天的最大粉丝数
-        yesterdayMaxFans = Math.max(...yesterdayStats.map(s => parseInt(s.fansCount) || 0));
+      // 计算昨日涨粉(今天粉丝数 - 昨天粉丝数)
+      let yesterdayFans: number;
+      if (yesterdayUserStat) {
+        // 昨天有数据,直接使用
+        yesterdayFans = yesterdayUserStat.fansCount || 0;
+        logger.debug(`[WorkDayStatistics] Account ${account.id} - yesterday has data: ${yesterdayFans}, today: ${accountFansCount}`);
       } else {
-        // 昨天没有数据,需要找最近一天的数据作为基准
-        // 查询该账号最近一天(早于今天)的统计数据
-        const recentStatsQuery = this.statisticsRepository
-          .createQueryBuilder('wds')
-          .select('MAX(wds.fans_count)', 'fansCount')
-          .where('wds.work_id IN (:...workIds)', { workIds })
-          .andWhere('wds.record_date < :today', { today: todayStr });
+        // 昨天没有数据,查找最近一天(早于今天)的数据作为基准
+        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();
         
-        const recentStat = await recentStatsQuery.getRawOne();
-        if (recentStat && recentStat.fansCount) {
-          // 找到了最近一天的数据,用它作为基准
-          yesterdayMaxFans = parseInt(recentStat.fansCount) || accountFansCount;
+        if (recentUserStat) {
+          yesterdayFans = recentUserStat.fansCount || 0;
+          logger.debug(`[WorkDayStatistics] Account ${account.id} - using recent data: ${yesterdayFans}, today: ${accountFansCount}`);
         } else {
-          // 完全没有历史数据,用账号表的当前粉丝数作为基准(但这样计算出来的增量可能不准确)
-          yesterdayMaxFans = accountFansCount;
+          // 完全没有历史数据,用账号表的当前粉丝数作为基准(涨粉为 0)
+          yesterdayFans = accountFansCount;
+          logger.debug(`[WorkDayStatistics] Account ${account.id} - no history data, using current: ${accountFansCount}`);
         }
       }
       
-      accountYesterdayFansIncrease = todayMaxFans - yesterdayMaxFans;
+      // 计算涨粉数(今天 - 昨天),确保不为负数
+      accountYesterdayFansIncrease = Math.max(0, accountFansCount - yesterdayFans);
+      logger.info(`[WorkDayStatistics] Account ${account.id} - fans increase: ${accountFansCount} - ${yesterdayFans} = ${accountYesterdayFansIncrease}`);
 
       accountList.push({
         id: account.id,

+ 1 - 2
server/src/services/WorkService.ts

@@ -275,10 +275,9 @@ export class WorkService {
       return;
     }
 
-    // 构建统计数据列表
+    // 构建统计数据列表(不再包含粉丝数,粉丝数从 user_day_statistics 表获取)
     const statisticsList = works.map(work => ({
       workId: work.id,
-      fansCount: account.fansCount || 0,
       playCount: work.playCount || 0,
       likeCount: work.likeCount || 0,
       commentCount: work.commentCount || 0,