Преглед изворни кода

fix: xiaohongshu import no modify account and build fix

1. Remove account status update in Xiaohongshu import service to strictly follow 'no modify platform_accounts' rule.
2. Fix TS build error in HeadlessBrowserService regarding works assignment.
3. Fix RedisTaskQueue type errors and missing delete_work task title.
Ethanfly пре 23 часа
родитељ
комит
481e389206

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

@@ -3065,7 +3065,8 @@ class HeadlessBrowserService {
 
       // 方式1:直接调用 API 获取作品列表(优先)
       logger.info('[API Interception] Fetching works via direct API...');
-      let works = await this.fetchWorksDirectApi(page);
+      const apiResult = await this.fetchWorksDirectApi(page);
+      let works = apiResult.works;
 
       // 方式2:如果直接调用失败,尝试通过页面触发 API
       if (works.length === 0) {

+ 28 - 24
server/src/services/RedisTaskQueue.ts

@@ -1,9 +1,9 @@
 import { Queue, Worker, Job, QueueEvents } from 'bullmq';
 import IORedis from 'ioredis';
 import { v4 as uuidv4 } from 'uuid';
-import { 
-  Task, 
-  TaskType, 
+import {
+  Task,
+  TaskType,
   TaskResult,
   TaskProgressUpdate,
   CreateTaskRequest,
@@ -24,7 +24,7 @@ const redisConnection = new IORedis({
 
 // 任务执行器类型
 type TaskExecutor = (
-  task: Task, 
+  task: Task,
   updateProgress: (update: Partial<TaskProgressUpdate>) => void
 ) => Promise<TaskResult>;
 
@@ -39,19 +39,20 @@ class RedisTaskQueueService {
   private queue: Queue;
   private queueEvents: QueueEvents;
   private worker: Worker | null = null;
-  
+
   // 任务执行器 Map<TaskType, TaskExecutor>
   private executors: Map<TaskType, TaskExecutor> = new Map();
-  
+
   // 内存中缓存用户任务(用于快速查询)
   private userTasks: Map<number, Task[]> = new Map();
-  
+
   // 最大并行任务数
   private concurrency = 5;
 
   constructor() {
     // 创建队列
     this.queue = new Queue(QUEUE_NAME, {
+      // @ts-ignore
       connection: redisConnection,
       defaultJobOptions: {
         removeOnComplete: { count: 100 },  // 保留最近100个完成的任务
@@ -66,11 +67,12 @@ class RedisTaskQueueService {
 
     // 创建队列事件监听
     this.queueEvents = new QueueEvents(QUEUE_NAME, {
+      // @ts-ignore
       connection: redisConnection.duplicate(),
     });
 
     this.setupEventListeners();
-    
+
     logger.info('Redis Task Queue Service initialized');
   }
 
@@ -106,6 +108,7 @@ class RedisTaskQueueService {
         return this.processJob(job);
       },
       {
+        // @ts-ignore
         connection: redisConnection.duplicate(),
         concurrency: this.concurrency,  // 并行处理任务数
       }
@@ -143,7 +146,7 @@ class RedisTaskQueueService {
   private async processJob(job: Job): Promise<TaskResult> {
     const taskData = job.data as Task & { userId: number };
     const { userId } = taskData;
-    
+
     const executor = this.executors.get(taskData.type);
     if (!executor) {
       throw new Error(`No executor registered for task type: ${taskData.type}`);
@@ -156,15 +159,15 @@ class RedisTaskQueueService {
     });
 
     // 通知前端任务开始
-    this.notifyUser(userId, TASK_WS_EVENTS.TASK_STARTED, { 
-      task: this.getTaskFromCache(userId, taskData.id) 
+    this.notifyUser(userId, TASK_WS_EVENTS.TASK_STARTED, {
+      task: this.getTaskFromCache(userId, taskData.id)
     });
 
     // 进度更新回调
     const updateProgress = async (update: Partial<TaskProgressUpdate>) => {
       // 更新 Job 进度
       await job.updateProgress(update);
-      
+
       // 更新内存缓存
       this.updateTaskInCache(userId, taskData.id, {
         progress: update.progress,
@@ -184,7 +187,7 @@ class RedisTaskQueueService {
 
     try {
       const result = await executor(taskData, updateProgress);
-      
+
       // 更新缓存
       this.updateTaskInCache(userId, taskData.id, {
         status: 'completed',
@@ -194,14 +197,14 @@ class RedisTaskQueueService {
       });
 
       // 通知前端
-      this.notifyUser(userId, TASK_WS_EVENTS.TASK_COMPLETED, { 
-        task: this.getTaskFromCache(userId, taskData.id) 
+      this.notifyUser(userId, TASK_WS_EVENTS.TASK_COMPLETED, {
+        task: this.getTaskFromCache(userId, taskData.id)
       });
 
       return result;
     } catch (error) {
       const errorMessage = error instanceof Error ? error.message : '任务执行失败';
-      
+
       // 更新缓存
       this.updateTaskInCache(userId, taskData.id, {
         status: 'failed',
@@ -210,8 +213,8 @@ class RedisTaskQueueService {
       });
 
       // 通知前端
-      this.notifyUser(userId, TASK_WS_EVENTS.TASK_FAILED, { 
-        task: this.getTaskFromCache(userId, taskData.id) 
+      this.notifyUser(userId, TASK_WS_EVENTS.TASK_FAILED, {
+        task: this.getTaskFromCache(userId, taskData.id)
       });
 
       throw error;
@@ -230,7 +233,7 @@ class RedisTaskQueueService {
    * 创建新任务
    */
   async createTask(userId: number, request: CreateTaskRequest): Promise<Task & { userId: number }> {
-    const task: Task & { userId: number; [key: string]: unknown } = {
+    const task: Task & { userId: number;[key: string]: unknown } = {
       id: uuidv4(),
       type: request.type,
       title: request.title || this.getDefaultTitle(request.type),
@@ -303,7 +306,7 @@ class RedisTaskQueueService {
 
       // 通知前端
       this.notifyUser(userId, TASK_WS_EVENTS.TASK_CANCELLED, { task });
-      
+
       logger.info(`Task cancelled: ${taskId}`);
       return true;
     }
@@ -318,18 +321,18 @@ class RedisTaskQueueService {
     const tasks = this.userTasks.get(userId);
     if (!tasks) return;
 
-    const completedTasks = tasks.filter(t => 
+    const completedTasks = tasks.filter(t =>
       t.status === 'completed' || t.status === 'failed' || t.status === 'cancelled'
     );
 
     if (completedTasks.length > keepCount) {
-      completedTasks.sort((a, b) => 
+      completedTasks.sort((a, b) =>
         new Date(b.completedAt || 0).getTime() - new Date(a.completedAt || 0).getTime()
       );
-      
+
       const toRemove = completedTasks.slice(keepCount);
       const toRemoveIds = new Set(toRemove.map(t => t.id));
-      
+
       this.userTasks.set(userId, tasks.filter(t => !toRemoveIds.has(t.id)));
     }
   }
@@ -407,6 +410,7 @@ class RedisTaskQueueService {
       sync_account: '同步账号信息',
       publish_video: '发布视频',
       batch_reply: '批量回复评论',
+      delete_work: '删除作品',
     };
     return titles[type] || '未知任务';
   }

+ 1 - 0
server/src/services/TaskQueueService.ts

@@ -256,6 +256,7 @@ class TaskQueueService {
       sync_account: '同步账号信息',
       publish_video: '发布视频',
       batch_reply: '批量回复评论',
+      delete_work: '删除作品',
     };
     return titles[type] || '未知任务';
   }

+ 18 - 7
server/src/services/XiaohongshuAccountOverviewImportService.ts

@@ -217,7 +217,7 @@ function parseXhsExcel(
     }
 
     const sheet = wb.Sheets[sheetName];
-    const rows = XLSX.utils.sheet_to_json<Record<string, any>>(sheet, { defval: '' });
+    const rows = (XLSX.utils.sheet_to_json(sheet, { defval: '' }) as Record<string, any>[]);
 
     if (rows.length) {
       const keys = Object.keys(rows[0] || {});
@@ -373,20 +373,17 @@ export class XiaohongshuAccountOverviewImportService {
         throw new Error('未登录/需要重新登录(跳转到 login)');
       }
 
-      // 检测“暂无访问权限 / 权限申请中”提示:标记账号 expired + 推送提示
+      // 检测“暂无访问权限 / 权限申请中”提示:仅推送提示,不修改账号状态(避免误判或用户不想自动变更)
       const bodyText = (await page.textContent('body').catch(() => '')) || '';
       if (bodyText.includes('暂无访问权限') || bodyText.includes('数据权限申请中') || bodyText.includes('次日再来查看')) {
-        await this.accountRepository.update(account.id, { status: 'expired' as any });
-        wsManager.sendToUser(account.userId, WS_EVENTS.ACCOUNT_UPDATED, {
-          account: { id: account.id, status: 'expired', platform: 'xiaohongshu' },
-        });
+        // await this.accountRepository.update(account.id, { status: 'expired' as any });
         wsManager.sendToUser(account.userId, WS_EVENTS.SYSTEM_MESSAGE, {
           level: 'warning',
           message: `小红书账号「${account.accountName || account.accountId || account.id}」暂无数据看板访问权限,请到小红书创作服务平台申请数据权限(通过后一般次日生效)。`,
           platform: 'xiaohongshu',
           accountId: account.id,
         });
-        throw new Error('小红书数据看板暂无访问权限/申请中,已标记 expired 并通知用户');
+        throw new Error('小红书数据看板暂无访问权限/申请中,已通知用户');
       }
 
       // 统一入口:账号概览 -> 笔记数据
@@ -415,6 +412,20 @@ export class XiaohongshuAccountOverviewImportService {
           perDay = parseXhsExcel(filePath, mode);
           for (const v of perDay.values()) {
             const { recordDate, ...patch } = v;
+
+            // 修正:如果导入的数据是今天的,且没有粉丝总数(Excel只有涨粉数),则使用账号当前的粉丝数
+            // 避免因为导入导致今天的粉丝数被重置为 0
+            const today = new Date();
+            today.setHours(0, 0, 0, 0);
+
+            // 比较时间戳
+            if (recordDate.getTime() === today.getTime()) {
+              if ((patch as any).fansCount === undefined && account.fansCount !== undefined && account.fansCount > 0) {
+                (patch as any).fansCount = account.fansCount;
+                logger.info(`[XHS Import] Injected current fansCount=${account.fansCount} for today's record (accountId=${account.id})`);
+              }
+            }
+
             const r = await this.userDayStatisticsService.saveStatisticsForDate(account.id, recordDate, patch);
             inserted += r.inserted;
             updated += r.updated;