Просмотр исходного кода

fix: scope sync and stabilize publish flow

ethanfly 2 дней назад
Родитель
Сommit
09e820eeaf

+ 30 - 0
.eslintrc.cjs

@@ -0,0 +1,30 @@
+module.exports = {
+  root: true,
+  env: {
+    browser: true,
+    node: true,
+    es2022: true,
+  },
+  parserOptions: {
+    ecmaVersion: 'latest',
+    sourceType: 'module',
+  },
+  plugins: ['@typescript-eslint', 'vue'],
+  overrides: [
+    {
+      files: ['*.ts'],
+      parser: '@typescript-eslint/parser',
+    },
+    {
+      files: ['*.vue'],
+      parser: 'vue-eslint-parser',
+      parserOptions: {
+        parser: '@typescript-eslint/parser',
+        ecmaVersion: 'latest',
+        sourceType: 'module',
+        extraFileExtensions: ['.vue'],
+      },
+    },
+  ],
+  rules: {},
+};

+ 0 - 4
client/src/main.ts

@@ -1,8 +1,5 @@
 import { createApp } from 'vue';
 import { createApp } from 'vue';
 import { createPinia } from 'pinia';
 import { createPinia } from 'pinia';
-import ElementPlus from 'element-plus';
-import zhCn from 'element-plus/es/locale/lang/zh-cn';
-import 'element-plus/dist/index.css';
 import './styles/index.scss';
 import './styles/index.scss';
 
 
 import App from './App.vue';
 import App from './App.vue';
@@ -12,6 +9,5 @@ const app = createApp(App);
 
 
 app.use(createPinia());
 app.use(createPinia());
 app.use(router);
 app.use(router);
-app.use(ElementPlus, { locale: zhCn });
 
 
 app.mount('#app');
 app.mount('#app');

+ 58 - 46
client/src/views/Dashboard/index.vue

@@ -140,9 +140,9 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { computed, nextTick, onActivated, onMounted, onUnmounted, ref, watch } from 'vue';
+import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
 import { useRouter } from 'vue-router';
 import { useRouter } from 'vue-router';
-import { Refresh, TrendCharts, Upload, User, UserFilled, VideoPlay } from '@element-plus/icons-vue';
+import { TrendCharts, User, UserFilled, VideoPlay } from '@element-plus/icons-vue';
 import { echarts } from '@/utils/echarts';
 import { echarts } from '@/utils/echarts';
 import { useTaskQueueStore } from '@/stores/taskQueue';
 import { useTaskQueueStore } from '@/stores/taskQueue';
 import { accountsApi } from '@/api/accounts';
 import { accountsApi } from '@/api/accounts';
@@ -158,7 +158,6 @@ const taskStore = useTaskQueueStore();
 const accounts = ref<PlatformAccount[]>([]);
 const accounts = ref<PlatformAccount[]>([]);
 const tasks = ref<PublishTask[]>([]);
 const tasks = ref<PublishTask[]>([]);
 const trendData = ref<TrendData | null>(null);
 const trendData = ref<TrendData | null>(null);
-const refreshing = ref(false);
 const lastUpdatedAt = ref(dayjs());
 const lastUpdatedAt = ref(dayjs());
 
 
 const primaryChartRef = ref<HTMLElement>();
 const primaryChartRef = ref<HTMLElement>();
@@ -166,8 +165,9 @@ const secondaryChartRef = ref<HTMLElement>();
 let primaryChart: echarts.ECharts | null = null;
 let primaryChart: echarts.ECharts | null = null;
 let secondaryChart: echarts.ECharts | null = null;
 let secondaryChart: echarts.ECharts | null = null;
 let resizeObserver: ResizeObserver | null = null;
 let resizeObserver: ResizeObserver | null = null;
+let refreshTimer: ReturnType<typeof window.setTimeout> | null = null;
+let loadDataPromise: Promise<void> | null = null;
 
 
-const updatedAtText = computed(() => lastUpdatedAt.value.format('YYYY-MM-DD HH:mm'));
 const accountPreviewList = computed(() => accounts.value.slice(0, 5));
 const accountPreviewList = computed(() => accounts.value.slice(0, 5));
 
 
 const totalFans = computed(() => accounts.value.reduce((sum, account) => sum + (account.fansCount || 0), 0));
 const totalFans = computed(() => accounts.value.reduce((sum, account) => sum + (account.fansCount || 0), 0));
@@ -234,15 +234,6 @@ function handleNavigate(path: string) {
   router.push(path).catch(() => {});
   router.push(path).catch(() => {});
 }
 }
 
 
-async function handleRefresh() {
-  refreshing.value = true;
-  try {
-    await loadData();
-  } finally {
-    refreshing.value = false;
-  }
-}
-
 function getPlatformName(platform: PlatformType) {
 function getPlatformName(platform: PlatformType) {
   return PLATFORMS[platform]?.name || platform;
   return PLATFORMS[platform]?.name || platform;
 }
 }
@@ -449,28 +440,44 @@ function renderCharts() {
 }
 }
 
 
 async function loadData() {
 async function loadData() {
-  try {
-    const [accountsData, worksStatsResult, tasksData, trendResult] = await Promise.all([
-      accountsApi.getAccounts(),
-      dashboardApi.getWorksStats().catch(() => null),
-      request.get('/api/publish', { params: { page: 1, pageSize: 5 } }).catch(() => null),
-      dashboardApi.getTrend({ days: 30 }).catch(() => null),
-    ]);
-
-    accounts.value = accountsData;
-    tasks.value = tasksData?.items || [];
-    workStats.value = {
-      totalCount: worksStatsResult?.totalCount || 0,
-      totalPlayCount: worksStatsResult?.totalPlayCount || 0,
-    };
-    trendData.value = trendResult;
-    lastUpdatedAt.value = dayjs();
-
-    await nextTick();
-    renderCharts();
-  } catch {
-    // handled by request interceptors
-  }
+  if (loadDataPromise) return loadDataPromise;
+
+  loadDataPromise = (async () => {
+    try {
+      const [accountsData, worksStatsResult, tasksData, trendResult] = await Promise.all([
+        accountsApi.getAccounts(),
+        dashboardApi.getWorksStats().catch(() => null),
+        request.get('/api/publish', { params: { page: 1, pageSize: 5 } }).catch(() => null),
+        dashboardApi.getTrend({ days: 30 }).catch(() => null),
+      ]);
+
+      accounts.value = accountsData;
+      tasks.value = tasksData?.items || [];
+      workStats.value = {
+        totalCount: worksStatsResult?.totalCount || 0,
+        totalPlayCount: worksStatsResult?.totalPlayCount || 0,
+      };
+      trendData.value = trendResult;
+      lastUpdatedAt.value = dayjs();
+
+      await nextTick();
+      renderCharts();
+    } catch {
+      // handled by request interceptors
+    } finally {
+      loadDataPromise = null;
+    }
+  })();
+
+  return loadDataPromise;
+}
+
+function scheduleLoadData() {
+  if (refreshTimer) window.clearTimeout(refreshTimer);
+  refreshTimer = window.setTimeout(() => {
+    refreshTimer = null;
+    void loadData();
+  }, 300);
 }
 }
 
 
 function initCharts() {
 function initCharts() {
@@ -506,16 +513,12 @@ onMounted(async () => {
   window.addEventListener('resize', handleResize);
   window.addEventListener('resize', handleResize);
 });
 });
 
 
-onActivated(async () => {
-  await loadData();
-  nextTick(() => {
-    initCharts();
-    handleResize();
-  });
-});
-
 onUnmounted(() => {
 onUnmounted(() => {
   window.removeEventListener('resize', handleResize);
   window.removeEventListener('resize', handleResize);
+  if (refreshTimer) {
+    window.clearTimeout(refreshTimer);
+    refreshTimer = null;
+  }
   resizeObserver?.disconnect();
   resizeObserver?.disconnect();
   resizeObserver = null;
   resizeObserver = null;
   primaryChart?.dispose();
   primaryChart?.dispose();
@@ -525,11 +528,20 @@ onUnmounted(() => {
 });
 });
 
 
 watch(
 watch(
-  () => taskStore.tasks,
+  () => [taskStore.worksRefreshTrigger, taskStore.accountRefreshTrigger],
+  () => {
+    scheduleLoadData();
+  },
+);
+
+watch(
+  () => taskStore.tasks
+    .filter((task) => task.type === 'publish_video')
+    .map((task) => `${task.id}:${task.status}`)
+    .join('|'),
   () => {
   () => {
-    loadData();
+    scheduleLoadData();
   },
   },
-  { deep: true },
 );
 );
 </script>
 </script>
 
 

+ 2 - 0
client/src/views/Login/index.vue

@@ -113,6 +113,8 @@ async function handleLogin() {
     });
     });
     ElMessage.success('登录成功');
     ElMessage.success('登录成功');
     router.push('/');
     router.push('/');
+  } catch {
+    // 錯誤提示已由 request 攔截器處理,這裡吞掉異常避免 Vue 報未處理事件錯誤。
   } finally {
   } finally {
     loading.value = false;
     loading.value = false;
   }
   }

+ 13 - 11
client/src/views/Publish/index.vue

