Ethanfly 2 dias atrás
pai
commit
c8d7fd13d0

+ 6 - 0
client/src/components.d.ts

@@ -15,10 +15,14 @@ declare module 'vue' {
     ElBadge: typeof import('element-plus/es')['ElBadge']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
+    ElCascader: typeof import('element-plus/es')['ElCascader']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
     ElContainer: typeof import('element-plus/es')['ElContainer']
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
+    ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
+    ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDivider: typeof import('element-plus/es')['ElDivider']
     ElDrawer: typeof import('element-plus/es')['ElDrawer']
@@ -46,6 +50,8 @@ declare module 'vue' {
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
+    ElText: typeof import('element-plus/es')['ElText']
+    ElUpload: typeof import('element-plus/es')['ElUpload']
     Icons: typeof import('./components/icons/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']

+ 65 - 1
server/src/services/AccountService.ts

@@ -1,4 +1,4 @@
-import { AppDataSource, PlatformAccount, AccountGroup } from '../models/index.js';
+import { AppDataSource, PlatformAccount, AccountGroup } from '../models/index.js';
 import { AppError } from '../middleware/error.js';
 import { ERROR_CODES, HTTP_STATUS } from '@media-manager/shared';
 import type {
@@ -245,6 +245,8 @@ export class AccountService {
 
     if (existing) {
       // 更新已存在的账号
+      const wasExpired = existing.status === 'expired';
+
       await this.accountRepository.update(existing.id, {
         cookieData: cookieData, // 存储原始数据(可能是加密的)
         accountName: accountInfo.accountName,
@@ -264,6 +266,16 @@ export class AccountService {
         logger.warn(`[addAccount] Background refresh failed for existing account ${existing.id}:`, err);
       });
 
+      // 账号从 expired 恢复为 active:异步补齐最近 30 天用户每日数据
+      if (wasExpired && updated && updated.status === 'active') {
+        this.backfillDailyStatisticsForReactivatedAccountAsync(updated).catch(err => {
+          logger.warn(
+            `[addAccount] Daily statistics backfill failed for reactivated account ${updated.id}:`,
+            err
+          );
+        });
+      }
+
       return this.formatAccount(updated!);
     }
 
@@ -363,6 +375,46 @@ export class AccountService {
     }
   }
 
+  /**
+   * 账号从失效(expired)恢复为 active 时,为该账号补齐最近 30 天的用户每日数据(user_day_statistics)
+   * 注意:仅在状态从 expired → active 的切换时触发,且按平台调用对应的单账号导入入口
+   */
+  private async backfillDailyStatisticsForReactivatedAccountAsync(
+    account: PlatformAccount
+  ): Promise<void> {
+    const platform = account.platform as PlatformType;
+
+    // 延迟几秒,避免与前端后续操作/其他浏览器任务抢占资源
+    await new Promise((resolve) => setTimeout(resolve, 3000));
+
+    try {
+      if (platform === 'xiaohongshu') {
+        await XiaohongshuAccountOverviewImportService.runDailyImportForAccount(account.id);
+      } else if (platform === 'douyin') {
+        await DouyinAccountOverviewImportService.runDailyImportForAccount(account.id);
+      } else if (platform === 'baijiahao') {
+        await BaijiahaoContentOverviewImportService.runDailyImportForAccount(account.id);
+      } else if (platform === 'weixin_video') {
+        await WeixinVideoDataCenterImportService.runDailyImportForAccount(account.id);
+      } else {
+        logger.info(
+          `[AccountService] Skip daily statistics backfill for unsupported platform ${platform} (accountId=${account.id})`
+        );
+        return;
+      }
+
+      logger.info(
+        `[AccountService] Completed daily statistics backfill for reactivated account ${account.id} (${platform})`
+      );
+    } catch (error) {
+      logger.warn(
+        `[AccountService] Daily statistics backfill failed for reactivated account ${account.id} (${platform}):`,
+        error
+      );
+      // 出错不影响账号激活本身
+    }
+  }
+
   async updateAccount(
     userId: number,
     accountId: number,
@@ -411,6 +463,7 @@ export class AccountService {
     }
 
     const platform = account.platform as PlatformType;
+    const wasExpired = account.status === 'expired';
     const updateData: Partial<PlatformAccount> = {
       updatedAt: new Date(),
     };
@@ -597,6 +650,17 @@ export class AccountService {
 
     const updated = await this.accountRepository.findOne({ where: { id: accountId } });
 
+    // 账号从 expired 恢复为 active:异步补齐最近 30 天用户每日数据
+    const newStatus = updated?.status ?? account.status;
+    if (wasExpired && newStatus === 'active' && updated) {
+      this.backfillDailyStatisticsForReactivatedAccountAsync(updated).catch((error) => {
+        logger.warn(
+          `[AccountService] Daily statistics backfill failed after refresh for reactivated account ${updated.id}:`,
+          error
+        );
+      });
+    }
+
     // 刷新账号时不再写入 user_day_statistics,仅保留:每天定时任务 + 添加账号时的 initStatisticsForNewAccountAsync
     // if (updated) {
     //   try {

+ 14 - 0
server/src/services/BaijiahaoContentOverviewImportService.ts

@@ -535,6 +535,20 @@ export class BaijiahaoContentOverviewImportService {
   }
 
   /**
+   * 单账号入口:仅为指定百家号账号执行近30天「内容分析-基础数据」+粉丝数据导入(用于账号从失效恢复为 active 时补数)
+   */
+  static async runDailyImportForAccount(accountId: number): Promise<void> {
+    const svc = new BaijiahaoContentOverviewImportService();
+    const account = await svc.accountRepository.findOne({
+      where: { id: accountId, platform: 'baijiahao' as any },
+    });
+    if (!account) {
+      throw new Error(`未找到百家号账号 id=${accountId}`);
+    }
+    await svc.importAccountLast30Days(account);
+  }
+
+  /**
    * 为所有百家号账号导出“数据中心-内容分析-基础数据-近30天”并导入 user_day_statistics
    */
   async runDailyImportForAllBaijiahaoAccounts(): Promise<void> {

+ 14 - 0
server/src/services/DouyinAccountOverviewImportService.ts

@@ -266,6 +266,20 @@ export class DouyinAccountOverviewImportService {
   }
 
   /**
+   * 单账号入口:仅为指定抖音账号执行近30天账号总览导入(用于账号从失效恢复为 active 时补数)
+   */
+  static async runDailyImportForAccount(accountId: number): Promise<void> {
+    const svc = new DouyinAccountOverviewImportService();
+    const account = await svc.accountRepository.findOne({
+      where: { id: accountId, platform: 'douyin' as any },
+    });
+    if (!account) {
+      throw new Error(`未找到抖音账号 id=${accountId}`);
+    }
+    await svc.importAccountLast30Days(account);
+  }
+
+  /**
    * 为所有抖音账号导出“账号总览-短视频-数据表现-近30天”并导入 user_day_statistics
    */
   async runDailyImportForAllDouyinAccounts(): Promise<void> {

+ 14 - 0
server/src/services/WeixinVideoDataCenterImportService.ts

@@ -589,6 +589,20 @@ export class WeixinVideoDataCenterImportService {
     await svc.runDailyImportForAllWeixinVideoAccounts();
   }
 
+  /**
+   * 单账号入口:仅为指定视频号账号执行近30天「数据中心-各子菜单-增长详情」导入(用于账号从失效恢复为 active 时补数)
+   */
+  static async runDailyImportForAccount(accountId: number): Promise<void> {
+    const svc = new WeixinVideoDataCenterImportService();
+    const account = await svc.accountRepository.findOne({
+      where: { id: accountId, platform: 'weixin_video' as any },
+    });
+    if (!account) {
+      throw new Error(`未找到视频号账号 id=${accountId}`);
+    }
+    await svc.importAccountLast30Days(account);
+  }
+
   async runDailyImportForAllWeixinVideoAccounts(): Promise<void> {
     await ensureDir(this.downloadDir);
     const accounts = await this.accountRepository.find({ where: { platform: 'weixin_video' as any } });

+ 28 - 0
server/src/services/XiaohongshuAccountOverviewImportService.ts

@@ -337,6 +337,34 @@ export class XiaohongshuAccountOverviewImportService {
   }
 
   /**
+   * 单账号入口:仅为指定账号执行近30日账号概览导入(用于账号从失效恢复为 active 时补数)
+   */
+  static async runDailyImportForAccount(accountId: number): Promise<void> {
+    const svc = new XiaohongshuAccountOverviewImportService();
+    const account = await svc.accountRepository.findOne({
+      where: { id: accountId, platform: 'xiaohongshu' as any },
+    });
+    if (!account) {
+      throw new Error(`未找到小红书账号 id=${accountId}`);
+    }
+    await svc.importAccountLast30Days(account);
+  }
+
+  /**
+   * 单账号入口:仅为指定账号执行近30日账号概览导入(用于账号从失效恢复为 active 时补数)
+   */
+  static async runDailyImportForAccount(accountId: number): Promise<void> {
+    const svc = new XiaohongshuAccountOverviewImportService();
+    const account = await svc.accountRepository.findOne({
+      where: { id: accountId, platform: 'xiaohongshu' as any },
+    });
+    if (!account) {
+      throw new Error(`未找到小红书账号 id=${accountId}`);
+    }
+    await svc.importAccountLast30Days(account);
+  }
+
+  /**
    * 为所有小红书账号导出“观看数据-近30日”并导入 user_day_statistics
    */
   async runDailyImportForAllXhsAccounts(): Promise<void> {