@@ -957,15 +957,17 @@ async function handleCreate() {
   
   
   submitting.value = true;
   submitting.value = true;
   try {
   try {
-    // 1. 上传视频
-    const formData = new FormData();
-    formData.append('video', createForm.videoFile);
-    
-    const uploadResult = await request.post('/api/upload/video', formData, {
-      headers: {
-        'Content-Type': 'multipart/form-data',
-      },
-    });
+    let uploadResult: { path: string; originalname: string } | null = null;
+    if (createForm.videoFile) {
+      const formData = new FormData();
+      formData.append('video', createForm.videoFile);
+
+      uploadResult = await request.post('/api/upload/video', formData, {
+        headers: {
+          'Content-Type': 'multipart/form-data',
+        },
+      });
+    }
     
     
     // 2. 创建发布任务
     // 2. 创建发布任务
     const proxy = createForm.usePublishProxy
     const proxy = createForm.usePublishProxy
@@ -973,8 +975,8 @@ async function handleCreate() {
       : null;
       : null;
 
 
     await request.post('/api/publish', {
     await request.post('/api/publish', {
-      videoPath: uploadResult.path,
-      videoFilename: uploadResult.originalname,
+      videoPath: uploadResult?.path || '',
+      videoFilename: uploadResult?.originalname || '',
       title: createForm.title,
       title: createForm.title,
       description: createForm.description,
       description: createForm.description,
       tags: createForm.tags,
       tags: createForm.tags,

+ 11 - 0
server/env.example

@@ -18,6 +18,10 @@ PORT=3000
 # 0.0.0.0 = 允许局域网/外网访问(需要时再开启)
 # 0.0.0.0 = 允许局域网/外网访问(需要时再开启)
 HOST=127.0.0.1
 HOST=127.0.0.1
 
 
+# Do not kill a process that already owns PORT unless explicitly enabled for local development.
+# Keep false for production/high availability.
+ALLOW_PORT_PROCESS_KILL=false
+
 # ----------------------------------------
 # ----------------------------------------
 # 数据库配置 (MySQL)
 # 数据库配置 (MySQL)
 # ----------------------------------------
 # ----------------------------------------
@@ -60,6 +64,13 @@ REDIS_DB=0
 USE_REDIS_QUEUE=false
 USE_REDIS_QUEUE=false
 
 
 # ----------------------------------------
 # ----------------------------------------
+# Scheduler
+# ----------------------------------------
+# Disabled by default so account/data sync is triggered from the logged-in client
+# instead of iterating every stored platform account on the server.
+ENABLE_SERVER_ACCOUNT_JOBS=false
+
+# ----------------------------------------
 # JWT 认证配置
 # JWT 认证配置
 # ----------------------------------------
 # ----------------------------------------
 # JWT 密钥 (生产环境请使用强随机字符串)
 # JWT 密钥 (生产环境请使用强随机字符串)

+ 43 - 12
server/src/app.ts

@@ -163,6 +163,12 @@ async function ensurePortAvailable(port: number, maxRetries: number = 3): Promis
     const pid = await getProcessOnPort(port);
     const pid = await getProcessOnPort(port);
     
     
     if (pid) {
     if (pid) {
+      if (!config.server.allowPortProcessKill) {
+        throw new Error(
+          `Port ${port} is already in use by process ${pid}. Set ALLOW_PORT_PROCESS_KILL=true only for local development if you want this server to terminate it.`
+        );
+      }
+
       logger.info(`[Port Check] Found process ${pid} using port ${port}, attempting to terminate...`);
       logger.info(`[Port Check] Found process ${pid} using port ${port}, attempting to terminate...`);
       const killed = await killProcess(pid);
       const killed = await killProcess(pid);
       
       
@@ -289,20 +295,45 @@ function setupBrowserLoginEvents(): void {
   logger.info('登录服务事件监听已注册');
   logger.info('登录服务事件监听已注册');
 }
 }
 
 
-// 优雅关闭
-process.on('SIGTERM', async () => {
-  logger.info('SIGTERM received, shutting down gracefully');
-  taskScheduler.stop();
-  await taskQueueService.close();
-  // 关闭所有无头/有头浏览器,释放资源
-  await BrowserManager.closeBrowser();
-  // 修复: 关闭 WebSocket manager,清理心跳定时器和 captchaListeners
-  wsManager.close();
-  httpServer.close(() => {
+let isShuttingDown = false;
+
+// 浼橀泤鍏抽棴
+async function shutdown(signal: string): Promise<void> {
+  if (isShuttingDown) return;
+  isShuttingDown = true;
+
+  logger.info(`${signal} received, shutting down gracefully`);
+  const forceExitTimer = setTimeout(() => {
+    logger.error('Graceful shutdown timed out, forcing exit');
+    process.exit(1);
+  }, 30000);
+  forceExitTimer.unref();
+
+  try {
+    taskScheduler.stop();
+    await taskQueueService.close();
+    // 鍏抽棴鎵€鏈夋棤澶?鏈夊ご娴忚鍣紝閲婃斁璧勬簮
+    await BrowserManager.closeBrowser();
+    // 淇: 鍏抽棴 WebSocket manager锛屾竻鐞嗗績璺冲畾鏃跺櫒鍜?captchaListeners
+    wsManager.close();
+    await new Promise<void>((resolve, reject) => {
+      httpServer.close((error) => {
+        if (error) reject(error);
+        else resolve();
+      });
+    });
     logger.info('Server closed');
     logger.info('Server closed');
+    clearTimeout(forceExitTimer);
     process.exit(0);
     process.exit(0);
-  });
-});
+  } catch (error) {
+    logger.error('Graceful shutdown failed:', error);
+    clearTimeout(forceExitTimer);
+    process.exit(1);
+  }
+}
+
+process.on('SIGTERM', () => void shutdown('SIGTERM'));
+process.on('SIGINT', () => void shutdown('SIGINT'));
 
 
 bootstrap();
 bootstrap();
 
 

+ 13 - 0
server/src/config/index.ts

@@ -16,6 +16,12 @@ export const config = {
   // 服务监听地址:127.0.0.1 仅本地访问,0.0.0.0 允许局域网访问
   // 服务监听地址:127.0.0.1 仅本地访问,0.0.0.0 允许局域网访问
   host: process.env.HOST || '127.0.0.1',
   host: process.env.HOST || '127.0.0.1',
 
 
+  server: {
+    // Never terminate an arbitrary process on the configured port unless this
+    // is explicitly enabled for a local development workflow.
+    allowPortProcessKill: process.env.ALLOW_PORT_PROCESS_KILL === 'true',
+  },
+
   // 数据库配置
   // 数据库配置
   database: {
   database: {
     host: process.env.DB_HOST || 'localhost',
     host: process.env.DB_HOST || 'localhost',
@@ -33,6 +39,13 @@ export const config = {
     db: parseInt(process.env.REDIS_DB || '0', 10),
     db: parseInt(process.env.REDIS_DB || '0', 10),
   },
   },
 
 
+  // Scheduler
+  scheduler: {
+    // Keep account/data sync client-triggered by default. Enabling this will let
+    // the server iterate stored platform accounts on a schedule.
+    enableServerAccountJobs: process.env.ENABLE_SERVER_ACCOUNT_JOBS === 'true',
+  },
+
   // JWT 配置
   // JWT 配置
   jwt: {
   jwt: {
     secret: process.env.JWT_SECRET || 'your-super-secret-key-change-in-production',
     secret: process.env.JWT_SECRET || 'your-super-secret-key-change-in-production',

+ 1 - 1
server/src/routes/comments.ts

@@ -95,7 +95,7 @@ router.post(
     const userId = req.user!.userId;
     const userId = req.user!.userId;
     
     
     // 创建任务加入队列
     // 创建任务加入队列
-    const task = taskQueueService.createTask(userId, {
+    const task = await taskQueueService.createTask(userId, {
       type: 'sync_comments',
       type: 'sync_comments',
       title: accountName ? `同步评论 - ${accountName}` : '同步所有评论',
       title: accountName ? `同步评论 - ${accountName}` : '同步所有评论',
       accountId: accountId ? Number(accountId) : undefined,
       accountId: accountId ? Number(accountId) : undefined,

+ 2 - 2
server/src/routes/internal.ts

@@ -410,13 +410,13 @@ router.post(
     }
     }
 
 
     const encrypted = CookieManager.encrypt(cookies);
     const encrypted = CookieManager.encrypt(cookies);
-    await accountRepository.update(accountId, {
+    await accountRepository.update({ id: accountId, userId }, {
       cookieData: encrypted,
       cookieData: encrypted,
       status: 'active',
       status: 'active',
       updatedAt: new Date(),
       updatedAt: new Date(),
     });
     });
 
 
-    const updated = await accountRepository.findOne({ where: { id: accountId } });
+    const updated = await accountRepository.findOne({ where: { id: accountId, userId } });
     wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_UPDATED, { account: updated });
     wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_UPDATED, { account: updated });
 
 
     res.json({ success: true });
     res.json({ success: true });

+ 3 - 3
server/src/routes/publish.ts

@@ -60,7 +60,7 @@ router.post(
 
 
     // 2. 如果不是定时任务,加入任务队列
     // 2. 如果不是定时任务,加入任务队列
     if (!req.body.scheduledAt) {
     if (!req.body.scheduledAt) {
-      taskQueueService.createTask(userId, {
+      await taskQueueService.createTask(userId, {
         type: 'publish_video',
         type: 'publish_video',
         title: `发布视频: ${req.body.title}`,
         title: `发布视频: ${req.body.title}`,
         description: `发布到 ${req.body.targetAccounts.length} 个账号`,
         description: `发布到 ${req.body.targetAccounts.length} 个账号`,
@@ -92,7 +92,7 @@ router.put(
 
 
     // 如果不是定时任务且状态允许,加入任务队列重新执行
     // 如果不是定时任务且状态允许,加入任务队列重新执行
     if (!req.body.scheduledAt && task.status !== 'cancelled') {
     if (!req.body.scheduledAt && task.status !== 'cancelled') {
-      taskQueueService.createTask(userId, {
+      await taskQueueService.createTask(userId, {
         type: 'publish_video',
         type: 'publish_video',
         title: `重新发布: ${task.title}`,
         title: `重新发布: ${task.title}`,
         description: `发布到 ${task.targetAccounts.length} 个账号`,
         description: `发布到 ${task.targetAccounts.length} 个账号`,
@@ -134,7 +134,7 @@ router.post(
     const task = await publishService.retryTask(userId, taskId);
     const task = await publishService.retryTask(userId, taskId);
 
 
     // 2. 加入任务队列重新执行
     // 2. 加入任务队列重新执行
-    taskQueueService.createTask(userId, {
+    await taskQueueService.createTask(userId, {
       type: 'publish_video',
       type: 'publish_video',
       title: `重试发布: ${task.title}`,
       title: `重试发布: ${task.title}`,
       description: `重新发布到 ${task.targetAccounts.length} 个账号`,
       description: `重新发布到 ${task.targetAccounts.length} 个账号`,

+ 2 - 2
server/src/routes/tasks.ts

@@ -44,7 +44,7 @@ router.post(
     const userId = req.user!.userId;
     const userId = req.user!.userId;
     const request = req.body as CreateTaskRequest;
     const request = req.body as CreateTaskRequest;
     
     
-    const task = taskQueueService.createTask(userId, request);
+    const task = await taskQueueService.createTask(userId, request);
     
     
     res.json({
     res.json({
       success: true,
       success: true,
@@ -63,7 +63,7 @@ router.post(
     const userId = req.user!.userId;
     const userId = req.user!.userId;
     const { taskId } = req.params;
     const { taskId } = req.params;
     
     
-    const success = taskQueueService.cancelTask(userId, taskId);
+    const success = await taskQueueService.cancelTask(userId, taskId);
     
     
     if (success) {
     if (success) {
       res.json({
       res.json({

+ 2 - 2
server/src/routes/works.ts

@@ -63,7 +63,7 @@ router.post(
     let title = '同步所有作品';
     let title = '同步所有作品';
     if (accountName) title = `同步作品 - ${accountName}`;
     if (accountName) title = `同步作品 - ${accountName}`;
     else if (platformName) title = `同步作品 - ${platformName}`;
     else if (platformName) title = `同步作品 - ${platformName}`;
-    const task = taskQueueService.createTask(userId, {
+    const task = await taskQueueService.createTask(userId, {
       type: 'sync_works',
       type: 'sync_works',
       title,
       title,
       accountId: accountId ? Number(accountId) : undefined,
       accountId: accountId ? Number(accountId) : undefined,
@@ -104,7 +104,7 @@ router.post(
     const workId = parseInt(req.params.id);
     const workId = parseInt(req.params.id);
     
     
     // 创建删除任务
     // 创建删除任务
-    const task = taskQueueService.createTask(userId, {
+    const task = await taskQueueService.createTask(userId, {
       type: 'delete_work',
       type: 'delete_work',
       title: '删除平台作品',
       title: '删除平台作品',
       data: { workId },
       data: { workId },

+ 66 - 124
server/src/scheduler/index.ts

@@ -1,10 +1,10 @@
 import schedule from 'node-schedule';
 import schedule from 'node-schedule';
-import { AppDataSource, PublishTask, PublishResult, PlatformAccount, AnalyticsData } from '../models/index.js';
+import { AppDataSource, PublishTask, PlatformAccount, AnalyticsData } from '../models/index.js';
 import { logger } from '../utils/logger.js';
 import { logger } from '../utils/logger.js';
 import { wsManager } from '../websocket/index.js';
 import { wsManager } from '../websocket/index.js';
 import { WS_EVENTS } from '@media-manager/shared';
 import { WS_EVENTS } from '@media-manager/shared';
 import { getAdapter, isPlatformSupported } from '../automation/platforms/index.js';
 import { getAdapter, isPlatformSupported } from '../automation/platforms/index.js';
-import { LessThanOrEqual, In } from 'typeorm';
+import { LessThanOrEqual } from 'typeorm';
 import { taskQueueService } from '../services/TaskQueueService.js';
 import { taskQueueService } from '../services/TaskQueueService.js';
 import { XiaohongshuAccountOverviewImportService } from '../services/XiaohongshuAccountOverviewImportService.js';
 import { XiaohongshuAccountOverviewImportService } from '../services/XiaohongshuAccountOverviewImportService.js';
 import { DouyinAccountOverviewImportService } from '../services/DouyinAccountOverviewImportService.js';
 import { DouyinAccountOverviewImportService } from '../services/DouyinAccountOverviewImportService.js';
@@ -15,7 +15,8 @@ import { DouyinWorkStatisticsImportService } from '../services/DouyinWorkStatist
 import { WeixinVideoWorkStatisticsImportService } from '../services/WeixinVideoWorkStatisticsImportService.js';
 import { WeixinVideoWorkStatisticsImportService } from '../services/WeixinVideoWorkStatisticsImportService.js';
 import { BaijiahaoWorkDailyStatisticsImportService } from '../services/BaijiahaoWorkDailyStatisticsImportService.js';
 import { BaijiahaoWorkDailyStatisticsImportService } from '../services/BaijiahaoWorkDailyStatisticsImportService.js';
 import { WeixinAutoReplyService } from '../services/WeixinAutoReplyService.js';
 import { WeixinAutoReplyService } from '../services/WeixinAutoReplyService.js';
-import { CookieManager } from '../automation/cookie.js';
+import { config } from '../config/index.js';
+import { PublishService } from '../services/PublishService.js';
 
 
 /**
 /**
  * 定时任务调度器
  * 定时任务调度器
@@ -47,38 +48,42 @@ export class TaskScheduler {
 
 
     // 每天中午 12 点:批量导出小红书“账号概览-笔记数据-观看数据-近30日”,导入 user_day_statistics
     // 每天中午 12 点:批量导出小红书“账号概览-笔记数据-观看数据-近30日”,导入 user_day_statistics
     // 注意:node-schedule 使用服务器本地时区
     // 注意:node-schedule 使用服务器本地时区
-    this.scheduleJob('xhs-account-overview-import', '0 12 * * *', this.importXhsAccountOverviewLast30Days.bind(this));
+    if (config.scheduler.enableServerAccountJobs) {
+      this.scheduleJob('xhs-account-overview-import', '0 12 * * *', this.importXhsAccountOverviewLast30Days.bind(this));
 
 
     // 每天 12:40:同步小红书作品维度的「笔记详情-按天」数据,写入 work_day_statistics
     // 每天 12:40:同步小红书作品维度的「笔记详情-按天」数据,写入 work_day_statistics
-    this.scheduleJob(
-      'xhs-work-note-statistics-import',
-      '40 12 * * *',
-      this.importXhsWorkNoteStatistics.bind(this)
-    );
+      this.scheduleJob(
+        'xhs-work-note-statistics-import',
+        '40 12 * * *',
+        this.importXhsWorkNoteStatistics.bind(this)
+      );
 
 
     // 每天 12:10:批量导出抖音“数据中心-账号总览-短视频-数据表现-近30天”,导入 user_day_statistics
     // 每天 12:10:批量导出抖音“数据中心-账号总览-短视频-数据表现-近30天”,导入 user_day_statistics
-    this.scheduleJob('dy-account-overview-import', '10 12 * * *', this.importDyAccountOverviewLast30Days.bind(this));
+      this.scheduleJob('dy-account-overview-import', '10 12 * * *', this.importDyAccountOverviewLast30Days.bind(this));
 
 
     // 每天 12:50:同步抖音作品维度的「作品详情-按天」数据,写入 work_day_statistics
     // 每天 12:50:同步抖音作品维度的「作品详情-按天」数据,写入 work_day_statistics
-    this.scheduleJob('dy-work-statistics-import', '50 12 * * *', this.importDyWorkStatistics.bind(this));
+      this.scheduleJob('dy-work-statistics-import', '50 12 * * *', this.importDyWorkStatistics.bind(this));
 
 
     // 每天 12:20:批量导出百家号“数据中心-内容分析-基础数据-近30天”,导入 user_day_statistics
     // 每天 12:20:批量导出百家号“数据中心-内容分析-基础数据-近30天”,导入 user_day_statistics
-    this.scheduleJob('bj-content-overview-import', '20 12 * * *', this.importBaijiahaoContentOverviewLast30Days.bind(this));
+      this.scheduleJob('bj-content-overview-import', '20 12 * * *', this.importBaijiahaoContentOverviewLast30Days.bind(this));
 
 
     // 每天 12:25:百家号作品维度「每日数据」同步(列表分页 + 逐条趋势),写入 works.yesterday_* 与 work_day_statistics
     // 每天 12:25:百家号作品维度「每日数据」同步(列表分页 + 逐条趋势),写入 works.yesterday_* 与 work_day_statistics
-    this.scheduleJob('bj-work-daily-import', '25 12 * * *', this.importBaijiahaoWorkDailyStatistics.bind(this));
+      this.scheduleJob('bj-work-daily-import', '25 12 * * *', this.importBaijiahaoWorkDailyStatistics.bind(this));
 
 
     // 每天 12:30:批量导出视频号“数据中心-各子菜单-增长详情(数据详情)-近30天-下载表格”,导入 user_day_statistics
     // 每天 12:30:批量导出视频号“数据中心-各子菜单-增长详情(数据详情)-近30天-下载表格”,导入 user_day_statistics
-    this.scheduleJob('wx-video-data-center-import', '30 12 * * *', this.importWeixinVideoDataCenterLast30Days.bind(this));
+      this.scheduleJob('wx-video-data-center-import', '30 12 * * *', this.importWeixinVideoDataCenterLast30Days.bind(this));
 
 
     // 每天 12:35:同步视频号作品维度的「作品列表 + 按天聚合-全部」数据,写入 work_day_statistics
     // 每天 12:35:同步视频号作品维度的「作品列表 + 按天聚合-全部」数据,写入 work_day_statistics
-    this.scheduleJob(
-      'wx-video-work-statistics-import',
-      '35 12 * * *',
-      this.importWeixinVideoWorkStatistics.bind(this)
-    );
+      this.scheduleJob(
+        'wx-video-work-statistics-import',
+        '35 12 * * *',
+        this.importWeixinVideoWorkStatistics.bind(this)
+      );
 
 
-    this.scheduleJob('auto-reply-messages', '* * * * *', this.autoReplyMessages.bind(this));
+      this.scheduleJob('auto-reply-messages', '* * * * *', this.autoReplyMessages.bind(this));
+    } else {
+      logger.info('[Scheduler] Server account/data jobs disabled; client-triggered sync only');
+    }
     // 注意:账号刷新由客户端定时触发,不在服务端自动执行
     // 注意:账号刷新由客户端定时触发,不在服务端自动执行
     // 这样可以确保只刷新当前登录用户的账号,避免处理其他用户的数据
     // 这样可以确保只刷新当前登录用户的账号,避免处理其他用户的数据
     
     
@@ -87,17 +92,19 @@ export class TaskScheduler {
     
     
     logger.info('[Scheduler] Scheduled jobs:');
     logger.info('[Scheduler] Scheduled jobs:');
     logger.info('[Scheduler]   - check-publish-tasks: every minute (* * * * *)');
     logger.info('[Scheduler]   - check-publish-tasks: every minute (* * * * *)');
-    logger.info('[Scheduler]   - xhs-account-overview-import: daily at 12:00 (0 12 * * *)');
-    logger.info(
-      '[Scheduler]   - xhs-work-note-statistics-import: daily at 12:40 (40 12 * * *)'
-    );
-    logger.info('[Scheduler]   - dy-account-overview-import:  daily at 12:10 (10 12 * * *)');
-    logger.info('[Scheduler]   - dy-work-statistics-import:  daily at 12:50 (50 12 * * *)');
-    logger.info('[Scheduler]   - bj-content-overview-import: daily at 12:20 (20 12 * * *)');
-    logger.info('[Scheduler]   - bj-work-daily-import:       daily at 12:25 (25 12 * * *)');
-    logger.info('[Scheduler]   - wx-video-data-center-import: daily at 12:30 (30 12 * * *)');
-    logger.info('[Scheduler]   - wx-video-work-statistics-import: daily at 12:35 (35 12 * * *)');
-    logger.info('[Scheduler]   - auto-reply-messages: every minute (* * * * *)');
+    if (config.scheduler.enableServerAccountJobs) {
+      logger.info('[Scheduler]   - xhs-account-overview-import: daily at 12:00 (0 12 * * *)');
+      logger.info(
+        '[Scheduler]   - xhs-work-note-statistics-import: daily at 12:40 (40 12 * * *)'
+      );
+      logger.info('[Scheduler]   - dy-account-overview-import:  daily at 12:10 (10 12 * * *)');
+      logger.info('[Scheduler]   - dy-work-statistics-import:  daily at 12:50 (50 12 * * *)');
+      logger.info('[Scheduler]   - bj-content-overview-import: daily at 12:20 (20 12 * * *)');
+      logger.info('[Scheduler]   - bj-work-daily-import:       daily at 12:25 (25 12 * * *)');
+      logger.info('[Scheduler]   - wx-video-data-center-import: daily at 12:30 (30 12 * * *)');
+      logger.info('[Scheduler]   - wx-video-work-statistics-import: daily at 12:35 (35 12 * * *)');
+      logger.info('[Scheduler]   - auto-reply-messages: every minute (* * * * *)');
+    }
     logger.info('[Scheduler] Note: Account refresh is triggered by client, not server');
     logger.info('[Scheduler] Note: Account refresh is triggered by client, not server');
     logger.info('[Scheduler] ========================================');
     logger.info('[Scheduler] ========================================');
     
     
@@ -147,21 +154,32 @@ export class TaskScheduler {
         status: 'pending',
         status: 'pending',
         scheduledAt: LessThanOrEqual(new Date()),
         scheduledAt: LessThanOrEqual(new Date()),
       },
       },
-      relations: ['results'],
     });
     });
     
     
     for (const task of tasks) {
     for (const task of tasks) {
-      logger.info(`Executing scheduled task: ${task.id}`);
+      try {
+        logger.info(`Executing scheduled task: ${task.id}`);
       
       
       // 更新状态为处理中
       // 更新状态为处理中
-      await taskRepository.update(task.id, { status: 'processing' });
-      wsManager.sendToUser(task.userId, WS_EVENTS.TASK_STATUS_CHANGED, {
-        taskId: task.id,
-        status: 'processing',
-      });
+        await taskRepository.update(task.id, { status: 'processing' });
+        wsManager.sendToUser(task.userId, WS_EVENTS.TASK_STATUS_CHANGED, {
+          taskId: task.id,
+          status: 'processing',
+        });
       
       
       // 执行发布
       // 执行发布
-      await this.executePublishTask(task);
+        await this.executePublishTask(task);
+      } catch (error) {
+        logger.error(`Scheduled publish task ${task.id} failed:`, error);
+        await taskRepository.update(task.id, {
+          status: 'failed',
+          publishedAt: new Date(),
+        });
+        wsManager.sendToUser(task.userId, WS_EVENTS.TASK_STATUS_CHANGED, {
+          taskId: task.id,
+          status: 'failed',
+        });
+      }
     }
     }
   }
   }
   
   
@@ -169,90 +187,14 @@ export class TaskScheduler {
    * 执行发布任务
    * 执行发布任务
    */
    */
   private async executePublishTask(task: PublishTask): Promise<void> {
   private async executePublishTask(task: PublishTask): Promise<void> {
-    const taskRepository = AppDataSource.getRepository(PublishTask);
-    const accountRepository = AppDataSource.getRepository(PlatformAccount);
-    const resultRepository = AppDataSource.getRepository(PublishResult);
-    
-    const targetAccounts = task.targetAccounts || [];
-    const accounts = await accountRepository.find({
-      where: { id: In(targetAccounts) },
-    });
-    
-    let successCount = 0;
-    let failCount = 0;
-    
-    for (const account of accounts) {
-      if (!isPlatformSupported(account.platform)) {
-        logger.warn(`Platform ${account.platform} not supported`);
-        failCount++;
-        continue;
-      }
-      
-      try {
-        const adapter = getAdapter(account.platform);
-
-        // 解密 Cookie(修复:直接使用加密 Cookie 会导致发布失败)
-        let decryptedCookies: string;
-        try {
-          decryptedCookies = CookieManager.decrypt(account.cookieData || '');
-        } catch {
-          decryptedCookies = account.cookieData || '';
-        }
-
-        const publishResult = await adapter.publishVideo(decryptedCookies, {
-          videoPath: task.videoPath || '',
-          title: task.title || '',
-          description: task.description || undefined,
-          coverPath: task.coverPath || undefined,
-          tags: task.tags || undefined,
-        });
-        
-        // 保存发布结果到 publish_results 表
-        const publishResultRecord = new PublishResult();
-        publishResultRecord.taskId = task.id;
-        publishResultRecord.accountId = account.id;
-        publishResultRecord.platform = account.platform;
-        publishResultRecord.status = publishResult.success ? 'success' : 'failed';
-        publishResultRecord.videoUrl = publishResult.videoUrl || null;
-        publishResultRecord.platformVideoId = publishResult.platformVideoId || null;
-        publishResultRecord.errorMessage = publishResult.errorMessage || null;
-        publishResultRecord.publishedAt = publishResult.success ? new Date() : null;
-        await resultRepository.save(publishResultRecord);
-        
-        if (publishResult.success) {
-          successCount++;
-        } else {
-          failCount++;
-        }
-        
-      } catch (error) {
-        logger.error(`Publish to ${account.platform} failed:`, error);
-        
-        // 保存失败结果到 publish_results 表
-        const errorMessage = error instanceof Error ? error.message : String(error);
-        const failedRecord = new PublishResult();
-        failedRecord.taskId = task.id;
-        failedRecord.accountId = account.id;
-        failedRecord.platform = account.platform;
-        failedRecord.status = 'failed';
-        failedRecord.errorMessage = errorMessage;
-        failedRecord.publishedAt = null;
-        await resultRepository.save(failedRecord);
-        
-        failCount++;
-      }
-    }
-    
-    // 更新任务状态
-    const finalStatus = failCount === 0 ? 'completed' : (successCount === 0 ? 'failed' : 'completed');
-    await taskRepository.update(task.id, {
-      status: finalStatus,
-      publishedAt: new Date(),
-    });
-    
-    wsManager.sendToUser(task.userId, WS_EVENTS.TASK_STATUS_CHANGED, {
-      taskId: task.id,
-      status: finalStatus,
+    const publishService = new PublishService();
+    await publishService.executePublishTaskWithProgress(task.id, task.userId, (progress, message) => {
+      wsManager.sendToUser(task.userId, WS_EVENTS.PUBLISH_PROGRESS, {
+        taskId: task.id,
+        status: 'processing',
+        progress,
+        message,
+      });
     });
     });
   }
   }
   
   
@@ -298,7 +240,7 @@ export class TaskScheduler {
         
         
         try {
         try {
           // 创建 sync_account 任务加入队列(静默执行,前台不弹框)
           // 创建 sync_account 任务加入队列(静默执行,前台不弹框)
-          taskQueueService.createTask(account.userId, {
+          await taskQueueService.createTask(account.userId, {
             type: 'sync_account',
             type: 'sync_account',
             title: `自动刷新: ${account.accountName || account.platform}`,
             title: `自动刷新: ${account.accountName || account.platform}`,
             description: `定时刷新账号 ${account.accountName} 的状态和信息`,
             description: `定时刷新账号 ${account.accountName} 的状态和信息`,

+ 19 - 19
server/src/services/AccountService.ts

@@ -65,12 +65,12 @@ export class AccountService {
       throw new AppError('分组不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND);
       throw new AppError('分组不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND);
     }
     }
 
 
-    await this.groupRepository.update(groupId, {
+    await this.groupRepository.update({ id: groupId, userId }, {
       name: data.name ?? group.name,
       name: data.name ?? group.name,
       description: data.description ?? group.description,
       description: data.description ?? group.description,
     });
     });
 
 
-    const updated = await this.groupRepository.findOne({ where: { id: groupId } });
+    const updated = await this.groupRepository.findOne({ where: { id: groupId, userId } });
     return this.formatGroup(updated!);
     return this.formatGroup(updated!);
   }
   }
 
 
@@ -81,7 +81,7 @@ export class AccountService {
     if (!group) {
     if (!group) {
       throw new AppError('分组不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND);
       throw new AppError('分组不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND);
     }
     }
-    await this.groupRepository.delete(groupId);
+    await this.groupRepository.delete({ id: groupId, userId });
   }
   }
 
 
   // ============ 平台账号 ============
   // ============ 平台账号 ============
@@ -264,7 +264,7 @@ export class AccountService {
         proxyConfig: data.proxyConfig || existing.proxyConfig,
         proxyConfig: data.proxyConfig || existing.proxyConfig,
       });
       });
 
 
-      const updated = await this.accountRepository.findOne({ where: { id: existing.id } });
+      const updated = await this.accountRepository.findOne({ where: { id: existing.id, userId } });
       wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_UPDATED, { account: this.formatAccount(updated!) });
       wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_UPDATED, { account: this.formatAccount(updated!) });
 
 
       // Bug #6070: 通知前端账号数据已变更,触发其他模块(如首页数据看板)刷新
       // Bug #6070: 通知前端账号数据已变更,触发其他模块(如首页数据看板)刷新
@@ -361,13 +361,13 @@ export class AccountService {
 
 
     try {
     try {
       if (platform === 'xiaohongshu') {
       if (platform === 'xiaohongshu') {
-        await XiaohongshuAccountOverviewImportService.runDailyImport();
+        await XiaohongshuAccountOverviewImportService.runDailyImportForAccount(account.id);
       } else if (platform === 'douyin') {
       } else if (platform === 'douyin') {
-        await DouyinAccountOverviewImportService.runDailyImport();
+        await DouyinAccountOverviewImportService.runDailyImportForAccount(account.id);
       } else if (platform === 'baijiahao') {
       } else if (platform === 'baijiahao') {
-        await BaijiahaoContentOverviewImportService.runDailyImport();
+        await BaijiahaoContentOverviewImportService.runDailyImportForAccount(account.id);
       } else if (platform === 'weixin_video') {
       } else if (platform === 'weixin_video') {
-        await WeixinVideoDataCenterImportService.runDailyImport();
+        await WeixinVideoDataCenterImportService.runDailyImportForAccount(account.id);
       } else {
       } else {
         logger.info(
         logger.info(
           `[addAccount] Initial statistics import skipped for unsupported platform ${platform}`
           `[addAccount] Initial statistics import skipped for unsupported platform ${platform}`
@@ -439,13 +439,13 @@ export class AccountService {
       throw new AppError('账号不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.ACCOUNT_NOT_FOUND);
       throw new AppError('账号不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.ACCOUNT_NOT_FOUND);
     }
     }
 
 
-    await this.accountRepository.update(accountId, {
+    await this.accountRepository.update({ id: accountId, userId }, {
       groupId: data.groupId !== undefined ? data.groupId : account.groupId,
       groupId: data.groupId !== undefined ? data.groupId : account.groupId,
       proxyConfig: data.proxyConfig !== undefined ? data.proxyConfig : account.proxyConfig,
       proxyConfig: data.proxyConfig !== undefined ? data.proxyConfig : account.proxyConfig,
       status: data.status ?? account.status,
       status: data.status ?? account.status,
     });
     });
 
 
-    const updated = await this.accountRepository.findOne({ where: { id: accountId } });
+    const updated = await this.accountRepository.findOne({ where: { id: accountId, userId } });
 
 
     // 通知其他客户端
     // 通知其他客户端
     wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_UPDATED, { account: this.formatAccount(updated!) });
     wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_UPDATED, { account: this.formatAccount(updated!) });
@@ -460,7 +460,7 @@ export class AccountService {
     if (!account) {
     if (!account) {
       throw new AppError('账号不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.ACCOUNT_NOT_FOUND);
       throw new AppError('账号不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.ACCOUNT_NOT_FOUND);
     }
     }
-    await this.accountRepository.delete(accountId);
+    await this.accountRepository.delete({ id: accountId, userId });
 
 
     // 通知其他客户端(修复 #6068/#6070:删除账号后各模块需要刷新数据)
     // 通知其他客户端(修复 #6068/#6070:删除账号后各模块需要刷新数据)
     wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_DELETED, { accountId });
     wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_DELETED, { accountId });
@@ -489,8 +489,8 @@ export class AccountService {
       updateData.status = 'expired';
       updateData.status = 'expired';
       needReLogin = true;
       needReLogin = true;
       logger.warn(`[refreshAccount] Account ${accountId} has empty cookieData, marking as expired`);
       logger.warn(`[refreshAccount] Account ${accountId} has empty cookieData, marking as expired`);
-      await this.accountRepository.update(accountId, updateData as any);
-      const updated = await this.accountRepository.findOne({ where: { id: accountId } });
+      await this.accountRepository.update({ id: accountId, userId }, updateData as any);
+      const updated = await this.accountRepository.findOne({ where: { id: accountId, userId } });
       wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_UPDATED, { account: this.formatAccount(updated!) });
       wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_UPDATED, { account: this.formatAccount(updated!) });
       return { ...this.formatAccount(updated!), needReLogin };
       return { ...this.formatAccount(updated!), needReLogin };
     }
     }
@@ -677,9 +677,9 @@ export class AccountService {
     }
     }
     // 没有 Cookie 数据时,不改变状态
     // 没有 Cookie 数据时,不改变状态
 
 
-    await this.accountRepository.update(accountId, updateData as any);
+    await this.accountRepository.update({ id: accountId, userId }, updateData as any);
 
 
-    const updated = await this.accountRepository.findOne({ where: { id: accountId } });
+    const updated = await this.accountRepository.findOne({ where: { id: accountId, userId } });
 
 
     // 账号从 expired 恢复为 active:异步补齐最近 30 天用户每日数据
     // 账号从 expired 恢复为 active:异步补齐最近 30 天用户每日数据
     const newStatus = updated?.status ?? account.status;
     const newStatus = updated?.status ?? account.status;
@@ -803,7 +803,7 @@ export class AccountService {
 
 
     if (!account.cookieData) {
     if (!account.cookieData) {
       // 更新状态为过期
       // 更新状态为过期
-      await this.accountRepository.update(accountId, { status: 'expired' });
+      await this.accountRepository.update({ id: accountId, userId }, { status: 'expired' });
       return { isValid: false, needReLogin: true, uncertain: false };
       return { isValid: false, needReLogin: true, uncertain: false };
     }
     }
 
 
@@ -827,7 +827,7 @@ export class AccountService {
         // 如果 JSON 解析失败,尝试解析 "name=value; name2=value2" 格式
         // 如果 JSON 解析失败,尝试解析 "name=value; name2=value2" 格式
         cookieList = parseCookieString(decryptedCookies, platform);
         cookieList = parseCookieString(decryptedCookies, platform);
         if (cookieList.length === 0) {
         if (cookieList.length === 0) {
-          await this.accountRepository.update(accountId, { status: 'expired' });
+          await this.accountRepository.update({ id: accountId, userId }, { status: 'expired' });
           return { isValid: false, needReLogin: true, uncertain: false };
           return { isValid: false, needReLogin: true, uncertain: false };
         }
         }
       }
       }
@@ -836,7 +836,7 @@ export class AccountService {
 
 
       // 更新账号状态
       // 更新账号状态
       if (cookieStatus.needReLogin) {
       if (cookieStatus.needReLogin) {
-        await this.accountRepository.update(accountId, { status: 'expired' });
+        await this.accountRepository.update({ id: accountId, userId }, { status: 'expired' });
         wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_UPDATED, {
         wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_UPDATED, {
           account: { ...this.formatAccount(account), status: 'expired' }
           account: { ...this.formatAccount(account), status: 'expired' }
         });
         });
@@ -849,7 +849,7 @@ export class AccountService {
 
 
       if (cookieStatus.isValid && account.status === 'expired') {
       if (cookieStatus.isValid && account.status === 'expired') {
         // 如果之前是过期状态但现在有效了,更新为正常
         // 如果之前是过期状态但现在有效了,更新为正常
-        await this.accountRepository.update(accountId, { status: 'active' });
+        await this.accountRepository.update({ id: accountId, userId }, { status: 'active' });
       }
       }
 
 
       return { isValid: cookieStatus.isValid, needReLogin: false, uncertain: false };
       return { isValid: cookieStatus.isValid, needReLogin: false, uncertain: false };

+ 28 - 19
server/src/services/CommentService.ts

@@ -78,23 +78,25 @@ export class CommentService {
   }
   }
 
 
   async getStats(userId: number): Promise<CommentStats> {
   async getStats(userId: number): Promise<CommentStats> {
-    const totalCount = await this.commentRepository.count({ where: { userId } });
-    const unreadCount = await this.commentRepository.count({ where: { userId, isRead: false } });
-    const unrepliedCount = await this.commentRepository
-      .createQueryBuilder('comment')
-      .where('comment.userId = :userId', { userId })
-      .andWhere('comment.replyContent IS NULL')
-      .getCount();
-
     const today = new Date();
     const today = new Date();
     today.setHours(0, 0, 0, 0);
     today.setHours(0, 0, 0, 0);
-    const todayCount = await this.commentRepository
+
+    const result = await this.commentRepository
       .createQueryBuilder('comment')
       .createQueryBuilder('comment')
+      .select('COUNT(*)', 'totalCount')
+      .addSelect('SUM(CASE WHEN comment.isRead = 0 THEN 1 ELSE 0 END)', 'unreadCount')
+      .addSelect('SUM(CASE WHEN comment.replyContent IS NULL THEN 1 ELSE 0 END)', 'unrepliedCount')
+      .addSelect('SUM(CASE WHEN comment.createdAt >= :today THEN 1 ELSE 0 END)', 'todayCount')
       .where('comment.userId = :userId', { userId })
       .where('comment.userId = :userId', { userId })
-      .andWhere('comment.createdAt >= :today', { today })
-      .getCount();
+      .setParameter('today', today)
+      .getRawOne();
 
 
-    return { totalCount, unreadCount, unrepliedCount, todayCount };
+    return {
+      totalCount: Number(result?.totalCount) || 0,
+      unreadCount: Number(result?.unreadCount) || 0,
+      unrepliedCount: Number(result?.unrepliedCount) || 0,
+      todayCount: Number(result?.todayCount) || 0,
+    };
   }
   }
 
 
   async markAsRead(userId: number, commentIds: number[]): Promise<void> {
   async markAsRead(userId: number, commentIds: number[]): Promise<void> {
@@ -117,13 +119,13 @@ export class CommentService {
     // TODO: 调用平台适配器发送回复
     // TODO: 调用平台适配器发送回复
     // 这里只更新本地记录
     // 这里只更新本地记录
 
 
-    await this.commentRepository.update(commentId, {
+    await this.commentRepository.update({ id: commentId, userId }, {
       replyContent: content,
       replyContent: content,
       repliedAt: new Date(),
       repliedAt: new Date(),
       isRead: true,
       isRead: true,
     });
     });
 
 
-    const updated = await this.commentRepository.findOne({ where: { id: commentId } });
+    const updated = await this.commentRepository.findOne({ where: { id: commentId, userId } });
 
 
     wsManager.sendToUser(userId, WS_EVENTS.COMMENT_REPLIED, {
     wsManager.sendToUser(userId, WS_EVENTS.COMMENT_REPLIED, {
       comment: this.formatComment(updated!),
       comment: this.formatComment(updated!),
@@ -297,6 +299,7 @@ export class CommentService {
         const workRepository = AppDataSource.getRepository(Work);
         const workRepository = AppDataSource.getRepository(Work);
         const accountWorks = await workRepository.find({
         const accountWorks = await workRepository.find({
           where: { userId, accountId: account.id },
           where: { userId, accountId: account.id },
+          select: ['id', 'title', 'platform', 'platformVideoId'],
         });
         });
         
         
         logger.info(`Found ${accountWorks.length} works for account ${account.id}`);
         logger.info(`Found ${accountWorks.length} works for account ${account.id}`);
@@ -434,8 +437,10 @@ export class CommentService {
           logger.info(`Final work mapping: videoId="${commentVideoId}", title="${commentVideoTitle}", workId=${workId}`);
           logger.info(`Final work mapping: videoId="${commentVideoId}", title="${commentVideoTitle}", workId=${workId}`);
           
           
           // Batch prepare existing comments for this work to avoid N+1 queries
           // Batch prepare existing comments for this work to avoid N+1 queries
-          const batchPairs = workComment.comments.map(c => ({ accountId: account.id, authorName: c.authorName, content: c.content }));
-          const existingList = await this.commentRepository.find({ where: batchPairs as any[] });
+          const batchPairs = workComment.comments.map(c => ({ userId, accountId: account.id, authorName: c.authorName, content: c.content }));
+          const existingList = batchPairs.length > 0
+            ? await this.commentRepository.find({ where: batchPairs as any[] })
+            : [];
           const existingMap = new Map<string, any>();
           const existingMap = new Map<string, any>();
           for (const ex of existingList) {
           for (const ex of existingList) {
             const key = `${ex.authorName}|||${ex.content}`;
             const key = `${ex.authorName}|||${ex.content}`;
@@ -477,7 +482,7 @@ export class CommentService {
               } else {
               } else {
                 // 如果评论已存在但没有 workId,更新它
                 // 如果评论已存在但没有 workId,更新它
                 if (!existing.workId && workId) {
                 if (!existing.workId && workId) {
-                  await this.commentRepository.update(existing.id, { workId });
+                  await this.commentRepository.update({ id: existing.id, userId }, { workId });
                   logger.info(`Updated existing comment workId: ${existing.id} -> ${workId}`);
                   logger.info(`Updated existing comment workId: ${existing.id} -> ${workId}`);
                 }
                 }
               }
               }
@@ -524,6 +529,7 @@ export class CommentService {
       // 获取所有没有 workId 的评论
       // 获取所有没有 workId 的评论
       const orphanedComments = await this.commentRepository.find({
       const orphanedComments = await this.commentRepository.find({
         where: { userId, workId: IsNull() },
         where: { userId, workId: IsNull() },
+        select: ['id', 'accountId', 'platform', 'videoId'],
       });
       });
       
       
       if (orphanedComments.length === 0) return;
       if (orphanedComments.length === 0) return;
@@ -531,7 +537,10 @@ export class CommentService {
       logger.info(`Found ${orphanedComments.length} comments without workId, trying to fix...`);
       logger.info(`Found ${orphanedComments.length} comments without workId, trying to fix...`);
       
       
       // 获取用户的所有作品
       // 获取用户的所有作品
-      const works = await workRepository.find({ where: { userId } });
+      const works = await workRepository.find({
+        where: { userId },
+        select: ['id', 'accountId', 'platform', 'platformVideoId', 'title'],
+      });
       
       
       // 创建多种格式的 videoId -> workId 映射
       // 创建多种格式的 videoId -> workId 映射
       const videoIdToWork = new Map<string, { id: number; title: string }>();
       const videoIdToWork = new Map<string, { id: number; title: string }>();
@@ -603,7 +612,7 @@ export class CommentService {
         }
         }
         
         
         if (matchedWorkId) {
         if (matchedWorkId) {
-          await this.commentRepository.update(comment.id, { workId: matchedWorkId });
+          await this.commentRepository.update({ id: comment.id, userId }, { workId: matchedWorkId });
           fixedCount++;
           fixedCount++;
           logger.info(`Fixed comment ${comment.id} (videoId: ${comment.videoId}) -> workId: ${matchedWorkId}`);
           logger.info(`Fixed comment ${comment.id} (videoId: ${comment.videoId}) -> workId: ${matchedWorkId}`);
         }
         }

+ 40 - 16
server/src/services/HeadlessBrowserService.ts

@@ -123,6 +123,7 @@ export interface CookieData {
   value: string;
   value: string;
   domain: string;
   domain: string;
   path: string;
   path: string;
+  sameSite?: string;
 }
 }
 
 
 export type CookieCheckSource = 'api' | 'browser';
 export type CookieCheckSource = 'api' | 'browser';
@@ -341,6 +342,29 @@ class HeadlessBrowserService {
     return referers[platform] || '';
     return referers[platform] || '';
   }
   }
 
 
+  private normalizePlaywrightCookies(
+    cookies: CookieData[]
+  ): Array<Omit<CookieData, 'sameSite'> & { sameSite?: 'Strict' | 'Lax' | 'None' }> {
+    return cookies.map((cookie) => {
+      const { sameSite, ...baseCookie } = cookie;
+      let normalizedSameSite: 'Strict' | 'Lax' | 'None' | undefined;
+
+      if (sameSite) {
+        const value = sameSite.toLowerCase();
+        if (value === 'strict') normalizedSameSite = 'Strict';
+        else if (value === 'lax') normalizedSameSite = 'Lax';
+        else if (value === 'none' || value === 'no_restriction') normalizedSameSite = 'None';
+        else if (value !== 'unspecified') {
+          logger.warn(`[Cookie] Invalid sameSite value: ${sameSite}, omitting sameSite`);
+        }
+      }
+
+      return normalizedSameSite
+        ? { ...baseCookie, sameSite: normalizedSameSite }
+        : baseCookie;
+    });
+  }
+
   /**
   /**
    * 通过浏览器检查 Cookie 是否有效(检查是否被重定向到登录页)
    * 通过浏览器检查 Cookie 是否有效(检查是否被重定向到登录页)
    * 注意:网络错误或服务不可用时返回 true(保持原状态),避免误判为过期
    * 注意:网络错误或服务不可用时返回 true(保持原状态),避免误判为过期
@@ -361,7 +385,7 @@ class HeadlessBrowserService {
         userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
         userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
       });
       });
 
 
-      await context.addCookies(cookies);
+      await context.addCookies(this.normalizePlaywrightCookies(cookies));
       const page = await context.newPage();
       const page = await context.newPage();
 
 
       const config = this.getPlatformConfig(platform);
       const config = this.getPlatformConfig(platform);
@@ -468,7 +492,7 @@ class HeadlessBrowserService {
         userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
         userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
       });
       });
 
 
-      await context.addCookies(cookies);
+      await context.addCookies(this.normalizePlaywrightCookies(cookies));
       page = await context.newPage();
       page = await context.newPage();
 
 
       // 绑定监听器
       // 绑定监听器
@@ -658,7 +682,7 @@ class HeadlessBrowserService {
         userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
         userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
       });
       });
 
 
-      await context.addCookies(cookies);
+      await context.addCookies(this.normalizePlaywrightCookies(cookies));
       const page = await context.newPage();
       const page = await context.newPage();
 
 
       const config = this.getPlatformConfig(platform);
       const config = this.getPlatformConfig(platform);
@@ -2605,12 +2629,12 @@ class HeadlessBrowserService {
       });
       });
 
 
       // 设置 Cookie
       // 设置 Cookie
-      const playwrightCookies = cookies.map(c => ({
+      const playwrightCookies = this.normalizePlaywrightCookies(cookies.map(c => ({
         name: c.name,
         name: c.name,
         value: c.value,
         value: c.value,
         domain: c.domain || '.douyin.com',
         domain: c.domain || '.douyin.com',
         path: c.path || '/',
         path: c.path || '/',
-      }));
+      })));
       await context.addCookies(playwrightCookies);
       await context.addCookies(playwrightCookies);
 
 
       const page = await context.newPage();
       const page = await context.newPage();
@@ -2954,12 +2978,12 @@ class HeadlessBrowserService {
       });
       });
 
 
       // 设置 Cookie
       // 设置 Cookie
-      const playwrightCookies = cookies.map(c => ({
+      const playwrightCookies = this.normalizePlaywrightCookies(cookies.map(c => ({
         name: c.name,
         name: c.name,
         value: c.value,
         value: c.value,
         domain: c.domain || '.douyin.com',
         domain: c.domain || '.douyin.com',
         path: c.path || '/',
         path: c.path || '/',
-      }));
+      })));
       await context.addCookies(playwrightCookies);
       await context.addCookies(playwrightCookies);
       logger.info(`[API Interception] Set ${playwrightCookies.length} cookies`);
       logger.info(`[API Interception] Set ${playwrightCookies.length} cookies`);
 
 
@@ -3831,12 +3855,12 @@ class HeadlessBrowserService {
       });
       });
 
 
       // 设置 Cookie
       // 设置 Cookie
-      const playwrightCookies = cookies.map(c => ({
+      const playwrightCookies = this.normalizePlaywrightCookies(cookies.map(c => ({
         name: c.name,
         name: c.name,
         value: c.value,
         value: c.value,
         domain: c.domain || '.douyin.com',
         domain: c.domain || '.douyin.com',
         path: c.path || '/',
         path: c.path || '/',
-      }));
+      })));
       await context.addCookies(playwrightCookies);
       await context.addCookies(playwrightCookies);
       logger.info(`Set ${playwrightCookies.length} cookies`);
       logger.info(`Set ${playwrightCookies.length} cookies`);
 
 
@@ -4308,7 +4332,7 @@ class HeadlessBrowserService {
   /**
   /**
    * 通过 Node ????? 获取小红书评论 - 一次性获取所有作品的评论
    * 通过 Node ????? 获取小红书评论 - 一次性获取所有作品的评论
    */
    */
-    async fetchXiaohongshuCommentsViaApi(cookies: CookieData[]): Promise<WorkComments[]> {
+  async fetchXiaohongshuCommentsViaApi(cookies: CookieData[]): Promise<WorkComments[]> {
     const browser = await chromium.launch({
     const browser = await chromium.launch({
       headless: true,
       headless: true,
       args: ['--no-sandbox', '--disable-setuid-sandbox'],
       args: ['--no-sandbox', '--disable-setuid-sandbox'],
@@ -4323,12 +4347,12 @@ class HeadlessBrowserService {
       });
       });
 
 
       // 设置 Cookie
       // 设置 Cookie
-      const playwrightCookies = cookies.map(c => ({
+      const playwrightCookies = this.normalizePlaywrightCookies(cookies.map(c => ({
         name: c.name,
         name: c.name,
         value: c.value,
         value: c.value,
         domain: c.domain || '.xiaohongshu.com',
         domain: c.domain || '.xiaohongshu.com',
         path: c.path || '/',
         path: c.path || '/',
-      }));
+      })));
       await context.addCookies(playwrightCookies);
       await context.addCookies(playwrightCookies);
       logger.info(`[Xiaohongshu Comments] Set ${playwrightCookies.length} cookies`);
       logger.info(`[Xiaohongshu Comments] Set ${playwrightCookies.length} cookies`);
 
 
@@ -4541,14 +4565,14 @@ class HeadlessBrowserService {
           'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
           'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
       });
       });
 
 
-      await context.addCookies(
+      await context.addCookies(this.normalizePlaywrightCookies(
         cookies.map((cookie) => ({
         cookies.map((cookie) => ({
           name: cookie.name,
           name: cookie.name,
           value: cookie.value,
           value: cookie.value,
           domain: cookie.domain || '.baidu.com',
           domain: cookie.domain || '.baidu.com',
           path: cookie.path || '/',
           path: cookie.path || '/',
         }))
         }))
-      );
+      ));
 
 
       const page = await context.newPage();
       const page = await context.newPage();
       await page.goto('https://baijiahao.baidu.com/builder/rc/commentmanage/comment/all', {
       await page.goto('https://baijiahao.baidu.com/builder/rc/commentmanage/comment/all', {
@@ -4677,12 +4701,12 @@ class HeadlessBrowserService {
       });
       });
 
 
       // 设置 Cookie
       // 设置 Cookie
-      const playwrightCookies = cookies.map(c => ({
+      const playwrightCookies = this.normalizePlaywrightCookies(cookies.map(c => ({
         name: c.name,
         name: c.name,
         value: c.value,
         value: c.value,
         domain: c.domain || '.weixin.qq.com',
         domain: c.domain || '.weixin.qq.com',
         path: c.path || '/',
         path: c.path || '/',
-      }));
+      })));
       await context.addCookies(playwrightCookies);
       await context.addCookies(playwrightCookies);
       logger.info(`[Weixin Video Comments] Set ${playwrightCookies.length} cookies`);
       logger.info(`[Weixin Video Comments] Set ${playwrightCookies.length} cookies`);
 
 

+ 57 - 46
server/src/services/PublishService.ts

@@ -22,6 +22,7 @@ import path from 'path';
 import { config } from '../config/index.js';
 import { config } from '../config/index.js';
 import { CookieManager } from '../automation/cookie.js';
 import { CookieManager } from '../automation/cookie.js';
 import { taskQueueService } from './TaskQueueService.js';
 import { taskQueueService } from './TaskQueueService.js';
+import { In } from 'typeorm';
 
 
 interface GetTasksParams {
 interface GetTasksParams {
   page: number;
   page: number;
@@ -89,6 +90,38 @@ export class PublishService {
     return adapter;
     return adapter;
   }
   }
 
 
+  private normalizeTargetAccountIds(targetAccounts: unknown): number[] {
+    if (!Array.isArray(targetAccounts)) {
+      return [];
+    }
+
+    const ids = targetAccounts
+      .map((accountId) => Number(accountId))
+      .filter((accountId) => Number.isInteger(accountId) && accountId > 0);
+
+    return [...new Set(ids)];
+  }
+
+  private async getOwnedTargetAccountIds(
+    userId: number,
+    requestedAccountIds: number[]
+  ): Promise<{ validAccountIds: number[]; invalidAccountIds: number[] }> {
+    if (requestedAccountIds.length === 0) {
+      return { validAccountIds: [], invalidAccountIds: [] };
+    }
+
+    const accounts = await this.accountRepository.find({
+      where: { id: In(requestedAccountIds), userId },
+      select: ['id'],
+    });
+    const ownedAccountIds = new Set(accounts.map((account) => account.id));
+
+    return {
+      validAccountIds: requestedAccountIds.filter((accountId) => ownedAccountIds.has(accountId)),
+      invalidAccountIds: requestedAccountIds.filter((accountId) => !ownedAccountIds.has(accountId)),
+    };
+  }
+
   private resolvePublishText(task: PublishTask, platform: PlatformType): { title: string; description: string } {
   private resolvePublishText(task: PublishTask, platform: PlatformType): { title: string; description: string } {
     const taskTitle = String(task.title || '').trim();
     const taskTitle = String(task.title || '').trim();
     const taskDescription = task.description == null ? '' : String(task.description).trim();
     const taskDescription = task.description == null ? '' : String(task.description).trim();
@@ -218,36 +251,25 @@ export class PublishService {
 
 
   async createTask(userId: number, data: CreatePublishTaskRequest): Promise<PublishTaskType> {
   async createTask(userId: number, data: CreatePublishTaskRequest): Promise<PublishTaskType> {
     // 验证目标账号是否存在
     // 验证目标账号是否存在
-    const validAccountIds: number[] = [];
-    const invalidAccountIds: number[] = [];
-
-    for (const accountId of data.targetAccounts) {
-      const account = await this.accountRepository.findOne({
-        where: { id: accountId, userId }
-      });
-      if (account) {
-        validAccountIds.push(accountId);
-      } else {
-        invalidAccountIds.push(accountId);
-        logger.warn(`[PublishService] Account ${accountId} not found or not owned by user ${userId}, skipping`);
-      }
-    }
+    const requestedAccountIds = this.normalizeTargetAccountIds(data.targetAccounts);
+    const { validAccountIds, invalidAccountIds } = await this.getOwnedTargetAccountIds(userId, requestedAccountIds);
 
 
     if (validAccountIds.length === 0) {
     if (validAccountIds.length === 0) {
       throw new AppError('所选账号不存在或已被删除', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION);
       throw new AppError('所选账号不存在或已被删除', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION);
     }
     }
 
 
     // 去重(修复#6034:作品数可能重复)
     // 去重(修复#6034:作品数可能重复)
-    const dedupAccountIds = [...new Set(validAccountIds)];
+    const dedupAccountIds = validAccountIds;
 
 
     if (invalidAccountIds.length > 0) {
     if (invalidAccountIds.length > 0) {
       logger.warn(`[PublishService] ${invalidAccountIds.length} invalid accounts skipped: ${invalidAccountIds.join(', ')}`);
       logger.warn(`[PublishService] ${invalidAccountIds.length} invalid accounts skipped: ${invalidAccountIds.join(', ')}`);
     }
     }
 
 
+    const videoPath = data.videoPath || '';
     const task = this.taskRepository.create({
     const task = this.taskRepository.create({
       userId,
       userId,
-      videoPath: data.videoPath,
-      videoFilename: data.videoPath.split('/').pop() || null,
+      videoPath,
+      videoFilename: videoPath ? videoPath.split(/[\\/]/).pop() || null : null,
       title: data.title,
       title: data.title,
       description: data.description || null,
       description: data.description || null,
       coverPath: data.coverPath || null,
       coverPath: data.coverPath || null,
@@ -262,12 +284,12 @@ export class PublishService {
     await this.taskRepository.save(task);
     await this.taskRepository.save(task);
 
 
     // 创建发布结果记录(只为有效且去重的账号创建)
     // 创建发布结果记录(只为有效且去重的账号创建)
-    for (const accountId of dedupAccountIds) {
-      const result = this.resultRepository.create({
-        taskId: task.id,
-        accountId,
-      });
-      await this.resultRepository.save(result);
+    const results = dedupAccountIds.map((accountId) => this.resultRepository.create({
+      taskId: task.id,
+      accountId,
+    }));
+    if (results.length > 0) {
+      await this.resultRepository.save(results);
     }
     }
 
 
     // 通知客户端
     // 通知客户端
@@ -299,25 +321,14 @@ export class PublishService {
     const requestedAccounts = Array.isArray(data.targetAccounts)
     const requestedAccounts = Array.isArray(data.targetAccounts)
       ? data.targetAccounts
       ? data.targetAccounts
       : (Array.isArray(task.targetAccounts) ? task.targetAccounts : []);
       : (Array.isArray(task.targetAccounts) ? task.targetAccounts : []);
-    const validAccountIds: number[] = [];
-    const invalidAccountIds: number[] = [];
-
-    for (const accountId of requestedAccounts) {
-      const account = await this.accountRepository.findOne({
-        where: { id: accountId, userId },
-      });
-      if (account) {
-        validAccountIds.push(accountId);
-      } else {
-        invalidAccountIds.push(accountId);
-      }
-    }
+    const requestedAccountIds = this.normalizeTargetAccountIds(requestedAccounts);
+    const { validAccountIds, invalidAccountIds } = await this.getOwnedTargetAccountIds(userId, requestedAccountIds);
 
 
     if (validAccountIds.length === 0) {
     if (validAccountIds.length === 0) {
       throw new AppError('所选账号不存在或已被删除', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION);
       throw new AppError('所选账号不存在或已被删除', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION);
     }
     }
 
 
-    const dedupAccountIds = [...new Set(validAccountIds)];
+    const dedupAccountIds = validAccountIds;
     if (invalidAccountIds.length > 0) {
     if (invalidAccountIds.length > 0) {
       logger.warn(`[PublishService] ${invalidAccountIds.length} invalid accounts skipped on update: ${invalidAccountIds.join(', ')}`);
       logger.warn(`[PublishService] ${invalidAccountIds.length} invalid accounts skipped on update: ${invalidAccountIds.join(', ')}`);
     }
     }
@@ -326,7 +337,7 @@ export class PublishService {
     const nextTitle = data.title ?? task.title ?? '';
     const nextTitle = data.title ?? task.title ?? '';
 
 
     task.videoPath = nextVideoPath;
     task.videoPath = nextVideoPath;
-    task.videoFilename = nextVideoPath ? nextVideoPath.split('/').pop() || null : null;
+    task.videoFilename = nextVideoPath ? nextVideoPath.split(/[\\/]/).pop() || null : null;
     task.title = nextTitle;
     task.title = nextTitle;
     task.description = data.description ?? task.description ?? null;
     task.description = data.description ?? task.description ?? null;
     task.coverPath = data.coverPath ?? task.coverPath ?? null;
     task.coverPath = data.coverPath ?? task.coverPath ?? null;
@@ -341,9 +352,9 @@ export class PublishService {
     await this.taskRepository.save(task);
     await this.taskRepository.save(task);
 
 
     await this.resultRepository.delete({ taskId });
     await this.resultRepository.delete({ taskId });
-    for (const accountId of dedupAccountIds) {
-      const result = this.resultRepository.create({ taskId: task.id, accountId });
-      await this.resultRepository.save(result);
+    const results = dedupAccountIds.map((accountId) => this.resultRepository.create({ taskId: task.id, accountId }));
+    if (results.length > 0) {
+      await this.resultRepository.save(results);
     }
     }
 
 
     wsManager.sendToUser(userId, WS_EVENTS.TASK_CREATED, { task: this.formatTask(task) });
     wsManager.sendToUser(userId, WS_EVENTS.TASK_CREATED, { task: this.formatTask(task) });
@@ -447,7 +458,7 @@ export class PublishService {
       try {
       try {
         // 获取账号信息
         // 获取账号信息
         const account = await this.accountRepository.findOne({
         const account = await this.accountRepository.findOne({
-          where: { id: result.accountId },
+          where: { id: result.accountId, userId },
         });
         });
 
 
         if (!account) {
         if (!account) {
@@ -657,9 +668,9 @@ export class PublishService {
     if (successCount > 0) {
     if (successCount > 0) {
       // 为每个成功的账号创建同步任务
       // 为每个成功的账号创建同步任务
       for (const accountId of successAccountIds) {
       for (const accountId of successAccountIds) {
-        const account = await this.accountRepository.findOne({ where: { id: accountId } });
+        const account = await this.accountRepository.findOne({ where: { id: accountId, userId } });
         if (account) {
         if (account) {
-          taskQueueService.createTask(userId, {
+          await taskQueueService.createTask(userId, {
             type: 'sync_works',
             type: 'sync_works',
             title: `同步作品 - ${account.accountName || '账号'}`,
             title: `同步作品 - ${account.accountName || '账号'}`,
             accountId: account.id,
             accountId: account.id,
@@ -721,7 +732,7 @@ export class PublishService {
       { status: null, errorMessage: null }
       { status: null, errorMessage: null }
     );
     );
 
 
-    const updated = await this.taskRepository.findOne({ where: { id: taskId } });
+    const updated = await this.taskRepository.findOne({ where: { id: taskId, userId } });
 
 
     wsManager.sendToUser(userId, WS_EVENTS.TASK_STATUS_CHANGED, {
     wsManager.sendToUser(userId, WS_EVENTS.TASK_STATUS_CHANGED, {
       taskId,
       taskId,
@@ -750,7 +761,7 @@ export class PublishService {
     }
     }
 
 
     const account = await this.accountRepository.findOne({
     const account = await this.accountRepository.findOne({
-      where: { id: accountId },
+      where: { id: accountId, userId },
     });
     });
     if (!account) {
     if (!account) {
       throw new AppError('Account not found', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND);
       throw new AppError('Account not found', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND);

+ 6 - 0
server/src/services/RedisTaskQueue.ts

@@ -48,6 +48,7 @@ class RedisTaskQueueService {
 
 
   // 最大并行任务数
   // 最大并行任务数
   private concurrency = 5;
   private concurrency = 5;
+  private maxTasksPerUser = 100;
 
 
   constructor() {
   constructor() {
     // 创建队列
     // 创建队列
@@ -200,6 +201,7 @@ class RedisTaskQueueService {
       this.notifyUser(userId, TASK_WS_EVENTS.TASK_COMPLETED, {
       this.notifyUser(userId, TASK_WS_EVENTS.TASK_COMPLETED, {
         task: this.getTaskFromCache(userId, taskData.id)
         task: this.getTaskFromCache(userId, taskData.id)
       });
       });
+      this.cleanupCompletedTasks(userId, this.maxTasksPerUser);
 
 
       return result;
       return result;
     } catch (error) {
     } catch (error) {
@@ -216,6 +218,7 @@ class RedisTaskQueueService {
       this.notifyUser(userId, TASK_WS_EVENTS.TASK_FAILED, {
       this.notifyUser(userId, TASK_WS_EVENTS.TASK_FAILED, {
         task: this.getTaskFromCache(userId, taskData.id)
         task: this.getTaskFromCache(userId, taskData.id)
       });
       });
+      this.cleanupCompletedTasks(userId, this.maxTasksPerUser);
 
 
       throw error;
       throw error;
     }
     }
@@ -241,6 +244,7 @@ class RedisTaskQueueService {
       status: 'pending',
       status: 'pending',
       progress: 0,
       progress: 0,
       priority: request.priority || 'normal',
       priority: request.priority || 'normal',
+      silent: request.silent || false,
       createdAt: new Date().toISOString(),
       createdAt: new Date().toISOString(),
       accountId: request.accountId,
       accountId: request.accountId,
       platform: request.platform,
       platform: request.platform,
@@ -253,6 +257,7 @@ class RedisTaskQueueService {
       this.userTasks.set(userId, []);
       this.userTasks.set(userId, []);
     }
     }
     this.userTasks.get(userId)!.push(task);
     this.userTasks.get(userId)!.push(task);
+    this.cleanupCompletedTasks(userId, this.maxTasksPerUser);
 
 
     // 添加到 Redis 队列
     // 添加到 Redis 队列
     const priority = request.priority === 'high' ? 1 : (request.priority === 'low' ? 3 : 2);
     const priority = request.priority === 'high' ? 1 : (request.priority === 'low' ? 3 : 2);
@@ -307,6 +312,7 @@ class RedisTaskQueueService {
 
 
       // 通知前端
       // 通知前端
       this.notifyUser(userId, TASK_WS_EVENTS.TASK_CANCELLED, { task });
       this.notifyUser(userId, TASK_WS_EVENTS.TASK_CANCELLED, { task });
+      this.cleanupCompletedTasks(userId, this.maxTasksPerUser);
 
 
       logger.info(`Task cancelled: ${taskId}`);
       logger.info(`Task cancelled: ${taskId}`);
       return true;
       return true;

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

@@ -48,6 +48,7 @@ class TaskQueueService {
     this.cleanupTimer = setInterval(() => {
     this.cleanupTimer = setInterval(() => {
       this.performPeriodicCleanup();
       this.performPeriodicCleanup();
     }, 2 * 60 * 60 * 1000);
     }, 2 * 60 * 60 * 1000);
+    this.cleanupTimer.unref();
   }
   }
 
 
   /**
   /**
@@ -107,6 +108,7 @@ class TaskQueueService {
       this.userTasks.set(userId, []);
       this.userTasks.set(userId, []);
     }
     }
     this.userTasks.get(userId)!.push(task);
     this.userTasks.get(userId)!.push(task);
+    this.cleanupCompletedTasks(userId, this.maxTasksPerUser);
 
 
     // 通知前端任务已创建
     // 通知前端任务已创建
     this.notifyUser(userId, TASK_WS_EVENTS.TASK_CREATED, { task });
     this.notifyUser(userId, TASK_WS_EVENTS.TASK_CREATED, { task });

+ 103 - 182
server/src/services/WorkService.ts

@@ -126,9 +126,6 @@ export class WorkService {
     logger.info(`[SyncWorks] Starting sync for userId: ${userId}, accountId: ${accountId || 'all'}, platform: ${platform || 'all'}`);
     logger.info(`[SyncWorks] Starting sync for userId: ${userId}, accountId: ${accountId || 'all'}, platform: ${platform || 'all'}`);
 
 
     // 先查看所有账号(调试用)
     // 先查看所有账号(调试用)
-    const allAccounts = await this.accountRepository.find({ where: { userId } });
-    logger.info(`[SyncWorks] All accounts for user ${userId}: ${allAccounts.map(a => `id=${a.id},status=${a.status},platform=${a.platform}`).join('; ')}`);
-
     // 同时查询 active 和 expired 状态的账号(expired 的账号 cookie 可能实际上还有效)
     // 同时查询 active 和 expired 状态的账号(expired 的账号 cookie 可能实际上还有效)
     const queryBuilder = this.accountRepository
     const queryBuilder = this.accountRepository
       .createQueryBuilder('account')
       .createQueryBuilder('account')
@@ -181,7 +178,7 @@ export class WorkService {
 
 
         // 如果同步成功且账号状态是 expired,则恢复为 active
         // 如果同步成功且账号状态是 expired,则恢复为 active
         if (result.syncedCount > 0 && account.status === 'expired') {
         if (result.syncedCount > 0 && account.status === 'expired') {
-          await this.accountRepository.update(account.id, { status: 'active' });
+          await this.accountRepository.update({ id: account.id, userId }, { status: 'active' });
           logger.info(`[SyncWorks] Account ${account.id} status restored to active`);
           logger.info(`[SyncWorks] Account ${account.id} status restored to active`);
         }
         }
       } catch (error) {
       } catch (error) {
@@ -271,7 +268,7 @@ export class WorkService {
         accountUpdate.avatarUrl = accountInfo.avatarUrl;
         accountUpdate.avatarUrl = accountInfo.avatarUrl;
       }
       }
       if (Object.keys(accountUpdate).length > 0) {
       if (Object.keys(accountUpdate).length > 0) {
-        await this.accountRepository.update(account.id, accountUpdate as any);
+        await this.accountRepository.update({ id: account.id, userId }, accountUpdate as any);
         logger.info(`[SyncAccountWorks] Updated account info: ${Object.keys(accountUpdate).join(', ')}`);
         logger.info(`[SyncAccountWorks] Updated account info: ${Object.keys(accountUpdate).join(', ')}`);
         // Bug #6075: 改用前端监听的 ACCOUNT_DATA_CHANGED 事件,确保前端能刷新账号数据(头像、昵称等)
         // Bug #6075: 改用前端监听的 ACCOUNT_DATA_CHANGED 事件,确保前端能刷新账号数据(头像、昵称等)
         wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_DATA_CHANGED, {
         wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_DATA_CHANGED, {
@@ -339,11 +336,11 @@ export class WorkService {
           });
           });
           if (legacyWork && legacyWork.id !== work.id) {
           if (legacyWork && legacyWork.id !== work.id) {
             await AppDataSource.getRepository(Comment).update(
             await AppDataSource.getRepository(Comment).update(
-              { workId: legacyWork.id },
+              { workId: legacyWork.id, userId },
               { workId: work.id }
               { workId: work.id }
             );
             );
             await this.workDayStatisticsService.deleteByWorkId(legacyWork.id);
             await this.workDayStatisticsService.deleteByWorkId(legacyWork.id);
-            await this.workRepository.delete(legacyWork.id);
+            await this.workRepository.delete({ id: legacyWork.id, userId });
           }
           }
         }
         }
 
 
@@ -359,14 +356,14 @@ export class WorkService {
 
 
             if (canonicalWork) {
             if (canonicalWork) {
               await AppDataSource.getRepository(Comment).update(
               await AppDataSource.getRepository(Comment).update(
-                { workId: legacyWork.id },
+                { workId: legacyWork.id, userId },
                 { workId: canonicalWork.id }
                 { workId: canonicalWork.id }
               );
               );
               await this.workDayStatisticsService.deleteByWorkId(legacyWork.id);
               await this.workDayStatisticsService.deleteByWorkId(legacyWork.id);
-              await this.workRepository.delete(legacyWork.id);
+              await this.workRepository.delete({ id: legacyWork.id, userId });
               work = canonicalWork;
               work = canonicalWork;
             } else {
             } else {
-              await this.workRepository.update(legacyWork.id, {
+              await this.workRepository.update({ id: legacyWork.id, userId }, {
                 platformVideoId: canonicalVideoId,
                 platformVideoId: canonicalVideoId,
               });
               });
               work = { ...legacyWork, platformVideoId: canonicalVideoId };
               work = { ...legacyWork, platformVideoId: canonicalVideoId };
@@ -431,7 +428,7 @@ export class WorkService {
     }
     }
 
 
     if (platform === 'weixin_video') {
     if (platform === 'weixin_video') {
-      await this.dedupeWeixinVideoWorks(account.id);
+      await this.dedupeWeixinVideoWorks(account.id, userId);
     }
     }
 
 
     // 删除本地存在但远程已删除的作品
     // 删除本地存在但远程已删除的作品
@@ -455,7 +452,8 @@ export class WorkService {
       skipLocalDeletions = true;
       skipLocalDeletions = true;
     } else {
     } else {
       const localWorks = await this.workRepository.find({
       const localWorks = await this.workRepository.find({
-        where: { accountId: account.id },
+        where: { accountId: account.id, userId },
+        select: ['id', 'title', 'platformVideoId'],
       });
       });
 
 
       if (platform === 'weixin_video') {
       if (platform === 'weixin_video') {
@@ -483,9 +481,9 @@ export class WorkService {
         let deletedCount = 0;
         let deletedCount = 0;
         for (const localWork of localWorks) {
         for (const localWork of localWorks) {
           if (!remotePlatformVideoIds.has(localWork.platformVideoId)) {
           if (!remotePlatformVideoIds.has(localWork.platformVideoId)) {
-            await AppDataSource.getRepository(Comment).delete({ workId: localWork.id });
+            await AppDataSource.getRepository(Comment).delete({ workId: localWork.id, userId });
             await this.workDayStatisticsService.deleteByWorkId(localWork.id);
             await this.workDayStatisticsService.deleteByWorkId(localWork.id);
-            await this.workRepository.delete(localWork.id);
+            await this.workRepository.delete({ id: localWork.id, userId });
             deletedCount++;
             deletedCount++;
             logger.info(`Deleted work ${localWork.id} (${localWork.title}) - no longer exists on platform`);
             logger.info(`Deleted work ${localWork.id} (${localWork.title}) - no longer exists on platform`);
           }
           }
@@ -505,165 +503,7 @@ export class WorkService {
     // }
     // }
 
 
     // 小红书:如果是新作品且 work_day_statistics 中尚无任何记录,则补首批日统计 & works.yesterday_*(不受14天限制)
     // 小红书:如果是新作品且 work_day_statistics 中尚无任何记录,则补首批日统计 & works.yesterday_*(不受14天限制)
-    if (platform === 'xiaohongshu') {
-      try {
-        const works = await this.workRepository.find({
-          where: { accountId: account.id, platform },
-          select: ['id'],
-        });
-        const workIds = works.map((w) => w.id);
-        if (workIds.length > 0) {
-          const rows = await AppDataSource.getRepository(WorkDayStatistics)
-            .createQueryBuilder('wds')
-            .select('DISTINCT wds.work_id', 'workId')
-            .where('wds.work_id IN (:...ids)', { ids: workIds })
-            .getRawMany();
-          const hasStats = new Set<number>(rows.map((r: any) => Number(r.workId)));
-          const needInitIds = workIds.filter((id) => !hasStats.has(id));
-
-          if (needInitIds.length > 0) {
-            logger.info(
-              `[SyncAccountWorks] XHS account ${account.id} has ${needInitIds.length} works without statistics, running initial note/base import.`
-            );
-            // 放入任务队列异步执行,避免阻塞同步作品流程
-            taskQueueService.createTask(account.userId, {
-              type: 'xhs_work_stats_backfill',
-              title: `小红书作品补数(${needInitIds.length})`,
-              accountId: account.id,
-              platform: 'xiaohongshu',
-              data: {
-                workIds: needInitIds,
-              },
-            });
-          }
-        }
-      } catch (err) {
-        logger.error(
-          `[SyncAccountWorks] Failed to backfill XHS work_day_statistics for account ${account.id}:`,
-          err
-        );
-      }
-    }
-
-    // 抖音:如果是新作品且 work_day_statistics 中尚无任何记录,则异步补齐历史日统计 & works.yesterday_*(使用 DouyinWorkStatisticsImportService)
-    if (platform === 'douyin') {
-      try {
-        const works = await this.workRepository.find({
-          where: { accountId: account.id, platform },
-          select: ['id'],
-        });
-        const workIds = works.map((w) => w.id);
-        if (workIds.length > 0) {
-          const rows = await AppDataSource.getRepository(WorkDayStatistics)
-            .createQueryBuilder('wds')
-            .select('DISTINCT wds.work_id', 'workId')
-            .where('wds.work_id IN (:...ids)', { ids: workIds })
-            .getRawMany();
-          const hasStats = new Set<number>(rows.map((r: any) => Number(r.workId)));
-          const needInitIds = workIds.filter((id) => !hasStats.has(id));
-
-          if (needInitIds.length > 0) {
-            logger.info(
-              `[SyncAccountWorks] DY account ${account.id} has ${needInitIds.length} works without statistics, enqueue dy_work_stats_backfill task.`
-            );
-            taskQueueService.createTask(account.userId, {
-              type: 'dy_work_stats_backfill',
-              title: `抖音作品补数(${needInitIds.length})`,
-              accountId: account.id,
-              platform: 'douyin',
-              data: {
-                workIds: needInitIds,
-              },
-            });
-          }
-        }
-      } catch (err) {
-        logger.error(
-          `[SyncAccountWorks] Failed to enqueue DY work_day_statistics backfill for account ${account.id}:`,
-          err
-        );
-      }
-    }
-
-    // 百家号:如果是新作品且 work_day_statistics 中尚无任何记录,则异步补齐历史日统计 & works.yesterday_*(使用 BaijiahaoWorkDailyStatisticsImportService)
-    if (platform === 'baijiahao') {
-      try {
-        const works = await this.workRepository.find({
-          where: { accountId: account.id, platform },
-          select: ['id'],
-        });
-        const workIds = works.map((w) => w.id);
-        if (workIds.length > 0) {
-          const rows = await AppDataSource.getRepository(WorkDayStatistics)
-            .createQueryBuilder('wds')
-            .select('DISTINCT wds.work_id', 'workId')
-            .where('wds.work_id IN (:...ids)', { ids: workIds })
-            .getRawMany();
-          const hasStats = new Set<number>(rows.map((r: any) => Number(r.workId)));
-          const needInitIds = workIds.filter((id) => !hasStats.has(id));
-
-          if (needInitIds.length > 0) {
-            logger.info(
-              `[SyncAccountWorks] BJ account ${account.id} has ${needInitIds.length} works without statistics, enqueue bj_work_stats_backfill task.`
-            );
-            taskQueueService.createTask(account.userId, {
-              type: 'bj_work_stats_backfill',
-              title: `百家号作品补数(${needInitIds.length})`,
-              accountId: account.id,
-              platform: 'baijiahao',
-              data: {
-                workIds: needInitIds,
-              },
-            });
-          }
-        }
-      } catch (err) {
-        logger.error(
-          `[SyncAccountWorks] Failed to enqueue BJ work_day_statistics backfill for account ${account.id}:`,
-          err
-        );
-      }
-    }
-
-    // 视频号:如果是新作品且 work_day_statistics 中尚无任何记录,则异步补齐日统计 & works.yesterday_*(使用 WeixinVideoWorkStatisticsImportService)
-    if (platform === 'weixin_video') {
-      try {
-        const works = await this.workRepository.find({
-          where: { accountId: account.id, platform },
-          select: ['id'],
-        });
-        const workIds = works.map((w) => w.id);
-        if (workIds.length > 0) {
-          const rows = await AppDataSource.getRepository(WorkDayStatistics)
-            .createQueryBuilder('wds')
-            .select('DISTINCT wds.work_id', 'workId')
-            .where('wds.work_id IN (:...ids)', { ids: workIds })
-            .getRawMany();
-          const hasStats = new Set<number>(rows.map((r: any) => Number(r.workId)));
-          const needInitIds = workIds.filter((id) => !hasStats.has(id));
-
-          if (needInitIds.length > 0) {
-            logger.info(
-              `[SyncAccountWorks] WX account ${account.id} has ${needInitIds.length} works without statistics, enqueue wx_work_stats_backfill task.`
-            );
-            taskQueueService.createTask(account.userId, {
-              type: 'wx_work_stats_backfill',
-              title: `视频号作品补数(${needInitIds.length})`,
-              accountId: account.id,
-              platform: 'weixin_video',
-              data: {
-                workIds: needInitIds,
-              },
-            });
-          }
-        }
-      } catch (err) {
-        logger.error(
-          `[SyncAccountWorks] Failed to enqueue WX work_day_statistics backfill for account ${account.id}:`,
-          err
-        );
-      }
-    }
+    await this.enqueueInitialWorkStatsBackfill(account, platform);
 
 
     return {
     return {
       syncedCount,
       syncedCount,
@@ -674,6 +514,84 @@ export class WorkService {
   }
   }
 
 
   /**
   /**
+   * Find works without day statistics once and enqueue the matching backfill task.
+   */
+  private async enqueueInitialWorkStatsBackfill(
+    account: PlatformAccount,
+    platform: PlatformType
+  ): Promise<void> {
+    const configByPlatform = {
+      xiaohongshu: {
+        type: 'xhs_work_stats_backfill',
+        label: 'XHS',
+      },
+      douyin: {
+        type: 'dy_work_stats_backfill',
+        label: 'DY',
+      },
+      baijiahao: {
+        type: 'bj_work_stats_backfill',
+        label: 'BJ',
+      },
+      weixin_video: {
+        type: 'wx_work_stats_backfill',
+        label: 'WX',
+      },
+    } as const;
+
+    const backfillConfig = configByPlatform[platform as keyof typeof configByPlatform];
+    if (!backfillConfig) {
+      return;
+    }
+
+    try {
+      const rows = await this.workRepository
+        .createQueryBuilder('work')
+        .select('work.id', 'id')
+        .leftJoin(WorkDayStatistics, 'wds', 'wds.workId = work.id')
+        .where('work.accountId = :accountId', { accountId: account.id })
+        .andWhere('work.userId = :userId', { userId: account.userId })
+        .andWhere('work.platform = :platform', { platform })
+        .andWhere('wds.id IS NULL')
+        .getRawMany<{ id: number | string }>();
+
+      const workIds = rows
+        .map((row) => Number(row.id))
+        .filter((id) => Number.isFinite(id) && id > 0);
+
+      if (workIds.length === 0) {
+        return;
+      }
+
+      logger.info(
+        `[SyncAccountWorks] ${backfillConfig.label} account ${account.id} has ${workIds.length} works without statistics, enqueue backfill task.`
+      );
+
+      const activeBackfillExists = taskQueueService
+        .getActiveTasks(account.userId)
+        .some((task) => task.type === backfillConfig.type && task.accountId === account.id);
+
+      if (activeBackfillExists) {
+        logger.info(`[SyncAccountWorks] ${backfillConfig.label} backfill for account ${account.id} is already active, skip enqueue.`);
+        return;
+      }
+
+      await taskQueueService.createTask(account.userId, {
+        type: backfillConfig.type,
+        title: `${platform} work stats backfill (${workIds.length})`,
+        accountId: account.id,
+        platform,
+        data: { workIds },
+      });
+    } catch (err) {
+      logger.error(
+        `[SyncAccountWorks] Failed to enqueue work_day_statistics backfill for account ${account.id}:`,
+        err
+      );
+    }
+  }
+
+  /**
    * 保存作品每日统计数据
    * 保存作品每日统计数据
    */
    */
   private async saveWorkDayStatistics(account: PlatformAccount): Promise<void> {
   private async saveWorkDayStatistics(account: PlatformAccount): Promise<void> {
@@ -737,11 +655,11 @@ export class WorkService {
     }
     }
 
 
     // 先删除关联的评论和作品每日统计
     // 先删除关联的评论和作品每日统计
-    await AppDataSource.getRepository(Comment).delete({ workId });
+    await AppDataSource.getRepository(Comment).delete({ workId, userId });
     await this.workDayStatisticsService.deleteByWorkId(workId);
     await this.workDayStatisticsService.deleteByWorkId(workId);
 
 
     // 删除作品
     // 删除作品
-    await this.workRepository.delete(workId);
+    await this.workRepository.delete({ id: workId, userId });
 
 
     logger.info(`Deleted work ${workId} for user ${userId}`);
     logger.info(`Deleted work ${workId} for user ${userId}`);
   }
   }
@@ -765,7 +683,7 @@ export class WorkService {
     }
     }
 
 
     const account = await this.accountRepository.findOne({
     const account = await this.accountRepository.findOne({
-      where: { id: work.accountId },
+      where: { id: work.accountId, userId },
     });
     });
 
 
     if (!account || !account.cookieData) {
     if (!account || !account.cookieData) {
@@ -789,7 +707,7 @@ export class WorkService {
 
 
       if (result.success) {
       if (result.success) {
         // 更新作品状态为已删除
         // 更新作品状态为已删除
-        await this.workRepository.update(workId, { status: 'deleted' });
+        await this.workRepository.update({ id: workId, userId }, { status: 'deleted' });
         logger.info(`Platform work ${workId} deleted successfully`);
         logger.info(`Platform work ${workId} deleted successfully`);
       }
       }
 
 
@@ -804,7 +722,7 @@ export class WorkService {
 
 
       if (result.success) {
       if (result.success) {
         // 更新作品状态为已删除
         // 更新作品状态为已删除
-        await this.workRepository.update(workId, { status: 'deleted' });
+        await this.workRepository.update({ id: workId, userId }, { status: 'deleted' });
         logger.info(`Platform work ${workId} (xiaohongshu) deleted successfully`);
         logger.info(`Platform work ${workId} (xiaohongshu) deleted successfully`);
       }
       }
 
 
@@ -854,8 +772,11 @@ export class WorkService {
     };
     };
   }
   }
 
 
-  private async dedupeWeixinVideoWorks(accountId: number): Promise<void> {
-    const works = await this.workRepository.find({ where: { accountId } });
+  private async dedupeWeixinVideoWorks(accountId: number, userId: number): Promise<void> {
+    const works = await this.workRepository.find({
+      where: { accountId, userId },
+      select: ['id', 'title', 'publishTime', 'platformVideoId', 'updatedAt'],
+    });
     const groups = new Map<string, Work[]>();
     const groups = new Map<string, Work[]>();
 
 
     for (const w of works) {
     for (const w of works) {
@@ -879,8 +800,8 @@ export class WorkService {
       const keep = list[0];
       const keep = list[0];
       for (const dup of list.slice(1)) {
       for (const dup of list.slice(1)) {
         if (dup.id === keep.id) continue;
         if (dup.id === keep.id) continue;
-        await this.commentRepository.update({ workId: dup.id }, { workId: keep.id });
-        await this.workRepository.delete(dup.id);
+        await this.commentRepository.update({ workId: dup.id, userId }, { workId: keep.id });
+        await this.workRepository.delete({ id: dup.id, userId });
       }
       }
     }
     }
   }
   }

+ 1 - 1
server/src/services/taskExecutors.ts

@@ -203,7 +203,7 @@ async function deleteWorkExecutor(task: Task, updateProgress: ProgressUpdater):
     if (result.accountId) {
     if (result.accountId) {
       updateProgress({ progress: 90, currentStep: '刷新作品列表...' });
       updateProgress({ progress: 90, currentStep: '刷新作品列表...' });
       try {
       try {
-        taskQueueService.createTask(userId, {
+        await taskQueueService.createTask(userId, {
           type: 'sync_works',
           type: 'sync_works',
           title: '刷新作品列表',
           title: '刷新作品列表',
           accountId: result.accountId,
           accountId: result.accountId,

+ 32 - 4
server/src/websocket/index.ts

@@ -31,7 +31,7 @@ class WebSocketManager {
       });
       });
 
 
       ws.on('message', (data) => {
       ws.on('message', (data) => {
-        logger.info(`[WS] Received message: ${data.toString().slice(0, 200)}`);
+        logger.info(`[WS] Received message: ${this.sanitizeIncomingMessage(data.toString())}`);
         this.handleMessage(ws, data.toString());
         this.handleMessage(ws, data.toString());
       });
       });
 
 
@@ -61,6 +61,7 @@ class WebSocketManager {
         ws.ping();
         ws.ping();
       });
       });
     }, 30000);
     }, 30000);
+    this.heartbeatTimer.unref();
 
 
     logger.info('WebSocket server initialized');
     logger.info('WebSocket server initialized');
   }
   }
@@ -111,7 +112,7 @@ class WebSocketManager {
   }
   }
 
 
   private handleAuth(ws: AuthenticatedWebSocket, token?: string): void {
   private handleAuth(ws: AuthenticatedWebSocket, token?: string): void {
-    logger.info(`[WS] Authenticating, token: ${token ? token.slice(0, 20) + '...' : 'none'}`);
+    logger.info(`[WS] Authenticating, token=${token ? 'present' : 'none'}`);
     
     
     if (!token) {
     if (!token) {
       logger.warn('[WS] Auth failed: no token');
       logger.warn('[WS] Auth failed: no token');
@@ -166,8 +167,7 @@ class WebSocketManager {
     const userClients = this.clients.get(userId);
     const userClients = this.clients.get(userId);
     if (userClients) {
     if (userClients) {
       const message = JSON.stringify({ type, payload, timestamp: Date.now() });
       const message = JSON.stringify({ type, payload, timestamp: Date.now() });
-      logger.info(`[WS] Sending to user ${userId}: type=${type}, payload=${JSON.stringify(payload)}`);
-      logger.info(`[WS] Full message: ${message}`);
+      logger.info(`[WS] Sending to user ${userId}: type=${type}, ${this.summarizePayload(payload)}`);
       userClients.forEach((ws) => {
       userClients.forEach((ws) => {
         if (ws.readyState === WebSocket.OPEN) {
         if (ws.readyState === WebSocket.OPEN) {
           ws.send(message);
           ws.send(message);
@@ -192,6 +192,33 @@ class WebSocketManager {
     });
     });
   }
   }
 
 
+  private sanitizeIncomingMessage(raw: string): string {
+    try {
+      const message = JSON.parse(raw) as { type?: string; payload?: Record<string, unknown> };
+      const payload = message.payload && typeof message.payload === 'object'
+        ? { ...message.payload }
+        : message.payload;
+
+      if (payload && typeof payload === 'object') {
+        if ('token' in payload) payload.token = '[redacted]';
+        if ('code' in payload) payload.code = '[redacted]';
+      }
+
+      return JSON.stringify({ type: message.type, payload }).slice(0, 200);
+    } catch {
+      return `[unparseable message, length=${raw.length}]`;
+    }
+  }
+
+  private summarizePayload(payload?: unknown): string {
+    if (!payload || typeof payload !== 'object') {
+      return `payloadType=${typeof payload}`;
+    }
+
+    const keys = Object.keys(payload as Record<string, unknown>);
+    return `payloadKeys=${keys.join(',') || 'none'}`;
+  }
+
   /**
   /**
    * 获取在线用户数
    * 获取在线用户数
    */
    */
@@ -221,6 +248,7 @@ class WebSocketManager {
       this.captchaListeners.delete(captchaTaskId);
       this.captchaListeners.delete(captchaTaskId);
       logger.info(`[WS] Captcha listener timed out for ${captchaTaskId}`);
       logger.info(`[WS] Captcha listener timed out for ${captchaTaskId}`);
     }, 5 * 60 * 1000);
     }, 5 * 60 * 1000);
+    timer.unref();
     this.captchaListeners.set(captchaTaskId, { callback, timer });
     this.captchaListeners.set(captchaTaskId, { callback, timer });
     logger.info(`[WS] Registered captcha listener for ${captchaTaskId}`);
     logger.info(`[WS] Registered captcha listener for ${captchaTaskId}`);
   }
   }

+ 2 - 1
shared/src/types/publish.ts

@@ -79,7 +79,8 @@ export interface PublishResult {
  * 创建发布任务请求
  * 创建发布任务请求
  */
  */
 export interface CreatePublishTaskRequest {
 export interface CreatePublishTaskRequest {
-  videoPath: string;
+  videoPath?: string;
+  videoFilename?: string;
   title: string;
   title: string;
   description?: string;
   description?: string;
   coverPath?: string;
   coverPath?: string;